From 441a6aee9e0286f389d23083b43a0bb7c3c79885 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 9 Mar 2021 15:18:43 +0800 Subject: [PATCH 01/31] feat: implement boost for toot --- CoreDataStack/Entity/Toot.swift | 8 +- Mastodon.xcodeproj/project.pbxproj | 4 + .../Diffiable/Section/StatusSection.swift | 65 +++--- Mastodon/Generated/Assets.swift | 1 + ...Provider+StatusTableViewCellDelegate.swift | 4 + .../StatusProvider+UITableViewDelegate.swift | 1 + .../StatusProvider/StatusProviderFacade.swift | 110 ++++++++++- .../system.green.colorset/Contents.json | 20 ++ .../TableviewCell/StatusTableViewCell.swift | 5 +- .../View/ToolBar/ActionToolBarContainer.swift | 69 ++++--- .../APIService/APIService+Favorite.swift | 5 +- .../APIService/APIService+Reblog.swift | 169 ++++++++++++++++ .../API/Mastodon+API+Favorites.swift | 6 +- .../API/Mastodon+API+Status+Reblog.swift | 186 ++++++++++++++++++ .../MastodonSDK/API/Mastodon+API+Status.swift | 12 ++ .../MastodonSDK/API/Mastodon+API.swift | 1 + 16 files changed, 598 insertions(+), 68 deletions(-) create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/system.green.colorset/Contents.json create mode 100644 Mastodon/Service/APIService/APIService+Reblog.swift create mode 100644 MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Status+Reblog.swift create mode 100644 MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Status.swift diff --git a/CoreDataStack/Entity/Toot.swift b/CoreDataStack/Entity/Toot.swift index c5fcf4869..56d112d74 100644 --- a/CoreDataStack/Entity/Toot.swift +++ b/CoreDataStack/Entity/Toot.swift @@ -163,7 +163,7 @@ public extension Toot { func update(liked: Bool, mastodonUser: MastodonUser) { if liked { if !(self.favouritedBy ?? Set()).contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Toot.favouritedBy)).addObjects(from: [mastodonUser]) + self.mutableSetValue(forKey: #keyPath(Toot.favouritedBy)).add(mastodonUser) } } else { if (self.favouritedBy ?? Set()).contains(mastodonUser) { @@ -174,7 +174,7 @@ public extension Toot { func update(reblogged: Bool, mastodonUser: MastodonUser) { if reblogged { if !(self.rebloggedBy ?? Set()).contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Toot.rebloggedBy)).addObjects(from: [mastodonUser]) + self.mutableSetValue(forKey: #keyPath(Toot.rebloggedBy)).add(mastodonUser) } } else { if (self.rebloggedBy ?? Set()).contains(mastodonUser) { @@ -186,7 +186,7 @@ public extension Toot { func update(muted: Bool, mastodonUser: MastodonUser) { if muted { if !(self.mutedBy ?? Set()).contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Toot.mutedBy)).addObjects(from: [mastodonUser]) + self.mutableSetValue(forKey: #keyPath(Toot.mutedBy)).add(mastodonUser) } } else { if (self.mutedBy ?? Set()).contains(mastodonUser) { @@ -198,7 +198,7 @@ public extension Toot { func update(bookmarked: Bool, mastodonUser: MastodonUser) { if bookmarked { if !(self.bookmarkedBy ?? Set()).contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Toot.bookmarkedBy)).addObjects(from: [mastodonUser]) + self.mutableSetValue(forKey: #keyPath(Toot.bookmarkedBy)).add(mastodonUser) } } else { if (self.bookmarkedBy ?? Set()).contains(mastodonUser) { diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index b3abfd84a..448d81a51 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -183,6 +183,7 @@ DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2D25E504AC0051B173 /* Attachment.swift */; }; DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C3725E508BE0051B173 /* Attachment.swift */; }; DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; }; + DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */; }; DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; }; DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; }; DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; }; @@ -425,6 +426,7 @@ DB9D6C2D25E504AC0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; DB9D6C3725E508BE0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = ""; }; + DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Reblog.swift"; sourceTree = ""; }; DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = ""; }; DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = ""; }; DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewModel.swift; sourceTree = ""; }; @@ -906,6 +908,7 @@ DB98337E25C9452D00AD9700 /* APIService+APIError.swift */, DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */, 2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */, + DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */, DB98336A25C9420100AD9700 /* APIService+App.swift */, DB98337025C9443200AD9700 /* APIService+Authentication.swift */, DB98339B25C96DE600AD9700 /* APIService+Account.swift */, @@ -1663,6 +1666,7 @@ 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */, 0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */, DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */, + DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 5f9d43ed5..3bdfffc56 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -159,7 +159,7 @@ extension StatusSection { // set poll let poll = (toot.reblog ?? toot).poll - StatusSection.configure( + StatusSection.configurePoll( cell: cell, poll: poll, requestUserID: requestUserID, @@ -173,7 +173,7 @@ extension StatusSection { } receiveValue: { change in guard case let .update(object) = change.changeType, let newPoll = object as? Poll else { return } - StatusSection.configure( + StatusSection.configurePoll( cell: cell, poll: newPoll, requestUserID: requestUserID, @@ -185,19 +185,7 @@ extension StatusSection { } // toolbar - let replyCountTitle: String = { - let count = (toot.reblog ?? toot).repliesCount?.intValue ?? 0 - return StatusSection.formattedNumberTitleForActionButton(count) - }() - cell.statusView.actionToolbarContainer.replyButton.setTitle(replyCountTitle, for: .normal) - - let isLike = (toot.reblog ?? toot).favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false - let favoriteCountTitle: String = { - let count = (toot.reblog ?? toot).favouritesCount.intValue - return StatusSection.formattedNumberTitleForActionButton(count) - }() - cell.statusView.actionToolbarContainer.starButton.setTitle(favoriteCountTitle, for: .normal) - cell.statusView.actionToolbarContainer.isStarButtonHighlight = isLike + StatusSection.configureActionToolBar(cell: cell, toot: toot, requestUserID: requestUserID) // set date let createdAt = (toot.reblog ?? toot).createdAt @@ -215,20 +203,47 @@ extension StatusSection { // do nothing } receiveValue: { change in guard case .update(let object) = change.changeType, - let newToot = object as? Toot else { return } - let targetToot = newToot.reblog ?? newToot - - let isLike = targetToot.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false - let favoriteCount = targetToot.favouritesCount.intValue - let favoriteCountTitle = StatusSection.formattedNumberTitleForActionButton(favoriteCount) - cell.statusView.actionToolbarContainer.starButton.setTitle(favoriteCountTitle, for: .normal) - cell.statusView.actionToolbarContainer.isStarButtonHighlight = isLike - os_log("%{public}s[%{public}ld], %{public}s: like count label for toot %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, targetToot.id, favoriteCount) + let toot = object as? Toot else { return } + StatusSection.configureActionToolBar(cell: cell, toot: toot, requestUserID: requestUserID) + + os_log("%{public}s[%{public}ld], %{public}s: boost count label for toot %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, toot.id, toot.reblogsCount.intValue) + os_log("%{public}s[%{public}ld], %{public}s: like count label for toot %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, toot.id, toot.favouritesCount.intValue) } .store(in: &cell.disposeBag) } - static func configure( + static func configureActionToolBar( + cell: StatusTableViewCell, + toot: Toot, + requestUserID: String + ) { + let toot = toot.reblog ?? toot + + // set reply + let replyCountTitle: String = { + let count = toot.repliesCount?.intValue ?? 0 + return StatusSection.formattedNumberTitleForActionButton(count) + }() + cell.statusView.actionToolbarContainer.replyButton.setTitle(replyCountTitle, for: .normal) + // set boost + let isBoosted = toot.rebloggedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false + let boostCountTitle: String = { + let count = toot.reblogsCount.intValue + return StatusSection.formattedNumberTitleForActionButton(count) + }() + cell.statusView.actionToolbarContainer.boostButton.setTitle(boostCountTitle, for: .normal) + cell.statusView.actionToolbarContainer.isBoostButtonHighlight = isBoosted + // set like + let isLike = toot.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false + let favoriteCountTitle: String = { + let count = toot.favouritesCount.intValue + return StatusSection.formattedNumberTitleForActionButton(count) + }() + cell.statusView.actionToolbarContainer.favoriteButton.setTitle(favoriteCountTitle, for: .normal) + cell.statusView.actionToolbarContainer.isFavoriteButtonHighlight = isLike + } + + static func configurePoll( cell: StatusTableViewCell, poll: Poll?, requestUserID: String, diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index 32786b40d..a0831c9a7 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -74,6 +74,7 @@ internal enum Asset { internal static let lightSuccessGreen = ColorAsset(name: "Colors/lightSuccessGreen") internal static let lightWhite = ColorAsset(name: "Colors/lightWhite") internal static let plusCircleFill = ImageAsset(name: "Colors/plus.circle.fill") + internal static let systemGreen = ColorAsset(name: "Colors/system.green") internal static let systemOrange = ColorAsset(name: "Colors/system.orange") } internal enum Welcome { diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift index cd4e5160d..e768ae3c9 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift @@ -16,6 +16,10 @@ import ActiveLabel // MARK: - ActionToolbarContainerDelegate extension StatusTableViewCellDelegate where Self: StatusProvider { + func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, boostButtonDidPressed sender: UIButton) { + StatusProviderFacade.responseToStatusBoostAction(provider: self, cell: cell) + } + func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) { StatusProviderFacade.responseToStatusLikeAction(provider: self, cell: cell) } diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift index 93f627c09..13bb63f06 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift @@ -17,6 +17,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { // } func handleTableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + // update poll when toot appear let now = Date() var pollID: Mastodon.Entity.Poll.ID? toot(for: cell, indexPath: indexPath) diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift index 894461566..cc5589978 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift @@ -16,6 +16,7 @@ import ActiveLabel enum StatusProviderFacade { } + extension StatusProviderFacade { static func responseToStatusLikeAction(provider: StatusProvider) { @@ -56,10 +57,9 @@ extension StatusProviderFacade { toot .compactMap { toot -> (NSManagedObjectID, Mastodon.API.Favorites.FavoriteKind)? in - guard let toot = toot else { return nil } + guard let toot = toot?.reblog ?? toot else { return nil } let favoriteKind: Mastodon.API.Favorites.FavoriteKind = { - let targetToot = (toot.reblog ?? toot) - let isLiked = targetToot.favouritedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false + let isLiked = toot.favouritedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false return isLiked ? .destroy : .create }() return (toot.objectID, favoriteKind) @@ -120,6 +120,110 @@ extension StatusProviderFacade { } +extension StatusProviderFacade { + + + static func responseToStatusBoostAction(provider: StatusProvider) { + _responseToStatusBoostAction( + provider: provider, + toot: provider.toot() + ) + } + + static func responseToStatusBoostAction(provider: StatusProvider, cell: UITableViewCell) { + _responseToStatusBoostAction( + provider: provider, + toot: provider.toot(for: cell, indexPath: nil) + ) + } + + private static func _responseToStatusBoostAction(provider: StatusProvider, toot: Future) { + // prepare authentication + guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { + assertionFailure() + return + } + + // prepare current user infos + guard let _currentMastodonUser = provider.context.authenticationService.activeMastodonAuthentication.value?.user else { + assertionFailure() + return + } + let mastodonUserID = activeMastodonAuthenticationBox.userID + assert(_currentMastodonUser.id == mastodonUserID) + let mastodonUserObjectID = _currentMastodonUser.objectID + + guard let context = provider.context else { return } + + // haptic feedback generator + let generator = UIImpactFeedbackGenerator(style: .light) + let responseFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium) + + toot + .compactMap { toot -> (NSManagedObjectID, Mastodon.API.Status.Reblog.BoostKind)? in + guard let toot = toot?.reblog ?? toot else { return nil } + let boostKind: Mastodon.API.Status.Reblog.BoostKind = { + let isBoosted = toot.rebloggedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false + return isBoosted ? .undoBoost : .boost + }() + return (toot.objectID, boostKind) + } + .map { tootObjectID, boostKind -> AnyPublisher<(Toot.ID, Mastodon.API.Status.Reblog.BoostKind), Error> in + return context.apiService.boost( + tootObjectID: tootObjectID, + mastodonUserObjectID: mastodonUserObjectID, + boostKind: boostKind + ) + .map { tootID in (tootID, boostKind) } + .eraseToAnyPublisher() + } + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + .switchToLatest() + .receive(on: DispatchQueue.main) + .handleEvents { _ in + generator.prepare() + responseFeedbackGenerator.prepare() + } receiveOutput: { _, boostKind in + generator.impactOccurred() + os_log("%{public}s[%{public}ld], %{public}s: [Boost] update local toot reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, boostKind == .boost ? "boost" : "unboost") + } receiveCompletion: { completion in + switch completion { + case .failure: + // TODO: handle error + break + case .finished: + break + } + } + .map { tootID, boostKind in + return context.apiService.boost( + statusID: tootID, + boostKind: boostKind, + mastodonAuthenticationBox: activeMastodonAuthenticationBox + ) + } + .switchToLatest() + .receive(on: DispatchQueue.main) + .sink { [weak provider] completion in + guard let provider = provider else { return } + if provider.view.window != nil { + responseFeedbackGenerator.impactOccurred() + } + switch completion { + case .failure(let error): + os_log("%{public}s[%{public}ld], %{public}s: [Boost] remote boost request fail: %{public}s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + case .finished: + os_log("%{public}s[%{public}ld], %{public}s: [Boost] remote boost request success", ((#file as NSString).lastPathComponent), #line, #function) + } + } receiveValue: { response in + // do nothing + } + .store(in: &provider.disposeBag) + } + +} + extension StatusProviderFacade { enum Target { case toot diff --git a/Mastodon/Resources/Assets.xcassets/Colors/system.green.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/system.green.colorset/Contents.json new file mode 100644 index 000000000..8716dcb74 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/system.green.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.604", + "green" : "0.741", + "red" : "0.475" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index 13c3afba4..fdebd5769 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -19,6 +19,7 @@ protocol StatusTableViewCellDelegate: class { func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) + func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, boostButtonDidPressed sender: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton) @@ -207,8 +208,8 @@ extension StatusTableViewCell: ActionToolbarContainerDelegate { func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) { } - func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, retootButtonDidPressed sender: UIButton) { - + func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, boostButtonDidPressed sender: UIButton) { + delegate?.statusTableViewCell(self, actionToolbarContainer: actionToolbarContainer, boostButtonDidPressed: sender) } func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, starButtonDidPressed sender: UIButton) { delegate?.statusTableViewCell(self, actionToolbarContainer: actionToolbarContainer, likeButtonDidPressed: sender) diff --git a/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift b/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift index 02f60d518..991592c13 100644 --- a/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift +++ b/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift @@ -10,7 +10,7 @@ import UIKit protocol ActionToolbarContainerDelegate: class { func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) - func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, retootButtonDidPressed sender: UIButton) + func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, boostButtonDidPressed sender: UIButton) func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, starButtonDidPressed sender: UIButton) func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, moreButtonDidPressed sender: UIButton) } @@ -19,12 +19,16 @@ protocol ActionToolbarContainerDelegate: class { final class ActionToolbarContainer: UIView { let replyButton = HitTestExpandedButton() - let retootButton = HitTestExpandedButton() - let starButton = HitTestExpandedButton() + let boostButton = HitTestExpandedButton() + let favoriteButton = HitTestExpandedButton() let moreButton = HitTestExpandedButton() - var isStarButtonHighlight: Bool = false { - didSet { isStarButtonHighlightStateDidChange(to: isStarButtonHighlight) } + var isBoostButtonHighlight: Bool = false { + didSet { isBoostButtonHighlightStateDidChange(to: isBoostButtonHighlight) } + } + + var isFavoriteButtonHighlight: Bool = false { + didSet { isFavoriteButtonHighlightStateDidChange(to: isFavoriteButtonHighlight) } } weak var delegate: ActionToolbarContainerDelegate? @@ -57,8 +61,8 @@ extension ActionToolbarContainer { ]) replyButton.addTarget(self, action: #selector(ActionToolbarContainer.replyButtonDidPressed(_:)), for: .touchUpInside) - retootButton.addTarget(self, action: #selector(ActionToolbarContainer.retootButtonDidPressed(_:)), for: .touchUpInside) - starButton.addTarget(self, action: #selector(ActionToolbarContainer.starButtonDidPressed(_:)), for: .touchUpInside) + boostButton.addTarget(self, action: #selector(ActionToolbarContainer.boostButtonDidPressed(_:)), for: .touchUpInside) + favoriteButton.addTarget(self, action: #selector(ActionToolbarContainer.favoriteButtonDidPressed(_:)), for: .touchUpInside) moreButton.addTarget(self, action: #selector(ActionToolbarContainer.moreButtonDidPressed(_:)), for: .touchUpInside) } @@ -89,7 +93,7 @@ extension ActionToolbarContainer { subview.removeFromSuperview() } - let buttons = [replyButton, retootButton, starButton, moreButton] + let buttons = [replyButton, boostButton, favoriteButton, moreButton] buttons.forEach { button in button.tintColor = Asset.Colors.Button.actionToolbar.color button.titleLabel?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular) @@ -109,28 +113,28 @@ extension ActionToolbarContainer { button.contentHorizontalAlignment = .leading } replyButton.setImage(replyImage, for: .normal) - retootButton.setImage(reblogImage, for: .normal) - starButton.setImage(starImage, for: .normal) + boostButton.setImage(reblogImage, for: .normal) + favoriteButton.setImage(starImage, for: .normal) moreButton.setImage(moreImage, for: .normal) container.axis = .horizontal container.distribution = .fill replyButton.translatesAutoresizingMaskIntoConstraints = false - retootButton.translatesAutoresizingMaskIntoConstraints = false - starButton.translatesAutoresizingMaskIntoConstraints = false + boostButton.translatesAutoresizingMaskIntoConstraints = false + favoriteButton.translatesAutoresizingMaskIntoConstraints = false moreButton.translatesAutoresizingMaskIntoConstraints = false container.addArrangedSubview(replyButton) - container.addArrangedSubview(retootButton) - container.addArrangedSubview(starButton) + container.addArrangedSubview(boostButton) + container.addArrangedSubview(favoriteButton) container.addArrangedSubview(moreButton) NSLayoutConstraint.activate([ replyButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh), - replyButton.heightAnchor.constraint(equalTo: retootButton.heightAnchor).priority(.defaultHigh), - replyButton.heightAnchor.constraint(equalTo: starButton.heightAnchor).priority(.defaultHigh), + replyButton.heightAnchor.constraint(equalTo: boostButton.heightAnchor).priority(.defaultHigh), + replyButton.heightAnchor.constraint(equalTo: favoriteButton.heightAnchor).priority(.defaultHigh), replyButton.heightAnchor.constraint(equalTo: moreButton.heightAnchor).priority(.defaultHigh), - replyButton.widthAnchor.constraint(equalTo: retootButton.widthAnchor).priority(.defaultHigh), - replyButton.widthAnchor.constraint(equalTo: starButton.widthAnchor).priority(.defaultHigh), + replyButton.widthAnchor.constraint(equalTo: boostButton.widthAnchor).priority(.defaultHigh), + replyButton.widthAnchor.constraint(equalTo: favoriteButton.widthAnchor).priority(.defaultHigh), ]) moreButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) moreButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) @@ -140,16 +144,16 @@ extension ActionToolbarContainer { button.contentHorizontalAlignment = .center } replyButton.setImage(replyImage, for: .normal) - retootButton.setImage(reblogImage, for: .normal) - starButton.setImage(starImage, for: .normal) + boostButton.setImage(reblogImage, for: .normal) + favoriteButton.setImage(starImage, for: .normal) container.axis = .horizontal container.spacing = 8 container.distribution = .fillEqually container.addArrangedSubview(replyButton) - container.addArrangedSubview(retootButton) - container.addArrangedSubview(starButton) + container.addArrangedSubview(boostButton) + container.addArrangedSubview(favoriteButton) } } @@ -158,11 +162,18 @@ extension ActionToolbarContainer { return oldStyle != style } - private func isStarButtonHighlightStateDidChange(to isHighlight: Bool) { + private func isBoostButtonHighlightStateDidChange(to isHighlight: Bool) { + let tintColor = isHighlight ? Asset.Colors.systemGreen.color : Asset.Colors.Button.actionToolbar.color + boostButton.tintColor = tintColor + boostButton.setTitleColor(tintColor, for: .normal) + boostButton.setTitleColor(tintColor, for: .highlighted) + } + + private func isFavoriteButtonHighlightStateDidChange(to isHighlight: Bool) { let tintColor = isHighlight ? Asset.Colors.systemOrange.color : Asset.Colors.Button.actionToolbar.color - starButton.tintColor = tintColor - starButton.setTitleColor(tintColor, for: .normal) - starButton.setTitleColor(tintColor, for: .highlighted) + favoriteButton.tintColor = tintColor + favoriteButton.setTitleColor(tintColor, for: .normal) + favoriteButton.setTitleColor(tintColor, for: .highlighted) } } @@ -173,12 +184,12 @@ extension ActionToolbarContainer { delegate?.actionToolbarContainer(self, replayButtonDidPressed: sender) } - @objc private func retootButtonDidPressed(_ sender: UIButton) { + @objc private func boostButtonDidPressed(_ sender: UIButton) { os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - delegate?.actionToolbarContainer(self, retootButtonDidPressed: sender) + delegate?.actionToolbarContainer(self, boostButtonDidPressed: sender) } - @objc private func starButtonDidPressed(_ sender: UIButton) { + @objc private func favoriteButtonDidPressed(_ sender: UIButton) { os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) delegate?.actionToolbarContainer(self, starButtonDidPressed: sender) } diff --git a/Mastodon/Service/APIService/APIService+Favorite.swift b/Mastodon/Service/APIService/APIService+Favorite.swift index e1d5febe7..af8f0ffa7 100644 --- a/Mastodon/Service/APIService/APIService+Favorite.swift +++ b/Mastodon/Service/APIService/APIService+Favorite.swift @@ -78,7 +78,7 @@ extension APIService { }() let _oldToot: Toot? = { let request = Toot.sortedFetchRequest - request.predicate = Toot.predicate(domain: mastodonAuthenticationBox.domain, id: entity.id) + request.predicate = Toot.predicate(domain: mastodonAuthenticationBox.domain, id: statusID) request.returnsObjectsAsFaults = false request.relationshipKeyPathsForPrefetching = [#keyPath(Toot.reblog)] do { @@ -112,7 +112,8 @@ extension APIService { .handleEvents(receiveCompletion: { completion in switch completion { case .failure(let error): - print(error) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: error:", ((#file as NSString).lastPathComponent), #line, #function) + debugPrint(error) case .finished: break } diff --git a/Mastodon/Service/APIService/APIService+Reblog.swift b/Mastodon/Service/APIService/APIService+Reblog.swift new file mode 100644 index 000000000..ca47ec713 --- /dev/null +++ b/Mastodon/Service/APIService/APIService+Reblog.swift @@ -0,0 +1,169 @@ +// +// APIService+Reblog.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-9. +// + +import Foundation +import Combine +import MastodonSDK +import CoreData +import CoreDataStack +import CommonOSLog + +extension APIService { + + // make local state change only + func boost( + tootObjectID: NSManagedObjectID, + mastodonUserObjectID: NSManagedObjectID, + boostKind: Mastodon.API.Status.Reblog.BoostKind + ) -> AnyPublisher { + var _targetTootID: Toot.ID? + let managedObjectContext = backgroundManagedObjectContext + return managedObjectContext.performChanges { + let toot = managedObjectContext.object(with: tootObjectID) as! Toot + let mastodonUser = managedObjectContext.object(with: mastodonUserObjectID) as! MastodonUser + let targetToot = toot.reblog ?? toot + let targetTootID = targetToot.id + _targetTootID = targetTootID + + targetToot.update(reblogged: boostKind == .boost, mastodonUser: mastodonUser) + + } + .tryMap { result in + switch result { + case .success: + guard let targetTootID = _targetTootID else { + throw APIError.implicit(.badRequest) + } + return targetTootID + + case .failure(let error): + assertionFailure(error.localizedDescription) + throw error + } + } + .eraseToAnyPublisher() + } + + // send boost request to remote + func boost( + statusID: Mastodon.Entity.Status.ID, + boostKind: Mastodon.API.Status.Reblog.BoostKind, + mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + ) -> AnyPublisher, Error> { + let domain = mastodonAuthenticationBox.domain + let authorization = mastodonAuthenticationBox.userAuthorization + let requestMastodonUserID = mastodonAuthenticationBox.userID + return Mastodon.API.Status.Reblog.boost( + session: session, + domain: domain, + statusID: statusID, + boostKind: boostKind, + authorization: authorization + ) + .map { response -> AnyPublisher, Error> in + let log = OSLog.api + let entity = response.value + let managedObjectContext = self.backgroundManagedObjectContext + + return managedObjectContext.performChanges { + let _requestMastodonUser: MastodonUser? = { + let request = MastodonUser.sortedFetchRequest + request.predicate = MastodonUser.predicate(domain: mastodonAuthenticationBox.domain, id: requestMastodonUserID) + request.fetchLimit = 1 + request.returnsObjectsAsFaults = false + do { + return try managedObjectContext.fetch(request).first + } catch { + assertionFailure(error.localizedDescription) + return nil + } + }() + let _oldToot: Toot? = { + let request = Toot.sortedFetchRequest + request.predicate = Toot.predicate(domain: domain, id: statusID) + request.returnsObjectsAsFaults = false + request.relationshipKeyPathsForPrefetching = [#keyPath(Toot.reblog)] + do { + return try managedObjectContext.fetch(request).first + } catch { + assertionFailure(error.localizedDescription) + return nil + } + }() + + guard let requestMastodonUser = _requestMastodonUser, + let oldToot = _oldToot else { + assertionFailure() + return + } + APIService.CoreData.merge(toot: oldToot, entity: entity.reblog ?? entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate) + os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update toot %{public}s reblog status to: %{public}s. now %ld boosts", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.reblogged.flatMap { $0 ? "boost" : "unboost" } ?? "", entity.reblogsCount ) + } + .setFailureType(to: Error.self) + .tryMap { result -> Mastodon.Response.Content in + switch result { + case .success: + return response + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + } + .switchToLatest() + .handleEvents(receiveCompletion: { completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: error:", ((#file as NSString).lastPathComponent), #line, #function) + debugPrint(error) + case .finished: + break + } + }) + .eraseToAnyPublisher() + } + +} + +extension APIService { +// func likeList( +// limit: Int = onceRequestTootMaxCount, +// userID: String, +// maxID: String? = nil, +// mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox +// ) -> AnyPublisher, Error> { +// +// let requestMastodonUserID = mastodonAuthenticationBox.userID +// let query = Mastodon.API.Favorites.ListQuery(limit: limit, minID: nil, maxID: maxID) +// return Mastodon.API.Favorites.favoritedStatus(domain: mastodonAuthenticationBox.domain, session: session, authorization: mastodonAuthenticationBox.userAuthorization, query: query) +// .map { response -> AnyPublisher, Error> in +// let log = OSLog.api +// +// return APIService.Persist.persistTimeline( +// managedObjectContext: self.backgroundManagedObjectContext, +// domain: mastodonAuthenticationBox.domain, +// query: query, +// response: response, +// persistType: .likeList, +// requestMastodonUserID: requestMastodonUserID, +// log: log +// ) +// .setFailureType(to: Error.self) +// .tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Status]> in +// switch result { +// case .success: +// return response +// case .failure(let error): +// throw error +// } +// } +// .eraseToAnyPublisher() +// } +// .switchToLatest() +// .eraseToAnyPublisher() +// } +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Favorites.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Favorites.swift index ce77a51d9..3b01c2c13 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Favorites.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Favorites.swift @@ -114,14 +114,14 @@ extension Mastodon.API.Favorites { } -public extension Mastodon.API.Favorites { +extension Mastodon.API.Favorites { - enum FavoriteKind { + public enum FavoriteKind { case create case destroy } - struct ListQuery: GetQuery,TimelineQueryType { + public struct ListQuery: GetQuery,TimelineQueryType { public var limit: Int? public var minID: String? diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Status+Reblog.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Status+Reblog.swift new file mode 100644 index 000000000..bc898de9a --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Status+Reblog.swift @@ -0,0 +1,186 @@ +// +// Mastodon+API+Status+Reblog.swift +// +// +// Created by MainasuK Cirno on 2021-3-9. +// + +import Foundation +import Combine + +extension Mastodon.API.Status.Reblog { + + static func boostedByEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL { + let pathComponent = "statuses/" + statusID + "/reblogged_by" + return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent) + } + + /// Boosted by + /// + /// View who boosted a given status. + /// + /// - Since: 0.0.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2021/3/9 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/statuses/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - statusID: id for status + /// - authorization: User token. Could be nil if status is public + /// - Returns: `AnyPublisher` contains `Status` nested in the response + public static func poll( + session: URLSession, + domain: String, + statusID: Mastodon.Entity.Status.ID, + authorization: Mastodon.API.OAuth.Authorization? + ) -> AnyPublisher, Error> { + let request = Mastodon.API.get( + url: boostedByEndpointURL(domain: domain, statusID: statusID), + query: nil, + authorization: authorization + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: Mastodon.Entity.Status.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } + +} + +extension Mastodon.API.Status.Reblog { + + static func reblogEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL { + let pathComponent = "statuses/" + statusID + "/reblog" + return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent) + } + + /// Boost + /// + /// Reshare a status. + /// + /// - Since: 0.0.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2021/3/9 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/statuses/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - statusID: id for status + /// - authorization: User token. + /// - Returns: `AnyPublisher` contains `Status` nested in the response + public static func boost( + session: URLSession, + domain: String, + statusID: Mastodon.Entity.Status.ID, + query: BoostQuery, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + let request = Mastodon.API.post( + url: reblogEndpointURL(domain: domain, statusID: statusID), + query: nil, + authorization: authorization + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: Mastodon.Entity.Status.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } + + public typealias Visibility = Mastodon.Entity.Source.Privacy + + public struct BoostQuery: Codable, PostQuery { + public let visibility: Visibility + + public init(visibility: Visibility) { + self.visibility = visibility + } + } + +} + +extension Mastodon.API.Status.Reblog { + + static func unreblogEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL { + let pathComponent = "statuses/" + statusID + "/unreblog" + return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent) + } + + /// Undo boost + /// + /// Undo a reshare of a status. + /// + /// - Since: 0.0.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2021/3/9 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/statuses/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - statusID: id for status + /// - authorization: User token. + /// - Returns: `AnyPublisher` contains `Status` nested in the response + public static func undoBoost( + session: URLSession, + domain: String, + statusID: Mastodon.Entity.Status.ID, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + let request = Mastodon.API.post( + url: unreblogEndpointURL(domain: domain, statusID: statusID), + query: nil, + authorization: authorization + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: Mastodon.Entity.Status.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } + +} + +extension Mastodon.API.Status.Reblog { + + public enum BoostKind { + case boost + case undoBoost + } + + public static func boost( + session: URLSession, + domain: String, + statusID: Mastodon.Entity.Status.ID, + boostKind: BoostKind, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + let url: URL + switch boostKind { + case .boost: url = reblogEndpointURL(domain: domain, statusID: statusID) + case .undoBoost: url = unreblogEndpointURL(domain: domain, statusID: statusID) + } + let request = Mastodon.API.post( + url: url, + query: nil, + authorization: authorization + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: Mastodon.Entity.Status.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } + +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Status.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Status.swift new file mode 100644 index 000000000..fd3d96277 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Status.swift @@ -0,0 +1,12 @@ +// +// Mastodon+API+Status.swift +// +// +// Created by MainasuK Cirno on 2021-3-9. +// + +import Foundation + +extension Mastodon.API.Status { + public enum Reblog { } +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index 5a55ee103..8dbb2c3c7 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -95,6 +95,7 @@ extension Mastodon.API { public enum OAuth { } public enum Onboarding { } public enum Polls { } + public enum Status { } public enum Timeline { } public enum Favorites { } } From 51b6455c3783cce7b17827dbc8dd1c07b1abdfa3 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 9 Mar 2021 19:39:44 +0800 Subject: [PATCH 02/31] chore: rename reblog API --- .../Protocol/StatusProvider/StatusProviderFacade.swift | 6 +++--- Mastodon/Service/APIService/APIService+Reblog.swift | 6 +++--- ...+API+Status+Reblog.swift => Mastodon+API+Reblog.swift} | 8 ++++---- .../Sources/MastodonSDK/API/Mastodon+API+Status.swift | 4 +--- MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift | 1 + 5 files changed, 12 insertions(+), 13 deletions(-) rename MastodonSDK/Sources/MastodonSDK/API/{Mastodon+API+Status+Reblog.swift => Mastodon+API+Reblog.swift} (97%) diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift index cc5589978..9d1e33219 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift @@ -160,15 +160,15 @@ extension StatusProviderFacade { let responseFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium) toot - .compactMap { toot -> (NSManagedObjectID, Mastodon.API.Status.Reblog.BoostKind)? in + .compactMap { toot -> (NSManagedObjectID, Mastodon.API.Reblog.BoostKind)? in guard let toot = toot?.reblog ?? toot else { return nil } - let boostKind: Mastodon.API.Status.Reblog.BoostKind = { + let boostKind: Mastodon.API.Reblog.BoostKind = { let isBoosted = toot.rebloggedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false return isBoosted ? .undoBoost : .boost }() return (toot.objectID, boostKind) } - .map { tootObjectID, boostKind -> AnyPublisher<(Toot.ID, Mastodon.API.Status.Reblog.BoostKind), Error> in + .map { tootObjectID, boostKind -> AnyPublisher<(Toot.ID, Mastodon.API.Reblog.BoostKind), Error> in return context.apiService.boost( tootObjectID: tootObjectID, mastodonUserObjectID: mastodonUserObjectID, diff --git a/Mastodon/Service/APIService/APIService+Reblog.swift b/Mastodon/Service/APIService/APIService+Reblog.swift index ca47ec713..796a7817a 100644 --- a/Mastodon/Service/APIService/APIService+Reblog.swift +++ b/Mastodon/Service/APIService/APIService+Reblog.swift @@ -18,7 +18,7 @@ extension APIService { func boost( tootObjectID: NSManagedObjectID, mastodonUserObjectID: NSManagedObjectID, - boostKind: Mastodon.API.Status.Reblog.BoostKind + boostKind: Mastodon.API.Reblog.BoostKind ) -> AnyPublisher { var _targetTootID: Toot.ID? let managedObjectContext = backgroundManagedObjectContext @@ -51,13 +51,13 @@ extension APIService { // send boost request to remote func boost( statusID: Mastodon.Entity.Status.ID, - boostKind: Mastodon.API.Status.Reblog.BoostKind, + boostKind: Mastodon.API.Reblog.BoostKind, mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox ) -> AnyPublisher, Error> { let domain = mastodonAuthenticationBox.domain let authorization = mastodonAuthenticationBox.userAuthorization let requestMastodonUserID = mastodonAuthenticationBox.userID - return Mastodon.API.Status.Reblog.boost( + return Mastodon.API.Reblog.boost( session: session, domain: domain, statusID: statusID, diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Status+Reblog.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Reblog.swift similarity index 97% rename from MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Status+Reblog.swift rename to MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Reblog.swift index bc898de9a..862028d33 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Status+Reblog.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Reblog.swift @@ -8,7 +8,7 @@ import Foundation import Combine -extension Mastodon.API.Status.Reblog { +extension Mastodon.API.Reblog { static func boostedByEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL { let pathComponent = "statuses/" + statusID + "/reblogged_by" @@ -52,7 +52,7 @@ extension Mastodon.API.Status.Reblog { } -extension Mastodon.API.Status.Reblog { +extension Mastodon.API.Reblog { static func reblogEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL { let pathComponent = "statuses/" + statusID + "/reblog" @@ -107,7 +107,7 @@ extension Mastodon.API.Status.Reblog { } -extension Mastodon.API.Status.Reblog { +extension Mastodon.API.Reblog { static func unreblogEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL { let pathComponent = "statuses/" + statusID + "/unreblog" @@ -151,7 +151,7 @@ extension Mastodon.API.Status.Reblog { } -extension Mastodon.API.Status.Reblog { +extension Mastodon.API.Reblog { public enum BoostKind { case boost diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Status.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Status.swift index fd3d96277..09dd07c4d 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Status.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Status.swift @@ -7,6 +7,4 @@ import Foundation -extension Mastodon.API.Status { - public enum Reblog { } -} +extension Mastodon.API.Status { } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index 8dbb2c3c7..8431287f8 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -95,6 +95,7 @@ extension Mastodon.API { public enum OAuth { } public enum Onboarding { } public enum Polls { } + public enum Reblog { } public enum Status { } public enum Timeline { } public enum Favorites { } From 81c22fee24dfdfb303579f10392f6e3fed7385e0 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 9 Mar 2021 19:41:18 +0800 Subject: [PATCH 03/31] fix: count not change after undo boost and undo favorite issue --- Mastodon/Service/APIService/APIService+Favorite.swift | 3 +++ Mastodon/Service/APIService/APIService+Reblog.swift | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Mastodon/Service/APIService/APIService+Favorite.swift b/Mastodon/Service/APIService/APIService+Favorite.swift index af8f0ffa7..98b00cf18 100644 --- a/Mastodon/Service/APIService/APIService+Favorite.swift +++ b/Mastodon/Service/APIService/APIService+Favorite.swift @@ -95,6 +95,9 @@ extension APIService { return } APIService.CoreData.merge(toot: oldToot, entity: entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate) + if favoriteKind == .destroy { + oldToot.update(favouritesCount: NSNumber(value: max(0, oldToot.favouritesCount.intValue - 1))) + } os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update toot %{public}s like status to: %{public}s. now %ld likes", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.favourited.flatMap { $0 ? "like" : "unlike" } ?? "", entity.favouritesCount ) } .setFailureType(to: Error.self) diff --git a/Mastodon/Service/APIService/APIService+Reblog.swift b/Mastodon/Service/APIService/APIService+Reblog.swift index 796a7817a..cfec5c7f4 100644 --- a/Mastodon/Service/APIService/APIService+Reblog.swift +++ b/Mastodon/Service/APIService/APIService+Reblog.swift @@ -101,6 +101,9 @@ extension APIService { return } APIService.CoreData.merge(toot: oldToot, entity: entity.reblog ?? entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate) + if boostKind == .undoBoost { + oldToot.update(reblogsCount: NSNumber(value: max(0, oldToot.reblogsCount.intValue - 1))) + } os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update toot %{public}s reblog status to: %{public}s. now %ld boosts", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.reblogged.flatMap { $0 ? "boost" : "unboost" } ?? "", entity.reblogsCount ) } .setFailureType(to: Error.self) From 1256ef1d8e3a2d43c6c64371790868a6316a467c Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 10 Mar 2021 13:36:01 +0800 Subject: [PATCH 04/31] feat: implement boost toot. Add stacked style avatar --- Mastodon.xcodeproj/project.pbxproj | 4 + .../Diffiable/Section/StatusSection.swift | 14 +- .../Protocol/AvatarConfigurableView.swift | 13 +- ...meTimelineViewController+DebugAction.swift | 49 +++++ .../Container/AvatarStackContainerView.swift | 193 ++++++++++++++++++ .../Scene/Share/View/Content/StatusView.swift | 55 +++++ 6 files changed, 323 insertions(+), 5 deletions(-) create mode 100644 Mastodon/Scene/Share/View/Container/AvatarStackContainerView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 081fb06ee..d6d3afb00 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -149,6 +149,7 @@ DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; }; DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A06225E905E000CFDF14 /* UIApplication.swift */; }; DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; }; + DB71FD2C25F86A5100512AE1 /* AvatarStackContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerView.swift */; }; DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; }; DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; }; DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; }; @@ -396,6 +397,7 @@ DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; DB68A06225E905E000CFDF14 /* UIApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = ""; }; DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = ""; }; + DB71FD2B25F86A5100512AE1 /* AvatarStackContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerView.swift; sourceTree = ""; }; DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = ""; }; DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = ""; }; DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1168,6 +1170,7 @@ children = ( DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */, 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */, + DB71FD2B25F86A5100512AE1 /* AvatarStackContainerView.swift */, ); path = Container; sourceTree = ""; @@ -1684,6 +1687,7 @@ DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */, DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */, 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */, + DB71FD2C25F86A5100512AE1 /* AvatarStackContainerView.swift in Sources */, 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */, DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */, 0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */, diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 216d778ad..9d4fcad92 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -86,13 +86,23 @@ extension StatusSection { return L10n.Common.Controls.Status.userBoosted(name) }() - // set name username avatar + // set name username cell.statusView.nameLabel.text = { let author = (toot.reblog ?? toot).author return author.displayName.isEmpty ? author.username : author.displayName }() cell.statusView.usernameLabel.text = "@" + (toot.reblog ?? toot).author.acct - cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: (toot.reblog ?? toot).author.avatarImageURL())) + // set avatar + if let reblog = toot.reblog { + cell.statusView.avatarButton.isHidden = true + cell.statusView.avatarStackedContainerButton.isHidden = false + cell.statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: reblog.author.avatarImageURL())) + cell.statusView.avatarStackedContainerButton.bottomTrailingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: toot.author.avatarImageURL())) + } else { + cell.statusView.avatarButton.isHidden = false + cell.statusView.avatarStackedContainerButton.isHidden = true + cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: (toot.reblog ?? toot).author.avatarImageURL())) + } // set text cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content) diff --git a/Mastodon/Protocol/AvatarConfigurableView.swift b/Mastodon/Protocol/AvatarConfigurableView.swift index 6c51d576c..6391066e1 100644 --- a/Mastodon/Protocol/AvatarConfigurableView.swift +++ b/Mastodon/Protocol/AvatarConfigurableView.swift @@ -23,7 +23,13 @@ extension AvatarConfigurableView { public func configure(with configuration: AvatarConfigurableViewConfiguration) { let placeholderImage: UIImage = { let placeholderImage = configuration.placeholderImage ?? UIImage.placeholder(size: Self.configurableAvatarImageSize, color: .systemFill) - return placeholderImage.af.imageRoundedIntoCircle() + if Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 { + return placeholderImage + .af.imageAspectScaled(toFill: Self.configurableAvatarImageSize) + .af.imageRounded(withCornerRadius: 4, divideRadiusByImageScale: true) + } else { + return placeholderImage.af.imageRoundedIntoCircle() + } }() // cancel previous task @@ -65,7 +71,8 @@ extension AvatarConfigurableView { ) avatarImageView.layer.masksToBounds = true avatarImageView.layer.cornerRadius = Self.configurableAvatarImageCornerRadius - avatarImageView.layer.cornerCurve = .circular + avatarImageView.layer.cornerCurve = Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 ? .continuous :.circular + default: let filter = ScaledToSizeWithRoundedCornersFilter(size: Self.configurableAvatarImageSize, radius: Self.configurableAvatarImageCornerRadius) avatarImageView.af.setImage( @@ -92,7 +99,7 @@ extension AvatarConfigurableView { ) avatarButton.layer.masksToBounds = true avatarButton.layer.cornerRadius = Self.configurableAvatarImageCornerRadius - avatarButton.layer.cornerCurve = .continuous + avatarButton.layer.cornerCurve = Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 ? .continuous : .circular default: let filter = ScaledToSizeWithRoundedCornersFilter(size: Self.configurableAvatarImageSize, radius: Self.configurableAvatarImageCornerRadius) avatarButton.af.setImage( diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index 0937e1fb4..9f2b4e720 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -45,10 +45,18 @@ extension HomeTimelineViewController { guard let self = self else { return } self.moveToTopGapAction(action) }), + UIAction(title: "First Reblog Toot", image: nil, attributes: [], handler: { [weak self] action in + guard let self = self else { return } + self.moveToFirstReblogToot(action) + }), UIAction(title: "First Poll Toot", image: nil, attributes: [], handler: { [weak self] action in guard let self = self else { return } self.moveToFirstPollToot(action) }), + UIAction(title: "First Audio Toot", image: nil, attributes: [], handler: { [weak self] action in + guard let self = self else { return } + self.moveToFirstAudioToot(action) + }), // UIAction(title: "First Reply Toot", image: nil, attributes: [], handler: { [weak self] action in // guard let self = self else { return } // self.moveToFirstReplyToot(action) @@ -101,6 +109,26 @@ extension HomeTimelineViewController { } } + @objc private func moveToFirstReblogToot(_ sender: UIAction) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + let snapshotTransitioning = diffableDataSource.snapshot() + let item = snapshotTransitioning.itemIdentifiers.first(where: { item in + switch item { + case .homeTimelineIndex(let objectID, _): + let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex + return homeTimelineIndex.toot.reblog != nil + default: + return false + } + }) + if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) { + tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) + tableView.blinkRow(at: IndexPath(row: index, section: 0)) + } else { + print("Not found reblog toot") + } + } + @objc private func moveToFirstPollToot(_ sender: UIAction) { guard let diffableDataSource = viewModel.diffableDataSource else { return } let snapshotTransitioning = diffableDataSource.snapshot() @@ -122,6 +150,27 @@ extension HomeTimelineViewController { } } + @objc private func moveToFirstAudioToot(_ sender: UIAction) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + let snapshotTransitioning = diffableDataSource.snapshot() + let item = snapshotTransitioning.itemIdentifiers.first(where: { item in + switch item { + case .homeTimelineIndex(let objectID, _): + let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex + let toot = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot + return toot.mediaAttachments?.contains(where: { $0.type == .audio }) ?? false + default: + return false + } + }) + if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) { + tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) + tableView.blinkRow(at: IndexPath(row: index, section: 0)) + } else { + print("Not found audio toot") + } + } + @objc private func dropRecentTootsAction(_ sender: UIAction, count: Int) { guard let diffableDataSource = viewModel.diffableDataSource else { return } let snapshotTransitioning = diffableDataSource.snapshot() diff --git a/Mastodon/Scene/Share/View/Container/AvatarStackContainerView.swift b/Mastodon/Scene/Share/View/Container/AvatarStackContainerView.swift new file mode 100644 index 000000000..f2f216059 --- /dev/null +++ b/Mastodon/Scene/Share/View/Container/AvatarStackContainerView.swift @@ -0,0 +1,193 @@ +// +// AvatarStackContainerView.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-10. +// + +import UIKit + +final class AvatarStackedImageView: UIImageView { } + +// MARK: - AvatarConfigurableView +extension AvatarStackedImageView: AvatarConfigurableView { + static var configurableAvatarImageSize: CGSize { CGSize(width: 28, height: 28) } + static var configurableAvatarImageCornerRadius: CGFloat { 4 } + var configurableAvatarImageView: UIImageView? { self } + var configurableAvatarButton: UIButton? { nil } +} + +import os.log +import UIKit + +extension UIControl.State: Hashable { } + +final class AvatarStackContainerButton: UIControl { + + static let containerSize = CGSize(width: 42, height: 42) + static let maskOffset: CGFloat = 2 + + // UIControl.Event - Application: 0x0F000000 + static let primaryAction = UIControl.Event(rawValue: 1 << 25) // 0x01000000 + var primaryActionState: UIControl.State = .normal + + let topLeadingAvatarStackedImageView = AvatarStackedImageView() + let bottomTrailingAvatarStackedImageView = AvatarStackedImageView() + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension AvatarStackContainerButton { + + private func _init() { + topLeadingAvatarStackedImageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(topLeadingAvatarStackedImageView) + NSLayoutConstraint.activate([ + topLeadingAvatarStackedImageView.topAnchor.constraint(equalTo: topAnchor), + topLeadingAvatarStackedImageView.leadingAnchor.constraint(equalTo: leadingAnchor), + topLeadingAvatarStackedImageView.widthAnchor.constraint(equalToConstant: AvatarStackedImageView.configurableAvatarImageSize.width).priority(.defaultHigh), + topLeadingAvatarStackedImageView.heightAnchor.constraint(equalToConstant: AvatarStackedImageView.configurableAvatarImageSize.height).priority(.defaultHigh), + ]) + + bottomTrailingAvatarStackedImageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(bottomTrailingAvatarStackedImageView) + NSLayoutConstraint.activate([ + bottomTrailingAvatarStackedImageView.bottomAnchor.constraint(equalTo: bottomAnchor), + bottomTrailingAvatarStackedImageView.trailingAnchor.constraint(equalTo: trailingAnchor), + bottomTrailingAvatarStackedImageView.widthAnchor.constraint(equalToConstant: AvatarStackedImageView.configurableAvatarImageSize.width).priority(.defaultHigh), + bottomTrailingAvatarStackedImageView.heightAnchor.constraint(equalToConstant: AvatarStackedImageView.configurableAvatarImageSize.height).priority(.defaultHigh), + ]) + + // mask topLeadingAvatarStackedImageView + let offset: CGFloat = 2 + let path: CGPath = { + let path = CGMutablePath() + path.addRect(CGRect(origin: .zero, size: AvatarStackContainerButton.containerSize)) + if UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft { + path.addPath(UIBezierPath( + roundedRect: CGRect( + x: AvatarStackedImageView.configurableAvatarImageSize.width + offset, + y: AvatarStackContainerButton.containerSize.height - AvatarStackedImageView.configurableAvatarImageSize.height - offset, + width: AvatarStackedImageView.configurableAvatarImageSize.width, + height: AvatarStackedImageView.configurableAvatarImageSize.height + ), + cornerRadius: AvatarStackedImageView.configurableAvatarImageCornerRadius + ).cgPath) + } else { + path.addPath(UIBezierPath( + roundedRect: CGRect( + x: AvatarStackContainerButton.containerSize.width - AvatarStackedImageView.configurableAvatarImageSize.width - offset, + y: AvatarStackContainerButton.containerSize.height - AvatarStackedImageView.configurableAvatarImageSize.height - offset, + width: AvatarStackedImageView.configurableAvatarImageSize.width, + height: AvatarStackedImageView.configurableAvatarImageSize.height + ), + cornerRadius: AvatarStackedImageView.configurableAvatarImageCornerRadius + ).cgPath) + } + return path + }() + let maskShapeLayer = CAShapeLayer() + maskShapeLayer.backgroundColor = UIColor.black.cgColor + maskShapeLayer.fillRule = .evenOdd + maskShapeLayer.path = path + topLeadingAvatarStackedImageView.layer.mask = maskShapeLayer + + topLeadingAvatarStackedImageView.image = UIImage.placeholder(color: .systemFill) + bottomTrailingAvatarStackedImageView.image = UIImage.placeholder(color: .systemFill) + } + + override var intrinsicContentSize: CGSize { + return AvatarStackContainerButton.containerSize + } + + override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + defer { updateAppearance() } + + updateState(touch: touch, event: event) + return super.beginTracking(touch, with: event) + } + + override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + defer { updateAppearance() } + + updateState(touch: touch, event: event) + return super.continueTracking(touch, with: event) + } + + override func endTracking(_ touch: UITouch?, with event: UIEvent?) { + defer { updateAppearance() } + resetState() + + if let touch = touch { + if AvatarStackContainerButton.isTouching(touch, view: self, event: event) { + sendActions(for: AvatarStackContainerButton.primaryAction) + } else { + // do nothing + } + } + + super.endTracking(touch, with: event) + } + + override func cancelTracking(with event: UIEvent?) { + defer { updateAppearance() } + + resetState() + super.cancelTracking(with: event) + } + +} + +extension AvatarStackContainerButton { + + private func updateAppearance() { + topLeadingAvatarStackedImageView.alpha = primaryActionState.contains(.highlighted) ? 0.6 : 1.0 + bottomTrailingAvatarStackedImageView.alpha = primaryActionState.contains(.highlighted) ? 0.6 : 1.0 + } + + private static func isTouching(_ touch: UITouch, view: UIView, event: UIEvent?) -> Bool { + let location = touch.location(in: view) + return view.point(inside: location, with: event) + } + + private func resetState() { + primaryActionState = .normal + } + + private func updateState(touch: UITouch, event: UIEvent?) { + primaryActionState = AvatarStackContainerButton.isTouching(touch, view: self, event: event) ? .highlighted : .normal + } + +} + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct AvatarStackContainerButton_Previews: PreviewProvider { + + static var previews: some View { + UIViewPreview(width: 42) { + let avatarStackContainerButton = AvatarStackContainerButton() + avatarStackContainerButton.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + avatarStackContainerButton.widthAnchor.constraint(equalToConstant: 42), + avatarStackContainerButton.heightAnchor.constraint(equalToConstant: 42), + ]) + return avatarStackContainerButton + } + .previewLayout(.fixed(width: 42, height: 42)) + } + +} + +#endif + diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 2713647fe..5db0e11ee 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -60,6 +60,7 @@ final class StatusView: UIView { button.setImage(placeholderImage, for: .normal) return button }() + let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton() let nameLabel: UILabel = { let label = UILabel() @@ -238,6 +239,14 @@ extension StatusView { avatarButton.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor), avatarButton.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor), ]) + avatarStackedContainerButton.translatesAutoresizingMaskIntoConstraints = false + avatarView.addSubview(avatarStackedContainerButton) + NSLayoutConstraint.activate([ + avatarStackedContainerButton.topAnchor.constraint(equalTo: avatarView.topAnchor), + avatarStackedContainerButton.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor), + avatarStackedContainerButton.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor), + avatarStackedContainerButton.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor), + ]) // author meta container: [title container | subtitle container] let authorMetaContainerStackView = UIStackView() @@ -360,6 +369,7 @@ extension StatusView { pollStatusStackView.isHidden = true audioView.isHidden = true + avatarStackedContainerButton.isHidden = true contentWarningBlurContentImageView.isHidden = true statusContentWarningContainerStackView.isHidden = true statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false @@ -429,6 +439,7 @@ import SwiftUI struct StatusView_Previews: PreviewProvider { static let avatarFlora = UIImage(named: "tiraya-adam") + static let avatarMarkus = UIImage(named: "markus-spiske") static var previews: some View { Group { @@ -443,6 +454,49 @@ struct StatusView_Previews: PreviewProvider { return statusView } .previewLayout(.fixed(width: 375, height: 200)) + .previewDisplayName("Normal") + UIViewPreview(width: 375) { + let statusView = StatusView() + statusView.headerContainerStackView.isHidden = false + statusView.avatarButton.isHidden = true + statusView.avatarStackedContainerButton.isHidden = false + statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure( + with: AvatarConfigurableViewConfiguration( + avatarImageURL: nil, + placeholderImage: avatarFlora + ) + ) + statusView.avatarStackedContainerButton.bottomTrailingAvatarStackedImageView.configure( + with: AvatarConfigurableViewConfiguration( + avatarImageURL: nil, + placeholderImage: avatarMarkus + ) + ) + return statusView + } + .previewLayout(.fixed(width: 375, height: 200)) + .previewDisplayName("Boost") + UIViewPreview(width: 375) { + let statusView = StatusView(frame: CGRect(x: 0, y: 0, width: 375, height: 500)) + statusView.configure( + with: AvatarConfigurableViewConfiguration( + avatarImageURL: nil, + placeholderImage: avatarFlora + ) + ) + statusView.headerContainerStackView.isHidden = false + let images = MosaicImageView_Previews.images + let imageViews = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxHeight: 162) + for (i, imageView) in imageViews.enumerated() { + imageView.image = images[i] + } + statusView.statusMosaicImageViewContainer.isHidden = false + statusView.statusMosaicImageViewContainer.blurVisualEffectView.isHidden = true + statusView.isStatusTextSensitive = false + return statusView + } + .previewLayout(.fixed(width: 375, height: 380)) + .previewDisplayName("Image Meida") UIViewPreview(width: 375) { let statusView = StatusView(frame: CGRect(x: 0, y: 0, width: 375, height: 500)) statusView.configure( @@ -466,6 +520,7 @@ struct StatusView_Previews: PreviewProvider { return statusView } .previewLayout(.fixed(width: 375, height: 380)) + .previewDisplayName("Content Sensitive") } } From cd112c5102ba1ba26ae8f8229e91bc45b6830932 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 10 Mar 2021 14:06:08 +0800 Subject: [PATCH 05/31] fix: reblog avatar RTL support issue --- .../Container/AvatarStackContainerView.swift | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/Mastodon/Scene/Share/View/Container/AvatarStackContainerView.swift b/Mastodon/Scene/Share/View/Container/AvatarStackContainerView.swift index f2f216059..361f744bb 100644 --- a/Mastodon/Scene/Share/View/Container/AvatarStackContainerView.swift +++ b/Mastodon/Scene/Share/View/Container/AvatarStackContainerView.swift @@ -71,28 +71,17 @@ extension AvatarStackContainerButton { let offset: CGFloat = 2 let path: CGPath = { let path = CGMutablePath() - path.addRect(CGRect(origin: .zero, size: AvatarStackContainerButton.containerSize)) - if UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft { - path.addPath(UIBezierPath( - roundedRect: CGRect( - x: AvatarStackedImageView.configurableAvatarImageSize.width + offset, - y: AvatarStackContainerButton.containerSize.height - AvatarStackedImageView.configurableAvatarImageSize.height - offset, - width: AvatarStackedImageView.configurableAvatarImageSize.width, - height: AvatarStackedImageView.configurableAvatarImageSize.height - ), - cornerRadius: AvatarStackedImageView.configurableAvatarImageCornerRadius - ).cgPath) - } else { - path.addPath(UIBezierPath( - roundedRect: CGRect( - x: AvatarStackContainerButton.containerSize.width - AvatarStackedImageView.configurableAvatarImageSize.width - offset, - y: AvatarStackContainerButton.containerSize.height - AvatarStackedImageView.configurableAvatarImageSize.height - offset, - width: AvatarStackedImageView.configurableAvatarImageSize.width, - height: AvatarStackedImageView.configurableAvatarImageSize.height - ), - cornerRadius: AvatarStackedImageView.configurableAvatarImageCornerRadius - ).cgPath) - } + path.addRect(CGRect(origin: .zero, size: AvatarStackedImageView.configurableAvatarImageSize)) + let mirrorScale: CGFloat = UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft ? -1 : 1 + path.addPath(UIBezierPath( + roundedRect: CGRect( + x: mirrorScale * (AvatarStackContainerButton.containerSize.width - AvatarStackedImageView.configurableAvatarImageSize.width - offset), + y: AvatarStackContainerButton.containerSize.height - AvatarStackedImageView.configurableAvatarImageSize.height - offset, + width: AvatarStackedImageView.configurableAvatarImageSize.width, + height: AvatarStackedImageView.configurableAvatarImageSize.height + ), + cornerRadius: AvatarStackedImageView.configurableAvatarImageCornerRadius + ).cgPath) return path }() let maskShapeLayer = CAShapeLayer() From a5f2bf2334687f766613b1b8b21b3f9d52d07e4c Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 10 Mar 2021 14:09:38 +0800 Subject: [PATCH 06/31] chore: code cleanup --- .../Container/AvatarStackContainerView.swift | 5 +-- .../APIService/APIService+Reblog.swift | 39 ------------------- 2 files changed, 1 insertion(+), 43 deletions(-) diff --git a/Mastodon/Scene/Share/View/Container/AvatarStackContainerView.swift b/Mastodon/Scene/Share/View/Container/AvatarStackContainerView.swift index 361f744bb..ad828d9d2 100644 --- a/Mastodon/Scene/Share/View/Container/AvatarStackContainerView.swift +++ b/Mastodon/Scene/Share/View/Container/AvatarStackContainerView.swift @@ -5,8 +5,8 @@ // Created by MainasuK Cirno on 2021-3-10. // +import os.log import UIKit - final class AvatarStackedImageView: UIImageView { } // MARK: - AvatarConfigurableView @@ -17,9 +17,6 @@ extension AvatarStackedImageView: AvatarConfigurableView { var configurableAvatarButton: UIButton? { nil } } -import os.log -import UIKit - extension UIControl.State: Hashable { } final class AvatarStackContainerButton: UIControl { diff --git a/Mastodon/Service/APIService/APIService+Reblog.swift b/Mastodon/Service/APIService/APIService+Reblog.swift index cfec5c7f4..55b4699ae 100644 --- a/Mastodon/Service/APIService/APIService+Reblog.swift +++ b/Mastodon/Service/APIService/APIService+Reblog.swift @@ -131,42 +131,3 @@ extension APIService { } } - -extension APIService { -// func likeList( -// limit: Int = onceRequestTootMaxCount, -// userID: String, -// maxID: String? = nil, -// mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox -// ) -> AnyPublisher, Error> { -// -// let requestMastodonUserID = mastodonAuthenticationBox.userID -// let query = Mastodon.API.Favorites.ListQuery(limit: limit, minID: nil, maxID: maxID) -// return Mastodon.API.Favorites.favoritedStatus(domain: mastodonAuthenticationBox.domain, session: session, authorization: mastodonAuthenticationBox.userAuthorization, query: query) -// .map { response -> AnyPublisher, Error> in -// let log = OSLog.api -// -// return APIService.Persist.persistTimeline( -// managedObjectContext: self.backgroundManagedObjectContext, -// domain: mastodonAuthenticationBox.domain, -// query: query, -// response: response, -// persistType: .likeList, -// requestMastodonUserID: requestMastodonUserID, -// log: log -// ) -// .setFailureType(to: Error.self) -// .tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Status]> in -// switch result { -// case .success: -// return response -// case .failure(let error): -// throw error -// } -// } -// .eraseToAnyPublisher() -// } -// .switchToLatest() -// .eraseToAnyPublisher() -// } -} From 2657dde18496981d0ec3b6344e238aeb1d3d03c9 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Wed, 10 Mar 2021 21:19:56 +0800 Subject: [PATCH 07/31] chore: the play interrupts event could be sent with the notification --- Mastodon/Diffiable/Section/StatusSection.swift | 14 +++++++++++--- .../HomeTimelineViewController.swift | 7 +++++-- .../PublicTimelineViewController.swift | 10 +++++++++- .../ViewModel/AudioContainerViewModel.swift | 6 +----- .../Share/ViewModel/VideoPlayerViewModel.swift | 3 ++- Mastodon/Service/AudioPlayer.swift | 16 +++++++++++++++- Mastodon/Service/ViedeoPlaybackService.swift | 7 +++++++ 7 files changed, 50 insertions(+), 13 deletions(-) diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 1d0169ab8..53f7aec87 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -37,7 +37,11 @@ extension StatusSection { StatusSection.configure( cell: cell, dependency: dependency, - readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: timelineIndex.toot, requestUserID: timelineIndex.userID, statusItemAttribute: attribute + readableLayoutFrame: tableView.readableContentGuide.layoutFrame, + timestampUpdatePublisher: timestampUpdatePublisher, + toot: timelineIndex.toot, + requestUserID: timelineIndex.userID, + statusItemAttribute: attribute ) } cell.delegate = statusTableViewCellDelegate @@ -52,7 +56,11 @@ extension StatusSection { StatusSection.configure( cell: cell, dependency: dependency, - readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: toot, requestUserID: requestUserID, statusItemAttribute: attribute + readableLayoutFrame: tableView.readableContentGuide.layoutFrame, + timestampUpdatePublisher: timestampUpdatePublisher, + toot: toot, + requestUserID: requestUserID, + statusItemAttribute: attribute ) } cell.delegate = statusTableViewCellDelegate @@ -168,7 +176,7 @@ extension StatusSection { // set audio if let audioAttachment = mediaAttachments.filter({ $0.type == .audio }).first { cell.statusView.audioView.isHidden = false - AudioContainerViewModel.configure(cell: cell, audioAttachment: audioAttachment, videoPlaybackService: dependency.context.videoPlaybackService) + AudioContainerViewModel.configure(cell: cell, audioAttachment: audioAttachment ) } else { cell.statusView.audioView.isHidden = true } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index c9498dbea..01b860da8 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -141,7 +141,7 @@ extension HomeTimelineViewController { override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - + context.videoPlaybackService.viewDidDisappear(from: self) } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { @@ -236,7 +236,10 @@ extension HomeTimelineViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { handleTableView(tableView, willDisplay: cell, forRowAt: indexPath) } - + + func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { + handleTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath) + } } // MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift index 50d36d296..820094b1e 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift @@ -81,6 +81,10 @@ extension PublicTimelineViewController { ) } + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + context.videoPlaybackService.viewDidDisappear(from: self) + } } // MARK: - UIScrollViewDelegate @@ -103,6 +107,7 @@ extension PublicTimelineViewController { // MARK: - UITableViewDelegate extension PublicTimelineViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { + guard let diffableDataSource = viewModel.diffableDataSource else { return 100 } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return 100 } @@ -114,8 +119,11 @@ extension PublicTimelineViewController: UITableViewDelegate { } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {} - + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + handleTableView(tableView, willDisplay: cell, forRowAt: indexPath) + } func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { + handleTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath) guard let diffableDataSource = viewModel.diffableDataSource else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } diff --git a/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift b/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift index 7370c4fef..de804a475 100644 --- a/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift @@ -12,8 +12,7 @@ import UIKit class AudioContainerViewModel { static func configure( cell: StatusTableViewCell, - audioAttachment: Attachment, - videoPlaybackService: VideoPlaybackService + audioAttachment: Attachment ) { guard let duration = audioAttachment.meta?.original?.duration else { return } let audioView = cell.statusView.audioView @@ -26,15 +25,12 @@ class AudioContainerViewModel { AudioPlayer.shared.pause() } else { AudioPlayer.shared.resume() - videoPlaybackService.pauseWhenPlayAudio() } if AudioPlayer.shared.currentTimeSubject.value == 0 { AudioPlayer.shared.playAudio(audioAttachment: audioAttachment) - videoPlaybackService.pauseWhenPlayAudio() } } else { AudioPlayer.shared.playAudio(audioAttachment: audioAttachment) - videoPlaybackService.pauseWhenPlayAudio() } } .store(in: &cell.disposeBag) diff --git a/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift b/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift index e0a2f5ef4..c3f2cf369 100644 --- a/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift @@ -14,6 +14,7 @@ import UIKit final class VideoPlayerViewModel { var disposeBag = Set() + static let appWillPlayVideoNotification = NSNotification.Name(rawValue: "appWillPlayVideoNotification") // input let previewImageURL: URL? let videoURL: URL @@ -63,7 +64,7 @@ final class VideoPlayerViewModel { .sink { [weak self] timeControlStatus in guard let _ = self else { return } guard timeControlStatus == .playing else { return } - AudioPlayer.shared.pauseIfNeed() + NotificationCenter.default.post(name: VideoPlayerViewModel.appWillPlayVideoNotification, object: nil) switch videoKind { case .gif: break diff --git a/Mastodon/Service/AudioPlayer.swift b/Mastodon/Service/AudioPlayer.swift index 02c948c29..646207edb 100644 --- a/Mastodon/Service/AudioPlayer.swift +++ b/Mastodon/Service/AudioPlayer.swift @@ -12,6 +12,9 @@ import Foundation import UIKit final class AudioPlayer: NSObject { + + static let appWillPlayAudioNotification = NSNotification.Name(rawValue: "appWillPlayAudioNotification") + var disposeBag = Set() var player = AVPlayer() @@ -45,6 +48,7 @@ extension AudioPlayer { return } + pushWillPlayAudioNotification() if audioAttachment == attachment { if self.playbackState.value == .stopped { self.seekToTime(time: .zero) @@ -83,6 +87,12 @@ extension AudioPlayer { } } .store(in: &disposeBag) + NotificationCenter.default.publisher(for: VideoPlayerViewModel.appWillPlayVideoNotification) + .sink { [weak self] _ in + guard let self = self else { return } + self.pauseIfNeed() + } + .store(in: &disposeBag) timeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: CMTimeScale(NSEC_PER_SEC)), queue: DispatchQueue.main, using: { [weak self] time in guard let self = self else { return } @@ -119,10 +129,14 @@ extension AudioPlayer { .store(in: &disposeBag) } + func pushWillPlayAudioNotification() { + NotificationCenter.default.post(name: AudioPlayer.appWillPlayAudioNotification, object: nil) + } func isPlaying() -> Bool { - return self.playbackState.value == .readyToPlay || self.playbackState.value == .playing + return playbackState.value == .readyToPlay || playbackState.value == .playing } func resume() { + pushWillPlayAudioNotification() player.play() playbackState.value = .playing } diff --git a/Mastodon/Service/ViedeoPlaybackService.swift b/Mastodon/Service/ViedeoPlaybackService.swift index 724026fdd..49ac3b0df 100644 --- a/Mastodon/Service/ViedeoPlaybackService.swift +++ b/Mastodon/Service/ViedeoPlaybackService.swift @@ -90,6 +90,13 @@ extension VideoPlaybackService { self.playerViewModel(viewModel, didUpdateTimeControlStatus: timeControlStatus) } .store(in: &disposeBag) + + NotificationCenter.default.publisher(for: AudioPlayer.appWillPlayAudioNotification) + .sink { [weak self] _ in + guard let self = self else { return } + self.pauseWhenPlayAudio() + } + .store(in: &disposeBag) } } From 0be862c6b3b129190c18c4c23c4d311ae6cb1bdc Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 11 Mar 2021 11:19:07 +0800 Subject: [PATCH 08/31] chore: remove useless extension for UIControl.State. Correct AvatarStackContainerButton filename --- Mastodon.xcodeproj/project.pbxproj | 8 ++++---- .../AvatarStackContainerButton.swift} | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) rename Mastodon/Scene/Share/View/{Container/AvatarStackContainerView.swift => Control/AvatarStackContainerButton.swift} (98%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index d6d3afb00..439b8ba9c 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -149,7 +149,7 @@ DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; }; DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A06225E905E000CFDF14 /* UIApplication.swift */; }; DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; }; - DB71FD2C25F86A5100512AE1 /* AvatarStackContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerView.swift */; }; + DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; }; DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; }; DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; }; DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; }; @@ -397,7 +397,7 @@ DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; DB68A06225E905E000CFDF14 /* UIApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = ""; }; DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = ""; }; - DB71FD2B25F86A5100512AE1 /* AvatarStackContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerView.swift; sourceTree = ""; }; + DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerButton.swift; sourceTree = ""; }; DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = ""; }; DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = ""; }; DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1170,7 +1170,6 @@ children = ( DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */, 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */, - DB71FD2B25F86A5100512AE1 /* AvatarStackContainerView.swift */, ); path = Container; sourceTree = ""; @@ -1187,6 +1186,7 @@ DBA9B90325F1D4420012E7B6 /* Control */ = { isa = PBXGroup; children = ( + DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */, DB59F11725EFA35B001F1DAB /* StripProgressView.swift */, ); path = Control; @@ -1687,7 +1687,7 @@ DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */, DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */, 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */, - DB71FD2C25F86A5100512AE1 /* AvatarStackContainerView.swift in Sources */, + DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */, 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */, DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */, 0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */, diff --git a/Mastodon/Scene/Share/View/Container/AvatarStackContainerView.swift b/Mastodon/Scene/Share/View/Control/AvatarStackContainerButton.swift similarity index 98% rename from Mastodon/Scene/Share/View/Container/AvatarStackContainerView.swift rename to Mastodon/Scene/Share/View/Control/AvatarStackContainerButton.swift index ad828d9d2..1e4bd24fe 100644 --- a/Mastodon/Scene/Share/View/Container/AvatarStackContainerView.swift +++ b/Mastodon/Scene/Share/View/Control/AvatarStackContainerButton.swift @@ -1,5 +1,5 @@ // -// AvatarStackContainerView.swift +// AvatarStackContainerButton.swift // Mastodon // // Created by MainasuK Cirno on 2021-3-10. @@ -17,8 +17,6 @@ extension AvatarStackedImageView: AvatarConfigurableView { var configurableAvatarButton: UIButton? { nil } } -extension UIControl.State: Hashable { } - final class AvatarStackContainerButton: UIControl { static let containerSize = CGSize(width: 42, height: 42) From 51ee576c24acfb8bab27b31d4013a4a2d2acd524 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 11 Mar 2021 11:36:42 +0800 Subject: [PATCH 09/31] fix: redundant execute path for avatar setting --- Mastodon/Diffiable/Section/StatusSection.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 9d4fcad92..bd6b805ec 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -101,7 +101,7 @@ extension StatusSection { } else { cell.statusView.avatarButton.isHidden = false cell.statusView.avatarStackedContainerButton.isHidden = true - cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: (toot.reblog ?? toot).author.avatarImageURL())) + cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: toot.author.avatarImageURL())) } // set text From f4136d03bad16d2afaea1dfb356afd371af22368 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 11 Mar 2021 11:43:49 +0800 Subject: [PATCH 10/31] fix: add missing fetchLimit --- Mastodon/Service/APIService/APIService+Favorite.swift | 1 + Mastodon/Service/APIService/APIService+Reblog.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Mastodon/Service/APIService/APIService+Favorite.swift b/Mastodon/Service/APIService/APIService+Favorite.swift index 98b00cf18..2577b6cb0 100644 --- a/Mastodon/Service/APIService/APIService+Favorite.swift +++ b/Mastodon/Service/APIService/APIService+Favorite.swift @@ -79,6 +79,7 @@ extension APIService { let _oldToot: Toot? = { let request = Toot.sortedFetchRequest request.predicate = Toot.predicate(domain: mastodonAuthenticationBox.domain, id: statusID) + request.fetchLimit = 1 request.returnsObjectsAsFaults = false request.relationshipKeyPathsForPrefetching = [#keyPath(Toot.reblog)] do { diff --git a/Mastodon/Service/APIService/APIService+Reblog.swift b/Mastodon/Service/APIService/APIService+Reblog.swift index 55b4699ae..5525fe5bf 100644 --- a/Mastodon/Service/APIService/APIService+Reblog.swift +++ b/Mastodon/Service/APIService/APIService+Reblog.swift @@ -85,6 +85,7 @@ extension APIService { let _oldToot: Toot? = { let request = Toot.sortedFetchRequest request.predicate = Toot.predicate(domain: domain, id: statusID) + request.fetchLimit = 1 request.returnsObjectsAsFaults = false request.relationshipKeyPathsForPrefetching = [#keyPath(Toot.reblog)] do { From 6c0a767435ee0aa3bac7f2b6bd830a1503f57e7a Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 11 Mar 2021 13:11:13 +0800 Subject: [PATCH 11/31] chore: auto-pause when audio cell disappeared --- Mastodon.xcodeproj/project.pbxproj | 8 ++-- .../Diffiable/Section/StatusSection.swift | 2 +- .../StatusProvider+UITableViewDelegate.swift | 10 +++-- .../HomeTimelineViewController.swift | 1 + .../PublicTimelineViewController.swift | 2 +- .../ViewModel/AudioContainerViewModel.swift | 42 ++++++++++--------- .../ViewModel/VideoPlayerViewModel.swift | 2 +- ...layer.swift => AudioPlaybackService.swift} | 27 +++++++----- Mastodon/Service/ViedeoPlaybackService.swift | 2 +- Mastodon/State/AppContext.swift | 1 + 10 files changed, 56 insertions(+), 41 deletions(-) rename Mastodon/Service/{AudioPlayer.swift => AudioPlaybackService.swift} (87%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 69f431909..98bfe2074 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -25,7 +25,7 @@ 2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */; }; 2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7F25F5F45E00143C56 /* UIImage.swift */; }; 2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; }; - 2D206B8C25F6015000143C56 /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8B25F6015000143C56 /* AudioPlayer.swift */; }; + 2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */; }; 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; }; 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; }; 2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAB925CB9B0500C9ED86 /* UIView.swift */; }; @@ -269,7 +269,7 @@ 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = ""; }; 2D206B7F25F5F45E00143C56 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; 2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; - 2D206B8B25F6015000143C56 /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = ""; }; + 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackService.swift; sourceTree = ""; }; 2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = ""; }; 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = ""; }; 2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; @@ -659,7 +659,7 @@ DB45FB0425CA87B4005A8AC7 /* APIService */, DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */, DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */, - 2D206B8B25F6015000143C56 /* AudioPlayer.swift */, + 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */, 2DA6054625F716A2006356F9 /* PlaybackState.swift */, 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */, ); @@ -1569,7 +1569,7 @@ DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */, 5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */, 0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */, - 2D206B8C25F6015000143C56 /* AudioPlayer.swift in Sources */, + 2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */, 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */, DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */, 2D7631B325C159F700929FB9 /* Item.swift in Sources */, diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 53f7aec87..daaa98523 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -176,7 +176,7 @@ extension StatusSection { // set audio if let audioAttachment = mediaAttachments.filter({ $0.type == .audio }).first { cell.statusView.audioView.isHidden = false - AudioContainerViewModel.configure(cell: cell, audioAttachment: audioAttachment ) + AudioContainerViewModel.configure(cell: cell, audioAttachment: audioAttachment, audioService: dependency.context.audioPlaybackService) } else { cell.statusView.audioView.isHidden = true } diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift index c68ff6e77..1157b7993 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift @@ -87,10 +87,14 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { .sink { [weak self] toot in guard let self = self else { return } guard let media = (toot?.mediaAttachments ?? Set()).first else { return } - guard let videoPlayerViewModel = self.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: media) else { return } - DispatchQueue.main.async { - videoPlayerViewModel.didEndDisplaying() + if let videoPlayerViewModel = self.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: media) { + DispatchQueue.main.async { + videoPlayerViewModel.didEndDisplaying() + } + } + if let currentAudioAttachment = self.context.audioPlaybackService.attachment, let _ = toot?.mediaAttachments?.contains(currentAudioAttachment) { + self.context.audioPlaybackService.pause() } } .store(in: &disposeBag) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 01b860da8..9db551f62 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -142,6 +142,7 @@ extension HomeTimelineViewController { override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) context.videoPlaybackService.viewDidDisappear(from: self) + context.audioPlaybackService.viewDidDisappear(from: self) } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift index 820094b1e..1fc0978e2 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift @@ -84,6 +84,7 @@ extension PublicTimelineViewController { override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) context.videoPlaybackService.viewDidDisappear(from: self) + context.audioPlaybackService.viewDidDisappear(from: self) } } @@ -107,7 +108,6 @@ extension PublicTimelineViewController { // MARK: - UITableViewDelegate extension PublicTimelineViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - guard let diffableDataSource = viewModel.diffableDataSource else { return 100 } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return 100 } diff --git a/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift b/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift index de804a475..56bf0cbc3 100644 --- a/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift @@ -12,54 +12,58 @@ import UIKit class AudioContainerViewModel { static func configure( cell: StatusTableViewCell, - audioAttachment: Attachment + audioAttachment: Attachment, + audioService: AudioPlaybackService ) { guard let duration = audioAttachment.meta?.original?.duration else { return } let audioView = cell.statusView.audioView audioView.timeLabel.text = duration.asString(style: .positional) audioView.playButton.publisher(for: .touchUpInside) - .sink { _ in - if audioAttachment === AudioPlayer.shared.attachment { - if AudioPlayer.shared.isPlaying() { - AudioPlayer.shared.pause() + .sink { [weak audioService] _ in + guard let audioService = audioService else { return } + if audioAttachment === audioService.attachment { + if audioService.isPlaying() { + audioService.pause() } else { - AudioPlayer.shared.resume() + audioService.resume() } - if AudioPlayer.shared.currentTimeSubject.value == 0 { - AudioPlayer.shared.playAudio(audioAttachment: audioAttachment) + if audioService.currentTimeSubject.value == 0 { + audioService.playAudio(audioAttachment: audioAttachment) } } else { - AudioPlayer.shared.playAudio(audioAttachment: audioAttachment) + audioService.playAudio(audioAttachment: audioAttachment) } } .store(in: &cell.disposeBag) audioView.slider.publisher(for: .valueChanged) - .sink { slider in + .sink { [weak audioService] slider in + guard let audioService = audioService else { return } let slider = slider as! UISlider let time = Double(slider.value) * duration - AudioPlayer.shared.seekToTime(time: time) + audioService.seekToTime(time: time) } .store(in: &cell.disposeBag) - observePlayer(cell: cell, audioAttachment: audioAttachment) - if audioAttachment != AudioPlayer.shared.attachment { + observePlayer(cell: cell, audioAttachment: audioAttachment, audioService: audioService) + if audioAttachment != audioService.attachment { configureAudioView(audioView: audioView, audioAttachment: audioAttachment, playbackState: .stopped) } } static func observePlayer( cell: StatusTableViewCell, - audioAttachment: Attachment + audioAttachment: Attachment, + audioService: AudioPlaybackService ) { let audioView = cell.statusView.audioView var lastCurrentTimeSubject: TimeInterval? - AudioPlayer.shared.currentTimeSubject + audioService.currentTimeSubject .throttle(for: 0.33, scheduler: DispatchQueue.main, latest: true) - .compactMap { time -> (TimeInterval, Float)? in + .compactMap { [weak audioService] time -> (TimeInterval, Float)? in defer { lastCurrentTimeSubject = time } - guard audioAttachment === AudioPlayer.shared.attachment else { return nil } + guard audioAttachment === audioService?.attachment else { return nil } guard let duration = audioAttachment.meta?.original?.duration else { return nil } if let lastCurrentTimeSubject = lastCurrentTimeSubject, time != 0.0 { @@ -74,10 +78,10 @@ class AudioContainerViewModel { audioView.slider.setValue(progress, animated: true) }) .store(in: &cell.disposeBag) - AudioPlayer.shared.playbackState + audioService.playbackState .receive(on: DispatchQueue.main) .sink(receiveValue: { playbackState in - if audioAttachment === AudioPlayer.shared.attachment { + if audioAttachment === audioService.attachment { configureAudioView(audioView: audioView, audioAttachment: audioAttachment, playbackState: playbackState) } else { configureAudioView(audioView: audioView, audioAttachment: audioAttachment, playbackState: .stopped) diff --git a/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift b/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift index c3f2cf369..3fa241486 100644 --- a/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift @@ -14,7 +14,7 @@ import UIKit final class VideoPlayerViewModel { var disposeBag = Set() - static let appWillPlayVideoNotification = NSNotification.Name(rawValue: "appWillPlayVideoNotification") + static let appWillPlayVideoNotification = NSNotification.Name(rawValue: "org.joinmastodon.Mastodon.VideoPlayerViewModel.appWillPlayVideo") // input let previewImageURL: URL? let videoURL: URL diff --git a/Mastodon/Service/AudioPlayer.swift b/Mastodon/Service/AudioPlaybackService.swift similarity index 87% rename from Mastodon/Service/AudioPlayer.swift rename to Mastodon/Service/AudioPlaybackService.swift index 646207edb..314f39649 100644 --- a/Mastodon/Service/AudioPlayer.swift +++ b/Mastodon/Service/AudioPlaybackService.swift @@ -10,10 +10,11 @@ import Combine import CoreDataStack import Foundation import UIKit +import os.log -final class AudioPlayer: NSObject { +final class AudioPlaybackService: NSObject { - static let appWillPlayAudioNotification = NSNotification.Name(rawValue: "appWillPlayAudioNotification") + static let appWillPlayAudioNotification = NSNotification.Name(rawValue: "org.joinmastodon.Mastodon.AudioPlayer.appWillPlayAudio") var disposeBag = Set() @@ -24,19 +25,16 @@ final class AudioPlayer: NSObject { let session = AVAudioSession.sharedInstance() let playbackState = CurrentValueSubject(PlaybackState.unknown) - - // MARK: - singleton - public static let shared = AudioPlayer() let currentTimeSubject = CurrentValueSubject(0) - private override init() { + override init() { super.init() addObserver() } } -extension AudioPlayer { +extension AudioPlaybackService { func playAudio(audioAttachment: Attachment) { guard let url = URL(string: audioAttachment.url) else { return @@ -48,7 +46,7 @@ extension AudioPlayer { return } - pushWillPlayAudioNotification() + notifyWillPlayAudioNotification() if audioAttachment == attachment { if self.playbackState.value == .stopped { self.seekToTime(time: .zero) @@ -129,14 +127,14 @@ extension AudioPlayer { .store(in: &disposeBag) } - func pushWillPlayAudioNotification() { - NotificationCenter.default.post(name: AudioPlayer.appWillPlayAudioNotification, object: nil) + func notifyWillPlayAudioNotification() { + NotificationCenter.default.post(name: AudioPlaybackService.appWillPlayAudioNotification, object: nil) } func isPlaying() -> Bool { return playbackState.value == .readyToPlay || playbackState.value == .playing } func resume() { - pushWillPlayAudioNotification() + notifyWillPlayAudioNotification() player.play() playbackState.value = .playing } @@ -154,3 +152,10 @@ extension AudioPlayer { player.seek(to: CMTimeMake(value:Int64(time), timescale: 1)) } } + +extension AudioPlaybackService { + func viewDidDisappear(from viewController: UIViewController?) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function) + pause() + } +} diff --git a/Mastodon/Service/ViedeoPlaybackService.swift b/Mastodon/Service/ViedeoPlaybackService.swift index 49ac3b0df..24ea6e6ce 100644 --- a/Mastodon/Service/ViedeoPlaybackService.swift +++ b/Mastodon/Service/ViedeoPlaybackService.swift @@ -91,7 +91,7 @@ extension VideoPlaybackService { } .store(in: &disposeBag) - NotificationCenter.default.publisher(for: AudioPlayer.appWillPlayAudioNotification) + NotificationCenter.default.publisher(for: AudioPlaybackService.appWillPlayAudioNotification) .sink { [weak self] _ in guard let self = self else { return } self.pauseWhenPlayAudio() diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index 30069ec30..c5330fc0d 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -28,6 +28,7 @@ class AppContext: ObservableObject { private var documentStoreSubscription: AnyCancellable! let videoPlaybackService = VideoPlaybackService() + let audioPlaybackService = AudioPlaybackService() let overrideTraitCollection = CurrentValueSubject(nil) From 6b5bb4f178d3e22590fee1a12457db9d8d97f391 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 11 Mar 2021 14:08:00 +0800 Subject: [PATCH 12/31] chore: make the database request Fail-Fast --- .../Service/APIService/APIService+Reblog.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Mastodon/Service/APIService/APIService+Reblog.swift b/Mastodon/Service/APIService/APIService+Reblog.swift index 5525fe5bf..a0268b345 100644 --- a/Mastodon/Service/APIService/APIService+Reblog.swift +++ b/Mastodon/Service/APIService/APIService+Reblog.swift @@ -70,7 +70,7 @@ extension APIService { let managedObjectContext = self.backgroundManagedObjectContext return managedObjectContext.performChanges { - let _requestMastodonUser: MastodonUser? = { + guard let requestMastodonUser: MastodonUser = { let request = MastodonUser.sortedFetchRequest request.predicate = MastodonUser.predicate(domain: mastodonAuthenticationBox.domain, id: requestMastodonUserID) request.fetchLimit = 1 @@ -81,8 +81,11 @@ extension APIService { assertionFailure(error.localizedDescription) return nil } - }() - let _oldToot: Toot? = { + }() else { + return + } + + guard let oldToot: Toot = { let request = Toot.sortedFetchRequest request.predicate = Toot.predicate(domain: domain, id: statusID) request.fetchLimit = 1 @@ -94,13 +97,10 @@ extension APIService { assertionFailure(error.localizedDescription) return nil } - }() - - guard let requestMastodonUser = _requestMastodonUser, - let oldToot = _oldToot else { - assertionFailure() + }() else { return } + APIService.CoreData.merge(toot: oldToot, entity: entity.reblog ?? entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate) if boostKind == .undoBoost { oldToot.update(reblogsCount: NSNumber(value: max(0, oldToot.reblogsCount.intValue - 1))) From 71c5ca327ae4caa9d3095b62383b698b3abd83f6 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 11 Mar 2021 14:34:10 +0800 Subject: [PATCH 13/31] chore: make fetch free from exception --- Mastodon.xcodeproj/project.pbxproj | 4 ++++ .../Extension/NSManagedObjectContext.swift | 20 +++++++++++++++++++ .../APIService/APIService+Reblog.swift | 14 ++----------- 3 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 Mastodon/Extension/NSManagedObjectContext.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 439b8ba9c..8bb813fe2 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -136,6 +136,7 @@ DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */; }; DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */; }; DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; }; + DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */; }; DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; }; DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; }; DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; }; @@ -385,6 +386,7 @@ DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+MastodonAuthentication.swift"; sourceTree = ""; }; DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = ""; }; DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = ""; }; + DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContext.swift; sourceTree = ""; }; DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = ""; }; DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = ""; }; DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = ""; }; @@ -1118,6 +1120,7 @@ 2D206B7F25F5F45E00143C56 /* UIImage.swift */, 2D206B8525F5FB0900143C56 /* Double.swift */, 2D206B9125F60EA700143C56 /* UIControl.swift */, + DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */, ); path = Extension; sourceTree = ""; @@ -1625,6 +1628,7 @@ 2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */, 2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */, DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */, + DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */, 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */, DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */, diff --git a/Mastodon/Extension/NSManagedObjectContext.swift b/Mastodon/Extension/NSManagedObjectContext.swift new file mode 100644 index 000000000..9c569a8f4 --- /dev/null +++ b/Mastodon/Extension/NSManagedObjectContext.swift @@ -0,0 +1,20 @@ +// +// NSManagedObjectContext.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-11. +// + +import Foundation +import CoreData + +extension NSManagedObjectContext { + func safeFetch(_ request: NSFetchRequest) -> [T] where T : NSFetchRequestResult { + do { + return try fetch(request) + } catch { + assertionFailure(error.localizedDescription) + return [] + } + } +} diff --git a/Mastodon/Service/APIService/APIService+Reblog.swift b/Mastodon/Service/APIService/APIService+Reblog.swift index a0268b345..3f750954f 100644 --- a/Mastodon/Service/APIService/APIService+Reblog.swift +++ b/Mastodon/Service/APIService/APIService+Reblog.swift @@ -75,12 +75,7 @@ extension APIService { request.predicate = MastodonUser.predicate(domain: mastodonAuthenticationBox.domain, id: requestMastodonUserID) request.fetchLimit = 1 request.returnsObjectsAsFaults = false - do { - return try managedObjectContext.fetch(request).first - } catch { - assertionFailure(error.localizedDescription) - return nil - } + return managedObjectContext.safeFetch(request).first }() else { return } @@ -91,12 +86,7 @@ extension APIService { request.fetchLimit = 1 request.returnsObjectsAsFaults = false request.relationshipKeyPathsForPrefetching = [#keyPath(Toot.reblog)] - do { - return try managedObjectContext.fetch(request).first - } catch { - assertionFailure(error.localizedDescription) - return nil - } + return managedObjectContext.safeFetch(request).first }() else { return } From 6b9ae8d05df30d6b7d8939f4d6855491d7359eb2 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 11 Mar 2021 15:10:41 +0800 Subject: [PATCH 14/31] chore: add mosaicView --- Mastodon.xcodeproj/project.pbxproj | 4 + .../Diffiable/Section/StatusSection.swift | 8 ++ ...Provider+StatusTableViewCellDelegate.swift | 23 +++++- .../View/Container/MosaicPlayerView.swift | 13 ++++ .../Share/View/Container/MosaicView.swift | 74 +++++++++++++++++++ .../TableviewCell/StatusTableViewCell.swift | 1 + 6 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 Mastodon/Scene/Share/View/Container/MosaicView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 98bfe2074..84aade870 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -60,6 +60,7 @@ 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */; }; + 2D694A7425F9EB4E0038ADDC /* MosaicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D694A7325F9EB4E0038ADDC /* MosaicView.swift */; }; 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; }; 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */; }; 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */; }; @@ -301,6 +302,7 @@ 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = ""; }; 2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error+Detail.swift"; sourceTree = ""; }; + 2D694A7325F9EB4E0038ADDC /* MosaicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MosaicView.swift; sourceTree = ""; }; 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Toot.swift"; sourceTree = ""; }; 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewController.swift; sourceTree = ""; }; @@ -1177,6 +1179,7 @@ isa = PBXGroup; children = ( DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */, + 2D694A7325F9EB4E0038ADDC /* MosaicView.swift */, 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */, 5DF1057825F88A1D00D6C0D4 /* MosaicPlayerView.swift */, 5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */, @@ -1632,6 +1635,7 @@ 2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */, + 2D694A7425F9EB4E0038ADDC /* MosaicView.swift in Sources */, DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */, DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */, DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */, diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index daaa98523..f742e4731 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -192,6 +192,14 @@ extension StatusSection { let scale: CGFloat = 1.3 return CGSize(width: maxWidth, height: maxWidth * scale) }() + cell.statusView.mosaicPlayerView.mosaicView.blurVisualEffectView.effect = isStatusSensitive ? MosaicImageViewContainer.blurVisualEffect : nil + cell.statusView.mosaicPlayerView.mosaicView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 + cell.statusView.mosaicPlayerView.mosaicView.mosaicButton.publisher(for: .touchUpInside) + .sink { [weak cell] _ in + guard let cell = cell else { return } + cell.delegate?.statusTableViewCell(cell, mosaicView: cell.statusView.mosaicPlayerView.mosaicView, didTapContentWarningVisualEffectView: cell.statusView.mosaicPlayerView.mosaicView.blurVisualEffectView) + } + .store(in: &cell.disposeBag) if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first, let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment) diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift index cd4e5160d..db26930c1 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift @@ -45,7 +45,28 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) { } - + func statusTableViewCell(_ cell: StatusTableViewCell, mosaicView: MosaicView, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) { + guard let diffableDataSource = self.tableViewDiffableDataSource else { return } + guard let item = item(for: cell, indexPath: nil) 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) { + mosaicView.blurVisualEffectView.effect = nil + mosaicView.vibrancyVisualEffectView.alpha = 0.0 + } completion: { _ in + diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil) + } + } func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) { guard let diffableDataSource = self.tableViewDiffableDataSource else { return } guard let item = item(for: cell, indexPath: nil) else { return } diff --git a/Mastodon/Scene/Share/View/Container/MosaicPlayerView.swift b/Mastodon/Scene/Share/View/Container/MosaicPlayerView.swift index e7c478cea..52596162e 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicPlayerView.swift +++ b/Mastodon/Scene/Share/View/Container/MosaicPlayerView.swift @@ -15,6 +15,11 @@ final class MosaicPlayerView: UIView { private let touchBlockingView = TouchBlockingView() private var containerHeightLayoutConstraint: NSLayoutConstraint! + let mosaicView: MosaicView = { + let mosaicView = MosaicView() + return mosaicView + }() + let playerViewController = AVPlayerViewController() let gifIndicatorLabel: UILabel = { @@ -38,6 +43,14 @@ final class MosaicPlayerView: UIView { extension MosaicPlayerView { private func _init() { + addSubview(mosaicView) + NSLayoutConstraint.activate([ + mosaicView.topAnchor.constraint(equalTo: topAnchor), + mosaicView.leadingAnchor.constraint(equalTo: leadingAnchor), + mosaicView.trailingAnchor.constraint(equalTo: trailingAnchor), + mosaicView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + container.translatesAutoresizingMaskIntoConstraints = false addSubview(container) containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: 162).priority(.required - 1) diff --git a/Mastodon/Scene/Share/View/Container/MosaicView.swift b/Mastodon/Scene/Share/View/Container/MosaicView.swift new file mode 100644 index 000000000..10adf3ab4 --- /dev/null +++ b/Mastodon/Scene/Share/View/Container/MosaicView.swift @@ -0,0 +1,74 @@ +// +// MosaicView.swift +// Mastodon +// +// Created by sxiaojian on 2021/3/11. +// + +import Foundation +import UIKit + +class MosaicView: UIView { + static let cornerRadius: CGFloat = 4 + static let blurVisualEffect = UIBlurEffect(style: .systemUltraThinMaterial) + let blurVisualEffectView = UIVisualEffectView(effect: MosaicView.blurVisualEffect) + let vibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: MosaicView.blurVisualEffect)) + + let mosaicButton: UIButton = { + let button = UIButton(type: .custom) + button.backgroundColor = .clear + return button + }() + + 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 + }() + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } +} + +extension MosaicView { + private func _init() { + translatesAutoresizingMaskIntoConstraints = false + addSubview(mosaicButton) + NSLayoutConstraint.activate([ + mosaicButton.topAnchor.constraint(equalTo: topAnchor), + mosaicButton.trailingAnchor.constraint(equalTo: trailingAnchor), + mosaicButton.bottomAnchor.constraint(equalTo: bottomAnchor), + mosaicButton.leadingAnchor.constraint(equalTo: leadingAnchor), + ]) + // add blur visual effect view in the setup method + blurVisualEffectView.layer.masksToBounds = true + blurVisualEffectView.layer.cornerRadius = MosaicView.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), + ]) + } +} diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index 2f4000b95..d1b5a5c25 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -23,6 +23,7 @@ protocol StatusTableViewCellDelegate: class { func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) + func statusTableViewCell(_ cell: StatusTableViewCell, mosaicView: MosaicView, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) From 1342471c5dc9c4d3ac56006bd241168c1bfd4ff2 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 11 Mar 2021 15:34:30 +0800 Subject: [PATCH 15/31] chore: handle Video&Gif sensitive situation --- .../Diffiable/Section/StatusSection.swift | 2 ++ ...Provider+StatusTableViewCellDelegate.swift | 2 +- .../View/Container/MosaicPlayerView.swift | 16 +++++------ .../Share/View/Container/MosaicView.swift | 27 ++++++++++++++----- 4 files changed, 31 insertions(+), 16 deletions(-) diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index f742e4731..53a2207a0 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -192,8 +192,10 @@ extension StatusSection { let scale: CGFloat = 1.3 return CGSize(width: maxWidth, height: maxWidth * scale) }() + cell.statusView.mosaicPlayerView.mosaicView.blurVisualEffectView.effect = isStatusSensitive ? MosaicImageViewContainer.blurVisualEffect : nil cell.statusView.mosaicPlayerView.mosaicView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 + cell.statusView.mosaicPlayerView.mosaicView.isUserInteractionEnabled = isStatusSensitive cell.statusView.mosaicPlayerView.mosaicView.mosaicButton.publisher(for: .touchUpInside) .sink { [weak cell] _ in guard let cell = cell else { return } diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift index db26930c1..9e2797f2e 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift @@ -57,7 +57,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { default: return } - + mosaicView.isUserInteractionEnabled = false var snapshot = diffableDataSource.snapshot() snapshot.reloadItems([item]) UIView.animate(withDuration: 0.33) { diff --git a/Mastodon/Scene/Share/View/Container/MosaicPlayerView.swift b/Mastodon/Scene/Share/View/Container/MosaicPlayerView.swift index 52596162e..1851154b6 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicPlayerView.swift +++ b/Mastodon/Scene/Share/View/Container/MosaicPlayerView.swift @@ -43,14 +43,6 @@ final class MosaicPlayerView: UIView { extension MosaicPlayerView { private func _init() { - addSubview(mosaicView) - NSLayoutConstraint.activate([ - mosaicView.topAnchor.constraint(equalTo: topAnchor), - mosaicView.leadingAnchor.constraint(equalTo: leadingAnchor), - mosaicView.trailingAnchor.constraint(equalTo: trailingAnchor), - mosaicView.bottomAnchor.constraint(equalTo: bottomAnchor) - ]) - container.translatesAutoresizingMaskIntoConstraints = false addSubview(container) containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: 162).priority(.required - 1) @@ -74,6 +66,14 @@ extension MosaicPlayerView { playerViewController.view.layer.masksToBounds = true playerViewController.view.layer.cornerRadius = MosaicPlayerView.cornerRadius playerViewController.view.layer.cornerCurve = .continuous + + addSubview(mosaicView) + NSLayoutConstraint.activate([ + mosaicView.topAnchor.constraint(equalTo: topAnchor), + mosaicView.leadingAnchor.constraint(equalTo: leadingAnchor), + mosaicView.trailingAnchor.constraint(equalTo: trailingAnchor), + mosaicView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) } } diff --git a/Mastodon/Scene/Share/View/Container/MosaicView.swift b/Mastodon/Scene/Share/View/Container/MosaicView.swift index 10adf3ab4..82049dbb8 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicView.swift +++ b/Mastodon/Scene/Share/View/Container/MosaicView.swift @@ -17,6 +17,7 @@ class MosaicView: UIView { let mosaicButton: UIButton = { let button = UIButton(type: .custom) button.backgroundColor = .clear + button.translatesAutoresizingMaskIntoConstraints = false return button }() @@ -41,14 +42,9 @@ class MosaicView: UIView { extension MosaicView { private func _init() { + backgroundColor = .clear translatesAutoresizingMaskIntoConstraints = false - addSubview(mosaicButton) - NSLayoutConstraint.activate([ - mosaicButton.topAnchor.constraint(equalTo: topAnchor), - mosaicButton.trailingAnchor.constraint(equalTo: trailingAnchor), - mosaicButton.bottomAnchor.constraint(equalTo: bottomAnchor), - mosaicButton.leadingAnchor.constraint(equalTo: leadingAnchor), - ]) + // add blur visual effect view in the setup method blurVisualEffectView.layer.masksToBounds = true blurVisualEffectView.layer.cornerRadius = MosaicView.cornerRadius @@ -70,5 +66,22 @@ extension MosaicView { contentWarningLabel.trailingAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.layoutMarginsGuide.trailingAnchor), contentWarningLabel.centerYAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.centerYAnchor), ]) + + blurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false + addSubview(blurVisualEffectView) + NSLayoutConstraint.activate([ + blurVisualEffectView.topAnchor.constraint(equalTo: topAnchor), + blurVisualEffectView.leadingAnchor.constraint(equalTo: leadingAnchor), + blurVisualEffectView.trailingAnchor.constraint(equalTo: trailingAnchor), + blurVisualEffectView.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + + addSubview(mosaicButton) + NSLayoutConstraint.activate([ + mosaicButton.topAnchor.constraint(equalTo: topAnchor), + mosaicButton.trailingAnchor.constraint(equalTo: trailingAnchor), + mosaicButton.bottomAnchor.constraint(equalTo: bottomAnchor), + mosaicButton.leadingAnchor.constraint(equalTo: leadingAnchor), + ]) } } From bbdd6926d64cf840e644105662fb2bdd54cb63cf Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 11 Mar 2021 17:24:00 +0800 Subject: [PATCH 16/31] chore: rename MosaicView to MosaicBlurView --- Mastodon.xcodeproj/project.pbxproj | 8 +++--- .../Diffiable/Section/StatusSection.swift | 10 +++---- ...Provider+StatusTableViewCellDelegate.swift | 10 ++++--- ...{MosaicView.swift => MosaicBlurView.swift} | 26 +++++++++---------- .../View/Container/MosaicPlayerView.swift | 16 ++++++------ .../TableviewCell/StatusTableViewCell.swift | 2 +- 6 files changed, 37 insertions(+), 35 deletions(-) rename Mastodon/Scene/Share/View/Container/{MosaicView.swift => MosaicBlurView.swift} (84%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 84aade870..5524314b9 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -60,7 +60,7 @@ 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */; }; - 2D694A7425F9EB4E0038ADDC /* MosaicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D694A7325F9EB4E0038ADDC /* MosaicView.swift */; }; + 2D694A7425F9EB4E0038ADDC /* MosaicBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D694A7325F9EB4E0038ADDC /* MosaicBlurView.swift */; }; 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; }; 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */; }; 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */; }; @@ -302,7 +302,7 @@ 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = ""; }; 2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error+Detail.swift"; sourceTree = ""; }; - 2D694A7325F9EB4E0038ADDC /* MosaicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MosaicView.swift; sourceTree = ""; }; + 2D694A7325F9EB4E0038ADDC /* MosaicBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MosaicBlurView.swift; sourceTree = ""; }; 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Toot.swift"; sourceTree = ""; }; 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewController.swift; sourceTree = ""; }; @@ -1179,7 +1179,7 @@ isa = PBXGroup; children = ( DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */, - 2D694A7325F9EB4E0038ADDC /* MosaicView.swift */, + 2D694A7325F9EB4E0038ADDC /* MosaicBlurView.swift */, 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */, 5DF1057825F88A1D00D6C0D4 /* MosaicPlayerView.swift */, 5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */, @@ -1635,7 +1635,7 @@ 2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */, - 2D694A7425F9EB4E0038ADDC /* MosaicView.swift in Sources */, + 2D694A7425F9EB4E0038ADDC /* MosaicBlurView.swift in Sources */, DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */, DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */, DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */, diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 53a2207a0..31ea87cdf 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -193,13 +193,13 @@ extension StatusSection { return CGSize(width: maxWidth, height: maxWidth * scale) }() - cell.statusView.mosaicPlayerView.mosaicView.blurVisualEffectView.effect = isStatusSensitive ? MosaicImageViewContainer.blurVisualEffect : nil - cell.statusView.mosaicPlayerView.mosaicView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 - cell.statusView.mosaicPlayerView.mosaicView.isUserInteractionEnabled = isStatusSensitive - cell.statusView.mosaicPlayerView.mosaicView.mosaicButton.publisher(for: .touchUpInside) + cell.statusView.mosaicPlayerView.mosaicBlurView.blurVisualEffectView.effect = isStatusSensitive ? MosaicImageViewContainer.blurVisualEffect : nil + cell.statusView.mosaicPlayerView.mosaicBlurView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 + cell.statusView.mosaicPlayerView.mosaicBlurView.isUserInteractionEnabled = isStatusSensitive + cell.statusView.mosaicPlayerView.mosaicBlurView.tapButton.publisher(for: .touchUpInside) .sink { [weak cell] _ in guard let cell = cell else { return } - cell.delegate?.statusTableViewCell(cell, mosaicView: cell.statusView.mosaicPlayerView.mosaicView, didTapContentWarningVisualEffectView: cell.statusView.mosaicPlayerView.mosaicView.blurVisualEffectView) + cell.delegate?.statusTableViewCell(cell, mosaicBlurView: cell.statusView.mosaicPlayerView.mosaicBlurView, didTapContentWarningVisualEffectView: cell.statusView.mosaicPlayerView.mosaicBlurView.blurVisualEffectView) } .store(in: &cell.disposeBag) diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift index 9e2797f2e..67753e88d 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift @@ -45,7 +45,8 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) { } - func statusTableViewCell(_ cell: StatusTableViewCell, mosaicView: MosaicView, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) { + + func statusTableViewCell(_ cell: StatusTableViewCell, mosaicBlurView: MosaicBlurView, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) { guard let diffableDataSource = self.tableViewDiffableDataSource else { return } guard let item = item(for: cell, indexPath: nil) else { return } @@ -57,16 +58,17 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { default: return } - mosaicView.isUserInteractionEnabled = false + mosaicBlurView.isUserInteractionEnabled = false var snapshot = diffableDataSource.snapshot() snapshot.reloadItems([item]) UIView.animate(withDuration: 0.33) { - mosaicView.blurVisualEffectView.effect = nil - mosaicView.vibrancyVisualEffectView.alpha = 0.0 + mosaicBlurView.blurVisualEffectView.effect = nil + mosaicBlurView.vibrancyVisualEffectView.alpha = 0.0 } completion: { _ in diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil) } } + func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) { guard let diffableDataSource = self.tableViewDiffableDataSource else { return } guard let item = item(for: cell, indexPath: nil) else { return } diff --git a/Mastodon/Scene/Share/View/Container/MosaicView.swift b/Mastodon/Scene/Share/View/Container/MosaicBlurView.swift similarity index 84% rename from Mastodon/Scene/Share/View/Container/MosaicView.swift rename to Mastodon/Scene/Share/View/Container/MosaicBlurView.swift index 82049dbb8..72f03ab67 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicView.swift +++ b/Mastodon/Scene/Share/View/Container/MosaicBlurView.swift @@ -8,13 +8,13 @@ import Foundation import UIKit -class MosaicView: UIView { +class MosaicBlurView: UIView { static let cornerRadius: CGFloat = 4 static let blurVisualEffect = UIBlurEffect(style: .systemUltraThinMaterial) - let blurVisualEffectView = UIVisualEffectView(effect: MosaicView.blurVisualEffect) - let vibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: MosaicView.blurVisualEffect)) + let blurVisualEffectView = UIVisualEffectView(effect: MosaicBlurView.blurVisualEffect) + let vibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: MosaicBlurView.blurVisualEffect)) - let mosaicButton: UIButton = { + let tapButton: UIButton = { let button = UIButton(type: .custom) button.backgroundColor = .clear button.translatesAutoresizingMaskIntoConstraints = false @@ -40,14 +40,14 @@ class MosaicView: UIView { } } -extension MosaicView { +extension MosaicBlurView { private func _init() { backgroundColor = .clear translatesAutoresizingMaskIntoConstraints = false // add blur visual effect view in the setup method blurVisualEffectView.layer.masksToBounds = true - blurVisualEffectView.layer.cornerRadius = MosaicView.cornerRadius + blurVisualEffectView.layer.cornerRadius = MosaicBlurView.cornerRadius blurVisualEffectView.layer.cornerCurve = .continuous vibrancyVisualEffectView.translatesAutoresizingMaskIntoConstraints = false @@ -66,7 +66,7 @@ extension MosaicView { contentWarningLabel.trailingAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.layoutMarginsGuide.trailingAnchor), contentWarningLabel.centerYAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.centerYAnchor), ]) - + blurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false addSubview(blurVisualEffectView) NSLayoutConstraint.activate([ @@ -75,13 +75,13 @@ extension MosaicView { blurVisualEffectView.trailingAnchor.constraint(equalTo: trailingAnchor), blurVisualEffectView.bottomAnchor.constraint(equalTo: bottomAnchor), ]) - - addSubview(mosaicButton) + + addSubview(tapButton) NSLayoutConstraint.activate([ - mosaicButton.topAnchor.constraint(equalTo: topAnchor), - mosaicButton.trailingAnchor.constraint(equalTo: trailingAnchor), - mosaicButton.bottomAnchor.constraint(equalTo: bottomAnchor), - mosaicButton.leadingAnchor.constraint(equalTo: leadingAnchor), + tapButton.topAnchor.constraint(equalTo: topAnchor), + tapButton.trailingAnchor.constraint(equalTo: trailingAnchor), + tapButton.bottomAnchor.constraint(equalTo: bottomAnchor), + tapButton.leadingAnchor.constraint(equalTo: leadingAnchor), ]) } } diff --git a/Mastodon/Scene/Share/View/Container/MosaicPlayerView.swift b/Mastodon/Scene/Share/View/Container/MosaicPlayerView.swift index 1851154b6..49ccca5b5 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicPlayerView.swift +++ b/Mastodon/Scene/Share/View/Container/MosaicPlayerView.swift @@ -15,9 +15,9 @@ final class MosaicPlayerView: UIView { private let touchBlockingView = TouchBlockingView() private var containerHeightLayoutConstraint: NSLayoutConstraint! - let mosaicView: MosaicView = { - let mosaicView = MosaicView() - return mosaicView + let mosaicBlurView: MosaicBlurView = { + let mosaicBlurView = MosaicBlurView() + return mosaicBlurView }() let playerViewController = AVPlayerViewController() @@ -67,12 +67,12 @@ extension MosaicPlayerView { playerViewController.view.layer.cornerRadius = MosaicPlayerView.cornerRadius playerViewController.view.layer.cornerCurve = .continuous - addSubview(mosaicView) + addSubview(mosaicBlurView) NSLayoutConstraint.activate([ - mosaicView.topAnchor.constraint(equalTo: topAnchor), - mosaicView.leadingAnchor.constraint(equalTo: leadingAnchor), - mosaicView.trailingAnchor.constraint(equalTo: trailingAnchor), - mosaicView.bottomAnchor.constraint(equalTo: bottomAnchor) + mosaicBlurView.topAnchor.constraint(equalTo: topAnchor), + mosaicBlurView.leadingAnchor.constraint(equalTo: leadingAnchor), + mosaicBlurView.trailingAnchor.constraint(equalTo: trailingAnchor), + mosaicBlurView.bottomAnchor.constraint(equalTo: bottomAnchor) ]) } } diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index d1b5a5c25..b6af98265 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -23,7 +23,7 @@ protocol StatusTableViewCellDelegate: class { func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) - func statusTableViewCell(_ cell: StatusTableViewCell, mosaicView: MosaicView, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) + func statusTableViewCell(_ cell: StatusTableViewCell, mosaicBlurView: MosaicBlurView, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) From 19a14b7761d6fa868a1d34a31b82e275202f87f0 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 11 Mar 2021 19:06:15 +0800 Subject: [PATCH 17/31] chore: patch for delegate chain --- Mastodon.xcodeproj/project.pbxproj | 16 +++---- .../Diffiable/Section/StatusSection.swift | 18 +++---- ...Provider+StatusTableViewCellDelegate.swift | 46 +++++++++--------- ...erView.swift => PlayerContainerView.swift} | 41 +++++++++++----- .../ContentWarningOverlayView.swift} | 47 ++++++++++--------- .../Scene/Share/View/Content/StatusView.swift | 16 +++++-- .../TableviewCell/StatusTableViewCell.swift | 10 ++-- 7 files changed, 111 insertions(+), 83 deletions(-) rename Mastodon/Scene/Share/View/Container/{MosaicPlayerView.swift => PlayerContainerView.swift} (76%) rename Mastodon/Scene/Share/View/{Container/MosaicBlurView.swift => Content/ContentWarningOverlayView.swift} (69%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 5524314b9..368a15f32 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -60,7 +60,7 @@ 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */; }; - 2D694A7425F9EB4E0038ADDC /* MosaicBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D694A7325F9EB4E0038ADDC /* MosaicBlurView.swift */; }; + 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */; }; 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; }; 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */; }; 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */; }; @@ -94,7 +94,7 @@ 5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */; }; 5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */; }; 5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */; }; - 5DF1057925F88A1D00D6C0D4 /* MosaicPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1057825F88A1D00D6C0D4 /* MosaicPlayerView.swift */; }; + 5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */; }; 5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */; }; 5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */; }; 5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; }; @@ -302,7 +302,7 @@ 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = ""; }; 2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error+Detail.swift"; sourceTree = ""; }; - 2D694A7325F9EB4E0038ADDC /* MosaicBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MosaicBlurView.swift; sourceTree = ""; }; + 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningOverlayView.swift; sourceTree = ""; }; 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Toot.swift"; sourceTree = ""; }; 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewController.swift; sourceTree = ""; }; @@ -339,7 +339,7 @@ 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViedeoPlaybackService.swift; sourceTree = ""; }; 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = ""; }; 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = ""; }; - 5DF1057825F88A1D00D6C0D4 /* MosaicPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MosaicPlayerView.swift; sourceTree = ""; }; + 5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerContainerView.swift; sourceTree = ""; }; 5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBlockingView.swift; sourceTree = ""; }; 5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AVPlayerViewControllerDelegate.swift"; sourceTree = ""; }; 75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = ""; }; @@ -578,6 +578,7 @@ isa = PBXGroup; children = ( 2D152A8B25C295CC009AA50C /* StatusView.swift */, + 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */, ); path = Content; sourceTree = ""; @@ -1179,9 +1180,8 @@ isa = PBXGroup; children = ( DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */, - 2D694A7325F9EB4E0038ADDC /* MosaicBlurView.swift */, 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */, - 5DF1057825F88A1D00D6C0D4 /* MosaicPlayerView.swift */, + 5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */, 5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */, ); path = Container; @@ -1635,7 +1635,7 @@ 2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */, - 2D694A7425F9EB4E0038ADDC /* MosaicBlurView.swift in Sources */, + 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */, DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */, DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */, DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */, @@ -1711,7 +1711,7 @@ 0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */, 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */, 0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */, - 5DF1057925F88A1D00D6C0D4 /* MosaicPlayerView.swift in Sources */, + 5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */, DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 31ea87cdf..3fc1d28a4 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -193,21 +193,15 @@ extension StatusSection { return CGSize(width: maxWidth, height: maxWidth * scale) }() - cell.statusView.mosaicPlayerView.mosaicBlurView.blurVisualEffectView.effect = isStatusSensitive ? MosaicImageViewContainer.blurVisualEffect : nil - cell.statusView.mosaicPlayerView.mosaicBlurView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 - cell.statusView.mosaicPlayerView.mosaicBlurView.isUserInteractionEnabled = isStatusSensitive - cell.statusView.mosaicPlayerView.mosaicBlurView.tapButton.publisher(for: .touchUpInside) - .sink { [weak cell] _ in - guard let cell = cell else { return } - cell.delegate?.statusTableViewCell(cell, mosaicBlurView: cell.statusView.mosaicPlayerView.mosaicBlurView, didTapContentWarningVisualEffectView: cell.statusView.mosaicPlayerView.mosaicBlurView.blurVisualEffectView) - } - .store(in: &cell.disposeBag) + cell.statusView.playerContainerView.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? MosaicImageViewContainer.blurVisualEffect : nil + cell.statusView.playerContainerView.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 + cell.statusView.playerContainerView.contentWarningOverlayView.isUserInteractionEnabled = isStatusSensitive if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first, let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment) { let parent = cell.delegate?.parent() - let mosaicPlayerView = cell.statusView.mosaicPlayerView + let mosaicPlayerView = cell.statusView.playerContainerView let playerViewController = mosaicPlayerView.setupPlayer( aspectRatio: videoPlayerViewModel.videoSize, maxSize: playerViewMaxSize, @@ -221,8 +215,8 @@ extension StatusSection { mosaicPlayerView.isHidden = false } else { - cell.statusView.mosaicPlayerView.playerViewController.player?.pause() - cell.statusView.mosaicPlayerView.playerViewController.player = nil + cell.statusView.playerContainerView.playerViewController.player?.pause() + cell.statusView.playerContainerView.playerViewController.player = nil } // set poll let poll = (toot.reblog ?? toot).poll diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift index 67753e88d..cebc845a3 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift @@ -46,29 +46,6 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { } - func statusTableViewCell(_ cell: StatusTableViewCell, mosaicBlurView: MosaicBlurView, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) { - guard let diffableDataSource = self.tableViewDiffableDataSource else { return } - guard let item = item(for: cell, indexPath: nil) else { return } - - switch item { - case .homeTimelineIndex(_, let attribute): - attribute.isStatusSensitive = false - case .toot(_, let attribute): - attribute.isStatusSensitive = false - default: - return - } - mosaicBlurView.isUserInteractionEnabled = false - var snapshot = diffableDataSource.snapshot() - snapshot.reloadItems([item]) - UIView.animate(withDuration: 0.33) { - mosaicBlurView.blurVisualEffectView.effect = nil - mosaicBlurView.vibrancyVisualEffectView.alpha = 0.0 - } completion: { _ in - diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil) - } - } - func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) { guard let diffableDataSource = self.tableViewDiffableDataSource else { return } guard let item = item(for: cell, indexPath: nil) else { return } @@ -92,6 +69,29 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { } } + func statusTableViewCell(_ cell: StatusTableViewCell, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) { + guard let diffableDataSource = self.tableViewDiffableDataSource else { return } + guard let item = item(for: cell, indexPath: nil) else { return } + + switch item { + case .homeTimelineIndex(_, let attribute): + attribute.isStatusSensitive = false + case .toot(_, let attribute): + attribute.isStatusSensitive = false + default: + return + } + contentWarningOverlayView.isUserInteractionEnabled = false + var snapshot = diffableDataSource.snapshot() + snapshot.reloadItems([item]) + UIView.animate(withDuration: 0.33) { + contentWarningOverlayView.blurVisualEffectView.effect = nil + contentWarningOverlayView.vibrancyVisualEffectView.alpha = 0.0 + } completion: { _ in + diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil) + } + } + } // MARK: - PollTableView diff --git a/Mastodon/Scene/Share/View/Container/MosaicPlayerView.swift b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift similarity index 76% rename from Mastodon/Scene/Share/View/Container/MosaicPlayerView.swift rename to Mastodon/Scene/Share/View/Container/PlayerContainerView.swift index 49ccca5b5..aa60bacda 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicPlayerView.swift +++ b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift @@ -1,5 +1,5 @@ // -// MosaicPlayerView.swift +// PlayerContainerView.swift // Mastodon // // Created by xiaojian sun on 2021/3/10. @@ -8,16 +8,20 @@ import AVKit import UIKit -final class MosaicPlayerView: UIView { +protocol PlayerContainerViewDelegate: class { + func playerContainerView(_ playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) +} + +final class PlayerContainerView: UIView { static let cornerRadius: CGFloat = 8 private let container = UIView() private let touchBlockingView = TouchBlockingView() private var containerHeightLayoutConstraint: NSLayoutConstraint! - let mosaicBlurView: MosaicBlurView = { - let mosaicBlurView = MosaicBlurView() - return mosaicBlurView + let contentWarningOverlayView: ContentWarningOverlayView = { + let contentWarningOverlayView = ContentWarningOverlayView() + return contentWarningOverlayView }() let playerViewController = AVPlayerViewController() @@ -30,6 +34,8 @@ final class MosaicPlayerView: UIView { return label }() + weak var delegate: PlayerContainerViewDelegate? + override init(frame: CGRect) { super.init(frame: frame) _init() @@ -41,7 +47,7 @@ final class MosaicPlayerView: UIView { } } -extension MosaicPlayerView { +extension PlayerContainerView { private func _init() { container.translatesAutoresizingMaskIntoConstraints = false addSubview(container) @@ -64,20 +70,29 @@ extension MosaicPlayerView { // will not influence full-screen playback playerViewController.view.layer.masksToBounds = true - playerViewController.view.layer.cornerRadius = MosaicPlayerView.cornerRadius + playerViewController.view.layer.cornerRadius = PlayerContainerView.cornerRadius playerViewController.view.layer.cornerCurve = .continuous - addSubview(mosaicBlurView) + addSubview(contentWarningOverlayView) NSLayoutConstraint.activate([ - mosaicBlurView.topAnchor.constraint(equalTo: topAnchor), - mosaicBlurView.leadingAnchor.constraint(equalTo: leadingAnchor), - mosaicBlurView.trailingAnchor.constraint(equalTo: trailingAnchor), - mosaicBlurView.bottomAnchor.constraint(equalTo: bottomAnchor) + contentWarningOverlayView.topAnchor.constraint(equalTo: topAnchor), + contentWarningOverlayView.leadingAnchor.constraint(equalTo: leadingAnchor), + contentWarningOverlayView.trailingAnchor.constraint(equalTo: trailingAnchor), + contentWarningOverlayView.bottomAnchor.constraint(equalTo: bottomAnchor) ]) + + contentWarningOverlayView.delegate = self } } -extension MosaicPlayerView { +// MARK: - ContentWarningOverlayViewDelegate +extension PlayerContainerView: ContentWarningOverlayViewDelegate { + func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) { + delegate?.playerContainerView(self, contentWarningOverlayViewDidPressed: contentWarningOverlayView) + } +} + +extension PlayerContainerView { func reset() { // note: set playerViewController.player pause() and nil in data source configuration process make reloadData not break playing diff --git a/Mastodon/Scene/Share/View/Container/MosaicBlurView.swift b/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift similarity index 69% rename from Mastodon/Scene/Share/View/Container/MosaicBlurView.swift rename to Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift index 72f03ab67..ec2607e25 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicBlurView.swift +++ b/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift @@ -1,25 +1,24 @@ // -// MosaicView.swift +// ContentWarningOverlayView.swift // Mastodon // // Created by sxiaojian on 2021/3/11. // +import os.log import Foundation import UIKit -class MosaicBlurView: UIView { +protocol ContentWarningOverlayViewDelegate: class { + func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) +} + +class ContentWarningOverlayView: UIView { + static let cornerRadius: CGFloat = 4 static let blurVisualEffect = UIBlurEffect(style: .systemUltraThinMaterial) - let blurVisualEffectView = UIVisualEffectView(effect: MosaicBlurView.blurVisualEffect) - let vibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: MosaicBlurView.blurVisualEffect)) - - let tapButton: UIButton = { - let button = UIButton(type: .custom) - button.backgroundColor = .clear - button.translatesAutoresizingMaskIntoConstraints = false - return button - }() + let blurVisualEffectView = UIVisualEffectView(effect: ContentWarningOverlayView.blurVisualEffect) + let vibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: ContentWarningOverlayView.blurVisualEffect)) let contentWarningLabel: UILabel = { let label = UILabel() @@ -28,6 +27,10 @@ class MosaicBlurView: UIView { label.textAlignment = .center return label }() + + let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer + + weak var delegate: ContentWarningOverlayViewDelegate? override init(frame: CGRect) { super.init(frame: frame) @@ -40,14 +43,14 @@ class MosaicBlurView: UIView { } } -extension MosaicBlurView { +extension ContentWarningOverlayView { private func _init() { backgroundColor = .clear translatesAutoresizingMaskIntoConstraints = false // add blur visual effect view in the setup method blurVisualEffectView.layer.masksToBounds = true - blurVisualEffectView.layer.cornerRadius = MosaicBlurView.cornerRadius + blurVisualEffectView.layer.cornerRadius = ContentWarningOverlayView.cornerRadius blurVisualEffectView.layer.cornerCurve = .continuous vibrancyVisualEffectView.translatesAutoresizingMaskIntoConstraints = false @@ -75,13 +78,15 @@ extension MosaicBlurView { blurVisualEffectView.trailingAnchor.constraint(equalTo: trailingAnchor), blurVisualEffectView.bottomAnchor.constraint(equalTo: bottomAnchor), ]) - - addSubview(tapButton) - NSLayoutConstraint.activate([ - tapButton.topAnchor.constraint(equalTo: topAnchor), - tapButton.trailingAnchor.constraint(equalTo: trailingAnchor), - tapButton.bottomAnchor.constraint(equalTo: bottomAnchor), - tapButton.leadingAnchor.constraint(equalTo: leadingAnchor), - ]) + + tapGestureRecognizer.addTarget(self, action: #selector(ContentWarningOverlayView.tapGestureRecognizerHandler(_:))) + addGestureRecognizer(tapGestureRecognizer) + } +} + +extension ContentWarningOverlayView { + @objc private func tapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.contentWarningOverlayViewDidPressed(self) } } diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index ad7734780..4f6f5d43f 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -13,6 +13,7 @@ import AlamofireImage protocol StatusViewDelegate: class { func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton) + func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) } @@ -156,7 +157,7 @@ final class StatusView: UIView { return imageView }() - let mosaicPlayerView = MosaicPlayerView() + let playerContainerView = PlayerContainerView() let audioView: AudioContainerView = { let audioView = AudioContainerView() @@ -353,7 +354,7 @@ extension StatusView { audioView.heightAnchor.constraint(equalToConstant: 44) ]) // video gif - statusContainerStackView.addArrangedSubview(mosaicPlayerView) + statusContainerStackView.addArrangedSubview(playerContainerView) // action toolbar container containerStackView.addArrangedSubview(actionToolbarContainer) @@ -364,12 +365,14 @@ extension StatusView { pollTableView.isHidden = true pollStatusStackView.isHidden = true audioView.isHidden = true - mosaicPlayerView.isHidden = true + playerContainerView.isHidden = true contentWarningBlurContentImageView.isHidden = true statusContentWarningContainerStackView.isHidden = true statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false + playerContainerView.delegate = self + contentWarningActionButton.addTarget(self, action: #selector(StatusView.contentWarningActionButtonPressed(_:)), for: .touchUpInside) pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonPressed(_:)), for: .touchUpInside) } @@ -420,6 +423,13 @@ extension StatusView { } +// MARK: - PlayerContainerViewDelegate +extension StatusView: PlayerContainerViewDelegate { + func playerContainerView(_ playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) { + delegate?.statusView(self, playerContainerView: playerContainerView, contentWarningOverlayViewDidPressed: contentWarningOverlayView) + } +} + // MARK: - AvatarConfigurableView extension StatusView: AvatarConfigurableView { static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize } diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index b6af98265..e8d986bd7 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -23,8 +23,8 @@ protocol StatusTableViewCellDelegate: class { func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) - func statusTableViewCell(_ cell: StatusTableViewCell, mosaicBlurView: MosaicBlurView, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) + func statusTableViewCell(_ cell: StatusTableViewCell, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton) @@ -55,8 +55,8 @@ final class StatusTableViewCell: UITableViewCell { statusView.isStatusTextSensitive = false statusView.cleanUpContentWarning() statusView.pollTableView.dataSource = nil - statusView.mosaicPlayerView.reset() - statusView.mosaicPlayerView.isHidden = true + statusView.playerContainerView.reset() + statusView.playerContainerView.isHidden = true disposeBag.removeAll() observations.removeAll() } @@ -198,6 +198,10 @@ extension StatusTableViewCell: StatusViewDelegate { delegate?.statusTableViewCell(self, statusView: statusView, contentWarningActionButtonPressed: button) } + func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) { + delegate?.statusTableViewCell(self, playerContainerView: playerContainerView, contentWarningOverlayViewDidPressed: contentWarningOverlayView) + } + func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) { delegate?.statusTableViewCell(self, statusView: statusView, pollVoteButtonPressed: button) } From 03496a4e97866c4d30e8585bcbff0bd73788ae4b Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 11 Mar 2021 19:23:44 +0800 Subject: [PATCH 18/31] chore: add missing renaming --- Mastodon/Diffiable/Section/StatusSection.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 3fc1d28a4..00ca16cc5 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -201,8 +201,8 @@ extension StatusSection { let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment) { let parent = cell.delegate?.parent() - let mosaicPlayerView = cell.statusView.playerContainerView - let playerViewController = mosaicPlayerView.setupPlayer( + let playerContainerView = cell.statusView.playerContainerView + let playerViewController = playerContainerView.setupPlayer( aspectRatio: videoPlayerViewModel.videoSize, maxSize: playerViewMaxSize, parent: parent @@ -211,8 +211,8 @@ extension StatusSection { playerViewController.player = videoPlayerViewModel.player playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif - mosaicPlayerView.gifIndicatorLabel.isHidden = videoPlayerViewModel.videoKind != .gif - mosaicPlayerView.isHidden = false + playerContainerView.gifIndicatorLabel.isHidden = videoPlayerViewModel.videoKind != .gif + playerContainerView.isHidden = false } else { cell.statusView.playerContainerView.playerViewController.player?.pause() From 2e31280819ffdc8b381247cc893cd204cf8d5763 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 12 Mar 2021 12:17:07 +0800 Subject: [PATCH 19/31] chore: extract the common blur effect part from MosaicImageViewContainer --- .../Diffiable/Section/StatusSection.swift | 6 +- ...Provider+StatusTableViewCellDelegate.swift | 28 ++---- .../Container/MosaicImageViewContainer.swift | 87 +++++++------------ .../TableviewCell/StatusTableViewCell.swift | 6 +- 4 files changed, 46 insertions(+), 81 deletions(-) diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 00ca16cc5..ef3134773 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -170,8 +170,8 @@ extension StatusSection { } cell.statusView.statusMosaicImageViewContainer.isHidden = mosiacImageViewModel.metas.isEmpty let isStatusSensitive = statusItemAttribute.isStatusSensitive - cell.statusView.statusMosaicImageViewContainer.blurVisualEffectView.effect = isStatusSensitive ? MosaicImageViewContainer.blurVisualEffect : nil - cell.statusView.statusMosaicImageViewContainer.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 + cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil + cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 // set audio if let audioAttachment = mediaAttachments.filter({ $0.type == .audio }).first { @@ -193,7 +193,7 @@ extension StatusSection { return CGSize(width: maxWidth, height: maxWidth * scale) }() - cell.statusView.playerContainerView.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? MosaicImageViewContainer.blurVisualEffect : nil + cell.statusView.playerContainerView.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil cell.statusView.playerContainerView.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 cell.statusView.playerContainerView.contentWarningOverlayView.isUserInteractionEnabled = isStatusSensitive diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift index cebc845a3..cce031c89 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift @@ -46,30 +46,16 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { } - func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) { - guard let diffableDataSource = self.tableViewDiffableDataSource else { return } - guard let item = item(for: cell, indexPath: nil) 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.statusMosaicImageViewContainer.blurVisualEffectView.effect = nil - cell.statusView.statusMosaicImageViewContainer.vibrancyVisualEffectView.alpha = 0.0 - } completion: { _ in - diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil) - } + func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) { + statusTableViewCell(cell, contentWarningOverlayViewDidPressed: contentWarningOverlayView) } func statusTableViewCell(_ cell: StatusTableViewCell, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) { + contentWarningOverlayView.isUserInteractionEnabled = false + statusTableViewCell(cell, contentWarningOverlayViewDidPressed: contentWarningOverlayView) + } + + func statusTableViewCell(_ cell: StatusTableViewCell, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) { guard let diffableDataSource = self.tableViewDiffableDataSource else { return } guard let item = item(for: cell, indexPath: nil) else { return } diff --git a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift index 5240d4e2c..8e6884463 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift +++ b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift @@ -15,15 +15,12 @@ protocol MosaicImageViewContainerPresentable: class { protocol MosaicImageViewContainerDelegate: class { func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) - func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) + func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) } final class MosaicImageViewContainer: UIView { - static let cornerRadius: CGFloat = 4 - static let blurVisualEffect = UIBlurEffect(style: .systemUltraThinMaterial) - weak var delegate: MosaicImageViewContainerDelegate? let container = UIStackView() @@ -37,14 +34,10 @@ 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 + + let contentWarningOverlayView: ContentWarningOverlayView = { + let contentWarningOverlayView = ContentWarningOverlayView() + return contentWarningOverlayView }() private var containerHeightLayoutConstraint: NSLayoutConstraint! @@ -61,9 +54,16 @@ final class MosaicImageViewContainer: UIView { } +extension MosaicImageViewContainer: ContentWarningOverlayViewDelegate { + func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) { + self.delegate?.mosaicImageViewContainer(self, contentWarningOverlayViewDidPressed: contentWarningOverlayView) + } +} + extension MosaicImageViewContainer { private func _init() { + contentWarningOverlayView.delegate = self container.translatesAutoresizingMaskIntoConstraints = false container.axis = .horizontal container.distribution = .fillEqually @@ -77,32 +77,13 @@ extension MosaicImageViewContainer { containerHeightLayoutConstraint ]) - // 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) + addSubview(contentWarningOverlayView) 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), + contentWarningOverlayView.topAnchor.constraint(equalTo: container.topAnchor), + contentWarningOverlayView.leadingAnchor.constraint(equalTo: container.leadingAnchor), + contentWarningOverlayView.bottomAnchor.constraint(equalTo: container.bottomAnchor), + contentWarningOverlayView.trailingAnchor.constraint(equalTo: container.trailingAnchor), ]) - - 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) } } @@ -117,9 +98,9 @@ extension MosaicImageViewContainer { container.subviews.forEach { subview in subview.removeFromSuperview() } - blurVisualEffectView.removeFromSuperview() - blurVisualEffectView.effect = MosaicImageViewContainer.blurVisualEffect - vibrancyVisualEffectView.alpha = 1.0 + contentWarningOverlayView.removeFromSuperview() + contentWarningOverlayView.blurVisualEffectView.effect = ContentWarningOverlayView.blurVisualEffect + contentWarningOverlayView.vibrancyVisualEffectView.alpha = 1.0 imageViews = [] container.spacing = 1 @@ -140,7 +121,7 @@ extension MosaicImageViewContainer { let imageView = UIImageView() imageViews.append(imageView) imageView.layer.masksToBounds = true - imageView.layer.cornerRadius = MosaicImageViewContainer.cornerRadius + imageView.layer.cornerRadius = ContentWarningOverlayView.cornerRadius imageView.layer.cornerCurve = .continuous imageView.contentMode = .scaleAspectFill @@ -155,13 +136,12 @@ extension MosaicImageViewContainer { containerHeightLayoutConstraint.constant = floor(rect.height) containerHeightLayoutConstraint.isActive = true - blurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false - addSubview(blurVisualEffectView) + addSubview(contentWarningOverlayView) 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), + contentWarningOverlayView.topAnchor.constraint(equalTo: imageView.topAnchor), + contentWarningOverlayView.leadingAnchor.constraint(equalTo: imageView.leadingAnchor), + contentWarningOverlayView.trailingAnchor.constraint(equalTo: imageView.trailingAnchor), + contentWarningOverlayView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor), ]) return imageView @@ -193,7 +173,7 @@ extension MosaicImageViewContainer { self.imageViews.append(contentsOf: imageViews) imageViews.forEach { imageView in imageView.layer.masksToBounds = true - imageView.layer.cornerRadius = MosaicImageViewContainer.cornerRadius + imageView.layer.cornerRadius = ContentWarningOverlayView.cornerRadius imageView.layer.cornerCurve = .continuous imageView.contentMode = .scaleAspectFill } @@ -242,13 +222,12 @@ extension MosaicImageViewContainer { } } - blurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false - addSubview(blurVisualEffectView) + addSubview(contentWarningOverlayView) 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), + contentWarningOverlayView.topAnchor.constraint(equalTo: container.topAnchor), + contentWarningOverlayView.leadingAnchor.constraint(equalTo: container.leadingAnchor), + contentWarningOverlayView.trailingAnchor.constraint(equalTo: container.trailingAnchor), + contentWarningOverlayView.bottomAnchor.constraint(equalTo: container.bottomAnchor), ]) return imageViews @@ -260,7 +239,7 @@ 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) + delegate?.mosaicImageViewContainer(self, contentWarningOverlayViewDidPressed: contentWarningOverlayView) } @objc private func photoTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index e8d986bd7..bc301b71c 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -22,7 +22,7 @@ protocol StatusTableViewCellDelegate: class { func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton) - func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) + func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) func statusTableViewCell(_ cell: StatusTableViewCell, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) @@ -215,8 +215,8 @@ extension StatusTableViewCell: MosaicImageViewContainerDelegate { delegate?.statusTableViewCell(self, mosaicImageViewContainer: mosaicImageViewContainer, didTapImageView: imageView, atIndex: index) } - func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) { - delegate?.statusTableViewCell(self, mosaicImageViewContainer: mosaicImageViewContainer, didTapContentWarningVisualEffectView: visualEffectView) + func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) { + delegate?.statusTableViewCell(self, mosaicImageViewContainer: mosaicImageViewContainer, contentWarningOverlayViewDidPressed: contentWarningOverlayView) } } From 6b5edff677aa13ebb396ad071184ff80f876a8bb Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 12 Mar 2021 15:41:57 +0800 Subject: [PATCH 20/31] chore: add media type with gif and video --- .../Diffiable/Section/StatusSection.swift | 3 +- Mastodon/Generated/Assets.swift | 1 + .../mediaTypeIndicotor.colorset/Contents.json | 20 ++++++ .../View/Container/PlayerContainerView.swift | 68 +++++++++++++------ 4 files changed, 70 insertions(+), 22 deletions(-) create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Background/mediaTypeIndicotor.colorset/Contents.json diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index ef3134773..aa6a89b91 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -210,8 +210,7 @@ extension StatusSection { playerViewController.delegate = cell.delegate?.playerViewControllerDelegate playerViewController.player = videoPlayerViewModel.player playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif - - playerContainerView.gifIndicatorLabel.isHidden = videoPlayerViewModel.videoKind != .gif + playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind) playerContainerView.isHidden = false } else { diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index f68170460..a760c40b8 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -37,6 +37,7 @@ internal enum Asset { internal static let disabled = ColorAsset(name: "Colors/Background/Poll/disabled") internal static let highlight = ColorAsset(name: "Colors/Background/Poll/highlight") } + internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/mediaTypeIndicotor") internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background") internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background") internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background") diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/mediaTypeIndicotor.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/mediaTypeIndicotor.colorset/Contents.json new file mode 100644 index 000000000..e9c583c0d --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/mediaTypeIndicotor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.600", + "blue" : "0", + "green" : "0", + "red" : "0" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift index aa60bacda..d002e13af 100644 --- a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift +++ b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift @@ -26,14 +26,28 @@ final class PlayerContainerView: UIView { let playerViewController = AVPlayerViewController() - let gifIndicatorLabel: UILabel = { + let mediaTypeIndicotorLabel: UILabel = { let label = UILabel() - label.font = .systemFont(ofSize: 16, weight: .heavy) - label.text = "GIF" + label.font = .systemFont(ofSize: 18, weight: .heavy) label.textColor = .white + label.textAlignment = .right + label.translatesAutoresizingMaskIntoConstraints = false return label }() + let mediaTypeIndicotorView: UIView = { + let view = UIView() + view.backgroundColor = Asset.Colors.Background.mediaTypeIndicotor.color + view.translatesAutoresizingMaskIntoConstraints = false + let rect = CGRect(x: 0, y: 0, width: 47, height: 50) + let path = UIBezierPath(roundedRect: rect, byRoundingCorners: [.topLeft], cornerRadii: CGSize(width: 50, height: 50)) + let maskLayer = CAShapeLayer() + maskLayer.frame = rect + maskLayer.path = path.cgPath + view.layer.mask = maskLayer + return view + }() + weak var delegate: PlayerContainerViewDelegate? override init(frame: CGRect) { @@ -60,14 +74,6 @@ extension PlayerContainerView { containerHeightLayoutConstraint, ]) - addSubview(gifIndicatorLabel) - gifIndicatorLabel.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - gifIndicatorLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 4), - gifIndicatorLabel.trailingAnchor.constraint(equalTo: trailingAnchor) - ]) - // will not influence full-screen playback playerViewController.view.layer.masksToBounds = true playerViewController.view.layer.cornerRadius = PlayerContainerView.cornerRadius @@ -80,8 +86,24 @@ extension PlayerContainerView { contentWarningOverlayView.trailingAnchor.constraint(equalTo: trailingAnchor), contentWarningOverlayView.bottomAnchor.constraint(equalTo: bottomAnchor) ]) - contentWarningOverlayView.delegate = self + + // mediaType + addSubview(mediaTypeIndicotorView) + NSLayoutConstraint.activate([ + mediaTypeIndicotorView.bottomAnchor.constraint(equalTo: bottomAnchor), + mediaTypeIndicotorView.trailingAnchor.constraint(equalTo: trailingAnchor), + mediaTypeIndicotorView.heightAnchor.constraint(equalToConstant: 25), + mediaTypeIndicotorView.widthAnchor.constraint(equalToConstant: 47) + ]) + + mediaTypeIndicotorView.addSubview(mediaTypeIndicotorLabel) + NSLayoutConstraint.activate([ + mediaTypeIndicotorLabel.topAnchor.constraint(equalTo: mediaTypeIndicotorView.topAnchor), + mediaTypeIndicotorLabel.leadingAnchor.constraint(equalTo: mediaTypeIndicotorView.leadingAnchor), + mediaTypeIndicotorLabel.bottomAnchor.constraint(equalTo: mediaTypeIndicotorView.bottomAnchor), + mediaTypeIndicotorView.trailingAnchor.constraint(equalTo: mediaTypeIndicotorLabel.trailingAnchor, constant: 8) + ]) } } @@ -96,8 +118,6 @@ extension PlayerContainerView { func reset() { // note: set playerViewController.player pause() and nil in data source configuration process make reloadData not break playing - gifIndicatorLabel.removeFromSuperview() - playerViewController.willMove(toParent: nil) playerViewController.view.removeFromSuperview() playerViewController.removeFromParent() @@ -137,13 +157,21 @@ extension PlayerContainerView { containerHeightLayoutConstraint.constant = floor(rect.height) containerHeightLayoutConstraint.isActive = true - gifIndicatorLabel.translatesAutoresizingMaskIntoConstraints = false - touchBlockingView.addSubview(gifIndicatorLabel) - NSLayoutConstraint.activate([ - touchBlockingView.trailingAnchor.constraint(equalTo: gifIndicatorLabel.trailingAnchor, constant: 8), - touchBlockingView.bottomAnchor.constraint(equalTo: gifIndicatorLabel.bottomAnchor, constant: 8), - ]) + bringSubviewToFront(mediaTypeIndicotorView) return playerViewController } + + func setMediaKind(kind: VideoPlayerViewModel.Kind) { + switch kind { + case .gif: + mediaTypeIndicotorLabel.text = "GIF" + case .video: + let configuration = UIImage.SymbolConfiguration(font: .systemFont(ofSize: 18, weight: .regular)) + let image = UIImage(systemName: "video.fill", withConfiguration: configuration)! + let attachment = NSTextAttachment() + attachment.image = image.withTintColor(.white) + mediaTypeIndicotorLabel.attributedText = NSAttributedString(attachment: attachment) + } + } } From 0c164a170cfb81c94374871c2edcb626a2e21a54 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 12 Mar 2021 15:53:19 +0800 Subject: [PATCH 21/31] chore: use rounded font --- .../Share/View/Container/PlayerContainerView.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift index d002e13af..3b4acc98c 100644 --- a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift +++ b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift @@ -28,7 +28,6 @@ final class PlayerContainerView: UIView { let mediaTypeIndicotorLabel: UILabel = { let label = UILabel() - label.font = .systemFont(ofSize: 18, weight: .heavy) label.textColor = .white label.textAlignment = .right label.translatesAutoresizingMaskIntoConstraints = false @@ -162,12 +161,21 @@ extension PlayerContainerView { return playerViewController } + func roundedFont(weight: UIFont.Weight,fontSize: CGFloat) -> UIFont { + let systemFont = UIFont.systemFont(ofSize: fontSize, weight: weight) + guard let descriptor = systemFont.fontDescriptor.withDesign(.rounded) else { return systemFont } + let roundedFont = UIFont(descriptor: descriptor, size: fontSize) + return roundedFont + } func setMediaKind(kind: VideoPlayerViewModel.Kind) { + let fontSize: CGFloat = 18 + switch kind { case .gif: + mediaTypeIndicotorLabel.font = roundedFont(weight: .heavy, fontSize: fontSize) mediaTypeIndicotorLabel.text = "GIF" case .video: - let configuration = UIImage.SymbolConfiguration(font: .systemFont(ofSize: 18, weight: .regular)) + let configuration = UIImage.SymbolConfiguration(font: roundedFont(weight: .regular, fontSize: fontSize)) let image = UIImage(systemName: "video.fill", withConfiguration: configuration)! let attachment = NSTextAttachment() attachment.image = image.withTintColor(.white) From 20283d187879a428f6ebb11d75fa2f0c2dfc7936 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Mon, 15 Mar 2021 15:08:58 +0800 Subject: [PATCH 22/31] chore: change video and audio notification string --- Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift | 2 +- Mastodon/Service/AudioPlaybackService.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift b/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift index 3fa241486..d34e73eba 100644 --- a/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift @@ -14,7 +14,7 @@ import UIKit final class VideoPlayerViewModel { var disposeBag = Set() - static let appWillPlayVideoNotification = NSNotification.Name(rawValue: "org.joinmastodon.Mastodon.VideoPlayerViewModel.appWillPlayVideo") + static let appWillPlayVideoNotification = NSNotification.Name(rawValue: "org.joinmastodon.Mastodon.video-playback-service.appWillPlayVideo") // input let previewImageURL: URL? let videoURL: URL diff --git a/Mastodon/Service/AudioPlaybackService.swift b/Mastodon/Service/AudioPlaybackService.swift index 314f39649..34ceb3bbe 100644 --- a/Mastodon/Service/AudioPlaybackService.swift +++ b/Mastodon/Service/AudioPlaybackService.swift @@ -14,7 +14,7 @@ import os.log final class AudioPlaybackService: NSObject { - static let appWillPlayAudioNotification = NSNotification.Name(rawValue: "org.joinmastodon.Mastodon.AudioPlayer.appWillPlayAudio") + static let appWillPlayAudioNotification = NSNotification.Name(rawValue: "org.joinmastodon.Mastodon.audio-playback-service.appWillPlayAudio") var disposeBag = Set() From 988723691e2b37346df3d5ca4ca061aa25a7d0a4 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 15 Mar 2021 16:28:22 +0800 Subject: [PATCH 23/31] fix: content warning overlay invalid due to cell reuse issue --- Mastodon/Diffiable/Section/StatusSection.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index f6a4b918b..8f89cdd3f 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -182,6 +182,7 @@ extension StatusSection { let isStatusSensitive = statusItemAttribute.isStatusSensitive cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 + cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isUserInteractionEnabled = isStatusSensitive // set audio if let audioAttachment = mediaAttachments.filter({ $0.type == .audio }).first { From 6a8dee037f17b764ee63756ae1eeff7e760f11ee Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 15 Mar 2021 17:52:28 +0800 Subject: [PATCH 24/31] fix: media not response for reblog issue --- .../StatusProvider+UITableViewDelegate.swift | 1 + .../PlayerContainerView+MediaTypeIndicotorView.swift | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift index 3d7074d3f..89ae8e6eb 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift @@ -71,6 +71,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { toot(for: cell, indexPath: indexPath) .sink { [weak self] toot in guard let self = self else { return } + let toot = toot?.reblog ?? toot guard let media = (toot?.mediaAttachments ?? Set()).first else { return } guard let videoPlayerViewModel = self.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: media) else { return } diff --git a/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift b/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift new file mode 100644 index 000000000..0accc40b6 --- /dev/null +++ b/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift @@ -0,0 +1,8 @@ +// +// PlayerContainerView+MediaTypeIndicotorView.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-15. +// + +import Foundation From 5b45224f7bfabefb1ea564fc8ed5e48cbc083771 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 15 Mar 2021 17:53:06 +0800 Subject: [PATCH 25/31] feat: make media indicator view hide when playing video --- Mastodon.xcodeproj/project.pbxproj | 4 + .../Diffiable/Section/StatusSection.swift | 17 ++++ ...ContainerView+MediaTypeIndicotorView.swift | 93 ++++++++++++++++++- .../View/Container/PlayerContainerView.swift | 79 ++++++---------- 4 files changed, 139 insertions(+), 54 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index d3bea6e88..9859676c1 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -143,6 +143,7 @@ DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */; }; DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; }; DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */; }; + DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; }; DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; }; DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; }; DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; }; @@ -400,6 +401,7 @@ DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = ""; }; DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = ""; }; DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContext.swift; sourceTree = ""; }; + DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlayerContainerView+MediaTypeIndicotorView.swift"; sourceTree = ""; }; DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = ""; }; DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = ""; }; DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = ""; }; @@ -1190,6 +1192,7 @@ DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */, 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */, 5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */, + DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */, 5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */, ); path = Container; @@ -1675,6 +1678,7 @@ DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */, 2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */, 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */, + DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */, DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */, 2D206B8625F5FB0900143C56 /* Double.swift in Sources */, diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 8f89cdd3f..fad4898d5 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -222,6 +222,23 @@ extension StatusSection { playerViewController.player = videoPlayerViewModel.player playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind) + if videoPlayerViewModel.videoKind == .gif { + playerContainerView.setMediaIndicator(isHidden: false) + } else { + videoPlayerViewModel.timeControlStatus.sink { timeControlStatus in + UIView.animate(withDuration: 0.33) { + switch timeControlStatus { + case .playing: + playerContainerView.setMediaIndicator(isHidden: true) + case .paused, .waitingToPlayAtSpecifiedRate: + playerContainerView.setMediaIndicator(isHidden: false) + @unknown default: + assertionFailure() + } + } + } + .store(in: &cell.disposeBag) + } playerContainerView.isHidden = false } else { diff --git a/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift b/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift index 0accc40b6..3bff6ef75 100644 --- a/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift +++ b/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift @@ -5,4 +5,95 @@ // Created by MainasuK Cirno on 2021-3-15. // -import Foundation +import UIKit + +extension PlayerContainerView { + + final class MediaTypeIndicotorView: UIView { + + static let indicatorViewSize = CGSize(width: 47, height: 25) + + let maskLayer = CAShapeLayer() + + let label: UILabel = { + let label = UILabel() + label.textColor = .white + label.textAlignment = .right + label.adjustsFontSizeToFitWidth = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + + override func layoutSubviews() { + super.layoutSubviews() + + let path = UIBezierPath() + path.move(to: CGPoint(x: bounds.width, y: bounds.height)) + path.addLine(to: CGPoint(x: bounds.width, y: 0)) + path.addLine(to: CGPoint(x: bounds.width * 0.5, y: 0)) + path.addCurve( + to: CGPoint(x: 0, y: bounds.height), + controlPoint1: CGPoint(x: bounds.width * 0.2, y: 0), + controlPoint2: CGPoint(x: 0, y: bounds.height * 0.3) + ) + path.close() + + maskLayer.frame = bounds + maskLayer.path = path.cgPath + layer.mask = maskLayer + } + } + +} + +extension PlayerContainerView.MediaTypeIndicotorView { + + private func _init() { + backgroundColor = Asset.Colors.Background.mediaTypeIndicotor.color + layoutMargins = UIEdgeInsets(top: 3, left: 13, bottom: 0, right: 6) + + addSubview(label) + NSLayoutConstraint.activate([ + label.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), + label.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), + label.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), + label.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor), + ]) + } + + private static func roundedFont(weight: UIFont.Weight,fontSize: CGFloat) -> UIFont { + let systemFont = UIFont.systemFont(ofSize: fontSize, weight: weight) + guard let descriptor = systemFont.fontDescriptor.withDesign(.rounded) else { return systemFont } + let roundedFont = UIFont(descriptor: descriptor, size: fontSize) + return roundedFont + } + + func setMediaKind(kind: VideoPlayerViewModel.Kind) { + let fontSize: CGFloat = 18 + + switch kind { + case .gif: + label.font = PlayerContainerView.MediaTypeIndicotorView.roundedFont(weight: .heavy, fontSize: fontSize) + label.text = "GIF" + case .video: + let configuration = UIImage.SymbolConfiguration(font: PlayerContainerView.MediaTypeIndicotorView.roundedFont(weight: .regular, fontSize: fontSize)) + let image = UIImage(systemName: "video.fill", withConfiguration: configuration)! + let attachment = NSTextAttachment() + attachment.image = image.withTintColor(.white) + label.attributedText = NSAttributedString(attachment: attachment) + } + } + +} + + diff --git a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift index 3b4acc98c..cabed9560 100644 --- a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift +++ b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift @@ -26,26 +26,8 @@ final class PlayerContainerView: UIView { let playerViewController = AVPlayerViewController() - let mediaTypeIndicotorLabel: UILabel = { - let label = UILabel() - label.textColor = .white - label.textAlignment = .right - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - let mediaTypeIndicotorView: UIView = { - let view = UIView() - view.backgroundColor = Asset.Colors.Background.mediaTypeIndicotor.color - view.translatesAutoresizingMaskIntoConstraints = false - let rect = CGRect(x: 0, y: 0, width: 47, height: 50) - let path = UIBezierPath(roundedRect: rect, byRoundingCorners: [.topLeft], cornerRadii: CGSize(width: 50, height: 50)) - let maskLayer = CAShapeLayer() - maskLayer.frame = rect - maskLayer.path = path.cgPath - view.layer.mask = maskLayer - return view - }() + let mediaTypeIndicotorView = MediaTypeIndicotorView() + let mediaTypeIndicotorViewInContentWarningOverlay = MediaTypeIndicotorView() weak var delegate: PlayerContainerViewDelegate? @@ -78,6 +60,16 @@ extension PlayerContainerView { playerViewController.view.layer.cornerRadius = PlayerContainerView.cornerRadius playerViewController.view.layer.cornerCurve = .continuous + // mediaType + mediaTypeIndicotorView.translatesAutoresizingMaskIntoConstraints = false + playerViewController.contentOverlayView!.addSubview(mediaTypeIndicotorView) + NSLayoutConstraint.activate([ + mediaTypeIndicotorView.bottomAnchor.constraint(equalTo: playerViewController.contentOverlayView!.bottomAnchor), + mediaTypeIndicotorView.trailingAnchor.constraint(equalTo: playerViewController.contentOverlayView!.trailingAnchor), + mediaTypeIndicotorView.heightAnchor.constraint(equalToConstant: 25).priority(.defaultHigh), + mediaTypeIndicotorView.widthAnchor.constraint(equalToConstant: 47).priority(.defaultHigh), + ]) + addSubview(contentWarningOverlayView) NSLayoutConstraint.activate([ contentWarningOverlayView.topAnchor.constraint(equalTo: topAnchor), @@ -87,21 +79,13 @@ extension PlayerContainerView { ]) contentWarningOverlayView.delegate = self - // mediaType - addSubview(mediaTypeIndicotorView) + mediaTypeIndicotorViewInContentWarningOverlay.translatesAutoresizingMaskIntoConstraints = false + contentWarningOverlayView.addSubview(mediaTypeIndicotorViewInContentWarningOverlay) NSLayoutConstraint.activate([ - mediaTypeIndicotorView.bottomAnchor.constraint(equalTo: bottomAnchor), - mediaTypeIndicotorView.trailingAnchor.constraint(equalTo: trailingAnchor), - mediaTypeIndicotorView.heightAnchor.constraint(equalToConstant: 25), - mediaTypeIndicotorView.widthAnchor.constraint(equalToConstant: 47) - ]) - - mediaTypeIndicotorView.addSubview(mediaTypeIndicotorLabel) - NSLayoutConstraint.activate([ - mediaTypeIndicotorLabel.topAnchor.constraint(equalTo: mediaTypeIndicotorView.topAnchor), - mediaTypeIndicotorLabel.leadingAnchor.constraint(equalTo: mediaTypeIndicotorView.leadingAnchor), - mediaTypeIndicotorLabel.bottomAnchor.constraint(equalTo: mediaTypeIndicotorView.bottomAnchor), - mediaTypeIndicotorView.trailingAnchor.constraint(equalTo: mediaTypeIndicotorLabel.trailingAnchor, constant: 8) + mediaTypeIndicotorViewInContentWarningOverlay.bottomAnchor.constraint(equalTo: contentWarningOverlayView.bottomAnchor), + mediaTypeIndicotorViewInContentWarningOverlay.trailingAnchor.constraint(equalTo: contentWarningOverlayView.trailingAnchor), + mediaTypeIndicotorViewInContentWarningOverlay.heightAnchor.constraint(equalToConstant: 25).priority(.defaultHigh), + mediaTypeIndicotorViewInContentWarningOverlay.widthAnchor.constraint(equalToConstant: 47).priority(.defaultHigh), ]) } } @@ -161,25 +145,14 @@ extension PlayerContainerView { return playerViewController } - func roundedFont(weight: UIFont.Weight,fontSize: CGFloat) -> UIFont { - let systemFont = UIFont.systemFont(ofSize: fontSize, weight: weight) - guard let descriptor = systemFont.fontDescriptor.withDesign(.rounded) else { return systemFont } - let roundedFont = UIFont(descriptor: descriptor, size: fontSize) - return roundedFont - } func setMediaKind(kind: VideoPlayerViewModel.Kind) { - let fontSize: CGFloat = 18 - - switch kind { - case .gif: - mediaTypeIndicotorLabel.font = roundedFont(weight: .heavy, fontSize: fontSize) - mediaTypeIndicotorLabel.text = "GIF" - case .video: - let configuration = UIImage.SymbolConfiguration(font: roundedFont(weight: .regular, fontSize: fontSize)) - let image = UIImage(systemName: "video.fill", withConfiguration: configuration)! - let attachment = NSTextAttachment() - attachment.image = image.withTintColor(.white) - mediaTypeIndicotorLabel.attributedText = NSAttributedString(attachment: attachment) - } + mediaTypeIndicotorView.setMediaKind(kind: kind) + mediaTypeIndicotorViewInContentWarningOverlay.setMediaKind(kind: kind) } + + func setMediaIndicator(isHidden: Bool) { + mediaTypeIndicotorView.alpha = isHidden ? 0 : 1 + mediaTypeIndicotorViewInContentWarningOverlay.alpha = isHidden ? 0 : 1 + } + } From b9cfd0d9e8da3b327dc20a967ff10fa2a2ff72fc Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 15 Mar 2021 17:54:15 +0800 Subject: [PATCH 26/31] chore: remove magic number --- .../Scene/Share/View/Container/PlayerContainerView.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift index cabed9560..3401bfe9e 100644 --- a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift +++ b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift @@ -66,8 +66,8 @@ extension PlayerContainerView { NSLayoutConstraint.activate([ mediaTypeIndicotorView.bottomAnchor.constraint(equalTo: playerViewController.contentOverlayView!.bottomAnchor), mediaTypeIndicotorView.trailingAnchor.constraint(equalTo: playerViewController.contentOverlayView!.trailingAnchor), - mediaTypeIndicotorView.heightAnchor.constraint(equalToConstant: 25).priority(.defaultHigh), - mediaTypeIndicotorView.widthAnchor.constraint(equalToConstant: 47).priority(.defaultHigh), + mediaTypeIndicotorView.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.defaultHigh), + mediaTypeIndicotorView.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.defaultHigh), ]) addSubview(contentWarningOverlayView) @@ -84,8 +84,8 @@ extension PlayerContainerView { NSLayoutConstraint.activate([ mediaTypeIndicotorViewInContentWarningOverlay.bottomAnchor.constraint(equalTo: contentWarningOverlayView.bottomAnchor), mediaTypeIndicotorViewInContentWarningOverlay.trailingAnchor.constraint(equalTo: contentWarningOverlayView.trailingAnchor), - mediaTypeIndicotorViewInContentWarningOverlay.heightAnchor.constraint(equalToConstant: 25).priority(.defaultHigh), - mediaTypeIndicotorViewInContentWarningOverlay.widthAnchor.constraint(equalToConstant: 47).priority(.defaultHigh), + mediaTypeIndicotorViewInContentWarningOverlay.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.defaultHigh), + mediaTypeIndicotorViewInContentWarningOverlay.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.defaultHigh), ]) } } From ea5b05107db150169c27b5656207dba664357d1f Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 15 Mar 2021 17:58:03 +0800 Subject: [PATCH 27/31] fix: add bottom-right corner radius and fix RTL layout issue for media indicator view --- .../PlayerContainerView+MediaTypeIndicotorView.swift | 4 ++++ Mastodon/Scene/Share/View/Container/PlayerContainerView.swift | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift b/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift index 3bff6ef75..a76c27972 100644 --- a/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift +++ b/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift @@ -51,6 +51,10 @@ extension PlayerContainerView { maskLayer.frame = bounds maskLayer.path = path.cgPath layer.mask = maskLayer + + layer.cornerRadius = PlayerContainerView.cornerRadius + layer.maskedCorners = [.layerMaxXMaxYCorner] + layer.cornerCurve = .continuous } } diff --git a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift index 3401bfe9e..3a42560a9 100644 --- a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift +++ b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift @@ -65,7 +65,7 @@ extension PlayerContainerView { playerViewController.contentOverlayView!.addSubview(mediaTypeIndicotorView) NSLayoutConstraint.activate([ mediaTypeIndicotorView.bottomAnchor.constraint(equalTo: playerViewController.contentOverlayView!.bottomAnchor), - mediaTypeIndicotorView.trailingAnchor.constraint(equalTo: playerViewController.contentOverlayView!.trailingAnchor), + mediaTypeIndicotorView.rightAnchor.constraint(equalTo: playerViewController.contentOverlayView!.rightAnchor), mediaTypeIndicotorView.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.defaultHigh), mediaTypeIndicotorView.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.defaultHigh), ]) @@ -83,7 +83,7 @@ extension PlayerContainerView { contentWarningOverlayView.addSubview(mediaTypeIndicotorViewInContentWarningOverlay) NSLayoutConstraint.activate([ mediaTypeIndicotorViewInContentWarningOverlay.bottomAnchor.constraint(equalTo: contentWarningOverlayView.bottomAnchor), - mediaTypeIndicotorViewInContentWarningOverlay.trailingAnchor.constraint(equalTo: contentWarningOverlayView.trailingAnchor), + mediaTypeIndicotorViewInContentWarningOverlay.rightAnchor.constraint(equalTo: contentWarningOverlayView.rightAnchor), mediaTypeIndicotorViewInContentWarningOverlay.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.defaultHigh), mediaTypeIndicotorViewInContentWarningOverlay.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.defaultHigh), ]) From 4005581d241754a6a5e2cebf11909792fe6adf51 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 15 Mar 2021 18:02:42 +0800 Subject: [PATCH 28/31] chore: add Xcode previews --- ...ContainerView+MediaTypeIndicotorView.swift | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift b/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift index a76c27972..12f822986 100644 --- a/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift +++ b/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift @@ -100,4 +100,39 @@ extension PlayerContainerView.MediaTypeIndicotorView { } +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct PlayerContainerViewMediaTypeIndicotorView_Previews: PreviewProvider { + + static var previews: some View { + Group { + UIViewPreview(width: 47) { + let view = PlayerContainerView.MediaTypeIndicotorView() + view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + view.heightAnchor.constraint(equalToConstant: 25), + view.widthAnchor.constraint(equalToConstant: 47), + ]) + view.setMediaKind(kind: .gif) + return view + } + .previewLayout(.fixed(width: 47, height: 25)) + UIViewPreview(width: 47) { + let view = PlayerContainerView.MediaTypeIndicotorView() + view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + view.heightAnchor.constraint(equalToConstant: 25), + view.widthAnchor.constraint(equalToConstant: 47), + ]) + view.setMediaKind(kind: .video) + return view + } + .previewLayout(.fixed(width: 47, height: 25)) + } + } + +} + +#endif From 6eb3816bab5e660ef75a5a19635b6fe3c2a2bd5d Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 15 Mar 2021 18:19:45 +0800 Subject: [PATCH 29/31] chore: renaming reblog --- .../Diffiable/Section/StatusSection.swift | 12 ++--- ...Provider+StatusTableViewCellDelegate.swift | 4 +- .../StatusProvider/StatusProviderFacade.swift | 39 ++++++++------ .../Scene/Share/View/Content/StatusView.swift | 4 +- .../TableviewCell/StatusTableViewCell.swift | 6 +-- .../View/ToolBar/ActionToolBarContainer.swift | 38 ++++++------- .../APIService/APIService+Reblog.swift | 28 ++++++---- .../MastodonSDK/API/Mastodon+API+Reblog.swift | 54 ++++++++----------- 8 files changed, 94 insertions(+), 91 deletions(-) diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index fad4898d5..90669d391 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -294,7 +294,7 @@ extension StatusSection { let toot = object as? Toot else { return } StatusSection.configureActionToolBar(cell: cell, toot: toot, requestUserID: requestUserID) - os_log("%{public}s[%{public}ld], %{public}s: boost count label for toot %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, toot.id, toot.reblogsCount.intValue) + os_log("%{public}s[%{public}ld], %{public}s: reblog count label for toot %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, toot.id, toot.reblogsCount.intValue) os_log("%{public}s[%{public}ld], %{public}s: like count label for toot %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, toot.id, toot.favouritesCount.intValue) } .store(in: &cell.disposeBag) @@ -313,14 +313,14 @@ extension StatusSection { return StatusSection.formattedNumberTitleForActionButton(count) }() cell.statusView.actionToolbarContainer.replyButton.setTitle(replyCountTitle, for: .normal) - // set boost - let isBoosted = toot.rebloggedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false - let boostCountTitle: String = { + // set reblog + let isReblogged = toot.rebloggedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false + let reblogCountTitle: String = { let count = toot.reblogsCount.intValue return StatusSection.formattedNumberTitleForActionButton(count) }() - cell.statusView.actionToolbarContainer.boostButton.setTitle(boostCountTitle, for: .normal) - cell.statusView.actionToolbarContainer.isBoostButtonHighlight = isBoosted + cell.statusView.actionToolbarContainer.reblogButton.setTitle(reblogCountTitle, for: .normal) + cell.statusView.actionToolbarContainer.isReblogButtonHighlight = isReblogged // set like let isLike = toot.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false let favoriteCountTitle: String = { diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift index fddd13049..7650471fd 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift @@ -16,8 +16,8 @@ import ActiveLabel // MARK: - ActionToolbarContainerDelegate extension StatusTableViewCellDelegate where Self: StatusProvider { - func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, boostButtonDidPressed sender: UIButton) { - StatusProviderFacade.responseToStatusBoostAction(provider: self, cell: cell) + func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, reblogButtonDidPressed sender: UIButton) { + StatusProviderFacade.responseToStatusReblogAction(provider: self, cell: cell) } func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) { diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift index 9d1e33219..0febef175 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift @@ -130,7 +130,7 @@ extension StatusProviderFacade { ) } - static func responseToStatusBoostAction(provider: StatusProvider, cell: UITableViewCell) { + static func responseToStatusReblogAction(provider: StatusProvider, cell: UITableViewCell) { _responseToStatusBoostAction( provider: provider, toot: provider.toot(for: cell, indexPath: nil) @@ -160,21 +160,21 @@ extension StatusProviderFacade { let responseFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium) toot - .compactMap { toot -> (NSManagedObjectID, Mastodon.API.Reblog.BoostKind)? in + .compactMap { toot -> (NSManagedObjectID, Mastodon.API.Reblog.ReblogKind)? in guard let toot = toot?.reblog ?? toot else { return nil } - let boostKind: Mastodon.API.Reblog.BoostKind = { - let isBoosted = toot.rebloggedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false - return isBoosted ? .undoBoost : .boost + let reblogKind: Mastodon.API.Reblog.ReblogKind = { + let isReblogged = toot.rebloggedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false + return isReblogged ? .undoReblog : .reblog(query: .init(visibility: nil)) }() - return (toot.objectID, boostKind) + return (toot.objectID, reblogKind) } - .map { tootObjectID, boostKind -> AnyPublisher<(Toot.ID, Mastodon.API.Reblog.BoostKind), Error> in - return context.apiService.boost( + .map { tootObjectID, reblogKind -> AnyPublisher<(Toot.ID, Mastodon.API.Reblog.ReblogKind), Error> in + return context.apiService.reblog( tootObjectID: tootObjectID, mastodonUserObjectID: mastodonUserObjectID, - boostKind: boostKind + reblogKind: reblogKind ) - .map { tootID in (tootID, boostKind) } + .map { tootID in (tootID, reblogKind) } .eraseToAnyPublisher() } .setFailureType(to: Error.self) @@ -184,9 +184,14 @@ extension StatusProviderFacade { .handleEvents { _ in generator.prepare() responseFeedbackGenerator.prepare() - } receiveOutput: { _, boostKind in + } receiveOutput: { _, reblogKind in generator.impactOccurred() - os_log("%{public}s[%{public}ld], %{public}s: [Boost] update local toot reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, boostKind == .boost ? "boost" : "unboost") + switch reblogKind { + case .reblog: + os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local toot reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "reblog") + case .undoReblog: + os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local toot reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "unboost") + } } receiveCompletion: { completion in switch completion { case .failure: @@ -196,10 +201,10 @@ extension StatusProviderFacade { break } } - .map { tootID, boostKind in - return context.apiService.boost( + .map { tootID, reblogKind in + return context.apiService.reblog( statusID: tootID, - boostKind: boostKind, + reblogKind: reblogKind, mastodonAuthenticationBox: activeMastodonAuthenticationBox ) } @@ -212,9 +217,9 @@ extension StatusProviderFacade { } switch completion { case .failure(let error): - os_log("%{public}s[%{public}ld], %{public}s: [Boost] remote boost request fail: %{public}s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + os_log("%{public}s[%{public}ld], %{public}s: [Reblog] remote reblog request fail: %{public}s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) case .finished: - os_log("%{public}s[%{public}ld], %{public}s: [Boost] remote boost request success", ((#file as NSString).lastPathComponent), #line, #function) + os_log("%{public}s[%{public}ld], %{public}s: [Reblog] remote reblog request success", ((#file as NSString).lastPathComponent), #line, #function) } } receiveValue: { response in // do nothing diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 09b9ce282..27aa3ecf8 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -49,7 +49,7 @@ final class StatusView: UIView { let label = UILabel() label.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium)) label.textColor = Asset.Colors.Label.secondary.color - label.text = "Bob boosted" + label.text = "Bob reblogged" return label }() @@ -491,7 +491,7 @@ struct StatusView_Previews: PreviewProvider { return statusView } .previewLayout(.fixed(width: 375, height: 200)) - .previewDisplayName("Boost") + .previewDisplayName("Reblog") UIViewPreview(width: 375) { let statusView = StatusView(frame: CGRect(x: 0, y: 0, width: 375, height: 500)) statusView.configure( diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index 1723e47c3..75845d6a7 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -25,7 +25,7 @@ protocol StatusTableViewCellDelegate: class { func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) func statusTableViewCell(_ cell: StatusTableViewCell, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) - func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, boostButtonDidPressed sender: UIButton) + func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, reblogButtonDidPressed sender: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton) @@ -227,8 +227,8 @@ extension StatusTableViewCell: ActionToolbarContainerDelegate { func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) { } - func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, boostButtonDidPressed sender: UIButton) { - delegate?.statusTableViewCell(self, actionToolbarContainer: actionToolbarContainer, boostButtonDidPressed: sender) + func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, reblogButtonDidPressed sender: UIButton) { + delegate?.statusTableViewCell(self, actionToolbarContainer: actionToolbarContainer, reblogButtonDidPressed: sender) } func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, starButtonDidPressed sender: UIButton) { delegate?.statusTableViewCell(self, actionToolbarContainer: actionToolbarContainer, likeButtonDidPressed: sender) diff --git a/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift b/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift index 991592c13..daaa607d9 100644 --- a/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift +++ b/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift @@ -10,7 +10,7 @@ import UIKit protocol ActionToolbarContainerDelegate: class { func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) - func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, boostButtonDidPressed sender: UIButton) + func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, reblogButtonDidPressed sender: UIButton) func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, starButtonDidPressed sender: UIButton) func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, moreButtonDidPressed sender: UIButton) } @@ -19,12 +19,12 @@ protocol ActionToolbarContainerDelegate: class { final class ActionToolbarContainer: UIView { let replyButton = HitTestExpandedButton() - let boostButton = HitTestExpandedButton() + let reblogButton = HitTestExpandedButton() let favoriteButton = HitTestExpandedButton() let moreButton = HitTestExpandedButton() - var isBoostButtonHighlight: Bool = false { - didSet { isBoostButtonHighlightStateDidChange(to: isBoostButtonHighlight) } + var isReblogButtonHighlight: Bool = false { + didSet { isReblogButtonHighlightStateDidChange(to: isReblogButtonHighlight) } } var isFavoriteButtonHighlight: Bool = false { @@ -61,7 +61,7 @@ extension ActionToolbarContainer { ]) replyButton.addTarget(self, action: #selector(ActionToolbarContainer.replyButtonDidPressed(_:)), for: .touchUpInside) - boostButton.addTarget(self, action: #selector(ActionToolbarContainer.boostButtonDidPressed(_:)), for: .touchUpInside) + reblogButton.addTarget(self, action: #selector(ActionToolbarContainer.reblogButtonDidPressed(_:)), for: .touchUpInside) favoriteButton.addTarget(self, action: #selector(ActionToolbarContainer.favoriteButtonDidPressed(_:)), for: .touchUpInside) moreButton.addTarget(self, action: #selector(ActionToolbarContainer.moreButtonDidPressed(_:)), for: .touchUpInside) } @@ -93,7 +93,7 @@ extension ActionToolbarContainer { subview.removeFromSuperview() } - let buttons = [replyButton, boostButton, favoriteButton, moreButton] + let buttons = [replyButton, reblogButton, favoriteButton, moreButton] buttons.forEach { button in button.tintColor = Asset.Colors.Button.actionToolbar.color button.titleLabel?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular) @@ -113,7 +113,7 @@ extension ActionToolbarContainer { button.contentHorizontalAlignment = .leading } replyButton.setImage(replyImage, for: .normal) - boostButton.setImage(reblogImage, for: .normal) + reblogButton.setImage(reblogImage, for: .normal) favoriteButton.setImage(starImage, for: .normal) moreButton.setImage(moreImage, for: .normal) @@ -121,19 +121,19 @@ extension ActionToolbarContainer { container.distribution = .fill replyButton.translatesAutoresizingMaskIntoConstraints = false - boostButton.translatesAutoresizingMaskIntoConstraints = false + reblogButton.translatesAutoresizingMaskIntoConstraints = false favoriteButton.translatesAutoresizingMaskIntoConstraints = false moreButton.translatesAutoresizingMaskIntoConstraints = false container.addArrangedSubview(replyButton) - container.addArrangedSubview(boostButton) + container.addArrangedSubview(reblogButton) container.addArrangedSubview(favoriteButton) container.addArrangedSubview(moreButton) NSLayoutConstraint.activate([ replyButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh), - replyButton.heightAnchor.constraint(equalTo: boostButton.heightAnchor).priority(.defaultHigh), + replyButton.heightAnchor.constraint(equalTo: reblogButton.heightAnchor).priority(.defaultHigh), replyButton.heightAnchor.constraint(equalTo: favoriteButton.heightAnchor).priority(.defaultHigh), replyButton.heightAnchor.constraint(equalTo: moreButton.heightAnchor).priority(.defaultHigh), - replyButton.widthAnchor.constraint(equalTo: boostButton.widthAnchor).priority(.defaultHigh), + replyButton.widthAnchor.constraint(equalTo: reblogButton.widthAnchor).priority(.defaultHigh), replyButton.widthAnchor.constraint(equalTo: favoriteButton.widthAnchor).priority(.defaultHigh), ]) moreButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) @@ -144,7 +144,7 @@ extension ActionToolbarContainer { button.contentHorizontalAlignment = .center } replyButton.setImage(replyImage, for: .normal) - boostButton.setImage(reblogImage, for: .normal) + reblogButton.setImage(reblogImage, for: .normal) favoriteButton.setImage(starImage, for: .normal) container.axis = .horizontal @@ -152,7 +152,7 @@ extension ActionToolbarContainer { container.distribution = .fillEqually container.addArrangedSubview(replyButton) - container.addArrangedSubview(boostButton) + container.addArrangedSubview(reblogButton) container.addArrangedSubview(favoriteButton) } } @@ -162,11 +162,11 @@ extension ActionToolbarContainer { return oldStyle != style } - private func isBoostButtonHighlightStateDidChange(to isHighlight: Bool) { + private func isReblogButtonHighlightStateDidChange(to isHighlight: Bool) { let tintColor = isHighlight ? Asset.Colors.systemGreen.color : Asset.Colors.Button.actionToolbar.color - boostButton.tintColor = tintColor - boostButton.setTitleColor(tintColor, for: .normal) - boostButton.setTitleColor(tintColor, for: .highlighted) + reblogButton.tintColor = tintColor + reblogButton.setTitleColor(tintColor, for: .normal) + reblogButton.setTitleColor(tintColor, for: .highlighted) } private func isFavoriteButtonHighlightStateDidChange(to isHighlight: Bool) { @@ -184,9 +184,9 @@ extension ActionToolbarContainer { delegate?.actionToolbarContainer(self, replayButtonDidPressed: sender) } - @objc private func boostButtonDidPressed(_ sender: UIButton) { + @objc private func reblogButtonDidPressed(_ sender: UIButton) { os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - delegate?.actionToolbarContainer(self, boostButtonDidPressed: sender) + delegate?.actionToolbarContainer(self, reblogButtonDidPressed: sender) } @objc private func favoriteButtonDidPressed(_ sender: UIButton) { diff --git a/Mastodon/Service/APIService/APIService+Reblog.swift b/Mastodon/Service/APIService/APIService+Reblog.swift index 3f750954f..92ff85c10 100644 --- a/Mastodon/Service/APIService/APIService+Reblog.swift +++ b/Mastodon/Service/APIService/APIService+Reblog.swift @@ -15,10 +15,10 @@ import CommonOSLog extension APIService { // make local state change only - func boost( + func reblog( tootObjectID: NSManagedObjectID, mastodonUserObjectID: NSManagedObjectID, - boostKind: Mastodon.API.Reblog.BoostKind + reblogKind: Mastodon.API.Reblog.ReblogKind ) -> AnyPublisher { var _targetTootID: Toot.ID? let managedObjectContext = backgroundManagedObjectContext @@ -29,7 +29,12 @@ extension APIService { let targetTootID = targetToot.id _targetTootID = targetTootID - targetToot.update(reblogged: boostKind == .boost, mastodonUser: mastodonUser) + switch reblogKind { + case .reblog: + targetToot.update(reblogged: true, mastodonUser: mastodonUser) + case .undoReblog: + targetToot.update(reblogged: false, mastodonUser: mastodonUser) + } } .tryMap { result in @@ -48,20 +53,20 @@ extension APIService { .eraseToAnyPublisher() } - // send boost request to remote - func boost( + // send reblog request to remote + func reblog( statusID: Mastodon.Entity.Status.ID, - boostKind: Mastodon.API.Reblog.BoostKind, + reblogKind: Mastodon.API.Reblog.ReblogKind, mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox ) -> AnyPublisher, Error> { let domain = mastodonAuthenticationBox.domain let authorization = mastodonAuthenticationBox.userAuthorization let requestMastodonUserID = mastodonAuthenticationBox.userID - return Mastodon.API.Reblog.boost( + return Mastodon.API.Reblog.reblog( session: session, domain: domain, statusID: statusID, - boostKind: boostKind, + reblogKind: reblogKind, authorization: authorization ) .map { response -> AnyPublisher, Error> in @@ -92,10 +97,13 @@ extension APIService { } APIService.CoreData.merge(toot: oldToot, entity: entity.reblog ?? entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate) - if boostKind == .undoBoost { + switch reblogKind { + case .undoReblog: oldToot.update(reblogsCount: NSNumber(value: max(0, oldToot.reblogsCount.intValue - 1))) + default: + break } - os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update toot %{public}s reblog status to: %{public}s. now %ld boosts", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.reblogged.flatMap { $0 ? "boost" : "unboost" } ?? "", entity.reblogsCount ) + os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update toot %{public}s reblog status to: %{public}s. now %ld reblog", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.reblogged.flatMap { $0 ? "reblog" : "unreblog" } ?? "", entity.reblogsCount ) } .setFailureType(to: Error.self) .tryMap { result -> Mastodon.Response.Content in diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Reblog.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Reblog.swift index 862028d33..cb7a96188 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Reblog.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Reblog.swift @@ -10,7 +10,7 @@ import Combine extension Mastodon.API.Reblog { - static func boostedByEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL { + static func rebloggedByEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL { let pathComponent = "statuses/" + statusID + "/reblogged_by" return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent) } @@ -22,7 +22,7 @@ extension Mastodon.API.Reblog { /// - Since: 0.0.0 /// - Version: 3.3.0 /// # Last Update - /// 2021/3/9 + /// 2021/3/15 /// # Reference /// [Document](https://docs.joinmastodon.org/methods/statuses/) /// - Parameters: @@ -31,14 +31,14 @@ extension Mastodon.API.Reblog { /// - statusID: id for status /// - authorization: User token. Could be nil if status is public /// - Returns: `AnyPublisher` contains `Status` nested in the response - public static func poll( + public static func rebloggedBy( session: URLSession, domain: String, statusID: Mastodon.Entity.Status.ID, authorization: Mastodon.API.OAuth.Authorization? ) -> AnyPublisher, Error> { let request = Mastodon.API.get( - url: boostedByEndpointURL(domain: domain, statusID: statusID), + url: rebloggedByEndpointURL(domain: domain, statusID: statusID), query: nil, authorization: authorization ) @@ -66,7 +66,7 @@ extension Mastodon.API.Reblog { /// - Since: 0.0.0 /// - Version: 3.3.0 /// # Last Update - /// 2021/3/9 + /// 2021/3/15 /// # Reference /// [Document](https://docs.joinmastodon.org/methods/statuses/) /// - Parameters: @@ -75,11 +75,11 @@ extension Mastodon.API.Reblog { /// - statusID: id for status /// - authorization: User token. /// - Returns: `AnyPublisher` contains `Status` nested in the response - public static func boost( + public static func reblog( session: URLSession, domain: String, statusID: Mastodon.Entity.Status.ID, - query: BoostQuery, + query: ReblogQuery, authorization: Mastodon.API.OAuth.Authorization ) -> AnyPublisher, Error> { let request = Mastodon.API.post( @@ -97,10 +97,10 @@ extension Mastodon.API.Reblog { public typealias Visibility = Mastodon.Entity.Source.Privacy - public struct BoostQuery: Codable, PostQuery { - public let visibility: Visibility + public struct ReblogQuery: Codable, PostQuery { + public let visibility: Visibility? - public init(visibility: Visibility) { + public init(visibility: Visibility?) { self.visibility = visibility } } @@ -114,7 +114,7 @@ extension Mastodon.API.Reblog { return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent) } - /// Undo boost + /// Undo reblog /// /// Undo a reshare of a status. /// @@ -130,7 +130,7 @@ extension Mastodon.API.Reblog { /// - statusID: id for status /// - authorization: User token. /// - Returns: `AnyPublisher` contains `Status` nested in the response - public static func undoBoost( + public static func undoReblog( session: URLSession, domain: String, statusID: Mastodon.Entity.Status.ID, @@ -153,34 +153,24 @@ extension Mastodon.API.Reblog { extension Mastodon.API.Reblog { - public enum BoostKind { - case boost - case undoBoost + public enum ReblogKind { + case reblog(query: ReblogQuery) + case undoReblog } - public static func boost( + public static func reblog( session: URLSession, domain: String, statusID: Mastodon.Entity.Status.ID, - boostKind: BoostKind, + reblogKind: ReblogKind, authorization: Mastodon.API.OAuth.Authorization ) -> AnyPublisher, Error> { - let url: URL - switch boostKind { - case .boost: url = reblogEndpointURL(domain: domain, statusID: statusID) - case .undoBoost: url = unreblogEndpointURL(domain: domain, statusID: statusID) + switch reblogKind { + case .reblog(let query): + return reblog(session: session, domain: domain, statusID: statusID, query: query, authorization: authorization) + case .undoReblog: + return undoReblog(session: session, domain: domain, statusID: statusID, authorization: authorization) } - let request = Mastodon.API.post( - url: url, - query: nil, - authorization: authorization - ) - return session.dataTaskPublisher(for: request) - .tryMap { data, response in - let value = try Mastodon.API.decode(type: Mastodon.Entity.Status.self, from: data, response: response) - return Mastodon.Response.Content(value: value, response: response) - } - .eraseToAnyPublisher() } } From fdf5b5fa663a7d98cdcf328731a146191a4c5931 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 15 Mar 2021 18:22:44 +0800 Subject: [PATCH 30/31] chore: update i18n --- Localization/app.json | 2 +- Mastodon/Diffiable/Section/StatusSection.swift | 2 +- Mastodon/Generated/Strings.swift | 6 +++--- .../Protocol/StatusProvider/StatusProviderFacade.swift | 10 +++++----- Mastodon/Resources/en.lproj/Localizable.strings | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 123655955..4c79a97bb 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -36,7 +36,7 @@ "open_in_safari": "Open in Safari" }, "status": { - "user_boosted": "%s boosted", + "user_reblogged": "%s reblogged", "show_post": "Show Post", "status_content_warning": "content warning", "media_content_warning": "Tap to reveal that may be sensitive", diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 90669d391..4b0532d5c 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -99,7 +99,7 @@ extension StatusSection { cell.statusView.headerInfoLabel.text = { let author = toot.author let name = author.displayName.isEmpty ? author.username : author.displayName - return L10n.Common.Controls.Status.userBoosted(name) + return L10n.Common.Controls.Status.userReblogged(name) }() // set name username diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 7c595918e..399d1a5e9 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -76,9 +76,9 @@ internal enum L10n { 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)) + /// %@ reblogged + internal static func userReblogged(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Status.UserReblogged", String(describing: p1)) } internal enum Poll { /// Closed diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift index 0febef175..cab16e68c 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift @@ -123,21 +123,21 @@ extension StatusProviderFacade { extension StatusProviderFacade { - static func responseToStatusBoostAction(provider: StatusProvider) { - _responseToStatusBoostAction( + static func responseToStatusReblogAction(provider: StatusProvider) { + _responseToStatusReblogAction( provider: provider, toot: provider.toot() ) } static func responseToStatusReblogAction(provider: StatusProvider, cell: UITableViewCell) { - _responseToStatusBoostAction( + _responseToStatusReblogAction( provider: provider, toot: provider.toot(for: cell, indexPath: nil) ) } - private static func _responseToStatusBoostAction(provider: StatusProvider, toot: Future) { + private static func _responseToStatusReblogAction(provider: StatusProvider, toot: Future) { // prepare authentication guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { assertionFailure() @@ -190,7 +190,7 @@ extension StatusProviderFacade { case .reblog: os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local toot reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "reblog") case .undoReblog: - os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local toot reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "unboost") + os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local toot reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "unreblog") } } receiveCompletion: { completion in switch completion { diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 54b69e274..c79e3d0c2 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -30,7 +30,7 @@ "Common.Controls.Status.Poll.VoterCount.Single" = "%d voter"; "Common.Controls.Status.ShowPost" = "Show Post"; "Common.Controls.Status.StatusContentWarning" = "content warning"; -"Common.Controls.Status.UserBoosted" = "%@ boosted"; +"Common.Controls.Status.UserReblogged" = "%@ reblogged"; "Common.Controls.Timeline.LoadMore" = "Load More"; "Common.Countable.Photo.Multiple" = "photos"; "Common.Countable.Photo.Single" = "photo"; @@ -92,4 +92,4 @@ any server."; "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 +back in your hands."; From e6b9252e6c13a787d74ca2db75435f315fe2c164 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 15 Mar 2021 19:31:31 +0800 Subject: [PATCH 31/31] chore: remove redundant layout --- .../View/Container/MosaicImageViewContainer.swift | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift index 8e6884463..b3c03a46b 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift +++ b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift @@ -63,7 +63,6 @@ extension MosaicImageViewContainer: ContentWarningOverlayViewDelegate { extension MosaicImageViewContainer { private func _init() { - contentWarningOverlayView.delegate = self container.translatesAutoresizingMaskIntoConstraints = false container.axis = .horizontal container.distribution = .fillEqually @@ -77,13 +76,7 @@ extension MosaicImageViewContainer { containerHeightLayoutConstraint ]) - addSubview(contentWarningOverlayView) - NSLayoutConstraint.activate([ - contentWarningOverlayView.topAnchor.constraint(equalTo: container.topAnchor), - contentWarningOverlayView.leadingAnchor.constraint(equalTo: container.leadingAnchor), - contentWarningOverlayView.bottomAnchor.constraint(equalTo: container.bottomAnchor), - contentWarningOverlayView.trailingAnchor.constraint(equalTo: container.trailingAnchor), - ]) + contentWarningOverlayView.delegate = self } } @@ -101,6 +94,7 @@ extension MosaicImageViewContainer { contentWarningOverlayView.removeFromSuperview() contentWarningOverlayView.blurVisualEffectView.effect = ContentWarningOverlayView.blurVisualEffect contentWarningOverlayView.vibrancyVisualEffectView.alpha = 1.0 + contentWarningOverlayView.isUserInteractionEnabled = true imageViews = [] container.spacing = 1