2021-02-23 08:16:55 +01:00
|
|
|
//
|
|
|
|
// StatusView.swift
|
|
|
|
// Mastodon
|
|
|
|
//
|
|
|
|
// Created by sxiaojian on 2021/1/28.
|
|
|
|
//
|
|
|
|
|
2021-02-24 09:11:48 +01:00
|
|
|
import os.log
|
2021-02-23 08:16:55 +01:00
|
|
|
import UIKit
|
|
|
|
import AVKit
|
|
|
|
import ActiveLabel
|
|
|
|
import AlamofireImage
|
|
|
|
|
2021-02-24 09:11:48 +01:00
|
|
|
protocol StatusViewDelegate: class {
|
2021-04-01 08:39:15 +02:00
|
|
|
func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel)
|
|
|
|
func statusView(_ statusView: StatusView, avatarButtonDidPressed button: UIButton)
|
2021-04-16 14:06:36 +02:00
|
|
|
func statusView(_ statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton)
|
|
|
|
func statusView(_ statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
2021-03-11 12:06:15 +01:00
|
|
|
func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
2021-03-05 08:53:36 +01:00
|
|
|
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
2021-04-02 13:33:29 +02:00
|
|
|
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity)
|
2021-02-24 09:11:48 +01:00
|
|
|
}
|
|
|
|
|
2021-02-23 08:16:55 +01:00
|
|
|
final class StatusView: UIView {
|
|
|
|
|
2021-03-02 12:10:45 +01:00
|
|
|
var statusPollTableViewHeightObservation: NSKeyValueObservation?
|
|
|
|
|
2021-02-23 08:16:55 +01:00
|
|
|
static let avatarImageSize = CGSize(width: 42, height: 42)
|
|
|
|
static let avatarImageCornerRadius: CGFloat = 4
|
2021-03-25 11:17:05 +01:00
|
|
|
static let avatarToLabelSpacing: CGFloat = 5
|
2021-02-24 08:29:16 +01:00
|
|
|
static let contentWarningBlurRadius: CGFloat = 12
|
2021-04-19 11:50:58 +02:00
|
|
|
static let containerStackViewSpacing: CGFloat = 10
|
|
|
|
|
|
|
|
weak var delegate: StatusViewDelegate?
|
|
|
|
private var needsDrawContentOverlay = false
|
|
|
|
var pollTableViewDataSource: UITableViewDiffableDataSource<PollSection, PollItem>?
|
|
|
|
var pollTableViewHeightLaoutConstraint: NSLayoutConstraint!
|
|
|
|
|
|
|
|
let containerStackView = UIStackView()
|
|
|
|
let headerContainerView = UIView()
|
|
|
|
let authorContainerView = UIView()
|
2021-02-23 08:16:55 +01:00
|
|
|
|
2021-04-14 09:24:54 +02:00
|
|
|
static let reblogIconImage: UIImage = {
|
2021-03-10 12:12:53 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-02-23 08:16:55 +01:00
|
|
|
let headerIconLabel: UILabel = {
|
|
|
|
let label = UILabel()
|
2021-04-14 09:24:54 +02:00
|
|
|
label.attributedText = StatusView.iconAttributedString(image: StatusView.reblogIconImage)
|
2021-02-23 08:16:55 +01:00
|
|
|
return label
|
|
|
|
}()
|
|
|
|
|
|
|
|
let headerInfoLabel: UILabel = {
|
|
|
|
let label = UILabel()
|
|
|
|
label.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium))
|
|
|
|
label.textColor = Asset.Colors.Label.secondary.color
|
2021-03-15 11:19:45 +01:00
|
|
|
label.text = "Bob reblogged"
|
2021-02-23 08:16:55 +01:00
|
|
|
return label
|
|
|
|
}()
|
|
|
|
|
|
|
|
let avatarView = UIView()
|
|
|
|
let avatarButton: UIButton = {
|
|
|
|
let button = HighlightDimmableButton(type: .custom)
|
|
|
|
let placeholderImage = UIImage.placeholder(size: avatarImageSize, color: .systemFill)
|
|
|
|
.af.imageRounded(withCornerRadius: StatusView.avatarImageCornerRadius, divideRadiusByImageScale: true)
|
|
|
|
button.setImage(placeholderImage, for: .normal)
|
|
|
|
return button
|
|
|
|
}()
|
2021-03-10 06:36:01 +01:00
|
|
|
let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton()
|
2021-02-23 08:16:55 +01:00
|
|
|
|
|
|
|
let nameLabel: UILabel = {
|
|
|
|
let label = UILabel()
|
|
|
|
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
|
|
|
label.textColor = Asset.Colors.Label.primary.color
|
|
|
|
label.text = "Alice"
|
|
|
|
return label
|
|
|
|
}()
|
|
|
|
|
2021-03-16 09:17:11 +01:00
|
|
|
let nameTrialingDotLabel: UILabel = {
|
|
|
|
let label = UILabel()
|
|
|
|
label.textColor = Asset.Colors.Label.secondary.color
|
|
|
|
label.font = .systemFont(ofSize: 17)
|
|
|
|
label.text = "·"
|
|
|
|
return label
|
|
|
|
}()
|
|
|
|
|
2021-02-23 08:16:55 +01:00
|
|
|
let usernameLabel: UILabel = {
|
|
|
|
let label = UILabel()
|
|
|
|
label.font = .systemFont(ofSize: 15, weight: .regular)
|
|
|
|
label.textColor = Asset.Colors.Label.secondary.color
|
|
|
|
label.text = "@alice"
|
|
|
|
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
|
|
|
|
}()
|
|
|
|
|
2021-04-16 14:06:36 +02:00
|
|
|
let revealContentWarningButton: UIButton = {
|
|
|
|
let button = HighlightDimmableButton()
|
|
|
|
button.setImage(UIImage(systemName: "eye", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .medium)), for: .normal)
|
|
|
|
button.tintColor = Asset.Colors.Button.normal.color
|
2021-02-24 08:29:16 +01:00
|
|
|
return button
|
|
|
|
}()
|
2021-04-16 14:06:36 +02:00
|
|
|
|
|
|
|
let statusContainerStackView = UIStackView()
|
2021-03-02 12:33:33 +01:00
|
|
|
let statusMosaicImageViewContainer = MosaicImageViewContainer()
|
2021-02-24 08:29:16 +01:00
|
|
|
|
2021-03-03 09:12:48 +01:00
|
|
|
let pollTableView: PollTableView = {
|
|
|
|
let tableView = PollTableView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
|
2021-03-02 12:10:45 +01:00
|
|
|
tableView.register(PollOptionTableViewCell.self, forCellReuseIdentifier: String(describing: PollOptionTableViewCell.self))
|
2021-03-02 09:27:11 +01:00
|
|
|
tableView.isScrollEnabled = false
|
2021-03-02 12:10:45 +01:00
|
|
|
tableView.separatorStyle = .none
|
|
|
|
tableView.backgroundColor = .clear
|
2021-03-02 09:27:11 +01:00
|
|
|
return tableView
|
|
|
|
}()
|
|
|
|
|
2021-03-02 12:33:33 +01:00
|
|
|
let pollStatusStackView = UIStackView()
|
|
|
|
let pollVoteCountLabel: UILabel = {
|
|
|
|
let label = UILabel()
|
|
|
|
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12, weight: .regular))
|
|
|
|
label.textColor = Asset.Colors.Label.secondary.color
|
|
|
|
label.text = L10n.Common.Controls.Status.Poll.VoteCount.single(0)
|
|
|
|
return label
|
|
|
|
}()
|
|
|
|
let pollStatusDotLabel: UILabel = {
|
|
|
|
let label = UILabel()
|
|
|
|
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12, weight: .regular))
|
|
|
|
label.textColor = Asset.Colors.Label.secondary.color
|
|
|
|
label.text = " · "
|
|
|
|
return label
|
|
|
|
}()
|
|
|
|
let pollCountdownLabel: UILabel = {
|
|
|
|
let label = UILabel()
|
|
|
|
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12, weight: .regular))
|
|
|
|
label.textColor = Asset.Colors.Label.secondary.color
|
|
|
|
label.text = L10n.Common.Controls.Status.Poll.timeLeft("6 hours")
|
|
|
|
return label
|
|
|
|
}()
|
2021-03-03 09:12:48 +01:00
|
|
|
let pollVoteButton: UIButton = {
|
|
|
|
let button = HitTestExpandedButton()
|
2021-03-05 08:53:36 +01:00
|
|
|
button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14, weight: .semibold))
|
2021-03-03 09:12:48 +01:00
|
|
|
button.setTitle(L10n.Common.Controls.Status.Poll.vote, for: .normal)
|
2021-03-18 08:16:35 +01:00
|
|
|
button.setTitleColor(Asset.Colors.Button.normal.color, for: .normal)
|
|
|
|
button.setTitleColor(Asset.Colors.Button.normal.color.withAlphaComponent(0.8), for: .highlighted)
|
2021-03-03 09:12:48 +01:00
|
|
|
button.setTitleColor(Asset.Colors.Button.disabled.color, for: .disabled)
|
2021-03-05 08:53:36 +01:00
|
|
|
button.isEnabled = false
|
2021-03-03 09:12:48 +01:00
|
|
|
return button
|
|
|
|
}()
|
2021-03-02 12:33:33 +01:00
|
|
|
|
2021-02-24 08:29:16 +01:00
|
|
|
// do not use visual effect view due to we blur text only without background
|
2021-04-16 14:06:36 +02:00
|
|
|
let contentWarningOverlayView: ContentWarningOverlayView = {
|
|
|
|
let contentWarningOverlayView = ContentWarningOverlayView()
|
|
|
|
contentWarningOverlayView.layer.masksToBounds = false
|
|
|
|
contentWarningOverlayView.configure(style: .blurContentImageView)
|
|
|
|
return contentWarningOverlayView
|
2021-02-24 08:29:16 +01:00
|
|
|
}()
|
2021-02-23 08:16:55 +01:00
|
|
|
|
2021-03-11 12:06:15 +01:00
|
|
|
let playerContainerView = PlayerContainerView()
|
2021-03-10 07:36:28 +01:00
|
|
|
|
2021-03-08 04:42:10 +01:00
|
|
|
let audioView: AudioContainerView = {
|
|
|
|
let audioView = AudioContainerView()
|
|
|
|
return audioView
|
|
|
|
}()
|
2021-02-23 08:16:55 +01:00
|
|
|
let actionToolbarContainer: ActionToolbarContainer = {
|
|
|
|
let actionToolbarContainer = ActionToolbarContainer()
|
|
|
|
actionToolbarContainer.configure(for: .inline)
|
|
|
|
return actionToolbarContainer
|
|
|
|
}()
|
|
|
|
|
|
|
|
let activeTextLabel = ActiveLabel(style: .default)
|
2021-04-01 08:39:15 +02:00
|
|
|
|
|
|
|
private let headerInfoLabelTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
2021-04-30 13:28:06 +02:00
|
|
|
|
|
|
|
var isRevealing = true
|
2021-02-23 08:16:55 +01:00
|
|
|
|
|
|
|
override init(frame: CGRect) {
|
|
|
|
super.init(frame: frame)
|
|
|
|
_init()
|
|
|
|
}
|
|
|
|
|
|
|
|
required init?(coder: NSCoder) {
|
|
|
|
super.init(coder: coder)
|
|
|
|
_init()
|
|
|
|
}
|
2021-02-24 11:07:00 +01:00
|
|
|
|
|
|
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
|
|
|
|
|
|
// update blur image when interface style changed
|
|
|
|
if previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle {
|
|
|
|
drawContentWarningImageView()
|
|
|
|
}
|
|
|
|
}
|
2021-03-02 12:10:45 +01:00
|
|
|
|
|
|
|
deinit {
|
|
|
|
statusPollTableViewHeightObservation = nil
|
|
|
|
}
|
2021-02-23 08:16:55 +01:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
extension StatusView {
|
|
|
|
|
|
|
|
func _init() {
|
2021-04-01 08:39:15 +02:00
|
|
|
// container: [reblog | author | status | action toolbar]
|
2021-04-19 11:50:58 +02:00
|
|
|
// note: do not set spacing for nested stackView to avoid SDK layout conflict issue
|
2021-02-23 08:16:55 +01:00
|
|
|
containerStackView.axis = .vertical
|
2021-04-19 11:50:58 +02:00
|
|
|
// containerStackView.spacing = 10
|
2021-02-23 08:16:55 +01:00
|
|
|
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),
|
|
|
|
])
|
2021-04-19 11:50:58 +02:00
|
|
|
containerStackView.setContentHuggingPriority(.required - 1, for: .vertical)
|
2021-02-23 08:16:55 +01:00
|
|
|
|
|
|
|
// header container: [icon | info]
|
2021-04-19 11:50:58 +02:00
|
|
|
let headerContainerStackView = UIStackView()
|
|
|
|
headerContainerStackView.axis = .horizontal
|
2021-02-23 08:16:55 +01:00
|
|
|
headerContainerStackView.addArrangedSubview(headerIconLabel)
|
|
|
|
headerContainerStackView.addArrangedSubview(headerInfoLabel)
|
|
|
|
headerIconLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
|
|
|
2021-04-19 11:50:58 +02:00
|
|
|
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: StatusView.containerStackViewSpacing).priority(.defaultHigh),
|
|
|
|
])
|
|
|
|
containerStackView.addArrangedSubview(headerContainerView)
|
|
|
|
|
2021-04-16 14:06:36 +02:00
|
|
|
// author container: [avatar | author meta container | reveal button]
|
2021-02-23 08:16:55 +01:00
|
|
|
let authorContainerStackView = UIStackView()
|
|
|
|
authorContainerStackView.axis = .horizontal
|
2021-03-25 11:17:05 +01:00
|
|
|
authorContainerStackView.spacing = StatusView.avatarToLabelSpacing
|
2021-04-16 14:06:36 +02:00
|
|
|
authorContainerStackView.distribution = .fill
|
2021-02-23 08:16:55 +01:00
|
|
|
|
|
|
|
// avatar
|
|
|
|
avatarView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
authorContainerStackView.addArrangedSubview(avatarView)
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
avatarView.widthAnchor.constraint(equalToConstant: StatusView.avatarImageSize.width).priority(.required - 1),
|
|
|
|
avatarView.heightAnchor.constraint(equalToConstant: StatusView.avatarImageSize.height).priority(.required - 1),
|
|
|
|
])
|
|
|
|
avatarButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
avatarView.addSubview(avatarButton)
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
avatarButton.topAnchor.constraint(equalTo: avatarView.topAnchor),
|
|
|
|
avatarButton.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor),
|
|
|
|
avatarButton.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor),
|
|
|
|
avatarButton.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor),
|
|
|
|
])
|
2021-03-10 06:36:01 +01:00
|
|
|
avatarStackedContainerButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
avatarView.addSubview(avatarStackedContainerButton)
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
avatarStackedContainerButton.topAnchor.constraint(equalTo: avatarView.topAnchor),
|
|
|
|
avatarStackedContainerButton.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor),
|
|
|
|
avatarStackedContainerButton.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor),
|
|
|
|
avatarStackedContainerButton.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor),
|
|
|
|
])
|
2021-02-23 08:16:55 +01:00
|
|
|
|
|
|
|
// author meta container: [title container | subtitle container]
|
|
|
|
let authorMetaContainerStackView = UIStackView()
|
|
|
|
authorContainerStackView.addArrangedSubview(authorMetaContainerStackView)
|
|
|
|
authorMetaContainerStackView.axis = .vertical
|
|
|
|
authorMetaContainerStackView.spacing = 4
|
|
|
|
|
|
|
|
// title container: [display name | "·" | date]
|
|
|
|
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
|
2021-03-16 09:17:11 +01:00
|
|
|
titleContainerStackView.addArrangedSubview(nameTrialingDotLabel)
|
2021-02-23 08:16:55 +01:00
|
|
|
titleContainerStackView.addArrangedSubview(dateLabel)
|
|
|
|
nameLabel.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
|
2021-03-16 09:17:11 +01:00
|
|
|
nameTrialingDotLabel.setContentHuggingPriority(.defaultHigh + 2, for: .horizontal)
|
|
|
|
nameTrialingDotLabel.setContentCompressionResistancePriority(.required - 2, for: .horizontal)
|
2021-02-23 08:16:55 +01:00
|
|
|
dateLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
|
|
dateLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
|
|
|
|
|
|
|
// subtitle container: [username]
|
|
|
|
let subtitleContainerStackView = UIStackView()
|
|
|
|
authorMetaContainerStackView.addArrangedSubview(subtitleContainerStackView)
|
|
|
|
subtitleContainerStackView.axis = .horizontal
|
|
|
|
subtitleContainerStackView.addArrangedSubview(usernameLabel)
|
2021-04-16 14:06:36 +02:00
|
|
|
|
|
|
|
// reveal button
|
|
|
|
authorContainerStackView.addArrangedSubview(revealContentWarningButton)
|
|
|
|
revealContentWarningButton.setContentHuggingPriority(.required - 2, for: .horizontal)
|
2021-02-23 08:16:55 +01:00
|
|
|
|
2021-04-19 11:50:58 +02:00
|
|
|
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: StatusView.containerStackViewSpacing).priority(.defaultHigh),
|
|
|
|
])
|
|
|
|
containerStackView.addArrangedSubview(authorContainerView)
|
|
|
|
|
2021-04-16 14:06:36 +02:00
|
|
|
// status container: [status | image / video | audio | poll | poll status] (overlay with content warning)
|
2021-02-23 08:16:55 +01:00
|
|
|
containerStackView.addArrangedSubview(statusContainerStackView)
|
|
|
|
statusContainerStackView.axis = .vertical
|
|
|
|
statusContainerStackView.spacing = 10
|
2021-04-16 14:06:36 +02:00
|
|
|
|
|
|
|
// content warning overlay
|
|
|
|
contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
containerStackView.addSubview(contentWarningOverlayView)
|
2021-02-24 08:29:16 +01:00
|
|
|
NSLayoutConstraint.activate([
|
2021-04-16 14:06:36 +02:00
|
|
|
statusContainerStackView.topAnchor.constraint(equalTo: contentWarningOverlayView.topAnchor, constant: StatusView.contentWarningBlurRadius).priority(.defaultLow),
|
|
|
|
statusContainerStackView.leftAnchor.constraint(equalTo: contentWarningOverlayView.leftAnchor, constant: StatusView.contentWarningBlurRadius).priority(.defaultLow),
|
|
|
|
// only layout to top-left corner and draw image to fit size
|
2021-02-24 08:29:16 +01:00
|
|
|
])
|
2021-04-16 14:06:36 +02:00
|
|
|
// avoid overlay clip author view
|
|
|
|
containerStackView.bringSubviewToFront(authorContainerStackView)
|
|
|
|
|
|
|
|
// status
|
|
|
|
statusContainerStackView.addArrangedSubview(activeTextLabel)
|
2021-03-03 05:46:38 +01:00
|
|
|
activeTextLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
2021-04-16 14:06:36 +02:00
|
|
|
|
|
|
|
// image
|
|
|
|
statusContainerStackView.addArrangedSubview(statusMosaicImageViewContainer)
|
|
|
|
|
|
|
|
// audio
|
|
|
|
audioView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
statusContainerStackView.addArrangedSubview(audioView)
|
2021-02-24 08:29:16 +01:00
|
|
|
NSLayoutConstraint.activate([
|
2021-04-16 14:06:36 +02:00
|
|
|
audioView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh)
|
2021-02-24 08:29:16 +01:00
|
|
|
])
|
2021-03-02 12:10:45 +01:00
|
|
|
|
2021-04-16 14:06:36 +02:00
|
|
|
// video & gifv
|
|
|
|
statusContainerStackView.addArrangedSubview(playerContainerView)
|
|
|
|
|
2021-03-02 12:33:33 +01:00
|
|
|
pollTableView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
statusContainerStackView.addArrangedSubview(pollTableView)
|
2021-03-03 09:12:48 +01:00
|
|
|
pollTableViewHeightLaoutConstraint = pollTableView.heightAnchor.constraint(equalToConstant: 44.0).priority(.required - 1)
|
2021-03-02 12:10:45 +01:00
|
|
|
NSLayoutConstraint.activate([
|
2021-03-03 09:12:48 +01:00
|
|
|
pollTableViewHeightLaoutConstraint,
|
2021-03-02 12:10:45 +01:00
|
|
|
])
|
|
|
|
|
2021-03-02 12:33:33 +01:00
|
|
|
statusPollTableViewHeightObservation = pollTableView.observe(\.contentSize, options: .new, changeHandler: { [weak self] tableView, _ in
|
2021-03-02 12:10:45 +01:00
|
|
|
guard let self = self else { return }
|
2021-03-02 12:33:33 +01:00
|
|
|
guard self.pollTableView.contentSize.height != .zero else {
|
2021-03-03 09:12:48 +01:00
|
|
|
self.pollTableViewHeightLaoutConstraint.constant = 44
|
2021-03-02 12:10:45 +01:00
|
|
|
return
|
|
|
|
}
|
2021-03-03 09:12:48 +01:00
|
|
|
self.pollTableViewHeightLaoutConstraint.constant = self.pollTableView.contentSize.height
|
2021-03-02 12:10:45 +01:00
|
|
|
})
|
2021-02-24 08:29:16 +01:00
|
|
|
|
2021-03-02 12:33:33 +01:00
|
|
|
statusContainerStackView.addArrangedSubview(pollStatusStackView)
|
|
|
|
pollStatusStackView.axis = .horizontal
|
|
|
|
pollStatusStackView.addArrangedSubview(pollVoteCountLabel)
|
|
|
|
pollStatusStackView.addArrangedSubview(pollStatusDotLabel)
|
|
|
|
pollStatusStackView.addArrangedSubview(pollCountdownLabel)
|
2021-03-03 09:12:48 +01:00
|
|
|
pollStatusStackView.addArrangedSubview(pollVoteButton)
|
2021-03-03 05:46:38 +01:00
|
|
|
pollVoteCountLabel.setContentHuggingPriority(.defaultHigh + 2, for: .horizontal)
|
|
|
|
pollStatusDotLabel.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
|
|
|
|
pollCountdownLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
2021-03-03 09:12:48 +01:00
|
|
|
pollVoteButton.setContentHuggingPriority(.defaultHigh + 3, for: .horizontal)
|
2021-03-02 12:33:33 +01:00
|
|
|
|
2021-02-23 08:16:55 +01:00
|
|
|
// action toolbar container
|
|
|
|
containerStackView.addArrangedSubview(actionToolbarContainer)
|
|
|
|
actionToolbarContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
|
|
|
|
2021-04-19 11:50:58 +02:00
|
|
|
headerContainerView.isHidden = true
|
2021-03-02 12:33:33 +01:00
|
|
|
statusMosaicImageViewContainer.isHidden = true
|
|
|
|
pollTableView.isHidden = true
|
|
|
|
pollStatusStackView.isHidden = true
|
2021-03-08 04:42:10 +01:00
|
|
|
audioView.isHidden = true
|
2021-03-11 12:06:15 +01:00
|
|
|
playerContainerView.isHidden = true
|
2021-03-10 07:36:28 +01:00
|
|
|
|
2021-03-10 06:36:01 +01:00
|
|
|
avatarStackedContainerButton.isHidden = true
|
2021-04-16 14:06:36 +02:00
|
|
|
contentWarningOverlayView.isHidden = true
|
2021-02-24 09:11:48 +01:00
|
|
|
|
2021-04-02 13:33:29 +02:00
|
|
|
activeTextLabel.delegate = self
|
2021-03-11 12:06:15 +01:00
|
|
|
playerContainerView.delegate = self
|
2021-04-16 14:06:36 +02:00
|
|
|
contentWarningOverlayView.delegate = self
|
2021-03-11 12:06:15 +01:00
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
headerInfoLabelTapGestureRecognizer.addTarget(self, action: #selector(StatusView.headerInfoLabelTapGestureRecognizerHandler(_:)))
|
|
|
|
headerInfoLabel.isUserInteractionEnabled = true
|
|
|
|
headerInfoLabel.addGestureRecognizer(headerInfoLabelTapGestureRecognizer)
|
|
|
|
|
|
|
|
avatarButton.addTarget(self, action: #selector(StatusView.avatarButtonDidPressed(_:)), for: .touchUpInside)
|
|
|
|
avatarStackedContainerButton.addTarget(self, action: #selector(StatusView.avatarStackedContainerButtonDidPressed(_:)), for: .touchUpInside)
|
2021-04-16 14:06:36 +02:00
|
|
|
revealContentWarningButton.addTarget(self, action: #selector(StatusView.revealContentWarningButtonDidPressed(_:)), for: .touchUpInside)
|
2021-03-05 08:53:36 +01:00
|
|
|
pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonPressed(_:)), for: .touchUpInside)
|
2021-02-24 08:29:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
extension StatusView {
|
|
|
|
|
2021-04-16 14:06:36 +02:00
|
|
|
private func cleanUpContentWarning() {
|
|
|
|
contentWarningOverlayView.blurContentImageView.image = nil
|
2021-02-24 08:29:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func drawContentWarningImageView() {
|
2021-04-16 14:06:36 +02:00
|
|
|
guard window != nil else {
|
2021-02-24 08:29:16 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-04-16 14:06:36 +02:00
|
|
|
guard needsDrawContentOverlay, statusContainerStackView.frame != .zero else {
|
|
|
|
cleanUpContentWarning()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let format = UIGraphicsImageRendererFormat()
|
|
|
|
format.opaque = false
|
|
|
|
let image = UIGraphicsImageRenderer(size: statusContainerStackView.frame.size, format: format).image { context in
|
|
|
|
statusContainerStackView.drawHierarchy(in: statusContainerStackView.bounds, afterScreenUpdates: true)
|
2021-02-24 08:29:16 +01:00
|
|
|
}
|
|
|
|
.blur(radius: StatusView.contentWarningBlurRadius)
|
2021-04-16 14:06:36 +02:00
|
|
|
contentWarningOverlayView.blurContentImageView.contentScaleFactor = traitCollection.displayScale
|
|
|
|
contentWarningOverlayView.blurContentImageView.image = image
|
2021-02-24 08:29:16 +01:00
|
|
|
}
|
|
|
|
|
2021-04-16 14:06:36 +02:00
|
|
|
func updateContentWarningDisplay(isHidden: Bool, animated: Bool) {
|
|
|
|
needsDrawContentOverlay = !isHidden
|
2021-04-16 14:29:08 +02:00
|
|
|
|
|
|
|
if !isHidden {
|
|
|
|
drawContentWarningImageView()
|
|
|
|
}
|
|
|
|
|
2021-04-16 14:06:36 +02:00
|
|
|
if animated {
|
|
|
|
UIView.animate(withDuration: 0.33, delay: 0, options: .curveEaseInOut) { [weak self] in
|
|
|
|
guard let self = self else { return }
|
|
|
|
self.contentWarningOverlayView.alpha = isHidden ? 0 : 1
|
|
|
|
} completion: { _ in
|
|
|
|
// do nothing
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
contentWarningOverlayView.alpha = isHidden ? 0 : 1
|
|
|
|
}
|
|
|
|
|
2021-04-16 14:29:08 +02:00
|
|
|
contentWarningOverlayView.blurContentWarningTitleLabel.isHidden = isHidden
|
|
|
|
contentWarningOverlayView.blurContentWarningLabel.isHidden = isHidden
|
2021-04-16 14:06:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func updateRevealContentWarningButton(isRevealing: Bool) {
|
2021-04-30 13:28:06 +02:00
|
|
|
self.isRevealing = isRevealing
|
|
|
|
|
2021-04-16 14:06:36 +02:00
|
|
|
if !isRevealing {
|
|
|
|
let image = traitCollection.userInterfaceStyle == .light ? UIImage(systemName: "eye")! : UIImage(systemName: "eye.fill")
|
|
|
|
revealContentWarningButton.setImage(image, for: .normal)
|
|
|
|
} else {
|
|
|
|
let image = traitCollection.userInterfaceStyle == .light ? UIImage(systemName: "eye.slash")! : UIImage(systemName: "eye.slash.fill")
|
|
|
|
revealContentWarningButton.setImage(image, for: .normal)
|
|
|
|
}
|
|
|
|
// TODO: a11y
|
2021-02-23 08:16:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-02-24 09:11:48 +01:00
|
|
|
extension StatusView {
|
2021-03-05 08:53:36 +01:00
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
@objc private func headerInfoLabelTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
|
|
delegate?.statusView(self, headerInfoLabelDidPressed: headerInfoLabel)
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc private func avatarButtonDidPressed(_ sender: UIButton) {
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
|
|
delegate?.statusView(self, avatarButtonDidPressed: sender)
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc private func avatarStackedContainerButtonDidPressed(_ sender: UIButton) {
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
|
|
delegate?.statusView(self, avatarButtonDidPressed: sender)
|
|
|
|
}
|
|
|
|
|
2021-04-16 14:06:36 +02:00
|
|
|
@objc private func revealContentWarningButtonDidPressed(_ sender: UIButton) {
|
2021-02-24 09:11:48 +01:00
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
2021-04-16 14:06:36 +02:00
|
|
|
delegate?.statusView(self, revealContentWarningButtonDidPressed: sender)
|
2021-02-24 09:11:48 +01:00
|
|
|
}
|
2021-03-05 08:53:36 +01:00
|
|
|
|
|
|
|
@objc private func pollVoteButtonPressed(_ sender: UIButton) {
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
|
|
delegate?.statusView(self, pollVoteButtonPressed: sender)
|
|
|
|
}
|
|
|
|
|
2021-02-24 09:11:48 +01:00
|
|
|
}
|
|
|
|
|
2021-04-02 13:33:29 +02:00
|
|
|
// MARK: - ActiveLabelDelegate
|
|
|
|
extension StatusView: ActiveLabelDelegate {
|
|
|
|
func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select entity: %s", ((#file as NSString).lastPathComponent), #line, #function, entity.primaryText)
|
|
|
|
delegate?.statusView(self, activeLabel: activeLabel, didSelectActiveEntity: entity)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-16 14:06:36 +02:00
|
|
|
// MARK: - ContentWarningOverlayViewDelegate
|
|
|
|
extension StatusView: ContentWarningOverlayViewDelegate {
|
|
|
|
func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) {
|
|
|
|
assert(contentWarningOverlayView === self.contentWarningOverlayView)
|
|
|
|
delegate?.statusView(self, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-03-11 12:06:15 +01:00
|
|
|
// MARK: - PlayerContainerViewDelegate
|
|
|
|
extension StatusView: PlayerContainerViewDelegate {
|
|
|
|
func playerContainerView(_ playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
|
|
|
|
delegate?.statusView(self, playerContainerView: playerContainerView, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-02 12:10:45 +01:00
|
|
|
// MARK: - AvatarConfigurableView
|
2021-02-23 08:16:55 +01:00
|
|
|
extension StatusView: AvatarConfigurableView {
|
|
|
|
static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize }
|
|
|
|
static var configurableAvatarImageCornerRadius: CGFloat { return 4 }
|
|
|
|
var configurableAvatarImageView: UIImageView? { return nil }
|
|
|
|
var configurableAvatarButton: UIButton? { return avatarButton }
|
|
|
|
var configurableVerifiedBadgeImageView: UIImageView? { nil }
|
|
|
|
}
|
|
|
|
|
|
|
|
#if canImport(SwiftUI) && DEBUG
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
struct StatusView_Previews: PreviewProvider {
|
|
|
|
|
2021-02-23 12:18:34 +01:00
|
|
|
static let avatarFlora = UIImage(named: "tiraya-adam")
|
2021-03-10 06:36:01 +01:00
|
|
|
static let avatarMarkus = UIImage(named: "markus-spiske")
|
2021-02-23 08:16:55 +01:00
|
|
|
|
|
|
|
static var previews: some View {
|
|
|
|
Group {
|
|
|
|
UIViewPreview(width: 375) {
|
|
|
|
let statusView = StatusView()
|
|
|
|
statusView.configure(
|
|
|
|
with: AvatarConfigurableViewConfiguration(
|
|
|
|
avatarImageURL: nil,
|
|
|
|
placeholderImage: avatarFlora
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return statusView
|
|
|
|
}
|
|
|
|
.previewLayout(.fixed(width: 375, height: 200))
|
2021-03-10 06:36:01 +01:00
|
|
|
.previewDisplayName("Normal")
|
|
|
|
UIViewPreview(width: 375) {
|
|
|
|
let statusView = StatusView()
|
2021-04-19 11:50:58 +02:00
|
|
|
statusView.headerContainerView.isHidden = false
|
2021-03-10 06:36:01 +01:00
|
|
|
statusView.avatarButton.isHidden = true
|
|
|
|
statusView.avatarStackedContainerButton.isHidden = false
|
|
|
|
statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure(
|
|
|
|
with: AvatarConfigurableViewConfiguration(
|
|
|
|
avatarImageURL: nil,
|
|
|
|
placeholderImage: avatarFlora
|
|
|
|
)
|
|
|
|
)
|
|
|
|
statusView.avatarStackedContainerButton.bottomTrailingAvatarStackedImageView.configure(
|
|
|
|
with: AvatarConfigurableViewConfiguration(
|
|
|
|
avatarImageURL: nil,
|
|
|
|
placeholderImage: avatarMarkus
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return statusView
|
|
|
|
}
|
|
|
|
.previewLayout(.fixed(width: 375, height: 200))
|
2021-03-15 11:19:45 +01:00
|
|
|
.previewDisplayName("Reblog")
|
2021-03-10 06:36:01 +01:00
|
|
|
UIViewPreview(width: 375) {
|
|
|
|
let statusView = StatusView(frame: CGRect(x: 0, y: 0, width: 375, height: 500))
|
|
|
|
statusView.configure(
|
|
|
|
with: AvatarConfigurableViewConfiguration(
|
|
|
|
avatarImageURL: nil,
|
|
|
|
placeholderImage: avatarFlora
|
|
|
|
)
|
|
|
|
)
|
2021-04-19 11:50:58 +02:00
|
|
|
statusView.headerContainerView.isHidden = false
|
2021-03-10 06:36:01 +01:00
|
|
|
let images = MosaicImageView_Previews.images
|
2021-04-16 14:06:36 +02:00
|
|
|
let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxHeight: 162)
|
|
|
|
for (i, mosaic) in mosaics.enumerated() {
|
|
|
|
let (imageView, _) = mosaic
|
2021-03-10 06:36:01 +01:00
|
|
|
imageView.image = images[i]
|
|
|
|
}
|
|
|
|
statusView.statusMosaicImageViewContainer.isHidden = false
|
2021-03-15 09:15:34 +01:00
|
|
|
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isHidden = true
|
2021-03-10 06:36:01 +01:00
|
|
|
return statusView
|
|
|
|
}
|
|
|
|
.previewLayout(.fixed(width: 375, height: 380))
|
|
|
|
.previewDisplayName("Image Meida")
|
2021-02-23 08:16:55 +01:00
|
|
|
UIViewPreview(width: 375) {
|
2021-02-24 12:19:16 +01:00
|
|
|
let statusView = StatusView(frame: CGRect(x: 0, y: 0, width: 375, height: 500))
|
2021-02-23 08:16:55 +01:00
|
|
|
statusView.configure(
|
|
|
|
with: AvatarConfigurableViewConfiguration(
|
|
|
|
avatarImageURL: nil,
|
|
|
|
placeholderImage: avatarFlora
|
|
|
|
)
|
|
|
|
)
|
2021-04-19 11:50:58 +02:00
|
|
|
statusView.headerContainerView.isHidden = false
|
2021-02-24 12:19:16 +01:00
|
|
|
statusView.setNeedsLayout()
|
|
|
|
statusView.layoutIfNeeded()
|
2021-04-16 14:06:36 +02:00
|
|
|
statusView.updateContentWarningDisplay(isHidden: false, animated: false)
|
2021-02-24 12:19:16 +01:00
|
|
|
statusView.drawContentWarningImageView()
|
|
|
|
let images = MosaicImageView_Previews.images
|
2021-04-16 14:06:36 +02:00
|
|
|
let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxHeight: 162)
|
|
|
|
for (i, mosaic) in mosaics.enumerated() {
|
|
|
|
let (imageView, _) = mosaic
|
2021-02-24 12:19:16 +01:00
|
|
|
imageView.image = images[i]
|
|
|
|
}
|
2021-03-02 12:33:33 +01:00
|
|
|
statusView.statusMosaicImageViewContainer.isHidden = false
|
2021-02-23 08:16:55 +01:00
|
|
|
return statusView
|
|
|
|
}
|
2021-02-24 12:19:16 +01:00
|
|
|
.previewLayout(.fixed(width: 375, height: 380))
|
2021-03-10 06:36:01 +01:00
|
|
|
.previewDisplayName("Content Sensitive")
|
2021-02-23 08:16:55 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|