fix: [WIP] add suggestion account scene back

This commit is contained in:
CMK 2022-02-10 19:30:41 +08:00
parent c1e1d527fe
commit 54e84ed814
23 changed files with 541 additions and 477 deletions

View File

@ -483,6 +483,8 @@
DBB45B5927B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5827B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift */; };
DBB45B5B27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5A27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift */; };
DBB45B5E27B4EB22002DC5A7 /* AdaptiveMarginStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5D27B4EB22002DC5A7 /* AdaptiveMarginStatusTableViewCell.swift */; };
DBB45B6027B50A4F002DC5A7 /* RecommendAccountItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */; };
DBB45B6227B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B6127B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift */; };
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; };
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; };
DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; };
@ -1227,6 +1229,8 @@
DBB45B5827B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewVideoViewModel.swift; sourceTree = "<group>"; };
DBB45B5A27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewTransitionViewController.swift; sourceTree = "<group>"; };
DBB45B5D27B4EB22002DC5A7 /* AdaptiveMarginStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveMarginStatusTableViewCell.swift; sourceTree = "<group>"; };
DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendAccountItem.swift; sourceTree = "<group>"; };
DBB45B6127B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountViewModel+Diffable.swift"; sourceTree = "<group>"; };
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; };
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; };
DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewModel.swift; sourceTree = "<group>"; };
@ -1756,6 +1760,7 @@
children = (
2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */,
2DAC9E3D262FC2400062E1A6 /* SuggestionAccountViewModel.swift */,
DBB45B6127B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift */,
2D4AD89A2631659400613EFC /* CollectionViewCell */,
2DAC9E43262FC9DE0062E1A6 /* TableViewCell */,
);
@ -2012,6 +2017,7 @@
isa = PBXGroup;
children = (
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */,
);
path = RecommandAccount;
sourceTree = "<group>";
@ -3845,6 +3851,7 @@
DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */,
DBBC24DE26A54BCB00398BB9 /* MastodonMetricFormatter.swift in Sources */,
DB06180A2785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift in Sources */,
DBB45B6227B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift in Sources */,
DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */,
DB6746ED278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift in Sources */,
DB0618032785A7100030EE79 /* RegisterSection.swift in Sources */,
@ -4068,6 +4075,7 @@
DB697DD1278F4871004EF2F7 /* AutoGenerateTableViewDelegate.swift in Sources */,
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */,
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
DBB45B6027B50A4F002DC5A7 /* RecommendAccountItem.swift in Sources */,
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
DB6180E626391B550018D199 /* MediaPreviewTransitionController.swift in Sources */,
DB0FCB922796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift in Sources */,

View File

