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": {
"show_avatar_image": "Show 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": {

View File

@ -546,6 +546,7 @@
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */; };
DBD376AC2692ECDB007FEC24 /* ThemePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD376AB2692ECDB007FEC24 /* ThemePreference.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 */; };
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.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>"; };
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>"; };
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>"; };
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>"; };
@ -1768,6 +1770,7 @@
isa = PBXGroup;
children = (
2DAC9E45262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift */,
DBD5B1F527BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift */,
);
path = TableViewCell;
sourceTree = "<group>";
@ -3835,6 +3838,7 @@
DBB45B5E27B4EB22002DC5A7 /* AdaptiveMarginStatusTableViewCell.swift in Sources */,
DB6180F226391CF40018D199 /* MediaPreviewImageViewModel.swift in Sources */,
DBA5E7A3263AD0A3004598BB /* PhotoLibraryService.swift in Sources */,
DBD5B1F627BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */,
5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */,
DB63F767279A5EB300455B82 /* NotificationTimelineViewModel.swift in Sources */,
2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */,

View File

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

View File

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

View File

@ -146,10 +146,15 @@ extension RecommendAccountSection {
case .account(let record):
context.managedObjectContext.performAndWait {
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
}
}

View File

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

View File

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

View File

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

View File

@ -74,7 +74,7 @@ extension SuggestionAccountViewController {
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: RunLoop.main)
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.setupBackgroundColor(theme: theme)
@ -169,29 +169,31 @@ extension SuggestionAccountViewController: UITableViewDelegate {
}
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)
func suggestionAccountTableViewCell(
_ cell: SuggestionAccountTableViewCell,
friendshipDidPressed button: UIButton
) {
guard let tableViewDiffableDataSource = viewModel.tableViewDiffableDataSource else { return }
guard let indexPath = tableView.indexPath(for: cell) else { return }
guard let item = tableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return }
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
switch item {
case .account(let user):
Task { @MainActor in
cell.startAnimating()
do {
try await DataSourceFacade.responseToUserFollowAction(
dependency: self,
user: user,
authenticationBox: authenticationBox
)
} catch {
// do noting
}
cell.stopAnimating()
} // end Task
}
}
}

View File

@ -22,6 +22,7 @@ extension SuggestionAccountViewModel {
)
userFetchedResultsController.$records
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }
@ -50,7 +51,7 @@ extension SuggestionAccountViewModel {
context: context
)
userFetchedResultsController.$records
selectedUserFetchedResultsController.$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }
@ -58,7 +59,16 @@ extension SuggestionAccountViewModel {
var snapshot = NSDiffableDataSourceSnapshot<SelectedAccountSection, SelectedAccountItem>()
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)
if #available(iOS 15.0, *) {

View File

