feat: add custom emoji and Dynamic Type supports for familiar followers component
This commit is contained in:
parent
bfd892e84e
commit
ce59a18d3e
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, Lee’s 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue