feat: add custom emoji and Dynamic Type supports for familiar followers component

This commit is contained in:
CMK 2022-05-17 17:40:19 +08:00
parent bfd892e84e
commit ce59a18d3e
7 changed files with 106 additions and 32 deletions

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
import CoreDataStack import CoreDataStack
import MastodonMeta import MastodonMeta
import MastodonSDK
extension Collection where Element == MastodonEmoji { extension Collection where Element == MastodonEmoji {
public var asDictionary: MastodonContent.Emojis { public var asDictionary: MastodonContent.Emojis {
@ -18,3 +19,13 @@ extension Collection where Element == MastodonEmoji {
return dictionary return dictionary
} }
} }
extension Collection where Element == Mastodon.Entity.Emoji {
public var asDictionary: MastodonContent.Emojis {
var dictionary: MastodonContent.Emojis = [:]
for emoji in self {
dictionary[emoji.shortcode] = emoji.url
}
return dictionary
}
}

View File

@ -22,6 +22,7 @@ extension MetaLabel {
case profileFieldValue case profileFieldValue
case profileCardName case profileCardName
case profileCardUsername case profileCardUsername
case profileCardFamiliarFollowerFooter
case recommendAccountName case recommendAccountName
case titleView case titleView
case settingTableFooter case settingTableFooter
@ -90,6 +91,14 @@ extension MetaLabel {
font = .systemFont(ofSize: 15, weight: .regular) font = .systemFont(ofSize: 15, weight: .regular)
textColor = Asset.Colors.Label.secondary.color textColor = Asset.Colors.Label.secondary.color
case .profileCardFamiliarFollowerFooter:
font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .regular), maximumPointSize: 26)
textColor = Asset.Colors.Label.secondary.color
numberOfLines = 2
textContainer.maximumNumberOfLines = 2
paragraphStyle.lineSpacing = 0
paragraphStyle.paragraphSpacing = 0
case .titleView: case .titleView:
font = .systemFont(ofSize: 17, weight: .semibold) font = .systemFont(ofSize: 17, weight: .semibold)
textColor = Asset.Colors.Label.primary.color textColor = Asset.Colors.Label.primary.color
@ -106,18 +115,23 @@ extension MetaLabel {
numberOfLines = 0 numberOfLines = 0
textContainer.maximumNumberOfLines = 0 textContainer.maximumNumberOfLines = 0
paragraphStyle.alignment = .center paragraphStyle.alignment = .center
case .autoCompletion: case .autoCompletion:
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22) font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)
textColor = Asset.Colors.brandBlue.color textColor = Asset.Colors.brandBlue.color
case .accountListName: case .accountListName:
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 22) font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 22)
textColor = Asset.Colors.Label.primary.color textColor = Asset.Colors.Label.primary.color
case .accountListUsername: case .accountListUsername:
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular), maximumPointSize: 20) font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular), maximumPointSize: 20)
textColor = Asset.Colors.Label.secondary.color textColor = Asset.Colors.Label.secondary.color
case .sidebarHeadline(let isSelected): case .sidebarHeadline(let isSelected):
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 22, weight: .regular), maximumPointSize: 20) font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 22, weight: .regular), maximumPointSize: 20)
textColor = isSelected ? .white : Asset.Colors.Label.primary.color textColor = isSelected ? .white : Asset.Colors.Label.primary.color
case .sidebarSubheadline(let isSelected): case .sidebarSubheadline(let isSelected):
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 13, weight: .regular), maximumPointSize: 18) font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 13, weight: .regular), maximumPointSize: 18)
textColor = isSelected ? .white : Asset.Colors.Label.secondary.color textColor = isSelected ? .white : Asset.Colors.Label.secondary.color

View File

@ -16,5 +16,12 @@ extension FamiliarFollowersDashboardView {
viewModel.avatarURLs = accounts.map { $0.avatarImageURL() } viewModel.avatarURLs = accounts.map { $0.avatarImageURL() }
viewModel.names = accounts.map { $0.displayNameWithFallback } viewModel.names = accounts.map { $0.displayNameWithFallback }
viewModel.emojis = {
var array: [Mastodon.Entity.Emoji] = []
for account in accounts {
array.append(contentsOf: account.emojis ?? [])
}
return array.asDictionary
}()
} }
} }

View File

