Add a cell for profiles in search results (IOS-141)
This commit is contained in:
parent
b74f17c6b6
commit
a7bab76f96
|
@ -151,6 +151,7 @@
|
|||
D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */; };
|
||||
D8BE30B32A179E26006B8270 /* SuggestionAccountTableViewFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */; };
|
||||
D8BEBCB62A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */; };
|
||||
D8D688F62AB869CB000F651A /* SearchResultsProfileTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */; };
|
||||
D8E5C346296DAB84007E76A7 /* DataSourceFacade+Status+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */; };
|
||||
D8E5C349296DB8A3007E76A7 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */; };
|
||||
D8F0372C29D232730027DE2E /* HashtagIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */; };
|
||||
|
@ -803,6 +804,7 @@
|
|||
D8A6FE6629325F5900666A47 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountTableViewFooter.swift; sourceTree = "<group>"; };
|
||||
D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountTableViewCell+ViewModel.swift"; sourceTree = "<group>"; };
|
||||
D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsProfileTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status+History.swift"; sourceTree = "<group>"; };
|
||||
D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = "<group>"; };
|
||||
D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagIntentHandler.swift; sourceTree = "<group>"; };
|
||||
|
@ -1811,6 +1813,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D81A227A2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift */,
|
||||
D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */,
|
||||
);
|
||||
path = Cells;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3890,6 +3893,7 @@
|
|||
DBFEEC99279BDCDE004F81DD /* ProfileAboutViewModel.swift in Sources */,
|
||||
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */,
|
||||
DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */,
|
||||
D8D688F62AB869CB000F651A /* SearchResultsProfileTableViewCell.swift in Sources */,
|
||||
2A82294F29262EE000D2A1F7 /* AppContext+NextAccount.swift in Sources */,
|
||||
DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */,
|
||||
DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import MastodonUI
|
||||
import MetaTextKit
|
||||
import MastodonLocalization
|
||||
import MastodonMeta
|
||||
import MastodonCore
|
||||
import MastodonAsset
|
||||
|
||||
class SearchResultsProfileTableViewCell: UITableViewCell {
|
||||
static let reuseIdentifier = "SearchResultsProfileTableViewCell"
|
||||
|
||||
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(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
|
||||
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(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
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()
|
||||
|
||||
avatarImageView.prepareForReuse()
|
||||
}
|
||||
|
||||
func configure(with account: Mastodon.Entity.Account) {
|
||||
let displayNameMetaContent: MetaContent
|
||||
do {
|
||||
let content = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojis?.asDictionary ?? [:])
|
||||
displayNameMetaContent = try MastodonMetaContent.convert(document: content)
|
||||
} catch {
|
||||
displayNameMetaContent = PlaintextMetaContent(string: account.displayNameWithFallback)
|
||||
}
|
||||
|
||||
displayNameLabel.configure(content: displayNameMetaContent)
|
||||
acctLabel.text = account.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: account.followersCount) ?? account.followersCount.formatted(), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))])
|
||||
)
|
||||
|
||||
avatarImageView.setImage(url: account.avatarImageURL())
|
||||
|
||||
if let verifiedLink = account.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
|
||||
|
||||
verifiedLinkLabel.configure(content: PlaintextMetaContent(string: L10n.Common.UserList.noVerifiedLink))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,11 +28,15 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc
|
|||
tableView.register(SearchResultDefaultSectionTableViewCell.self, forCellReuseIdentifier: SearchResultDefaultSectionTableViewCell.reuseIdentifier)
|
||||
tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: StatusTableViewCell.reuseIdentifier)
|
||||
tableView.register(HashtagTableViewCell.self, forCellReuseIdentifier: HashtagTableViewCell.reuseIdentifier)
|
||||
tableView.register(UserTableViewCell.self, forCellReuseIdentifier: UserTableViewCell.reuseIdentifier)
|
||||
tableView.register(SearchResultsProfileTableViewCell.self, forCellReuseIdentifier: SearchResultsProfileTableViewCell.reuseIdentifier)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
let dataSource = UITableViewDiffableDataSource<SearchResultOverviewSection, SearchResultOverviewItem>(tableView: tableView) { [weak self] tableView, indexPath, itemIdentifier in
|
||||
|
||||
guard let self else { fatalError("Ooops, no self!?") }
|
||||
|
||||
let dataSource = UITableViewDiffableDataSource<SearchResultOverviewSection, SearchResultOverviewItem>(tableView: tableView) { tableView, indexPath, itemIdentifier in
|
||||
switch itemIdentifier {
|
||||
|
||||
case .default(let item):
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultDefaultSectionTableViewCell.reuseIdentifier, for: indexPath) as? SearchResultDefaultSectionTableViewCell else { fatalError() }
|
||||
|
||||
|
@ -41,54 +45,23 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc
|
|||
return cell
|
||||
|
||||
case .suggestion(let suggestion):
|
||||
|
||||
switch suggestion {
|
||||
|
||||
case .hashtag(let hashtag):
|
||||
|
||||
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultDefaultSectionTableViewCell.reuseIdentifier, for: indexPath) as? SearchResultDefaultSectionTableViewCell else { fatalError() }
|
||||
|
||||
cell.configure(item: .hashtag(tag: hashtag))
|
||||
return cell
|
||||
|
||||
case .profile(let profile):
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as? UserTableViewCell else { fatalError() }
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultsProfileTableViewCell.reuseIdentifier, for: indexPath) as? SearchResultsProfileTableViewCell else { fatalError() }
|
||||
|
||||
let managedObjectContext = appContext.managedObjectContext
|
||||
Task {
|
||||
do {
|
||||
|
||||
try await managedObjectContext.perform {
|
||||
guard let user = Persistence.MastodonUser.fetch(in: managedObjectContext,
|
||||
context: Persistence.MastodonUser.PersistContext(
|
||||
domain: authContext.mastodonAuthenticationBox.domain,
|
||||
entity: profile,
|
||||
cache: nil,
|
||||
networkDate: Date()
|
||||
)) else { return }
|
||||
|
||||
cell.configure(
|
||||
me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user,
|
||||
tableView: tableView,
|
||||
viewModel: UserTableViewCell.ViewModel(
|
||||
user: user,
|
||||
followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(),
|
||||
blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(),
|
||||
followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()),
|
||||
delegate: nil)
|
||||
}
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
cell.configure(with: profile)
|
||||
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
tableView.dataSource = dataSource
|
||||
tableView.delegate = self
|
||||
self.dataSource = dataSource
|
||||
|
@ -349,3 +322,5 @@ extension SearchResultsOverviewTableViewController: UITableViewDelegate {
|
|||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchResultsOverviewTableViewController: UserTableViewCellDelegate {}
|
||||
|
|
|
@ -92,3 +92,10 @@ extension Mastodon.Entity.Account {
|
|||
return acct
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Account {
|
||||
public var verifiedLink: Mastodon.Entity.Field? {
|
||||
let firstVerified = fields?.first(where: { $0.verifiedAt != nil })
|
||||
return firstVerified
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,8 +39,8 @@ extension FLAnimatedImageView {
|
|||
|
||||
public func setImage(
|
||||
url: URL?,
|
||||
placeholder: UIImage?,
|
||||
scaleToSize: CGSize?
|
||||
placeholder: UIImage? = nil,
|
||||
scaleToSize: CGSize? = nil
|
||||
) {
|
||||
// cancel task
|
||||
cancelTask()
|
||||
|
|
Loading…
Reference in New Issue