From ecb6d2a8094d7f8f9e7a667b3d7e402573aacabd Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 5 Aug 2021 19:28:41 +0800 Subject: [PATCH] chore: fix follow button can not trigger issue. resolve #263 --- .../Search/RecommendAccountSection.swift | 107 +++++++++++++++++- ...hRecommendAccountsCollectionViewCell.swift | 65 ++++------- .../Search/SearchViewController+Follow.swift | 74 ++++-------- .../Search/Search/SearchViewController.swift | 9 +- .../Scene/Search/Search/SearchViewModel.swift | 13 ++- 5 files changed, 161 insertions(+), 107 deletions(-) diff --git a/Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift b/Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift index 64019e580..b894f818d 100644 --- a/Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift +++ b/Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift @@ -10,6 +10,9 @@ import CoreDataStack import Foundation import MastodonSDK import UIKit +import MetaTextKit +import MastodonMeta +import Combine enum RecommendAccountSection: Equatable, Hashable { case main @@ -18,18 +21,118 @@ enum RecommendAccountSection: Equatable, Hashable { extension RecommendAccountSection { static func collectionViewDiffableDataSource( for collectionView: UICollectionView, + dependency: NeedsDependency, delegate: SearchRecommendAccountsCollectionViewCellDelegate, managedObjectContext: NSManagedObjectContext ) -> UICollectionViewDiffableDataSource { UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak delegate] collectionView, indexPath, objectID -> UICollectionViewCell? in 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.config(with: user) 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( for tableView: UITableView, managedObjectContext: NSManagedObjectContext, diff --git a/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index cd1d196e4..365c1ee72 100644 --- a/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -5,6 +5,7 @@ // Created by sxiaojian on 2021/4/1. // +import os.log import Combine import CoreDataStack import Foundation @@ -14,12 +15,12 @@ import MetaTextKit import MastodonMeta protocol SearchRecommendAccountsCollectionViewCellDelegate: NSObject { - func followButtonDidPressed(clickedUser: MastodonUser) - - func configFollowButton(with mastodonUser: MastodonUser, followButton: HighlightDimmableButton) + func searchRecommendAccountsCollectionViewCell(_ cell: SearchRecommendAccountsCollectionViewCell, followButtonDidPressed button: UIButton) } class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { + + let logger = Logger(subsystem: "SearchRecommendAccountsCollectionViewCell", category: "UI") var disposeBag = Set() weak var delegate: SearchRecommendAccountsCollectionViewCellDelegate? @@ -72,7 +73,6 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { super.prepareForReuse() headerImageView.af.cancelImageRequest() avatarImageView.af.cancelImageRequest() - visualEffectView.removeFromSuperview() disposeBag.removeAll() } @@ -117,6 +117,15 @@ extension SearchRecommendAccountsCollectionViewCell { 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() containerStackView.axis = .vertical containerStackView.distribution = .fill @@ -156,48 +165,16 @@ extension SearchRecommendAccountsCollectionViewCell { followButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 24) ]) 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) - let metaContent = try MastodonMetaContent.convert(document: mastodonContent) - displayNameLabel.configure(content: metaContent) - } catch { - 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) +} + +extension SearchRecommendAccountsCollectionViewCell { + @objc private func followButtonDidPressed(_ sender: UIButton) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + delegate?.searchRecommendAccountsCollectionViewCell(self, followButtonDidPressed: sender) } } diff --git a/Mastodon/Scene/Search/Search/SearchViewController+Follow.swift b/Mastodon/Scene/Search/Search/SearchViewController+Follow.swift index c345336df..386b0af18 100644 --- a/Mastodon/Scene/Search/Search/SearchViewController+Follow.swift +++ b/Mastodon/Scene/Search/Search/SearchViewController+Follow.swift @@ -26,16 +26,30 @@ extension SearchViewController: UserProvider { } 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 { 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 { case .none: break case .follow, .following: - UserProviderFacade.toggleUserFollowRelationship(provider: self, mastodonUser: clickedUser) + UserProviderFacade.toggleUserFollowRelationship(provider: self, mastodonUser: mastodonUser) .sink { _ in // error handling } receiveValue: { _ in @@ -45,7 +59,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat case .pending: break case .muting: - let name = clickedUser.displayNameWithFallback + let name = mastodonUser.displayNameWithFallback let alertController = UIAlertController( title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title, 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 guard let self = self else { return } - UserProviderFacade.toggleUserMuteRelationship(provider: self, mastodonUser: clickedUser) + UserProviderFacade.toggleUserMuteRelationship(provider: self, mastodonUser: mastodonUser) .sink { _ in // do nothing } receiveValue: { _ in @@ -66,7 +80,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat alertController.addAction(cancelAction) present(alertController, animated: true, completion: nil) case .blocking: - let name = clickedUser.displayNameWithFallback + let name = mastodonUser.displayNameWithFallback let alertController = UIAlertController( title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.title, 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 guard let self = self else { return } - UserProviderFacade.toggleUserBlockRelationship(provider: self, mastodonUser: clickedUser) + UserProviderFacade.toggleUserBlockRelationship(provider: self, mastodonUser: mastodonUser) .sink { _ in // do nothing } 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 - } } diff --git a/Mastodon/Scene/Search/Search/SearchViewController.swift b/Mastodon/Scene/Search/Search/SearchViewController.swift index 7c5003703..a3d84cd6a 100644 --- a/Mastodon/Scene/Search/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/Search/SearchViewController.swift @@ -40,7 +40,7 @@ final class SearchViewController: UIViewController, NeedsDependency { var searchTransitionController = SearchTransitionController() var disposeBag = Set() - private(set) lazy var viewModel = SearchViewModel(context: context, coordinator: coordinator) + private(set) lazy var viewModel = SearchViewModel(context: context) // recommend let scrollView: UIScrollView = { @@ -167,7 +167,12 @@ extension SearchViewController { private func setupDataSource() { 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 + ) } } diff --git a/Mastodon/Scene/Search/Search/SearchViewModel.swift b/Mastodon/Scene/Search/Search/SearchViewModel.swift index 4929ccca4..1c2456091 100644 --- a/Mastodon/Scene/Search/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/Search/SearchViewModel.swift @@ -19,24 +19,25 @@ final class SearchViewModel: NSObject { // input let context: AppContext - weak var coordinator: SceneCoordinator! - - let currentMastodonUser = CurrentValueSubject(nil) let viewDidAppeared = PassthroughSubject() // output + let currentMastodonUser = CurrentValueSubject(nil) - // var recommendHashTags = [Mastodon.Entity.Tag]() var recommendAccounts = [NSManagedObjectID]() var recommendAccountsFallback = PassthroughSubject() var hashtagDiffableDataSource: UICollectionViewDiffableDataSource? var accountDiffableDataSource: UICollectionViewDiffableDataSource? - init(context: AppContext, coordinator: SceneCoordinator) { - self.coordinator = coordinator + init(context: AppContext) { self.context = context super.init() + + context.authenticationService.activeMastodonAuthentication + .map { $0?.user } + .assign(to: \.value, on: currentMastodonUser) + .store(in: &disposeBag) Publishers.CombineLatest( context.authenticationService.activeMastodonAuthenticationBox,