From ce37a8eb47c42cc36b2df9544cade88bf45c01f5 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 18 Sep 2023 17:05:29 +0200 Subject: [PATCH] Copy condensed version of user-view to collection-view in search-history (IOS-141) --- Mastodon.xcodeproj/project.pbxproj | 4 - ...toryUserCollectionViewCell+ViewModel.swift | 73 ------ .../SearchHistoryUserCollectionViewCell.swift | 219 +++++++++++++----- .../SearchHistory/SearchHistorySection.swift | 11 +- .../SearchHistoryViewController.swift | 2 - .../SearchResult/SearchResultSection.swift | 2 +- .../Entity/Mastodon/MastodonUser.swift | 7 + .../MastodonUI/View/Button/AvatarButton.swift | 24 -- 8 files changed, 175 insertions(+), 167 deletions(-) delete mode 100644 Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index f4936e22f..a61ad3f16 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -300,7 +300,6 @@ DB63F74F2799405600455B82 /* SearchHistoryViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */; }; DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */; }; DB63F7542799491600455B82 /* DataSourceFacade+SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F7532799491600455B82 /* DataSourceFacade+SearchHistory.swift */; }; - DB63F75A279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F759279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift */; }; DB63F76227996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F76127996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift */; }; DB63F764279A5E3C00455B82 /* NotificationTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F763279A5E3C00455B82 /* NotificationTimelineViewController.swift */; }; DB63F767279A5EB300455B82 /* NotificationTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F766279A5EB300455B82 /* NotificationTimelineViewModel.swift */; }; @@ -993,7 +992,6 @@ DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryViewModel+Diffable.swift"; sourceTree = ""; }; DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySectionHeaderCollectionReusableView.swift; sourceTree = ""; }; DB63F7532799491600455B82 /* DataSourceFacade+SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+SearchHistory.swift"; sourceTree = ""; }; - DB63F759279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryUserCollectionViewCell+ViewModel.swift"; sourceTree = ""; }; DB63F76127996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryViewController+DataSourceProvider.swift"; sourceTree = ""; }; DB63F763279A5E3C00455B82 /* NotificationTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTimelineViewController.swift; sourceTree = ""; }; DB63F766279A5EB300455B82 /* NotificationTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTimelineViewModel.swift; sourceTree = ""; }; @@ -2295,7 +2293,6 @@ isa = PBXGroup; children = ( DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */, - DB63F759279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift */, DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */, ); path = Cell; @@ -3560,7 +3557,6 @@ DB0FCB7427956939006C02E2 /* DataSourceFacade+Status.swift in Sources */, D81A22782AB4782400905D71 /* SearchResultOverviewSection.swift in Sources */, DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */, - DB63F75A279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift in Sources */, DB023D26279FFB0A005AC798 /* ShareActivityProvider.swift in Sources */, 5D0393962612D266007FE196 /* WebViewModel.swift in Sources */, 5B24BBDA262DB14800A9381B /* ReportViewModel.swift in Sources */, diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift deleted file mode 100644 index 8e9710e9a..000000000 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell+ViewModel.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// SearchHistoryUserCollectionViewCell+ViewModel.swift -// Mastodon -// -// Created by MainasuK on 2022-1-20. -// - -import Foundation -import CoreDataStack -import MastodonUI -import Combine - -extension SearchHistoryUserCollectionViewCell { - final class ViewModel { - let value: MastodonUser - - let followedUsers: AnyPublisher<[String], Never> - let blockedUsers: AnyPublisher<[String], Never> - let followRequestedUsers: AnyPublisher<[String], Never> - - init(value: MastodonUser, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>, followRequestedUsers: AnyPublisher<[String], Never>) { - self.value = value - self.followedUsers = followedUsers - self.followRequestedUsers = followRequestedUsers - self.blockedUsers = blockedUsers - } - } -} - -extension SearchHistoryUserCollectionViewCell { - func configure( - me: MastodonUser?, - viewModel: ViewModel, - delegate: SearchHistorySectionHeaderCollectionReusableViewDelegate? - ) { - let user = viewModel.value - - userView.configure(user: user, delegate: delegate) - - guard let me = me else { - return userView.setButtonState(.none) - } - - if user == me { - userView.setButtonState(.none) - } else { - userView.setButtonState(.loading) - } - - Publishers.CombineLatest3( - viewModel.followedUsers, - viewModel.followRequestedUsers, - viewModel.blockedUsers - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] followed, requested, blocked in - if user == me { - self?.userView.setButtonState(.none) - } else if blocked.contains(user.id) { - self?.userView.setButtonState(.blocked) - } else if followed.contains(user.id) { - self?.userView.setButtonState(.unfollow) - } else if requested.contains(user.id) { - self?.userView.setButtonState(.pending) - } else if user.locked { - self?.userView.setButtonState(.request) - } else if user != me { - self?.userView.setButtonState(.follow) - } - } - .store(in: &_disposeBag) - } -} diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift index 1da7075f2..3e655417c 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/Cell/SearchHistoryUserCollectionViewCell.swift @@ -1,74 +1,187 @@ -// -// SearchHistoryUserCollectionViewCell.swift -// Mastodon -// -// Created by MainasuK on 2022-1-20. -// +// Copyright © 2023 Mastodon gGmbH. All rights reserved. import UIKit -import Combine -import MastodonCore +import MastodonSDK import MastodonUI +import MetaTextKit +import MastodonLocalization +import MastodonMeta +import MastodonCore +import MastodonAsset +import CoreDataStack + +class SearchHistoryUserCollectionViewCell: UICollectionViewCell { + static let reuseIdentifier = "SearchHistoryUserCollectionViewCell" + + private static var metricFormatter = MastodonMetricFormatter() + + private let avatarImageWrapperView: UIView + let avatarImageView: AvatarImageView + + private let metaInformationStackView: UIStackView + + private let upperLineStackView: UIStackView + let displayNameLabel: MetaLabel + let acctLabel: UILabel + + private let lowerLineStackView: UIStackView + let followersLabel: UILabel + let verifiedLinkImageView: UIImageView + let verifiedLinkLabel: MetaLabel + + private let contentStackView: UIStackView + + override init(frame: CGRect) { + avatarImageView = AvatarImageView() + avatarImageView.cornerConfiguration = AvatarImageView.CornerConfiguration(corner: .fixed(radius: 8)) + avatarImageView.translatesAutoresizingMaskIntoConstraints = false + + avatarImageWrapperView = UIView() + avatarImageWrapperView.translatesAutoresizingMaskIntoConstraints = false + avatarImageWrapperView.addSubview(avatarImageView) + + displayNameLabel = MetaLabel(style: .statusName) + displayNameLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + displayNameLabel.setContentHuggingPriority(.required, for: .horizontal) + + acctLabel = UILabel() + acctLabel.textColor = .secondaryLabel + acctLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) + acctLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + + upperLineStackView = UIStackView(arrangedSubviews: [displayNameLabel, acctLabel]) + upperLineStackView.distribution = .fill + upperLineStackView.alignment = .center + + followersLabel = UILabel() + followersLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + followersLabel.textColor = .secondaryLabel + followersLabel.setContentHuggingPriority(.required, for: .horizontal) + + verifiedLinkImageView = UIImageView() + verifiedLinkImageView.setContentCompressionResistancePriority(.defaultHigh - 1, for: .vertical) + verifiedLinkImageView.setContentHuggingPriority(.required, for: .horizontal) + verifiedLinkImageView.contentMode = .scaleAspectFit + + verifiedLinkLabel = MetaLabel(style: .profileFieldValue) + verifiedLinkLabel.setContentCompressionResistancePriority(.defaultHigh - 2, for: .horizontal) + verifiedLinkLabel.translatesAutoresizingMaskIntoConstraints = false + verifiedLinkLabel.textAttributes = [ + .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)), + .foregroundColor: UIColor.secondaryLabel + ] + verifiedLinkLabel.linkAttributes = [ + .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)), + .foregroundColor: Asset.Colors.Brand.blurple.color + ] + verifiedLinkLabel.isUserInteractionEnabled = false + + lowerLineStackView = UIStackView(arrangedSubviews: [followersLabel, verifiedLinkImageView, verifiedLinkLabel]) + lowerLineStackView.distribution = .fill + lowerLineStackView.alignment = .center + lowerLineStackView.spacing = 4 + lowerLineStackView.setCustomSpacing(2, after: verifiedLinkImageView) + + metaInformationStackView = UIStackView(arrangedSubviews: [upperLineStackView, lowerLineStackView]) + metaInformationStackView.axis = .vertical + metaInformationStackView.alignment = .leading + + contentStackView = UIStackView(arrangedSubviews: [avatarImageWrapperView, metaInformationStackView]) + contentStackView.translatesAutoresizingMaskIntoConstraints = false + contentStackView.axis = .horizontal + contentStackView.alignment = .center + contentStackView.spacing = 16 + + super.init(frame: .zero) + + contentView.addSubview(contentStackView) + setupConstraints() + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + private func setupConstraints() { + let constraints = [ + contentStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + contentStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + contentView.trailingAnchor.constraint(greaterThanOrEqualTo: contentStackView.trailingAnchor, constant: 16), + contentView.bottomAnchor.constraint(equalTo: contentStackView.bottomAnchor, constant: 8), + + upperLineStackView.trailingAnchor.constraint(greaterThanOrEqualTo: metaInformationStackView.trailingAnchor), + lowerLineStackView.trailingAnchor.constraint(greaterThanOrEqualTo: metaInformationStackView.trailingAnchor), + metaInformationStackView.trailingAnchor.constraint(greaterThanOrEqualTo: contentStackView.trailingAnchor), + + avatarImageView.widthAnchor.constraint(equalToConstant: 30), + avatarImageView.heightAnchor.constraint(equalTo: avatarImageView.widthAnchor), + avatarImageView.topAnchor.constraint(greaterThanOrEqualTo: avatarImageWrapperView.topAnchor), + avatarImageView.leadingAnchor.constraint(equalTo: avatarImageWrapperView.leadingAnchor), + avatarImageWrapperView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor), + avatarImageWrapperView.bottomAnchor.constraint(greaterThanOrEqualTo: avatarImageView.bottomAnchor), + avatarImageView.centerYAnchor.constraint(equalTo: avatarImageWrapperView.centerYAnchor), + ] + + NSLayoutConstraint.activate(constraints) + } -final class SearchHistoryUserCollectionViewCell: UICollectionViewCell { - - var _disposeBag = Set() - - let userView = UserView() - override func prepareForReuse() { super.prepareForReuse() - - userView.prepareForReuse() - } - - override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} -extension SearchHistoryUserCollectionViewCell { - - private func _init() { - ThemeService.shared.currentTheme - .map { $0.secondarySystemGroupedBackgroundColor } - .sink { [weak self] backgroundColor in - guard let self = self else { return } - self.backgroundColor = backgroundColor - self.setNeedsUpdateConfiguration() + avatarImageView.prepareForReuse() + } + + func configure(with user: MastodonUser) { + let displayNameMetaContent: MetaContent + do { + let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojis.asDictionary) + displayNameMetaContent = try MastodonMetaContent.convert(document: content) + } catch { + displayNameMetaContent = PlaintextMetaContent(string: user.displayNameWithFallback) + } + + displayNameLabel.configure(content: displayNameMetaContent) + acctLabel.text = user.acct + followersLabel.attributedText = NSAttributedString( + format: NSAttributedString(string: L10n.Common.UserList.followersCount("%@"), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))]), + args: NSAttributedString(string: Self.metricFormatter.string(from: Int(user.followersCount)) ?? user.followersCount.formatted(), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))]) + ) + + avatarImageView.setImage(url: user.avatarImageURL()) + + if let verifiedLink = user.verifiedLink?.value { + verifiedLinkImageView.image = UIImage(systemName: "checkmark") + verifiedLinkImageView.tintColor = Asset.Colors.Brand.blurple.color + + let verifiedLinkMetaContent: MetaContent + do { + let mastodonContent = MastodonContent(content: verifiedLink, emojis: [:]) + verifiedLinkMetaContent = try MastodonMetaContent.convert(document: mastodonContent) + } catch { + verifiedLinkMetaContent = PlaintextMetaContent(string: verifiedLink) } - .store(in: &_disposeBag) - - userView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(userView) - NSLayoutConstraint.activate([ - userView.topAnchor.constraint(equalTo: contentView.topAnchor), - userView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), - contentView.trailingAnchor.constraint(equalTo: userView.trailingAnchor, constant: 16), - userView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - ]) - userView.accessibilityTraits.insert(.button) + verifiedLinkLabel.configure(content: verifiedLinkMetaContent) + } else { + verifiedLinkImageView.image = UIImage(systemName: "questionmark.circle") + verifiedLinkImageView.tintColor = .secondaryLabel + + verifiedLinkLabel.configure(content: PlaintextMetaContent(string: L10n.Common.UserList.noVerifiedLink)) + } } - + override func updateConfiguration(using state: UICellConfigurationState) { super.updateConfiguration(using: state) - + var backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell() backgroundConfiguration.backgroundColorTransformer = .init { _ in if state.isHighlighted || state.isSelected { return ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor + } else { + return ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor } - return ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor } + self.backgroundConfiguration = backgroundConfiguration + } - } + diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift index 2c0e19451..aa43f2467 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift @@ -31,16 +31,7 @@ extension SearchHistorySection { let userCellRegister = UICollectionView.CellRegistration> { cell, indexPath, item in context.managedObjectContext.performAndWait { guard let user = item.object(in: context.managedObjectContext) else { return } - cell.configure( - me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user, - viewModel: SearchHistoryUserCollectionViewCell.ViewModel( - value: user, - followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), - blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(), - followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher() - ), - delegate: configuration.searchHistorySectionHeaderCollectionReusableViewDelegate - ) + cell.configure(with: user) } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift index d55259d03..86340feed 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift @@ -14,8 +14,6 @@ import MastodonUI final class SearchHistoryViewController: UIViewController, NeedsDependency { - let logger = Logger(subsystem: "SearchHistoryViewController", category: "ViewController") - weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift index 49eeddb32..a7b10ae1f 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift @@ -45,7 +45,7 @@ extension SearchResultSection { return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in switch item { case .user(let record): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell + let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as! UserTableViewCell context.managedObjectContext.performAndWait { guard let user = record.object(in: context.managedObjectContext) else { return } configure( diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift index fe668ccb9..6f3c5f8eb 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift @@ -548,3 +548,10 @@ extension MastodonUser: AutoUpdatableObject { } } } + +extension MastodonUser { + public var verifiedLink: MastodonField? { + let firstVerified = fields.first(where: { $0.verifiedAt != nil }) + return firstVerified + } +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Button/AvatarButton.swift b/MastodonSDK/Sources/MastodonUI/View/Button/AvatarButton.swift index ab5c8a70e..6d399c1b2 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Button/AvatarButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Button/AvatarButton.swift @@ -5,7 +5,6 @@ // Created by MainasuK Cirno on 2021-7-21. // -import os.log import UIKit import MastodonLocalization @@ -117,26 +116,3 @@ extension AvatarButton { } } - -#if canImport(SwiftUI) && DEBUG -import SwiftUI - -struct AvatarButton_Previews: PreviewProvider { - - static var previews: some View { - UIViewPreview(width: 42) { - let avatarButton = AvatarButton() - avatarButton.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - avatarButton.widthAnchor.constraint(equalToConstant: 42), - avatarButton.heightAnchor.constraint(equalToConstant: 42), - ]) - return avatarButton - } - .previewLayout(.fixed(width: 42, height: 42)) - } - -} - -#endif -