mastodon-ios/Mastodon/Scene/Share/View/Content/StatusView.swift

726 lines
33 KiB
Swift
Raw Normal View History

2021-02-23 08:16:55 +01:00
//
// StatusView.swift
// Mastodon
//
// Created by sxiaojian on 2021/1/28.
//
import os.log
2021-02-23 08:16:55 +01:00
import UIKit
import Combine
2021-02-23 08:16:55 +01:00
import AVKit
import AlamofireImage
import FLAnimatedImage
2021-07-22 13:34:24 +02:00
import MetaTextKit
import Meta
import MastodonSDK
// TODO:
// import LinkPresentation
2021-02-23 08:16:55 +01:00
protocol StatusViewDelegate: AnyObject {
2021-04-01 08:39:15 +02:00
func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel)
func statusView(_ statusView: StatusView, avatarImageViewDidPressed imageView: UIImageView)
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)
func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
}
2021-02-23 08:16:55 +01:00
final class StatusView: UIView {
let logger = Logger(subsystem: "StatusView", category: "logic")
2021-02-23 08:16:55 +01:00
2021-03-02 12:10:45 +01:00
var statusPollTableViewHeightObservation: NSKeyValueObservation?
var pollCountdownSubscription: AnyCancellable?
2021-03-02 12:10:45 +01:00
2021-02-23 08:16:55 +01:00
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
weak var delegate: StatusViewDelegate?
var pollTableViewDataSource: UITableViewDiffableDataSource<PollSection, PollItem>?
var pollTableViewHeightLayoutConstraint: NSLayoutConstraint!
let containerStackView = UIStackView()
let headerContainerView = UIView()
let authorContainerView = UIView()
2021-02-23 08:16:55 +01: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
}
let headerIconLabel: MetaLabel = {
let label = MetaLabel(style: .statusHeader)
let attributedString = StatusView.iconAttributedString(image: StatusView.reblogIconImage)
label.configure(attributedString: attributedString)
2021-02-23 08:16:55 +01:00
return label
}()
2021-07-22 13:34:24 +02:00
let headerInfoLabel = MetaLabel(style: .statusHeader)
2021-02-23 08:16:55 +01:00
let avatarView: UIView = {
let view = UIView()
view.isAccessibilityElement = true
view.accessibilityTraits = .button
view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile
return view
}()
2021-07-21 13:45:24 +02:00
let avatarButton = AvatarButton()
let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton()
2021-02-23 08:16:55 +01:00
2021-07-22 13:34:24 +02:00
let nameMetaLabel: MetaLabel = {
let label = MetaLabel(style: .statusName)
2021-02-23 08:16:55 +01:00
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 = "·"
label.isAccessibilityElement = false
2021-03-16 09:17:11 +01:00
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"
label.isAccessibilityElement = false
2021-02-23 08:16:55 +01:00
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 revealContentWarningButton: UIButton = {
let button = HighlightDimmableButton()
button.setImage(UIImage(systemName: "eye", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .medium)), for: .normal)
2021-10-29 08:58:09 +02:00
// button.tintColor = Asset.Colors.brandBlue.color
return button
}()
let visibilityImageView: UIImageView = {
let imageView = UIImageView()
imageView.tintColor = Asset.Colors.Label.secondary.color
imageView.contentMode = .scaleAspectFit
return imageView
}()
let statusContainerStackView = UIStackView()
2021-03-02 12:33:33 +01:00
let statusMosaicImageViewContainer = MosaicImageViewContainer()
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))
tableView.rowHeight = PollOptionView.height
tableView.isScrollEnabled = false
2021-03-02 12:10:45 +01:00
tableView.separatorStyle = .none
tableView.backgroundColor = .clear
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
2021-07-06 08:55:24 +02:00
label.text = L10n.Plural.Count.vote(0)
2021-03-02 12:33:33 +01:00
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 = "1 day left"
2021-03-02 12:33:33 +01:00
return label
}()
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))
button.setTitle(L10n.Common.Controls.Status.Poll.vote, for: .normal)
2021-06-22 14:52:30 +02:00
button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal)
button.setTitleColor(Asset.Colors.brandBlue.color.withAlphaComponent(0.8), for: .highlighted)
button.setTitleColor(Asset.Colors.Button.disabled.color, for: .disabled)
2021-03-05 08:53:36 +01:00
button.isEnabled = false
return button
}()
2021-03-02 12:33:33 +01:00
// do not use visual effect view due to we blur text only without background
let contentWarningOverlayView: ContentWarningOverlayView = {
let contentWarningOverlayView = ContentWarningOverlayView()
contentWarningOverlayView.configure(style: .contentWarning)
contentWarningOverlayView.layer.masksToBounds = true
return contentWarningOverlayView
}()
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
}()
// set display when needs bottom padding
let actionToolbarPlaceholderPaddingView = UIView()
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.textView.textDragInteraction?.isEnabled = false // disable drag for link and attachment
2021-07-02 13:48:56 +02:00
2021-07-23 13:10:27 +02:00
metaText.paragraphStyle = {
2021-06-30 09:43:33 +02:00
let style = NSMutableParagraphStyle()
style.lineSpacing = 5
style.paragraphSpacing = 8
style.alignment = .natural
2021-06-30 09:43:33 +02:00
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
}()
2021-07-02 13:48:56 +02:00
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-06-23 15:21:22 +02:00
// TODO:
// let linkPreview = LPLinkView()
2021-06-23 15:21:22 +02:00
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-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]
// 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
// 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),
])
containerStackView.setContentHuggingPriority(.required - 1, for: .vertical)
containerStackView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
2021-02-23 08:16:55 +01:00
// header container: [icon | info]
let headerContainerStackView = UIStackView()
headerContainerStackView.axis = .horizontal
headerContainerStackView.spacing = 4
2021-02-23 08:16:55 +01:00
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: StatusView.containerStackViewSpacing).priority(.defaultHigh),
])
headerContainerStackView.setContentCompressionResistancePriority(.required - 5, for: .vertical)
containerStackView.addArrangedSubview(headerContainerView)
defer {
containerStackView.bringSubviewToFront(headerContainerView)
}
// author container: [avatar | author meta container | reveal button]
2021-02-23 08:16:55 +01:00
let authorContainerStackView = UIStackView()
authorContainerStackView.axis = .horizontal
authorContainerStackView.spacing = StatusView.avatarToLabelSpacing
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),
])
2021-07-21 13:45:24 +02:00
avatarButton.translatesAutoresizingMaskIntoConstraints = false
avatarView.addSubview(avatarButton)
2021-02-23 08:16:55 +01:00
NSLayoutConstraint.activate([
2021-07-21 13:45:24 +02:00
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-02-23 08:16:55 +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
2021-05-11 10:44:23 +02:00
// title container: [display name | "·" | date | padding | visibility]
2021-02-23 08:16:55 +01:00
let titleContainerStackView = UIStackView()
authorMetaContainerStackView.addArrangedSubview(titleContainerStackView)
titleContainerStackView.axis = .horizontal
titleContainerStackView.alignment = .center
2021-02-23 08:16:55 +01:00
titleContainerStackView.spacing = 4
2021-07-22 13:34:24 +02:00
nameMetaLabel.translatesAutoresizingMaskIntoConstraints = false
titleContainerStackView.addArrangedSubview(nameMetaLabel)
2021-02-23 08:16:55 +01:00
NSLayoutConstraint.activate([
2021-07-22 13:34:24 +02:00
nameMetaLabel.heightAnchor.constraint(equalToConstant: 22).priority(.defaultHigh),
2021-02-23 08:16:55 +01:00
])
2021-03-16 09:17:11 +01:00
titleContainerStackView.addArrangedSubview(nameTrialingDotLabel)
2021-02-23 08:16:55 +01:00
titleContainerStackView.addArrangedSubview(dateLabel)
2021-07-22 13:34:24 +02:00
let padding = UIView()
padding.translatesAutoresizingMaskIntoConstraints = false
titleContainerStackView.addArrangedSubview(padding) // padding
2021-05-11 10:44:23 +02:00
titleContainerStackView.addArrangedSubview(visibilityImageView)
2021-07-22 13:34:24 +02:00
nameMetaLabel.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)
2021-07-22 13:34:24 +02:00
dateLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal)
padding.setContentHuggingPriority(.defaultLow, for: .horizontal)
padding.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
visibilityImageView.setContentHuggingPriority(.required - 9, for: .horizontal)
visibilityImageView.setContentCompressionResistancePriority(.required - 9, for: .horizontal)
2021-06-30 09:51:05 +02:00
visibilityImageView.setContentHuggingPriority(.required - 1, for: .vertical)
2021-05-11 10:44:23 +02:00
2021-02-23 08:16:55 +01:00
// subtitle container: [username]
let subtitleContainerStackView = UIStackView()
authorMetaContainerStackView.addArrangedSubview(subtitleContainerStackView)
subtitleContainerStackView.axis = .horizontal
subtitleContainerStackView.addArrangedSubview(usernameLabel)
// reveal button
authorContainerStackView.addArrangedSubview(revealContentWarningButton)
revealContentWarningButton.setContentHuggingPriority(.required - 2, for: .horizontal)
2021-02-23 08:16:55 +01: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(.required - 1),
])
containerStackView.addArrangedSubview(authorContainerView)
// 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
// content warning overlay
contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false
containerStackView.addSubview(contentWarningOverlayView)
NSLayoutConstraint.activate([
statusContainerStackView.topAnchor.constraint(equalTo: contentWarningOverlayView.topAnchor).priority(.required - 10),
statusContainerStackView.leftAnchor.constraint(equalTo: contentWarningOverlayView.leftAnchor).priority(.required - 1),
contentWarningOverlayView.rightAnchor.constraint(equalTo: statusContainerStackView.rightAnchor).priority(.required - 1),
contentWarningOverlayView.bottomAnchor.constraint(equalTo: statusContainerStackView.bottomAnchor).priority(.required - 1),
])
// avoid overlay behind other views
defer {
containerStackView.bringSubviewToFront(authorContainerView)
}
// status
statusContainerStackView.addArrangedSubview(contentMetaText.textView)
contentMetaText.textView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
// image
statusContainerStackView.addArrangedSubview(statusMosaicImageViewContainer)
// audio
audioView.translatesAutoresizingMaskIntoConstraints = false
statusContainerStackView.addArrangedSubview(audioView)
NSLayoutConstraint.activate([
audioView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh)
])
2021-03-02 12:10:45 +01:00
// video & gifv
statusContainerStackView.addArrangedSubview(playerContainerView)
2021-03-02 12:33:33 +01:00
pollTableView.translatesAutoresizingMaskIntoConstraints = false
statusContainerStackView.addArrangedSubview(pollTableView)
pollTableViewHeightLayoutConstraint = pollTableView.heightAnchor.constraint(equalToConstant: 44.0).priority(.required - 1)
2021-03-02 12:10:45 +01:00
NSLayoutConstraint.activate([
pollTableViewHeightLayoutConstraint,
2021-03-02 12:10:45 +01:00
])
// statusPollTableViewHeightObservation = pollTableView.observe(\.contentSize, options: .new, changeHandler: { [weak self] tableView, _ in
// guard let self = self else { return }
// guard self.pollTableView.contentSize.height != .zero else {
// self.pollTableViewHeightLayoutConstraint.constant = 44
// return
// }
// self.pollTableViewHeightLayoutConstraint.constant = self.pollTableView.contentSize.height
// })
pollStatusStackView.translatesAutoresizingMaskIntoConstraints = false
2021-03-02 12:33:33 +01:00
statusContainerStackView.addArrangedSubview(pollStatusStackView)
NSLayoutConstraint.activate([
pollStatusStackView.heightAnchor.constraint(equalToConstant: 30).priority(.required - 10)
])
2021-03-02 12:33:33 +01:00
pollStatusStackView.axis = .horizontal
pollStatusStackView.addArrangedSubview(pollVoteCountLabel)
pollStatusStackView.addArrangedSubview(pollStatusDotLabel)
pollStatusStackView.addArrangedSubview(pollCountdownLabel)
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)
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)
containerStackView.sendSubviewToBack(actionToolbarContainer)
actionToolbarContainer.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
actionToolbarContainer.setContentHuggingPriority(.required - 1, for: .vertical)
actionToolbarPlaceholderPaddingView.translatesAutoresizingMaskIntoConstraints = false
containerStackView.addArrangedSubview(actionToolbarPlaceholderPaddingView)
NSLayoutConstraint.activate([
actionToolbarPlaceholderPaddingView.heightAnchor.constraint(equalToConstant: 12).priority(.required - 1),
])
actionToolbarPlaceholderPaddingView.isHidden = true
2021-02-23 08:16:55 +01: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
avatarStackedContainerButton.isHidden = true
contentWarningOverlayView.isHidden = true
contentMetaText.textView.delegate = self
contentMetaText.textView.linkDelegate = self
2021-03-11 12:06:15 +01:00
playerContainerView.delegate = self
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)
2021-07-21 13:45:24 +02:00
avatarButton.addTarget(self, action: #selector(StatusView.avatarButtonDidPressed(_:)), for: .touchUpInside)
2021-04-01 08:39:15 +02:00
avatarStackedContainerButton.addTarget(self, action: #selector(StatusView.avatarStackedContainerButtonDidPressed(_:)), for: .touchUpInside)
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)
}
}
extension StatusView {
func updateContentWarningDisplay(isHidden: Bool, animated: Bool, completion: (() -> Void)? = nil) {
func updateOverlayView() {
contentWarningOverlayView.contentOverlayView.alpha = isHidden ? 0 : 1
contentWarningOverlayView.isUserInteractionEnabled = !isHidden
2021-04-16 14:29:08 +02:00
}
contentWarningOverlayView.blurContentWarningTitleLabel.isHidden = isHidden
if animated {
UIView.animate(withDuration: 0.33, delay: 0, options: .curveEaseInOut) {
updateOverlayView()
} completion: { _ in
completion!()
}
} else {
updateOverlayView()
completion?()
}
}
func updateRevealContentWarningButton(isRevealing: Bool) {
2021-04-30 13:28:06 +02:00
self.isRevealing = isRevealing
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
}
func updateVisibility(visibility: Mastodon.Entity.Status.Visibility) {
switch visibility {
case .public:
visibilityImageView.image = UIImage(systemName: "globe", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13, weight: .regular))
case .private:
visibilityImageView.image = UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13, weight: .regular))
case .unlisted:
visibilityImageView.image = UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13, weight: .regular))
case .direct:
visibilityImageView.image = UIImage(systemName: "at", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13, weight: .regular))
case ._other:
visibilityImageView.image = nil
}
}
2021-02-23 08:16:55 +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)
}
2021-07-21 13:45:24 +02:00
@objc private func avatarButtonDidPressed(_ sender: UIButton) {
2021-04-01 08:39:15 +02:00
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
2021-07-21 13:45:24 +02:00
delegate?.statusView(self, avatarImageViewDidPressed: avatarButton.avatarImageView)
2021-04-01 08:39:15 +02:00
}
@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, avatarImageViewDidPressed: avatarStackedContainerButton.topLeadingAvatarStackedImageView)
2021-04-01 08:39:15 +02:00
}
@objc private func revealContentWarningButtonDidPressed(_ sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
delegate?.statusView(self, revealContentWarningButtonDidPressed: sender)
}
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)
}
}
// MARK: - MetaTextViewDelegate
extension StatusView: MetaTextViewDelegate {
2021-07-23 13:10:27 +02:00
func metaTextView(_ metaTextView: MetaTextView, didSelectMeta meta: Meta) {
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
switch metaTextView {
case contentMetaText.textView:
delegate?.statusView(self, metaText: contentMetaText, didSelectMeta: meta)
default:
assertionFailure()
break
}
}
}
// MARK: - UITextViewDelegate
extension StatusView: UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
switch textView {
case contentMetaText.textView:
return false
default:
assertionFailure()
return true
}
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
switch textView {
case contentMetaText.textView:
return false
default:
assertionFailure()
return true
}
}
}
// 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 }
2021-07-21 13:45:24 +02:00
var configurableAvatarImageView: FLAnimatedImageView? { avatarButton.avatarImageView }
2021-02-23 08:16:55 +01:00
}
#if canImport(SwiftUI) && DEBUG
import SwiftUI
struct StatusView_Previews: PreviewProvider {
static let avatarFlora = UIImage(named: "tiraya-adam")
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))
.previewDisplayName("Normal")
UIViewPreview(width: 375) {
let statusView = StatusView()
statusView.headerContainerView.isHidden = false
2021-07-21 13:45:24 +02: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")
UIViewPreview(width: 375) {
let statusView = StatusView(frame: CGRect(x: 0, y: 0, width: 375, height: 500))
statusView.configure(
with: AvatarConfigurableViewConfiguration(
avatarImageURL: nil,
placeholderImage: avatarFlora
)
)
statusView.headerContainerView.isHidden = false
let images = MosaicImageView_Previews.images
let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxSize: CGSize(width: 375, height: 162))
for (i, mosaic) in mosaics.enumerated() {
mosaic.imageView.image = images[i]
}
statusView.statusMosaicImageViewContainer.isHidden = false
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isHidden = true
return statusView
}
.previewLayout(.fixed(width: 375, height: 380))
.previewDisplayName("Image Meida")
2021-02-23 08:16:55 +01:00
UIViewPreview(width: 375) {
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
)
)
statusView.headerContainerView.isHidden = false
statusView.setNeedsLayout()
statusView.layoutIfNeeded()
statusView.updateContentWarningDisplay(isHidden: false, animated: false)
let images = MosaicImageView_Previews.images
let mosaics = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxSize: CGSize(width: 375, height: 162))
for (i, mosaic) in mosaics.enumerated() {
mosaic.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
}
.previewLayout(.fixed(width: 375, height: 380))
.previewDisplayName("Content Sensitive")
2021-02-23 08:16:55 +01:00
}
}
}
#endif