feat: restore user recommend scene

This commit is contained in:
CMK 2022-02-16 17:25:55 +08:00
parent 6596827837
commit 7da3bbcaa7
15 changed files with 285 additions and 491 deletions

View File

@ -449,7 +449,8 @@
"accessibility": { "accessibility": {
"show_avatar_image": "Show avatar image", "show_avatar_image": "Show avatar image",
"edit_avatar_image": "Edit avatar image", "edit_avatar_image": "Edit avatar image",
"show_banner_image": "Show banner image" "show_banner_image": "Show banner image",
"double_tap_to_open_the_list": "Double tap to open the list"
} }
}, },
"follower": { "follower": {

View File

@ -546,6 +546,7 @@
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */; }; DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */; };
DBD376AC2692ECDB007FEC24 /* ThemePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD376AB2692ECDB007FEC24 /* ThemePreference.swift */; }; DBD376AC2692ECDB007FEC24 /* ThemePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD376AB2692ECDB007FEC24 /* ThemePreference.swift */; };
DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD376B1269302A4007FEC24 /* UITableViewCell.swift */; }; DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD376B1269302A4007FEC24 /* UITableViewCell.swift */; };
DBD5B1F627BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD5B1F527BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift */; };
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; }; DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; };
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; }; DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; }; DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; };
@ -1279,6 +1280,7 @@
DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Reblog.swift"; sourceTree = "<group>"; }; DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Reblog.swift"; sourceTree = "<group>"; };
DBD376AB2692ECDB007FEC24 /* ThemePreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemePreference.swift; sourceTree = "<group>"; }; DBD376AB2692ECDB007FEC24 /* ThemePreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemePreference.swift; sourceTree = "<group>"; };
DBD376B1269302A4007FEC24 /* UITableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITableViewCell.swift; sourceTree = "<group>"; }; DBD376B1269302A4007FEC24 /* UITableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITableViewCell.swift; sourceTree = "<group>"; };
DBD5B1F527BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountTableViewCell+ViewModel.swift"; sourceTree = "<group>"; };
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = "<group>"; }; DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = "<group>"; };
DBDC1CF9272C0FD600055C3D /* ku-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ku-TR"; path = "ku-TR.lproj/Intents.strings"; sourceTree = "<group>"; }; DBDC1CF9272C0FD600055C3D /* ku-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ku-TR"; path = "ku-TR.lproj/Intents.strings"; sourceTree = "<group>"; };
DBDC1CFC272C0FD600055C3D /* ku-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ku-TR"; path = "ku-TR.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; DBDC1CFC272C0FD600055C3D /* ku-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ku-TR"; path = "ku-TR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
@ -1768,6 +1770,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
2DAC9E45262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift */, 2DAC9E45262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift */,
DBD5B1F527BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift */,
); );
path = TableViewCell; path = TableViewCell;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3835,6 +3838,7 @@
DBB45B5E27B4EB22002DC5A7 /* AdaptiveMarginStatusTableViewCell.swift in Sources */, DBB45B5E27B4EB22002DC5A7 /* AdaptiveMarginStatusTableViewCell.swift in Sources */,
DB6180F226391CF40018D199 /* MediaPreviewImageViewModel.swift in Sources */, DB6180F226391CF40018D199 /* MediaPreviewImageViewModel.swift in Sources */,
DBA5E7A3263AD0A3004598BB /* PhotoLibraryService.swift in Sources */, DBA5E7A3263AD0A3004598BB /* PhotoLibraryService.swift in Sources */,
DBD5B1F627BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */,
5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */, 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */,
DB63F767279A5EB300455B82 /* NotificationTimelineViewModel.swift in Sources */, DB63F767279A5EB300455B82 /* NotificationTimelineViewModel.swift in Sources */,
2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */, 2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */,

View File

@ -7,7 +7,7 @@
<key>AppShared.xcscheme_^#shared#^_</key> <key>AppShared.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>28</integer> <integer>20</integer>
</dict> </dict>
<key>CoreDataStack.xcscheme_^#shared#^_</key> <key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -97,7 +97,7 @@
<key>MastodonIntent.xcscheme_^#shared#^_</key> <key>MastodonIntent.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>30</integer> <integer>19</integer>
</dict> </dict>
<key>MastodonIntents.xcscheme_^#shared#^_</key> <key>MastodonIntents.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -117,7 +117,7 @@
<key>ShareActionExtension.xcscheme_^#shared#^_</key> <key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>29</integer> <integer>18</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key> <key>SuppressBuildableAutocreation</key>