@ -25,6 +25,7 @@ final class SuggestionAccountViewModel: NSObject {
// input
let context: AppContext
let userFetchedResultsController: UserFetchedResultsController
let selectedUserFetchedResultsController: UserFetchedResultsController
var viewWillAppear = PassthroughSubject<Void, Never>()
@ -32,11 +33,6 @@ final class SuggestionAccountViewModel: NSObject {
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>()
init(
context: AppContext
) {
@ -44,53 +40,26 @@ final class SuggestionAccountViewModel: NSObject {
self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: nil,
additionalTweetPredicate: nil
additionalPredicate: nil
)
self.selectedUserFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: nil,
additionalPredicate: nil
)
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 {
return
}
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 {
var userIDs: [MastodonUser.ID] = []
do {
@ -111,121 +80,25 @@ final class SuggestionAccountViewModel: NSObject {
guard !userIDs.isEmpty else { return }
userFetchedResultsController.userIDs = userIDs
selectedUserFetchedResultsController.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)
// fetch relationship
userFetchedResultsController.$records
.removeDuplicates()
.sink { [weak self] records in
guard let _ = self else { return }
Task {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
return
}
_ = try await context.apiService.relationship(
records: records,
authenticationBox: authenticationBox
)
}
}
.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.
//
import os.log
import Combine
import CoreData
import CoreDataStack
@ -15,23 +16,28 @@ import MetaTextKit
import MastodonMeta
import MastodonAsset
import MastodonLocalization
import MastodonUI
protocol SuggestionAccountTableViewCellDelegate: AnyObject {
func accountButtonPressed(objectID: NSManagedObjectID, cell: SuggestionAccountTableViewCell)
func suggestionAccountTableViewCell(_ cell: SuggestionAccountTableViewCell, friendshipDidPressed button: UIButton)
}
final class SuggestionAccountTableViewCell: UITableViewCell {
let logger = Logger(subsystem: "SuggestionAccountTableViewCell", category: "View")
var disposeBag = Set<AnyCancellable>()
weak var delegate: SuggestionAccountTableViewCellDelegate?
let _imageView: UIImageView = {
let imageView = UIImageView()
imageView.tintColor = Asset.Colors.Label.primary.color
imageView.layer.cornerRadius = 4
imageView.clipsToBounds = true
return imageView
public private(set) lazy var viewModel: ViewModel = {
let viewModel = ViewModel()
viewModel.bind(cell: self)
return viewModel
}()
let avatarButton = AvatarButton()
let titleLabel = MetaLabel(style: .statusName)
let subTitleLabel: UILabel = {
@ -49,12 +55,8 @@ final class SuggestionAccountTableViewCell: UITableViewCell {
let button: HighlightDimmableButton = {
let button = HighlightDimmableButton(type: .custom)
if let plusImage = UIImage(systemName: "plus.circle", withConfiguration: UIImage.SymbolConfiguration(pointSize: 22, weight: .regular))?.withRenderingMode(.alwaysTemplate) {
button.setImage(plusImage, for: .normal)
}
if let minusImage = UIImage(systemName: "minus.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 22, weight: .regular))?.withRenderingMode(.alwaysTemplate) {
button.setImage(minusImage, for: .selected)
}
let image = UIImage(systemName: "plus.circle", withConfiguration: UIImage.SymbolConfiguration(pointSize: 22, weight: .regular))
button.setImage(image, for: .normal)
return button
}()
@ -66,9 +68,10 @@ final class SuggestionAccountTableViewCell: UITableViewCell {
override func prepareForReuse() {
super.prepareForReuse()
_imageView.af.cancelImageRequest()
_imageView.image = nil
disposeBag.removeAll()
avatarButton.avatarImageView.prepareForReuse()
viewModel.prepareForReuse()
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
@ -80,9 +83,11 @@ final class SuggestionAccountTableViewCell: UITableViewCell {
super.init(coder: coder)
configure()
}
}
extension SuggestionAccountTableViewCell {
private func configure() {
let containerStackView = UIStackView()
containerStackView.axis = .horizontal
@ -99,11 +104,11 @@ extension SuggestionAccountTableViewCell {
containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
_imageView.translatesAutoresizingMaskIntoConstraints = false
containerStackView.addArrangedSubview(_imageView)
avatarButton.translatesAutoresizingMaskIntoConstraints = false
containerStackView.addArrangedSubview(avatarButton)
NSLayoutConstraint.activate([
_imageView.widthAnchor.constraint(equalToConstant: 42).priority(.required - 1),
_imageView.heightAnchor.constraint(equalToConstant: 42).priority(.required - 1),
avatarButton.widthAnchor.constraint(equalToConstant: 42).priority(.required - 1),
avatarButton.heightAnchor.constraint(equalToConstant: 42).priority(.required - 1),
])
let textStackView = UIStackView()
@ -139,56 +144,31 @@ extension SuggestionAccountTableViewCell {
buttonContainer.centerXAnchor.constraint(equalTo: button.centerXAnchor),
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(
withURL: url,
placeholderImage: UIImage.placeholder(color: .systemFill),
imageTransition: .crossDissolve(0.2)
)
}
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 {
@objc private func buttonDidPressed(_ sender: UIButton) {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
delegate?.suggestionAccountTableViewCell(self, friendshipDidPressed: sender)
}
}
extension SuggestionAccountTableViewCell {
func startAnimating() {
activityIndicatorView.isHidden = false
activityIndicatorView.startAnimating()
button.isHidden = true
}
func stopAnimating() {
activityIndicatorView.stopAnimating()
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
}
// @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)
}
}
//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
// }
//
// }
//}
//