Use accounts on FavoritedBy/RetootedBy-screens (IOS-214)
This commit is contained in:
parent
195029fc15
commit
9a5b4a3621
|
@ -11,7 +11,6 @@ import CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
enum UserItem: Hashable {
|
enum UserItem: Hashable {
|
||||||
case user(record: ManagedObjectRecord<MastodonUser>)
|
|
||||||
case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)
|
case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)
|
||||||
case bottomLoader
|
case bottomLoader
|
||||||
case bottomHeader(text: String)
|
case bottomHeader(text: String)
|
||||||
|
|
|
@ -48,27 +48,6 @@ extension UserSection {
|
||||||
delegate: userTableViewCellDelegate
|
delegate: userTableViewCellDelegate
|
||||||
)
|
)
|
||||||
|
|
||||||
return cell
|
|
||||||
|
|
||||||
case .user(let record):
|
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
|
|
||||||
context.managedObjectContext.performAndWait {
|
|
||||||
guard let user = record.object(in: context.managedObjectContext) else { return }
|
|
||||||
configure(
|
|
||||||
context: context,
|
|
||||||
authContext: authContext,
|
|
||||||
tableView: tableView,
|
|
||||||
cell: cell,
|
|
||||||
viewModel: UserTableViewCell.ViewModel(
|
|
||||||
user: user,
|
|
||||||
followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(),
|
|
||||||
blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(),
|
|
||||||
followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()
|
|
||||||
),
|
|
||||||
userTableViewCellDelegate: userTableViewCellDelegate
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cell
|
return cell
|
||||||
case .bottomLoader:
|
case .bottomLoader:
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||||
|
@ -82,23 +61,3 @@ extension UserSection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UserSection {
|
|
||||||
|
|
||||||
static func configure(
|
|
||||||
context: AppContext,
|
|
||||||
authContext: AuthContext,
|
|
||||||
tableView: UITableView,
|
|
||||||
cell: UserTableViewCell,
|
|
||||||
viewModel: UserTableViewCell.ViewModel,
|
|
||||||
userTableViewCellDelegate: UserTableViewCellDelegate?
|
|
||||||
) {
|
|
||||||
cell.configure(
|
|
||||||
me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext),
|
|
||||||
tableView: tableView,
|
|
||||||
viewModel: viewModel,
|
|
||||||
delegate: userTableViewCellDelegate
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -20,10 +20,10 @@ extension FavoritedByViewController: DataSourceProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch item {
|
switch item {
|
||||||
case .user(let record):
|
case .account(let account, let relationship):
|
||||||
return .user(record: record)
|
return .account(account: account, relationship: relationship)
|
||||||
default:
|
case .bottomHeader(_), .bottomLoader:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,10 +20,10 @@ extension RebloggedByViewController: DataSourceProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch item {
|
switch item {
|
||||||
case .user(let record):
|
case .account(let account, let relationship):
|
||||||
return .user(record: record)
|
return .account(account: account, relationship: relationship)
|
||||||
default:
|
case .bottomHeader(_), .bottomLoader:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import UIKit
|
||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
import Combine
|
import Combine
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
extension UserListViewModel {
|
extension UserListViewModel {
|
||||||
@MainActor
|
@MainActor
|
||||||
|
@ -33,17 +34,24 @@ extension UserListViewModel {
|
||||||
// trigger initial loading
|
// trigger initial loading
|
||||||
stateMachine.enter(UserListViewModel.State.Reloading.self)
|
stateMachine.enter(UserListViewModel.State.Reloading.self)
|
||||||
|
|
||||||
userFetchedResultsController.$records
|
$accounts
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] records in
|
.sink { [weak self] accounts in
|
||||||
guard let self = self else { return }
|
guard let self else { return }
|
||||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<UserSection, UserItem>()
|
var snapshot = NSDiffableDataSourceSnapshot<UserSection, UserItem>()
|
||||||
snapshot.appendSections([.main])
|
snapshot.appendSections([.main])
|
||||||
let items = records.map { UserItem.user(record: $0) }
|
|
||||||
|
let accountsWithRelationship: [(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)] = accounts.compactMap { account in
|
||||||
|
guard let relationship = self.relationships.first(where: {$0.id == account.id }) else { return (account: account, relationship: nil)}
|
||||||
|
|
||||||
|
return (account: account, relationship: relationship)
|
||||||
|
}
|
||||||
|
|
||||||
|
let items = accountsWithRelationship.map { UserItem.account(account: $0.account, relationship: $0.relationship) }
|
||||||
snapshot.appendItems(items, toSection: .main)
|
snapshot.appendItems(items, toSection: .main)
|
||||||
|
|
||||||
if let currentState = self.stateMachine.currentState {
|
if let currentState = self.stateMachine.currentState {
|
||||||
switch currentState {
|
switch currentState {
|
||||||
case is State.Initial, is State.Idle, is State.Reloading, is State.Loading, is State.Fail:
|
case is State.Initial, is State.Idle, is State.Reloading, is State.Loading, is State.Fail:
|
||||||
|
|
|
@ -52,11 +52,12 @@ extension UserListViewModel.State {
|
||||||
|
|
||||||
override func didEnter(from previousState: GKState?) {
|
override func didEnter(from previousState: GKState?) {
|
||||||
super.didEnter(from: previousState)
|
super.didEnter(from: previousState)
|
||||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
guard let viewModel, let stateMachine else { return }
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
viewModel.userFetchedResultsController.userIDs = []
|
viewModel.accounts = []
|
||||||
|
viewModel.relationships = []
|
||||||
|
|
||||||
stateMachine.enter(Loading.self)
|
stateMachine.enter(Loading.self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,40 +124,61 @@ extension UserListViewModel.State {
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
let response: Mastodon.Response.Content<[Mastodon.Entity.Account]>
|
let accountResponse: Mastodon.Response.Content<[Mastodon.Entity.Account]>
|
||||||
switch viewModel.kind {
|
switch viewModel.kind {
|
||||||
case .favoritedBy(let status):
|
case .favoritedBy(let status):
|
||||||
response = try await viewModel.context.apiService.favoritedBy(
|
accountResponse = try await viewModel.context.apiService.favoritedBy(
|
||||||
status: status,
|
status: status,
|
||||||
query: .init(maxID: maxID, limit: nil),
|
query: .init(maxID: maxID, limit: nil),
|
||||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||||
)
|
)
|
||||||
case .rebloggedBy(let status):
|
case .rebloggedBy(let status):
|
||||||
response = try await viewModel.context.apiService.rebloggedBy(
|
accountResponse = try await viewModel.context.apiService.rebloggedBy(
|
||||||
status: status,
|
status: status,
|
||||||
query: .init(maxID: maxID, limit: nil),
|
query: .init(maxID: maxID, limit: nil),
|
||||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if accountResponse.value.isEmpty {
|
||||||
|
await enter(state: NoMore.self)
|
||||||
|
|
||||||
|
viewModel.accounts = []
|
||||||
|
viewModel.relationships = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var hasNewAppend = false
|
var hasNewAppend = false
|
||||||
var userIDs = viewModel.userFetchedResultsController.userIDs
|
|
||||||
for user in response.value {
|
let newRelationships = try await viewModel.context.apiService.relationship(forAccounts: accountResponse.value, authenticationBox: viewModel.authContext.mastodonAuthenticationBox)
|
||||||
guard !userIDs.contains(user.id) else { continue }
|
|
||||||
userIDs.append(user.id)
|
var accounts = viewModel.accounts
|
||||||
|
|
||||||
|
for user in accountResponse.value {
|
||||||
|
guard accounts.contains(user) == false else { continue }
|
||||||
|
accounts.append(user)
|
||||||
hasNewAppend = true
|
hasNewAppend = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let maxID = response.link?.maxID
|
var relationships = viewModel.relationships
|
||||||
|
|
||||||
|
for relationship in newRelationships.value {
|
||||||
|
guard relationships.contains(relationship) == false else { continue }
|
||||||
|
relationships.append(relationship)
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxID = accountResponse.link?.maxID
|
||||||
|
|
||||||
if hasNewAppend, maxID != nil {
|
if hasNewAppend, maxID != nil {
|
||||||
await enter(state: Idle.self)
|
await enter(state: Idle.self)
|
||||||
} else {
|
} else {
|
||||||
await enter(state: NoMore.self)
|
await enter(state: NoMore.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.accounts = accounts
|
||||||
|
viewModel.relationships = relationships
|
||||||
self.maxID = maxID
|
self.maxID = maxID
|
||||||
viewModel.userFetchedResultsController.userIDs = userIDs
|
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
await enter(state: Fail.self)
|
await enter(state: Fail.self)
|
||||||
}
|
}
|
||||||
|
@ -177,9 +199,9 @@ extension UserListViewModel.State {
|
||||||
override func didEnter(from previousState: GKState?) {
|
override func didEnter(from previousState: GKState?) {
|
||||||
super.didEnter(from: previousState)
|
super.didEnter(from: previousState)
|
||||||
|
|
||||||
guard let viewModel = viewModel else { return }
|
guard let viewModel else { return }
|
||||||
// trigger reload
|
// trigger reload
|
||||||
viewModel.userFetchedResultsController.userIDs = viewModel.userFetchedResultsController.userIDs
|
viewModel.accounts = viewModel.accounts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import Combine
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import GameplayKit
|
import GameplayKit
|
||||||
import MastodonCore
|
import MastodonCore
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
final class UserListViewModel {
|
final class UserListViewModel {
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
@ -18,7 +19,8 @@ final class UserListViewModel {
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let authContext: AuthContext
|
let authContext: AuthContext
|
||||||
let kind: Kind
|
let kind: Kind
|
||||||
let userFetchedResultsController: UserFetchedResultsController
|
@Published var accounts: [Mastodon.Entity.Account]
|
||||||
|
@Published var relationships: [Mastodon.Entity.Relationship]
|
||||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||||
|
|
||||||
// output
|
// output
|
||||||
|
@ -43,12 +45,8 @@ final class UserListViewModel {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.authContext = authContext
|
self.authContext = authContext
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.userFetchedResultsController = UserFetchedResultsController(
|
self.accounts = []
|
||||||
managedObjectContext: context.managedObjectContext,
|
self.relationships = []
|
||||||
domain: authContext.mastodonAuthenticationBox.domain,
|
|
||||||
additionalPredicate: nil
|
|
||||||
)
|
|
||||||
// end init
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,111 +0,0 @@
|
||||||
//
|
|
||||||
// UserFetchedResultsController.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by MainasuK Cirno on 2021-7-7.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Combine
|
|
||||||
import CoreData
|
|
||||||
import CoreDataStack
|
|
||||||
import MastodonSDK
|
|
||||||
|
|
||||||
public final class UserFetchedResultsController: NSObject {
|
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
let fetchedResultsController: NSFetchedResultsController<MastodonUser>
|
|
||||||
|
|
||||||
// input
|
|
||||||
@Published public var domain: String? = nil
|
|
||||||
@Published public var userIDs: [Mastodon.Entity.Account.ID] = []
|
|
||||||
@Published public var additionalPredicate: NSPredicate?
|
|
||||||
|
|
||||||
// output
|
|
||||||
let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
|
||||||
@Published public private(set) var records: [ManagedObjectRecord<MastodonUser>] = []
|
|
||||||
|
|
||||||
public init(
|
|
||||||
managedObjectContext: NSManagedObjectContext,
|
|
||||||
domain: String?,
|
|
||||||
additionalPredicate: NSPredicate?
|
|
||||||
) {
|
|
||||||
self.domain = domain ?? ""
|
|
||||||
self.fetchedResultsController = {
|
|
||||||
let fetchRequest = MastodonUser.sortedFetchRequest
|
|
||||||
fetchRequest.predicate = MastodonUser.predicate(domain: domain ?? "", ids: [])
|
|
||||||
fetchRequest.returnsObjectsAsFaults = false
|
|
||||||
fetchRequest.fetchBatchSize = 20
|
|
||||||
let controller = NSFetchedResultsController(
|
|
||||||
fetchRequest: fetchRequest,
|
|
||||||
managedObjectContext: managedObjectContext,
|
|
||||||
sectionNameKeyPath: nil,
|
|
||||||
cacheName: nil
|
|
||||||
)
|
|
||||||
|
|
||||||
return controller
|
|
||||||
}()
|
|
||||||
self.additionalPredicate = additionalPredicate
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
// debounce output to prevent UI update issues
|
|
||||||
_objectIDs
|
|
||||||
.throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true)
|
|
||||||
.map { objectIDs in objectIDs.map { ManagedObjectRecord(objectID: $0) } }
|
|
||||||
.assign(to: &$records)
|
|
||||||
|
|
||||||
fetchedResultsController.delegate = self
|
|
||||||
|
|
||||||
Publishers.CombineLatest3(
|
|
||||||
self.$domain.removeDuplicates(),
|
|
||||||
self.$userIDs.removeDuplicates(),
|
|
||||||
self.$additionalPredicate.removeDuplicates()
|
|
||||||
)
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] domain, ids, additionalPredicate in
|
|
||||||
guard let self = self else { return }
|
|
||||||
var predicates = [MastodonUser.predicate(domain: domain ?? "", ids: ids)]
|
|
||||||
if let additionalPredicate = additionalPredicate {
|
|
||||||
predicates.append(additionalPredicate)
|
|
||||||
}
|
|
||||||
self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
|
|
||||||
do {
|
|
||||||
try self.fetchedResultsController.performFetch()
|
|
||||||
} catch {
|
|
||||||
assertionFailure(error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension UserFetchedResultsController {
|
|
||||||
|
|
||||||
public func append(userIDs: [Mastodon.Entity.Account.ID]) {
|
|
||||||
var result = self.userIDs
|
|
||||||
for userID in userIDs where !result.contains(userID) {
|
|
||||||
result.append(userID)
|
|
||||||
}
|
|
||||||
self.userIDs = result
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - NSFetchedResultsControllerDelegate
|
|
||||||
extension UserFetchedResultsController: NSFetchedResultsControllerDelegate {
|
|
||||||
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
|
||||||
|
|
||||||
let indexes = userIDs
|
|
||||||
let objects = fetchedResultsController.fetchedObjects ?? []
|
|
||||||
|
|
||||||
let items: [NSManagedObjectID] = objects
|
|
||||||
.compactMap { object in
|
|
||||||
indexes.firstIndex(of: object.id).map { index in (index, object) }
|
|
||||||
}
|
|
||||||
.sorted { $0.0 < $1.0 }
|
|
||||||
.map { $0.1.objectID }
|
|
||||||
self._objectIDs.value = items
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue