Copy condensed version of user-view to collection-view in search-history (IOS-141)

This commit is contained in:
Nathan Mattes 2023-09-18 17:05:29 +02:00
parent a7bab76f96
commit ce37a8eb47
8 changed files with 175 additions and 167 deletions

View File

@ -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 = "<group>"; };
DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySectionHeaderCollectionReusableView.swift; sourceTree = "<group>"; };
DB63F7532799491600455B82 /* DataSourceFacade+SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+SearchHistory.swift"; sourceTree = "<group>"; };
DB63F759279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryUserCollectionViewCell+ViewModel.swift"; sourceTree = "<group>"; };
DB63F76127996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryViewController+DataSourceProvider.swift"; sourceTree = "<group>"; };
DB63F763279A5E3C00455B82 /* NotificationTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTimelineViewController.swift; sourceTree = "<group>"; };
DB63F766279A5EB300455B82 /* NotificationTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTimelineViewModel.swift; sourceTree = "<group>"; };
@ -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 */,

View File

@ -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)
}
}

View File

@ -1,61 +1,171 @@
//
// 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
final class SearchHistoryUserCollectionViewCell: UICollectionViewCell {
class SearchHistoryUserCollectionViewCell: UICollectionViewCell {
static let reuseIdentifier = "SearchHistoryUserCollectionViewCell"
var _disposeBag = Set<AnyCancellable>()
private static var metricFormatter = MastodonMetricFormatter()
let userView = UserView()
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)
}
override func prepareForReuse() {
super.prepareForReuse()
userView.prepareForReuse()
avatarImageView.prepareForReuse()
}
override init(frame: CGRect) {
super.init(frame: frame)
_init()
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)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
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)
}
}
verifiedLinkLabel.configure(content: verifiedLinkMetaContent)
} else {
verifiedLinkImageView.image = UIImage(systemName: "questionmark.circle")
verifiedLinkImageView.tintColor = .secondaryLabel
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()
verifiedLinkLabel.configure(content: PlaintextMetaContent(string: L10n.Common.UserList.noVerifiedLink))
}
.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)
}
override func updateConfiguration(using state: UICellConfigurationState) {
@ -65,10 +175,13 @@ extension SearchHistoryUserCollectionViewCell {
backgroundConfiguration.backgroundColorTransformer = .init { _ in
if state.isHighlighted || state.isSelected {
return ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor
}
} else {
return ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
}
self.backgroundConfiguration = backgroundConfiguration
}
self.backgroundConfiguration = backgroundConfiguration
}
}

View File

@ -31,16 +31,7 @@ extension SearchHistorySection {
let userCellRegister = UICollectionView.CellRegistration<SearchHistoryUserCollectionViewCell, ManagedObjectRecord<MastodonUser>> { 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)
}
}

View File

@ -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) } }

View File

@ -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(

View File

@ -548,3 +548,10 @@ extension MastodonUser: AutoUpdatableObject {
}
}
}
extension MastodonUser {
public var verifiedLink: MastodonField? {
let firstVerified = fields.first(where: { $0.verifiedAt != nil })
return firstVerified
}
}

View File

@ -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