259 lines
8.1 KiB
Swift
259 lines
8.1 KiB
Swift
//
|
|
// Copyright 2022 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import SignalServiceKit
|
|
import SignalUI
|
|
|
|
public class MessageAction: NSObject {
|
|
|
|
let block: (_ sender: Any?) -> Void
|
|
let accessibilityIdentifier: String
|
|
let contextMenuTitle: String
|
|
let contextMenuAttributes: ContextMenuAction.Attributes
|
|
|
|
public enum MessageActionType {
|
|
case reply
|
|
case copy
|
|
case info
|
|
case delete
|
|
case share
|
|
case forward
|
|
case select
|
|
case speak
|
|
case stopSpeaking
|
|
case edit
|
|
case showPaymentDetails
|
|
}
|
|
|
|
let actionType: MessageActionType
|
|
|
|
public init(_ actionType: MessageActionType,
|
|
accessibilityLabel: String,
|
|
accessibilityIdentifier: String,
|
|
contextMenuTitle: String,
|
|
contextMenuAttributes: ContextMenuAction.Attributes,
|
|
block: @escaping (_ sender: Any?) -> Void) {
|
|
self.actionType = actionType
|
|
self.accessibilityIdentifier = accessibilityIdentifier
|
|
self.contextMenuTitle = contextMenuTitle
|
|
self.contextMenuAttributes = contextMenuAttributes
|
|
self.block = block
|
|
super.init()
|
|
self.accessibilityLabel = accessibilityLabel
|
|
}
|
|
|
|
var contextMenuIcon: UIImage {
|
|
let icon: ThemeIcon = {
|
|
switch actionType {
|
|
case .reply:
|
|
return .contextMenuReply
|
|
case .copy:
|
|
return .contextMenuCopy
|
|
case .info:
|
|
return .contextMenuInfo
|
|
case .delete:
|
|
return .contextMenuDelete
|
|
case .share:
|
|
return .contextMenuShare
|
|
case .forward:
|
|
return .contextMenuForward
|
|
case .select:
|
|
return .contextMenuSelect
|
|
case .speak:
|
|
return .contextMenuSpeak
|
|
case .stopSpeaking:
|
|
return .contextMenuStopSpeaking
|
|
case .edit:
|
|
return .contextMenuEdit
|
|
case .showPaymentDetails:
|
|
return .settingsPayments
|
|
}
|
|
}()
|
|
return Theme.iconImage(icon)
|
|
}
|
|
|
|
var barButtonImage: UIImage {
|
|
let icon: ThemeIcon = {
|
|
switch actionType {
|
|
case .delete:
|
|
return .buttonDelete
|
|
case .forward:
|
|
return .buttonForward
|
|
default:
|
|
owsFail("Invalid icon")
|
|
}
|
|
}()
|
|
return Theme.iconImage(icon)
|
|
}
|
|
}
|
|
|
|
public protocol MessageActionsToolbarDelegate: AnyObject {
|
|
func messageActionsToolbar(_ messageActionsToolbar: MessageActionsToolbar, executedAction: MessageAction)
|
|
var messageActionsToolbarSelectedInteractionCount: Int { get }
|
|
}
|
|
|
|
public class MessageActionsToolbar: UIToolbar {
|
|
|
|
weak var actionDelegate: MessageActionsToolbarDelegate?
|
|
|
|
enum Mode {
|
|
case normal(messagesActions: [MessageAction])
|
|
case selection(deleteMessagesAction: MessageAction,
|
|
forwardMessagesAction: MessageAction)
|
|
}
|
|
private let mode: Mode
|
|
|
|
init(mode: Mode) {
|
|
self.mode = mode
|
|
|
|
super.init(frame: .zero)
|
|
|
|
isTranslucent = false
|
|
isOpaque = true
|
|
|
|
autoresizingMask = .flexibleHeight
|
|
translatesAutoresizingMaskIntoConstraints = false
|
|
setShadowImage(UIImage(), forToolbarPosition: .any)
|
|
|
|
buildItems()
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(applyTheme), name: .themeDidChange, object: nil)
|
|
applyTheme()
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
private func applyTheme() {
|
|
AssertIsOnMainThread()
|
|
|
|
barTintColor = Theme.isDarkThemeEnabled ? .ows_gray75 : .ows_white
|
|
|
|
buildItems()
|
|
}
|
|
|
|
public func updateContent() {
|
|
buildItems()
|
|
}
|
|
|
|
private func buildItems() {
|
|
switch mode {
|
|
case .normal(let messagesActions):
|
|
buildNormalItems(messagesActions: messagesActions)
|
|
case .selection(let deleteMessagesAction, let forwardMessagesAction):
|
|
buildSelectionItems(deleteMessagesAction: deleteMessagesAction,
|
|
forwardMessagesAction: forwardMessagesAction)
|
|
}
|
|
}
|
|
|
|
var actionItems = [MessageActionsToolbarButton]()
|
|
|
|
private func buildNormalItems(messagesActions: [MessageAction]) {
|
|
var newItems = [UIBarButtonItem]()
|
|
|
|
var actionItems = [MessageActionsToolbarButton]()
|
|
for action in messagesActions {
|
|
if !newItems.isEmpty {
|
|
newItems.append(.flexibleSpace())
|
|
}
|
|
|
|
let actionItem = MessageActionsToolbarButton(actionsToolbar: self, messageAction: action)
|
|
actionItem.tintColor = Theme.primaryIconColor
|
|
actionItem.accessibilityLabel = action.accessibilityLabel
|
|
newItems.append(actionItem)
|
|
actionItems.append(actionItem)
|
|
}
|
|
|
|
// If we only have a single button, center it.
|
|
if newItems.count == 1 {
|
|
newItems.insert(.flexibleSpace(), at: 0)
|
|
newItems.append(.flexibleSpace())
|
|
}
|
|
|
|
items = newItems
|
|
self.actionItems = actionItems
|
|
}
|
|
|
|
private func buildSelectionItems(deleteMessagesAction: MessageAction,
|
|
forwardMessagesAction: MessageAction) {
|
|
|
|
let deleteItem = MessageActionsToolbarButton(actionsToolbar: self, messageAction: deleteMessagesAction)
|
|
let forwardItem = MessageActionsToolbarButton(actionsToolbar: self, messageAction: forwardMessagesAction)
|
|
|
|
let selectedCount: Int = actionDelegate?.messageActionsToolbarSelectedInteractionCount ?? 0
|
|
let labelFormat = OWSLocalizedString("MESSAGE_ACTIONS_TOOLBAR_CAPTION_%d", tableName: "PluralAware",
|
|
comment: "Label for the toolbar used in the multi-select mode. The number of selected items (1 or more) is passed.")
|
|
let labelTitle = String.localizedStringWithFormat(labelFormat, selectedCount)
|
|
let label = UILabel()
|
|
label.text = labelTitle
|
|
label.font = UIFont.dynamicTypeBodyClamped
|
|
label.textColor = Theme.primaryTextColor
|
|
label.sizeToFit()
|
|
let labelItem = UIBarButtonItem(customView: label)
|
|
|
|
var newItems = [UIBarButtonItem]()
|
|
newItems.append(deleteItem)
|
|
newItems.append(.flexibleSpace())
|
|
newItems.append(labelItem)
|
|
newItems.append(.flexibleSpace())
|
|
newItems.append(forwardItem)
|
|
|
|
items = newItems
|
|
self.actionItems = [ deleteItem, forwardItem ]
|
|
}
|
|
|
|
public func buttonItem(for actionType: MessageAction.MessageActionType) -> UIBarButtonItem? {
|
|
for actionItem in actionItems {
|
|
if let messageAction = actionItem.messageAction,
|
|
messageAction.actionType == actionType {
|
|
return actionItem
|
|
}
|
|
}
|
|
owsFailDebug("Missing action item: \(actionType).")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
class MessageActionsToolbarButton: UIBarButtonItem {
|
|
private weak var actionsToolbar: MessageActionsToolbar?
|
|
fileprivate var messageAction: MessageAction?
|
|
|
|
init(actionsToolbar: MessageActionsToolbar, messageAction: MessageAction) {
|
|
self.actionsToolbar = actionsToolbar
|
|
self.messageAction = messageAction
|
|
|
|
super.init()
|
|
|
|
self.image = messageAction.barButtonImage
|
|
self.style = .plain
|
|
self.target = self
|
|
self.action = #selector(didTapItem(_:))
|
|
self.tintColor = Theme.primaryIconColor
|
|
self.accessibilityLabel = messageAction.accessibilityLabel
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
@objc
|
|
private func didTapItem(_ item: UIBarButtonItem) {
|
|
AssertIsOnMainThread()
|
|
|
|
guard let messageAction = messageAction,
|
|
let actionsToolbar = actionsToolbar,
|
|
let actionDelegate = actionsToolbar.actionDelegate else {
|
|
return
|
|
}
|
|
actionDelegate.messageActionsToolbar(actionsToolbar, executedAction: messageAction)
|
|
}
|
|
}
|