@ -7,32 +7,9 @@
import CoreData
import Foundation
import CoreDataStack
enum SelectedAccountItem {
case accountObjectID(accountObjectID: NSManagedObjectID)
enum SelectedAccountItem: Hashable {
case account(ManagedObjectRecord<MastodonUser>)
case placeHolder(uuid: UUID)
}
extension SelectedAccountItem: Equatable {
static func == (lhs: SelectedAccountItem, rhs: SelectedAccountItem) -> Bool {
switch (lhs, rhs) {
case (.accountObjectID(let idLeft), .accountObjectID(let idRight)):
return idLeft == idRight
case (.placeHolder(let uuidLeft), .placeHolder(let uuidRight)):
return uuidLeft == uuidRight
default:
return false
}
}
}
extension SelectedAccountItem: Hashable {
func hash(into hasher: inout Hasher) {
switch self {
case .accountObjectID(let id):
hasher.combine(id)
case .placeHolder(let id):
hasher.combine(id.uuidString)
}
}
}

View File

@ -17,15 +17,17 @@ enum SelectedAccountSection: Equatable, Hashable {
extension SelectedAccountSection {
static func collectionViewDiffableDataSource(
for collectionView: UICollectionView,
managedObjectContext: NSManagedObjectContext
collectionView: UICollectionView,
context: AppContext
) -> UICollectionViewDiffableDataSource<SelectedAccountSection, SelectedAccountItem> {
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SuggestionAccountCollectionViewCell.self), for: indexPath) as! SuggestionAccountCollectionViewCell
switch item {
case .accountObjectID(let objectID):
let user = managedObjectContext.object(with: objectID) as! MastodonUser
cell.config(with: user)
case .account(let record):
context.managedObjectContext.performAndWait {
guard let user = record.object(in: context.managedObjectContext) else { return }
cell.config(with: user)
}
case .placeHolder:
cell.configAsPlaceHolder()
}

View File

@ -20,15 +20,15 @@ final class UserFetchedResultsController: NSObject {
let fetchedResultsController: NSFetchedResultsController<MastodonUser>
// input
let domain = CurrentValueSubject<String?, Never>(nil)
let userIDs = CurrentValueSubject<[Mastodon.Entity.Account.ID], Never>([])
@Published var domain: String? = nil
@Published var userIDs: [Mastodon.Entity.Account.ID] = []
// output
let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
@Published var records: [ManagedObjectRecord<MastodonUser>] = []
init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) {
self.domain.value = domain ?? ""
self.domain = domain ?? ""
self.fetchedResultsController = {
let fetchRequest = MastodonUser.sortedFetchRequest
fetchRequest.predicate = MastodonUser.predicate(domain: domain ?? "", ids: [])
@ -54,8 +54,8 @@ final class UserFetchedResultsController: NSObject {
fetchedResultsController.delegate = self
Publishers.CombineLatest(
self.domain.removeDuplicates(),
self.userIDs.removeDuplicates()
self.$domain.removeDuplicates(),
self.$userIDs.removeDuplicates()
)
.receive(on: DispatchQueue.main)
.sink { [weak self] domain, ids in
@ -79,11 +79,11 @@ final class UserFetchedResultsController: NSObject {
extension UserFetchedResultsController {
public func append(userIDs: [Mastodon.Entity.Account.ID]) {
var result = self.userIDs.value
var result = self.userIDs
for userID in userIDs where !result.contains(userID) {
result.append(userID)
}
self.userIDs.value = result
self.userIDs = result
}
}
@ -93,7 +93,7 @@ extension UserFetchedResultsController: NSFetchedResultsControllerDelegate {
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
let indexes = userIDs.value
let indexes = userIDs
let objects = fetchedResultsController.fetchedObjects ?? []
let items: [NSManagedObjectID] = objects

View File

@ -0,0 +1,13 @@
//
// RecommendAccountItem.swift
// Mastodon
//
// Created by MainasuK on 2022-2-10.
//
import Foundation
import CoreDataStack
enum RecommendAccountItem: Hashable {
case account(ManagedObjectRecord<MastodonUser>)
}

View File

@ -129,22 +129,29 @@ enum RecommendAccountSection: Equatable, Hashable {
//
//}
//
//extension RecommendAccountSection {
//
// static func tableViewDiffableDataSource(
// for tableView: UITableView,
// managedObjectContext: NSManagedObjectContext,
// viewModel: SuggestionAccountViewModel,
// delegate: SuggestionAccountTableViewCellDelegate
// ) -> UITableViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID> {
// UITableViewDiffableDataSource(tableView: tableView) { [weak viewModel, weak delegate] (tableView, indexPath, objectID) -> UITableViewCell? in
// guard let viewModel = viewModel else { return nil }
// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SuggestionAccountTableViewCell.self)) as! SuggestionAccountTableViewCell
// let user = managedObjectContext.object(with: objectID) as! MastodonUser
// let isSelected = viewModel.selectedAccounts.value.contains(objectID)
// cell.delegate = delegate
// cell.config(with: user, isSelected: isSelected)
// return cell
// }
// }
//}
extension RecommendAccountSection {
struct Configuration {
weak var suggestionAccountTableViewCellDelegate: SuggestionAccountTableViewCellDelegate?
}
static func tableViewDiffableDataSource(
tableView: UITableView,
context: AppContext,
configuration: Configuration
) -> UITableViewDiffableDataSource<RecommendAccountSection, RecommendAccountItem> {
UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SuggestionAccountTableViewCell.self)) as! SuggestionAccountTableViewCell
switch item {
case .account(let record):
context.managedObjectContext.performAndWait {
guard let user = record.object(in: context.managedObjectContext) else { return }
cell.config(with: user)
}
}
cell.delegate = configuration.suggestionAccountTableViewCellDelegate
return cell
}
}
}

View File

@ -74,6 +74,15 @@ extension HomeTimelineViewController {
guard let self = self else { return }
self.showThreadAction(action)
},
UIAction(title: "Account Recommend", image: UIImage(systemName: "human"), attributes: []) { [weak self] action in
guard let self = self else { return }
let suggestionAccountViewModel = SuggestionAccountViewModel(context: self.context)
self.coordinator.present(
scene: .suggestionAccount(viewModel: suggestionAccountViewModel),
from: self,
transition: .modal(animated: true, completion: nil)
)
},
UIAction(title: "Store Rating", image: UIImage(systemName: "star.fill"), attributes: []) { [weak self] action in
guard let self = self else { return }
guard let windowScene = self.view.window?.windowScene else { return }

View File

@ -383,10 +383,13 @@ extension HomeTimelineViewController {
extension HomeTimelineViewController {
@objc private func findPeopleButtonPressed(_ sender: PrimaryActionButton) {
// TODO:
// let viewModel = SuggestionAccountViewModel(context: context)
// viewModel.delegate = self.viewModel
// coordinator.present(scene: .suggestionAccount(viewModel: viewModel), from: self, transition: .modal(animated: true, completion: nil))
let suggestionAccountViewModel = SuggestionAccountViewModel(context: context)
suggestionAccountViewModel.delegate = viewModel
coordinator.present(
scene: .suggestionAccount(viewModel: suggestionAccountViewModel),
from: self,
transition: .modal(animated: true, completion: nil)
)
}
@objc private func manuallySearchButtonPressed(_ sender: UIButton) {

View File

@ -119,8 +119,6 @@ final class HomeTimelineViewModel: NSObject {
}
//extension HomeTimelineViewModel: SuggestionAccountViewModelDelegate { }
extension HomeTimelineViewModel {
struct ScrollPositionRecord {
let item: StatusItem
@ -197,3 +195,9 @@ extension HomeTimelineViewModel {
}
}
// MARK: - SuggestionAccountViewModelDelegate
extension HomeTimelineViewModel: SuggestionAccountViewModelDelegate {
}

View File

@ -72,7 +72,7 @@ extension FollowerListViewModel.State {
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
// reset
viewModel.userFetchedResultsController.userIDs.value = []
viewModel.userFetchedResultsController.userIDs = []
stateMachine.enter(Loading.self)
}
@ -158,7 +158,7 @@ extension FollowerListViewModel.State {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch \(response.value.count) followers")
var hasNewAppend = false
var userIDs = viewModel.userFetchedResultsController.userIDs.value
var userIDs = viewModel.userFetchedResultsController.userIDs
for user in response.value {
guard !userIDs.contains(user.id) else { continue }
userIDs.append(user.id)
@ -174,7 +174,7 @@ extension FollowerListViewModel.State {
}
self.maxID = maxID
viewModel.userFetchedResultsController.userIDs.value = userIDs
viewModel.userFetchedResultsController.userIDs = userIDs
} catch {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch follower fail: \(error.localizedDescription)")

View File

@ -72,7 +72,7 @@ extension FollowingListViewModel.State {
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
// reset
viewModel.userFetchedResultsController.userIDs.value = []
viewModel.userFetchedResultsController.userIDs = []
stateMachine.enter(Loading.self)
}
@ -159,7 +159,7 @@ extension FollowingListViewModel.State {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch \(response.value.count)")
var hasNewAppend = false
var userIDs = viewModel.userFetchedResultsController.userIDs.value
var userIDs = viewModel.userFetchedResultsController.userIDs
for user in response.value {
guard !userIDs.contains(user.id) else { continue }
userIDs.append(user.id)
@ -174,7 +174,7 @@ extension FollowingListViewModel.State {
await enter(state: NoMore.self)
}
self.maxID = maxID
viewModel.userFetchedResultsController.userIDs.value = userIDs
viewModel.userFetchedResultsController.userIDs = userIDs
} catch {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch following fail: \(error.localizedDescription)")

View File

@ -20,12 +20,12 @@ final class MeProfileViewModel: ProfileViewModel {
optionalMastodonUser: context.authenticationService.activeMastodonAuthentication.value?.user
)
self.currentMastodonUser
.sink { [weak self] currentMastodonUser in
os_log("%{public}s[%{public}ld], %{public}s: current active mastodon user: %s", ((#file as NSString).lastPathComponent), #line, #function, currentMastodonUser?.username ?? "<nil>")
$me
.sink { [weak self] me in
os_log("%{public}s[%{public}ld], %{public}s: current active mastodon user: %s", ((#file as NSString).lastPathComponent), #line, #function, me?.username ?? "<nil>")
guard let self = self else { return }
self.mastodonUser.value = currentMastodonUser
self.user = me
}
.store(in: &disposeBag)
}

View File

@ -577,7 +577,7 @@ extension ProfileViewController {
private func bindProfileRelationship() {
Publishers.CombineLatest(
viewModel.mastodonUser,
viewModel.$user,
viewModel.relationshipActionOptionSet
)
.asyncMap { [weak self] user, relationshipSet -> UIMenu? in
@ -725,7 +725,7 @@ extension ProfileViewController {
@objc private func shareBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
guard let user = viewModel.mastodonUser.value else { return }
guard let user = viewModel.user else { return }
let record: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
Task {
let _activityViewController = try await DataSourceFacade.createActivityViewController(
@ -754,7 +754,7 @@ extension ProfileViewController {
@objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let mastodonUser = viewModel.mastodonUser.value else { return }
guard let mastodonUser = viewModel.user else { return }
let composeViewModel = ComposeViewModel(
context: context,
composeKind: .mention(user: .init(objectID: mastodonUser.objectID)),
@ -849,7 +849,7 @@ extension ProfileViewController: ProfilePagingViewControllerDelegate {
// MARK: - ProfileHeaderViewDelegate
extension ProfileViewController: ProfileHeaderViewDelegate {
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarButtonDidPressed button: AvatarButton) {
guard let user = viewModel.mastodonUser.value else { return }
guard let user = viewModel.user else { return }
let record: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
Task {
@ -865,7 +865,7 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
}
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, bannerImageViewDidPressed imageView: UIImageView) {
guard let user = viewModel.mastodonUser.value else { return }
guard let user = viewModel.user else { return }
let record: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
Task {
@ -956,7 +956,7 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
case .none:
break
case .follow, .request, .pending, .following:
guard let user = viewModel.mastodonUser.value else { return }
guard let user = viewModel.user else { return }
let reocrd = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
Task {
@ -968,7 +968,7 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
}
case .muting:
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let user = viewModel.mastodonUser.value else { return }
guard let user = viewModel.user else { return }
let name = user.displayNameWithFallback
let alertController = UIAlertController(
@ -993,7 +993,7 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
present(alertController, animated: true, completion: nil)
case .blocking:
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let user = viewModel.mastodonUser.value else { return }
guard let user = viewModel.user else { return }
let name = user.displayNameWithFallback
let alertController = UIAlertController(
@ -1077,7 +1077,7 @@ extension ProfileViewController: ProfileAboutViewControllerDelegate {
extension ProfileViewController: MastodonMenuDelegate {
func menuAction(_ action: MastodonMenu.Action) {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let user = viewModel.mastodonUser.value else { return }
guard let user = viewModel.user else { return }
let userRecord: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)

View File

@ -28,8 +28,8 @@ class ProfileViewModel: NSObject {
// input
let context: AppContext
let mastodonUser: CurrentValueSubject<MastodonUser?, Never>
let currentMastodonUser = CurrentValueSubject<MastodonUser?, Never>(nil)
@Published var me: MastodonUser?
@Published var user: MastodonUser?
let viewDidAppear = PassthroughSubject<Void, Never>()
// output
@ -73,7 +73,7 @@ class ProfileViewModel: NSObject {
init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) {
self.context = context
self.mastodonUser = CurrentValueSubject(mastodonUser)
self.user = mastodonUser
self.domain = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value?.domain)
self.userID = CurrentValueSubject(mastodonUser?.id)
self.bannerImageURL = CurrentValueSubject(mastodonUser?.headerImageURL())
@ -98,21 +98,21 @@ class ProfileViewModel: NSObject {
.store(in: &disposeBag)
// bind active authentication
context.authenticationService.activeMastodonAuthentication
.sink { [weak self] activeMastodonAuthentication in
context.authenticationService.activeMastodonAuthenticationBox
.sink { [weak self] authenticationBox in
guard let self = self else { return }
guard let activeMastodonAuthentication = activeMastodonAuthentication else {
guard let authenticationBox = authenticationBox else {
self.domain.value = nil
self.currentMastodonUser.value = nil
self.me = nil
return
}
self.domain.value = activeMastodonAuthentication.domain
self.currentMastodonUser.value = activeMastodonAuthentication.user
self.domain.value = authenticationBox.domain
self.me = authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
}
.store(in: &disposeBag)
// query relationship
let userRecord = self.mastodonUser.map { user -> ManagedObjectRecord<MastodonUser>? in
let userRecord = $user.map { user -> ManagedObjectRecord<MastodonUser>? in
user.flatMap { ManagedObjectRecord<MastodonUser>(objectID: $0.objectID) }
}
let pendingRetryPublisher = CurrentValueSubject<TimeInterval, Never>(1)
@ -176,18 +176,18 @@ class ProfileViewModel: NSObject {
extension ProfileViewModel {
private func setup() {
Publishers.CombineLatest(
mastodonUser.eraseToAnyPublisher(),
currentMastodonUser.eraseToAnyPublisher()
$user,
$me
)
.receive(on: DispatchQueue.main)
.sink { [weak self] mastodonUser, currentMastodonUser in
.sink { [weak self] user, me in
guard let self = self else { return }
// Update view model attribute
self.update(mastodonUser: mastodonUser)
self.update(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
self.update(mastodonUser: user)
self.update(mastodonUser: user, currentMastodonUser: me)
// Setup observer for user
if let mastodonUser = mastodonUser {
if let mastodonUser = user {
// setup observer
self.mastodonUserObserver = ManagedObjectObserver.observe(object: mastodonUser)
.sink { completion in
@ -203,7 +203,7 @@ extension ProfileViewModel {
switch changeType {
case .update:
self.update(mastodonUser: mastodonUser)
self.update(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
self.update(mastodonUser: mastodonUser, currentMastodonUser: me)
case .delete:
// TODO:
break
@ -215,7 +215,7 @@ extension ProfileViewModel {
}
// Setup observer for user
if let currentMastodonUser = currentMastodonUser {
if let currentMastodonUser = me {
// setup observer
self.currentMastodonUserObserver = ManagedObjectObserver.observe(object: currentMastodonUser)
.sink { completion in
@ -230,7 +230,7 @@ extension ProfileViewModel {
guard let changeType = change.changeType else { return }
switch changeType {
case .update:
self.update(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
self.update(mastodonUser: user, currentMastodonUser: currentMastodonUser)
case .delete:
// TODO:
break
@ -347,13 +347,14 @@ extension ProfileViewModel {
// fetch profile info before edit
func fetchEditProfileInfo() -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
guard let currentMastodonUser = currentMastodonUser.value,
let mastodonAuthentication = currentMastodonUser.mastodonAuthentication else {
guard let me = me,
let mastodonAuthentication = me.mastodonAuthentication
else {
return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher()
}
let authorization = Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken)
return context.apiService.accountVerifyCredentials(domain: currentMastodonUser.domain, authorization: authorization)
return context.apiService.accountVerifyCredentials(domain: me.domain, authorization: authorization)
}
private func updateRelationship(

View File

@ -48,7 +48,7 @@ final class RemoteProfileViewModel: ProfileViewModel {
assertionFailure()
return
}
self.mastodonUser.value = mastodonUser
self.user = mastodonUser
}
.store(in: &disposeBag)
}

View File

@ -153,7 +153,7 @@ extension SearchDetailViewController {
assertionFailure()
break
case .people:
viewController.viewModel.userFetchedResultsController.userIDs.value = allSearchScopeViewController.viewModel.userFetchedResultsController.userIDs.value
viewController.viewModel.userFetchedResultsController.userIDs = allSearchScopeViewController.viewModel.userFetchedResultsController.userIDs
case .hashtags:
viewController.viewModel.hashtags = allSearchScopeViewController.viewModel.hashtags
case .posts:

View File

@ -155,7 +155,7 @@ extension SearchResultViewModel.State {
// reset data source when the search is refresh
if offset == nil {
viewModel.userFetchedResultsController.userIDs.value = []
viewModel.userFetchedResultsController.userIDs = []
viewModel.statusFetchedResultsController.statusIDs.value = []
viewModel.hashtags = []
}

View File

@ -63,7 +63,7 @@ final class SearchResultViewModel {
context.authenticationService.activeMastodonAuthenticationBox
.map { $0?.domain }
.assign(to: \.value, on: userFetchedResultsController.domain)
.assign(to: \.domain, on: userFetchedResultsController)
.store(in: &disposeBag)
context.authenticationService.activeMastodonAuthenticationBox

View File

@ -15,12 +15,43 @@ import MastodonAsset
import MastodonLocalization
class SuggestionAccountViewController: UIViewController, NeedsDependency {
static let collectionViewHeight: CGFloat = 24 + 64 + 24
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>()
var viewModel: SuggestionAccountViewModel!
private static func createCollectionViewLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(64), heightDimension: .absolute(64))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 24, leading: 0, bottom: 24, trailing: 0)
section.orthogonalScrollingBehavior = .continuous
section.contentInsetsReference = .readableContent
section.interGroupSpacing = 16
return UICollectionViewCompositionalLayout(section: section)
}
let collectionView: UICollectionView = {
let collectionViewLayout = SuggestionAccountViewController.createCollectionViewLayout()
let view = ControlContainableCollectionView(
frame: .zero,
collectionViewLayout: collectionViewLayout
)
view.register(SuggestionAccountCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SuggestionAccountCollectionViewCell.self))
view.backgroundColor = .clear
view.showsHorizontalScrollIndicator = false
view.showsVerticalScrollIndicator = false
view.layer.masksToBounds = false
return view
}()
let tableView: UITableView = {
let tableView = ControlContainableTableView()
@ -32,34 +63,6 @@ class SuggestionAccountViewController: UIViewController, NeedsDependency {
return tableView
}()
lazy var tableHeader: UIView = {
let view = UIView()
view.backgroundColor = ThemeService.shared.currentTheme.value.systemGroupedBackgroundColor
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
label.textColor = Asset.Colors.Label.primary.color
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
label.numberOfLines = 0
return label
}()
let selectedCollectionView: UICollectionView = {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .horizontal
let view = ControlContainableCollectionView(frame: .zero, collectionViewLayout: flowLayout)
view.register(SuggestionAccountCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SuggestionAccountCollectionViewCell.self))
view.backgroundColor = .clear
view.showsHorizontalScrollIndicator = false
view.showsVerticalScrollIndicator = false
view.layer.masksToBounds = false
return view
}()
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", (#file as NSString).lastPathComponent, #line, #function)
}
@ -68,164 +71,135 @@ class SuggestionAccountViewController: UIViewController, NeedsDependency {
extension SuggestionAccountViewController {
override func viewDidLoad() {
super.viewDidLoad()
fatalError()
// setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
// ThemeService.shared.currentTheme
// .receive(on: RunLoop.main)
// .sink { [weak self] theme in
// guard let self = self else { return }
// self.setupBackgroundColor(theme: theme)
// }
// .store(in: &disposeBag)
//
// title = L10n.Scene.SuggestionAccount.title
// navigationItem.rightBarButtonItem
// = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.done,
// target: self,
// action: #selector(SuggestionAccountViewController.doneButtonDidClick(_:)))
//
// tableView.delegate = self
// tableView.translatesAutoresizingMaskIntoConstraints = false
// view.addSubview(tableView)
// NSLayoutConstraint.activate([
// tableView.topAnchor.constraint(equalTo: view.topAnchor),
// tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
// tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
// ])
// viewModel.diffableDataSource = RecommendAccountSection.tableViewDiffableDataSource(
// for: tableView,
// managedObjectContext: context.managedObjectContext,
// viewModel: viewModel,
// delegate: self
// )
//
// viewModel.collectionDiffableDataSource = SelectedAccountSection.collectionViewDiffableDataSource(for: selectedCollectionView, managedObjectContext: context.managedObjectContext)
//
// viewModel.accounts
// .receive(on: DispatchQueue.main)
// .sink { [weak self] accounts in
// guard let self = self else { return }
// self.setupHeader(accounts: accounts)
// }
// .store(in: &disposeBag)
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: RunLoop.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.setupBackgroundColor(theme: theme)
}
.store(in: &disposeBag)
title = L10n.Scene.SuggestionAccount.title
navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: UIBarButtonItem.SystemItem.done,
target: self,
action: #selector(SuggestionAccountViewController.doneButtonDidClick(_:))
)
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.heightAnchor.constraint(equalToConstant: SuggestionAccountViewController.collectionViewHeight),
])
defer { view.bringSubviewToFront(collectionView) }
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: collectionView.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
collectionView.delegate = self
viewModel.setupDiffableDataSource(
collectionView: collectionView
)
tableView.delegate = self
viewModel.setupDiffableDataSource(
tableView: tableView,
suggestionAccountTableViewCellDelegate: self
)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.deselectRow(with: transitionCoordinator, animated: animated)
viewModel.checkAccountsFollowState()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let avatarImageViewHeight: Double = 56
let avatarImageViewCount = Int(floor((Double(view.frame.width) - 20) / (avatarImageViewHeight + 15)))
viewModel.headerPlaceholderCount.value = avatarImageViewCount
}
func setupHeader(accounts: [NSManagedObjectID]) {
if accounts.isEmpty {
return
}
followExplainLabel.translatesAutoresizingMaskIntoConstraints = false
tableHeader.addSubview(followExplainLabel)
NSLayoutConstraint.activate([
followExplainLabel.topAnchor.constraint(equalTo: tableHeader.topAnchor, constant: 20),
followExplainLabel.leadingAnchor.constraint(equalTo: tableHeader.leadingAnchor, constant: 20),
tableHeader.trailingAnchor.constraint(equalTo: followExplainLabel.trailingAnchor, constant: 20),
])
selectedCollectionView.translatesAutoresizingMaskIntoConstraints = false
tableHeader.addSubview(selectedCollectionView)
NSLayoutConstraint.activate([
selectedCollectionView.frameLayoutGuide.topAnchor.constraint(equalTo: followExplainLabel.topAnchor, constant: 20),
selectedCollectionView.frameLayoutGuide.leadingAnchor.constraint(equalTo: tableHeader.leadingAnchor, constant: 20),
selectedCollectionView.frameLayoutGuide.trailingAnchor.constraint(equalTo: tableHeader.trailingAnchor),
selectedCollectionView.frameLayoutGuide.bottomAnchor.constraint(equalTo: tableHeader.bottomAnchor),
])
selectedCollectionView.delegate = self
tableView.tableHeaderView = tableHeader
}
private func setupBackgroundColor(theme: Theme) {
view.backgroundColor = theme.systemBackgroundColor
tableHeader.backgroundColor = theme.systemGroupedBackgroundColor
collectionView.backgroundColor = theme.systemGroupedBackgroundColor
}
}
extension SuggestionAccountViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
15
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
CGSize(width: 56, height: 56)
}
// MARK: - UICollectionViewDelegateFlowLayout
extension SuggestionAccountViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let diffableDataSource = viewModel.collectionDiffableDataSource else { return }
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
switch item {
case .accountObjectID(let accountObjectID):
let mastodonUser = context.managedObjectContext.object(with: accountObjectID) as! MastodonUser
let viewModel = ProfileViewModel(context: context, optionalMastodonUser: mastodonUser)
DispatchQueue.main.async {
self.coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show)
}
default:
break
}
// guard let diffableDataSource = viewModel.collectionDiffableDataSource else { return }
// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
// switch item {
// case .accountObjectID(let accountObjectID):
// let mastodonUser = context.managedObjectContext.object(with: accountObjectID) as! MastodonUser
// let viewModel = ProfileViewModel(context: context, optionalMastodonUser: mastodonUser)
// DispatchQueue.main.async {
// self.coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show)
// }
// default:
// break
// }
}
}
// MARK: - UITableViewDelegate
extension SuggestionAccountViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let diffableDataSource = viewModel.diffableDataSource else { return }
guard let objectID = diffableDataSource.itemIdentifier(for: indexPath) else { return }
let mastodonUser = context.managedObjectContext.object(with: objectID) as! MastodonUser
let viewModel = ProfileViewModel(context: context, optionalMastodonUser: mastodonUser)
DispatchQueue.main.async {
self.coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show)
guard let tableViewDiffableDataSource = viewModel.tableViewDiffableDataSource else { return }
guard let item = tableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return }
switch item {
case .account(let record):
guard let account = record.object(in: context.managedObjectContext) else { return }
let cachedProfileViewModel = CachedProfileViewModel(context: context, mastodonUser: account)
coordinator.present(
scene: .profile(viewModel: cachedProfileViewModel),
from: self,
transition: .show
)
}
}
}
extension SuggestionAccountViewController: SuggestionAccountTableViewCellDelegate {
func accountButtonPressed(objectID: NSManagedObjectID, cell: SuggestionAccountTableViewCell) {
let selected = !viewModel.selectedAccounts.value.contains(objectID)
cell.startAnimating()
viewModel.followAction(objectID: objectID)?
.sink(receiveCompletion: { [weak self] completion in
guard let self = self else { return }
cell.stopAnimating()
switch completion {
case .failure(let error):
os_log("%{public}s[%{public}ld], %{public}s: follow failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
case .finished:
var selectedAccounts = self.viewModel.selectedAccounts.value
if selected {
selectedAccounts.append(objectID)
} else {
selectedAccounts.removeAll { $0 == objectID }
}
cell.button.isSelected = selected
self.viewModel.selectedAccounts.value = selectedAccounts
}
}, receiveValue: { _ in
})
.store(in: &disposeBag)
// let selected = !viewModel.selectedAccounts.value.contains(objectID)
// cell.startAnimating()
// viewModel.followAction(objectID: objectID)?
// .sink(receiveCompletion: { [weak self] completion in
// guard let self = self else { return }
// cell.stopAnimating()
// switch completion {
// case .failure(let error):
// os_log("%{public}s[%{public}ld], %{public}s: follow failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
// case .finished:
// var selectedAccounts = self.viewModel.selectedAccounts.value
// if selected {
// selectedAccounts.append(objectID)
// } else {
// selectedAccounts.removeAll { $0 == objectID }
// }
// cell.button.isSelected = selected
// self.viewModel.selectedAccounts.value = selectedAccounts
// }
// }, receiveValue: { _ in
// })
// .store(in: &disposeBag)
}
}
extension SuggestionAccountViewController {
@objc func doneButtonDidClick(_ sender: UIButton) {
dismiss(animated: true, completion: nil)
if viewModel.selectedAccounts.value.count > 0 {
viewModel.delegate?.homeTimelineNeedRefresh.send()
}
// if viewModel.selectedAccounts.value.count > 0 {
// viewModel.delegate?.homeTimelineNeedRefresh.send()
// }
}
}

View File

@ -0,0 +1,74 @@
//
// SuggestionAccountViewModel+Diffable.swift
// Mastodon
//
// Created by MainasuK on 2022-2-10.
//
import UIKit
extension SuggestionAccountViewModel {
func setupDiffableDataSource(
tableView: UITableView,
suggestionAccountTableViewCellDelegate: SuggestionAccountTableViewCellDelegate
) {
tableViewDiffableDataSource = RecommendAccountSection.tableViewDiffableDataSource(
tableView: tableView,
context: context,
configuration: RecommendAccountSection.Configuration(
suggestionAccountTableViewCellDelegate: suggestionAccountTableViewCellDelegate
)
)
userFetchedResultsController.$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }
guard let tableViewDiffableDataSource = self.tableViewDiffableDataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot<RecommendAccountSection, RecommendAccountItem>()
snapshot.appendSections([.main])
let items: [RecommendAccountItem] = records.map { RecommendAccountItem.account($0) }
snapshot.appendItems(items, toSection: .main)
if #available(iOS 15.0, *) {
tableViewDiffableDataSource.applySnapshotUsingReloadData(snapshot, completion: nil)
} else {
// Fallback on earlier versions
tableViewDiffableDataSource.applySnapshot(snapshot, animated: false, completion: nil)
}
}
.store(in: &disposeBag)
}
func setupDiffableDataSource(
collectionView: UICollectionView
) {
collectionViewDiffableDataSource = SelectedAccountSection.collectionViewDiffableDataSource(
collectionView: collectionView,
context: context
)
userFetchedResultsController.$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }
guard let collectionViewDiffableDataSource = self.collectionViewDiffableDataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot<SelectedAccountSection, SelectedAccountItem>()
snapshot.appendSections([.main])
let items: [SelectedAccountItem] = records.map { SelectedAccountItem.account($0) }
snapshot.appendItems(items, toSection: .main)
if #available(iOS 15.0, *) {
collectionViewDiffableDataSource.applySnapshotUsingReloadData(snapshot, completion: nil)
} else {
// Fallback on earlier versions
collectionViewDiffableDataSource.applySnapshot(snapshot, animated: false, completion: nil)
}
}
.store(in: &disposeBag)
}
}

View File

@ -20,177 +20,175 @@ protocol SuggestionAccountViewModelDelegate: AnyObject {
final class SuggestionAccountViewModel: NSObject {
var disposeBag = Set<AnyCancellable>()
weak var delegate: SuggestionAccountViewModelDelegate?
// input
let context: AppContext
let userFetchedResultsController: UserFetchedResultsController
let currentMastodonUser = CurrentValueSubject<MastodonUser?, Never>(nil)
weak var delegate: SuggestionAccountViewModelDelegate?
// output
let accounts = CurrentValueSubject<[NSManagedObjectID], Never>([])
var selectedAccounts = CurrentValueSubject<[NSManagedObjectID], Never>([])
var viewWillAppear = PassthroughSubject<Void, Never>()
// output
var collectionViewDiffableDataSource: UICollectionViewDiffableDataSource<SelectedAccountSection, SelectedAccountItem>?
var tableViewDiffableDataSource: UITableViewDiffableDataSource<RecommendAccountSection, RecommendAccountItem>?
@Published var selectedAccounts: [ManagedObjectRecord<MastodonUser>] = []
var headerPlaceholderCount = CurrentValueSubject<Int?, Never>(nil)
var suggestionAccountsFallback = PassthroughSubject<Void, Never>()
var viewWillAppear = PassthroughSubject<Void, Never>()
var diffableDataSource: UITableViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID>? {
didSet(value) {
if !accounts.value.isEmpty {
applyTableViewDataSource(accounts: accounts.value)
}
}
}
var collectionDiffableDataSource: UICollectionViewDiffableDataSource<SelectedAccountSection, SelectedAccountItem>?
init(context: AppContext, accounts: [NSManagedObjectID]? = nil) {
init(
context: AppContext
) {
self.context = context
self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: nil,
additionalTweetPredicate: nil
)
super.init()
Publishers.CombineLatest(
self.accounts,
self.selectedAccounts
)
.receive(on: RunLoop.main)
.sink { [weak self] accounts,selectedAccounts in
self?.applyTableViewDataSource(accounts: accounts)
self?.applySelectedCollectionViewDataSource(accounts: selectedAccounts)
}
.store(in: &disposeBag)
// Publishers.CombineLatest(
// $accounts,
// $selectedAccounts
// )
// .receive(on: RunLoop.main)
// .sink { [weak self] accounts,selectedAccounts in
// self?.applyTableViewDataSource(accounts: accounts)
// self?.applySelectedCollectionViewDataSource(accounts: selectedAccounts)
// }
// .store(in: &disposeBag)
Publishers.CombineLatest(
self.selectedAccounts,
self.headerPlaceholderCount
)
.receive(on: RunLoop.main)
.sink { [weak self] selectedAccount,count in
self?.applySelectedCollectionViewDataSource(accounts: selectedAccount)
}
.store(in: &disposeBag)
// Publishers.CombineLatest(
// self.selectedAccounts,
// self.headerPlaceholderCount
// )
// .receive(on: RunLoop.main)
// .sink { [weak self] selectedAccount,count in
// self?.applySelectedCollectionViewDataSource(accounts: selectedAccount)
// }
// .store(in: &disposeBag)
//
// viewWillAppear
// .sink { [weak self] _ in
// self?.checkAccountsFollowState()
// }
// .store(in: &disposeBag)
//
// context.authenticationService.activeMastodonAuthentication
// .sink { [weak self] activeMastodonAuthentication in
// guard let self = self else { return }
// guard let activeMastodonAuthentication = activeMastodonAuthentication else {
// self.currentMastodonUser.value = nil
// return
// }
// self.currentMastodonUser.value = activeMastodonAuthentication.user
// }
// .store(in: &disposeBag)
viewWillAppear
.sink { [weak self] _ in
self?.checkAccountsFollowState()
}
.store(in: &disposeBag)
if let accounts = accounts {
self.accounts.value = accounts
}
context.authenticationService.activeMastodonAuthentication
.sink { [weak self] activeMastodonAuthentication in
guard let self = self else { return }
guard let activeMastodonAuthentication = activeMastodonAuthentication else {
self.currentMastodonUser.value = nil
return
}
self.currentMastodonUser.value = activeMastodonAuthentication.user
}
.store(in: &disposeBag)
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 { [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
let ids = response.value.map(\.account.id)
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 applyTableViewDataSource(accounts: [NSManagedObjectID]) {
assert(Thread.isMainThread)
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 applySelectedCollectionViewDataSource(accounts: [NSManagedObjectID]) {
assert(Thread.isMainThread)
guard let count = headerPlaceholderCount.value else { return }
guard let dataSource = collectionDiffableDataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot<SelectedAccountSection, SelectedAccountItem>()
snapshot.appendSections([.main])
let placeholderCount = count - accounts.count
let accountItems = accounts.map { SelectedAccountItem.accountObjectID(accountObjectID: $0) }
snapshot.appendItems(accountItems, toSection: .main)
if placeholderCount > 0 {
for _ in 0 ..< placeholderCount {
snapshot.appendItems([SelectedAccountItem.placeHolder(uuid: UUID())], toSection: .main)
}
}
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
}
func receiveAccounts(ids: [String]) {
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
guard let authenticationBox = 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
userFetchedResultsController.domain = authenticationBox.domain
Task {
var userIDs: [MastodonUser.ID] = []
do {
return try self.context.managedObjectContext.fetch(userFetchRequest)
let response = try await context.apiService.suggestionAccountV2(
query: nil,
authenticationBox: authenticationBox
)
userIDs = response.value.map { $0.account.id }
} catch let error as Mastodon.API.Error where error.httpResponseStatus == .notFound {
let response = try await context.apiService.suggestionAccount(
query: nil,
authenticationBox: authenticationBox
)
userIDs = response.value.map { $0.id }
} catch {
assertionFailure(error.localizedDescription)
return nil
os_log("%{public}s[%{public}ld], %{public}s: fetch recommendAccountV2 failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
}
}()
if let users = mastodonUsers {
let sortedUsers = users.sorted { (user1, user2) -> Bool in
(ids.firstIndex(of: user1.id) ?? 0) < (ids.firstIndex(of: user2.id) ?? 0)
}
accounts.value = sortedUsers.map(\.objectID)
guard !userIDs.isEmpty else { return }
userFetchedResultsController.userIDs = userIDs
}
// .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()
// }
// }
// case .finished:
// // handle isFetchingLatestTimeline in fetch controller delegate
// break
// }
// } receiveValue: { [weak self] response in
// let ids = response.value.map(\.account.id)
// self?.receiveAccounts(ids: ids)
// }
// .store(in: &disposeBag)
//
// suggestionAccountsFallback
// .sink(receiveValue: { [weak self] _ in
// self?.requestSuggestionAccount()
// })
// .store(in: &disposeBag)
}
// func applyTableViewDataSource(accounts: [NSManagedObjectID]) {
// assert(Thread.isMainThread)
// 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 applySelectedCollectionViewDataSource(accounts: [NSManagedObjectID]) {
// assert(Thread.isMainThread)
// guard let count = headerPlaceholderCount.value else { return }
// guard let dataSource = collectionDiffableDataSource else { return }
// var snapshot = NSDiffableDataSourceSnapshot<SelectedAccountSection, SelectedAccountItem>()
// snapshot.appendSections([.main])
// let placeholderCount = count - accounts.count
// let accountItems = accounts.map { SelectedAccountItem.accountObjectID(accountObjectID: $0) }
// snapshot.appendItems(accountItems, toSection: .main)
//
// if placeholderCount > 0 {
// for _ in 0 ..< placeholderCount {
// snapshot.appendItems([SelectedAccountItem.placeHolder(uuid: UUID())], toSection: .main)
// }
// }
// dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
// }
// func receiveAccounts(userIDs: [MastodonUser.ID]) {
// guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
// return
// }
// let request = MastodonUser.sortedFetchRequest
// request.predicate = MastodonUser.predicate(domain: activeMastodonAuthenticationBox.domain, ids: userIDs)
// 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 {
// let sortedUsers = users.sorted { (user1, user2) -> Bool in
// (ids.firstIndex(of: user1.id) ?? 0) < (ids.firstIndex(of: user2.id) ?? 0)
// }
// accounts.value = sortedUsers.map(\.objectID)
// }
// }
func followAction(objectID: NSManagedObjectID) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>? {
fatalError()
@ -203,8 +201,8 @@ final class SuggestionAccountViewModel: NSObject {
// )
}
func checkAccountsFollowState() {
fatalError()
// func checkAccountsFollowState() {
// fatalError()
// guard let currentMastodonUser = currentMastodonUser.value else {
// return
// }
@ -229,5 +227,5 @@ final class SuggestionAccountViewModel: NSObject {
// }.map(\.objectID)
//
// selectedAccounts.value = followingUsers
}
// }
}

View File

@ -141,7 +141,7 @@ extension SuggestionAccountTableViewCell {
])
}
func config(with account: MastodonUser, isSelected: Bool) {
func config(with account: MastodonUser) {
if let url = account.avatarImageURL() {
_imageView.af.setImage(
withURL: url,

View File

@ -14,68 +14,62 @@ import OSLog
extension APIService {
func suggestionAccount(
domain: String,
query: Mastodon.API.Suggestions.Query?,
mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> {
fatalError()
// let authorization = mastodonAuthenticationBox.userAuthorization
//
// return Mastodon.API.Suggestions.get(session: session, domain: domain, query: query, authorization: authorization)
// .flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> in
// let log = OSLog.api
// return self.backgroundManagedObjectContext.performChanges {
// response.value.forEach { user in
// let (mastodonUser,isCreated) = APIService.CoreData.createOrMergeMastodonUser(into: self.backgroundManagedObjectContext, for: nil, in: domain, entity: user, userCache: nil, networkDate: Date(), log: log)
// let flag = isCreated ? "+" : "-"
// os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: fetch mastodon user [%s](%s)%s", (#file as NSString).lastPathComponent, #line, #function, flag, mastodonUser.id, mastodonUser.username)
// }
// }
// .setFailureType(to: Error.self)
// .tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Account]> in
// switch result {
// case .success:
// return response
// case .failure(let error):
// throw error
// }
// }
// .eraseToAnyPublisher()
// }
// .eraseToAnyPublisher()
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> {
let response = try await Mastodon.API.Suggestions.get(
session: session,
domain: authenticationBox.domain,
query: query,
authorization: authenticationBox.userAuthorization
).singleOutput()
let managedObjectContext = backgroundManagedObjectContext
try await managedObjectContext.performChanges {
for entity in response.value {
_ = Persistence.MastodonUser.createOrMerge(
in: managedObjectContext,
context: Persistence.MastodonUser.PersistContext(
domain: authenticationBox.domain,
entity: entity,
cache: nil,
networkDate: response.networkDate
)
)
} // end for in
}
return response
}
func suggestionAccountV2(
domain: String,
query: Mastodon.API.Suggestions.Query?,
mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.V2.SuggestionAccount]>, Error> {
fatalError()
// let authorization = mastodonAuthenticationBox.userAuthorization
//
// return Mastodon.API.V2.Suggestions.get(session: session, domain: domain, query: query, authorization: authorization)
// .flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.V2.SuggestionAccount]>, Error> in
// let log = OSLog.api
// return self.backgroundManagedObjectContext.performChanges {
// response.value.forEach { suggestionAccount in
// let user = suggestionAccount.account
// let (mastodonUser,isCreated) = APIService.CoreData.createOrMergeMastodonUser(into: self.backgroundManagedObjectContext, for: nil, in: domain, entity: user, userCache: nil, networkDate: Date(), log: log)
// let flag = isCreated ? "+" : "-"
// os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: fetch mastodon user [%s](%s)%s", (#file as NSString).lastPathComponent, #line, #function, flag, mastodonUser.id, mastodonUser.username)
// }
// }
// .setFailureType(to: Error.self)
// .tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.V2.SuggestionAccount]> in
// switch result {
// case .success:
// return response
// case .failure(let error):
// throw error
// }
// }
// .eraseToAnyPublisher()
// }
// .eraseToAnyPublisher()
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.V2.SuggestionAccount]> {
let response = try await Mastodon.API.V2.Suggestions.get(
session: session,
domain: authenticationBox.domain,
query: query,
authorization: authenticationBox.userAuthorization
).singleOutput()
let managedObjectContext = backgroundManagedObjectContext
try await managedObjectContext.performChanges {
for entity in response.value {
_ = Persistence.MastodonUser.createOrMerge(
in: managedObjectContext,
context: Persistence.MastodonUser.PersistContext(
domain: authenticationBox.domain,
entity: entity.account,
cache: nil,
networkDate: response.networkDate
)
)
} // end for in
}
return response
}
}