chore: fix follow button can not trigger issue. resolve #263

This commit is contained in:
CMK 2021-08-05 19:28:41 +08:00
parent 9242a859df
commit ecb6d2a809
5 changed files with 161 additions and 107 deletions

View File

@ -10,6 +10,9 @@ import CoreDataStack
import Foundation import Foundation
import MastodonSDK import MastodonSDK
import UIKit import UIKit
import MetaTextKit
import MastodonMeta
import Combine
enum RecommendAccountSection: Equatable, Hashable { enum RecommendAccountSection: Equatable, Hashable {
case main case main
@ -18,18 +21,118 @@ enum RecommendAccountSection: Equatable, Hashable {
extension RecommendAccountSection { extension RecommendAccountSection {
static func collectionViewDiffableDataSource( static func collectionViewDiffableDataSource(
for collectionView: UICollectionView, for collectionView: UICollectionView,
dependency: NeedsDependency,
delegate: SearchRecommendAccountsCollectionViewCellDelegate, delegate: SearchRecommendAccountsCollectionViewCellDelegate,
managedObjectContext: NSManagedObjectContext managedObjectContext: NSManagedObjectContext
) -> UICollectionViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID> { ) -> UICollectionViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID> {
UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak delegate] collectionView, indexPath, objectID -> UICollectionViewCell? in UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak delegate] collectionView, indexPath, objectID -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self), for: indexPath) as! SearchRecommendAccountsCollectionViewCell let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self), for: indexPath) as! SearchRecommendAccountsCollectionViewCell
let user = managedObjectContext.object(with: objectID) as! MastodonUser managedObjectContext.performAndWait {
let user = managedObjectContext.object(with: objectID) as! MastodonUser
configure(cell: cell, user: user, dependency: dependency)
}
cell.delegate = delegate cell.delegate = delegate
cell.config(with: user)
return cell return cell
} }
} }
static func configure(
cell: SearchRecommendAccountsCollectionViewCell,
user: MastodonUser,
dependency: NeedsDependency
) {
configureContent(cell: cell, user: user)
if let currentMastodonUser = dependency.context.authenticationService.activeMastodonAuthentication.value?.user {
configureFollowButton(with: user, currentMastodonUser: currentMastodonUser, followButton: cell.followButton)
}
Publishers.CombineLatest(
ManagedObjectObserver.observe(object: user).eraseToAnyPublisher().mapError { $0 as Error },
dependency.context.authenticationService.activeMastodonAuthentication.setFailureType(to: Error.self)
)
.receive(on: DispatchQueue.main)
.sink { _ in
// do nothing
} receiveValue: { [weak cell] change, authentication in
guard let cell = cell else { return }
guard case .update(let object) = change.changeType,
let user = object as? MastodonUser else { return }
guard let currentMastodonUser = authentication?.user else { return }
configureFollowButton(with: user, currentMastodonUser: currentMastodonUser, followButton: cell.followButton)
}
.store(in: &cell.disposeBag)
}
static func configureContent(
cell: SearchRecommendAccountsCollectionViewCell,
user: MastodonUser
) {
do {
let mastodonContent = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
cell.displayNameLabel.configure(content: metaContent)
} catch {
let metaContent = PlaintextMetaContent(string: user.displayNameWithFallback)
cell.displayNameLabel.configure(content: metaContent)
}
cell.acctLabel.text = "@" + user.acct
cell.avatarImageView.af.setImage(
withURL: user.avatarImageURLWithFallback(domain: user.domain),
placeholderImage: UIImage.placeholder(color: .systemFill),
imageTransition: .crossDissolve(0.2)
)
cell.headerImageView.af.setImage(
withURL: URL(string: user.header)!,
placeholderImage: UIImage.placeholder(color: .systemFill),
imageTransition: .crossDissolve(0.2)
) { [weak cell] _ in
// guard let cell = cell else { return }
}
}
static func configureFollowButton(
with mastodonUser: MastodonUser,
currentMastodonUser: MastodonUser,
followButton: HighlightDimmableButton
) {
let relationshipActionSet = relationShipActionSet(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
followButton.setTitle(relationshipActionSet.title, for: .normal)
}
static func relationShipActionSet(
mastodonUser: MastodonUser,
currentMastodonUser: MastodonUser
) -> ProfileViewModel.RelationshipActionOptionSet {
var relationshipActionSet = ProfileViewModel.RelationshipActionOptionSet([.follow])
let isFollowing = mastodonUser.followingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
if isFollowing {
relationshipActionSet.insert(.following)
}
let isPending = mastodonUser.followRequestedBy.flatMap { $0.contains(currentMastodonUser) } ?? false
if isPending {
relationshipActionSet.insert(.pending)
}
let isBlocking = mastodonUser.blockingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
if isBlocking {
relationshipActionSet.insert(.blocking)
}
let isBlockedBy = currentMastodonUser.blockingBy.flatMap { $0.contains(mastodonUser) } ?? false
if isBlockedBy {
relationshipActionSet.insert(.blocked)
}
return relationshipActionSet
}
}
extension RecommendAccountSection {
static func tableViewDiffableDataSource( static func tableViewDiffableDataSource(
for tableView: UITableView, for tableView: UITableView,
managedObjectContext: NSManagedObjectContext, managedObjectContext: NSManagedObjectContext,

View File

@ -5,6 +5,7 @@
// Created by sxiaojian on 2021/4/1. // Created by sxiaojian on 2021/4/1.
// //
import os.log
import Combine import Combine
import CoreDataStack import CoreDataStack
import Foundation import Foundation
@ -14,12 +15,12 @@ import MetaTextKit
import MastodonMeta import MastodonMeta
protocol SearchRecommendAccountsCollectionViewCellDelegate: NSObject { protocol SearchRecommendAccountsCollectionViewCellDelegate: NSObject {
func followButtonDidPressed(clickedUser: MastodonUser) func searchRecommendAccountsCollectionViewCell(_ cell: SearchRecommendAccountsCollectionViewCell, followButtonDidPressed button: UIButton)
func configFollowButton(with mastodonUser: MastodonUser, followButton: HighlightDimmableButton)
} }
class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell {
let logger = Logger(subsystem: "SearchRecommendAccountsCollectionViewCell", category: "UI")
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
weak var delegate: SearchRecommendAccountsCollectionViewCellDelegate? weak var delegate: SearchRecommendAccountsCollectionViewCellDelegate?
@ -72,7 +73,6 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell {
super.prepareForReuse() super.prepareForReuse()
headerImageView.af.cancelImageRequest() headerImageView.af.cancelImageRequest()
avatarImageView.af.cancelImageRequest() avatarImageView.af.cancelImageRequest()
visualEffectView.removeFromSuperview()
disposeBag.removeAll() disposeBag.removeAll()
} }
@ -117,6 +117,15 @@ extension SearchRecommendAccountsCollectionViewCell {
headerImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) headerImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
]) ])
headerImageView.addSubview(visualEffectView)
visualEffectView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
visualEffectView.topAnchor.constraint(equalTo: headerImageView.topAnchor),
visualEffectView.leadingAnchor.constraint(equalTo: headerImageView.leadingAnchor),
visualEffectView.trailingAnchor.constraint(equalTo: headerImageView.trailingAnchor),
visualEffectView.bottomAnchor.constraint(equalTo: headerImageView.bottomAnchor)
])
let containerStackView = UIStackView() let containerStackView = UIStackView()
containerStackView.axis = .vertical containerStackView.axis = .vertical
containerStackView.distribution = .fill containerStackView.distribution = .fill
@ -156,48 +165,16 @@ extension SearchRecommendAccountsCollectionViewCell {
followButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 24) followButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 24)
]) ])
containerStackView.addArrangedSubview(followButton) containerStackView.addArrangedSubview(followButton)
followButton.addTarget(self, action: #selector(SearchRecommendAccountsCollectionViewCell.followButtonDidPressed(_:)), for: .touchUpInside)
} }
func config(with mastodonUser: MastodonUser) { }
do {
let mastodonContent = MastodonContent(content: mastodonUser.displayNameWithFallback, emojis: mastodonUser.emojiMeta) extension SearchRecommendAccountsCollectionViewCell {
let metaContent = try MastodonMetaContent.convert(document: mastodonContent) @objc private func followButtonDidPressed(_ sender: UIButton) {
displayNameLabel.configure(content: metaContent) logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
} catch { delegate?.searchRecommendAccountsCollectionViewCell(self, followButtonDidPressed: sender)
let metaContent = PlaintextMetaContent(string: mastodonUser.displayNameWithFallback)
displayNameLabel.configure(content: metaContent)
}
acctLabel.text = "@" + mastodonUser.acct
avatarImageView.af.setImage(
withURL: URL(string: mastodonUser.avatar)!,
placeholderImage: UIImage.placeholder(color: .systemFill),
imageTransition: .crossDissolve(0.2)
)
headerImageView.af.setImage(
withURL: URL(string: mastodonUser.header)!,
placeholderImage: UIImage.placeholder(color: .systemFill),
imageTransition: .crossDissolve(0.2)
) { [weak self] _ in
guard let self = self else { return }
self.headerImageView.addSubview(self.visualEffectView)
self.visualEffectView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.visualEffectView.topAnchor.constraint(equalTo: self.headerImageView.topAnchor),
self.visualEffectView.leadingAnchor.constraint(equalTo: self.headerImageView.leadingAnchor),
self.visualEffectView.trailingAnchor.constraint(equalTo: self.headerImageView.trailingAnchor),
self.visualEffectView.bottomAnchor.constraint(equalTo: self.headerImageView.bottomAnchor)
])
}
delegate?.configFollowButton(with: mastodonUser, followButton: followButton)
followButton.publisher(for: .touchUpInside)
.sink { [weak self] _ in
self?.followButtonDidPressed(mastodonUser: mastodonUser)
}
.store(in: &disposeBag)
}
func followButtonDidPressed(mastodonUser: MastodonUser) {
delegate?.followButtonDidPressed(clickedUser: mastodonUser)
} }
} }

