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()
}
}
} 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)

View File

@ -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

View File

@ -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))
}
}

View File

@ -34,6 +34,7 @@ final class SearchViewModel: NSObject {
var recommendHashTags = [Mastodon.Entity.Tag]()
var recommendAccounts = [NSManagedObjectID]()
var recommendAccountsFallback = PassthroughSubject<Void, Never>()
var hashtagDiffableDataSource: UICollectionViewDiffableDataSource<RecommendHashTagSection, Mastodon.Entity.Tag>?
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
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<RecommendAccountSection, NSManagedObjectID>()
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<Void, Error> {
func requestRecommendAccountsV2() -> 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.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
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<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) {
let viewModel = ProfileViewModel(context: context, optionalMastodonUser: mastodonUser)
DispatchQueue.main.async {

View File

@ -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<AnyCancellable>()
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
}
}

View File

@ -18,64 +18,111 @@ final class SuggestionAccountViewModel: NSObject {
// input
let context: AppContext
let accounts = CurrentValueSubject<[NSManagedObjectID], Never>([])
var selectedAccounts = [NSManagedObjectID]()
// 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) {
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<RecommendAccountSection, NSManagedObjectID>()
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<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
}
}
func followAction() {
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
for objectID in selectedAccounts {

View File

@ -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<AnyCancellable>()
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)
}
}