View File

@ -22,12 +22,17 @@ final class UserFetchedResultsController: NSObject {
// input // input
@Published var domain: String? = nil @Published var domain: String? = nil
@Published var userIDs: [Mastodon.Entity.Account.ID] = [] @Published var userIDs: [Mastodon.Entity.Account.ID] = []
@Published var additionalPredicate: NSPredicate?
// output // output
let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([]) let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
@Published var records: [ManagedObjectRecord<MastodonUser>] = [] @Published var records: [ManagedObjectRecord<MastodonUser>] = []
init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) { init(
managedObjectContext: NSManagedObjectContext,
domain: String?,
additionalPredicate: NSPredicate?
) {
self.domain = domain ?? "" self.domain = domain ?? ""
self.fetchedResultsController = { self.fetchedResultsController = {
let fetchRequest = MastodonUser.sortedFetchRequest let fetchRequest = MastodonUser.sortedFetchRequest
@ -43,6 +48,7 @@ final class UserFetchedResultsController: NSObject {
return controller return controller
}() }()
self.additionalPredicate = additionalPredicate
super.init() super.init()
// debounce output to prevent UI update issues // debounce output to prevent UI update issues
@ -53,15 +59,16 @@ final class UserFetchedResultsController: NSObject {
fetchedResultsController.delegate = self fetchedResultsController.delegate = self
Publishers.CombineLatest( Publishers.CombineLatest3(
self.$domain.removeDuplicates(), self.$domain.removeDuplicates(),
self.$userIDs.removeDuplicates() self.$userIDs.removeDuplicates(),
self.$additionalPredicate.removeDuplicates()
) )
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] domain, ids in .sink { [weak self] domain, ids, additionalPredicate in
guard let self = self else { return } guard let self = self else { return }
var predicates = [MastodonUser.predicate(domain: domain ?? "", ids: ids)] var predicates = [MastodonUser.predicate(domain: domain ?? "", ids: ids)]
if let additionalPredicate = additionalTweetPredicate { if let additionalPredicate = additionalPredicate {
predicates.append(additionalPredicate) predicates.append(additionalPredicate)
} }
self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)

View File

@ -146,10 +146,15 @@ extension RecommendAccountSection {
case .account(let record): case .account(let record):
context.managedObjectContext.performAndWait { context.managedObjectContext.performAndWait {
guard let user = record.object(in: context.managedObjectContext) else { return } guard let user = record.object(in: context.managedObjectContext) else { return }
cell.config(with: user) cell.configure(user: user)
} }
context.authenticationService.activeMastodonAuthenticationBox
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.viewModel)
.store(in: &cell.disposeBag)
cell.delegate = configuration.suggestionAccountTableViewCellDelegate
} }
cell.delegate = configuration.suggestionAccountTableViewCellDelegate
return cell return cell
} }
} }

View File

@ -44,7 +44,7 @@ final class FollowerListViewModel {
self.userFetchedResultsController = UserFetchedResultsController( self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext, managedObjectContext: context.managedObjectContext,
domain: domain, domain: domain,
additionalTweetPredicate: nil additionalPredicate: nil
) )
self.domain = CurrentValueSubject(domain) self.domain = CurrentValueSubject(domain)
self.userID = CurrentValueSubject(userID) self.userID = CurrentValueSubject(userID)

View File

@ -44,7 +44,7 @@ final class FollowingListViewModel {
self.userFetchedResultsController = UserFetchedResultsController( self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext, managedObjectContext: context.managedObjectContext,
domain: domain, domain: domain,
additionalTweetPredicate: nil additionalPredicate: nil
) )
self.domain = CurrentValueSubject(domain) self.domain = CurrentValueSubject(domain)
self.userID = CurrentValueSubject(userID) self.userID = CurrentValueSubject(userID)

View File

