forked from zelo72/mastodon-ios
chore: compatible with the old server
This commit is contained in:
parent
c8474c6a7f
commit
776263aaf2
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue