From 17bdce13211a451cbb8048d875939938542f54c6 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 30 Jun 2021 00:56:37 +0800 Subject: [PATCH] fix: recommend request publisher logic issue --- .../xcschemes/xcschememanagement.plist | 8 +- .../UserProvider/UserProviderFacade.swift | 50 ++++ .../Search/SearchViewController+Follow.swift | 18 +- Mastodon/Scene/Search/SearchViewModel.swift | 218 +++++++++--------- 4 files changed, 177 insertions(+), 117 deletions(-) diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index ebbde8329..6a088c82b 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,17 +7,17 @@ AppShared.xcscheme_^#shared#^_ orderHint - 26 + 19 CoreDataStack.xcscheme_^#shared#^_ orderHint - 21 + 20 Mastodon - ASDK.xcscheme_^#shared#^_ orderHint - 2 + 1 Mastodon - RTL.xcscheme_^#shared#^_ @@ -37,7 +37,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 22 + 18 SuppressBuildableAutocreation diff --git a/Mastodon/Protocol/UserProvider/UserProviderFacade.swift b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift index b5e381f1b..79185338a 100644 --- a/Mastodon/Protocol/UserProvider/UserProviderFacade.swift +++ b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift @@ -29,6 +29,23 @@ extension UserProviderFacade { mastodonUser: provider.mastodonUser().eraseToAnyPublisher() ) } + + static func toggleUserFollowRelationship( + provider: UserProvider, + mastodonUser: MastodonUser + ) -> AnyPublisher, Error> { + // prepare authentication + guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { + assertionFailure() + return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher() + } + + return _toggleUserFollowRelationship( + context: provider.context, + activeMastodonAuthenticationBox: activeMastodonAuthenticationBox, + mastodonUser: Just(mastodonUser).eraseToAnyPublisher() + ) + } private static func _toggleUserFollowRelationship( context: AppContext, @@ -52,6 +69,22 @@ extension UserProviderFacade { } extension UserProviderFacade { + static func toggleUserBlockRelationship( + provider: UserProvider, + mastodonUser: MastodonUser + ) -> AnyPublisher, Error> { + // prepare authentication + guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { + assertionFailure() + return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher() + } + return _toggleUserBlockRelationship( + context: provider.context, + activeMastodonAuthenticationBox: activeMastodonAuthenticationBox, + mastodonUser: Just(mastodonUser).eraseToAnyPublisher() + ) + } + static func toggleUserBlockRelationship( provider: UserProvider, cell: UITableViewCell? @@ -98,6 +131,23 @@ extension UserProviderFacade { } extension UserProviderFacade { + + static func toggleUserMuteRelationship( + provider: UserProvider, + mastodonUser: MastodonUser + ) -> AnyPublisher, Error> { + // prepare authentication + guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { + assertionFailure() + return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher() + } + return _toggleUserMuteRelationship( + context: provider.context, + activeMastodonAuthenticationBox: activeMastodonAuthenticationBox, + mastodonUser: Just(mastodonUser).eraseToAnyPublisher() + ) + } + static func toggleUserMuteRelationship( provider: UserProvider, cell: UITableViewCell? diff --git a/Mastodon/Scene/Search/SearchViewController+Follow.swift b/Mastodon/Scene/Search/SearchViewController+Follow.swift index c31f6d82a..c345336df 100644 --- a/Mastodon/Scene/Search/SearchViewController+Follow.swift +++ b/Mastodon/Scene/Search/SearchViewController+Follow.swift @@ -20,14 +20,13 @@ extension SearchViewController: UserProvider { func mastodonUser() -> Future { Future { promise in - promise(.success(self.viewModel.mastodonUser.value)) + promise(.success(nil)) } } } extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegate { func followButtonDidPressed(clickedUser: MastodonUser) { - viewModel.mastodonUser.value = clickedUser guard let currentMastodonUser = viewModel.currentMastodonUser.value else { return } @@ -36,17 +35,17 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat case .none: break case .follow, .following: - UserProviderFacade.toggleUserFollowRelationship(provider: self) + UserProviderFacade.toggleUserFollowRelationship(provider: self, mastodonUser: clickedUser) .sink { _ in - + // error handling } receiveValue: { _ in + // success } .store(in: &disposeBag) case .pending: break case .muting: - guard let mastodonUser = viewModel.mastodonUser.value else { return } - let name = mastodonUser.displayNameWithFallback + let name = clickedUser.displayNameWithFallback let alertController = UIAlertController( title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title, message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name), @@ -54,7 +53,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat ) let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in guard let self = self else { return } - UserProviderFacade.toggleUserMuteRelationship(provider: self, cell: nil) + UserProviderFacade.toggleUserMuteRelationship(provider: self, mastodonUser: clickedUser) .sink { _ in // do nothing } receiveValue: { _ in @@ -67,8 +66,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat alertController.addAction(cancelAction) present(alertController, animated: true, completion: nil) case .blocking: - guard let mastodonUser = viewModel.mastodonUser.value else { return } - let name = mastodonUser.displayNameWithFallback + let name = clickedUser.displayNameWithFallback let alertController = UIAlertController( title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.title, message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.message(name), @@ -76,7 +74,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat ) let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in guard let self = self else { return } - UserProviderFacade.toggleUserBlockRelationship(provider: self, cell: nil) + UserProviderFacade.toggleUserBlockRelationship(provider: self, mastodonUser: clickedUser) .sink { _ in // do nothing } receiveValue: { _ in diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index 48bd87b88..35075dfe1 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -21,7 +21,6 @@ final class SearchViewModel: NSObject { let context: AppContext weak var coordinator: SceneCoordinator! - let mastodonUser = CurrentValueSubject(nil) let currentMastodonUser = CurrentValueSubject(nil) let viewDidAppeared = PassthroughSubject() @@ -33,7 +32,7 @@ final class SearchViewModel: NSObject { let searchResult = CurrentValueSubject(nil) - var recommendHashTags = [Mastodon.Entity.Tag]() + // var recommendHashTags = [Mastodon.Entity.Tag]() var recommendAccounts = [NSManagedObjectID]() var recommendAccountsFallback = PassthroughSubject() @@ -61,11 +60,7 @@ final class SearchViewModel: NSObject { self.coordinator = coordinator self.context = context super.init() - - guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } - + // bind active authentication context.authenticationService.activeMastodonAuthentication .sink { [weak self] activeMastodonAuthentication in @@ -86,26 +81,43 @@ final class SearchViewModel: NSObject { .filter { text, _ in !text.isEmpty } - .flatMap { (text, scope) -> AnyPublisher, Error> in - - let query = Mastodon.API.V2.Search.Query(q: text, - type: scope, - accountID: nil, - maxID: nil, - minID: nil, - excludeUnreviewed: nil, - resolve: nil, - limit: nil, - offset: nil, - following: nil) - return context.apiService.search(domain: activeMastodonAuthenticationBox.domain, query: query, mastodonAuthenticationBox: activeMastodonAuthenticationBox) + .compactMap { (text, scope) -> AnyPublisher, Error>, Never>? in + guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return nil } + let query = Mastodon.API.V2.Search.Query( + q: text, + type: scope, + accountID: nil, + maxID: nil, + minID: nil, + excludeUnreviewed: nil, + resolve: nil, + limit: nil, + offset: nil, + following: nil + ) + return context.apiService.search( + domain: activeMastodonAuthenticationBox.domain, + query: query, + mastodonAuthenticationBox: activeMastodonAuthenticationBox + ) + // .retry(3) // iOS 14.0 SDK may not works here. needs testing before add this + .map { response in Result, Error> { response } } + .catch { error in Just(Result, Error> { throw error }) } + .eraseToAnyPublisher() } - .sink { _ in - } receiveValue: { [weak self] result in - self?.searchResult.value = result.value + .switchToLatest() + .sink { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let response): + guard self.isSearching.value else { return } + self.searchResult.value = response.value + case .failure(let error): + break + } } .store(in: &disposeBag) - + isSearching .sink { [weak self] isSearching in if !isSearching { @@ -147,48 +159,71 @@ final class SearchViewModel: NSObject { } .store(in: &disposeBag) - viewDidAppeared - .compactMap { _ in self.requestRecommendHashTags() } - .receive(on: RunLoop.main) - .sink { [weak self] _ in - guard let self = self else { return } - if !self.recommendHashTags.isEmpty { - guard let dataSource = self.hashtagDiffableDataSource else { return } - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - snapshot.appendItems(self.recommendHashTags, toSection: .main) - dataSource.apply(snapshot, animatingDifferences: false, completion: nil) - } - } receiveValue: { _ in + Publishers.CombineLatest( + context.authenticationService.activeMastodonAuthenticationBox, + viewDidAppeared + ) + .compactMap { activeMastodonAuthenticationBox, _ -> AuthenticationService.MastodonAuthenticationBox? in + return activeMastodonAuthenticationBox + } + .throttle(for: 1, scheduler: DispatchQueue.main, latest: false) + .flatMap { box in + context.apiService.recommendTrends(domain: box.domain, query: nil) + .map { response in Result, Error> { response } } + .catch { error in Just(Result, Error> { throw error }) } + .eraseToAnyPublisher() + } + .receive(on: RunLoop.main) + .sink { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let response): + guard let dataSource = self.hashtagDiffableDataSource else { return } + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(response.value, toSection: .main) + dataSource.apply(snapshot, animatingDifferences: false, completion: nil) + case .failure(let error): + break } - .store(in: &disposeBag) - viewDidAppeared - .compactMap { _ in self.requestRecommendAccountsV2() } - .receive(on: RunLoop.main) - .sink { [weak self] _ in - guard let self = self else { return } - if !self.recommendAccounts.isEmpty { - self.applyDataSource() - } - } receiveValue: { _ in - } - .store(in: &disposeBag) - - recommendAccountsFallback - .receive(on: RunLoop.main) - .sink { [weak self] _ in - guard let self = self else { return } - self.requestRecommendAccounts() - .sink { [weak self] _ in - guard let self = self else { return } - if !self.recommendAccounts.isEmpty { - self.applyDataSource() - } - } receiveValue: { _ in + } + .store(in: &disposeBag) + + Publishers.CombineLatest( + context.authenticationService.activeMastodonAuthenticationBox, + viewDidAppeared + ) + .compactMap { activeMastodonAuthenticationBox, _ -> AuthenticationService.MastodonAuthenticationBox? in + return activeMastodonAuthenticationBox + } + .throttle(for: 1, scheduler: DispatchQueue.main, latest: false) + .flatMap { box -> AnyPublisher, Never> in + context.apiService.suggestionAccountV2(domain: box.domain, query: nil, mastodonAuthenticationBox: box) + .map { response in Result<[Mastodon.Entity.Account.ID], Error> { response.value.map { $0.account.id } } } + .catch { error -> AnyPublisher, Never> in + if let apiError = error as? Mastodon.API.Error, apiError.httpResponseStatus == .notFound { + return context.apiService.suggestionAccount(domain: box.domain, query: nil, mastodonAuthenticationBox: box) + .map { response in Result<[Mastodon.Entity.Account.ID], Error> { response.value.map { $0.id } } } + .catch { error in Just(Result<[Mastodon.Entity.Account.ID], Error> { throw error }) } + .eraseToAnyPublisher() + } else { + return Just(Result<[Mastodon.Entity.Account.ID], Error> { throw error }) + .eraseToAnyPublisher() } - .store(in: &self.disposeBag) + } + .eraseToAnyPublisher() + } + .receive(on: RunLoop.main) + .sink { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let userIDs): + self.receiveAccounts(ids: userIDs) + case .failure(let error): + break } - .store(in: &disposeBag) + } + .store(in: &disposeBag) searchResult .receive(on: DispatchQueue.main) @@ -216,30 +251,6 @@ final class SearchViewModel: NSObject { } .store(in: &disposeBag) } - - func requestRecommendHashTags() -> Future { - Future { promise in - guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { - promise(.failure(APIService.APIError.implicit(APIService.APIError.ErrorReason.authenticationMissing))) - return - } - self.context.apiService.recommendTrends(domain: activeMastodonAuthenticationBox.domain, query: nil) - .sink { completion in - switch completion { - case .failure(let error): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendHashTags request fail: %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription) - promise(.failure(error)) - case .finished: - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendHashTags request success", (#file as NSString).lastPathComponent, #line, #function) - promise(.success(())) - } - } receiveValue: { [weak self] tags in - guard let self = self else { return } - self.recommendHashTags = tags.value - } - .store(in: &self.disposeBag) - } - } func requestRecommendAccountsV2() -> Future { Future { promise in @@ -296,17 +307,7 @@ final class SearchViewModel: NSObject { } } - func applyDataSource() { - DispatchQueue.main.async { - guard let dataSource = self.accountDiffableDataSource else { return } - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - snapshot.appendItems(self.recommendAccounts, toSection: .main) - dataSource.apply(snapshot, animatingDifferences: false, completion: nil) - } - } - - func receiveAccounts(ids: [String]) { + func receiveAccounts(ids: [Mastodon.Entity.Account.ID]) { guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } @@ -323,12 +324,23 @@ final class SearchViewModel: NSObject { return nil } }() - if let users = mastodonUsers { - let sortedUsers = users.sorted { (user1, user2) -> Bool in - (ids.firstIndex(of: user1.id) ?? 0) < (ids.firstIndex(of: user2.id) ?? 0) + guard let mastodonUsers = mastodonUsers else { return } + let objectIDs = mastodonUsers + .compactMap { object in + ids.firstIndex(of: object.id).map { index in (index, object) } } - recommendAccounts = sortedUsers.map(\.objectID) - } + .sorted { $0.0 < $1.0 } + .map { $0.1.objectID } + + // append at front + let newObjectIDs = objectIDs.filter { !self.recommendAccounts.contains($0) } + self.recommendAccounts = newObjectIDs + self.recommendAccounts + + guard let dataSource = self.accountDiffableDataSource else { return } + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(self.recommendAccounts, toSection: .main) + dataSource.apply(snapshot, animatingDifferences: false, completion: nil) } func accountCollectionViewItemDidSelected(mastodonUser: MastodonUser, from: UIViewController) {