From 567c2af0eea50833e15a003b19f4bd68e72eddeb Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 9 Apr 2021 19:39:35 +0800 Subject: [PATCH] chore: add followAction --- Mastodon.xcodeproj/project.pbxproj | 4 + .../Section/RecommendAccountSection.swift | 15 +- ...hRecommendAccountsCollectionViewCell.swift | 60 +++----- .../Search/SearchViewController+Follow.swift | 137 ++++++++++++++++++ .../Scene/Search/SearchViewController.swift | 2 +- Mastodon/Scene/Search/SearchViewModel.swift | 16 ++ 6 files changed, 185 insertions(+), 49 deletions(-) create mode 100644 Mastodon/Scene/Search/SearchViewController+Follow.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 01d5f1f2..fda5de62 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -135,6 +135,7 @@ 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 */; }; + 5DFC35DF262068D20045711D /* SearchViewController+Follow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */; }; 5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; }; 5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; }; 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; @@ -506,6 +507,7 @@ 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 = ""; }; + 5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Follow.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 = ""; }; 9780A4C98FFC65B32B50D1C0 /* Pods-MastodonTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release.xcconfig"; sourceTree = ""; }; A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1613,6 +1615,7 @@ DB9D6BE825E4F5340051B173 /* SearchViewController.swift */, 2D34D9CA261489930081BFC0 /* SearchViewController+Recommend.swift */, 2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */, + 5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */, 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */, 2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */, 2D34D9E026149C550081BFC0 /* CollectionViewCell */, @@ -2355,6 +2358,7 @@ 2D42FF6B25C817D2004A627A /* MastodonStatusContent.swift in Sources */, 2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */, DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */, + 5DFC35DF262068D20045711D /* SearchViewController+Follow.swift in Sources */, DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */, 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */, 2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */, diff --git a/Mastodon/Diffiable/Section/RecommendAccountSection.swift b/Mastodon/Diffiable/Section/RecommendAccountSection.swift index 409adee3..3ecd4e3b 100644 --- a/Mastodon/Diffiable/Section/RecommendAccountSection.swift +++ b/Mastodon/Diffiable/Section/RecommendAccountSection.swift @@ -5,11 +5,11 @@ // Created by sxiaojian on 2021/4/1. // +import CoreData +import CoreDataStack import Foundation import MastodonSDK import UIKit -import CoreData -import CoreDataStack enum RecommendAccountSection: Equatable, Hashable { case main @@ -18,15 +18,14 @@ enum RecommendAccountSection: Equatable, Hashable { extension RecommendAccountSection { static func collectionViewDiffableDataSource( for collectionView: UICollectionView, - context: AppContext! + delegate: SearchRecommendAccountsCollectionViewCellDelegate, + managedObjectContext: NSManagedObjectContext ) -> UICollectionViewDiffableDataSource { - UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, objectID -> UICollectionViewCell? in + UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak delegate] collectionView, indexPath, objectID -> UICollectionViewCell? in let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self), for: indexPath) as! SearchRecommendAccountsCollectionViewCell - let user = context.managedObjectContext.object(with: objectID) as! MastodonUser + let user = managedObjectContext.object(with: objectID) as! MastodonUser + cell.delegate = delegate cell.config(with: user) - if let currentUser = context.authenticationService.activeMastodonAuthentication.value?.user { - cell.configFollowButton(with: user, currentMastodonUser: currentUser) - } return cell } } diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index 933eeb42..c64db498 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -5,16 +5,23 @@ // Created by sxiaojian on 2021/4/1. // +import Combine +import CoreDataStack import Foundation import MastodonSDK import UIKit -import CoreDataStack -import Combine + +protocol SearchRecommendAccountsCollectionViewCellDelegate: NSObject { + func followButtonDidPressed(clickedUser: MastodonUser) + + func configFollowButton(with mastodonUser: MastodonUser, followButton: HighlightDimmableButton) +} class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { - var disposeBag = Set() + weak var delegate: SearchRecommendAccountsCollectionViewCellDelegate? + let avatarImageView: UIImageView = { let imageView = UIImageView() imageView.layer.cornerRadius = 8.4 @@ -52,8 +59,8 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { return label }() - let followButton: UIButton = { - let button = UIButton(type: .custom) + let followButton: HighlightDimmableButton = { + let button = HighlightDimmableButton(type: .custom) button.setTitleColor(.white, for: .normal) button.setTitle(L10n.Scene.Search.Recommend.Accounts.follow, for: .normal) button.titleLabel?.font = .systemFont(ofSize: 14, weight: .semibold) @@ -138,49 +145,22 @@ extension SearchRecommendAccountsCollectionViewCell { headerImageView.af.setImage( withURL: URL(string: mastodonUser.header)!, placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2)) { [weak self] _ in + imageTransition: .crossDissolve(0.2) + ) { [weak self] _ in guard let self = self else { return } self.headerImageView.addSubview(self.visualEffectView) self.visualEffectView.pin(top: 0, left: 0, bottom: 0, right: 0) } - } - - func configFollowButton(with mastodonUser: MastodonUser, currentMastodonUser: MastodonUser) { - self._configFollowButton(with: mastodonUser, currentMastodonUser: currentMastodonUser) - ManagedObjectObserver.observe(object: currentMastodonUser) - .sink { _ in - - } receiveValue: { change in - guard case .update(let object) = change.changeType, - let newUser = object as? MastodonUser else { return } - self._configFollowButton(with: mastodonUser, currentMastodonUser: newUser) + delegate?.configFollowButton(with: mastodonUser, followButton: followButton) + followButton.publisher(for: .touchUpInside) + .sink { [weak self] _ in + self?.followButtonDidPressed(mastodonUser: mastodonUser) } .store(in: &disposeBag) } - func _configFollowButton(with mastodonUser: MastodonUser, currentMastodonUser: MastodonUser) { - var relationshipActionSet = ProfileViewModel.RelationshipActionOptionSet([.follow]) - - let isFollowing = mastodonUser.followingBy.flatMap { $0.contains(currentMastodonUser) } ?? false - if isFollowing { - relationshipActionSet.insert(.following) - } - - let isPending = mastodonUser.followRequestedBy.flatMap { $0.contains(currentMastodonUser) } ?? false - if isPending { - relationshipActionSet.insert(.pending) - } - - let isBlocking = mastodonUser.blockingBy.flatMap { $0.contains(currentMastodonUser) } ?? false - if isBlocking { - relationshipActionSet.insert(.blocking) - } - - let isBlockedBy = currentMastodonUser.blockingBy.flatMap { $0.contains(mastodonUser) } ?? false - if isBlockedBy { - relationshipActionSet.insert(.blocked) - } - self.followButton.setTitle(relationshipActionSet.title, for: .normal) + func followButtonDidPressed(mastodonUser: MastodonUser) { + delegate?.followButtonDidPressed(clickedUser: mastodonUser) } } diff --git a/Mastodon/Scene/Search/SearchViewController+Follow.swift b/Mastodon/Scene/Search/SearchViewController+Follow.swift new file mode 100644 index 00000000..ce7d13b8 --- /dev/null +++ b/Mastodon/Scene/Search/SearchViewController+Follow.swift @@ -0,0 +1,137 @@ +// +// SearchViewController+Follow.swift +// Mastodon +// +// Created by xiaojian sun on 2021/4/9. +// + +import Combine +import CoreDataStack +import Foundation +import UIKit + +extension SearchViewController: UserProvider { + func mastodonUser() -> Future { + Future { promise in + promise(.success(self.viewModel.mastodonUser.value)) + } + } +} + +extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegate { + func followButtonDidPressed(clickedUser: MastodonUser) { + viewModel.mastodonUser.value = clickedUser + guard let currentMastodonUser = viewModel.currentMastodonUser.value else { + return + } + let relationshipAction = relationShipActionSet(mastodonUser: clickedUser, currentMastodonUser: currentMastodonUser) + switch relationshipAction { + case .none: + break + case .follow, .following: + UserProviderFacade.toggleUserFollowRelationship(provider: self) + .sink { _ in + + } receiveValue: { _ in + } + .store(in: &disposeBag) + case .pending: + break + case .muting: + guard let mastodonUser = viewModel.mastodonUser.value else { return } + let name = mastodonUser.displayNameWithFallback + let alertController = UIAlertController( + title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title, + message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name), + preferredStyle: .alert + ) + let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Firendship.unmute, style: .default) { [weak self] _ in + guard let self = self else { return } + UserProviderFacade.toggleUserMuteRelationship(provider: self) + .sink { _ in + // do nothing + } receiveValue: { _ in + // do nothing + } + .store(in: &self.context.disposeBag) + } + alertController.addAction(unmuteAction) + let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) + alertController.addAction(cancelAction) + present(alertController, animated: true, completion: nil) + case .blocking: + guard let mastodonUser = viewModel.mastodonUser.value else { return } + let name = mastodonUser.displayNameWithFallback + let alertController = UIAlertController( + title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.title, + message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.message(name), + preferredStyle: .alert + ) + let unblockAction = UIAlertAction(title: L10n.Common.Controls.Firendship.unblock, style: .default) { [weak self] _ in + guard let self = self else { return } + UserProviderFacade.toggleUserBlockRelationship(provider: self) + .sink { _ in + // do nothing + } receiveValue: { _ in + // do nothing + } + .store(in: &self.context.disposeBag) + } + alertController.addAction(unblockAction) + let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) + alertController.addAction(cancelAction) + present(alertController, animated: true, completion: nil) + case .blocked: + break + default: + assertionFailure() + } + } + + func configFollowButton(with mastodonUser: MastodonUser, followButton: HighlightDimmableButton) { + guard let currentMastodonUser = viewModel.currentMastodonUser.value else { + return + } + _configFollowButton(with: mastodonUser, currentMastodonUser: currentMastodonUser, followButton: followButton) + ManagedObjectObserver.observe(object: currentMastodonUser) + .sink { _ in + + } receiveValue: { change in + guard case .update(let object) = change.changeType, + let newUser = object as? MastodonUser else { return } + self._configFollowButton(with: mastodonUser, currentMastodonUser: newUser, followButton: followButton) + } + .store(in: &disposeBag) + } +} + +extension SearchViewController { + func _configFollowButton(with mastodonUser: MastodonUser, currentMastodonUser: MastodonUser, followButton: HighlightDimmableButton) { + let relationshipActionSet = relationShipActionSet(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser) + followButton.setTitle(relationshipActionSet.title, for: .normal) + } + + func relationShipActionSet(mastodonUser: MastodonUser, currentMastodonUser: MastodonUser) -> ProfileViewModel.RelationshipActionOptionSet { + var relationshipActionSet = ProfileViewModel.RelationshipActionOptionSet([.follow]) + let isFollowing = mastodonUser.followingBy.flatMap { $0.contains(currentMastodonUser) } ?? false + if isFollowing { + relationshipActionSet.insert(.following) + } + + let isPending = mastodonUser.followRequestedBy.flatMap { $0.contains(currentMastodonUser) } ?? false + if isPending { + relationshipActionSet.insert(.pending) + } + + let isBlocking = mastodonUser.blockingBy.flatMap { $0.contains(currentMastodonUser) } ?? false + if isBlocking { + relationshipActionSet.insert(.blocking) + } + + let isBlockedBy = currentMastodonUser.blockingBy.flatMap { $0.contains(mastodonUser) } ?? false + if isBlockedBy { + relationshipActionSet.insert(.blocked) + } + return relationshipActionSet + } +} diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index 11a5630f..f3e2e0f0 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -150,7 +150,7 @@ extension SearchViewController { func setupDataSource() { viewModel.hashtagDiffableDataSource = RecommendHashTagSection.collectionViewDiffableDataSource(for: hashtagCollectionView) - viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView, context: context) + viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView, delegate: self, managedObjectContext: context.managedObjectContext) viewModel.searchResultDiffableDataSource = SearchResultSection.tableViewDiffableDataSource(for: searchingTableView, dependency: self) } } diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index c3a5987b..3313d176 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -21,6 +21,9 @@ final class SearchViewModel: NSObject { let context: AppContext weak var coordinator: SceneCoordinator! + let mastodonUser = CurrentValueSubject(nil) + let currentMastodonUser = CurrentValueSubject(nil) + // output let searchText = CurrentValueSubject("") let searchScope = CurrentValueSubject(Mastodon.API.Search.SearchType.default) @@ -60,6 +63,19 @@ final class SearchViewModel: NSObject { guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } + + // bind active authentication + context.authenticationService.activeMastodonAuthentication + .sink { [weak self] activeMastodonAuthentication in + guard let self = self else { return } + guard let activeMastodonAuthentication = activeMastodonAuthentication else { + self.currentMastodonUser.value = nil + return + } + self.currentMastodonUser.value = activeMastodonAuthentication.user + } + .store(in: &disposeBag) + Publishers.CombineLatest( searchText .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates(),