fix: recommend request publisher logic issue

This commit is contained in:
CMK 2021-06-30 00:56:37 +08:00
parent 0dd2aaa068
commit 17bdce1321
4 changed files with 177 additions and 117 deletions

View File

@ -7,17 +7,17 @@
<key>AppShared.xcscheme_^#shared#^_</key> <key>AppShared.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>26</integer> <integer>19</integer>
</dict> </dict>
<key>CoreDataStack.xcscheme_^#shared#^_</key> <key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>21</integer> <integer>20</integer>
</dict> </dict>
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key> <key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>2</integer> <integer>1</integer>
</dict> </dict>
<key>Mastodon - RTL.xcscheme_^#shared#^_</key> <key>Mastodon - RTL.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -37,7 +37,7 @@
<key>NotificationService.xcscheme_^#shared#^_</key> <key>NotificationService.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>22</integer> <integer>18</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key> <key>SuppressBuildableAutocreation</key>

View File

@ -30,6 +30,23 @@ extension UserProviderFacade {
) )
} }
static func toggleUserFollowRelationship(
provider: UserProvider,
mastodonUser: MastodonUser
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, 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( private static func _toggleUserFollowRelationship(
context: AppContext, context: AppContext,
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox, activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox,
@ -52,6 +69,22 @@ extension UserProviderFacade {
} }
extension UserProviderFacade { extension UserProviderFacade {
static func toggleUserBlockRelationship(
provider: UserProvider,
mastodonUser: MastodonUser
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, 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( static func toggleUserBlockRelationship(
provider: UserProvider, provider: UserProvider,
cell: UITableViewCell? cell: UITableViewCell?
@ -98,6 +131,23 @@ extension UserProviderFacade {
} }
extension UserProviderFacade { extension UserProviderFacade {
static func toggleUserMuteRelationship(
provider: UserProvider,
mastodonUser: MastodonUser
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, 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( static func toggleUserMuteRelationship(
provider: UserProvider, provider: UserProvider,
cell: UITableViewCell? cell: UITableViewCell?

View File

@ -20,14 +20,13 @@ extension SearchViewController: UserProvider {
func mastodonUser() -> Future<MastodonUser?, Never> { func mastodonUser() -> Future<MastodonUser?, Never> {
Future { promise in Future { promise in
promise(.success(self.viewModel.mastodonUser.value)) promise(.success(nil))
} }
} }
} }
extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegate { extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegate {
func followButtonDidPressed(clickedUser: MastodonUser) { func followButtonDidPressed(clickedUser: MastodonUser) {
viewModel.mastodonUser.value = clickedUser
guard let currentMastodonUser = viewModel.currentMastodonUser.value else { guard let currentMastodonUser = viewModel.currentMastodonUser.value else {
return return
} }
@ -36,17 +35,17 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat
case .none: case .none:
break break
case .follow, .following: case .follow, .following:
UserProviderFacade.toggleUserFollowRelationship(provider: self) UserProviderFacade.toggleUserFollowRelationship(provider: self, mastodonUser: clickedUser)
.sink { _ in .sink { _ in
// error handling
} receiveValue: { _ in } receiveValue: { _ in
// success
} }
.store(in: &disposeBag) .store(in: &disposeBag)
case .pending: case .pending:
break break
case .muting: case .muting:
guard let mastodonUser = viewModel.mastodonUser.value else { return } let name = clickedUser.displayNameWithFallback
let name = mastodonUser.displayNameWithFallback
let alertController = UIAlertController( let alertController = UIAlertController(
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title, title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title,
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name), 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 let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
UserProviderFacade.toggleUserMuteRelationship(provider: self, cell: nil) UserProviderFacade.toggleUserMuteRelationship(provider: self, mastodonUser: clickedUser)
.sink { _ in .sink { _ in
// do nothing // do nothing
} receiveValue: { _ in } receiveValue: { _ in
@ -67,8 +66,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat
alertController.addAction(cancelAction) alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil) present(alertController, animated: true, completion: nil)
case .blocking: case .blocking:
guard let mastodonUser = viewModel.mastodonUser.value else { return } let name = clickedUser.displayNameWithFallback
let name = mastodonUser.displayNameWithFallback
let alertController = UIAlertController( let alertController = UIAlertController(
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.title, title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.title,
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.message(name), 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 let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
UserProviderFacade.toggleUserBlockRelationship(provider: self, cell: nil) UserProviderFacade.toggleUserBlockRelationship(provider: self, mastodonUser: clickedUser)
.sink { _ in .sink { _ in
// do nothing // do nothing
} receiveValue: { _ in } receiveValue: { _ in

View File

@ -21,7 +21,6 @@ final class SearchViewModel: NSObject {
let context: AppContext let context: AppContext
weak var coordinator: SceneCoordinator! weak var coordinator: SceneCoordinator!
let mastodonUser = CurrentValueSubject<MastodonUser?, Never>(nil)
let currentMastodonUser = CurrentValueSubject<MastodonUser?, Never>(nil) let currentMastodonUser = CurrentValueSubject<MastodonUser?, Never>(nil)
let viewDidAppeared = PassthroughSubject<Void, Never>() let viewDidAppeared = PassthroughSubject<Void, Never>()
@ -33,7 +32,7 @@ final class SearchViewModel: NSObject {
let searchResult = CurrentValueSubject<Mastodon.Entity.SearchResult?, Never>(nil) let searchResult = CurrentValueSubject<Mastodon.Entity.SearchResult?, Never>(nil)
var recommendHashTags = [Mastodon.Entity.Tag]() // var recommendHashTags = [Mastodon.Entity.Tag]()
var recommendAccounts = [NSManagedObjectID]() var recommendAccounts = [NSManagedObjectID]()
var recommendAccountsFallback = PassthroughSubject<Void, Never>() var recommendAccountsFallback = PassthroughSubject<Void, Never>()
@ -62,10 +61,6 @@ final class SearchViewModel: NSObject {
self.context = context self.context = context
super.init() super.init()
guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else {
return
}
// bind active authentication // bind active authentication
context.authenticationService.activeMastodonAuthentication context.authenticationService.activeMastodonAuthentication
.sink { [weak self] activeMastodonAuthentication in .sink { [weak self] activeMastodonAuthentication in
@ -86,23 +81,40 @@ final class SearchViewModel: NSObject {
.filter { text, _ in .filter { text, _ in
!text.isEmpty !text.isEmpty
} }
.flatMap { (text, scope) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, Error> in .compactMap { (text, scope) -> AnyPublisher<Result<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, Error>, Never>? in
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return nil }
let query = Mastodon.API.V2.Search.Query(q: text, let query = Mastodon.API.V2.Search.Query(
type: scope, q: text,
accountID: nil, type: scope,
maxID: nil, accountID: nil,
minID: nil, maxID: nil,
excludeUnreviewed: nil, minID: nil,
resolve: nil, excludeUnreviewed: nil,
limit: nil, resolve: nil,
offset: nil, limit: nil,
following: nil) offset: nil,
return context.apiService.search(domain: activeMastodonAuthenticationBox.domain, query: query, mastodonAuthenticationBox: activeMastodonAuthenticationBox) 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<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, Error> { response } }
.catch { error in Just(Result<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, Error> { throw error }) }
.eraseToAnyPublisher()
} }
.sink { _ in .switchToLatest()
} receiveValue: { [weak self] result in .sink { [weak self] result in
self?.searchResult.value = result.value 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) .store(in: &disposeBag)
@ -147,48 +159,71 @@ final class SearchViewModel: NSObject {
} }
.store(in: &disposeBag) .store(in: &disposeBag)
viewDidAppeared Publishers.CombineLatest(
.compactMap { _ in self.requestRecommendHashTags() } context.authenticationService.activeMastodonAuthenticationBox,
.receive(on: RunLoop.main) viewDidAppeared
.sink { [weak self] _ in )
guard let self = self else { return } .compactMap { activeMastodonAuthenticationBox, _ -> AuthenticationService.MastodonAuthenticationBox? in
if !self.recommendHashTags.isEmpty { return activeMastodonAuthenticationBox
guard let dataSource = self.hashtagDiffableDataSource else { return } }
var snapshot = NSDiffableDataSourceSnapshot<RecommendHashTagSection, Mastodon.Entity.Tag>() .throttle(for: 1, scheduler: DispatchQueue.main, latest: false)
snapshot.appendSections([.main]) .flatMap { box in
snapshot.appendItems(self.recommendHashTags, toSection: .main) context.apiService.recommendTrends(domain: box.domain, query: nil)
dataSource.apply(snapshot, animatingDifferences: false, completion: nil) .map { response in Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { response } }
} .catch { error in Just(Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { throw error }) }
} receiveValue: { _ in .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<RecommendHashTagSection, Mastodon.Entity.Tag>()
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 .store(in: &disposeBag)
.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 Publishers.CombineLatest(
.receive(on: RunLoop.main) context.authenticationService.activeMastodonAuthenticationBox,
.sink { [weak self] _ in viewDidAppeared
guard let self = self else { return } )
self.requestRecommendAccounts() .compactMap { activeMastodonAuthenticationBox, _ -> AuthenticationService.MastodonAuthenticationBox? in
.sink { [weak self] _ in return activeMastodonAuthenticationBox
guard let self = self else { return } }
if !self.recommendAccounts.isEmpty { .throttle(for: 1, scheduler: DispatchQueue.main, latest: false)
self.applyDataSource() .flatMap { box -> AnyPublisher<Result<[Mastodon.Entity.Account.ID], Error>, Never> in
} context.apiService.suggestionAccountV2(domain: box.domain, query: nil, mastodonAuthenticationBox: box)
} receiveValue: { _ in .map { response in Result<[Mastodon.Entity.Account.ID], Error> { response.value.map { $0.account.id } } }
.catch { error -> AnyPublisher<Result<[Mastodon.Entity.Account.ID], Error>, 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 searchResult
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
@ -217,30 +252,6 @@ final class SearchViewModel: NSObject {
.store(in: &disposeBag) .store(in: &disposeBag)
} }
func requestRecommendHashTags() -> Future<Void, Error> {
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<Void, Error> { func requestRecommendAccountsV2() -> Future<Void, Error> {
Future { promise in Future { promise in
guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else {
@ -296,17 +307,7 @@ final class SearchViewModel: NSObject {
} }
} }
func applyDataSource() { func receiveAccounts(ids: [Mastodon.Entity.Account.ID]) {
DispatchQueue.main.async {
guard let dataSource = self.accountDiffableDataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot<RecommendAccountSection, NSManagedObjectID>()
snapshot.appendSections([.main])
snapshot.appendItems(self.recommendAccounts, toSection: .main)
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
}
}
func receiveAccounts(ids: [String]) {
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
return return
} }
@ -323,12 +324,23 @@ final class SearchViewModel: NSObject {
return nil return nil
} }
}() }()
if let users = mastodonUsers { guard let mastodonUsers = mastodonUsers else { return }
let sortedUsers = users.sorted { (user1, user2) -> Bool in let objectIDs = mastodonUsers
(ids.firstIndex(of: user1.id) ?? 0) < (ids.firstIndex(of: user2.id) ?? 0) .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<RecommendAccountSection, NSManagedObjectID>()
snapshot.appendSections([.main])
snapshot.appendItems(self.recommendAccounts, toSection: .main)
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
} }
func accountCollectionViewItemDidSelected(mastodonUser: MastodonUser, from: UIViewController) { func accountCollectionViewItemDidSelected(mastodonUser: MastodonUser, from: UIViewController) {