From a79f5140b58a6cd36a8db8bebdb4b5107168822c Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 24 Feb 2021 17:23:01 +0800 Subject: [PATCH 01/11] feat: profile timeline scroll performance --- ...adMoreConfigurableTableViewContainer.swift | 7 ++++++ .../HomeTimelineViewController.swift | 22 +++++++------------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Mastodon/Protocol/LoadMoreConfigurableTableViewContainer.swift b/Mastodon/Protocol/LoadMoreConfigurableTableViewContainer.swift index bc90ab1ed..4f32be54a 100644 --- a/Mastodon/Protocol/LoadMoreConfigurableTableViewContainer.swift +++ b/Mastodon/Protocol/LoadMoreConfigurableTableViewContainer.swift @@ -23,6 +23,13 @@ extension LoadMoreConfigurableTableViewContainer { func handleScrollViewDidScroll(_ scrollView: UIScrollView) { guard scrollView === loadMoreConfigurableTableView else { return } + // check if current scroll position is the bottom of table + let contentOffsetY = loadMoreConfigurableTableView.contentOffset.y + let bottomVisiblePageContentOffsetY = loadMoreConfigurableTableView.contentSize.height - (1.5 * loadMoreConfigurableTableView.visibleSize.height) + guard contentOffsetY > bottomVisiblePageContentOffsetY else { + return + } + let cells = loadMoreConfigurableTableView.visibleCells.compactMap { $0 as? BottomLoaderTableViewCell } guard let loaderTableViewCell = cells.first else { return } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 153f46130..906708fac 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -175,23 +175,17 @@ extension HomeTimelineViewController { // MARK: - UIScrollViewDelegate extension HomeTimelineViewController { func scrollViewDidScroll(_ scrollView: UIScrollView) { - guard scrollView === tableView else { return } - let cells = tableView.visibleCells.compactMap { $0 as? TimelineBottomLoaderTableViewCell } - guard let loaderTableViewCell = cells.first else { return } - - if let tabBar = tabBarController?.tabBar, let window = view.window { - let loaderTableViewCellFrameInWindow = tableView.convert(loaderTableViewCell.frame, to: nil) - let windowHeight = window.frame.height - let loaderAppear = (loaderTableViewCellFrameInWindow.origin.y + 0.8 * loaderTableViewCell.frame.height) < (windowHeight - tabBar.frame.height) - if loaderAppear { - viewModel.loadoldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self) - } - } else { - viewModel.loadoldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self) - } + handleScrollViewDidScroll(scrollView) } } +extension HomeTimelineViewController: LoadMoreConfigurableTableViewContainer { + typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell + typealias LoadingState = HomeTimelineViewModel.LoadOldestState.Loading + var loadMoreConfigurableTableView: UITableView { return tableView } + var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine } +} + // MARK: - UITableViewDelegate extension HomeTimelineViewController: UITableViewDelegate { From 5bc8adfd431780b67e6dd2a26c4af183703a1bdc Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 24 Feb 2021 17:23:30 +0800 Subject: [PATCH 02/11] fix: re-entry crash issue --- Mastodon/Scene/Welcome/WelcomeViewController.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Welcome/WelcomeViewController.swift b/Mastodon/Scene/Welcome/WelcomeViewController.swift index 99aa89f92..3c95fa4f4 100644 --- a/Mastodon/Scene/Welcome/WelcomeViewController.swift +++ b/Mastodon/Scene/Welcome/WelcomeViewController.swift @@ -14,7 +14,12 @@ final class WelcomeViewController: UIViewController, NeedsDependency { weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } #if DEBUG - let authenticationViewController = AuthenticationViewController() + lazy var authenticationViewController: AuthenticationViewController = { + let authenticationViewController = AuthenticationViewController() + authenticationViewController.context = context + authenticationViewController.coordinator = coordinator + return authenticationViewController + }() #endif let logoImageView: UIImageView = { @@ -105,8 +110,6 @@ extension WelcomeViewController { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) #if DEBUG - authenticationViewController.context = context - authenticationViewController.coordinator = coordinator authenticationViewController.viewModel = AuthenticationViewModel(context: context, coordinator: coordinator, isAuthenticationExist: true) authenticationViewController.viewModel.domain.value = "pawoo.net" let _ = authenticationViewController.view // trigger view load From 1345db9d7ebaf666bbcc2ad600977252ef2880e1 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 24 Feb 2021 18:07:00 +0800 Subject: [PATCH 03/11] fix: status content warning UI for Dark Mode related issue --- Mastodon/Scene/Share/View/Content/StatusView.swift | 11 ++++++++++- .../View/TableviewCell/StatusTableViewCell.swift | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index fc502597e..1bedb945e 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -103,7 +103,7 @@ final class StatusView: UIView { // do not use visual effect view due to we blur text only without background let contentWarningBlurContentImageView: UIImageView = { let imageView = UIImageView() - imageView.backgroundColor = .secondarySystemGroupedBackground + imageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color imageView.layer.masksToBounds = false return imageView }() @@ -126,6 +126,15 @@ final class StatusView: UIView { super.init(coder: coder) _init() } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + // update blur image when interface style changed + if previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle { + drawContentWarningImageView() + } + } } diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index 3c968f795..ed1d083cc 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -50,6 +50,7 @@ extension StatusTableViewCell { private func _init() { selectionStyle = .none backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color + statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color statusView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(statusView) From ca013f79dabbc91c9895b1e10a3727f8d9e7cb4b Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 24 Feb 2021 18:07:11 +0800 Subject: [PATCH 04/11] feat: add public timeline debug entry --- Mastodon/Coordinator/SceneCoordinator.swift | 10 ++++++++++ .../HomeTimelineViewController+DebugAction.swift | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index c48c60d79..409215b03 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -45,6 +45,10 @@ extension SceneCoordinator { case mastodonServerRules(viewModel: MastodonServerRulesViewModel) case alertController(alertController: UIAlertController) + + #if DEBUG + case publicTimeline + #endif } } @@ -161,6 +165,12 @@ private extension SceneCoordinator { ) } viewController = alertController + #if DEBUG + case .publicTimeline: + let _viewController = PublicTimelineViewController() + _viewController.viewModel = PublicTimelineViewModel(context: appContext) + viewController = _viewController + #endif } setupDependency(for: viewController as? NeedsDependency) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index 84c0885d8..db6ddfa67 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -17,6 +17,10 @@ extension HomeTimelineViewController { identifier: nil, options: .displayInline, children: [ + UIAction(title: "Show Public Timeline", image: UIImage(systemName: "list.dash"), attributes: []) { [weak self] action in + guard let self = self else { return } + self.showPublicTimelineAction(action) + }, UIAction(title: "Sign Out", image: UIImage(systemName: "escape"), attributes: .destructive) { [weak self] action in guard let self = self else { return } self.signOutAction(action) @@ -29,6 +33,10 @@ extension HomeTimelineViewController { extension HomeTimelineViewController { + @objc private func showPublicTimelineAction(_ sender: UIAction) { + coordinator.present(scene: .publicTimeline, from: self, transition: .show) + } + @objc private func signOutAction(_ sender: UIAction) { guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return From 618239069969ee905d53c8792c853b757769cf30 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 24 Feb 2021 18:35:55 +0800 Subject: [PATCH 05/11] fix: expiresAt attribute of Poll entity is not mark optional issue --- .../Sources/MastodonSDK/Entity/Mastodon+Entity+Poll.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Poll.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Poll.swift index 5a672e014..70b86e616 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Poll.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Poll.swift @@ -13,14 +13,14 @@ extension Mastodon.Entity { /// - Since: 2.8.0 /// - Version: 3.3.0 /// # Last Update - /// 2021/2/4 + /// 2021/2/24 /// # Reference /// [Document](https://docs.joinmastodon.org/entities/poll/) public struct Poll: Codable { public typealias ID = String public let id: ID - public let expiresAt: Date + public let expiresAt: Date? // if nil the poll does not end public let expired: Bool public let multiple: Bool public let votesCount: Int From 2988e71611dcdfe14d7292f73d9a798e2f942717 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 24 Feb 2021 18:40:47 +0800 Subject: [PATCH 06/11] chore: update UI appearance. remove not used separator line component --- Mastodon/Diffiable/Item/Item.swift | 12 +----------- Mastodon/Extension/UIView.swift | 4 ---- .../StatusProvider/StatusProviderFacade.swift | 2 +- .../HomeTimelineViewModel+Diffable.swift | 8 ++------ .../View/TableviewCell/StatusTableViewCell.swift | 11 ++++++++++- .../TimelineLoaderTableViewCell.swift | 14 ++++++++------ .../TimelineMiddleLoaderTableViewCell.swift | 10 ---------- .../Service/APIService/APIService+Favorite.swift | 2 +- 8 files changed, 23 insertions(+), 40 deletions(-) diff --git a/Mastodon/Diffiable/Item/Item.swift b/Mastodon/Diffiable/Item/Item.swift index 2b34753b3..2ab198226 100644 --- a/Mastodon/Diffiable/Item/Item.swift +++ b/Mastodon/Diffiable/Item/Item.swift @@ -30,32 +30,22 @@ protocol StatusContentWarningAttribute { extension Item { class StatusTimelineAttribute: Hashable, StatusContentWarningAttribute { - var separatorLineStyle: SeparatorLineStyle = .indent var isStatusTextSensitive: Bool = false public init( - separatorLineStyle: Item.StatusTimelineAttribute.SeparatorLineStyle = .indent, isStatusTextSensitive: Bool ) { - self.separatorLineStyle = separatorLineStyle self.isStatusTextSensitive = isStatusTextSensitive } static func == (lhs: Item.StatusTimelineAttribute, rhs: Item.StatusTimelineAttribute) -> Bool { - return lhs.separatorLineStyle == rhs.separatorLineStyle && - lhs.isStatusTextSensitive == rhs.isStatusTextSensitive + return lhs.isStatusTextSensitive == rhs.isStatusTextSensitive } func hash(into hasher: inout Hasher) { - hasher.combine(separatorLineStyle) hasher.combine(isStatusTextSensitive) } - enum SeparatorLineStyle { - case indent // alignment to name label - case expand // alignment to table view two edges - case normal // alignment to readable guideline - } } } diff --git a/Mastodon/Extension/UIView.swift b/Mastodon/Extension/UIView.swift index 7e1ba379e..d9e3af5b5 100644 --- a/Mastodon/Extension/UIView.swift +++ b/Mastodon/Extension/UIView.swift @@ -20,10 +20,6 @@ extension UIView { return 1.0 / view.traitCollection.displayScale } - static var floatyButtonBottomMargin: CGFloat { - return 16 - } - } // MARK: - Convinience view appearance modification method diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift index eaf202c01..894461566 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift @@ -85,7 +85,7 @@ extension StatusProviderFacade { os_log("%{public}s[%{public}ld], %{public}s: [Like] update local toot like status to: %s", ((#file as NSString).lastPathComponent), #line, #function, favoriteKind == .create ? "like" : "unlike") } receiveCompletion: { completion in switch completion { - case .failure(let error): + case .failure: // TODO: handle error break case .finished: diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index 0091f06bf..8f74b81a4 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -90,16 +90,12 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate { let isLast = i == timelineIndexes.count - 1 switch (isLast, timelineIndex.hasMore) { - case (true, false): - attribute.separatorLineStyle = .normal case (false, true): - attribute.separatorLineStyle = .expand newTimelineItems.append(.homeMiddleLoader(upperTimelineIndexAnchorObjectID: timelineIndex.objectID)) case (true, true): - attribute.separatorLineStyle = .normal shouldAddBottomLoader = true - case (false, false): - attribute.separatorLineStyle = .indent + default: + break } } // end for diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index ed1d083cc..758beed0c 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -18,6 +18,7 @@ protocol StatusTableViewCellDelegate: class { final class StatusTableViewCell: UITableViewCell { + static let bottomPaddingHeight: CGFloat = 10 weak var delegate: StatusTableViewCellDelegate? @@ -28,6 +29,7 @@ final class StatusTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() + statusView.isStatusTextSensitive = false statusView.cleanUpContentWarning() disposeBag.removeAll() observations.removeAll() @@ -43,6 +45,13 @@ final class StatusTableViewCell: UITableViewCell { _init() } + override func layoutSubviews() { + super.layoutSubviews() + DispatchQueue.main.async { + self.statusView.drawContentWarningImageView() + } + } + } extension StatusTableViewCell { @@ -68,7 +77,7 @@ extension StatusTableViewCell { bottomPaddingView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), bottomPaddingView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), bottomPaddingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - bottomPaddingView.heightAnchor.constraint(equalToConstant: 10).priority(.defaultHigh), + bottomPaddingView.heightAnchor.constraint(equalToConstant: StatusTableViewCell.bottomPaddingHeight).priority(.defaultHigh), ]) statusView.delegate = self diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift index 676f44ff2..f9508894a 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift @@ -10,7 +10,9 @@ import Combine class TimelineLoaderTableViewCell: UITableViewCell { - static let cellHeight: CGFloat = 48 + static let cellHeight: CGFloat = 44 + TimelineLoaderTableViewCell.extraTopPadding + TimelineLoaderTableViewCell.bottomPadding + static let extraTopPadding: CGFloat = 3 // the status cell already has 10pt bottom padding + static let bottomPadding: CGFloat = StatusTableViewCell.bottomPaddingHeight + TimelineLoaderTableViewCell.extraTopPadding // make balance var disposeBag = Set() @@ -50,18 +52,18 @@ class TimelineLoaderTableViewCell: UITableViewCell { loadMoreButton.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(loadMoreButton) NSLayoutConstraint.activate([ - loadMoreButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + loadMoreButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: TimelineLoaderTableViewCell.extraTopPadding), loadMoreButton.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), contentView.readableContentGuide.trailingAnchor.constraint(equalTo: loadMoreButton.trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: loadMoreButton.bottomAnchor, constant: 8), - loadMoreButton.heightAnchor.constraint(equalToConstant: TimelineLoaderTableViewCell.cellHeight - 2 * 8).priority(.defaultHigh), + contentView.bottomAnchor.constraint(equalTo: loadMoreButton.bottomAnchor, constant: TimelineLoaderTableViewCell.bottomPadding), + loadMoreButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh), ]) activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false addSubview(activityIndicatorView) NSLayoutConstraint.activate([ - activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor), - activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor), + activityIndicatorView.centerXAnchor.constraint(equalTo: loadMoreButton.centerXAnchor), + activityIndicatorView.centerYAnchor.constraint(equalTo: loadMoreButton.centerYAnchor), ]) loadMoreButton.isHidden = true diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift index d768865d6..6c692b646 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift @@ -23,16 +23,6 @@ final class TimelineMiddleLoaderTableViewCell: TimelineLoaderTableViewCell { backgroundColor = .clear - let separatorLine = UIView.separatorLine - separatorLine.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(separatorLine) - NSLayoutConstraint.activate([ - separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: separatorLine.trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: separatorLine.bottomAnchor), - separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: separatorLine)) - ]) - loadMoreButton.isHidden = false loadMoreButton.setImage(Asset.Arrows.arrowTriangle2Circlepath.image.withRenderingMode(.alwaysTemplate), for: .normal) loadMoreButton.setInsets(forContentPadding: .zero, imageTitlePadding: 4) diff --git a/Mastodon/Service/APIService/APIService+Favorite.swift b/Mastodon/Service/APIService/APIService+Favorite.swift index 8dd4839ac..34bd3f0e4 100644 --- a/Mastodon/Service/APIService/APIService+Favorite.swift +++ b/Mastodon/Service/APIService/APIService+Favorite.swift @@ -139,7 +139,7 @@ extension APIService { return APIService.Persist.persistTimeline( managedObjectContext: self.backgroundManagedObjectContext, domain: mastodonAuthenticationBox.domain, - query: query as! TimelineQueryType, + query: query, response: response, persistType: .likeList, requestMastodonUserID: requestMastodonUserID, From 10594d1eb67153e775102877b8da7912ec75e61a Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 24 Feb 2021 18:41:40 +0800 Subject: [PATCH 07/11] feat: store view state to prevent redundant image rending --- Mastodon/Diffiable/Section/StatusSection.swift | 1 + .../HomeTimeline/HomeTimelineViewController.swift | 11 ++--------- Mastodon/Scene/Share/View/Content/StatusView.swift | 5 ++++- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 4cc19bdc3..64b49ad2b 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -96,6 +96,7 @@ extension StatusSection { // set content warning let isStatusTextSensitive = statusContentWarningAttribute?.isStatusTextSensitive ?? (toot.reblog ?? toot).sensitive + cell.statusView.isStatusTextSensitive = isStatusTextSensitive cell.statusView.updateContentWarningDisplay(isHidden: !isStatusTextSensitive) cell.statusView.contentWarningTitle.text = (toot.reblog ?? toot).spoilerText.flatMap { spoilerText in guard !spoilerText.isEmpty else { return nil } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 906708fac..125ce8545 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -200,14 +200,7 @@ extension HomeTimelineViewController: UITableViewDelegate { return ceil(frame.height) } - - func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - if let cell = cell as? StatusTableViewCell { - DispatchQueue.main.async { - cell.statusView.drawContentWarningImageView() - } - } - } + } // MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate @@ -227,7 +220,7 @@ extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate viewModel.loadMiddleSateMachineList .receive(on: DispatchQueue.main) .sink { [weak self] ids in - guard let self = self else { return } + guard let _ = self else { return } if let stateMachine = ids[upperTimelineIndexObjectID] { guard let state = stateMachine.currentState else { assertionFailure() diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 1bedb945e..d52def46c 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -22,6 +22,7 @@ final class StatusView: UIView { static let contentWarningBlurRadius: CGFloat = 12 weak var delegate: StatusViewDelegate? + var isStatusTextSensitive = false let headerContainerStackView = UIStackView() @@ -281,7 +282,9 @@ extension StatusView { } func drawContentWarningImageView() { - guard activeTextLabel.frame != .zero, let text = activeTextLabel.text, !text.isEmpty else { + guard activeTextLabel.frame != .zero, + isStatusTextSensitive, + let text = activeTextLabel.text, !text.isEmpty else { cleanUpContentWarning() return } From b2706a50da2e8fd786dc3e1d28d271878e38753b Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 24 Feb 2021 19:08:30 +0800 Subject: [PATCH 08/11] chore: update sign up alert. Update gitignore for i18n files --- .gitignore | 4 +- .../StringsConvertor/input/en_US/app.json | 83 ------------------- .../input/en_US/ios-infoPlist.json | 4 - .../output/en.lproj/Localizable.strings | 40 --------- .../output/en.lproj/infoPlist.strings | 2 - Localization/app.json | 10 ++- Mastodon/Extension/UIAlertController.swift | 26 ++++-- Mastodon/Generated/Strings.swift | 10 +++ .../Resources/en.lproj/Localizable.strings | 2 + .../AuthenticationViewController.swift | 2 +- .../MastodonRegisterViewController.swift | 2 +- .../Scene/MainTab/MainTabBarController.swift | 2 +- .../TimelineBottomLoaderTableViewCell.swift | 17 ++++ .../TimelineMiddleLoaderTableViewCell.swift | 17 ++++ 14 files changed, 78 insertions(+), 143 deletions(-) delete mode 100644 Localization/StringsConvertor/input/en_US/app.json delete mode 100644 Localization/StringsConvertor/input/en_US/ios-infoPlist.json delete mode 100644 Localization/StringsConvertor/output/en.lproj/Localizable.strings delete mode 100644 Localization/StringsConvertor/output/en.lproj/infoPlist.strings diff --git a/.gitignore b/.gitignore index 13d57e655..51ba722ba 100644 --- a/.gitignore +++ b/.gitignore @@ -118,4 +118,6 @@ xcuserdata **/xcshareddata/WorkspaceSettings.xcsettings # End of https://www.toptal.com/developers/gitignore/api/swift,swiftpm,xcode,cocoapods -n \ No newline at end of file + +Mastodon/Localization/StringsConvertor/input +Mastodon/Localization/StringsConvertor/output \ No newline at end of file diff --git a/Localization/StringsConvertor/input/en_US/app.json b/Localization/StringsConvertor/input/en_US/app.json deleted file mode 100644 index 8fa054563..000000000 --- a/Localization/StringsConvertor/input/en_US/app.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "common": { - "alerts": {}, - "controls": { - "actions": { - "add": "Add", - "remove": "Remove", - "edit": "Edit", - "save": "Save", - "ok": "OK", - "confirm": "Confirm", - "continue": "Continue", - "cancel": "Cancel", - "take_photo": "Take photo", - "save_photo": "Save photo", - "sign_in": "Sign in", - "sign_up": "Sign up", - "see_more": "See More", - "preview": "Preview", - "open_in_safari": "Open in Safari" - }, - "status": { - "user_boosted": "%s boosted", - "content_warning": "content warning", - "show_post": "Show Post" - }, - "timeline": { - "load_more": "Load More" - } - }, - "countable": { - "photo": { - "single": "photo", - "multiple": "photos" - } - } - }, - "scene": { - "welcome": { - "slogan": "Social networking\nback in your hands." - }, - "server_picker": { - "title": "Pick a Server,\nany server.", - "input": { - "placeholder": "Find a server or join your own..." - } - }, - "register": { - "title": "Tell us about you.", - "input": { - "username": { - "placeholder": "username", - "duplicate_prompt": "This username is taken." - }, - "display_name": { - "placeholder": "display name" - }, - "email": { - "placeholder": "email" - }, - "password": { - "placeholder": "password", - "prompt": "Your password needs at least:", - "prompt_eight_characters": "Eight characters" - } - } - }, - "server_rules": { - "title": "Some ground rules.", - "subtitle": "These rules are set by the admins of %s.", - "prompt": "By continuing, you're subject to the terms of service and privacy policy for %s.", - "button": { - "confirm": "I Agree" - } - }, - "home_timeline": { - "title": "Home" - }, - "public_timeline": { - "title": "Public" - } - } -} diff --git a/Localization/StringsConvertor/input/en_US/ios-infoPlist.json b/Localization/StringsConvertor/input/en_US/ios-infoPlist.json deleted file mode 100644 index 0a260c273..000000000 --- a/Localization/StringsConvertor/input/en_US/ios-infoPlist.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "NSCameraUsageDescription": "Used to take photo for toot", - "NSPhotoLibraryAddUsageDescription": "Used to save photo into the Photo Library" -} diff --git a/Localization/StringsConvertor/output/en.lproj/Localizable.strings b/Localization/StringsConvertor/output/en.lproj/Localizable.strings deleted file mode 100644 index 75dc3999b..000000000 --- a/Localization/StringsConvertor/output/en.lproj/Localizable.strings +++ /dev/null @@ -1,40 +0,0 @@ -"Common.Controls.Actions.Add" = "Add"; -"Common.Controls.Actions.Cancel" = "Cancel"; -"Common.Controls.Actions.Confirm" = "Confirm"; -"Common.Controls.Actions.Continue" = "Continue"; -"Common.Controls.Actions.Edit" = "Edit"; -"Common.Controls.Actions.Ok" = "OK"; -"Common.Controls.Actions.OpenInSafari" = "Open in Safari"; -"Common.Controls.Actions.Preview" = "Preview"; -"Common.Controls.Actions.Remove" = "Remove"; -"Common.Controls.Actions.Save" = "Save"; -"Common.Controls.Actions.SavePhoto" = "Save photo"; -"Common.Controls.Actions.SeeMore" = "See More"; -"Common.Controls.Actions.SignIn" = "Sign in"; -"Common.Controls.Actions.SignUp" = "Sign up"; -"Common.Controls.Actions.TakePhoto" = "Take photo"; -"Common.Controls.Status.ContentWarning" = "content warning"; -"Common.Controls.Status.ShowPost" = "Show Post"; -"Common.Controls.Status.UserBoosted" = "%@ boosted"; -"Common.Controls.Timeline.LoadMore" = "Load More"; -"Common.Countable.Photo.Multiple" = "photos"; -"Common.Countable.Photo.Single" = "photo"; -"Scene.HomeTimeline.Title" = "Home"; -"Scene.PublicTimeline.Title" = "Public"; -"Scene.Register.Input.DisplayName.Placeholder" = "display name"; -"Scene.Register.Input.Email.Placeholder" = "email"; -"Scene.Register.Input.Password.Placeholder" = "password"; -"Scene.Register.Input.Password.Prompt" = "Your password needs at least:"; -"Scene.Register.Input.Password.PromptEightCharacters" = "Eight characters"; -"Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken."; -"Scene.Register.Input.Username.Placeholder" = "username"; -"Scene.Register.Title" = "Tell us about you."; -"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own..."; -"Scene.ServerPicker.Title" = "Pick a Server, -any server."; -"Scene.ServerRules.Button.Confirm" = "I Agree"; -"Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@."; -"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@."; -"Scene.ServerRules.Title" = "Some ground rules."; -"Scene.Welcome.Slogan" = "Social networking -back in your hands."; \ No newline at end of file diff --git a/Localization/StringsConvertor/output/en.lproj/infoPlist.strings b/Localization/StringsConvertor/output/en.lproj/infoPlist.strings deleted file mode 100644 index 972e1a7a2..000000000 --- a/Localization/StringsConvertor/output/en.lproj/infoPlist.strings +++ /dev/null @@ -1,2 +0,0 @@ -"NSCameraUsageDescription" = "Used to take photo for toot"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; \ No newline at end of file diff --git a/Localization/app.json b/Localization/app.json index 8fa054563..56a54a7f4 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -1,6 +1,14 @@ { "common": { - "alerts": {}, + "alerts": { + "sign_up_failure": { + "title": "Sign Up Failure" + }, + "server_error": { + "title": "Server Error" + } + + }, "controls": { "actions": { "add": "Add", diff --git a/Mastodon/Extension/UIAlertController.swift b/Mastodon/Extension/UIAlertController.swift index 7abe20cbd..83c0ff555 100644 --- a/Mastodon/Extension/UIAlertController.swift +++ b/Mastodon/Extension/UIAlertController.swift @@ -9,26 +9,34 @@ import UIKit // https://nshipster.com/swift-foundation-error-protocols/ extension UIAlertController { convenience init( - _ error: Error, + for error: Error, + title: String?, preferredStyle: UIAlertController.Style ) { - let title: String + let _title: String let message: String? if let error = error as? LocalizedError { - title = error.errorDescription ?? "Unknown Error" - message = [ + var messages: [String?] = [] + if let title = title { + _title = title + messages.append(error.errorDescription) + } else { + _title = error.errorDescription ?? "Error" + } + messages.append(contentsOf: [ error.failureReason, error.recoverySuggestion - ] - .compactMap { $0 } - .joined(separator: " ") + ]) + message = messages + .compactMap { $0 } + .joined(separator: " ") } else { - title = "Internal Error" + _title = "Internal Error" message = error.localizedDescription } self.init( - title: title, + title: _title, message: message, preferredStyle: preferredStyle ) diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 7b9d9eca5..fa641fbbb 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -12,6 +12,16 @@ import Foundation internal enum L10n { internal enum Common { + internal enum Alerts { + internal enum ServerError { + /// Server Error + internal static let title = L10n.tr("Localizable", "Common.Alerts.ServerError.Title") + } + internal enum SignUpFailure { + /// Sign Up Failure + internal static let title = L10n.tr("Localizable", "Common.Alerts.SignUpFailure.Title") + } + } internal enum Controls { internal enum Actions { /// Add diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 75dc3999b..efb3fed70 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -1,3 +1,5 @@ +"Common.Alerts.ServerError.Title" = "Server Error"; +"Common.Alerts.SignUpFailure.Title" = "Sign Up Failure"; "Common.Controls.Actions.Add" = "Add"; "Common.Controls.Actions.Cancel" = "Cancel"; "Common.Controls.Actions.Confirm" = "Confirm"; diff --git a/Mastodon/Scene/Authentication/AuthenticationViewController.swift b/Mastodon/Scene/Authentication/AuthenticationViewController.swift index 5ab500bc2..a4a96dd59 100644 --- a/Mastodon/Scene/Authentication/AuthenticationViewController.swift +++ b/Mastodon/Scene/Authentication/AuthenticationViewController.swift @@ -195,7 +195,7 @@ extension AuthenticationViewController { .receive(on: DispatchQueue.main) .sink { [weak self] error in guard let self = self else { return } - let alertController = UIAlertController(error, preferredStyle: .alert) + let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert) let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) alertController.addAction(okAction) self.coordinator.present( diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift index 1ebb17bd5..1ff30bf66 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift @@ -398,7 +398,7 @@ extension MastodonRegisterViewController { .receive(on: DispatchQueue.main) .sink { [weak self] error in guard let self = self else { return } - let alertController = UIAlertController(error, preferredStyle: .alert) + let alertController = UIAlertController(for: error, title: "Sign Up Failure", preferredStyle: .alert) let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) alertController.addAction(okAction) self.coordinator.present( diff --git a/Mastodon/Scene/MainTab/MainTabBarController.swift b/Mastodon/Scene/MainTab/MainTabBarController.swift index c946ffefe..a556854e5 100644 --- a/Mastodon/Scene/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/MainTab/MainTabBarController.swift @@ -112,7 +112,7 @@ extension MainTabBarController { case .implicit: break case .explicit: - let alertController = UIAlertController(error, preferredStyle: .alert) + let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert) let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) alertController.addAction(okAction) coordinator.present( diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift index 2dfbd6257..7fe4c0a77 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift @@ -17,3 +17,20 @@ final class TimelineBottomLoaderTableViewCell: TimelineLoaderTableViewCell { activityIndicatorView.startAnimating() } } + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct TimelineBottomLoaderTableViewCell_Previews: PreviewProvider { + + static var previews: some View { + UIViewPreview(width: 375) { + TimelineBottomLoaderTableViewCell() + } + .previewLayout(.fixed(width: 375, height: 100)) + } + +} + +#endif + diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift index 6c692b646..16ab241f0 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift @@ -36,3 +36,20 @@ extension TimelineMiddleLoaderTableViewCell { delegate?.timelineMiddleLoaderTableViewCell(self, loadMoreButtonDidPressed: sender) } } + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct TimelineMiddleLoaderTableViewCell_Previews: PreviewProvider { + + static var previews: some View { + UIViewPreview(width: 375) { + TimelineMiddleLoaderTableViewCell() + } + .previewLayout(.fixed(width: 375, height: 100)) + } + +} + +#endif + From 0ad43fb0ebf1f794ddd01cfa3225eb79803e62bc Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 24 Feb 2021 19:19:16 +0800 Subject: [PATCH 09/11] chore: update Xcode Preview for StatusView. Rename MosaicImageView to MosaicImageViewContainer --- Mastodon.xcodeproj/project.pbxproj | 8 ++--- .../Diffiable/Section/StatusSection.swift | 6 ++-- ...w.swift => MosaicImageViewContainer.swift} | 34 +++++++++---------- .../Scene/Share/View/Content/StatusView.swift | 21 +++++++++--- 4 files changed, 40 insertions(+), 29 deletions(-) rename Mastodon/Scene/Share/View/Container/{MosaicImageView.swift => MosaicImageViewContainer.swift} (90%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 821dc8a60..37a4a52af 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -138,7 +138,7 @@ DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; }; DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; }; DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */; }; - DB9D6C0E25E4F9780051B173 /* MosaicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C0D25E4F9780051B173 /* MosaicImageView.swift */; }; + DB9D6C0E25E4F9780051B173 /* MosaicImageViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */; }; DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */; }; DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2D25E504AC0051B173 /* Attachment.swift */; }; DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C3725E508BE0051B173 /* Attachment.swift */; }; @@ -340,7 +340,7 @@ DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; - DB9D6C0D25E4F9780051B173 /* MosaicImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageView.swift; sourceTree = ""; }; + DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageViewContainer.swift; sourceTree = ""; }; DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageViewModel.swift; sourceTree = ""; }; DB9D6C2D25E504AC0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; DB9D6C3725E508BE0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; @@ -953,7 +953,7 @@ DB9D6C1325E4F97A0051B173 /* Container */ = { isa = PBXGroup; children = ( - DB9D6C0D25E4F9780051B173 /* MosaicImageView.swift */, + DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */, ); path = Container; sourceTree = ""; @@ -1388,7 +1388,7 @@ DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */, DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */, - DB9D6C0E25E4F9780051B173 /* MosaicImageView.swift in Sources */, + DB9D6C0E25E4F9780051B173 /* MosaicImageViewContainer.swift in Sources */, DB98338725C945ED00AD9700 /* Strings.swift in Sources */, DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */, DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */, diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 64b49ad2b..727f4074f 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -128,14 +128,14 @@ extension StatusSection { }() if mosiacImageViewModel.metas.count == 1 { let meta = mosiacImageViewModel.metas[0] - let imageView = cell.statusView.mosaicImageView.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize) + let imageView = cell.statusView.statusMosaicImageView.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize) imageView.af.setImage( withURL: meta.url, placeholderImage: UIImage.placeholder(color: .systemFill), imageTransition: .crossDissolve(0.2) ) } else { - let imageViews = cell.statusView.mosaicImageView.setupImageViews(count: mosiacImageViewModel.metas.count, maxHeight: imageViewMaxSize.height) + let imageViews = cell.statusView.statusMosaicImageView.setupImageViews(count: mosiacImageViewModel.metas.count, maxHeight: imageViewMaxSize.height) for (i, imageView) in imageViews.enumerated() { let meta = mosiacImageViewModel.metas[i] imageView.af.setImage( @@ -145,7 +145,7 @@ extension StatusSection { ) } } - cell.statusView.mosaicImageView.isHidden = mosiacImageViewModel.metas.isEmpty + cell.statusView.statusMosaicImageView.isHidden = mosiacImageViewModel.metas.isEmpty // toolbar let replyCountTitle: String = { diff --git a/Mastodon/Scene/Share/View/Container/MosaicImageView.swift b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift similarity index 90% rename from Mastodon/Scene/Share/View/Container/MosaicImageView.swift rename to Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift index 5f8c877db..14e2012e0 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicImageView.swift +++ b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift @@ -1,5 +1,5 @@ // -// MosaicImageView.swift +// MosaicImageViewContainer.swift // Mastodon // // Created by Cirno MainasuK on 2021-2-23. @@ -9,15 +9,15 @@ import os.log import func AVFoundation.AVMakeRect import UIKit -protocol MosaicImageViewPresentable: class { - var mosaicImageView: MosaicImageView { get } +protocol MosaicImageViewContainerPresentable: class { + var mosaicImageViewContainer: MosaicImageViewContainer { get } } protocol MosaicImageViewDelegate: class { - func mosaicImageView(_ mosaicImageView: MosaicImageView, didTapImageView imageView: UIImageView, atIndex index: Int) + func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) } -final class MosaicImageView: UIView { +final class MosaicImageViewContainer: UIView { static let cornerRadius: CGFloat = 4 @@ -29,7 +29,7 @@ final class MosaicImageView: UIView { imageViews.forEach { imageView in imageView.isUserInteractionEnabled = true let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer - tapGesture.addTarget(self, action: #selector(MosaicImageView.photoTapGestureRecognizerHandler(_:))) + tapGesture.addTarget(self, action: #selector(MosaicImageViewContainer.photoTapGestureRecognizerHandler(_:))) imageView.addGestureRecognizer(tapGesture) } } @@ -49,7 +49,7 @@ final class MosaicImageView: UIView { } -extension MosaicImageView { +extension MosaicImageViewContainer { private func _init() { container.translatesAutoresizingMaskIntoConstraints = false @@ -69,7 +69,7 @@ extension MosaicImageView { } -extension MosaicImageView { +extension MosaicImageViewContainer { func reset() { container.arrangedSubviews.forEach { subview in @@ -99,7 +99,7 @@ extension MosaicImageView { let imageView = UIImageView() imageViews.append(imageView) imageView.layer.masksToBounds = true - imageView.layer.cornerRadius = MosaicImageView.cornerRadius + imageView.layer.cornerRadius = MosaicImageViewContainer.cornerRadius imageView.contentMode = .scaleAspectFill imageView.translatesAutoresizingMaskIntoConstraints = false @@ -142,7 +142,7 @@ extension MosaicImageView { self.imageViews.append(contentsOf: imageViews) imageViews.forEach { imageView in imageView.layer.masksToBounds = true - imageView.layer.cornerRadius = MosaicImageView.cornerRadius + imageView.layer.cornerRadius = MosaicImageViewContainer.cornerRadius imageView.layer.cornerCurve = .continuous imageView.contentMode = .scaleAspectFill } @@ -195,13 +195,13 @@ extension MosaicImageView { } } -extension MosaicImageView { +extension MosaicImageViewContainer { @objc private func photoTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { guard let imageView = sender.view as? UIImageView else { return } guard let index = imageViews.firstIndex(of: imageView) else { return } os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: tap photo at index: %ld", ((#file as NSString).lastPathComponent), #line, #function, index) - delegate?.mosaicImageView(self, didTapImageView: imageView, atIndex: index) + delegate?.mosaicImageViewContainer(self, didTapImageView: imageView, atIndex: index) } } @@ -218,7 +218,7 @@ struct MosaicImageView_Previews: PreviewProvider { static var previews: some View { Group { UIViewPreview(width: 375) { - let view = MosaicImageView() + let view = MosaicImageViewContainer() let image = images[3] let imageView = view.setupImageView( aspectRatio: image.size, @@ -230,7 +230,7 @@ struct MosaicImageView_Previews: PreviewProvider { .previewLayout(.fixed(width: 375, height: 400)) .previewDisplayName("Portrait - one image") UIViewPreview(width: 375) { - let view = MosaicImageView() + let view = MosaicImageViewContainer() let image = images[1] let imageView = view.setupImageView( aspectRatio: image.size, @@ -245,7 +245,7 @@ struct MosaicImageView_Previews: PreviewProvider { .previewLayout(.fixed(width: 375, height: 400)) .previewDisplayName("Landscape - one image") UIViewPreview(width: 375) { - let view = MosaicImageView() + let view = MosaicImageViewContainer() let images = self.images.prefix(2) let imageViews = view.setupImageViews(count: images.count, maxHeight: 162) for (i, imageView) in imageViews.enumerated() { @@ -256,7 +256,7 @@ struct MosaicImageView_Previews: PreviewProvider { .previewLayout(.fixed(width: 375, height: 200)) .previewDisplayName("two image") UIViewPreview(width: 375) { - let view = MosaicImageView() + let view = MosaicImageViewContainer() let images = self.images.prefix(3) let imageViews = view.setupImageViews(count: images.count, maxHeight: 162) for (i, imageView) in imageViews.enumerated() { @@ -267,7 +267,7 @@ struct MosaicImageView_Previews: PreviewProvider { .previewLayout(.fixed(width: 375, height: 200)) .previewDisplayName("three image") UIViewPreview(width: 375) { - let view = MosaicImageView() + let view = MosaicImageViewContainer() let images = self.images.prefix(4) let imageViews = view.setupImageViews(count: images.count, maxHeight: 162) for (i, imageView) in imageViews.enumerated() { diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index d52def46c..2d8d966b9 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -99,7 +99,7 @@ final class StatusView: UIView { button.setTitle(L10n.Common.Controls.Status.showPost, for: .normal) return button }() - let mosaicImageView = MosaicImageView() + let statusMosaicImageView = MosaicImageViewContainer() // do not use visual effect view due to we blur text only without background let contentWarningBlurContentImageView: UIImageView = { @@ -257,7 +257,7 @@ extension StatusView { ]) statusContentWarningContainerStackView.addArrangedSubview(contentWarningTitle) statusContentWarningContainerStackView.addArrangedSubview(contentWarningActionButton) - statusContainerStackView.addArrangedSubview(mosaicImageView) + statusContainerStackView.addArrangedSubview(statusMosaicImageView) // action toolbar container @@ -265,7 +265,7 @@ extension StatusView { actionToolbarContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical) headerContainerStackView.isHidden = true - mosaicImageView.isHidden = true + statusMosaicImageView.isHidden = true contentWarningBlurContentImageView.isHidden = true statusContentWarningContainerStackView.isHidden = true statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false @@ -343,7 +343,7 @@ struct StatusView_Previews: PreviewProvider { } .previewLayout(.fixed(width: 375, height: 200)) UIViewPreview(width: 375) { - let statusView = StatusView() + let statusView = StatusView(frame: CGRect(x: 0, y: 0, width: 375, height: 500)) statusView.configure( with: AvatarConfigurableViewConfiguration( avatarImageURL: nil, @@ -351,9 +351,20 @@ struct StatusView_Previews: PreviewProvider { ) ) statusView.headerContainerStackView.isHidden = false + statusView.isStatusTextSensitive = true + statusView.setNeedsLayout() + statusView.layoutIfNeeded() + statusView.drawContentWarningImageView() + statusView.updateContentWarningDisplay(isHidden: false) + let images = MosaicImageView_Previews.images + let imageViews = statusView.statusMosaicImageView.setupImageViews(count: 4, maxHeight: 162) + for (i, imageView) in imageViews.enumerated() { + imageView.image = images[i] + } + statusView.statusMosaicImageView.isHidden = false return statusView } - .previewLayout(.fixed(width: 375, height: 200)) + .previewLayout(.fixed(width: 375, height: 380)) } } From ccc8741ccd1b695d927a87a10f976f42fce669fa Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 25 Feb 2021 11:48:16 +0800 Subject: [PATCH 10/11] chore: set public timeline title and update loader height --- .../Scene/PublicTimeline/PublicTimelineViewController.swift | 1 + .../Share/View/TableviewCell/TimelineLoaderTableViewCell.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift index 62993bfb5..5a5001c19 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift @@ -42,6 +42,7 @@ extension PublicTimelineViewController { override func viewDidLoad() { super.viewDidLoad() + title = "Public" view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color tableView.refreshControl = refreshControl diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift index f9508894a..6aa195241 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift @@ -11,7 +11,7 @@ import Combine class TimelineLoaderTableViewCell: UITableViewCell { static let cellHeight: CGFloat = 44 + TimelineLoaderTableViewCell.extraTopPadding + TimelineLoaderTableViewCell.bottomPadding - static let extraTopPadding: CGFloat = 3 // the status cell already has 10pt bottom padding + static let extraTopPadding: CGFloat = 0 // the status cell already has 10pt bottom padding static let bottomPadding: CGFloat = StatusTableViewCell.bottomPaddingHeight + TimelineLoaderTableViewCell.extraTopPadding // make balance var disposeBag = Set() From 414aa086b4ab5bc943db3b158c61c270ae6c5a31 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 25 Feb 2021 13:47:30 +0800 Subject: [PATCH 11/11] feat: implement image media content warning overlay --- .gitignore | 4 +- Localization/app.json | 5 +- Mastodon/Diffiable/Item/Item.swift | 12 ++- .../Diffiable/Section/StatusSection.swift | 19 +++-- Mastodon/Generated/Strings.swift | 6 +- ...er+TimelinePostTableViewCellDelegate.swift | 36 +++++++++ .../Resources/en.lproj/Localizable.strings | 3 +- .../HomeTimelineViewModel+Diffable.swift | 7 +- .../PublicTimelineViewController.swift | 5 +- .../PublicTimelineViewModel+Diffable.swift | 7 +- .../Container/MosaicImageViewContainer.swift | 81 +++++++++++++++++-- .../Scene/Share/View/Content/StatusView.swift | 2 +- .../TableviewCell/StatusTableViewCell.swift | 19 ++++- 13 files changed, 178 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 51ba722ba..a766fc629 100644 --- a/.gitignore +++ b/.gitignore @@ -119,5 +119,5 @@ xcuserdata # End of https://www.toptal.com/developers/gitignore/api/swift,swiftpm,xcode,cocoapods -Mastodon/Localization/StringsConvertor/input -Mastodon/Localization/StringsConvertor/output \ No newline at end of file +Localization/StringsConvertor/input +Localization/StringsConvertor/output \ No newline at end of file diff --git a/Localization/app.json b/Localization/app.json index 56a54a7f4..f2c2839a6 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -29,8 +29,9 @@ }, "status": { "user_boosted": "%s boosted", - "content_warning": "content warning", - "show_post": "Show Post" + "show_post": "Show Post", + "status_content_warning": "content warning", + "media_content_warning": "Tap to reveal that may be sensitive" }, "timeline": { "load_more": "Load More" diff --git a/Mastodon/Diffiable/Item/Item.swift b/Mastodon/Diffiable/Item/Item.swift index 2ab198226..c6a182b4d 100644 --- a/Mastodon/Diffiable/Item/Item.swift +++ b/Mastodon/Diffiable/Item/Item.swift @@ -26,24 +26,30 @@ enum Item { protocol StatusContentWarningAttribute { var isStatusTextSensitive: Bool { get set } + var isStatusSensitive: Bool { get set } } extension Item { class StatusTimelineAttribute: Hashable, StatusContentWarningAttribute { - var isStatusTextSensitive: Bool = false + var isStatusTextSensitive: Bool + var isStatusSensitive: Bool public init( - isStatusTextSensitive: Bool + isStatusTextSensitive: Bool, + isStatusSensitive: Bool ) { self.isStatusTextSensitive = isStatusTextSensitive + self.isStatusSensitive = isStatusSensitive } static func == (lhs: Item.StatusTimelineAttribute, rhs: Item.StatusTimelineAttribute) -> Bool { - return lhs.isStatusTextSensitive == rhs.isStatusTextSensitive + return lhs.isStatusTextSensitive == rhs.isStatusTextSensitive && + lhs.isStatusSensitive == rhs.isStatusSensitive } func hash(into hasher: inout Hasher) { hasher.combine(isStatusTextSensitive) + hasher.combine(isStatusSensitive) } } diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 727f4074f..4fac88b4c 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -94,14 +94,18 @@ extension StatusSection { // set text cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content) - // set content warning - let isStatusTextSensitive = statusContentWarningAttribute?.isStatusTextSensitive ?? (toot.reblog ?? toot).sensitive + // set status text content warning + let spoilerText = (toot.reblog ?? toot).spoilerText ?? "" + let isStatusTextSensitive = statusContentWarningAttribute?.isStatusTextSensitive ?? !spoilerText.isEmpty cell.statusView.isStatusTextSensitive = isStatusTextSensitive cell.statusView.updateContentWarningDisplay(isHidden: !isStatusTextSensitive) - cell.statusView.contentWarningTitle.text = (toot.reblog ?? toot).spoilerText.flatMap { spoilerText in - guard !spoilerText.isEmpty else { return nil } - return L10n.Common.Controls.Status.contentWarning + ": \(spoilerText)" - } ?? L10n.Common.Controls.Status.contentWarning + cell.statusView.contentWarningTitle.text = { + if spoilerText.isEmpty { + return L10n.Common.Controls.Status.statusContentWarning + } else { + return L10n.Common.Controls.Status.statusContentWarning + ": \(spoilerText)" + } + }() // prepare media attachments let mediaAttachments = Array((toot.reblog ?? toot).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending } @@ -146,6 +150,9 @@ extension StatusSection { } } cell.statusView.statusMosaicImageView.isHidden = mosiacImageViewModel.metas.isEmpty + let isStatusSensitive = statusContentWarningAttribute?.isStatusSensitive ?? (toot.reblog ?? toot).sensitive + cell.statusView.statusMosaicImageView.blurVisualEffectView.effect = isStatusSensitive ? MosaicImageViewContainer.blurVisualEffect : nil + cell.statusView.statusMosaicImageView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 // toolbar let replyCountTitle: String = { diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index fa641fbbb..5c6f58bbf 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -56,10 +56,12 @@ internal enum L10n { internal static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto") } internal enum Status { - /// content warning - internal static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning") + /// Tap to reveal that may be sensitive + internal static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning") /// Show Post internal static let showPost = L10n.tr("Localizable", "Common.Controls.Status.ShowPost") + /// content warning + internal static let statusContentWarning = L10n.tr("Localizable", "Common.Controls.Status.StatusContentWarning") /// %@ boosted internal static func userBoosted(_ p1: Any) -> String { return L10n.tr("Localizable", "Common.Controls.Status.UserBoosted", String(describing: p1)) diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+TimelinePostTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+TimelinePostTableViewCellDelegate.swift index 9c6127b08..336434ff0 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+TimelinePostTableViewCellDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+TimelinePostTableViewCellDelegate.swift @@ -43,3 +43,39 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { } } + +extension StatusTableViewCellDelegate where Self: StatusProvider { + + func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) { + + } + + func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) { + guard let diffableDataSource = self.tableViewDiffableDataSource else { return } + item(for: cell, indexPath: nil) + .receive(on: DispatchQueue.main) + .sink { [weak self] item in + guard let _ = self else { return } + guard let item = item else { return } + switch item { + case .homeTimelineIndex(_, let attribute): + attribute.isStatusSensitive = false + case .toot(_, let attribute): + attribute.isStatusSensitive = false + default: + return + } + + var snapshot = diffableDataSource.snapshot() + snapshot.reloadItems([item]) + UIView.animate(withDuration: 0.33) { + cell.statusView.statusMosaicImageView.blurVisualEffectView.effect = nil + cell.statusView.statusMosaicImageView.vibrancyVisualEffectView.alpha = 0.0 + } completion: { _ in + diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil) + } + } + .store(in: &cell.disposeBag) + } + +} diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index efb3fed70..08a8d8293 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -15,8 +15,9 @@ "Common.Controls.Actions.SignIn" = "Sign in"; "Common.Controls.Actions.SignUp" = "Sign up"; "Common.Controls.Actions.TakePhoto" = "Take photo"; -"Common.Controls.Status.ContentWarning" = "content warning"; +"Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive"; "Common.Controls.Status.ShowPost" = "Show Post"; +"Common.Controls.Status.StatusContentWarning" = "content warning"; "Common.Controls.Status.UserBoosted" = "%@ boosted"; "Common.Controls.Timeline.LoadMore" = "Load More"; "Common.Countable.Photo.Multiple" = "photos"; diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index 8f74b81a4..d5345de4f 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -83,7 +83,12 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate { var newTimelineItems: [Item] = [] for (i, timelineIndex) in timelineIndexes.enumerated() { - let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.StatusTimelineAttribute(isStatusTextSensitive: timelineIndex.toot.sensitive) + let toot = timelineIndex.toot.reblog ?? timelineIndex.toot + let isStatusTextSensitive: Bool = { + guard let spoilerText = toot.spoilerText, !spoilerText.isEmpty else { return false } + return true + }() + let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.StatusTimelineAttribute(isStatusTextSensitive: isStatusTextSensitive, isStatusSensitive: toot.sensitive) // append new item into snapshot newTimelineItems.append(.homeTimelineIndex(objectID: timelineIndex.objectID, attribute: attribute)) diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift index 5a5001c19..dd5ffc84e 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift @@ -13,7 +13,7 @@ import GameplayKit import os.log import UIKit -final class PublicTimelineViewController: UIViewController, NeedsDependency, StatusTableViewCellDelegate { +final class PublicTimelineViewController: UIViewController, NeedsDependency { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } @@ -203,3 +203,6 @@ extension PublicTimelineViewController: TimelineMiddleLoaderTableViewCellDelegat } } } + +// MARK: - StatusTableViewCellDelegate +extension PublicTimelineViewController: StatusTableViewCellDelegate { } diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift index 26638578f..f9c92fa0f 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift @@ -58,7 +58,12 @@ extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate { var items = [Item]() for (_, toot) in indexTootTuples { - let attribute = oldSnapshotAttributeDict[toot.objectID] ?? Item.StatusTimelineAttribute(isStatusTextSensitive: toot.sensitive) + let targetToot = toot.reblog ?? toot + let isStatusTextSensitive: Bool = { + guard let spoilerText = targetToot.spoilerText, !spoilerText.isEmpty else { return false } + return true + }() + let attribute = oldSnapshotAttributeDict[toot.objectID] ?? Item.StatusTimelineAttribute(isStatusTextSensitive: isStatusTextSensitive, isStatusSensitive: targetToot.sensitive) items.append(Item.toot(objectID: toot.objectID, attribute: attribute)) if tootIDsWhichHasGap.contains(toot.id) { items.append(Item.publicMiddleLoader(tootID: toot.id)) diff --git a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift index 14e2012e0..5240d4e2c 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift +++ b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift @@ -13,18 +13,21 @@ protocol MosaicImageViewContainerPresentable: class { var mosaicImageViewContainer: MosaicImageViewContainer { get } } -protocol MosaicImageViewDelegate: class { +protocol MosaicImageViewContainerDelegate: class { func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) + func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) + } final class MosaicImageViewContainer: UIView { static let cornerRadius: CGFloat = 4 + static let blurVisualEffect = UIBlurEffect(style: .systemUltraThinMaterial) - weak var delegate: MosaicImageViewDelegate? + weak var delegate: MosaicImageViewContainerDelegate? let container = UIStackView() - var imageViews = [UIImageView]() { + var imageViews: [UIImageView] = [] { didSet { imageViews.forEach { imageView in imageView.isUserInteractionEnabled = true @@ -34,7 +37,16 @@ final class MosaicImageViewContainer: UIView { } } } - + let blurVisualEffectView = UIVisualEffectView(effect: MosaicImageViewContainer.blurVisualEffect) + let vibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: MosaicImageViewContainer.blurVisualEffect)) + let contentWarningLabel: UILabel = { + let label = UILabel() + label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15)) + label.text = L10n.Common.Controls.Status.mediaContentWarning + label.textAlignment = .center + return label + }() + private var containerHeightLayoutConstraint: NSLayoutConstraint! override init(frame: CGRect) { @@ -53,6 +65,8 @@ extension MosaicImageViewContainer { private func _init() { container.translatesAutoresizingMaskIntoConstraints = false + container.axis = .horizontal + container.distribution = .fillEqually addSubview(container) containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: 162).priority(.required - 1) NSLayoutConstraint.activate([ @@ -63,8 +77,32 @@ extension MosaicImageViewContainer { containerHeightLayoutConstraint ]) - container.axis = .horizontal - container.distribution = .fillEqually + // add blur visual effect view in the setup method + blurVisualEffectView.layer.masksToBounds = true + blurVisualEffectView.layer.cornerRadius = MosaicImageViewContainer.cornerRadius + blurVisualEffectView.layer.cornerCurve = .continuous + + vibrancyVisualEffectView.translatesAutoresizingMaskIntoConstraints = false + blurVisualEffectView.contentView.addSubview(vibrancyVisualEffectView) + NSLayoutConstraint.activate([ + vibrancyVisualEffectView.topAnchor.constraint(equalTo: blurVisualEffectView.topAnchor), + vibrancyVisualEffectView.leadingAnchor.constraint(equalTo: blurVisualEffectView.leadingAnchor), + vibrancyVisualEffectView.trailingAnchor.constraint(equalTo: blurVisualEffectView.trailingAnchor), + vibrancyVisualEffectView.bottomAnchor.constraint(equalTo: blurVisualEffectView.bottomAnchor), + ]) + + contentWarningLabel.translatesAutoresizingMaskIntoConstraints = false + vibrancyVisualEffectView.contentView.addSubview(contentWarningLabel) + NSLayoutConstraint.activate([ + contentWarningLabel.leadingAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.layoutMarginsGuide.leadingAnchor), + contentWarningLabel.trailingAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.layoutMarginsGuide.trailingAnchor), + contentWarningLabel.centerYAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.centerYAnchor), + ]) + + blurVisualEffectView.isUserInteractionEnabled = true + let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer + tapGesture.addTarget(self, action: #selector(MosaicImageViewContainer.visualEffectViewTapGestureRecognizerHandler(_:))) + blurVisualEffectView.addGestureRecognizer(tapGesture) } } @@ -79,6 +117,9 @@ extension MosaicImageViewContainer { container.subviews.forEach { subview in subview.removeFromSuperview() } + blurVisualEffectView.removeFromSuperview() + blurVisualEffectView.effect = MosaicImageViewContainer.blurVisualEffect + vibrancyVisualEffectView.alpha = 1.0 imageViews = [] container.spacing = 1 @@ -100,6 +141,7 @@ extension MosaicImageViewContainer { imageViews.append(imageView) imageView.layer.masksToBounds = true imageView.layer.cornerRadius = MosaicImageViewContainer.cornerRadius + imageView.layer.cornerCurve = .continuous imageView.contentMode = .scaleAspectFill imageView.translatesAutoresizingMaskIntoConstraints = false @@ -112,6 +154,15 @@ extension MosaicImageViewContainer { ]) containerHeightLayoutConstraint.constant = floor(rect.height) containerHeightLayoutConstraint.isActive = true + + blurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false + addSubview(blurVisualEffectView) + NSLayoutConstraint.activate([ + blurVisualEffectView.topAnchor.constraint(equalTo: imageView.topAnchor), + blurVisualEffectView.leadingAnchor.constraint(equalTo: imageView.leadingAnchor), + blurVisualEffectView.trailingAnchor.constraint(equalTo: imageView.trailingAnchor), + blurVisualEffectView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor), + ]) return imageView } @@ -191,18 +242,34 @@ extension MosaicImageViewContainer { } } + blurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false + addSubview(blurVisualEffectView) + NSLayoutConstraint.activate([ + blurVisualEffectView.topAnchor.constraint(equalTo: container.topAnchor), + blurVisualEffectView.leadingAnchor.constraint(equalTo: container.leadingAnchor), + blurVisualEffectView.trailingAnchor.constraint(equalTo: container.trailingAnchor), + blurVisualEffectView.bottomAnchor.constraint(equalTo: container.bottomAnchor), + ]) + return imageViews } + } extension MosaicImageViewContainer { - + + @objc private func visualEffectViewTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.mosaicImageViewContainer(self, didTapContentWarningVisualEffectView: blurVisualEffectView) + } + @objc private func photoTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { guard let imageView = sender.view as? UIImageView else { return } guard let index = imageViews.firstIndex(of: imageView) else { return } os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: tap photo at index: %ld", ((#file as NSString).lastPathComponent), #line, #function, index) delegate?.mosaicImageViewContainer(self, didTapImageView: imageView, atIndex: index) } + } #if DEBUG && canImport(SwiftUI) diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 2d8d966b9..be754ed86 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -89,7 +89,7 @@ final class StatusView: UIView { let label = UILabel() label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) label.textColor = Asset.Colors.Label.primary.color - label.text = L10n.Common.Controls.Status.contentWarning + label.text = L10n.Common.Controls.Status.statusContentWarning return label }() let contentWarningActionButton: UIButton = { diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index 758beed0c..572f23e01 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -14,6 +14,9 @@ import Combine protocol StatusTableViewCellDelegate: class { func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton) + func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) + func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) + } final class StatusTableViewCell: UITableViewCell { @@ -79,10 +82,11 @@ extension StatusTableViewCell { bottomPaddingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), bottomPaddingView.heightAnchor.constraint(equalToConstant: StatusTableViewCell.bottomPaddingHeight).priority(.defaultHigh), ]) + bottomPaddingView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color statusView.delegate = self + statusView.statusMosaicImageView.delegate = self statusView.actionToolbarContainer.delegate = self - bottomPaddingView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color } } @@ -94,6 +98,19 @@ extension StatusTableViewCell: StatusViewDelegate { } } +// MARK: - MosaicImageViewDelegate +extension StatusTableViewCell: MosaicImageViewContainerDelegate { + + func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) { + delegate?.statusTableViewCell(self, mosaicImageViewContainer: mosaicImageViewContainer, didTapImageView: imageView, atIndex: index) + } + + func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) { + delegate?.statusTableViewCell(self, mosaicImageViewContainer: mosaicImageViewContainer, didTapContentWarningVisualEffectView: visualEffectView) + } + +} + // MARK: - ActionToolbarContainerDelegate extension StatusTableViewCell: ActionToolbarContainerDelegate { func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) {