chore: compatible with the old server

This commit is contained in:
sunxiaojian 2021-04-21 17:58:56 +08:00
parent c8474c6a7f
commit 776263aaf2
7 changed files with 189 additions and 80 deletions

View File

@ -159,6 +159,8 @@ extension HomeTimelineViewController {
self.emptyView.removeFromSuperview() self.emptyView.removeFromSuperview()
} }
} }
} else {
self.emptyView.removeFromSuperview()
} }
} }
.store(in: &disposeBag) .store(in: &disposeBag)
@ -245,6 +247,9 @@ extension HomeTimelineViewController {
emptyView.bottomAnchor.constraint(equalTo: view.readableContentGuide.bottomAnchor) emptyView.bottomAnchor.constraint(equalTo: view.readableContentGuide.bottomAnchor)
]) ])
if emptyView.arrangedSubviews.count > 0 {
return
}
let findPeopleButton: PrimaryActionButton = { let findPeopleButton: PrimaryActionButton = {
let button = PrimaryActionButton() let button = PrimaryActionButton()
button.setTitle(L10n.Common.Controls.Actions.findPeople, for: .normal) button.setTitle(L10n.Common.Controls.Actions.findPeople, for: .normal)

View File

@ -98,10 +98,7 @@ extension SearchRecommendAccountsCollectionViewCell {
headerImageView.layer.borderColor = Asset.Colors.Border.searchCard.color.cgColor 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) 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() { private func configure() {
headerImageView.backgroundColor = Asset.Colors.brandBlue.color headerImageView.backgroundColor = Asset.Colors.brandBlue.color
layer.cornerRadius = 10 layer.cornerRadius = 10

View File

@ -101,5 +101,11 @@ extension SearchViewController: UICollectionViewDelegateFlowLayout {
extension SearchViewController { extension SearchViewController {
@objc func hashtagSeeAllButtonPressed(_ sender: UIButton) {} @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))
}
} }

View File

@ -34,6 +34,7 @@ final class SearchViewModel: NSObject {
var recommendHashTags = [Mastodon.Entity.Tag]() var recommendHashTags = [Mastodon.Entity.Tag]()
var recommendAccounts = [NSManagedObjectID]() var recommendAccounts = [NSManagedObjectID]()
var recommendAccountsFallback = PassthroughSubject<Void, Never>()
var hashtagDiffableDataSource: UICollectionViewDiffableDataSource<RecommendHashTagSection, Mastodon.Entity.Tag>? var hashtagDiffableDataSource: UICollectionViewDiffableDataSource<RecommendHashTagSection, Mastodon.Entity.Tag>?
var accountDiffableDataSource: UICollectionViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID>? var accountDiffableDataSource: UICollectionViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID>?
@ -87,15 +88,15 @@ final class SearchViewModel: NSObject {
.flatMap { (text, scope) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, Error> in .flatMap { (text, scope) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, Error> in
let query = Mastodon.API.V2.Search.Query(q: text, let query = Mastodon.API.V2.Search.Query(q: text,
type: scope, type: scope,
accountID: nil, accountID: nil,
maxID: nil, maxID: nil,
minID: nil, minID: nil,
excludeUnreviewed: nil, excludeUnreviewed: nil,
resolve: nil, resolve: nil,
limit: nil, limit: nil,
offset: nil, offset: nil,
following: nil) following: nil)
return context.apiService.search(domain: activeMastodonAuthenticationBox.domain, query: query, mastodonAuthenticationBox: activeMastodonAuthenticationBox) return context.apiService.search(domain: activeMastodonAuthenticationBox.domain, query: query, mastodonAuthenticationBox: activeMastodonAuthenticationBox)
} }
.sink { _ in .sink { _ in
@ -142,7 +143,6 @@ final class SearchViewModel: NSObject {
} }
} }
dataSource.apply(snapshot, animatingDifferences: false, completion: nil) dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
} }
.store(in: &disposeBag) .store(in: &disposeBag)
@ -161,21 +161,33 @@ final class SearchViewModel: NSObject {
} }
.store(in: &disposeBag) .store(in: &disposeBag)
requestRecommendAccounts() requestRecommendAccountsV2()
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] _ in .sink { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
if !self.recommendAccounts.isEmpty { if !self.recommendAccounts.isEmpty {
guard let dataSource = self.accountDiffableDataSource else { return } self.applyDataSource()
var snapshot = NSDiffableDataSourceSnapshot<RecommendAccountSection, NSManagedObjectID>()
snapshot.appendSections([.main])
snapshot.appendItems(self.recommendAccounts, toSection: .main)
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
} }
} receiveValue: { _ in } receiveValue: { _ in
} }
.store(in: &disposeBag) .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 searchResult
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] searchResult in .sink { [weak self] searchResult in
@ -227,13 +239,43 @@ final class SearchViewModel: NSObject {
} }
} }
func requestRecommendAccounts() -> 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 {
promise(.failure(APIService.APIError.implicit(APIService.APIError.ErrorReason.authenticationMissing))) promise(.failure(APIService.APIError.implicit(APIService.APIError.ErrorReason.authenticationMissing)))
return return
} }
self.context.apiService.suggestionAccountV2(domain: activeMastodonAuthenticationBox.domain, query: nil, mastodonAuthenticationBox: activeMastodonAuthenticationBox) 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<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.suggestionAccount(domain: activeMastodonAuthenticationBox.domain, query: nil, mastodonAuthenticationBox: activeMastodonAuthenticationBox)
.sink { completion in .sink { completion in
switch completion { switch completion {
case .failure(let error): case .failure(let error):
@ -245,28 +287,43 @@ final class SearchViewModel: NSObject {
} }
} receiveValue: { [weak self] accounts in } receiveValue: { [weak self] accounts in
guard let self = self else { return } guard let self = self else { return }
let ids = accounts.value.compactMap({$0.account.id}) let ids = accounts.value.compactMap({$0.id})
let userFetchRequest = MastodonUser.sortedFetchRequest self.receiveAccounts(ids: ids)
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) .store(in: &self.disposeBag)
} }
} }
func applyDataSource() {
guard let dataSource = accountDiffableDataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot<RecommendAccountSection, NSManagedObjectID>()
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) { func accountCollectionViewItemDidSelected(mastodonUser: MastodonUser, from: UIViewController) {
let viewModel = ProfileViewModel(context: context, optionalMastodonUser: mastodonUser) let viewModel = ProfileViewModel(context: context, optionalMastodonUser: mastodonUser)
DispatchQueue.main.async { DispatchQueue.main.async {

View File

@ -18,61 +18,108 @@ final class SuggestionAccountViewModel: NSObject {
// input // input
let context: AppContext let context: AppContext
let accounts = CurrentValueSubject<[NSManagedObjectID], Never>([])
var selectedAccounts = [NSManagedObjectID]()
// output // output
var diffableDataSource: UITableViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID>? let accounts = CurrentValueSubject<[NSManagedObjectID], Never>([])
var selectedAccounts = [NSManagedObjectID]()
var suggestionAccountsFallback = PassthroughSubject<Void, Never>()
var diffableDataSource: UITableViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID>? {
didSet(value) {
if !accounts.value.isEmpty {
applyDataSource(accounts: accounts.value)
}
}
}
init(context: AppContext, accounts: [NSManagedObjectID]? = nil) { init(context: AppContext, accounts: [NSManagedObjectID]? = nil) {
self.context = context self.context = context
if let accounts = accounts {
self.accounts.value = accounts
}
super.init() super.init()
self.accounts self.accounts
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] accounts in .sink { [weak self] accounts in
guard let dataSource = self?.diffableDataSource else { return } self?.applyDataSource(accounts: accounts)
var snapshot = NSDiffableDataSourceSnapshot<RecommendAccountSection, NSManagedObjectID>()
snapshot.appendSections([.main])
snapshot.appendItems(accounts, toSection: .main)
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
} }
.store(in: &disposeBag) .store(in: &disposeBag)
if let accounts = accounts {
self.accounts.value = accounts
}
if accounts == nil || (accounts ?? []).isEmpty { if accounts == nil || (accounts ?? []).isEmpty {
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
context.apiService.suggestionAccountV2(domain: activeMastodonAuthenticationBox.domain, query: nil, mastodonAuthenticationBox: activeMastodonAuthenticationBox) context.apiService.suggestionAccountV2(domain: activeMastodonAuthenticationBox.domain, query: nil, mastodonAuthenticationBox: activeMastodonAuthenticationBox)
.sink { completion in .sink { [weak self] completion in
switch completion { switch completion {
case .failure(let error): 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) os_log("%{public}s[%{public}ld], %{public}s: fetch recommendAccountV2 failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
case .finished: case .finished:
// handle isFetchingLatestTimeline in fetch controller delegate // handle isFetchingLatestTimeline in fetch controller delegate
break break
} }
} receiveValue: { [weak self] response in } receiveValue: { [weak self] response in
guard let self = self else { return }
let ids = response.value.map(\.account.id) let ids = response.value.map(\.account.id)
let users: [MastodonUser]? = { self?.receiveAccounts(ids: ids)
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
}
} }
.store(in: &disposeBag) .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<RecommendAccountSection, NSManagedObjectID>()
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
} }
} }

View File

@ -5,8 +5,6 @@
// Created by sxiaojian on 2021/4/21. // Created by sxiaojian on 2021/4/21.
// //
import Foundation
import UIKit
import Combine import Combine
import CoreData import CoreData
import CoreDataStack import CoreDataStack
@ -19,7 +17,6 @@ protocol SuggestionAccountTableViewCellDelegate: AnyObject {
} }
final class SuggestionAccountTableViewCell: UITableViewCell { final class SuggestionAccountTableViewCell: UITableViewCell {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
weak var delegate: SuggestionAccountTableViewCellDelegate? weak var delegate: SuggestionAccountTableViewCellDelegate?
@ -65,6 +62,7 @@ final class SuggestionAccountTableViewCell: UITableViewCell {
.store(in: &self.disposeBag) .store(in: &self.disposeBag)
return button return button
}() }()
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
_imageView.af.cancelImageRequest() _imageView.af.cancelImageRequest()
@ -139,11 +137,10 @@ extension SuggestionAccountTableViewCell {
subTitleLabel.text = account.acct subTitleLabel.text = account.acct
button.isSelected = isSelected button.isSelected = isSelected
button.publisher(for: .touchUpInside) button.publisher(for: .touchUpInside)
.sink { [weak self] sender in .sink { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
self.delegate?.accountButtonPressed(objectID: account.objectID, sender: self.button) self.delegate?.accountButtonPressed(objectID: account.objectID, sender: self.button)
} }
.store(in: &disposeBag) .store(in: &disposeBag)
} }
} }