mastodon-ios/Mastodon/Scene/Compose/View/ReplicaStatusView.swift

261 lines
12 KiB
Swift

//
// ReplicaStatusView.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-6-29.
//
import os.log
import UIKit
import FLAnimatedImage
import MetaTextKit
final class ReplicaStatusView: UIView {
static let avatarImageSize = CGSize(width: 42, height: 42)
static let avatarImageCornerRadius: CGFloat = 4
static let avatarToLabelSpacing: CGFloat = 5
static let contentWarningBlurRadius: CGFloat = 12
static let containerStackViewSpacing: CGFloat = 10
let containerStackView = UIStackView()
let headerContainerView = UIView()
let authorContainerView = UIView()
static let reblogIconImage: UIImage = {
let font = UIFont.systemFont(ofSize: 13, weight: .medium)
let configuration = UIImage.SymbolConfiguration(font: font)
let image = UIImage(systemName: "arrow.2.squarepath", withConfiguration: configuration)!.withTintColor(Asset.Colors.Label.secondary.color)
return image
}()
static let replyIconImage: UIImage = {
let font = UIFont.systemFont(ofSize: 13, weight: .medium)
let configuration = UIImage.SymbolConfiguration(font: font)
let image = UIImage(systemName: "arrowshape.turn.up.left.fill", withConfiguration: configuration)!.withTintColor(Asset.Colors.Label.secondary.color)
return image
}()
static func iconAttributedString(image: UIImage) -> NSAttributedString {
let attributedString = NSMutableAttributedString()
let imageTextAttachment = NSTextAttachment()
let imageAttribute = NSAttributedString(attachment: imageTextAttachment)
imageTextAttachment.image = image
attributedString.append(imageAttribute)
return attributedString
}
let headerIconLabel: UILabel = {
let label = UILabel()
label.attributedText = ReplicaStatusView.iconAttributedString(image: ReplicaStatusView.reblogIconImage)
return label
}()
let headerInfoLabel = MetaLabel(style: .statusHeader)
let avatarView: UIView = {
let view = UIView()
view.isAccessibilityElement = true
view.accessibilityTraits = .button
view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile
return view
}()
let avatarImageView = FLAnimatedImageView()
let nameLabel = MetaLabel(style: .statusName)
let nameTrialingDotLabel: UILabel = {
let label = UILabel()
label.textColor = Asset.Colors.Label.secondary.color
label.font = .systemFont(ofSize: 17)
label.text = "·"
label.isAccessibilityElement = false
return label
}()
let usernameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 15, weight: .regular)
label.textColor = Asset.Colors.Label.secondary.color
label.text = "@alice"
label.isAccessibilityElement = false
return label
}()
let dateLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 13, weight: .regular)
label.textColor = Asset.Colors.Label.secondary.color
label.text = "1d"
return label
}()
let contentMetaText: MetaText = {
let metaText = MetaText()
metaText.textView.backgroundColor = .clear
metaText.textView.isEditable = false
metaText.textView.isSelectable = false
metaText.textView.isScrollEnabled = false
metaText.textView.textContainer.lineFragmentPadding = 0
metaText.textView.textContainerInset = .zero
metaText.textView.layer.masksToBounds = false
metaText.paragraphStyle = {
let style = NSMutableParagraphStyle()
style.lineSpacing = 5
style.paragraphSpacing = 8
return style
}()
metaText.textAttributes = [
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)),
.foregroundColor: Asset.Colors.Label.primary.color,
]
metaText.linkAttributes = [
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)),
.foregroundColor: Asset.Colors.brandBlue.color,
]
return metaText
}()
let statusContainerStackView = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension ReplicaStatusView {
private func _init() {
// container: [reblog | author | status | action toolbar]
// note: do not set spacing for nested stackView to avoid SDK layout conflict issue
containerStackView.axis = .vertical
// containerStackView.spacing = 10
containerStackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(containerStackView)
NSLayoutConstraint.activate([
containerStackView.topAnchor.constraint(equalTo: topAnchor),
containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
trailingAnchor.constraint(equalTo: containerStackView.trailingAnchor),
bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor),
])
containerStackView.setContentHuggingPriority(.required - 1, for: .vertical)
containerStackView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
// header container: [icon | info]
let headerContainerStackView = UIStackView()
headerContainerStackView.axis = .horizontal
headerContainerStackView.spacing = 4
headerContainerStackView.addArrangedSubview(headerIconLabel)
headerContainerStackView.addArrangedSubview(headerInfoLabel)
headerIconLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
headerContainerStackView.translatesAutoresizingMaskIntoConstraints = false
headerContainerView.addSubview(headerContainerStackView)
NSLayoutConstraint.activate([
headerContainerStackView.topAnchor.constraint(equalTo: headerContainerView.topAnchor),
headerContainerStackView.leadingAnchor.constraint(equalTo: headerContainerView.leadingAnchor),
headerContainerStackView.trailingAnchor.constraint(equalTo: headerContainerView.trailingAnchor),
headerContainerView.bottomAnchor.constraint(equalTo: headerContainerStackView.bottomAnchor, constant: ReplicaStatusView.containerStackViewSpacing).priority(.defaultHigh),
])
containerStackView.addArrangedSubview(headerContainerView)
defer {
containerStackView.bringSubviewToFront(headerContainerView)
}
// author container: [avatar | author meta container | reveal button]
let authorContainerStackView = UIStackView()
authorContainerStackView.axis = .horizontal
authorContainerStackView.spacing = ReplicaStatusView.avatarToLabelSpacing
authorContainerStackView.distribution = .fill
// avatar
avatarView.translatesAutoresizingMaskIntoConstraints = false
authorContainerStackView.addArrangedSubview(avatarView)
NSLayoutConstraint.activate([
avatarView.widthAnchor.constraint(equalToConstant: ReplicaStatusView.avatarImageSize.width).priority(.required - 1),
avatarView.heightAnchor.constraint(equalToConstant: ReplicaStatusView.avatarImageSize.height).priority(.required - 1),
])
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
avatarView.addSubview(avatarImageView)
NSLayoutConstraint.activate([
avatarImageView.topAnchor.constraint(equalTo: avatarView.topAnchor),
avatarImageView.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor),
avatarImageView.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor),
avatarImageView.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor),
])
// author meta container: [title container | subtitle container]
let authorMetaContainerStackView = UIStackView()
authorContainerStackView.addArrangedSubview(authorMetaContainerStackView)
authorMetaContainerStackView.axis = .vertical
authorMetaContainerStackView.spacing = 4
// title container: [display name | "·" | date | padding]
let titleContainerStackView = UIStackView()
authorMetaContainerStackView.addArrangedSubview(titleContainerStackView)
titleContainerStackView.axis = .horizontal
titleContainerStackView.spacing = 4
nameLabel.translatesAutoresizingMaskIntoConstraints = false
titleContainerStackView.addArrangedSubview(nameLabel)
NSLayoutConstraint.activate([
nameLabel.heightAnchor.constraint(equalToConstant: 22).priority(.defaultHigh),
])
titleContainerStackView.alignment = .firstBaseline
titleContainerStackView.addArrangedSubview(nameTrialingDotLabel)
titleContainerStackView.addArrangedSubview(dateLabel)
let padding = UIView()
titleContainerStackView.addArrangedSubview(padding) // padding
nameLabel.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
nameTrialingDotLabel.setContentHuggingPriority(.defaultHigh + 2, for: .horizontal)
nameTrialingDotLabel.setContentCompressionResistancePriority(.required - 2, for: .horizontal)
dateLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
dateLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
padding.setContentHuggingPriority(.defaultLow - 1, for: .horizontal)
padding.setContentCompressionResistancePriority(.defaultLow - 1, for: .horizontal)
// subtitle container: [username]
let subtitleContainerStackView = UIStackView()
authorMetaContainerStackView.addArrangedSubview(subtitleContainerStackView)
subtitleContainerStackView.axis = .horizontal
subtitleContainerStackView.addArrangedSubview(usernameLabel)
authorContainerStackView.translatesAutoresizingMaskIntoConstraints = false
authorContainerView.addSubview(authorContainerStackView)
NSLayoutConstraint.activate([
authorContainerStackView.topAnchor.constraint(equalTo: authorContainerView.topAnchor),
authorContainerStackView.leadingAnchor.constraint(equalTo: authorContainerView.leadingAnchor),
authorContainerStackView.trailingAnchor.constraint(equalTo: authorContainerView.trailingAnchor),
authorContainerView.bottomAnchor.constraint(equalTo: authorContainerStackView.bottomAnchor, constant: ReplicaStatusView.containerStackViewSpacing).priority(.defaultHigh),
])
containerStackView.addArrangedSubview(authorContainerView)
// status container: [status]
containerStackView.addArrangedSubview(statusContainerStackView)
statusContainerStackView.axis = .vertical
statusContainerStackView.spacing = 10
// avoid overlay behind other views
defer {
containerStackView.bringSubviewToFront(authorContainerView)
}
// status
statusContainerStackView.addArrangedSubview(contentMetaText.textView)
contentMetaText.textView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
}
}
// MARK: - AvatarConfigurableView
extension ReplicaStatusView: AvatarConfigurableView {
static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize }
static var configurableAvatarImageCornerRadius: CGFloat { return 4 }
var configurableAvatarImageView: FLAnimatedImageView? { avatarImageView }
}