@ -8,6 +8,8 @@
import os.log import os.log
import UIKit import UIKit
import Combine import Combine
import CoreDataStack
import MastodonMeta
extension FamiliarFollowersDashboardView { extension FamiliarFollowersDashboardView {
public final class ViewModel: ObservableObject { public final class ViewModel: ObservableObject {
@ -17,47 +19,86 @@ extension FamiliarFollowersDashboardView {
@Published var avatarURLs: [URL?] = [] @Published var avatarURLs: [URL?] = []
@Published var names: [String] = [] @Published var names: [String] = []
@Published var emojis: MastodonContent.Emojis = [:]
@Published var backgroundColor: UIColor? @Published var backgroundColor: UIColor?
} }
} }
extension FamiliarFollowersDashboardView.ViewModel { extension FamiliarFollowersDashboardView.ViewModel {
func bind(view: FamiliarFollowersDashboardView) { func bind(view: FamiliarFollowersDashboardView) {
Publishers.CombineLatest( Publishers.CombineLatest3(
$avatarURLs, $avatarURLs,
$backgroundColor $backgroundColor,
UIContentSizeCategory.publisher
) )
.sink { avatarURLs, backgroundColor in .sink { avatarURLs, backgroundColor, contentSizeCategory in
view.avatarContainerView.subviews.forEach { $0.removeFromSuperview() } view.avatarContainerView.subviews.forEach { $0.removeFromSuperview() }
let initialOffset = min(12 * 1.5, UIFontMetrics(forTextStyle: .headline).scaledValue(for: 12)) // max 1.5x
let offset = min(20 * 1.5, UIFontMetrics(forTextStyle: .headline).scaledValue(for: 20))
let dimension = min(32 * 1.5, UIFontMetrics(forTextStyle: .headline).scaledValue(for: 32))
let borderWidth = min(1.5, UIFontMetrics.default.scaledValue(for: 1))
for (i, avatarURL) in avatarURLs.enumerated() { for (i, avatarURL) in avatarURLs.enumerated() {
let avatarButton = AvatarButton() let avatarButton = AvatarButton()
let origin = CGPoint(x: 20 * i, y: 0) let origin = CGPoint(x: offset * CGFloat(i), y: 0)
let size = CGSize(width: 32, height: 32) let size = CGSize(width: dimension, height: dimension)
avatarButton.size = size avatarButton.size = size
avatarButton.frame = CGRect(origin: origin, size: size) avatarButton.frame = CGRect(origin: origin, size: size)
view.avatarContainerView.addSubview(avatarButton) view.avatarContainerView.addSubview(avatarButton)
avatarButton.avatarImageView.configure(configuration: .init(url: avatarURL)) avatarButton.avatarImageView.configure(
configuration: .init(
url: avatarURL,
placeholder: .placeholder(color: .systemGray3)
)
)
avatarButton.avatarImageView.configure( avatarButton.avatarImageView.configure(
cornerConfiguration: .init( cornerConfiguration: .init(
corner: .fixed(radius: 7), corner: .fixed(radius: 7),
border: .init( border: .init(
color: backgroundColor ?? .clear, color: backgroundColor ?? .clear,
width: 1 width: borderWidth
) )
) )
) )
} }
view.avatarContainerViewWidthLayoutConstraint.constant = CGFloat(12 + 20 * avatarURLs.count) let avatarContainerViewWidth = initialOffset + offset * CGFloat(avatarURLs.count)
view.avatarContainerViewWidthLayoutConstraint.constant = avatarContainerViewWidth
view.avatarContainerViewHeightLayoutConstraint.constant = dimension
} }
.store(in: &disposeBag) .store(in: &disposeBag)
$names Publishers.CombineLatest(
.sink { names in $names,
// TODO: i18n $emojis
let description = "Followed by" + names.joined(separator: ", ") )
view.descriptionLabel.text = description .sink { names, emojis in
} let content: String = {
.store(in: &disposeBag) guard names.count > 0 else { return " " }
let count = names.count
let firstTwoNames = names.prefix(2).joined(separator: ", ")
switch names.count {
case 1..<3:
return "Followed by \(firstTwoNames)"
case 3:
return "Followed by \(firstTwoNames), and another mutual"
default:
let remains = count - 2
return "Followed by \(firstTwoNames), and \(remains) mutuals"
}
}()
let document = MastodonContent(content: content, emojis: emojis)
do {
let metaContent = try MastodonMetaContent.convert(document: document)
view.descriptionMetaLabel.configure(content: metaContent)
} catch {
assertionFailure()
view.descriptionMetaLabel.configure(content: PlaintextMetaContent(string: content))
}
}
.store(in: &disposeBag)
} }
} }

View File

@ -7,20 +7,15 @@
import UIKit import UIKit
import MastodonAsset import MastodonAsset
import MetaTextKit
public final class FamiliarFollowersDashboardView: UIView { public final class FamiliarFollowersDashboardView: UIView {
let avatarContainerView = UIView() let avatarContainerView = UIView()
var avatarContainerViewWidthLayoutConstraint: NSLayoutConstraint! var avatarContainerViewWidthLayoutConstraint: NSLayoutConstraint!
var avatarContainerViewHeightLayoutConstraint: NSLayoutConstraint!
let descriptionLabel: UILabel = { let descriptionMetaLabel = MetaLabel(style: .profileCardFamiliarFollowerFooter)
let label = UILabel()
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 13, weight: .regular))
label.text = "Followed by Pixelflowers, Lees Food, and 4 other mutuals"
label.textColor = Asset.Colors.Label.secondary.color
label.numberOfLines = 0
return label
}()
public private(set) lazy var viewModel: ViewModel = { public private(set) lazy var viewModel: ViewModel = {
let viewModel = ViewModel() let viewModel = ViewModel()
@ -28,10 +23,6 @@ public final class FamiliarFollowersDashboardView: UIView {
return viewModel return viewModel
}() }()
public func prepareForReuse() {
}
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
_init() _init()
@ -49,6 +40,7 @@ extension FamiliarFollowersDashboardView {
private func _init() { private func _init() {
let stackView = UIStackView() let stackView = UIStackView()
stackView.axis = .horizontal stackView.axis = .horizontal
stackView.alignment = .center
stackView.spacing = 8 stackView.spacing = 8
stackView.translatesAutoresizingMaskIntoConstraints = false stackView.translatesAutoresizingMaskIntoConstraints = false
@ -63,11 +55,14 @@ extension FamiliarFollowersDashboardView {
avatarContainerView.translatesAutoresizingMaskIntoConstraints = false avatarContainerView.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(avatarContainerView) stackView.addArrangedSubview(avatarContainerView)
avatarContainerViewWidthLayoutConstraint = avatarContainerView.widthAnchor.constraint(equalToConstant: 32).priority(.required - 1) avatarContainerViewWidthLayoutConstraint = avatarContainerView.widthAnchor.constraint(equalToConstant: 32).priority(.required - 1)
avatarContainerViewHeightLayoutConstraint = avatarContainerView.heightAnchor.constraint(equalToConstant: 32).priority(.required - 1)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
avatarContainerViewWidthLayoutConstraint, avatarContainerViewWidthLayoutConstraint,
avatarContainerView.heightAnchor.constraint(equalToConstant: 32).priority(.required - 1) avatarContainerViewHeightLayoutConstraint
]) ])
stackView.addArrangedSubview(descriptionLabel) stackView.addArrangedSubview(descriptionMetaLabel)
descriptionMetaLabel.setContentHuggingPriority(.required - 1, for: .vertical)
descriptionMetaLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical)
} }
} }

