From 0418ec147021f2f3d1fa2646814a8323b7bd5ffa Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 9 Apr 2021 13:09:30 +0800 Subject: [PATCH 1/4] chore: recommend account use CoreData dateSource --- .../Section/RecommendAccountSection.swift | 10 ++- ...hRecommendAccountsCollectionViewCell.swift | 3 +- .../SearchViewController+Recommend.swift | 5 +- .../Scene/Search/SearchViewController.swift | 2 +- Mastodon/Scene/Search/SearchViewModel.swift | 70 +++++++++---------- .../APIService/APIService+Recommend.swift | 31 ++++++-- 6 files changed, 73 insertions(+), 48 deletions(-) diff --git a/Mastodon/Diffiable/Section/RecommendAccountSection.swift b/Mastodon/Diffiable/Section/RecommendAccountSection.swift index b08c9abab..ac3feb328 100644 --- a/Mastodon/Diffiable/Section/RecommendAccountSection.swift +++ b/Mastodon/Diffiable/Section/RecommendAccountSection.swift @@ -8,6 +8,8 @@ import Foundation import MastodonSDK import UIKit +import CoreData +import CoreDataStack enum RecommendAccountSection: Equatable, Hashable { case main @@ -15,10 +17,12 @@ enum RecommendAccountSection: Equatable, Hashable { extension RecommendAccountSection { static func collectionViewDiffableDataSource( - for collectionView: UICollectionView - ) -> UICollectionViewDiffableDataSource { - UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, account -> UICollectionViewCell? in + for collectionView: UICollectionView, + managedObjectContext: NSManagedObjectContext + ) -> UICollectionViewDiffableDataSource { + UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, objectID -> UICollectionViewCell? in let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self), for: indexPath) as! SearchRecommendAccountsCollectionViewCell + let account = managedObjectContext.object(with: objectID) as! MastodonUser cell.config(with: account) return cell } diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index 626a2b4b6..4380d98f9 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -8,6 +8,7 @@ import Foundation import MastodonSDK import UIKit +import CoreDataStack class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { let avatarImageView: UIImageView = { @@ -122,7 +123,7 @@ extension SearchRecommendAccountsCollectionViewCell { ]) } - func config(with account: Mastodon.Entity.Account) { + func config(with account: MastodonUser) { displayNameLabel.text = account.displayName.isEmpty ? account.username : account.displayName acctLabel.text = account.acct avatarImageView.af.setImage( diff --git a/Mastodon/Scene/Search/SearchViewController+Recommend.swift b/Mastodon/Scene/Search/SearchViewController+Recommend.swift index 056df2e38..e941fa841 100644 --- a/Mastodon/Scene/Search/SearchViewController+Recommend.swift +++ b/Mastodon/Scene/Search/SearchViewController+Recommend.swift @@ -59,8 +59,9 @@ extension SearchViewController: UICollectionViewDelegate { switch collectionView { case self.accountsCollectionView: guard let diffableDataSource = viewModel.accountDiffableDataSource else { return } - guard let account = diffableDataSource.itemIdentifier(for: indexPath) else { return } - viewModel.accountCollectionViewItemDidSelected(account: account, from: self) + guard let accountObjectID = diffableDataSource.itemIdentifier(for: indexPath) else { return } + let user = context.managedObjectContext.object(with: accountObjectID) as! MastodonUser + viewModel.accountCollectionViewItemDidSelected(mastodonUser: user, from: self) case self.hashtagCollectionView: guard let diffableDataSource = viewModel.hashtagDiffableDataSource else { return } guard let hashtag = diffableDataSource.itemIdentifier(for: indexPath) else { return } diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index b98a34b95..f697ef528 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) + viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView, 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 46255fde1..c3a5987ba 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -30,10 +30,10 @@ final class SearchViewModel: NSObject { let searchResult = CurrentValueSubject(nil) var recommendHashTags = [Mastodon.Entity.Tag]() - var recommendAccounts = [Mastodon.Entity.Account]() + var recommendAccounts = [NSManagedObjectID]() var hashtagDiffableDataSource: UICollectionViewDiffableDataSource? - var accountDiffableDataSource: UICollectionViewDiffableDataSource? + var accountDiffableDataSource: UICollectionViewDiffableDataSource? var searchResultDiffableDataSource: UITableViewDiffableDataSource? // bottom loader @@ -52,7 +52,7 @@ final class SearchViewModel: NSObject { lazy var loadOldestStateMachinePublisher = CurrentValueSubject(nil) - init(context: AppContext,coordinator: SceneCoordinator) { + init(context: AppContext, coordinator: SceneCoordinator) { self.coordinator = coordinator self.context = context super.init() @@ -102,7 +102,7 @@ final class SearchViewModel: NSObject { searchText, searchScope ) - .filter { isSearching, text, _ in + .filter { isSearching, _, _ in isSearching } .sink { [weak self] _, text, scope in @@ -151,7 +151,7 @@ final class SearchViewModel: NSObject { guard let self = self else { return } if !self.recommendAccounts.isEmpty { guard let dataSource = self.accountDiffableDataSource else { return } - var snapshot = NSDiffableDataSourceSnapshot() + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) snapshot.appendItems(self.recommendAccounts, toSection: .main) dataSource.apply(snapshot, animatingDifferences: false, completion: nil) @@ -170,7 +170,7 @@ final class SearchViewModel: NSObject { snapshot.appendSections([.account]) let items = accounts.compactMap { SearchResultItem.account(account: $0) } snapshot.appendItems(items, toSection: .account) - if self.searchScope.value == Mastodon.API.Search.SearchType.accounts && !items.isEmpty { + if self.searchScope.value == Mastodon.API.Search.SearchType.accounts, !items.isEmpty { snapshot.appendItems([.bottomLoader], toSection: .account) } } @@ -178,7 +178,7 @@ final class SearchViewModel: NSObject { snapshot.appendSections([.hashtag]) let items = tags.compactMap { SearchResultItem.hashtag(tag: $0) } snapshot.appendItems(items, toSection: .hashtag) - if self.searchScope.value == Mastodon.API.Search.SearchType.hashtags && !items.isEmpty { + if self.searchScope.value == Mastodon.API.Search.SearchType.hashtags, !items.isEmpty { snapshot.appendItems([.bottomLoader], toSection: .hashtag) } } @@ -229,49 +229,45 @@ final class SearchViewModel: NSObject { } } receiveValue: { [weak self] accounts in guard let self = self else { return } - self.recommendAccounts = accounts.value + let ids = accounts.value.compactMap({$0.id}) + let userFetchRequest = MastodonUser.sortedFetchRequest + userFetchRequest.predicate = MastodonUser.predicate(domain: activeMastodonAuthenticationBox.domain, ids: ids) + let mastodonUsers: [MastodonUser]? = { + let userFetchRequest = MastodonUser.sortedFetchRequest + userFetchRequest.predicate = MastodonUser.predicate(domain: activeMastodonAuthenticationBox.domain, ids: ids) + userFetchRequest.returnsObjectsAsFaults = false + do { + return try self.context.managedObjectContext.fetch(userFetchRequest) + } catch { + assertionFailure(error.localizedDescription) + return nil + } + }() + if let users = mastodonUsers { + self.recommendAccounts = users.map(\.objectID) + } } .store(in: &self.disposeBag) } } - func accountCollectionViewItemDidSelected(account: Mastodon.Entity.Account, from: UIViewController) { - _ = context.managedObjectContext.performChanges { [weak self] in - guard let self = self else { return } - guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } - // load request mastodon user - let requestMastodonUser: MastodonUser? = { - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: activeMastodonAuthenticationBox.domain, id: activeMastodonAuthenticationBox.userID) - request.fetchLimit = 1 - request.returnsObjectsAsFaults = false - do { - return try self.context.managedObjectContext.fetch(request).first - } catch { - assertionFailure(error.localizedDescription) - return nil - } - }() - let (mastodonUser, _) = APIService.CoreData.createOrMergeMastodonUser(into: self.context.managedObjectContext, for: requestMastodonUser, in: activeMastodonAuthenticationBox.domain, entity: account, userCache: nil, networkDate: Date(), log: OSLog.api) - let viewModel = ProfileViewModel(context: self.context, optionalMastodonUser: mastodonUser) - DispatchQueue.main.async { - self.coordinator.present(scene: .profile(viewModel: viewModel), from: from, transition: .show) - } + func accountCollectionViewItemDidSelected(mastodonUser: MastodonUser, from: UIViewController) { + let viewModel = ProfileViewModel(context: context, optionalMastodonUser: mastodonUser) + DispatchQueue.main.async { + self.coordinator.present(scene: .profile(viewModel: viewModel), from: from, transition: .show) } } func hashtagCollectionViewItemDidSelected(hashtag: Mastodon.Entity.Tag, from: UIViewController) { - let (tagInCoreData,_) = APIService.CoreData.createOrMergeTag(into: self.context.managedObjectContext, entity: hashtag) - let viewModel = HashtagTimelineViewModel(context: self.context, hashtag: tagInCoreData.name) + let (tagInCoreData, _) = APIService.CoreData.createOrMergeTag(into: context.managedObjectContext, entity: hashtag) + let viewModel = HashtagTimelineViewModel(context: context, hashtag: tagInCoreData.name) DispatchQueue.main.async { self.coordinator.present(scene: .hashtagTimeline(viewModel: viewModel), from: from, transition: .show) } } - func searchResultItemDidSelected(item: SearchResultItem,from: UIViewController) { - let searchHistories = self.fetchSearchHistory() + func searchResultItemDidSelected(item: SearchResultItem, from: UIViewController) { + let searchHistories = fetchSearchHistory() _ = context.managedObjectContext.performChanges { [weak self] in guard let self = self else { return } switch item { @@ -312,7 +308,7 @@ final class SearchViewModel: NSObject { } case .hashtag(let tag): - let (tagInCoreData,_) = APIService.CoreData.createOrMergeTag(into: self.context.managedObjectContext, entity: tag) + let (tagInCoreData, _) = APIService.CoreData.createOrMergeTag(into: self.context.managedObjectContext, entity: tag) if let searchHistories = searchHistories { let history = searchHistories.first { history -> Bool in guard let hashtag = history.hashtag else { return false } diff --git a/Mastodon/Service/APIService/APIService+Recommend.swift b/Mastodon/Service/APIService/APIService+Recommend.swift index bf6db0179..1c58fc575 100644 --- a/Mastodon/Service/APIService/APIService+Recommend.swift +++ b/Mastodon/Service/APIService/APIService+Recommend.swift @@ -5,12 +5,14 @@ // Created by sxiaojian on 2021/3/31. // +import Combine import Foundation import MastodonSDK -import Combine +import CoreData +import CoreDataStack +import OSLog extension APIService { - func recommendAccount( domain: String, query: Mastodon.API.Suggestions.Query?, @@ -19,12 +21,33 @@ extension APIService { let authorization = mastodonAuthenticationBox.userAuthorization return Mastodon.API.Suggestions.get(session: session, domain: domain, query: query, authorization: authorization) + .flatMap { response -> AnyPublisher, Error> in + let log = OSLog.api + return self.backgroundManagedObjectContext.performChanges { + response.value.forEach { user in + let (mastodonUser,isCreated) = APIService.CoreData.createOrMergeMastodonUser(into: self.backgroundManagedObjectContext, for: nil, in: domain, entity: user, userCache: nil, networkDate: Date(), log: log) + let flag = isCreated ? "+" : "-" + os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: fetch mastodon user [%s](%s)%s", (#file as NSString).lastPathComponent, #line, #function, flag, mastodonUser.id, mastodonUser.username) + } + } + .setFailureType(to: Error.self) + .tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Account]> in + switch result { + case .success: + return response + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() } - + func recommendTrends( domain: String, query: Mastodon.API.Trends.Query? ) -> AnyPublisher, Error> { - return Mastodon.API.Trends.get(session: session, domain: domain, query: query) + Mastodon.API.Trends.get(session: session, domain: domain, query: query) } } From c74314ef11d5fa075fdeb7e7bd80cd7f0b8530cd Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 9 Apr 2021 13:59:33 +0800 Subject: [PATCH 2/4] chore: observe Follow state --- .../Section/RecommendAccountSection.swift | 9 ++-- ...hRecommendAccountsCollectionViewCell.swift | 52 +++++++++++++++++-- .../Scene/Search/SearchViewController.swift | 2 +- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/Mastodon/Diffiable/Section/RecommendAccountSection.swift b/Mastodon/Diffiable/Section/RecommendAccountSection.swift index ac3feb328..409adee3e 100644 --- a/Mastodon/Diffiable/Section/RecommendAccountSection.swift +++ b/Mastodon/Diffiable/Section/RecommendAccountSection.swift @@ -18,12 +18,15 @@ enum RecommendAccountSection: Equatable, Hashable { extension RecommendAccountSection { static func collectionViewDiffableDataSource( for collectionView: UICollectionView, - managedObjectContext: NSManagedObjectContext + context: AppContext! ) -> UICollectionViewDiffableDataSource { UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, objectID -> UICollectionViewCell? in let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self), for: indexPath) as! SearchRecommendAccountsCollectionViewCell - let account = managedObjectContext.object(with: objectID) as! MastodonUser - cell.config(with: account) + let user = context.managedObjectContext.object(with: objectID) as! MastodonUser + 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 4380d98f9..933eeb42a 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -9,8 +9,12 @@ import Foundation import MastodonSDK import UIKit import CoreDataStack +import Combine class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { + + var disposeBag = Set() + let avatarImageView: UIImageView = { let imageView = UIImageView() imageView.layer.cornerRadius = 8.4 @@ -123,16 +127,16 @@ extension SearchRecommendAccountsCollectionViewCell { ]) } - func config(with account: MastodonUser) { - displayNameLabel.text = account.displayName.isEmpty ? account.username : account.displayName - acctLabel.text = account.acct + func config(with mastodonUser: MastodonUser) { + displayNameLabel.text = mastodonUser.displayName.isEmpty ? mastodonUser.username : mastodonUser.displayName + acctLabel.text = mastodonUser.acct avatarImageView.af.setImage( - withURL: URL(string: account.avatar)!, + withURL: URL(string: mastodonUser.avatar)!, placeholderImage: UIImage.placeholder(color: .systemFill), imageTransition: .crossDissolve(0.2) ) headerImageView.af.setImage( - withURL: URL(string: account.header)!, + withURL: URL(string: mastodonUser.header)!, placeholderImage: UIImage.placeholder(color: .systemFill), imageTransition: .crossDissolve(0.2)) { [weak self] _ in guard let self = self else { return } @@ -140,6 +144,44 @@ extension SearchRecommendAccountsCollectionViewCell { 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) + } + .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) + } } #if canImport(SwiftUI) && DEBUG diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index f697ef528..11a5630f3 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, managedObjectContext: context.managedObjectContext) + viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView, context: context) viewModel.searchResultDiffableDataSource = SearchResultSection.tableViewDiffableDataSource(for: searchingTableView, dependency: self) } } From 567c2af0eea50833e15a003b19f4bd68e72eddeb Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 9 Apr 2021 19:39:35 +0800 Subject: [PATCH 3/4] 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 01d5f1f20..fda5de623 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 409adee3e..3ecd4e3b2 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 933eeb42a..c64db4981 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 000000000..ce7d13b8a --- /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 11a5630f3..f3e2e0f0d 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 c3a5987ba..3313d1760 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(), From aa23ce398f7ccf676e2295dbd3e17cbda9cd0b1f Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 9 Apr 2021 20:04:12 +0800 Subject: [PATCH 4/4] fix: fix crash when unfollowing , fix cell reuse issue --- .../SearchRecommendAccountsCollectionViewCell.swift | 1 + Mastodon/Scene/Search/SearchViewController+Follow.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index c64db4981..85c543b40 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -75,6 +75,7 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { headerImageView.af.cancelImageRequest() avatarImageView.af.cancelImageRequest() visualEffectView.removeFromSuperview() + disposeBag.removeAll() } override init(frame: CGRect) { diff --git a/Mastodon/Scene/Search/SearchViewController+Follow.swift b/Mastodon/Scene/Search/SearchViewController+Follow.swift index ce7d13b8a..8b0acda0a 100644 --- a/Mastodon/Scene/Search/SearchViewController+Follow.swift +++ b/Mastodon/Scene/Search/SearchViewController+Follow.swift @@ -24,7 +24,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat guard let currentMastodonUser = viewModel.currentMastodonUser.value else { return } - let relationshipAction = relationShipActionSet(mastodonUser: clickedUser, currentMastodonUser: currentMastodonUser) + guard let relationshipAction = relationShipActionSet(mastodonUser: clickedUser, currentMastodonUser: currentMastodonUser).highPriorityAction(except: .editOptions) else { return } switch relationshipAction { case .none: break