From 9a5b4a3621e24fe888cbfb6ebc032d3a37566f72 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 15 Dec 2023 15:51:35 +0100 Subject: [PATCH 1/3] Use accounts on FavoritedBy/RetootedBy-screens (IOS-214) --- Mastodon/Diffable/User/UserItem.swift | 1 - Mastodon/Diffable/User/UserSection.swift | 41 ------- ...dByViewController+DataSourceProvider.swift | 8 +- ...dByViewController+DataSourceProvider.swift | 8 +- .../UserLIst/UserListViewModel+Diffable.swift | 18 ++- .../UserLIst/UserListViewModel+State.swift | 56 ++++++--- .../Profile/UserLIst/UserListViewModel.swift | 12 +- .../UserFetchedResultsController.swift | 111 ------------------ 8 files changed, 65 insertions(+), 190 deletions(-) delete mode 100644 MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift diff --git a/Mastodon/Diffable/User/UserItem.swift b/Mastodon/Diffable/User/UserItem.swift index ba44aa52a..51c9cb443 100644 --- a/Mastodon/Diffable/User/UserItem.swift +++ b/Mastodon/Diffable/User/UserItem.swift @@ -11,7 +11,6 @@ import CoreDataStack import MastodonSDK enum UserItem: Hashable { - case user(record: ManagedObjectRecord) case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) case bottomLoader case bottomHeader(text: String) diff --git a/Mastodon/Diffable/User/UserSection.swift b/Mastodon/Diffable/User/UserSection.swift index 6997e5159..2d99d2f36 100644 --- a/Mastodon/Diffable/User/UserSection.swift +++ b/Mastodon/Diffable/User/UserSection.swift @@ -48,27 +48,6 @@ extension UserSection { 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 case .bottomLoader: 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 - ) - } - -} diff --git a/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift index 437873d36..bae2325c5 100644 --- a/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift @@ -20,10 +20,10 @@ extension FavoritedByViewController: DataSourceProvider { } switch item { - case .user(let record): - return .user(record: record) - default: - return nil + case .account(let account, let relationship): + return .account(account: account, relationship: relationship) + case .bottomHeader(_), .bottomLoader: + return nil } } diff --git a/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift index 04d5d2596..c7afa6e9c 100644 --- a/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift @@ -20,10 +20,10 @@ extension RebloggedByViewController: DataSourceProvider { } switch item { - case .user(let record): - return .user(record: record) - default: - return nil + case .account(let account, let relationship): + return .account(account: account, relationship: relationship) + case .bottomHeader(_), .bottomLoader: + return nil } } diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift index d4830affc..b172d4c4a 100644 --- a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift @@ -9,6 +9,7 @@ import UIKit import MastodonAsset import MastodonLocalization import Combine +import MastodonSDK extension UserListViewModel { @MainActor @@ -33,17 +34,24 @@ extension UserListViewModel { // trigger initial loading stateMachine.enter(UserListViewModel.State.Reloading.self) - userFetchedResultsController.$records + $accounts .receive(on: DispatchQueue.main) - .sink { [weak self] records in - guard let self = self else { return } + .sink { [weak self] accounts in + guard let self else { return } guard let diffableDataSource = self.diffableDataSource else { return } var snapshot = NSDiffableDataSourceSnapshot() 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) - + if let currentState = self.stateMachine.currentState { switch currentState { case is State.Initial, is State.Idle, is State.Reloading, is State.Loading, is State.Fail: diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift index cb6e9d3fa..0cf980f50 100644 --- a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift +++ b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift @@ -52,11 +52,12 @@ extension UserListViewModel.State { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + guard let viewModel, let stateMachine else { return } // reset - viewModel.userFetchedResultsController.userIDs = [] - + viewModel.accounts = [] + viewModel.relationships = [] + stateMachine.enter(Loading.self) } } @@ -123,40 +124,61 @@ extension UserListViewModel.State { Task { do { - let response: Mastodon.Response.Content<[Mastodon.Entity.Account]> + let accountResponse: Mastodon.Response.Content<[Mastodon.Entity.Account]> switch viewModel.kind { case .favoritedBy(let status): - response = try await viewModel.context.apiService.favoritedBy( + accountResponse = try await viewModel.context.apiService.favoritedBy( status: status, query: .init(maxID: maxID, limit: nil), authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) case .rebloggedBy(let status): - response = try await viewModel.context.apiService.rebloggedBy( + accountResponse = try await viewModel.context.apiService.rebloggedBy( status: status, query: .init(maxID: maxID, limit: nil), authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) } + if accountResponse.value.isEmpty { + await enter(state: NoMore.self) + + viewModel.accounts = [] + viewModel.relationships = [] + return + } + var hasNewAppend = false - var userIDs = viewModel.userFetchedResultsController.userIDs - for user in response.value { - guard !userIDs.contains(user.id) else { continue } - userIDs.append(user.id) + + let newRelationships = try await viewModel.context.apiService.relationship(forAccounts: accountResponse.value, authenticationBox: viewModel.authContext.mastodonAuthenticationBox) + + var accounts = viewModel.accounts + + for user in accountResponse.value { + guard accounts.contains(user) == false else { continue } + accounts.append(user) 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 { await enter(state: Idle.self) } else { await enter(state: NoMore.self) } + + viewModel.accounts = accounts + viewModel.relationships = relationships self.maxID = maxID - viewModel.userFetchedResultsController.userIDs = userIDs - + } catch { await enter(state: Fail.self) } @@ -177,9 +199,9 @@ extension UserListViewModel.State { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let viewModel = viewModel else { return } + guard let viewModel else { return } // trigger reload - viewModel.userFetchedResultsController.userIDs = viewModel.userFetchedResultsController.userIDs + viewModel.accounts = viewModel.accounts } } } diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift b/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift index d27562b94..f9610ee61 100644 --- a/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift +++ b/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift @@ -10,6 +10,7 @@ import Combine import CoreDataStack import GameplayKit import MastodonCore +import MastodonSDK final class UserListViewModel { var disposeBag = Set() @@ -18,7 +19,8 @@ final class UserListViewModel { let context: AppContext let authContext: AuthContext let kind: Kind - let userFetchedResultsController: UserFetchedResultsController + @Published var accounts: [Mastodon.Entity.Account] + @Published var relationships: [Mastodon.Entity.Relationship] let listBatchFetchViewModel = ListBatchFetchViewModel() // output @@ -43,12 +45,8 @@ final class UserListViewModel { self.context = context self.authContext = authContext self.kind = kind - self.userFetchedResultsController = UserFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalPredicate: nil - ) - // end init + self.accounts = [] + self.relationships = [] } } diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift deleted file mode 100644 index 452fa2914..000000000 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift +++ /dev/null @@ -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() - - let fetchedResultsController: NSFetchedResultsController - - // 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] = [] - - 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, 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 - } -} From b00625c99a6fcdd02053f92cc2616f85e5820d99 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 3 Jan 2024 16:36:03 +0100 Subject: [PATCH 2/3] Fix Typo (:see_no_evil:) --- Mastodon.xcodeproj/project.pbxproj | 6 +++--- .../FavoritedByViewController+DataSourceProvider.swift | 0 .../FavoritedBy/FavoritedByViewController.swift | 0 .../RebloggedByViewController+DataSourceProvider.swift | 0 .../RebloggedBy/RebloggedByViewController.swift | 0 .../{UserLIst => UserList}/UserListViewModel+Diffable.swift | 0 .../{UserLIst => UserList}/UserListViewModel+State.swift | 0 .../Profile/{UserLIst => UserList}/UserListViewModel.swift | 0 8 files changed, 3 insertions(+), 3 deletions(-) rename Mastodon/Scene/Profile/{UserLIst => UserList}/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift (100%) rename Mastodon/Scene/Profile/{UserLIst => UserList}/FavoritedBy/FavoritedByViewController.swift (100%) rename Mastodon/Scene/Profile/{UserLIst => UserList}/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift (100%) rename Mastodon/Scene/Profile/{UserLIst => UserList}/RebloggedBy/RebloggedByViewController.swift (100%) rename Mastodon/Scene/Profile/{UserLIst => UserList}/UserListViewModel+Diffable.swift (100%) rename Mastodon/Scene/Profile/{UserLIst => UserList}/UserListViewModel+State.swift (100%) rename Mastodon/Scene/Profile/{UserLIst => UserList}/UserListViewModel.swift (100%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 363660d02..8f3bf4428 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -2309,7 +2309,7 @@ path = FamiliarFollowers; sourceTree = ""; }; - DB5B54A42833BD1D00DEF8B2 /* UserLIst */ = { + DB5B54A42833BD1D00DEF8B2 /* UserList */ = { isa = PBXGroup; children = ( DB5B54A22833BD1A00DEF8B2 /* UserListViewModel.swift */, @@ -2318,7 +2318,7 @@ DB5B54A92833BFAC00DEF8B2 /* FavoritedBy */, DB5B54AC2833C12D00DEF8B2 /* RebloggedBy */, ); - path = UserLIst; + path = UserList; sourceTree = ""; }; DB5B54A92833BFAC00DEF8B2 /* FavoritedBy */ = { @@ -2778,7 +2778,7 @@ DB6B74F0272FB55400C70B6E /* Follower */, DB5B7296273112B400081888 /* Following */, DB5B549B2833A60600DEF8B2 /* FamiliarFollowers */, - DB5B54A42833BD1D00DEF8B2 /* UserLIst */, + DB5B54A42833BD1D00DEF8B2 /* UserList */, DBFEEC97279BDC6A004F81DD /* About */, DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */, DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */, diff --git a/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/UserList/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift similarity index 100% rename from Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift rename to Mastodon/Scene/Profile/UserList/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift diff --git a/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController.swift b/Mastodon/Scene/Profile/UserList/FavoritedBy/FavoritedByViewController.swift similarity index 100% rename from Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController.swift rename to Mastodon/Scene/Profile/UserList/FavoritedBy/FavoritedByViewController.swift diff --git a/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/UserList/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift similarity index 100% rename from Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift rename to Mastodon/Scene/Profile/UserList/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift diff --git a/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController.swift b/Mastodon/Scene/Profile/UserList/RebloggedBy/RebloggedByViewController.swift similarity index 100% rename from Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController.swift rename to Mastodon/Scene/Profile/UserList/RebloggedBy/RebloggedByViewController.swift diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift b/Mastodon/Scene/Profile/UserList/UserListViewModel+Diffable.swift similarity index 100% rename from Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift rename to Mastodon/Scene/Profile/UserList/UserListViewModel+Diffable.swift diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift b/Mastodon/Scene/Profile/UserList/UserListViewModel+State.swift similarity index 100% rename from Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift rename to Mastodon/Scene/Profile/UserList/UserListViewModel+State.swift diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift b/Mastodon/Scene/Profile/UserList/UserListViewModel.swift similarity index 100% rename from Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift rename to Mastodon/Scene/Profile/UserList/UserListViewModel.swift From 695d31720a6a65a5d44c2fc2c9d5c4b2cedaa8fa Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 3 Jan 2024 17:00:38 +0100 Subject: [PATCH 3/3] Fix indention --- Mastodon/Scene/Profile/UserList/UserListViewModel+State.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/Profile/UserList/UserListViewModel+State.swift b/Mastodon/Scene/Profile/UserList/UserListViewModel+State.swift index 0cf980f50..6b96b3400 100644 --- a/Mastodon/Scene/Profile/UserList/UserListViewModel+State.swift +++ b/Mastodon/Scene/Profile/UserList/UserListViewModel+State.swift @@ -127,13 +127,13 @@ extension UserListViewModel.State { let accountResponse: Mastodon.Response.Content<[Mastodon.Entity.Account]> switch viewModel.kind { case .favoritedBy(let status): - accountResponse = try await viewModel.context.apiService.favoritedBy( + accountResponse = try await viewModel.context.apiService.favoritedBy( status: status, query: .init(maxID: maxID, limit: nil), authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) case .rebloggedBy(let status): - accountResponse = try await viewModel.context.apiService.rebloggedBy( + accountResponse = try await viewModel.context.apiService.rebloggedBy( status: status, query: .init(maxID: maxID, limit: nil), authenticationBox: viewModel.authContext.mastodonAuthenticationBox