View File

@ -26,16 +26,30 @@ extension SearchViewController: UserProvider {
} }
extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegate { extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegate {
func followButtonDidPressed(clickedUser: MastodonUser) { func searchRecommendAccountsCollectionViewCell(_ cell: SearchRecommendAccountsCollectionViewCell, followButtonDidPressed button: UIButton) {
guard let diffableDataSource = viewModel.accountDiffableDataSource else { return }
guard let indexPath = accountsCollectionView.indexPath(for: cell),
let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
context.managedObjectContext.performAndWait {
guard let user = try? context.managedObjectContext.existingObject(with: item) as? MastodonUser else { return }
self.toggleFriendship(for: user)
}
}
func toggleFriendship(for mastodonUser: MastodonUser) {
guard let currentMastodonUser = viewModel.currentMastodonUser.value else { guard let currentMastodonUser = viewModel.currentMastodonUser.value else {
return return
} }
guard let relationshipAction = relationShipActionSet(mastodonUser: clickedUser, currentMastodonUser: currentMastodonUser).highPriorityAction(except: .editOptions) else { return } guard let relationshipAction = RecommendAccountSection.relationShipActionSet(
mastodonUser: mastodonUser,
currentMastodonUser: currentMastodonUser).highPriorityAction(except: .editOptions)
else { return }
switch relationshipAction { switch relationshipAction {
case .none: case .none:
break break
case .follow, .following: case .follow, .following:
UserProviderFacade.toggleUserFollowRelationship(provider: self, mastodonUser: clickedUser) UserProviderFacade.toggleUserFollowRelationship(provider: self, mastodonUser: mastodonUser)
.sink { _ in .sink { _ in
// error handling // error handling
} receiveValue: { _ in } receiveValue: { _ in
@ -45,7 +59,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat
case .pending: case .pending:
break break
case .muting: case .muting:
let name = clickedUser.displayNameWithFallback let name = mastodonUser.displayNameWithFallback
let alertController = UIAlertController( let alertController = UIAlertController(
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title, title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title,
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name), message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name),
@ -53,7 +67,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat
) )
let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
UserProviderFacade.toggleUserMuteRelationship(provider: self, mastodonUser: clickedUser) UserProviderFacade.toggleUserMuteRelationship(provider: self, mastodonUser: mastodonUser)
.sink { _ in .sink { _ in
// do nothing // do nothing
} receiveValue: { _ in } receiveValue: { _ in
@ -66,7 +80,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat
alertController.addAction(cancelAction) alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil) present(alertController, animated: true, completion: nil)
case .blocking: case .blocking:
let name = clickedUser.displayNameWithFallback let name = mastodonUser.displayNameWithFallback
let alertController = UIAlertController( let alertController = UIAlertController(
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.title, title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.title,
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.message(name), message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.message(name),
@ -74,7 +88,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat
) )
let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
UserProviderFacade.toggleUserBlockRelationship(provider: self, mastodonUser: clickedUser) UserProviderFacade.toggleUserBlockRelationship(provider: self, mastodonUser: mastodonUser)
.sink { _ in .sink { _ in
// do nothing // do nothing
} receiveValue: { _ in } receiveValue: { _ in
@ -93,50 +107,4 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat
} }
} }
func configFollowButton(with mastodonUser: MastodonUser, followButton: HighlightDimmableButton) {
guard let currentMastodonUser = viewModel.currentMastodonUser.value else {
return
}
_configFollowButton(with: mastodonUser, currentMastodonUser: currentMastodonUser, followButton: followButton)
ManagedObjectObserver.observe(object: currentMastodonUser)
.sink { _ in
} receiveValue: { change in
guard case .update(let object) = change.changeType,
let newUser = object as? MastodonUser else { return }
self._configFollowButton(with: mastodonUser, currentMastodonUser: newUser, followButton: followButton)
}
.store(in: &disposeBag)
}
}
extension SearchViewController {
func _configFollowButton(with mastodonUser: MastodonUser, currentMastodonUser: MastodonUser, followButton: HighlightDimmableButton) {
let relationshipActionSet = relationShipActionSet(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
followButton.setTitle(relationshipActionSet.title, for: .normal)
}
func relationShipActionSet(mastodonUser: MastodonUser, currentMastodonUser: MastodonUser) -> ProfileViewModel.RelationshipActionOptionSet {
var relationshipActionSet = ProfileViewModel.RelationshipActionOptionSet([.follow])
let isFollowing = mastodonUser.followingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
if isFollowing {
relationshipActionSet.insert(.following)
}
let isPending = mastodonUser.followRequestedBy.flatMap { $0.contains(currentMastodonUser) } ?? false
if isPending {
relationshipActionSet.insert(.pending)
}
let isBlocking = mastodonUser.blockingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
if isBlocking {
relationshipActionSet.insert(.blocking)
}
let isBlockedBy = currentMastodonUser.blockingBy.flatMap { $0.contains(mastodonUser) } ?? false
if isBlockedBy {
relationshipActionSet.insert(.blocked)
}
return relationshipActionSet
}
} }

View File

@ -40,7 +40,7 @@ final class SearchViewController: UIViewController, NeedsDependency {
var searchTransitionController = SearchTransitionController() var searchTransitionController = SearchTransitionController()
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
private(set) lazy var viewModel = SearchViewModel(context: context, coordinator: coordinator) private(set) lazy var viewModel = SearchViewModel(context: context)
// recommend // recommend
let scrollView: UIScrollView = { let scrollView: UIScrollView = {
@ -167,7 +167,12 @@ extension SearchViewController {
private func setupDataSource() { private func setupDataSource() {
viewModel.hashtagDiffableDataSource = RecommendHashTagSection.collectionViewDiffableDataSource(for: hashtagCollectionView) viewModel.hashtagDiffableDataSource = RecommendHashTagSection.collectionViewDiffableDataSource(for: hashtagCollectionView)
viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView, delegate: self, managedObjectContext: context.managedObjectContext) viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(
for: accountsCollectionView,
dependency: self,
delegate: self,
managedObjectContext: context.managedObjectContext
)
} }
} }

View File

@ -19,24 +19,25 @@ final class SearchViewModel: NSObject {
// input // input
let context: AppContext let context: AppContext
weak var coordinator: SceneCoordinator!
let currentMastodonUser = CurrentValueSubject<MastodonUser?, Never>(nil)
let viewDidAppeared = PassthroughSubject<Void, Never>() let viewDidAppeared = PassthroughSubject<Void, Never>()
// output // output
let currentMastodonUser = CurrentValueSubject<MastodonUser?, Never>(nil)
// var recommendHashTags = [Mastodon.Entity.Tag]()
var recommendAccounts = [NSManagedObjectID]() var recommendAccounts = [NSManagedObjectID]()
var recommendAccountsFallback = PassthroughSubject<Void, Never>() var recommendAccountsFallback = PassthroughSubject<Void, Never>()
var hashtagDiffableDataSource: UICollectionViewDiffableDataSource<RecommendHashTagSection, Mastodon.Entity.Tag>? var hashtagDiffableDataSource: UICollectionViewDiffableDataSource<RecommendHashTagSection, Mastodon.Entity.Tag>?
var accountDiffableDataSource: UICollectionViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID>? var accountDiffableDataSource: UICollectionViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID>?
init(context: AppContext, coordinator: SceneCoordinator) { init(context: AppContext) {
self.coordinator = coordinator
self.context = context self.context = context
super.init() super.init()
context.authenticationService.activeMastodonAuthentication
.map { $0?.user }
.assign(to: \.value, on: currentMastodonUser)
.store(in: &disposeBag)
Publishers.CombineLatest( Publishers.CombineLatest(
context.authenticationService.activeMastodonAuthenticationBox, context.authenticationService.activeMastodonAuthenticationBox,