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 CoreDataStack
import MastodonMeta
import MastodonSDK
extension Collection where Element == MastodonEmoji {
public var asDictionary: MastodonContent.Emojis {
@ -18,3 +19,13 @@ extension Collection where Element == MastodonEmoji {
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 profileCardName
case profileCardUsername
case profileCardFamiliarFollowerFooter
case recommendAccountName
case titleView
case settingTableFooter
@ -90,6 +91,14 @@ extension MetaLabel {
font = .systemFont(ofSize: 15, weight: .regular)
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:
font = .systemFont(ofSize: 17, weight: .semibold)
textColor = Asset.Colors.Label.primary.color
@ -106,18 +115,23 @@ extension MetaLabel {
numberOfLines = 0
textContainer.maximumNumberOfLines = 0
paragraphStyle.alignment = .center
case .autoCompletion:
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)
textColor = Asset.Colors.brandBlue.color
case .accountListName:
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 22)
textColor = Asset.Colors.Label.primary.color
case .accountListUsername:
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular), maximumPointSize: 20)
textColor = Asset.Colors.Label.secondary.color
case .sidebarHeadline(let isSelected):
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 22, weight: .regular), maximumPointSize: 20)
textColor = isSelected ? .white : Asset.Colors.Label.primary.color
case .sidebarSubheadline(let isSelected):
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 13, weight: .regular), maximumPointSize: 18)
textColor = isSelected ? .white : Asset.Colors.Label.secondary.color

View File

@ -16,5 +16,12 @@ extension FamiliarFollowersDashboardView {
viewModel.avatarURLs = accounts.map { $0.avatarImageURL() }
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 UIKit
import Combine
import CoreDataStack
import MastodonMeta
extension FamiliarFollowersDashboardView {
public final class ViewModel: ObservableObject {
@ -17,47 +19,86 @@ extension FamiliarFollowersDashboardView {
@Published var avatarURLs: [URL?] = []
@Published var names: [String] = []
@Published var emojis: MastodonContent.Emojis = [:]
@Published var backgroundColor: UIColor?
}
}
extension FamiliarFollowersDashboardView.ViewModel {
func bind(view: FamiliarFollowersDashboardView) {
Publishers.CombineLatest(
Publishers.CombineLatest3(
$avatarURLs,
$backgroundColor
$backgroundColor,
UIContentSizeCategory.publisher
)
.sink { avatarURLs, backgroundColor in
.sink { avatarURLs, backgroundColor, contentSizeCategory in
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() {
let avatarButton = AvatarButton()
let origin = CGPoint(x: 20 * i, y: 0)
let size = CGSize(width: 32, height: 32)
let origin = CGPoint(x: offset * CGFloat(i), y: 0)
let size = CGSize(width: dimension, height: dimension)
avatarButton.size = size
avatarButton.frame = CGRect(origin: origin, size: size)
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(
cornerConfiguration: .init(
corner: .fixed(radius: 7),
border: .init(
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)
$names
.sink { names in
// TODO: i18n
let description = "Followed by" + names.joined(separator: ", ")
view.descriptionLabel.text = description
}
.store(in: &disposeBag)
Publishers.CombineLatest(
$names,
$emojis
)
.sink { names, emojis in
let content: String = {
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 MastodonAsset
import MetaTextKit
public final class FamiliarFollowersDashboardView: UIView {
let avatarContainerView = UIView()
var avatarContainerViewWidthLayoutConstraint: NSLayoutConstraint!
var avatarContainerViewHeightLayoutConstraint: NSLayoutConstraint!
let descriptionLabel: UILabel = {
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
}()
let descriptionMetaLabel = MetaLabel(style: .profileCardFamiliarFollowerFooter)
public private(set) lazy var viewModel: ViewModel = {
let viewModel = ViewModel()
@ -28,10 +23,6 @@ public final class FamiliarFollowersDashboardView: UIView {
return viewModel
}()
public func prepareForReuse() {
}
override init(frame: CGRect) {
super.init(frame: frame)
_init()
@ -49,6 +40,7 @@ extension FamiliarFollowersDashboardView {
private func _init() {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.alignment = .center
stackView.spacing = 8
stackView.translatesAutoresizingMaskIntoConstraints = false
@ -63,11 +55,14 @@ extension FamiliarFollowersDashboardView {
avatarContainerView.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(avatarContainerView)
avatarContainerViewWidthLayoutConstraint = avatarContainerView.widthAnchor.constraint(equalToConstant: 32).priority(.required - 1)
avatarContainerViewHeightLayoutConstraint = avatarContainerView.heightAnchor.constraint(equalToConstant: 32).priority(.required - 1)
NSLayoutConstraint.activate([
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.margin = ProfileCardView.contentMargin
container.addArrangedSubview(authorContainerAdaptiveMarginContainerView)
container.setCustomSpacing(6, after: bannerContainer)
// avatarPlaceholder
let avatarPlaceholder = UIView()
avatarPlaceholder.translatesAutoresizingMaskIntoConstraints = false
@ -220,6 +221,7 @@ extension ProfileCardView {
infoContainerAdaptiveMarginContainerView.contentView = infoContainer
infoContainerAdaptiveMarginContainerView.margin = ProfileCardView.contentMargin
container.addArrangedSubview(infoContainerAdaptiveMarginContainerView)
container.setCustomSpacing(16, after: infoContainerAdaptiveMarginContainerView)
infoContainer.addArrangedSubview(statusDashboardView)
let infoContainerSpacer = UIView()
@ -249,7 +251,7 @@ extension ProfileCardView {
bottomPadding.translatesAutoresizingMaskIntoConstraints = false
container.addArrangedSubview(bottomPadding)
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)

View File

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