forked from zelo72/mastodon-ios
451 lines
20 KiB
Swift
451 lines
20 KiB
Swift
//
|
|
// NotificationView.swift
|
|
//
|
|
//
|
|
// Created by MainasuK on 2022-1-21.
|
|
//
|
|
|
|
import os.log
|
|
import UIKit
|
|
import Combine
|
|
import MetaTextKit
|
|
import Meta
|
|
import MastodonAsset
|
|
import MastodonLocalization
|
|
|
|
public protocol NotificationViewDelegate: AnyObject {
|
|
func notificationView(_ notificationView: NotificationView, authorAvatarButtonDidPressed button: AvatarButton)
|
|
func notificationView(_ notificationView: NotificationView, menuButton button: UIButton, didSelectAction action: MastodonMenu.Action)
|
|
|
|
func notificationView(_ notificationView: NotificationView, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
|
func notificationView(_ notificationView: NotificationView, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
|
func notificationView(_ notificationView: NotificationView, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int)
|
|
|
|
func notificationView(_ notificationView: NotificationView, statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action)
|
|
|
|
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton)
|
|
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
|
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
|
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int)
|
|
|
|
// a11y
|
|
func notificationView(_ notificationView: NotificationView, accessibilityActivate: Void)
|
|
}
|
|
|
|
public final class NotificationView: UIView {
|
|
|
|
static let containerLayoutMargin = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
|
|
|
|
let logger = Logger(subsystem: "NotificationView", category: "View")
|
|
|
|
public weak var delegate: NotificationViewDelegate?
|
|
|
|
var _disposeBag = Set<AnyCancellable>()
|
|
public var disposeBag = Set<AnyCancellable>()
|
|
|
|
public private(set) lazy var viewModel: ViewModel = {
|
|
let viewModel = ViewModel()
|
|
viewModel.bind(notificationView: self)
|
|
return viewModel
|
|
}()
|
|
|
|
let containerStackView: UIStackView = {
|
|
let stackView = UIStackView()
|
|
stackView.axis = .vertical
|
|
stackView.spacing = 10
|
|
return stackView
|
|
}()
|
|
|
|
// author
|
|
let authorAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
|
let authorContainerView: UIStackView = {
|
|
let stackView = UIStackView()
|
|
stackView.axis = .horizontal
|
|
stackView.spacing = 12
|
|
return stackView
|
|
}()
|
|
let authorContainerViewBottomPaddingView = UIView()
|
|
|
|
// avatar
|
|
public let avatarButton = AvatarButton()
|
|
|
|
// author name
|
|
public let authorNameLabel = MetaLabel(style: .statusName)
|
|
|
|
// author username
|
|
public let authorUsernameLabel = MetaLabel(style: .statusUsername)
|
|
|
|
public let usernameTrialingDotLabel: MetaLabel = {
|
|
let label = MetaLabel(style: .statusUsername)
|
|
label.configure(content: PlaintextMetaContent(string: "·"))
|
|
return label
|
|
}()
|
|
|
|
// timestamp
|
|
public let dateLabel = MetaLabel(style: .statusUsername)
|
|
|
|
public let menuButton: UIButton = {
|
|
let button = HitTestExpandedButton(type: .system)
|
|
let image = UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(font: .systemFont(ofSize: 15)))
|
|
button.setImage(image, for: .normal)
|
|
return button
|
|
}()
|
|
|
|
// notification type indicator imageView
|
|
public let notificationTypeIndicatorImageView: UIImageView = {
|
|
let imageView = UIImageView()
|
|
imageView.tintColor = Asset.Colors.Label.secondary.color
|
|
return imageView
|
|
}()
|
|
|
|
// notification type indicator imageView
|
|
public let notificationTypeIndicatorLabel = MetaLabel(style: .notificationTitle)
|
|
|
|
public let statusView = StatusView()
|
|
|
|
public let quoteStatusViewContainerView = UIView()
|
|
public let quoteBackgroundView = UIView()
|
|
public let quoteStatusView = StatusView()
|
|
|
|
public func prepareForReuse() {
|
|
disposeBag.removeAll()
|
|
|
|
viewModel.authorAvatarImageURL = nil
|
|
avatarButton.avatarImageView.cancelTask()
|
|
|
|
authorContainerViewBottomPaddingView.isHidden = true
|
|
|
|
statusView.isHidden = true
|
|
statusView.prepareForReuse()
|
|
|
|
quoteStatusViewContainerView.isHidden = true
|
|
quoteStatusView.prepareForReuse()
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
_init()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
_init()
|
|
}
|
|
|
|
}
|
|
|
|
extension NotificationView {
|
|
private func _init() {
|
|
// container: V - [ author container | (authorContainerViewBottomPaddingView) | statusView | quoteStatusView ]
|
|
// containerStackView.layoutMargins = StatusView.containerLayoutMargin
|
|
|
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
|
addSubview(containerStackView)
|
|
NSLayoutConstraint.activate([
|
|
containerStackView.topAnchor.constraint(equalTo: topAnchor),
|
|
containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor),
|
|
])
|
|
|
|
// author container: H - [ avatarButton | author meta container ]
|
|
authorAdaptiveMarginContainerView.contentView = authorContainerView
|
|
authorAdaptiveMarginContainerView.margin = StatusView.containerLayoutMargin
|
|
containerStackView.addArrangedSubview(authorAdaptiveMarginContainerView)
|
|
|
|
UIContentSizeCategory.publisher
|
|
.sink { [weak self] category in
|
|
guard let self = self else { return }
|
|
self.authorContainerView.axis = category > .accessibilityLarge ? .vertical : .horizontal
|
|
self.authorContainerView.alignment = category > .accessibilityLarge ? .leading : .center
|
|
}
|
|
.store(in: &_disposeBag)
|
|
|
|
// avatarButton
|
|
let authorAvatarButtonSize = CGSize(width: 46, height: 46)
|
|
avatarButton.size = authorAvatarButtonSize
|
|
avatarButton.avatarImageView.imageViewSize = authorAvatarButtonSize
|
|
avatarButton.translatesAutoresizingMaskIntoConstraints = false
|
|
authorContainerView.addArrangedSubview(avatarButton)
|
|
NSLayoutConstraint.activate([
|
|
avatarButton.widthAnchor.constraint(equalToConstant: authorAvatarButtonSize.width).priority(.required - 1),
|
|
avatarButton.heightAnchor.constraint(equalToConstant: authorAvatarButtonSize.height).priority(.required - 1),
|
|
])
|
|
avatarButton.setContentHuggingPriority(.required - 1, for: .vertical)
|
|
avatarButton.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
|
|
|
// authrMetaContainer: V - [ authorPrimaryContainer | authorSecondaryMetaContainer ]
|
|
let authrMetaContainer = UIStackView()
|
|
authrMetaContainer.axis = .vertical
|
|
authrMetaContainer.spacing = 4
|
|
authorContainerView.addArrangedSubview(authrMetaContainer)
|
|
|
|
// authorPrimaryContainer: H - [ authorNameLabel | notificationTypeIndicatorLabel | (padding) | menuButton ]
|
|
let authorPrimaryContainer = UIStackView()
|
|
authorPrimaryContainer.axis = .horizontal
|
|
authrMetaContainer.addArrangedSubview(authorPrimaryContainer)
|
|
|
|
authorPrimaryContainer.addArrangedSubview(authorNameLabel)
|
|
authorPrimaryContainer.addArrangedSubview(notificationTypeIndicatorLabel)
|
|
authorPrimaryContainer.addArrangedSubview(UIView())
|
|
authorPrimaryContainer.addArrangedSubview(menuButton)
|
|
authorNameLabel.setContentHuggingPriority(.required - 10, for: .horizontal)
|
|
authorNameLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal)
|
|
notificationTypeIndicatorLabel.setContentHuggingPriority(.required - 4, for: .horizontal)
|
|
notificationTypeIndicatorLabel.setContentCompressionResistancePriority(.required - 4, for: .horizontal)
|
|
menuButton.setContentHuggingPriority(.required - 5, for: .horizontal)
|
|
menuButton.setContentCompressionResistancePriority(.required - 5, for: .horizontal)
|
|
|
|
// authorSecondaryMetaContainer: H - [ authorUsername | (padding) ]
|
|
let authorSecondaryMetaContainer = UIStackView()
|
|
authorSecondaryMetaContainer.axis = .horizontal
|
|
authorSecondaryMetaContainer.spacing = 4
|
|
authrMetaContainer.addArrangedSubview(authorSecondaryMetaContainer)
|
|
authrMetaContainer.setCustomSpacing(4, after: authorSecondaryMetaContainer)
|
|
|
|
authorSecondaryMetaContainer.addArrangedSubview(authorUsernameLabel)
|
|
authorUsernameLabel.setContentHuggingPriority(.required - 8, for: .horizontal)
|
|
authorUsernameLabel.setContentCompressionResistancePriority(.required - 8, for: .horizontal)
|
|
authorSecondaryMetaContainer.addArrangedSubview(usernameTrialingDotLabel)
|
|
usernameTrialingDotLabel.setContentHuggingPriority(.required - 2, for: .horizontal)
|
|
usernameTrialingDotLabel.setContentCompressionResistancePriority(.required - 2, for: .horizontal)
|
|
authorSecondaryMetaContainer.addArrangedSubview(dateLabel)
|
|
dateLabel.setContentHuggingPriority(.required - 1, for: .horizontal)
|
|
dateLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
|
authorSecondaryMetaContainer.addArrangedSubview(UIView())
|
|
|
|
// authorContainerViewBottomPaddingView
|
|
authorContainerViewBottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
|
containerStackView.addArrangedSubview(authorContainerViewBottomPaddingView)
|
|
NSLayoutConstraint.activate([
|
|
authorContainerViewBottomPaddingView.heightAnchor.constraint(equalToConstant: 16).priority(.required - 1),
|
|
])
|
|
authorContainerViewBottomPaddingView.isHidden = true
|
|
|
|
// statusView
|
|
containerStackView.addArrangedSubview(statusView)
|
|
statusView.setup(style: .notification)
|
|
|
|
// quoteStatusView
|
|
containerStackView.addArrangedSubview(quoteStatusViewContainerView)
|
|
quoteStatusViewContainerView.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 16, right: 0)
|
|
|
|
quoteBackgroundView.layoutMargins = UIEdgeInsets(top: 16, left: 0, bottom: 0, right: 0)
|
|
quoteBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
|
quoteStatusViewContainerView.addSubview(quoteBackgroundView)
|
|
NSLayoutConstraint.activate([
|
|
quoteBackgroundView.topAnchor.constraint(equalTo: quoteStatusViewContainerView.layoutMarginsGuide.topAnchor),
|
|
quoteBackgroundView.leadingAnchor.constraint(equalTo: quoteStatusViewContainerView.layoutMarginsGuide.leadingAnchor),
|
|
quoteBackgroundView.trailingAnchor.constraint(equalTo: quoteStatusViewContainerView.layoutMarginsGuide.trailingAnchor),
|
|
quoteBackgroundView.bottomAnchor.constraint(equalTo: quoteStatusViewContainerView.layoutMarginsGuide.bottomAnchor),
|
|
])
|
|
quoteBackgroundView.backgroundColor = .secondarySystemBackground
|
|
quoteBackgroundView.layer.masksToBounds = true
|
|
quoteBackgroundView.layer.cornerCurve = .continuous
|
|
quoteBackgroundView.layer.cornerRadius = 8
|
|
quoteBackgroundView.layer.borderWidth = 1
|
|
quoteBackgroundView.layer.borderColor = UIColor.separator.cgColor
|
|
|
|
quoteStatusView.translatesAutoresizingMaskIntoConstraints = false
|
|
quoteBackgroundView.addSubview(quoteStatusView)
|
|
NSLayoutConstraint.activate([
|
|
quoteStatusView.topAnchor.constraint(equalTo: quoteBackgroundView.layoutMarginsGuide.topAnchor),
|
|
quoteStatusView.leadingAnchor.constraint(equalTo: quoteBackgroundView.layoutMarginsGuide.leadingAnchor),
|
|
quoteStatusView.trailingAnchor.constraint(equalTo: quoteBackgroundView.layoutMarginsGuide.trailingAnchor),
|
|
quoteStatusView.bottomAnchor.constraint(equalTo: quoteBackgroundView.layoutMarginsGuide.bottomAnchor),
|
|
])
|
|
quoteStatusView.setup(style: .notificationQuote)
|
|
|
|
statusView.isHidden = true
|
|
quoteStatusViewContainerView.isHidden = true
|
|
|
|
authorNameLabel.isUserInteractionEnabled = false
|
|
authorUsernameLabel.isUserInteractionEnabled = false
|
|
notificationTypeIndicatorLabel.isUserInteractionEnabled = false
|
|
|
|
avatarButton.addTarget(self, action: #selector(NotificationView.avatarButtonDidPressed(_:)), for: .touchUpInside)
|
|
|
|
statusView.delegate = self
|
|
quoteStatusView.delegate = self
|
|
}
|
|
}
|
|
|
|
extension NotificationView {
|
|
@objc private func avatarButtonDidPressed(_ sender: UIButton) {
|
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
|
delegate?.notificationView(self, authorAvatarButtonDidPressed: avatarButton)
|
|
}
|
|
}
|
|
|
|
extension NotificationView {
|
|
|
|
public func setAuthorContainerBottomPaddingViewDisplay() {
|
|
authorContainerViewBottomPaddingView.isHidden = false
|
|
}
|
|
|
|
public func setStatusViewDisplay() {
|
|
statusView.isHidden = false
|
|
}
|
|
|
|
public func setQuoteStatusViewDisplay() {
|
|
quoteStatusViewContainerView.isHidden = false
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - AdaptiveContainerView
|
|
extension NotificationView: AdaptiveContainerView {
|
|
public func updateContainerViewComponentsLayoutMarginsRelativeArrangementBehavior(isEnabled: Bool) {
|
|
let margin = isEnabled ? StatusView.containerLayoutMargin : .zero
|
|
authorAdaptiveMarginContainerView.margin = margin
|
|
quoteStatusViewContainerView.layoutMargins.left = margin
|
|
quoteStatusViewContainerView.layoutMargins.right = margin
|
|
|
|
statusView.updateContainerViewComponentsLayoutMarginsRelativeArrangementBehavior(isEnabled: isEnabled)
|
|
quoteStatusView.updateContainerViewComponentsLayoutMarginsRelativeArrangementBehavior(isEnabled: true) // always set margins
|
|
}
|
|
}
|
|
|
|
extension NotificationView {
|
|
public typealias AuthorMenuContext = StatusView.AuthorMenuContext
|
|
|
|
public func setupAuthorMenu(menuContext: AuthorMenuContext) -> UIMenu {
|
|
var actions: [MastodonMenu.Action] = []
|
|
|
|
actions = [
|
|
.muteUser(.init(
|
|
name: menuContext.name,
|
|
isMuting: menuContext.isMuting
|
|
)),
|
|
.blockUser(.init(
|
|
name: menuContext.name,
|
|
isBlocking: menuContext.isBlocking
|
|
)),
|
|
.reportUser(
|
|
.init(name: menuContext.name)
|
|
),
|
|
]
|
|
|
|
if menuContext.isMyself {
|
|
actions.append(.deleteStatus)
|
|
}
|
|
|
|
|
|
let menu = MastodonMenu.setupMenu(
|
|
actions: actions,
|
|
delegate: self
|
|
)
|
|
|
|
return menu
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - StatusViewDelegate
|
|
extension NotificationView: StatusViewDelegate {
|
|
|
|
public func statusView(_ statusView: StatusView, headerDidPressed header: UIView) {
|
|
// do nothing
|
|
}
|
|
|
|
public func statusView(_ statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) {
|
|
switch statusView {
|
|
case self.statusView:
|
|
assertionFailure()
|
|
case quoteStatusView:
|
|
delegate?.notificationView(self, quoteStatusView: statusView, authorAvatarButtonDidPressed: button)
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
public func statusView(_ statusView: StatusView, contentSensitiveeToggleButtonDidPressed button: UIButton) {
|
|
assertionFailure()
|
|
}
|
|
|
|
public func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
|
|
switch statusView {
|
|
case self.statusView:
|
|
delegate?.notificationView(self, statusView: statusView, metaText: metaText, didSelectMeta: meta)
|
|
case quoteStatusView:
|
|
delegate?.notificationView(self, quoteStatusView: statusView, metaText: metaText, didSelectMeta: meta)
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
public func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int) {
|
|
switch statusView {
|
|
case self.statusView:
|
|
delegate?.notificationView(self, statusView: statusView, mediaGridContainerView: mediaGridContainerView, mediaView: mediaView, didSelectMediaViewAt: index)
|
|
case quoteStatusView:
|
|
delegate?.notificationView(self, quoteStatusView: statusView, mediaGridContainerView: mediaGridContainerView, mediaView: mediaView, didSelectMediaViewAt: index)
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
public func statusView(_ statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
assertionFailure()
|
|
}
|
|
|
|
public func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) {
|
|
assertionFailure()
|
|
}
|
|
|
|
public func statusView(_ statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action) {
|
|
switch statusView {
|
|
case self.statusView:
|
|
delegate?.notificationView(self, statusView: statusView, actionToolbarContainer: actionToolbarContainer, buttonDidPressed: button, action: action)
|
|
case quoteStatusView:
|
|
assertionFailure()
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
public func statusView(_ statusView: StatusView, menuButton button: UIButton, didSelectAction action: MastodonMenu.Action) {
|
|
assertionFailure()
|
|
}
|
|
|
|
public func statusView(_ statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) {
|
|
switch statusView {
|
|
case self.statusView:
|
|
delegate?.notificationView(self, statusView: statusView, spoilerOverlayViewDidPressed: overlayView)
|
|
case quoteStatusView:
|
|
delegate?.notificationView(self, quoteStatusView: statusView, spoilerOverlayViewDidPressed: overlayView)
|
|
default:
|
|
assertionFailure()
|
|
}
|
|
}
|
|
|
|
// public func statusView(_ statusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView) {
|
|
// switch statusView {
|
|
// case self.statusView:
|
|
// delegate?.notificationView(self, statusView: statusView, spoilerBannerViewDidPressed: bannerView)
|
|
// case quoteStatusView:
|
|
// delegate?.notificationView(self, quoteStatusView: statusView, spoilerBannerViewDidPressed: bannerView)
|
|
// default:
|
|
// assertionFailure()
|
|
// }
|
|
// }
|
|
|
|
public func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton) {
|
|
assertionFailure()
|
|
}
|
|
|
|
public func statusView(_ statusView: StatusView, accessibilityActivate: Void) {
|
|
assertionFailure()
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - MastodonMenuDelegate
|
|
extension NotificationView: MastodonMenuDelegate {
|
|
public func menuAction(_ action: MastodonMenu.Action) {
|
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
|
|
|
delegate?.notificationView(self, menuButton: menuButton, didSelectAction: action)
|
|
}
|
|
}
|