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>
<dict>
<key>orderHint</key>
<integer>26</integer>
<integer>19</integer>
</dict>
<key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>21</integer>
<integer>20</integer>
</dict>
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
<integer>1</integer>
</dict>
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
<dict>
@ -37,7 +37,7 @@
<key>NotificationService.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>22</integer>
<integer>18</integer>
</dict>
</dict>
<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(
context: AppContext,
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox,
@ -52,6 +69,22 @@ 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(
provider: UserProvider,
cell: UITableViewCell?
@ -98,6 +131,23 @@ 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(
provider: UserProvider,
cell: UITableViewCell?

View File

@ -20,14 +20,13 @@ extension SearchViewController: UserProvider {
func mastodonUser() -> Future<MastodonUser?, Never> {
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

View File

@ -21,7 +21,6 @@ final class SearchViewModel: NSObject {
let context: AppContext
weak var coordinator: SceneCoordinator!
let mastodonUser = CurrentValueSubject<MastodonUser?, Never>(nil)
let currentMastodonUser = CurrentValueSubject<MastodonUser?, Never>(nil)
let viewDidAppeared = PassthroughSubject<Void, Never>()
@ -33,7 +32,7 @@ final class SearchViewModel: NSObject {
let searchResult = CurrentValueSubject<Mastodon.Entity.SearchResult?, Never>(nil)
var recommendHashTags = [Mastodon.Entity.Tag]()
// var recommendHashTags = [Mastodon.Entity.Tag]()
var recommendAccounts = [NSManagedObjectID]()
var recommendAccountsFallback = PassthroughSubject<Void, Never>()
@ -62,10 +61,6 @@ final class SearchViewModel: NSObject {
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,23 +81,40 @@ final class SearchViewModel: NSObject {
.filter { text, _ in
!text.isEmpty
}
.flatMap { (text, scope) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, 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<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,
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<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
} 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)
@ -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<RecommendHashTagSection, Mastodon.Entity.Tag>()
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<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { response } }
.catch { error in Just(Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, 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<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
.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)
}
.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
Publishers.CombineLatest(
context.authenticationService.activeMastodonAuthenticationBox,
viewDidAppeared
)
.compactMap { activeMastodonAuthenticationBox, _ -> AuthenticationService.MastodonAuthenticationBox? in
return activeMastodonAuthenticationBox
}
.throttle(for: 1, scheduler: DispatchQueue.main, latest: false)
.flatMap { box -> AnyPublisher<Result<[Mastodon.Entity.Account.ID], Error>, 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<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
.receive(on: DispatchQueue.main)
@ -217,30 +252,6 @@ final class SearchViewModel: NSObject {
.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> {
Future { promise in
guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else {
@ -296,17 +307,7 @@ final class SearchViewModel: NSObject {
}
}
func applyDataSource() {
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]) {
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<RecommendAccountSection, NSManagedObjectID>()
snapshot.appendSections([.main])
snapshot.appendItems(self.recommendAccounts, toSection: .main)
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
}
func accountCollectionViewItemDidSelected(mastodonUser: MastodonUser, from: UIViewController) {