Merge pull request #491 from j-f1/post-author-a11y
Improve accessibility of the thread screen
This commit is contained in:
commit
d50b8b94aa
|
@ -79,7 +79,7 @@ extension StatusThreadRootTableViewCell {
|
|||
statusView.delegate = self
|
||||
|
||||
// a11y
|
||||
statusView.contentMetaText.textView.isAccessibilityElement = false
|
||||
statusView.contentMetaText.textView.isAccessibilityElement = true
|
||||
statusView.contentMetaText.textView.isSelectable = true
|
||||
}
|
||||
|
||||
|
@ -97,25 +97,17 @@ extension StatusThreadRootTableViewCell {
|
|||
get {
|
||||
var elements = [
|
||||
statusView.headerContainerView,
|
||||
statusView.avatarButton,
|
||||
statusView.authorNameLabel,
|
||||
statusView.menuButton,
|
||||
statusView.authorUsernameLabel,
|
||||
statusView.dateLabel,
|
||||
statusView.contentSensitiveeToggleButton,
|
||||
statusView.spoilerOverlayView,
|
||||
statusView.contentMetaText.textView,
|
||||
statusView.authorView,
|
||||
statusView.viewModel.isContentReveal
|
||||
? statusView.contentMetaText.textView
|
||||
: statusView.spoilerOverlayView,
|
||||
statusView.mediaGridContainerView,
|
||||
statusView.pollTableView,
|
||||
statusView.pollStatusStackView,
|
||||
statusView.actionToolbarContainer,
|
||||
statusView.statusMetricView
|
||||
statusView.actionToolbarContainer
|
||||
// statusMetricView is intentionally excluded
|
||||
]
|
||||
|
||||
if !statusView.viewModel.isMediaSensitive {
|
||||
elements.removeAll(where: { $0 === statusView.contentSensitiveeToggleButton })
|
||||
}
|
||||
|
||||
if statusView.viewModel.isContentReveal {
|
||||
elements.removeAll(where: { $0 === statusView.spoilerOverlayView })
|
||||
} else {
|
||||
|
|
|
@ -106,6 +106,12 @@ extension ThreadViewController {
|
|||
|
||||
tableView.deselectRow(with: transitionCoordinator, animated: animated)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
UIAccessibility.post(notification: .screenChanged, argument: tableView)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -439,7 +439,7 @@ extension NotificationView: AdaptiveContainerView {
|
|||
}
|
||||
|
||||
extension NotificationView {
|
||||
public typealias AuthorMenuContext = StatusView.AuthorMenuContext
|
||||
public typealias AuthorMenuContext = StatusAuthorView.AuthorMenuContext
|
||||
|
||||
public func setupAuthorMenu(menuContext: AuthorMenuContext) -> UIMenu {
|
||||
var actions: [MastodonMenu.Action] = []
|
||||
|
|
|
@ -0,0 +1,305 @@
|
|||
//
|
||||
// StatusAuthorView.swift
|
||||
//
|
||||
//
|
||||
// Created by Jed Fox on 2022-10-31.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import Meta
|
||||
import MetaTextKit
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
|
||||
public class StatusAuthorView: UIStackView {
|
||||
let logger = Logger(subsystem: "StatusAuthorView", category: "View")
|
||||
private var _disposeBag = Set<AnyCancellable>() // which lifetime same to view scope
|
||||
|
||||
weak var statusView: StatusView?
|
||||
|
||||
// accessibility actions
|
||||
var authorActions = [UIAccessibilityCustomAction]()
|
||||
|
||||
// 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)
|
||||
button.expandEdgeInsets = UIEdgeInsets(top: -20, left: -10, bottom: -5, right: -10)
|
||||
button.tintColor = Asset.Colors.Label.secondary.color
|
||||
let image = UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(font: .systemFont(ofSize: 15)))
|
||||
button.setImage(image, for: .normal)
|
||||
button.accessibilityLabel = L10n.Common.Controls.Status.Actions.menu
|
||||
return button
|
||||
}()
|
||||
|
||||
public let contentSensitiveeToggleButton: UIButton = {
|
||||
let button = HitTestExpandedButton(type: .system)
|
||||
button.expandEdgeInsets = UIEdgeInsets(top: -5, left: -10, bottom: -20, right: -10)
|
||||
button.tintColor = Asset.Colors.Label.secondary.color
|
||||
button.imageView?.contentMode = .scaleAspectFill
|
||||
button.imageView?.clipsToBounds = false
|
||||
let image = UIImage(systemName: "eye.slash.fill", withConfiguration: UIImage.SymbolConfiguration(font: .systemFont(ofSize: 15)))
|
||||
button.setImage(image, for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
func layout(style: StatusView.Style) {
|
||||
switch style {
|
||||
case .inline: layoutBase()
|
||||
case .plain: layoutBase()
|
||||
case .report: layoutReport()
|
||||
case .notification: layoutBase()
|
||||
case .notificationQuote: layoutNotificationQuote()
|
||||
case .composeStatusReplica: layoutComposeStatusReplica()
|
||||
case .composeStatusAuthor: layoutComposeStatusAuthor()
|
||||
}
|
||||
}
|
||||
|
||||
public override var accessibilityElements: [Any]? {
|
||||
get { [] }
|
||||
set {}
|
||||
}
|
||||
|
||||
public override var accessibilityCustomActions: [UIAccessibilityCustomAction]? {
|
||||
get {
|
||||
var actions = authorActions
|
||||
if !contentSensitiveeToggleButton.isHidden {
|
||||
actions.append(UIAccessibilityCustomAction(
|
||||
name: contentSensitiveeToggleButton.accessibilityLabel!,
|
||||
image: contentSensitiveeToggleButton.image(for: .normal),
|
||||
actionHandler: { _ in
|
||||
self.contentSensitiveeToggleButtonDidPressed(self.contentSensitiveeToggleButton)
|
||||
return true
|
||||
}
|
||||
))
|
||||
}
|
||||
return actions
|
||||
}
|
||||
set {}
|
||||
}
|
||||
|
||||
public override func accessibilityActivate() -> Bool {
|
||||
guard let statusView = statusView else { return false }
|
||||
statusView.delegate?.statusView(statusView, authorAvatarButtonDidPressed: avatarButton)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusAuthorView {
|
||||
func _init() {
|
||||
axis = .horizontal
|
||||
spacing = 12
|
||||
isAccessibilityElement = true
|
||||
|
||||
UIContentSizeCategory.publisher
|
||||
.sink { [unowned self] category in
|
||||
axis = category > .accessibilityLarge ? .vertical : .horizontal
|
||||
alignment = category > .accessibilityLarge ? .leading : .center
|
||||
}
|
||||
.store(in: &_disposeBag)
|
||||
|
||||
// avatar button
|
||||
avatarButton.addTarget(self, action: #selector(StatusAuthorView.authorAvatarButtonDidPressed(_:)), for: .touchUpInside)
|
||||
authorNameLabel.isUserInteractionEnabled = false
|
||||
authorUsernameLabel.isUserInteractionEnabled = false
|
||||
|
||||
// contentSensitiveeToggleButton
|
||||
contentSensitiveeToggleButton.addTarget(self, action: #selector(StatusAuthorView.contentSensitiveeToggleButtonDidPressed(_:)), for: .touchUpInside)
|
||||
|
||||
// dateLabel
|
||||
dateLabel.isUserInteractionEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusAuthorView {
|
||||
|
||||
public struct AuthorMenuContext {
|
||||
public let name: String
|
||||
|
||||
public let isMuting: Bool
|
||||
public let isBlocking: Bool
|
||||
public let isMyself: Bool
|
||||
public let isBookmarking: Bool
|
||||
}
|
||||
|
||||
public func setupAuthorMenu(menuContext: AuthorMenuContext) -> (UIMenu, [UIAccessibilityCustomAction]) {
|
||||
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)
|
||||
),
|
||||
.bookmarkStatus(
|
||||
.init(isBookmarking: menuContext.isBookmarking)
|
||||
),
|
||||
.shareStatus
|
||||
]
|
||||
|
||||
if menuContext.isMyself {
|
||||
actions.append(.deleteStatus)
|
||||
}
|
||||
|
||||
|
||||
let menu = MastodonMenu.setupMenu(
|
||||
actions: actions,
|
||||
delegate: self.statusView!
|
||||
)
|
||||
|
||||
let accessibilityActions = MastodonMenu.setupAccessibilityActions(
|
||||
actions: actions,
|
||||
delegate: self.statusView!
|
||||
)
|
||||
|
||||
return (menu, accessibilityActions)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusAuthorView {
|
||||
@objc private func authorAvatarButtonDidPressed(_ sender: UIButton) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
guard let statusView = statusView else { return }
|
||||
statusView.delegate?.statusView(statusView, authorAvatarButtonDidPressed: avatarButton)
|
||||
}
|
||||
|
||||
@objc private func contentSensitiveeToggleButtonDidPressed(_ sender: UIButton) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
guard let statusView = statusView else { return }
|
||||
statusView.delegate?.statusView(statusView, contentSensitiveeToggleButtonDidPressed: sender)
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusAuthorView {
|
||||
// author container: H - [ avatarButton | authorMetaContainer ]
|
||||
private func layoutBase() {
|
||||
// avatarButton
|
||||
let authorAvatarButtonSize = CGSize(width: 46, height: 46)
|
||||
avatarButton.size = authorAvatarButtonSize
|
||||
avatarButton.avatarImageView.imageViewSize = authorAvatarButtonSize
|
||||
avatarButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
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)
|
||||
|
||||
// authorMetaContainer: V - [ authorPrimaryMetaContainer | authorSecondaryMetaContainer ]
|
||||
let authorMetaContainer = UIStackView()
|
||||
authorMetaContainer.axis = .vertical
|
||||
authorMetaContainer.spacing = 4
|
||||
addArrangedSubview(authorMetaContainer)
|
||||
|
||||
// authorPrimaryMetaContainer: H - [ authorNameLabel | (padding) | menuButton ]
|
||||
let authorPrimaryMetaContainer = UIStackView()
|
||||
authorPrimaryMetaContainer.axis = .horizontal
|
||||
authorPrimaryMetaContainer.spacing = 10
|
||||
authorMetaContainer.addArrangedSubview(authorPrimaryMetaContainer)
|
||||
|
||||
// authorNameLabel
|
||||
authorPrimaryMetaContainer.addArrangedSubview(authorNameLabel)
|
||||
authorNameLabel.setContentHuggingPriority(.required - 10, for: .horizontal)
|
||||
authorNameLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal)
|
||||
authorPrimaryMetaContainer.addArrangedSubview(UIView())
|
||||
// menuButton
|
||||
authorPrimaryMetaContainer.addArrangedSubview(menuButton)
|
||||
menuButton.setContentHuggingPriority(.required - 2, for: .horizontal)
|
||||
menuButton.setContentCompressionResistancePriority(.required - 2, for: .horizontal)
|
||||
|
||||
// authorSecondaryMetaContainer: H - [ authorUsername | usernameTrialingDotLabel | dateLabel | (padding) | contentSensitiveeToggleButton ]
|
||||
let authorSecondaryMetaContainer = UIStackView()
|
||||
authorSecondaryMetaContainer.axis = .horizontal
|
||||
authorSecondaryMetaContainer.spacing = 4
|
||||
authorMetaContainer.addArrangedSubview(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())
|
||||
contentSensitiveeToggleButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
authorSecondaryMetaContainer.addArrangedSubview(contentSensitiveeToggleButton)
|
||||
NSLayoutConstraint.activate([
|
||||
contentSensitiveeToggleButton.heightAnchor.constraint(equalTo: authorUsernameLabel.heightAnchor, multiplier: 1.0).priority(.required - 1),
|
||||
contentSensitiveeToggleButton.widthAnchor.constraint(equalTo: contentSensitiveeToggleButton.heightAnchor, multiplier: 1.0).priority(.required - 1),
|
||||
])
|
||||
authorUsernameLabel.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||
authorUsernameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||
contentSensitiveeToggleButton.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||
contentSensitiveeToggleButton.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
contentSensitiveeToggleButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
contentSensitiveeToggleButton.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
||||
}
|
||||
|
||||
func layoutReport() {
|
||||
layoutBase()
|
||||
|
||||
menuButton.removeFromSuperview()
|
||||
}
|
||||
|
||||
func layoutNotificationQuote() {
|
||||
layoutBase()
|
||||
|
||||
contentSensitiveeToggleButton.removeFromSuperview()
|
||||
menuButton.removeFromSuperview()
|
||||
}
|
||||
|
||||
func layoutComposeStatusReplica() {
|
||||
layoutBase()
|
||||
|
||||
avatarButton.isUserInteractionEnabled = false
|
||||
menuButton.removeFromSuperview()
|
||||
}
|
||||
|
||||
func layoutComposeStatusAuthor() {
|
||||
layoutBase()
|
||||
|
||||
avatarButton.isUserInteractionEnabled = false
|
||||
menuButton.removeFromSuperview()
|
||||
usernameTrialingDotLabel.removeFromSuperview()
|
||||
dateLabel.removeFromSuperview()
|
||||
}
|
||||
}
|
|
@ -212,6 +212,7 @@ extension StatusView.ViewModel {
|
|||
}
|
||||
|
||||
private func bindAuthor(statusView: StatusView) {
|
||||
let authorView = statusView.authorView
|
||||
// avatar
|
||||
Publishers.CombineLatest(
|
||||
$authorAvatarImage.removeDuplicates(),
|
||||
|
@ -225,26 +226,27 @@ extension StatusView.ViewModel {
|
|||
return AvatarImageView.Configuration(url: url)
|
||||
}
|
||||
}()
|
||||
statusView.avatarButton.avatarImageView.configure(configuration: configuration)
|
||||
statusView.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 12)))
|
||||
authorView.avatarButton.avatarImageView.configure(configuration: configuration)
|
||||
authorView.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 12)))
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
// name
|
||||
$authorName
|
||||
.sink { metaContent in
|
||||
let metaContent = metaContent ?? PlaintextMetaContent(string: " ")
|
||||
statusView.authorNameLabel.configure(content: metaContent)
|
||||
authorView.authorNameLabel.configure(content: metaContent)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
// username
|
||||
$authorUsername
|
||||
let usernamePublisher = $authorUsername
|
||||
.map { text -> String in
|
||||
guard let text = text else { return "" }
|
||||
return "@\(text)"
|
||||
}
|
||||
usernamePublisher
|
||||
.sink { username in
|
||||
let metaContent = PlaintextMetaContent(string: username)
|
||||
statusView.authorUsernameLabel.configure(content: metaContent)
|
||||
authorView.authorUsernameLabel.configure(content: metaContent)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
// timestamp
|
||||
|
@ -265,9 +267,21 @@ extension StatusView.ViewModel {
|
|||
$timestampText
|
||||
.sink { [weak self] text in
|
||||
guard let _ = self else { return }
|
||||
statusView.dateLabel.configure(content: PlaintextMetaContent(string: text))
|
||||
authorView.dateLabel.configure(content: PlaintextMetaContent(string: text))
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// accessibility label
|
||||
Publishers.CombineLatest4($authorName, usernamePublisher, $timestampText, $timestamp)
|
||||
.map { name, username, timestampText, timestamp in
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeStyle = .short
|
||||
let longTimestamp = timestamp.map { formatter.string(from: $0) } ?? ""
|
||||
return "\(name?.string ?? "") \(username), \(timestampText). \(longTimestamp)"
|
||||
}
|
||||
.assign(to: \.accessibilityLabel, on: authorView)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
private func bindContent(statusView: StatusView) {
|
||||
|
@ -330,7 +344,7 @@ extension StatusView.ViewModel {
|
|||
// eye: when media is hidden
|
||||
// eye-slash: when media display
|
||||
let image = isSensitiveToggled ? UIImage(systemName: "eye.slash.fill") : UIImage(systemName: "eye.fill")
|
||||
statusView.contentSensitiveeToggleButton.setImage(image, for: .normal)
|
||||
statusView.authorView.contentSensitiveeToggleButton.setImage(image, for: .normal)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
@ -570,6 +584,7 @@ extension StatusView.ViewModel {
|
|||
}
|
||||
|
||||
private func bindMenu(statusView: StatusView) {
|
||||
let authorView = statusView.authorView
|
||||
let publisherOne = Publishers.CombineLatest(
|
||||
$authorName,
|
||||
$isMyself
|
||||
|
@ -589,19 +604,21 @@ extension StatusView.ViewModel {
|
|||
let (isMuting, isBlocking, isBookmark) = tupleTwo
|
||||
|
||||
guard let name = authorName?.string else {
|
||||
statusView.menuButton.menu = nil
|
||||
statusView.authorView.menuButton.menu = nil
|
||||
return
|
||||
}
|
||||
|
||||
let menuContext = StatusView.AuthorMenuContext(
|
||||
let menuContext = StatusAuthorView.AuthorMenuContext(
|
||||
name: name,
|
||||
isMuting: isMuting,
|
||||
isBlocking: isBlocking,
|
||||
isMyself: isMyself,
|
||||
isBookmarking: isBookmark
|
||||
)
|
||||
statusView.menuButton.menu = statusView.setupAuthorMenu(menuContext: menuContext)
|
||||
statusView.menuButton.showsMenuAsPrimaryAction = true
|
||||
let (menu, actions) = authorView.setupAuthorMenu(menuContext: menuContext)
|
||||
authorView.menuButton.menu = menu
|
||||
authorView.authorActions = actions
|
||||
authorView.menuButton.showsMenuAsPrimaryAction = true
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
@ -669,7 +686,7 @@ extension StatusView.ViewModel {
|
|||
isContentReveal ? L10n.Scene.Compose.Accessibility.enableContentWarning : L10n.Scene.Compose.Accessibility.disableContentWarning
|
||||
}
|
||||
.sink { label in
|
||||
statusView.contentSensitiveeToggleButton.accessibilityLabel = label
|
||||
statusView.authorView.contentSensitiveeToggleButton.accessibilityLabel = label
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
|
|
@ -76,52 +76,8 @@ public final class StatusView: UIView {
|
|||
|
||||
// author
|
||||
let authorAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
||||
let authorContainerView: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.spacing = 12
|
||||
return stackView
|
||||
}()
|
||||
|
||||
// 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
|
||||
}()
|
||||
public let authorView = StatusAuthorView()
|
||||
|
||||
// timestamp
|
||||
public let dateLabel = MetaLabel(style: .statusUsername)
|
||||
|
||||
public let menuButton: UIButton = {
|
||||
let button = HitTestExpandedButton(type: .system)
|
||||
button.expandEdgeInsets = UIEdgeInsets(top: -20, left: -10, bottom: -5, right: -10)
|
||||
button.tintColor = Asset.Colors.Label.secondary.color
|
||||
let image = UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(font: .systemFont(ofSize: 15)))
|
||||
button.setImage(image, for: .normal)
|
||||
button.accessibilityLabel = L10n.Common.Controls.Status.Actions.menu
|
||||
return button
|
||||
}()
|
||||
|
||||
public let contentSensitiveeToggleButton: UIButton = {
|
||||
let button = HitTestExpandedButton(type: .system)
|
||||
button.expandEdgeInsets = UIEdgeInsets(top: -5, left: -10, bottom: -20, right: -10)
|
||||
button.tintColor = Asset.Colors.Label.secondary.color
|
||||
button.imageView?.contentMode = .scaleAspectFill
|
||||
button.imageView?.clipsToBounds = false
|
||||
let image = UIImage(systemName: "eye.slash.fill", withConfiguration: UIImage.SymbolConfiguration(font: .systemFont(ofSize: 15)))
|
||||
button.setImage(image, for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
// content
|
||||
let contentAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
||||
let contentContainer = UIStackView()
|
||||
|
@ -240,7 +196,7 @@ public final class StatusView: UIView {
|
|||
viewModel.objects.removeAll()
|
||||
viewModel.prepareForReuse()
|
||||
|
||||
avatarButton.avatarImageView.cancelTask()
|
||||
authorView.avatarButton.avatarImageView.cancelTask()
|
||||
if var snapshot = pollTableViewDiffableDataSource?.snapshot() {
|
||||
snapshot.deleteAllItems()
|
||||
if #available(iOS 15.0, *) {
|
||||
|
@ -289,18 +245,10 @@ extension StatusView {
|
|||
let headerTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||
headerTapGestureRecognizer.addTarget(self, action: #selector(StatusView.headerDidPressed(_:)))
|
||||
headerContainerView.addGestureRecognizer(headerTapGestureRecognizer)
|
||||
|
||||
// avatar button
|
||||
avatarButton.addTarget(self, action: #selector(StatusView.authorAvatarButtonDidPressed(_:)), for: .touchUpInside)
|
||||
authorNameLabel.isUserInteractionEnabled = false
|
||||
authorUsernameLabel.isUserInteractionEnabled = false
|
||||
|
||||
// contentSensitiveeToggleButton
|
||||
contentSensitiveeToggleButton.addTarget(self, action: #selector(StatusView.contentSensitiveeToggleButtonDidPressed(_:)), for: .touchUpInside)
|
||||
|
||||
// dateLabel
|
||||
dateLabel.isUserInteractionEnabled = false
|
||||
|
||||
|
||||
// author view
|
||||
authorView.statusView = self
|
||||
|
||||
// content warning
|
||||
let spoilerOverlayViewTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||
spoilerOverlayView.addGestureRecognizer(spoilerOverlayViewTapGestureRecognizer)
|
||||
|
@ -337,16 +285,6 @@ extension StatusView {
|
|||
assert(sender.view === headerContainerView)
|
||||
delegate?.statusView(self, headerDidPressed: headerContainerView)
|
||||
}
|
||||
|
||||
@objc private func authorAvatarButtonDidPressed(_ sender: UIButton) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
delegate?.statusView(self, authorAvatarButtonDidPressed: avatarButton)
|
||||
}
|
||||
|
||||
@objc private func contentSensitiveeToggleButtonDidPressed(_ sender: UIButton) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
delegate?.statusView(self, contentSensitiveeToggleButtonDidPressed: sender)
|
||||
}
|
||||
|
||||
@objc private func pollVoteButtonDidPressed(_ sender: UIButton) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
|
@ -395,6 +333,8 @@ extension StatusView.Style {
|
|||
case .composeStatusReplica: composeStatusReplica(statusView: statusView)
|
||||
case .composeStatusAuthor: composeStatusAuthor(statusView: statusView)
|
||||
}
|
||||
|
||||
statusView.authorView.layout(style: self)
|
||||
}
|
||||
|
||||
private func base(statusView: StatusView) {
|
||||
|
@ -426,81 +366,9 @@ extension StatusView.Style {
|
|||
statusView.headerIconImageView.setContentCompressionResistancePriority(.defaultLow - 100, for: .vertical)
|
||||
statusView.headerIconImageView.setContentCompressionResistancePriority(.defaultLow - 100, for: .horizontal)
|
||||
|
||||
// author container: H - [ avatarButton | author meta container | contentWarningToggleButton ]
|
||||
statusView.authorAdaptiveMarginContainerView.contentView = statusView.authorContainerView
|
||||
statusView.authorAdaptiveMarginContainerView.contentView = statusView.authorView
|
||||
statusView.authorAdaptiveMarginContainerView.margin = StatusView.containerLayoutMargin
|
||||
statusView.containerStackView.addArrangedSubview(statusView.authorAdaptiveMarginContainerView)
|
||||
|
||||
UIContentSizeCategory.publisher
|
||||
.sink { category in
|
||||
statusView.authorContainerView.axis = category > .accessibilityLarge ? .vertical : .horizontal
|
||||
statusView.authorContainerView.alignment = category > .accessibilityLarge ? .leading : .center
|
||||
}
|
||||
.store(in: &statusView._disposeBag)
|
||||
|
||||
// avatarButton
|
||||
let authorAvatarButtonSize = CGSize(width: 46, height: 46)
|
||||
statusView.avatarButton.size = authorAvatarButtonSize
|
||||
statusView.avatarButton.avatarImageView.imageViewSize = authorAvatarButtonSize
|
||||
statusView.avatarButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
statusView.authorContainerView.addArrangedSubview(statusView.avatarButton)
|
||||
NSLayoutConstraint.activate([
|
||||
statusView.avatarButton.widthAnchor.constraint(equalToConstant: authorAvatarButtonSize.width).priority(.required - 1),
|
||||
statusView.avatarButton.heightAnchor.constraint(equalToConstant: authorAvatarButtonSize.height).priority(.required - 1),
|
||||
])
|
||||
statusView.avatarButton.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||
statusView.avatarButton.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||
|
||||
// authrMetaContainer: V - [ authorPrimaryMetaContainer | authorSecondaryMetaContainer ]
|
||||
let authorMetaContainer = UIStackView()
|
||||
authorMetaContainer.axis = .vertical
|
||||
authorMetaContainer.spacing = 4
|
||||
statusView.authorContainerView.addArrangedSubview(authorMetaContainer)
|
||||
|
||||
// authorPrimaryMetaContainer: H - [ authorNameLabel | (padding) | menuButton ]
|
||||
let authorPrimaryMetaContainer = UIStackView()
|
||||
authorPrimaryMetaContainer.axis = .horizontal
|
||||
authorPrimaryMetaContainer.spacing = 10
|
||||
authorMetaContainer.addArrangedSubview(authorPrimaryMetaContainer)
|
||||
|
||||
// authorNameLabel
|
||||
authorPrimaryMetaContainer.addArrangedSubview(statusView.authorNameLabel)
|
||||
statusView.authorNameLabel.setContentHuggingPriority(.required - 10, for: .horizontal)
|
||||
statusView.authorNameLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal)
|
||||
authorPrimaryMetaContainer.addArrangedSubview(UIView())
|
||||
// menuButton
|
||||
authorPrimaryMetaContainer.addArrangedSubview(statusView.menuButton)
|
||||
statusView.menuButton.setContentHuggingPriority(.required - 2, for: .horizontal)
|
||||
statusView.menuButton.setContentCompressionResistancePriority(.required - 2, for: .horizontal)
|
||||
|
||||
// authorSecondaryMetaContainer: H - [ authorUsername | usernameTrialingDotLabel | dateLabel | (padding) | contentSensitiveeToggleButton ]
|
||||
let authorSecondaryMetaContainer = UIStackView()
|
||||
authorSecondaryMetaContainer.axis = .horizontal
|
||||
authorSecondaryMetaContainer.spacing = 4
|
||||
authorMetaContainer.addArrangedSubview(authorSecondaryMetaContainer)
|
||||
|
||||
authorSecondaryMetaContainer.addArrangedSubview(statusView.authorUsernameLabel)
|
||||
statusView.authorUsernameLabel.setContentHuggingPriority(.required - 8, for: .horizontal)
|
||||
statusView.authorUsernameLabel.setContentCompressionResistancePriority(.required - 8, for: .horizontal)
|
||||
authorSecondaryMetaContainer.addArrangedSubview(statusView.usernameTrialingDotLabel)
|
||||
statusView.usernameTrialingDotLabel.setContentHuggingPriority(.required - 2, for: .horizontal)
|
||||
statusView.usernameTrialingDotLabel.setContentCompressionResistancePriority(.required - 2, for: .horizontal)
|
||||
authorSecondaryMetaContainer.addArrangedSubview(statusView.dateLabel)
|
||||
statusView.dateLabel.setContentHuggingPriority(.required - 1, for: .horizontal)
|
||||
statusView.dateLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||
authorSecondaryMetaContainer.addArrangedSubview(UIView())
|
||||
statusView.contentSensitiveeToggleButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
authorSecondaryMetaContainer.addArrangedSubview(statusView.contentSensitiveeToggleButton)
|
||||
NSLayoutConstraint.activate([
|
||||
statusView.contentSensitiveeToggleButton.heightAnchor.constraint(equalTo: statusView.authorUsernameLabel.heightAnchor, multiplier: 1.0).priority(.required - 1),
|
||||
statusView.contentSensitiveeToggleButton.widthAnchor.constraint(equalTo: statusView.contentSensitiveeToggleButton.heightAnchor, multiplier: 1.0).priority(.required - 1),
|
||||
])
|
||||
statusView.authorUsernameLabel.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||
statusView.authorUsernameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||
statusView.contentSensitiveeToggleButton.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||
statusView.contentSensitiveeToggleButton.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
statusView.contentSensitiveeToggleButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
statusView.contentSensitiveeToggleButton.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
||||
|
||||
// content container: V - [ contentMetaText ]
|
||||
statusView.contentContainer.axis = .vertical
|
||||
|
@ -606,7 +474,6 @@ extension StatusView.Style {
|
|||
func report(statusView: StatusView) {
|
||||
base(statusView: statusView) // override the base style
|
||||
|
||||
statusView.menuButton.removeFromSuperview()
|
||||
statusView.actionToolbarAdaptiveMarginContainerView.removeFromSuperview()
|
||||
}
|
||||
|
||||
|
@ -622,26 +489,18 @@ extension StatusView.Style {
|
|||
|
||||
statusView.contentAdaptiveMarginContainerView.bottomLayoutConstraint?.constant = 16 // fix bottom margin missing issue
|
||||
statusView.pollAdaptiveMarginContainerView.bottomLayoutConstraint?.constant = 16 // fix bottom margin missing issue
|
||||
statusView.contentSensitiveeToggleButton.removeFromSuperview()
|
||||
statusView.menuButton.removeFromSuperview()
|
||||
statusView.actionToolbarAdaptiveMarginContainerView.removeFromSuperview()
|
||||
}
|
||||
|
||||
func composeStatusReplica(statusView: StatusView) {
|
||||
base(statusView: statusView)
|
||||
|
||||
statusView.avatarButton.isUserInteractionEnabled = false
|
||||
statusView.menuButton.removeFromSuperview()
|
||||
statusView.actionToolbarAdaptiveMarginContainerView.removeFromSuperview()
|
||||
}
|
||||
|
||||
func composeStatusAuthor(statusView: StatusView) {
|
||||
base(statusView: statusView)
|
||||
|
||||
statusView.avatarButton.isUserInteractionEnabled = false
|
||||
statusView.menuButton.removeFromSuperview()
|
||||
statusView.usernameTrialingDotLabel.removeFromSuperview()
|
||||
statusView.dateLabel.removeFromSuperview()
|
||||
statusView.contentAdaptiveMarginContainerView.removeFromSuperview()
|
||||
statusView.spoilerOverlayView.removeFromSuperview()
|
||||
statusView.mediaContainerView.removeFromSuperview()
|
||||
|
@ -657,7 +516,7 @@ extension StatusView {
|
|||
}
|
||||
|
||||
func setContentSensitiveeToggleButtonDisplay(isDisplay: Bool = true) {
|
||||
contentSensitiveeToggleButton.isHidden = !isDisplay
|
||||
authorView.contentSensitiveeToggleButton.isHidden = !isDisplay
|
||||
}
|
||||
|
||||
func setSpoilerOverlayViewHidden(isHidden: Bool) {
|
||||
|
@ -697,53 +556,6 @@ extension StatusView: AdaptiveContainerView {
|
|||
}
|
||||
}
|
||||
|
||||
extension StatusView {
|
||||
|
||||
public struct AuthorMenuContext {
|
||||
public let name: String
|
||||
|
||||
public let isMuting: Bool
|
||||
public let isBlocking: Bool
|
||||
public let isMyself: Bool
|
||||
public let isBookmarking: Bool
|
||||
}
|
||||
|
||||
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)
|
||||
),
|
||||
.bookmarkStatus(
|
||||
.init(isBookmarking: menuContext.isBookmarking)
|
||||
),
|
||||
.shareStatus
|
||||
]
|
||||
|
||||
if menuContext.isMyself {
|
||||
actions.append(.deleteStatus)
|
||||
}
|
||||
|
||||
|
||||
let menu = MastodonMenu.setupMenu(
|
||||
actions: actions,
|
||||
delegate: self
|
||||
)
|
||||
|
||||
return menu
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITextViewDelegate
|
||||
extension StatusView: UITextViewDelegate {
|
||||
|
||||
|
@ -812,6 +624,14 @@ extension StatusView: ActionToolbarContainerDelegate {
|
|||
public func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action) {
|
||||
delegate?.statusView(self, actionToolbarContainer: actionToolbarContainer, buttonDidPressed: button, action: action)
|
||||
}
|
||||
|
||||
public func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, showReblogs action: UIAccessibilityCustomAction) {
|
||||
delegate?.statusView(self, statusMetricView: statusMetricView, reblogButtonDidPressed: statusMetricView.reblogButton)
|
||||
}
|
||||
|
||||
public func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, showFavorites action: UIAccessibilityCustomAction) {
|
||||
delegate?.statusView(self, statusMetricView: statusMetricView, favoriteButtonDidPressed: statusMetricView.favoriteButton)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - StatusMetricViewDelegate
|
||||
|
@ -829,7 +649,7 @@ extension StatusView: StatusMetricViewDelegate {
|
|||
extension StatusView: MastodonMenuDelegate {
|
||||
public func menuAction(_ action: MastodonMenu.Action) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
delegate?.statusView(self, menuButton: menuButton, didSelectAction: action)
|
||||
delegate?.statusView(self, menuButton: authorView.menuButton, didSelectAction: action)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ import MastodonLocalization
|
|||
|
||||
public protocol ActionToolbarContainerDelegate: AnyObject {
|
||||
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action)
|
||||
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, showReblogs action: UIAccessibilityCustomAction)
|
||||
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, showFavorites action: UIAccessibilityCustomAction)
|
||||
}
|
||||
|
||||
public final class ActionToolbarContainer: UIView {
|
||||
|
@ -217,12 +219,14 @@ extension ActionToolbarContainer {
|
|||
public func configureReply(count: Int, isEnabled: Bool) {
|
||||
let title = ActionToolbarContainer.title(from: count)
|
||||
replyButton.setTitle(title, for: .normal)
|
||||
replyButton.accessibilityLabel = L10n.Plural.Count.reply(count)
|
||||
replyButton.accessibilityLabel = L10n.Common.Controls.Actions.reply
|
||||
replyButton.accessibilityValue = L10n.Plural.Count.reply(count)
|
||||
}
|
||||
|
||||
public func configureReblog(count: Int, isEnabled: Bool, isHighlighted: Bool) {
|
||||
let title = ActionToolbarContainer.title(from: count)
|
||||
reblogButton.setTitle(title, for: .normal)
|
||||
reblogButton.accessibilityValue = L10n.Plural.Count.reblog(count)
|
||||
reblogButton.isEnabled = isEnabled
|
||||
reblogButton.setImage(ActionToolbarContainer.reblogImage, for: .normal)
|
||||
let tintColor = isHighlighted ? Asset.Colors.successGreen.color : Asset.Colors.Button.actionToolbar.color
|
||||
|
@ -232,15 +236,24 @@ extension ActionToolbarContainer {
|
|||
|
||||
if isHighlighted {
|
||||
reblogButton.accessibilityTraits.insert(.selected)
|
||||
reblogButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.unreblog
|
||||
} else {
|
||||
reblogButton.accessibilityTraits.remove(.selected)
|
||||
reblogButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.reblog
|
||||
}
|
||||
reblogButton.accessibilityLabel = L10n.Plural.Count.reblog(count)
|
||||
reblogButton.accessibilityCustomActions = [
|
||||
UIAccessibilityCustomAction(name: "Show All Reblogs") { [weak self] action in
|
||||
guard let self = self else { return false }
|
||||
self.delegate?.actionToolbarContainer(self, showReblogs: action)
|
||||
return true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
public func configureFavorite(count: Int, isEnabled: Bool, isHighlighted: Bool) {
|
||||
let title = ActionToolbarContainer.title(from: count)
|
||||
favoriteButton.setTitle(title, for: .normal)
|
||||
favoriteButton.accessibilityValue = L10n.Plural.Count.favorite(count)
|
||||
favoriteButton.isEnabled = isEnabled
|
||||
let image = isHighlighted ? ActionToolbarContainer.starFillImage : ActionToolbarContainer.starImage
|
||||
favoriteButton.setImage(image, for: .normal)
|
||||
|
@ -251,10 +264,18 @@ extension ActionToolbarContainer {
|
|||
|
||||
if isHighlighted {
|
||||
favoriteButton.accessibilityTraits.insert(.selected)
|
||||
favoriteButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.unfavorite
|
||||
} else {
|
||||
favoriteButton.accessibilityTraits.remove(.selected)
|
||||
favoriteButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.favorite
|
||||
}
|
||||
favoriteButton.accessibilityLabel = L10n.Plural.Count.favorite(count)
|
||||
favoriteButton.accessibilityCustomActions = [
|
||||
UIAccessibilityCustomAction(name: "Show All Favorites") { [weak self] action in
|
||||
guard let self = self else { return false }
|
||||
self.delegate?.actionToolbarContainer(self, showFavorites: action)
|
||||
return true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -72,6 +72,7 @@ extension SpoilerOverlayView {
|
|||
spoilerMetaLabel.isUserInteractionEnabled = false
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits.insert(.button)
|
||||
}
|
||||
|
||||
public func setComponentHidden(_ isHidden: Bool) {
|
||||
|
|
|
@ -20,10 +20,22 @@ public enum MastodonMenu {
|
|||
var children: [UIMenuElement] = []
|
||||
for action in actions {
|
||||
let element = action.build(delegate: delegate)
|
||||
children.append(element)
|
||||
children.append(element.menuElement)
|
||||
}
|
||||
return UIMenu(title: "", options: [], children: children)
|
||||
}
|
||||
|
||||
public static func setupAccessibilityActions(
|
||||
actions: [Action],
|
||||
delegate: MastodonMenuDelegate
|
||||
) -> [UIAccessibilityCustomAction] {
|
||||
var accessibilityActions: [UIAccessibilityCustomAction] = []
|
||||
for action in actions {
|
||||
let element = action.build(delegate: delegate)
|
||||
accessibilityActions.append(element.accessibilityCustomAction)
|
||||
}
|
||||
return accessibilityActions
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonMenu {
|
||||
|
@ -36,102 +48,117 @@ extension MastodonMenu {
|
|||
case shareStatus
|
||||
case deleteStatus
|
||||
|
||||
func build(delegate: MastodonMenuDelegate) -> UIMenuElement {
|
||||
func build(delegate: MastodonMenuDelegate) -> BuiltAction {
|
||||
switch self {
|
||||
case .muteUser(let context):
|
||||
let muteAction = UIAction(
|
||||
let muteAction = BuiltAction(
|
||||
title: context.isMuting ? L10n.Common.Controls.Friendship.unmuteUser(context.name) : L10n.Common.Controls.Friendship.muteUser(context.name),
|
||||
image: context.isMuting ? UIImage(systemName: "speaker.wave.2") : UIImage(systemName: "speaker.slash"),
|
||||
identifier: nil,
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
) { [weak delegate] _ in
|
||||
image: context.isMuting ? UIImage(systemName: "speaker.wave.2") : UIImage(systemName: "speaker.slash")
|
||||
) { [weak delegate] in
|
||||
guard let delegate = delegate else { return }
|
||||
delegate.menuAction(self)
|
||||
}
|
||||
return muteAction
|
||||
case .blockUser(let context):
|
||||
let blockAction = UIAction(
|
||||
let blockAction = BuiltAction(
|
||||
title: context.isBlocking ? L10n.Common.Controls.Friendship.unblockUser(context.name) : L10n.Common.Controls.Friendship.blockUser(context.name),
|
||||
image: context.isBlocking ? UIImage(systemName: "hand.raised") : UIImage(systemName: "hand.raised"),
|
||||
identifier: nil,
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
) { [weak delegate] _ in
|
||||
image: context.isBlocking ? UIImage(systemName: "hand.raised") : UIImage(systemName: "hand.raised")
|
||||
) { [weak delegate] in
|
||||
guard let delegate = delegate else { return }
|
||||
delegate.menuAction(self)
|
||||
}
|
||||
return blockAction
|
||||
case .reportUser(let context):
|
||||
let reportAction = UIAction(
|
||||
let reportAction = BuiltAction(
|
||||
title: L10n.Common.Controls.Actions.reportUser(context.name),
|
||||
image: UIImage(systemName: "flag"),
|
||||
identifier: nil,
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
) { [weak delegate] _ in
|
||||
image: UIImage(systemName: "flag")
|
||||
) { [weak delegate] in
|
||||
guard let delegate = delegate else { return }
|
||||
delegate.menuAction(self)
|
||||
}
|
||||
return reportAction
|
||||
case .shareUser(let context):
|
||||
let shareAction = UIAction(
|
||||
let shareAction = BuiltAction(
|
||||
title: L10n.Common.Controls.Actions.shareUser(context.name),
|
||||
image: UIImage(systemName: "square.and.arrow.up"),
|
||||
identifier: nil,
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
) { [weak delegate] _ in
|
||||
image: UIImage(systemName: "square.and.arrow.up")
|
||||
) { [weak delegate] in
|
||||
guard let delegate = delegate else { return }
|
||||
delegate.menuAction(self)
|
||||
}
|
||||
return shareAction
|
||||
case .bookmarkStatus(let context):
|
||||
let action = UIAction(
|
||||
let action = BuiltAction(
|
||||
title: context.isBookmarking ? "Remove Bookmark" : "Bookmark", // TODO: i18n
|
||||
image: context.isBookmarking ? UIImage(systemName: "bookmark.slash.fill") : UIImage(systemName: "bookmark"),
|
||||
identifier: nil,
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
) { [weak delegate] _ in
|
||||
image: context.isBookmarking ? UIImage(systemName: "bookmark.slash.fill") : UIImage(systemName: "bookmark")
|
||||
) { [weak delegate] in
|
||||
guard let delegate = delegate else { return }
|
||||
delegate.menuAction(self)
|
||||
}
|
||||
return action
|
||||
case .shareStatus:
|
||||
let action = UIAction(
|
||||
let action = BuiltAction(
|
||||
title: "Share", // TODO: i18n
|
||||
image: UIImage(systemName: "square.and.arrow.up"),
|
||||
identifier: nil,
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: .off
|
||||
) { [weak delegate] _ in
|
||||
image: UIImage(systemName: "square.and.arrow.up")
|
||||
) { [weak delegate] in
|
||||
guard let delegate = delegate else { return }
|
||||
delegate.menuAction(self)
|
||||
}
|
||||
return action
|
||||
case .deleteStatus:
|
||||
let deleteAction = UIAction(
|
||||
let deleteAction = BuiltAction(
|
||||
title: L10n.Common.Controls.Actions.delete,
|
||||
image: UIImage(systemName: "minus.circle"),
|
||||
identifier: nil,
|
||||
discoverabilityTitle: nil,
|
||||
attributes: .destructive,
|
||||
state: .off
|
||||
) { [weak delegate] _ in
|
||||
attributes: .destructive
|
||||
) { [weak delegate] in
|
||||
guard let delegate = delegate else { return }
|
||||
delegate.menuAction(self)
|
||||
}
|
||||
return UIMenu(options: .displayInline, children: [deleteAction])
|
||||
return deleteAction
|
||||
} // end switch
|
||||
} // end func build
|
||||
} // end enum Action
|
||||
|
||||
struct BuiltAction {
|
||||
init(
|
||||
title: String,
|
||||
image: UIImage? = nil,
|
||||
attributes: UIMenuElement.Attributes = [],
|
||||
state: UIMenuElement.State = .off,
|
||||
handler: @escaping () -> Void
|
||||
) {
|
||||
self.title = title
|
||||
self.image = image
|
||||
self.attributes = attributes
|
||||
self.state = state
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
let title: String
|
||||
let image: UIImage?
|
||||
let attributes: UIMenuElement.Attributes
|
||||
let state: UIMenuElement.State
|
||||
let handler: () -> Void
|
||||
|
||||
var menuElement: UIMenuElement {
|
||||
UIAction(
|
||||
title: title,
|
||||
image: image,
|
||||
identifier: nil,
|
||||
discoverabilityTitle: nil,
|
||||
attributes: attributes,
|
||||
state: .off
|
||||
) { _ in
|
||||
handler()
|
||||
}
|
||||
}
|
||||
|
||||
var accessibilityCustomAction: UIAccessibilityCustomAction {
|
||||
UIAccessibilityCustomAction(name: title, image: image) { _ in
|
||||
handler()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonMenu {
|
||||
|
|
Loading…
Reference in New Issue