View File

@ -166,7 +166,8 @@ extension ProfileCardView {
authorContainerAdaptiveMarginContainerView.contentView = authorContainer authorContainerAdaptiveMarginContainerView.contentView = authorContainer
authorContainerAdaptiveMarginContainerView.margin = ProfileCardView.contentMargin authorContainerAdaptiveMarginContainerView.margin = ProfileCardView.contentMargin
container.addArrangedSubview(authorContainerAdaptiveMarginContainerView) container.addArrangedSubview(authorContainerAdaptiveMarginContainerView)
container.setCustomSpacing(6, after: bannerContainer)
// avatarPlaceholder // avatarPlaceholder
let avatarPlaceholder = UIView() let avatarPlaceholder = UIView()
avatarPlaceholder.translatesAutoresizingMaskIntoConstraints = false avatarPlaceholder.translatesAutoresizingMaskIntoConstraints = false
@ -220,6 +221,7 @@ extension ProfileCardView {
infoContainerAdaptiveMarginContainerView.contentView = infoContainer infoContainerAdaptiveMarginContainerView.contentView = infoContainer
infoContainerAdaptiveMarginContainerView.margin = ProfileCardView.contentMargin infoContainerAdaptiveMarginContainerView.margin = ProfileCardView.contentMargin
container.addArrangedSubview(infoContainerAdaptiveMarginContainerView) container.addArrangedSubview(infoContainerAdaptiveMarginContainerView)
container.setCustomSpacing(16, after: infoContainerAdaptiveMarginContainerView)
infoContainer.addArrangedSubview(statusDashboardView) infoContainer.addArrangedSubview(statusDashboardView)
let infoContainerSpacer = UIView() let infoContainerSpacer = UIView()
@ -249,7 +251,7 @@ extension ProfileCardView {
bottomPadding.translatesAutoresizingMaskIntoConstraints = false bottomPadding.translatesAutoresizingMaskIntoConstraints = false
container.addArrangedSubview(bottomPadding) container.addArrangedSubview(bottomPadding)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
bottomPadding.heightAnchor.constraint(equalToConstant: 16).priority(.required - 10), bottomPadding.heightAnchor.constraint(equalToConstant: 8).priority(.required - 10),
]) ])
relationshipActionButton.addTarget(self, action: #selector(ProfileCardView.relationshipActionButtonDidPressed(_:)), for: .touchUpInside) relationshipActionButton.addTarget(self, action: #selector(ProfileCardView.relationshipActionButtonDidPressed(_:)), for: .touchUpInside)

View File

@ -103,7 +103,11 @@ extension AvatarImageView {
return ScaledToSizeFilter(size: self.frame.size) return ScaledToSizeFilter(size: self.frame.size)
}() }()
af.setImage(withURL: url, filter: filter) af.setImage(
withURL: url,
placeholderImage: configuration.placeholder,
filter: filter
)
} }
} }