@ -53,7 +53,7 @@ final class SearchResultViewModel {
self.userFetchedResultsController = UserFetchedResultsController( self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext, managedObjectContext: context.managedObjectContext,
domain: nil, domain: nil,
additionalTweetPredicate: nil additionalPredicate: nil
) )
self.statusFetchedResultsController = StatusFetchedResultsController( self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext, managedObjectContext: context.managedObjectContext,

View File

@ -74,7 +74,7 @@ extension SuggestionAccountViewController {
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme ThemeService.shared.currentTheme
.receive(on: RunLoop.main) .receive(on: DispatchQueue.main)
.sink { [weak self] theme in .sink { [weak self] theme in
guard let self = self else { return } guard let self = self else { return }
self.setupBackgroundColor(theme: theme) self.setupBackgroundColor(theme: theme)
@ -169,29 +169,31 @@ extension SuggestionAccountViewController: UITableViewDelegate {
} }
extension SuggestionAccountViewController: SuggestionAccountTableViewCellDelegate { extension SuggestionAccountViewController: SuggestionAccountTableViewCellDelegate {
func accountButtonPressed(objectID: NSManagedObjectID, cell: SuggestionAccountTableViewCell) { func suggestionAccountTableViewCell(
// let selected = !viewModel.selectedAccounts.value.contains(objectID) _ cell: SuggestionAccountTableViewCell,
// cell.startAnimating() friendshipDidPressed button: UIButton
// viewModel.followAction(objectID: objectID)? ) {
// .sink(receiveCompletion: { [weak self] completion in guard let tableViewDiffableDataSource = viewModel.tableViewDiffableDataSource else { return }
// guard let self = self else { return } guard let indexPath = tableView.indexPath(for: cell) else { return }
// cell.stopAnimating() guard let item = tableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return }
// switch completion { guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
// case .failure(let error):
// os_log("%{public}s[%{public}ld], %{public}s: follow failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription) switch item {
// case .finished: case .account(let user):
// var selectedAccounts = self.viewModel.selectedAccounts.value Task { @MainActor in
// if selected { cell.startAnimating()
// selectedAccounts.append(objectID) do {
// } else { try await DataSourceFacade.responseToUserFollowAction(
// selectedAccounts.removeAll { $0 == objectID } dependency: self,
// } user: user,
// cell.button.isSelected = selected authenticationBox: authenticationBox
// self.viewModel.selectedAccounts.value = selectedAccounts )
// } } catch {
// }, receiveValue: { _ in // do noting
// }) }
// .store(in: &disposeBag) cell.stopAnimating()
} // end Task
}
} }
} }

View File

@ -22,6 +22,7 @@ extension SuggestionAccountViewModel {
) )
userFetchedResultsController.$records userFetchedResultsController.$records
.removeDuplicates()
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] records in .sink { [weak self] records in
guard let self = self else { return } guard let self = self else { return }
@ -50,7 +51,7 @@ extension SuggestionAccountViewModel {
context: context context: context
) )
userFetchedResultsController.$records selectedUserFetchedResultsController.$records
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] records in .sink { [weak self] records in
guard let self = self else { return } guard let self = self else { return }
@ -58,7 +59,16 @@ extension SuggestionAccountViewModel {
var snapshot = NSDiffableDataSourceSnapshot<SelectedAccountSection, SelectedAccountItem>() var snapshot = NSDiffableDataSourceSnapshot<SelectedAccountSection, SelectedAccountItem>()
snapshot.appendSections([.main]) snapshot.appendSections([.main])
let items: [SelectedAccountItem] = records.map { SelectedAccountItem.account($0) } var items: [SelectedAccountItem] = records.map { SelectedAccountItem.account($0) }
if items.count < 10 {
let count = 10 - items.count
let placeholderItems: [SelectedAccountItem] = (0..<count).map { _ in
SelectedAccountItem.placeHolder(uuid: UUID())
}
items.append(contentsOf: placeholderItems)
}
snapshot.appendItems(items, toSection: .main) snapshot.appendItems(items, toSection: .main)
if #available(iOS 15.0, *) { if #available(iOS 15.0, *) {

View File

@ -25,6 +25,7 @@ final class SuggestionAccountViewModel: NSObject {
// input // input
let context: AppContext let context: AppContext
let userFetchedResultsController: UserFetchedResultsController let userFetchedResultsController: UserFetchedResultsController
let selectedUserFetchedResultsController: UserFetchedResultsController
var viewWillAppear = PassthroughSubject<Void, Never>() var viewWillAppear = PassthroughSubject<Void, Never>()
@ -32,11 +33,6 @@ final class SuggestionAccountViewModel: NSObject {
var collectionViewDiffableDataSource: UICollectionViewDiffableDataSource<SelectedAccountSection, SelectedAccountItem>? var collectionViewDiffableDataSource: UICollectionViewDiffableDataSource<SelectedAccountSection, SelectedAccountItem>?
var tableViewDiffableDataSource: UITableViewDiffableDataSource<RecommendAccountSection, RecommendAccountItem>? var tableViewDiffableDataSource: UITableViewDiffableDataSource<RecommendAccountSection, RecommendAccountItem>?
@Published var selectedAccounts: [ManagedObjectRecord<MastodonUser>] = []
var headerPlaceholderCount = CurrentValueSubject<Int?, Never>(nil)
var suggestionAccountsFallback = PassthroughSubject<Void, Never>()
init( init(
context: AppContext context: AppContext
) { ) {
@ -44,53 +40,26 @@ final class SuggestionAccountViewModel: NSObject {
self.userFetchedResultsController = UserFetchedResultsController( self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext, managedObjectContext: context.managedObjectContext,
domain: nil, domain: nil,
additionalTweetPredicate: nil additionalPredicate: nil
)
self.selectedUserFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: nil,
additionalPredicate: nil
) )
super.init() super.init()
// 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)
//
// 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)
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
return return
} }
userFetchedResultsController.domain = authenticationBox.domain userFetchedResultsController.domain = authenticationBox.domain
selectedUserFetchedResultsController.domain = authenticationBox.domain
selectedUserFetchedResultsController.additionalPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: [
MastodonUser.predicate(followingBy: authenticationBox.userID),
MastodonUser.predicate(followRequestedBy: authenticationBox.userID)
])
// fetch recomment users
Task { Task {
var userIDs: [MastodonUser.ID] = [] var userIDs: [MastodonUser.ID] = []
do { do {
@ -111,121 +80,25 @@ final class SuggestionAccountViewModel: NSObject {
guard !userIDs.isEmpty else { return } guard !userIDs.isEmpty else { return }
userFetchedResultsController.userIDs = userIDs userFetchedResultsController.userIDs = userIDs
selectedUserFetchedResultsController.userIDs = userIDs
} }
// .sink { [weak self] completion in // fetch relationship
// switch completion { userFetchedResultsController.$records
// case .failure(let error): .removeDuplicates()
// if let apiError = error as? Mastodon.API.Error { .sink { [weak self] records in
// if apiError.httpResponseStatus == .notFound { guard let _ = self else { return }
// self?.suggestionAccountsFallback.send() Task {
// } guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
// } return
// case .finished: }
// // handle isFetchingLatestTimeline in fetch controller delegate _ = try await context.apiService.relationship(
// break records: records,
// } authenticationBox: authenticationBox
// } receiveValue: { [weak self] response in )
// let ids = response.value.map(\.account.id) }
// self?.receiveAccounts(ids: ids) }
// } .store(in: &disposeBag)
// .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()
// guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return nil }
//
// let mastodonUser = context.managedObjectContext.object(with: objectID) as! MastodonUser
// return context.apiService.toggleFollow(
// for: mastodonUser,
// activeMastodonAuthenticationBox: activeMastodonAuthenticationBox
// )
}
// func checkAccountsFollowState() {
// fatalError()
// guard let currentMastodonUser = currentMastodonUser.value else {
// return
// }
// let users: [MastodonUser] = accounts.value.compactMap {
// guard let user = context.managedObjectContext.object(with: $0) as? MastodonUser else {
// return nil
// }
// let isBlock = user.blockingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
// let isDomainBlock = user.domainBlockingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
// if isBlock || isDomainBlock {
// return nil
// } else {
// return user
// }
// }
// accounts.value = users.map(\.objectID)
//
// let followingUsers = users.filter { user -> Bool in
// let isFollowing = user.followingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
// let isPending = user.followRequestedBy.flatMap { $0.contains(currentMastodonUser) } ?? false
// return isFollowing || isPending
// }.map(\.objectID)
//
// selectedAccounts.value = followingUsers
// }
} }

View File

@ -0,0 +1,139 @@
//
// SuggestionAccountTableViewCell+ViewModel.swift
// Mastodon
//
// Created by MainasuK on 2022-2-16.
//
import UIKit
import Combine
import CoreDataStack
import MastodonAsset
import MastodonMeta
import Meta
extension SuggestionAccountTableViewCell {
class ViewModel {
var disposeBag = Set<AnyCancellable>()
@Published public var userIdentifier: UserIdentifier? // me
@Published var avatarImageURL: URL?
@Published public var authorName: MetaContent?
@Published public var authorUsername: String?
@Published var isFollowing = false
@Published var isPending = false
func prepareForReuse() {
isFollowing = false
isPending = false
}
}
}
extension SuggestionAccountTableViewCell.ViewModel {
func bind(cell: SuggestionAccountTableViewCell) {
// avatar
$avatarImageURL.removeDuplicates()
.sink { url in
let configuration = AvatarImageView.Configuration(url: url)
cell.avatarButton.avatarImageView.configure(configuration: configuration)
cell.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 12)))
}
.store(in: &disposeBag)
// name
$authorName
.sink { metaContent in
let metaContent = metaContent ?? PlaintextMetaContent(string: " ")
cell.titleLabel.configure(content: metaContent)
}
.store(in: &disposeBag)
// username
$authorUsername
.map { text -> String in
guard let text = text else { return "" }
return "@\(text)"
}
.sink { username in
cell.subTitleLabel.text = username
}
.store(in: &disposeBag)
// button
Publishers.CombineLatest(
$isFollowing,
$isPending
)
.sink { isFollowing, isPending in
let isFollowState = isFollowing || isPending
let imageName = isFollowState ? "minus.circle.fill" : "plus.circle"
let image = UIImage(systemName: imageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 22, weight: .regular))
cell.button.setImage(image, for: .normal)
cell.button.tintColor = isFollowState ? Asset.Colors.danger.color : Asset.Colors.Label.secondary.color
}
.store(in: &disposeBag)
}
}
extension SuggestionAccountTableViewCell {
func configure(user: MastodonUser) {
// author avatar
Publishers.CombineLatest(
user.publisher(for: \.avatar),
UserDefaults.shared.publisher(for: \.preferredStaticAvatar)
)
.map { _ in user.avatarImageURL() }
.assign(to: \.avatarImageURL, on: viewModel)
.store(in: &disposeBag)
// author name
Publishers.CombineLatest(
user.publisher(for: \.displayName),
user.publisher(for: \.emojis)
)
.map { _, emojis in
do {
let content = MastodonContent(content: user.displayNameWithFallback, emojis: emojis.asDictionary)
let metaContent = try MastodonMetaContent.convert(document: content)
return metaContent
} catch {
assertionFailure(error.localizedDescription)
return PlaintextMetaContent(string: user.displayNameWithFallback)
}
}
.assign(to: \.authorName, on: viewModel)
.store(in: &disposeBag)
// author username
user.publisher(for: \.acct)
.map { $0 as String? }
.assign(to: \.authorUsername, on: viewModel)
.store(in: &disposeBag)
// isFollowing
Publishers.CombineLatest(
viewModel.$userIdentifier,
user.publisher(for: \.followingBy)
)
.map { userIdentifier, followingBy in
guard let userIdentifier = userIdentifier else { return false }
return followingBy.contains(where: {
$0.id == userIdentifier.userID && $0.domain == userIdentifier.domain
})
}
.assign(to: \.isFollowing, on: viewModel)
.store(in: &disposeBag)
// isPending
Publishers.CombineLatest(
viewModel.$userIdentifier,
user.publisher(for: \.followRequestedBy)
)
.map { userIdentifier, followRequestedBy in
guard let userIdentifier = userIdentifier else { return false }
return followRequestedBy.contains(where: {
$0.id == userIdentifier.userID && $0.domain == userIdentifier.domain
})
}
.assign(to: \.isPending, on: viewModel)
.store(in: &disposeBag)
}
}

