TM-SGNL-iOS/Signal/ConversationView/ConversationViewController+MessageActions.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

274 lines
11 KiB
Swift

//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
public import SignalServiceKit
extension ConversationViewController {
func presentContextMenu(with messageActions: [MessageAction],
focusedOn cell: UICollectionViewCell,
andModel model: CVItemViewModelImpl) {
let keyboardActive = inputToolbar?.isInputViewFirstResponder ?? false
let interaction = ChatHistoryContextMenuInteraction(delegate: self, itemViewModel: model, thread: thread, messageActions: messageActions, initiatingGestureRecognizer: collectionViewContextMenuGestureRecognizer, keyboardWasActive: keyboardActive)
collectionViewActiveContextMenuInteraction = interaction
cell.addInteraction(interaction)
let cellCenterPoint = cell.frame.center
let screenPoint = self.collectionView .convert(cellCenterPoint, from: cell)
var presentImmediately = false
if let secondaryClickRecognizer = collectionViewContextMenuSecondaryClickRecognizer, secondaryClickRecognizer.state == .ended {
presentImmediately = true
}
interaction.initiateContextMenuGesture(locationInView: screenPoint, presentImmediately: presentImmediately)
}
public var isPresentingContextMenu: Bool {
if let interaction = viewState.collectionViewActiveContextMenuInteraction, interaction.contextMenuVisible {
return true
}
return false
}
@objc
public func dismissMessageContextMenu(animated: Bool) {
if let collectionViewActiveContextMenuInteraction = self.collectionViewActiveContextMenuInteraction {
collectionViewActiveContextMenuInteraction.dismissMenu(animated: animated, completion: { })
}
}
func dismissMessageContextMenuIfNecessary() {
if shouldDismissMessageContextMenu {
dismissMessageContextMenu(animated: true)
}
}
var shouldDismissMessageContextMenu: Bool {
guard let collectionViewActiveContextMenuInteraction = self.collectionViewActiveContextMenuInteraction else {
return false
}
let messageActionInteractionId = collectionViewActiveContextMenuInteraction.itemViewModel.interaction.uniqueId
// Check whether there is still a view item for this interaction.
return self.indexPath(forInteractionUniqueId: messageActionInteractionId) == nil
}
public func reloadReactionsDetailSheetWithSneakyTransaction() {
AssertIsOnMainThread()
guard let reactionsDetailSheet = self.reactionsDetailSheet else {
return
}
let messageId = reactionsDetailSheet.messageId
guard let indexPath = self.indexPath(forInteractionUniqueId: messageId),
let renderItem = self.renderItem(forIndex: indexPath.row) else {
// The message no longer exists, dismiss the sheet.
dismissReactionsDetailSheet(animated: true)
return
}
guard let reactionState = renderItem.reactionState,
reactionState.hasReactions else {
// There are no longer reactions on this message, dismiss the sheet.
dismissReactionsDetailSheet(animated: true)
return
}
// Update the detail sheet with the latest reaction
// state, in case the reactions have changed.
SSKEnvironment.shared.databaseStorageRef.read { tx in
reactionsDetailSheet.setReactionState(reactionState, transaction: tx)
}
}
public func dismissReactionsDetailSheet(animated: Bool) {
AssertIsOnMainThread()
guard let reactionsDetailSheet = self.reactionsDetailSheet else {
return
}
reactionsDetailSheet.dismiss(animated: animated) {
self.reactionsDetailSheet = nil
}
}
}
extension ConversationViewController: ContextMenuInteractionDelegate {
public func contextMenuInteraction(
_ interaction: ContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint) -> ContextMenuConfiguration? {
return ContextMenuConfiguration(identifier: UUID() as NSCopying, actionProvider: { [weak self] _ in
guard let self = self else {
owsFailDebug("conversationViewController was unexpectedly nil")
return ContextMenu([])
}
var contextMenuActions: [ContextMenuAction] = []
if let actions = self.collectionViewActiveContextMenuInteraction?.messageActions {
let actionOrder: [MessageAction.MessageActionType] = [
.reply,
.forward,
.edit,
.copy,
.share,
.select,
.showPaymentDetails,
.speak,
.stopSpeaking,
.info,
.delete
]
for type in actionOrder {
let actionWithType = actions.first { $0.actionType == type }
if let messageAction = actionWithType {
contextMenuActions.append(ContextMenuAction(
title: messageAction.contextMenuTitle,
image: messageAction.contextMenuIcon,
attributes: messageAction.contextMenuAttributes,
handler: { _ in
messageAction.block(nil)
}
))
}
}
}
return ContextMenu(contextMenuActions)
})
}
public func contextMenuInteraction(
_ interaction: ContextMenuInteraction,
previewForHighlightingMenuWithConfiguration configuration: ContextMenuConfiguration) -> ContextMenuTargetedPreview? {
guard let contextInteraction = interaction as? ChatHistoryContextMenuInteraction else {
owsFailDebug("Expected ChatHistoryContextMenuInteraction.")
return nil
}
guard let cell = contextInteraction.view as? CVCell else {
owsFailDebug("Expected context interaction view to be of CVCell type")
return nil
}
guard let componentView = cell.componentView else {
owsFailDebug("Expected cell to have component view")
return nil
}
var accessories = cell.rootComponent?.contextMenuAccessoryViews(componentView: componentView) ?? []
// Add reaction bar if necessary
if thread.canSendReactionToThread && shouldShowReactionPickerForInteraction(contextInteraction.itemViewModel.interaction) {
let reactionBarAccessory = ContextMenuRectionBarAccessory(thread: self.thread, itemViewModel: contextInteraction.itemViewModel)
reactionBarAccessory.didSelectReactionHandler = { [weak self] (message: TSMessage, reaction: String, isRemoving: Bool) in
guard self != nil else {
owsFailDebug("conversationViewController was unexpectedly nil")
return
}
SSKEnvironment.shared.databaseStorageRef.asyncWrite { transaction in
ReactionManager.localUserReacted(
to: message.uniqueId,
emoji: reaction,
isRemoving: isRemoving,
tx: transaction
)
}
}
accessories.append(reactionBarAccessory)
}
var alignment: ContextMenuTargetedPreview.Alignment = .center
let interactionType = contextInteraction.itemViewModel.interaction.interactionType
let isRTL = CurrentAppContext().isRTL
if interactionType == .incomingMessage {
alignment = isRTL ? .right : .left
} else if interactionType == .outgoingMessage {
alignment = isRTL ? .left : .right
}
if let componentView = cell.componentView, let contentView = componentView.contextMenuContentView?() {
let preview = ContextMenuTargetedPreview(view: contentView, alignment: alignment, accessoryViews: accessories)
preview?.auxiliaryView = componentView.contextMenuAuxiliaryContentView?()
return preview
} else {
return ContextMenuTargetedPreview(view: cell, alignment: alignment, accessoryViews: accessories)
}
}
public func contextMenuInteraction(_ interaction: ContextMenuInteraction, willDisplayMenuForConfiguration: ContextMenuConfiguration) {
// Reset scroll view pan gesture recognizer, so CV does not scroll behind context menu post presentation on user swipe
collectionView.panGestureRecognizer.isEnabled = false
collectionView.panGestureRecognizer.isEnabled = true
if let contextInteraction = interaction as? ChatHistoryContextMenuInteraction, let cell = contextInteraction.view as? CVCell, let componentView = cell.componentView {
componentView.contextMenuPresentationWillBegin?()
}
dismissKeyBoard()
}
public func contextMenuInteraction(_ interaction: ContextMenuInteraction, willEndForConfiguration: ContextMenuConfiguration) {
}
public func contextMenuInteraction(_ interaction: ContextMenuInteraction, didEndForConfiguration: ContextMenuConfiguration) {
if let contextInteraction = interaction as? ChatHistoryContextMenuInteraction, let cell = contextInteraction.view as? CVCell, let componentView = cell.componentView {
componentView.contextMenuPresentationDidEnd?()
// Restore the keyboard unless the context menu item presented
// a view controller.
if contextInteraction.keyboardWasActive {
if self.presentedViewController == nil {
popKeyBoard()
} else {
// If we're not going to restore the keyboard, update
// chat history layout.
self.loadCoordinator.enqueueReload()
}
}
}
collectionViewActiveContextMenuInteraction = nil
}
public func shouldShowReactionPickerForInteraction(_ interaction: TSInteraction) -> Bool {
if threadViewModel.hasPendingMessageRequest {
return false
}
guard threadViewModel.isLocalUserFullMemberOfThread else {
return false
}
switch interaction {
case let outgoingMessage as TSOutgoingMessage:
if outgoingMessage.wasRemotelyDeleted { return false }
switch outgoingMessage.messageState {
case .failed, .sending, .pending:
return false
default:
return true
}
case let incomingMessage as TSIncomingMessage:
if incomingMessage.wasRemotelyDeleted { return false }
return true
default:
return false
}
}
}