From 776263aaf2f37179f19b6b2d49a60e46d38b7902 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Wed, 21 Apr 2021 17:58:56 +0800 Subject: [PATCH] chore: compatible with the old server --- .../HomeTimelineViewController.swift | 5 + ...hRecommendAccountsCollectionViewCell.swift | 5 +- .../SearchViewController+Recommend.swift | 8 +- Mastodon/Scene/Search/SearchViewModel.swift | 125 +++++++++++++----- .../SuggestionAccountViewController.swift | 18 +-- .../SuggestionAccountViewModel.swift | 101 ++++++++++---- .../SuggestionAccountTableViewCell.swift | 7 +- 7 files changed, 189 insertions(+), 80 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 60fa3c9c..74a6ae00 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -159,6 +159,8 @@ extension HomeTimelineViewController { self.emptyView.removeFromSuperview() } } + } else { + self.emptyView.removeFromSuperview() } } .store(in: &disposeBag) @@ -245,6 +247,9 @@ extension HomeTimelineViewController { emptyView.bottomAnchor.constraint(equalTo: view.readableContentGuide.bottomAnchor) ]) + if emptyView.arrangedSubviews.count > 0 { + return + } let findPeopleButton: PrimaryActionButton = { let button = PrimaryActionButton() button.setTitle(L10n.Common.Controls.Actions.findPeople, for: .normal) diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index 9d6bbedc..289583ae 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -98,10 +98,7 @@ extension SearchRecommendAccountsCollectionViewCell { headerImageView.layer.borderColor = Asset.Colors.Border.searchCard.color.cgColor applyShadow(color: Asset.Colors.Shadow.searchCard.color, alpha: 0.1, x: 0, y: 3, blur: 12, spread: 0) } - override open func layoutSubviews() { - super.layoutSubviews() - followButton.layer.cornerRadius = followButton.frame.height/2 - } + private func configure() { headerImageView.backgroundColor = Asset.Colors.brandBlue.color layer.cornerRadius = 10 diff --git a/Mastodon/Scene/Search/SearchViewController+Recommend.swift b/Mastodon/Scene/Search/SearchViewController+Recommend.swift index f394f09f..b5ff0e54 100644 --- a/Mastodon/Scene/Search/SearchViewController+Recommend.swift +++ b/Mastodon/Scene/Search/SearchViewController+Recommend.swift @@ -101,5 +101,11 @@ extension SearchViewController: UICollectionViewDelegateFlowLayout { extension SearchViewController { @objc func hashtagSeeAllButtonPressed(_ sender: UIButton) {} - @objc func accountSeeAllButtonPressed(_ sender: UIButton) {} + @objc func accountSeeAllButtonPressed(_ sender: UIButton) { + if self.viewModel.recommendAccounts.isEmpty { + return + } + let viewModel = SuggestionAccountViewModel(context: context, accounts: self.viewModel.recommendAccounts) + coordinator.present(scene: .suggestionAccount(viewModel: viewModel), from: self, transition: .modal(animated: true, completion: nil)) + } } diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index 1a1d87fc..04a977e0 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -34,6 +34,7 @@ final class SearchViewModel: NSObject { var recommendHashTags = [Mastodon.Entity.Tag]() var recommendAccounts = [NSManagedObjectID]() + var recommendAccountsFallback = PassthroughSubject() var hashtagDiffableDataSource: UICollectionViewDiffableDataSource? var accountDiffableDataSource: UICollectionViewDiffableDataSource? @@ -87,15 +88,15 @@ final class SearchViewModel: NSObject { .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) + 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) } .sink { _ in @@ -142,7 +143,6 @@ final class SearchViewModel: NSObject { } } dataSource.apply(snapshot, animatingDifferences: false, completion: nil) - } .store(in: &disposeBag) @@ -161,21 +161,33 @@ final class SearchViewModel: NSObject { } .store(in: &disposeBag) - requestRecommendAccounts() + requestRecommendAccountsV2() .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self = self else { return } if !self.recommendAccounts.isEmpty { - 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) + self.applyDataSource() } } receiveValue: { _ in } .store(in: &disposeBag) + recommendAccountsFallback + .sink { [weak self] _ in + guard let self = self else { return } + self.requestRecommendAccounts() + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + if !self.recommendAccounts.isEmpty { + self.applyDataSource() + } + } receiveValue: { _ in + } + .store(in: &self.disposeBag) + } + .store(in: &disposeBag) + searchResult .receive(on: DispatchQueue.main) .sink { [weak self] searchResult in @@ -227,13 +239,43 @@ final class SearchViewModel: NSObject { } } - func requestRecommendAccounts() -> Future { + func requestRecommendAccountsV2() -> 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.suggestionAccountV2(domain: activeMastodonAuthenticationBox.domain, query: nil, mastodonAuthenticationBox: activeMastodonAuthenticationBox) + .sink { [weak self] completion in + switch completion { + case .failure(let error): + if let apiError = error as? Mastodon.API.Error { + if apiError.httpResponseStatus == .notFound { + self?.recommendAccountsFallback.send() + } + } + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendAccount 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: recommendAccount request success", (#file as NSString).lastPathComponent, #line, #function) + promise(.success(())) + } + } receiveValue: { [weak self] accounts in + guard let self = self else { return } + let ids = accounts.value.compactMap({$0.account.id}) + self.receiveAccounts(ids: ids) + } + .store(in: &self.disposeBag) + } + } + + func requestRecommendAccounts() -> 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.suggestionAccount(domain: activeMastodonAuthenticationBox.domain, query: nil, mastodonAuthenticationBox: activeMastodonAuthenticationBox) .sink { completion in switch completion { case .failure(let error): @@ -245,28 +287,43 @@ final class SearchViewModel: NSObject { } } receiveValue: { [weak self] accounts in guard let self = self else { return } - let ids = accounts.value.compactMap({$0.account.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) - } + let ids = accounts.value.compactMap({$0.id}) + self.receiveAccounts(ids: ids) } .store(in: &self.disposeBag) } } + func applyDataSource() { + guard let dataSource = accountDiffableDataSource else { return } + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(recommendAccounts, toSection: .main) + dataSource.apply(snapshot, animatingDifferences: false, completion: nil) + } + + func receiveAccounts(ids: [String]) { + guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { + return + } + 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 { + recommendAccounts = users.map(\.objectID) + } + } + func accountCollectionViewItemDidSelected(mastodonUser: MastodonUser, from: UIViewController) { let viewModel = ProfileViewModel(context: context, optionalMastodonUser: mastodonUser) DispatchQueue.main.async { diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift index 16a916db..9cc6f33a 100644 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift +++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift @@ -15,11 +15,11 @@ import UIKit class SuggestionAccountViewController: UIViewController, NeedsDependency { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - + var disposeBag = Set() - + var viewModel: SuggestionAccountViewModel! - + let tableView: UITableView = { let tableView = ControlContainableTableView() tableView.register(SuggestionAccountTableViewCell.self, forCellReuseIdentifier: String(describing: SuggestionAccountTableViewCell.self)) @@ -29,14 +29,14 @@ class SuggestionAccountViewController: UIViewController, NeedsDependency { tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) return tableView }() - + lazy var tableHeader: UIView = { let view = UIView() view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color view.frame = CGRect(origin: .zero, size: CGSize(width: tableView.frame.width, height: 156)) return view }() - + let followExplainLabel: UILabel = { let label = UILabel() label.text = L10n.Scene.SuggestionAccount.followExplain @@ -45,7 +45,7 @@ class SuggestionAccountViewController: UIViewController, NeedsDependency { label.numberOfLines = 0 return label }() - + let avatarStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal @@ -84,7 +84,7 @@ extension SuggestionAccountViewController { viewModel: viewModel, delegate: self ) - + viewModel.accounts .receive(on: DispatchQueue.main) .sink { [weak self] accounts in @@ -105,7 +105,7 @@ extension SuggestionAccountViewController { followExplainLabel.leadingAnchor.constraint(equalTo: tableHeader.leadingAnchor, constant: 20), tableHeader.trailingAnchor.constraint(equalTo: followExplainLabel.trailingAnchor, constant: 20), ]) - + avatarStackView.translatesAutoresizingMaskIntoConstraints = false tableHeader.addSubview(avatarStackView) NSLayoutConstraint.activate([ @@ -136,7 +136,7 @@ extension SuggestionAccountViewController { } avatarStackView.addArrangedSubview(imageView) } - + tableView.tableHeaderView = tableHeader } } diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift index 9a92b059..33313bad 100644 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift +++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift @@ -18,64 +18,111 @@ final class SuggestionAccountViewModel: NSObject { // input let context: AppContext - let accounts = CurrentValueSubject<[NSManagedObjectID], Never>([]) - var selectedAccounts = [NSManagedObjectID]() // output - var diffableDataSource: UITableViewDiffableDataSource? + let accounts = CurrentValueSubject<[NSManagedObjectID], Never>([]) + var selectedAccounts = [NSManagedObjectID]() + var suggestionAccountsFallback = PassthroughSubject() + + var diffableDataSource: UITableViewDiffableDataSource? { + didSet(value) { + if !accounts.value.isEmpty { + applyDataSource(accounts: accounts.value) + } + } + } init(context: AppContext, accounts: [NSManagedObjectID]? = nil) { self.context = context - if let accounts = accounts { - self.accounts.value = accounts - } + super.init() self.accounts .receive(on: DispatchQueue.main) .sink { [weak self] accounts in - guard let dataSource = self?.diffableDataSource else { return } - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - snapshot.appendItems(accounts, toSection: .main) - dataSource.apply(snapshot, animatingDifferences: false, completion: nil) + self?.applyDataSource(accounts: accounts) } .store(in: &disposeBag) + if let accounts = accounts { + self.accounts.value = accounts + } + if accounts == nil || (accounts ?? []).isEmpty { guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } context.apiService.suggestionAccountV2(domain: activeMastodonAuthenticationBox.domain, query: nil, mastodonAuthenticationBox: activeMastodonAuthenticationBox) - .sink { completion in + .sink { [weak self] completion in switch completion { case .failure(let error): + if let apiError = error as? Mastodon.API.Error { + if apiError.httpResponseStatus == .notFound { + self?.suggestionAccountsFallback.send() + } + } os_log("%{public}s[%{public}ld], %{public}s: fetch recommendAccountV2 failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription) case .finished: // handle isFetchingLatestTimeline in fetch controller delegate break } } receiveValue: { [weak self] response in - guard let self = self else { return } let ids = response.value.map(\.account.id) - let users: [MastodonUser]? = { - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: activeMastodonAuthenticationBox.domain, ids: ids) - request.returnsObjectsAsFaults = false - do { - return try context.managedObjectContext.fetch(request) - } catch { - assertionFailure(error.localizedDescription) - return nil - } - }() - if let accounts = users?.map(\.objectID) { - self.accounts.value = accounts - } + self?.receiveAccounts(ids: ids) } .store(in: &disposeBag) + + suggestionAccountsFallback + .sink(receiveValue: { [weak self] _ in + self?.requestSuggestionAccount() + }) + .store(in: &disposeBag) } } + func requestSuggestionAccount() { + guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } + context.apiService.suggestionAccount(domain: activeMastodonAuthenticationBox.domain, query: nil, mastodonAuthenticationBox: activeMastodonAuthenticationBox) + .sink { completion in + switch completion { + case .failure(let error): + os_log("%{public}s[%{public}ld], %{public}s: fetch recommendAccount failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription) + case .finished: + // handle isFetchingLatestTimeline in fetch controller delegate + break + } + } receiveValue: { [weak self] response in + let ids = response.value.map(\.id) + self?.receiveAccounts(ids: ids) + } + .store(in: &disposeBag) + } + + func applyDataSource(accounts: [NSManagedObjectID]) { + guard let dataSource = diffableDataSource else { return } + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(accounts, toSection: .main) + dataSource.apply(snapshot, animatingDifferences: false, completion: nil) + } + + func receiveAccounts(ids: [String]) { + guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } + let users: [MastodonUser]? = { + let request = MastodonUser.sortedFetchRequest + request.predicate = MastodonUser.predicate(domain: activeMastodonAuthenticationBox.domain, ids: ids) + request.returnsObjectsAsFaults = false + do { + return try context.managedObjectContext.fetch(request) + } catch { + assertionFailure(error.localizedDescription) + return nil + } + }() + if let accounts = users?.map(\.objectID) { + self.accounts.value = accounts + } + } + func followAction() { guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } for objectID in selectedAccounts { diff --git a/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell.swift b/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell.swift index 9dafaedb..256b7bab 100644 --- a/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell.swift +++ b/Mastodon/Scene/SuggestionAccount/TableViewCell/SuggestionAccountTableViewCell.swift @@ -5,8 +5,6 @@ // Created by sxiaojian on 2021/4/21. // -import Foundation -import UIKit import Combine import CoreData import CoreDataStack @@ -19,7 +17,6 @@ protocol SuggestionAccountTableViewCellDelegate: AnyObject { } final class SuggestionAccountTableViewCell: UITableViewCell { - var disposeBag = Set() weak var delegate: SuggestionAccountTableViewCellDelegate? @@ -65,6 +62,7 @@ final class SuggestionAccountTableViewCell: UITableViewCell { .store(in: &self.disposeBag) return button }() + override func prepareForReuse() { super.prepareForReuse() _imageView.af.cancelImageRequest() @@ -139,11 +137,10 @@ extension SuggestionAccountTableViewCell { subTitleLabel.text = account.acct button.isSelected = isSelected button.publisher(for: .touchUpInside) - .sink { [weak self] sender in + .sink { [weak self] _ in guard let self = self else { return } self.delegate?.accountButtonPressed(objectID: account.objectID, sender: self.button) } .store(in: &disposeBag) } - }