View File

@ -5,6 +5,7 @@
// Created by sxiaojian on 2021/4/21. // Created by sxiaojian on 2021/4/21.
// //
import os.log
import Combine import Combine
import CoreData import CoreData
import CoreDataStack import CoreDataStack
@ -15,23 +16,28 @@ import MetaTextKit
import MastodonMeta import MastodonMeta
import MastodonAsset import MastodonAsset
import MastodonLocalization import MastodonLocalization
import MastodonUI
protocol SuggestionAccountTableViewCellDelegate: AnyObject { protocol SuggestionAccountTableViewCellDelegate: AnyObject {
func accountButtonPressed(objectID: NSManagedObjectID, cell: SuggestionAccountTableViewCell) func suggestionAccountTableViewCell(_ cell: SuggestionAccountTableViewCell, friendshipDidPressed button: UIButton)
} }
final class SuggestionAccountTableViewCell: UITableViewCell { final class SuggestionAccountTableViewCell: UITableViewCell {
let logger = Logger(subsystem: "SuggestionAccountTableViewCell", category: "View")
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
weak var delegate: SuggestionAccountTableViewCellDelegate? weak var delegate: SuggestionAccountTableViewCellDelegate?
let _imageView: UIImageView = { public private(set) lazy var viewModel: ViewModel = {
let imageView = UIImageView() let viewModel = ViewModel()
imageView.tintColor = Asset.Colors.Label.primary.color viewModel.bind(cell: self)
imageView.layer.cornerRadius = 4 return viewModel
imageView.clipsToBounds = true
return imageView
}() }()
let avatarButton = AvatarButton()
let titleLabel = MetaLabel(style: .statusName) let titleLabel = MetaLabel(style: .statusName)
let subTitleLabel: UILabel = { let subTitleLabel: UILabel = {
@ -49,12 +55,8 @@ final class SuggestionAccountTableViewCell: UITableViewCell {
let button: HighlightDimmableButton = { let button: HighlightDimmableButton = {
let button = HighlightDimmableButton(type: .custom) let button = HighlightDimmableButton(type: .custom)
if let plusImage = UIImage(systemName: "plus.circle", withConfiguration: UIImage.SymbolConfiguration(pointSize: 22, weight: .regular))?.withRenderingMode(.alwaysTemplate) { let image = UIImage(systemName: "plus.circle", withConfiguration: UIImage.SymbolConfiguration(pointSize: 22, weight: .regular))
button.setImage(plusImage, for: .normal) button.setImage(image, for: .normal)
}
if let minusImage = UIImage(systemName: "minus.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 22, weight: .regular))?.withRenderingMode(.alwaysTemplate) {
button.setImage(minusImage, for: .selected)
}
return button return button
}() }()
@ -66,9 +68,10 @@ final class SuggestionAccountTableViewCell: UITableViewCell {
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
_imageView.af.cancelImageRequest()
_imageView.image = nil
disposeBag.removeAll() disposeBag.removeAll()
avatarButton.avatarImageView.prepareForReuse()
viewModel.prepareForReuse()
} }
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
@ -80,9 +83,11 @@ final class SuggestionAccountTableViewCell: UITableViewCell {
super.init(coder: coder) super.init(coder: coder)
configure() configure()
} }
} }
extension SuggestionAccountTableViewCell { extension SuggestionAccountTableViewCell {
private func configure() { private func configure() {
let containerStackView = UIStackView() let containerStackView = UIStackView()
containerStackView.axis = .horizontal containerStackView.axis = .horizontal
@ -99,11 +104,11 @@ extension SuggestionAccountTableViewCell {
containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
]) ])
_imageView.translatesAutoresizingMaskIntoConstraints = false avatarButton.translatesAutoresizingMaskIntoConstraints = false
containerStackView.addArrangedSubview(_imageView) containerStackView.addArrangedSubview(avatarButton)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
_imageView.widthAnchor.constraint(equalToConstant: 42).priority(.required - 1), avatarButton.widthAnchor.constraint(equalToConstant: 42).priority(.required - 1),
_imageView.heightAnchor.constraint(equalToConstant: 42).priority(.required - 1), avatarButton.heightAnchor.constraint(equalToConstant: 42).priority(.required - 1),
]) ])
let textStackView = UIStackView() let textStackView = UIStackView()
@ -139,56 +144,31 @@ extension SuggestionAccountTableViewCell {
buttonContainer.centerXAnchor.constraint(equalTo: button.centerXAnchor), buttonContainer.centerXAnchor.constraint(equalTo: button.centerXAnchor),
buttonContainer.centerYAnchor.constraint(equalTo: button.centerYAnchor), buttonContainer.centerYAnchor.constraint(equalTo: button.centerYAnchor),
]) ])
button.addTarget(self, action: #selector(SuggestionAccountTableViewCell.buttonDidPressed(_:)), for: .touchUpInside)
} }
func config(with account: MastodonUser) { }
if let url = account.avatarImageURL() {
_imageView.af.setImage( extension SuggestionAccountTableViewCell {
withURL: url, @objc private func buttonDidPressed(_ sender: UIButton) {
placeholderImage: UIImage.placeholder(color: .systemFill), logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
imageTransition: .crossDissolve(0.2) delegate?.suggestionAccountTableViewCell(self, friendshipDidPressed: sender)
)
}
let mastodonContent = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojis.asDictionary)
do {
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
titleLabel.configure(content: metaContent)
} catch {
let metaContent = PlaintextMetaContent(string: account.displayNameWithFallback)
titleLabel.configure(content: metaContent)
}
subTitleLabel.text = "@" + account.acct
button.isSelected = isSelected
button.publisher(for: .touchUpInside)
.sink { [weak self] _ in
guard let self = self else { return }
self.delegate?.accountButtonPressed(objectID: account.objectID, cell: self)
}
.store(in: &disposeBag)
button.publisher(for: \.isSelected)
.sink { [weak self] isSelected in
if isSelected {
self?.button.tintColor = Asset.Colors.danger.color
} else {
self?.button.tintColor = Asset.Colors.Label.secondary.color
}
}
.store(in: &disposeBag)
activityIndicatorView.publisher(for: \.isHidden)
.receive(on: DispatchQueue.main)
.sink { [weak self] isHidden in
self?.button.isHidden = !isHidden
}
.store(in: &disposeBag)
} }
}
extension SuggestionAccountTableViewCell {
func startAnimating() { func startAnimating() {
activityIndicatorView.isHidden = false activityIndicatorView.isHidden = false
activityIndicatorView.startAnimating() activityIndicatorView.startAnimating()
button.isHidden = true
} }
func stopAnimating() { func stopAnimating() {
activityIndicatorView.stopAnimating() activityIndicatorView.stopAnimating()
activityIndicatorView.isHidden = true activityIndicatorView.isHidden = true
button.isHidden = false
} }
} }

View File

@ -203,6 +203,14 @@ extension MastodonUser {
]) ])
} }
public static func predicate(followingBy userID: MastodonUser.ID) -> NSPredicate {
NSPredicate(format: "ANY %K.%K == %@", #keyPath(MastodonUser.followingBy), #keyPath(MastodonUser.id), userID)
}
public static func predicate(followRequestedBy userID: MastodonUser.ID) -> NSPredicate {
NSPredicate(format: "ANY %K.%K == %@", #keyPath(MastodonUser.followRequestedBy), #keyPath(MastodonUser.id), userID)
}
} }

View File

@ -197,166 +197,6 @@ extension Status {
return object return object
} }
// @discardableResult
// public static func insert(
// into context: NSManagedObjectContext,
// property: Property,
// author: MastodonUser,
// reblog: Status?,
// application: Application?,
// replyTo: Status?,
// poll: Poll?,
// mentions: [Mention]?,
// mediaAttachments: [Attachment]?,
// favouritedBy: MastodonUser?,
// rebloggedBy: MastodonUser?,
// mutedBy: MastodonUser?,
// bookmarkedBy: MastodonUser?,
// pinnedBy: MastodonUser?
// ) -> Status {
// let status: Status = context.insertObject()
//
// status.identifier = property.identifier
// status.domain = property.domain
//
// status.id = property.id
// status.uri = property.uri
// status.createdAt = property.createdAt
// status.content = property.content
//
// status.visibility = property.visibility
// status.sensitive = property.sensitive
// status.spoilerText = property.spoilerText
// status.application = application
//
// status.emojisData = property.emojisData
//
// status.reblogsCount = property.reblogsCount
// status.favouritesCount = property.favouritesCount
// status.repliesCount = property.repliesCount
//
// status.url = property.url
// status.inReplyToID = property.inReplyToID
// status.inReplyToAccountID = property.inReplyToAccountID
//
// status.language = property.language
// status.text = property.text
//
// status.author = author
// status.reblog = reblog
//
// status.pinnedBy = pinnedBy
// status.poll = poll
//
// if let mentions = mentions {
// status.mutableSetValue(forKey: #keyPath(Status.mentions)).addObjects(from: mentions)
// }
// if let mediaAttachments = mediaAttachments {
// status.mutableSetValue(forKey: #keyPath(Status.mediaAttachments)).addObjects(from: mediaAttachments)
// }
// if let favouritedBy = favouritedBy {
// status.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).add(favouritedBy)
// }
// if let rebloggedBy = rebloggedBy {
// status.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).add(rebloggedBy)
// }
// if let mutedBy = mutedBy {
// status.mutableSetValue(forKey: #keyPath(Status.mutedBy)).add(mutedBy)
// }
// if let bookmarkedBy = bookmarkedBy {
// status.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).add(bookmarkedBy)
// }
//
// status.updatedAt = property.networkDate
//
// return status
// }
//
// public func update(emojisData: Data?) {
// if self.emojisData != emojisData {
// self.emojisData = emojisData
// }
// }
//
// public func update(reblogsCount: NSNumber) {
// if self.reblogsCount.intValue != reblogsCount.intValue {
// self.reblogsCount = reblogsCount
// }
// }
//
// public func update(favouritesCount: NSNumber) {
// if self.favouritesCount.intValue != favouritesCount.intValue {
// self.favouritesCount = favouritesCount
// }
// }
//
// public func update(repliesCount: NSNumber?) {
// guard let count = repliesCount else {
// return
// }
// if self.repliesCount?.intValue != count.intValue {
// self.repliesCount = repliesCount
// }
// }
//
// public func update(replyTo: Status?) {
// if self.replyTo != replyTo {
// self.replyTo = replyTo
// }
// }
//
// public func update(liked: Bool, by mastodonUser: MastodonUser) {
// if liked {
// if !(self.favouritedBy ?? Set()).contains(mastodonUser) {
// self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).add(mastodonUser)
// }
// } else {
// if (self.favouritedBy ?? Set()).contains(mastodonUser) {
// self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).remove(mastodonUser)
// }
// }
// }
//
// public func update(reblogged: Bool, by mastodonUser: MastodonUser) {
// if reblogged {
// if !(self.rebloggedBy ?? Set()).contains(mastodonUser) {
// self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).add(mastodonUser)
// }
// } else {
// if (self.rebloggedBy ?? Set()).contains(mastodonUser) {
// self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).remove(mastodonUser)
// }
// }
// }
//
// public func update(muted: Bool, by mastodonUser: MastodonUser) {
// if muted {
// if !(self.mutedBy ?? Set()).contains(mastodonUser) {
// self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).add(mastodonUser)
// }
// } else {
// if (self.mutedBy ?? Set()).contains(mastodonUser) {
// self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).remove(mastodonUser)
// }
// }
// }
//
// public func update(bookmarked: Bool, by mastodonUser: MastodonUser) {
// if bookmarked {
// if !(self.bookmarkedBy ?? Set()).contains(mastodonUser) {
// self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).add(mastodonUser)
// }
// } else {
// if (self.bookmarkedBy ?? Set()).contains(mastodonUser) {
// self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).remove(mastodonUser)
// }
// }
// }
//
// public func didUpdate(at networkDate: Date) {
// self.updatedAt = networkDate
// }
} }
@ -737,78 +577,3 @@ extension Status {
mutableSetValue(forKey: #keyPath(Status.feeds)).add(feed) mutableSetValue(forKey: #keyPath(Status.feeds)).add(feed)
} }
} }
//extension Status {
// public struct Property {
//
// public let identifier: ID
// public let domain: String
//
// public let id: String
// public let uri: String
// public let createdAt: Date
// public let content: String
//
// public let visibility: String?
// public let sensitive: Bool
// public let spoilerText: String?
//
// public let emojisData: Data?
//
// public let reblogsCount: NSNumber
// public let favouritesCount: NSNumber
// public let repliesCount: NSNumber?
//
// public let url: String?
// public let inReplyToID: Status.ID?
// public let inReplyToAccountID: MastodonUser.ID?
// public let language: String? // (ISO 639 Part @1 two-letter language code)
// public let text: String?
//
// public let networkDate: Date
//
// public init(
// domain: String,
// id: String,
// uri: String,
// createdAt: Date,
// content: String,
// visibility: String?,
// sensitive: Bool,
// spoilerText: String?,
// emojisData: Data?,
// reblogsCount: NSNumber,
// favouritesCount: NSNumber,
// repliesCount: NSNumber?,
// url: String?,
// inReplyToID: Status.ID?,
// inReplyToAccountID: MastodonUser.ID?,
// language: String?,
// text: String?,
// networkDate: Date
// ) {
// self.identifier = id + "@" + domain
// self.domain = domain
// self.id = id
// self.uri = uri
// self.createdAt = createdAt
// self.content = content
// self.visibility = visibility
// self.sensitive = sensitive
// self.spoilerText = spoilerText
// self.emojisData = emojisData
// self.reblogsCount = reblogsCount
// self.favouritesCount = favouritesCount
// self.repliesCount = repliesCount
// self.url = url
// self.inReplyToID = inReplyToID
// self.inReplyToAccountID = inReplyToAccountID
// self.language = language
// self.text = text
// self.networkDate = networkDate
// }
//
// }
//}
//