From 54c061561c33f527e6f5b9abd0250d6f9c7adf66 Mon Sep 17 00:00:00 2001 From: Dmitrij Meidus Date: Tue, 22 Apr 2025 21:39:15 +0300 Subject: [PATCH 1/6] [trello.com/c/4FomJzhO] Add Copy action to Failed messages menu. Refactoring of copy mechanizm. --- Adamant/Helpers/String+localized.swift | 3 +++ Adamant/Modules/Chat/ChatFactory.swift | 5 +--- .../View/Managers/ChatDataSourceManager.swift | 16 ++++++------ .../View/Managers/ChatDialogManager.swift | 9 +++++++ .../ChatBaseMessage/ChatMessageCell.swift | 10 +++---- .../Subviews/ChatMedia/ChatMediaCell.swift | 8 +++--- .../ChatReply/ChatMessageReplyCell.swift | 9 +++---- .../ChatTransaction/ChatTransactionCell.swift | 7 +++-- .../ChatTransactionContainerView.swift | 5 ++-- .../ViewModel/ChatMessagesListViewModel.swift | 11 +++----- .../Chat/ViewModel/ChatViewModel.swift | 26 +++++++++++++++++-- .../Chat/ViewModel/Models/ChatDialog.swift | 1 + .../PartnerQR/PartnerQRViewModel.swift | 3 +-- Adamant/ServiceProtocols/DialogService.swift | 1 + .../AdamantDialogService.swift | 10 +++++++ 15 files changed, 78 insertions(+), 46 deletions(-) diff --git a/Adamant/Helpers/String+localized.swift b/Adamant/Helpers/String+localized.swift index bd57dda18..3c3b83494 100644 --- a/Adamant/Helpers/String+localized.swift +++ b/Adamant/Helpers/String+localized.swift @@ -34,6 +34,9 @@ extension String.adamant { static var delete: String { String.localized("Shared.Delete", comment: "Shared alert 'Delete' button. Used anywhere") } + static var copy: String { + String.localized("Shared.Copy", comment: "Shared alert 'Copy' button. Used anywhere") + } // MARK: Titles and messages static var error: String { diff --git a/Adamant/Modules/Chat/ChatFactory.swift b/Adamant/Modules/Chat/ChatFactory.swift index 35f13ad30..05d911c1f 100644 --- a/Adamant/Modules/Chat/ChatFactory.swift +++ b/Adamant/Modules/Chat/ChatFactory.swift @@ -134,10 +134,7 @@ extension ChatFactory { chatCacheService: chatCacheService, walletServiceCompose: walletServiceCompose, avatarService: avatarService, - chatMessagesListViewModel: .init( - avatarService: avatarService, - emojiService: emojiService - ), + chatMessagesListViewModel: .init(avatarService: avatarService), emojiService: emojiService, chatPreservation: chatPreservation, filesStorage: filesStorage, diff --git a/Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift index f552673d4..a6fb0fe7b 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift @@ -92,8 +92,8 @@ final class ChatDataSourceManager: MessagesDataSource { cell.model = model.value cell.configure(with: message, at: indexPath, and: messagesCollectionView) cell.setSubscription(publisher: publisher, collection: messagesCollectionView) - cell.copyNotification = { [weak self] in - self?.viewModel.dialog.send(.toast(.adamant.alert.copiedToPasteboardNotification)) + cell.copyAction = { [weak self] text in + self?.viewModel.copyMessageAction(text) } return cell } @@ -117,8 +117,8 @@ final class ChatDataSourceManager: MessagesDataSource { cell.model = model.value cell.configure(with: message, at: indexPath, and: messagesCollectionView) cell.setSubscription(publisher: publisher, collection: messagesCollectionView) - cell.copyNotification = { [weak self] in - self?.viewModel.dialog.send(.toast(.adamant.alert.copiedToPasteboardNotification)) + cell.copyAction = { [weak self] text in + self?.viewModel.copyMessageAction(text) } return cell } @@ -152,8 +152,8 @@ final class ChatDataSourceManager: MessagesDataSource { cell.model = model.value cell.setSubscription(publisher: publisher, collection: messagesCollectionView) cell.configure(with: message, at: indexPath, and: messagesCollectionView) - cell.copyNotification = { [weak self] in - self?.viewModel.dialog.send(.toast(.adamant.alert.copiedToPasteboardNotification)) + cell.copyAction = { [weak self] text in + self?.viewModel.copyMessageAction(text) } return cell } @@ -177,8 +177,8 @@ final class ChatDataSourceManager: MessagesDataSource { cell.model = model.value cell.setSubscription(publisher: publisher, collection: messagesCollectionView) cell.configure(with: message, at: indexPath, and: messagesCollectionView) - cell.copyNotification = { [weak self] in - self?.viewModel.dialog.send(.toast(.adamant.alert.copiedToPasteboardNotification)) + cell.copyAction = { [weak self] text in + self?.viewModel.copyMessageAction(text) } return cell } diff --git a/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift b/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift index 6e0ae5f94..d474358a4 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift @@ -117,6 +117,8 @@ extension ChatDialogManager { showRenameAlert() case .actionMenu: showActionMenu() + case let .copy(text): + dialogService.copyToPasteboard(text: text, withNotification: true) } } @@ -291,6 +293,7 @@ extension ChatDialogManager { actions: [ makeRetryAction(id: id), makeCancelSendingAction(id: id), + makeCopyAction(id: id), makeCancelAction() ], from: nil @@ -459,6 +462,12 @@ extension ChatDialogManager { viewModel?.cancelMessage(id: id) } } + + fileprivate func makeCopyAction(id: String) -> UIAlertAction { + .init(title: .adamant.alert.copy, style: .default) { [weak viewModel] _ in + viewModel?.copyMessage(id: id) + } + } fileprivate func setProgress(_ show: Bool) { if show { diff --git a/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift b/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift index 60fc5d694..a0cc99810 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift @@ -18,6 +18,8 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { // MARK: Dependencies var chatMessagesListViewModel: ChatMessagesListViewModel? + + var copyAction: ((String) -> Void)? // MARK: Proprieties @@ -88,8 +90,6 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { } } - var copyNotification: (() -> Void)? - var reactionsContanerViewWidth: CGFloat { if getReaction(for: model.address) == nil && getReaction(for: model.opponentAddress) == nil { return .zero @@ -117,8 +117,7 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { originalColor: model.backgroundColor.uiColor ) if isSelected { - UIPasteboard.general.string = self.model.text.string - self.copyNotification?() + copyAction?(self.model.text.string) } } } @@ -526,8 +525,7 @@ private extension ChatMessageCell { private func longPressCopyAction() { self.messageContainerView.animatePressUp() - UIPasteboard.general.string = self.model.text.string - self.copyNotification?() + copyAction?(self.model.text.string) } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatMedia/ChatMediaCell.swift b/Adamant/Modules/Chat/View/Subviews/ChatMedia/ChatMediaCell.swift index 8833e048a..40624672a 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatMedia/ChatMediaCell.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatMedia/ChatMediaCell.swift @@ -19,7 +19,7 @@ final class ChatMediaCell: MessageContentCell, ChatModelView { private var didCopy = false var subscription: AnyCancellable? - var copyNotification: (() -> Void)? + var copyAction: ((String) -> Void)? var model: ChatMediaContainerView.Model = .default { didSet { @@ -42,8 +42,7 @@ final class ChatMediaCell: MessageContentCell, ChatModelView { didSet { containerMediaView.isSelected = isSelected if isSelected && model.content.comment.string != "" { - UIPasteboard.general.string = model.content.comment.string - copyNotification?() + copyAction?(model.content.comment.string) } } } @@ -146,8 +145,7 @@ extension ChatMediaCell { private func longPressCopyAction() { cellContainerView.animatePressUp() if model.content.comment.string != "" { - UIPasteboard.general.string = model.content.comment.string - copyNotification?() + copyAction?(model.content.comment.string) } } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift b/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift index c110641a7..114fd59f8 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift @@ -18,6 +18,8 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { // MARK: Dependencies var chatMessagesListViewModel: ChatMessagesListViewModel? + + var copyAction: ((String) -> Void)? // MARK: Proprieties @@ -149,7 +151,6 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { layoutReactionLabel() } } - var copyNotification: (() -> Void)? var reactionsContanerViewWidth: CGFloat { if getReaction(for: model.address) == nil && getReaction(for: model.opponentAddress) == nil { @@ -178,8 +179,7 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { originalColor: model.backgroundColor.uiColor ) if model.message.string != "" && isSelected { - UIPasteboard.general.string = model.message.string - copyNotification?() + copyAction?(model.message.string) } } } @@ -625,8 +625,7 @@ private extension ChatMessageReplyCell { private func longPressCopyAction() { messageContainerView.animatePressUp() if model.message.string != "" { - UIPasteboard.general.string = model.message.string - copyNotification?() + copyAction?(model.message.string) } } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/ChatTransactionCell.swift b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/ChatTransactionCell.swift index 8860f6672..60bf4a087 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/ChatTransactionCell.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/ChatTransactionCell.swift @@ -17,9 +17,9 @@ final class ChatTransactionCell: MessageContentCell, ChatModelView { private lazy var swipeWrapper = ChatSwipeWrapper(cellContainerView) var subscription: AnyCancellable? - var copyNotification: (() -> Void)? { + var copyAction: ((String) -> Void)? { didSet { - transactionView.copyNotification = copyNotification + transactionView.copyAction = copyAction } } @@ -58,8 +58,7 @@ final class ChatTransactionCell: MessageContentCell, ChatModelView { didSet { transactionView.isSelected = isSelected if let comment = model.content.comment, !comment.isEmpty && isSelected { - UIPasteboard.general.string = comment - copyNotification?() + copyAction?(comment) } } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift index 84cf996ea..cbb7e5fbd 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift @@ -28,7 +28,7 @@ final class ChatTransactionContainerView: UIView { didSet { contentView.actionHandler = actionHandler } } - var copyNotification: (() -> Void)? + var copyAction: ((String) -> Void)? private let contentView = ChatTransactionContentView() @@ -235,8 +235,7 @@ extension ChatTransactionContainerView { fileprivate func longPressCopyAction() { contentView.animatePressUp() if let comment = model.content.comment, !comment.isEmpty { - UIPasteboard.general.string = comment - copyNotification?() + copyAction?(comment) } } diff --git a/Adamant/Modules/Chat/ViewModel/ChatMessagesListViewModel.swift b/Adamant/Modules/Chat/ViewModel/ChatMessagesListViewModel.swift index eae253eaf..ce8f66bdb 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatMessagesListViewModel.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatMessagesListViewModel.swift @@ -9,16 +9,13 @@ import Foundation final class ChatMessagesListViewModel { + // MARK: Dependencies - let avatarService: AvatarService - let emojiService: EmojiService - + init( - avatarService: AvatarService, - emojiService: EmojiService - ) { + avatarService: AvatarService + ){ self.avatarService = avatarService - self.emojiService = emojiService } } diff --git a/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift b/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift index 6654a836e..c4c2de9d6 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift @@ -491,6 +491,29 @@ final class ChatViewModel: NSObject { } }.stored(in: tasksStorage) } + + func copyMessage(id: String) { + guard let chatMessage = messages.first(where: { $0.messageId == id }) else { return } + + var textToCopy: String? = .none + + switch chatMessage.content { + case let .message(model): + textToCopy = model.value.text.string + + case let .reply(model): + textToCopy = model.value.message.string + + case let .transaction(model): + textToCopy = model.value.content.comment ?? "" + + case let .file(model): + textToCopy = model.value.content.comment.string + } + + dialog.send(.copy(textToCopy ?? "")) + } + func retrySendMessage(id: String) { Task { @@ -612,8 +635,7 @@ final class ChatViewModel: NSObject { } func copyMessageAction(_ text: String) { - UIPasteboard.general.string = text - dialog.send(.toast(.adamant.alert.copiedToPasteboardNotification)) + dialog.send(.copy(text)) } func copyTextInPartAction(_ text: String) { diff --git a/Adamant/Modules/Chat/ViewModel/Models/ChatDialog.swift b/Adamant/Modules/Chat/ViewModel/Models/ChatDialog.swift index 6efbdab90..bc3ac1882 100644 --- a/Adamant/Modules/Chat/ViewModel/Models/ChatDialog.swift +++ b/Adamant/Modules/Chat/ViewModel/Models/ChatDialog.swift @@ -38,4 +38,5 @@ enum ChatDialog { case dismissMenu case renameAlert case actionMenu + case copy(String) } diff --git a/Adamant/Modules/PartnerQR/PartnerQRViewModel.swift b/Adamant/Modules/PartnerQR/PartnerQRViewModel.swift index 740c01d81..b57071759 100644 --- a/Adamant/Modules/PartnerQR/PartnerQRViewModel.swift +++ b/Adamant/Modules/PartnerQR/PartnerQRViewModel.swift @@ -128,8 +128,7 @@ final class PartnerQRViewModel: NSObject, ObservableObject { } func copyToPasteboard() { - UIPasteboard.general.string = title - dialogService.showToastMessage(.adamant.alert.copiedToPasteboardNotification) + dialogService.copyToPasteboard(text: title, withNotification: true) } } diff --git a/Adamant/ServiceProtocols/DialogService.swift b/Adamant/ServiceProtocols/DialogService.swift index b08659fc0..12b0d7cfc 100644 --- a/Adamant/ServiceProtocols/DialogService.swift +++ b/Adamant/ServiceProtocols/DialogService.swift @@ -282,4 +282,5 @@ protocol DialogService: AnyObject { ) -> UIAlertController func selectAllTextFields(in alert: UIAlertController) func showFreeTokenAlert(url: String?, type: FreeTokensAlertType, showVC: @escaping () -> Void) + func copyToPasteboard(text: String, withNotification: Bool) } diff --git a/Adamant/Services/AdmDialogService/AdamantDialogService.swift b/Adamant/Services/AdmDialogService/AdamantDialogService.swift index 7354984df..a37aa2ccc 100644 --- a/Adamant/Services/AdmDialogService/AdamantDialogService.swift +++ b/Adamant/Services/AdmDialogService/AdamantDialogService.swift @@ -440,6 +440,7 @@ extension AdamantDialogService { UIAlertAction(title: type.localized, style: .default) { [weak self] _ in UIPasteboard.general.string = stringForPasteboard self?.showToastMessage(String.adamant.alert.copiedToPasteboardNotification) + self?.copyToPasteboard(text: stringForPasteboard, withNotification: true) didSelect?(.copyToPasteboard) } ) @@ -722,6 +723,15 @@ extension AdamantDialogService { } } +extension AdamantDialogService { + func copyToPasteboard(text: String, withNotification showNotification: Bool = true) { + UIPasteboard.general.string = text + if showNotification { + showToastMessage(String.adamant.alert.copiedToPasteboardNotification) + } + } +} + private let warningImage = UIImage( systemName: "multiply.circle", withConfiguration: UIImage.SymbolConfiguration(pointSize: 25, weight: .light) From 389ec242aa249da54e61fb283724d531d2425935 Mon Sep 17 00:00:00 2001 From: Dmitrij Meidus Date: Wed, 23 Apr 2025 14:18:27 +0300 Subject: [PATCH 2/6] [trello.com/c/4FomJzhO] Fix localization --- Adamant/Helpers/String+localized.swift | 3 --- Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Adamant/Helpers/String+localized.swift b/Adamant/Helpers/String+localized.swift index 3c3b83494..bd57dda18 100644 --- a/Adamant/Helpers/String+localized.swift +++ b/Adamant/Helpers/String+localized.swift @@ -34,9 +34,6 @@ extension String.adamant { static var delete: String { String.localized("Shared.Delete", comment: "Shared alert 'Delete' button. Used anywhere") } - static var copy: String { - String.localized("Shared.Copy", comment: "Shared alert 'Copy' button. Used anywhere") - } // MARK: Titles and messages static var error: String { diff --git a/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift b/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift index d474358a4..62eb252e2 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift @@ -464,7 +464,7 @@ extension ChatDialogManager { } fileprivate func makeCopyAction(id: String) -> UIAlertAction { - .init(title: .adamant.alert.copy, style: .default) { [weak viewModel] _ in + .init(title: .adamant.alert.copyToPasteboard, style: .default) { [weak viewModel] _ in viewModel?.copyMessage(id: id) } } From 5a3d8a67997af98fbaf95e62168694a1a7f0b8a4 Mon Sep 17 00:00:00 2001 From: Dmitrij Meidus Date: Wed, 23 Apr 2025 16:17:50 +0300 Subject: [PATCH 3/6] [trello.com/c/4FomJzhO] Open menu for failed message instead of copy in a long press --- .../Chat/View/Managers/ChatAction.swift | 1 + .../View/Managers/ChatDataSourceManager.swift | 2 + .../ChatBaseMessage/ChatMessageCell.swift | 109 +++++++++++------- .../Chat/ViewModel/ChatViewModel.swift | 4 + 4 files changed, 76 insertions(+), 40 deletions(-) diff --git a/Adamant/Modules/Chat/View/Managers/ChatAction.swift b/Adamant/Modules/Chat/View/Managers/ChatAction.swift index b941b73fc..4a429a1a2 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatAction.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatAction.swift @@ -24,4 +24,5 @@ enum ChatAction { case cancelUploading(messageId: String, file: ChatFile) case autoDownloadContentIfNeeded(messageId: String, files: [ChatFile]) case forceDownloadAllFiles(messageId: String, files: [ChatFile]) + case showFailedMessageAlert(id: String) } diff --git a/Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift index a6fb0fe7b..658af93b2 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift @@ -222,6 +222,8 @@ extension ChatDataSourceManager { ) case let .forceDownloadAllFiles(messageId, files): viewModel.forceDownloadAllFiles(messageId: messageId, files: files) + case let .showFailedMessageAlert(id): + viewModel.showFailedMessageAlert(id: id) } } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift b/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift index a0cc99810..b145f36e9 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift @@ -130,8 +130,8 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { private let opponentReactionSize = CGSize(width: 55, height: 27) private let opponentReactionImageSize = CGSize(width: 12, height: 12) private var layoutAttributes: MessagesCollectionViewLayoutAttributes? - private var taskManager = TaskManager() - private var didCopy = false + private var longPressTask: Task? + private var didPerformLongPressAction = false // MARK: - Methods @@ -172,7 +172,7 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { } private func configureLongPressGesture() { - let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressToCopy(_:))) + let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:))) longPress.minimumPressDuration = 0.2 messageContainerView.addGestureRecognizer(longPress) messageContainerView.isUserInteractionEnabled = true @@ -490,43 +490,6 @@ private extension ChatMessageCell { @objc func tapReactionAction() { chatMenuManager.presentMenuProgrammatically(for: containerView) } - - @objc private func handleLongPressToCopy(_ gesture: UILongPressGestureRecognizer) { - switch gesture.state { - case .began: - didCopy = false - messageContainerView.animatePressDown() - - Task { [weak self] in - try? await Task.sleep(nanoseconds: UInt64(1.5) * 1_000_000_000) - - guard let self = self, - gesture.state == .began || gesture.state == .changed else { return } - - await MainActor.run { - self.longPressCopyAction() - self.didCopy = true - } - }.stored(in: taskManager) - - case .ended: - if !didCopy { - taskManager.clean() - longPressCopyAction() - } - - case .cancelled, .failed: - messageContainerView.animatePressUp() - - default: - break - } - } - - private func longPressCopyAction() { - self.messageContainerView.animatePressUp() - copyAction?(self.model.text.string) - } } extension ChatMessageCell: ChatMenuManagerDelegate { @@ -597,6 +560,72 @@ extension ChatMessageCell: ChatCellProtocol { } } +// MARK: - Long Press +extension ChatMessageCell { + @objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) { + if !isMacOS, model.backgroundColor == .failed { + handleLongPressToShowFailed(gesture) + return + } + handleLongPressToCopy(gesture) + } + + private func handleLongPressToCopy(_ gesture: UILongPressGestureRecognizer) { + processLongPress(gesture) { [weak self] in + self?.longPressCopyAction() + } + } + + private func handleLongPressToShowFailed(_ gesture: UILongPressGestureRecognizer) { + processLongPress(gesture) { [weak self] in + guard let id = self?.model.id else { return } + self?.actionHandler(.showFailedMessageAlert(id: id)) + } + } + + private func processLongPress( + _ gesture: UILongPressGestureRecognizer, + perform action: @escaping () -> Void + ) { + switch gesture.state { + case .began: + didPerformLongPressAction = false + messageContainerView.animatePressDown() + + longPressTask = Task { [weak self] in + try? await Task.sleep(interval: 1.5) + guard let self = self, + gesture.state == .began || gesture.state == .changed + else { return } + await MainActor.run { + action() + self.didPerformLongPressAction = true + } + } + + case .ended: + messageContainerView.animatePressUp() + longPressTask?.cancel() + + if !didPerformLongPressAction { + action() + } + + case .cancelled, .failed: + messageContainerView.animatePressUp() + longPressTask?.cancel() + + default: + break + } + } + + private func longPressCopyAction() { + self.messageContainerView.animatePressUp() + copyAction?(self.model.text.string) + } +} + private let reactionsContanerVerticalSpace: CGFloat = 10 private let minReactionsSpacingToOwnBoundary: CGFloat = 60 private let minReactionsSpacingToOppositeBoundary: CGFloat = 15 diff --git a/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift b/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift index c4c2de9d6..3be8e676a 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift @@ -650,6 +650,10 @@ final class ChatViewModel: NSObject { dialog.send(.removeMessageAlert(id: id)) } + func showFailedMessageAlert(id: String) { + dialog.send(.failedMessageAlert(id: id, sender: nil)) + } + func reactAction(_ id: String, emoji: String) { guard let partnerAddress = chatroom?.partner?.address else { return } From 04c13340f6dada82fc485e75f5c412f817c01876 Mon Sep 17 00:00:00 2001 From: Dmitrij Meidus Date: Thu, 24 Apr 2025 14:58:21 +0300 Subject: [PATCH 4/6] [trello.com/c/4FomJzhO] Open menu for by double touch on MacOS, long tap on iOS. --- .../View/Managers/ChatDataSourceManager.swift | 3 +- .../Chat/View/Managers/ChatMenuManager.swift | 39 +---- .../ChatBaseMessage/ChatMessageCell.swift | 135 +++++++++--------- .../Subviews/ChatMedia/ChatMediaCell.swift | 96 +++++++------ .../Container/ChatMediaContainerView.swift | 12 ++ .../ChatReply/ChatMessageReplyCell.swift | 99 ++++++++----- .../ChatTransactionContainerView.swift | 107 ++++++++------ .../CommonKit/Helpers/GestureHelper.swift | 57 ++++++++ .../CommonKit}/Helpers/TaskManager.swift | 17 ++- 9 files changed, 332 insertions(+), 233 deletions(-) create mode 100644 CommonKit/Sources/CommonKit/Helpers/GestureHelper.swift rename {Adamant => CommonKit/Sources/CommonKit}/Helpers/TaskManager.swift (50%) diff --git a/Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift index 658af93b2..7a71b41f4 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift @@ -87,7 +87,8 @@ final class ChatDataSourceManager: MessagesDataSource { return model.value } - cell.actionHandler = { [weak self] in self?.handleAction($0) } + cell.actionHandler = { [weak self] in self?.handleAction($0) + } cell.chatMessagesListViewModel = viewModel.chatMessagesListViewModel cell.model = model.value cell.configure(with: message, at: indexPath, and: messagesCollectionView) diff --git a/Adamant/Modules/Chat/View/Managers/ChatMenuManager.swift b/Adamant/Modules/Chat/View/Managers/ChatMenuManager.swift index 4384bb0f3..024b12692 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatMenuManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatMenuManager.swift @@ -21,6 +21,8 @@ protocol ChatMenuManagerDelegate: AnyObject { tapLocation: CGPoint, getPositionOnScreen: @escaping () -> CGPoint ) + var isFailedMessage: Bool { get } + func showFailedMenu() } @MainActor @@ -39,13 +41,6 @@ final class ChatMenuManager: NSObject { contentView.addInteraction(interaction) return } - - let longPressGesture = UILongPressGestureRecognizer( - target: self, - action: #selector(handleLongPress(_:)) - ) - longPressGesture.minimumPressDuration = 0.17 - contentView.addGestureRecognizer(longPressGesture) } func presentMenuProgrammatically(for contentView: UIView) { @@ -70,32 +65,6 @@ final class ChatMenuManager: NSObject { getPositionOnScreen: getPositionOnScreen ) } - - @objc func handleLongPress(_ gesture: UILongPressGestureRecognizer) { - guard !isMacOS else { return } - - guard gesture.state == .began, - let contentView = gesture.view - else { return } - - let locationOnScreen = contentView.convert(CGPoint.zero, to: nil) - - let size = contentView.frame.size - - let copyView = delegate?.getCopyView() ?? contentView - - let getPositionOnScreen: () -> CGPoint = { - contentView.convert(CGPoint.zero, to: nil) - } - - delegate?.presentMenu( - copyView: copyView, - size: size, - location: locationOnScreen, - tapLocation: .zero, - getPositionOnScreen: getPositionOnScreen - ) - } } extension ChatMenuManager: UIContextMenuInteractionDelegate { @@ -103,6 +72,10 @@ extension ChatMenuManager: UIContextMenuInteractionDelegate { _ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint ) -> UIContextMenuConfiguration? { + if let delegate = delegate, delegate.isFailedMessage { + delegate.showFailedMenu() + return nil + } presentMacOverlay(interaction, configurationForMenuAtLocation: location) return nil } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift b/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift index b145f36e9..7a7edef8a 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift @@ -22,6 +22,10 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { var copyAction: ((String) -> Void)? // MARK: Proprieties + + // MARK: Gesture Helper + var GestureTaskManager: TaskManager = TaskManager() + var didPerformLongPressAction: Bool = false private lazy var reactionsContanerView: UIStackView = { let stack = UIStackView(arrangedSubviews: [ownReactionLabel, opponentReactionLabel]) @@ -130,8 +134,6 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { private let opponentReactionSize = CGSize(width: 55, height: 27) private let opponentReactionImageSize = CGSize(width: 12, height: 12) private var layoutAttributes: MessagesCollectionViewLayoutAttributes? - private var longPressTask: Task? - private var didPerformLongPressAction = false // MARK: - Methods @@ -410,29 +412,29 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { /// Handle tap gesture on contentView and its subviews. override func handleTapGesture(_ gesture: UIGestureRecognizer) { let touchLocation = gesture.location(in: self) - + let containerViewContains = containerView.frame.contains(touchLocation) let canHandle = !cellContentView( canHandle: convert(touchLocation, to: containerView) ) - + switch true { - case containerViewContains && canHandle: - delegate?.didTapMessage(in: self) - case avatarView.frame.contains(touchLocation): - delegate?.didTapAvatar(in: self) - case cellTopLabel.frame.contains(touchLocation): - delegate?.didTapCellTopLabel(in: self) - case cellBottomLabel.frame.contains(touchLocation): - delegate?.didTapCellBottomLabel(in: self) - case messageTopLabel.frame.contains(touchLocation): - delegate?.didTapMessageTopLabel(in: self) - case messageBottomLabel.frame.contains(touchLocation): - delegate?.didTapMessageBottomLabel(in: self) - case accessoryView.frame.contains(touchLocation): - delegate?.didTapAccessoryView(in: self) - default: - delegate?.didTapBackground(in: self) + case containerViewContains && canHandle: + delegate?.didTapMessage(in: self) + case avatarView.frame.contains(touchLocation): + delegate?.didTapAvatar(in: self) + case cellTopLabel.frame.contains(touchLocation): + delegate?.didTapCellTopLabel(in: self) + case cellBottomLabel.frame.contains(touchLocation): + delegate?.didTapCellBottomLabel(in: self) + case messageTopLabel.frame.contains(touchLocation): + delegate?.didTapMessageTopLabel(in: self) + case messageBottomLabel.frame.contains(touchLocation): + delegate?.didTapMessageBottomLabel(in: self) + case accessoryView.frame.contains(touchLocation): + delegate?.didTapAccessoryView(in: self) + default: + delegate?.didTapBackground(in: self) } } @@ -493,6 +495,14 @@ private extension ChatMessageCell { } extension ChatMessageCell: ChatMenuManagerDelegate { + var isFailedMessage: Bool { + model.backgroundColor == .failed + } + + func showFailedMenu(){ + self.actionHandler(.showFailedMessageAlert(id: model.id)) + } + func getCopyView() -> UIView? { copy( with: model, @@ -561,68 +571,55 @@ extension ChatMessageCell: ChatCellProtocol { } // MARK: - Long Press -extension ChatMessageCell { +extension ChatMessageCell: GestureHelper { @objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) { if !isMacOS, model.backgroundColor == .failed { handleLongPressToShowFailed(gesture) return } - handleLongPressToCopy(gesture) + if isMacOS { + handleLongPressToCopy(gesture) + return + } + handleLongPressToOpenMenu(gesture) } private func handleLongPressToCopy(_ gesture: UILongPressGestureRecognizer) { - processLongPress(gesture) { [weak self] in - self?.longPressCopyAction() - } + processLongPress( + gesture: gesture, + perform: { [weak self] in + guard let text = self?.model.text.string else { return } + self?.copyAction?(text) }, + onGestureBegan: { [weak self] in + self?.messageContainerView.animatePressDown() }, + onGestureEnded: { [weak self] in self?.messageContainerView.animatePressUp() } + ) } private func handleLongPressToShowFailed(_ gesture: UILongPressGestureRecognizer) { - processLongPress(gesture) { [weak self] in - guard let id = self?.model.id else { return } - self?.actionHandler(.showFailedMessageAlert(id: id)) - } - } - - private func processLongPress( - _ gesture: UILongPressGestureRecognizer, - perform action: @escaping () -> Void - ) { - switch gesture.state { - case .began: - didPerformLongPressAction = false - messageContainerView.animatePressDown() - - longPressTask = Task { [weak self] in - try? await Task.sleep(interval: 1.5) - guard let self = self, - gesture.state == .began || gesture.state == .changed - else { return } - await MainActor.run { - action() - self.didPerformLongPressAction = true - } - } - - case .ended: - messageContainerView.animatePressUp() - longPressTask?.cancel() - - if !didPerformLongPressAction { - action() - } - - case .cancelled, .failed: - messageContainerView.animatePressUp() - longPressTask?.cancel() - - default: - break - } + processLongPress( + gesture: gesture, + perform: { [weak self] in + guard let id = self?.model.id else { return } + self?.actionHandler(.showFailedMessageAlert(id: id)) }, + onGestureBegan: { [weak self] in + self?.messageContainerView.animatePressDown() }, + onGestureEnded: { [weak self] in self?.messageContainerView.animatePressUp() } + ) } - private func longPressCopyAction() { - self.messageContainerView.animatePressUp() - copyAction?(self.model.text.string) + private func handleLongPressToOpenMenu(_ gesture: UILongPressGestureRecognizer) { + processLongPress( + gesture: gesture, + touchDuration: 0.2, + perform: { [weak self] in + guard let view = self?.containerView else { return } + self?.chatMenuManager.presentMenuProgrammatically(for: view) + }, + onGestureBegan: { [weak self] in + self?.messageContainerView.animatePressDown() }, + onGestureEnded: { [weak self] in self?.messageContainerView.animatePressUp() } + ) } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatMedia/ChatMediaCell.swift b/Adamant/Modules/Chat/View/Subviews/ChatMedia/ChatMediaCell.swift index 40624672a..35c39d8a3 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatMedia/ChatMediaCell.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatMedia/ChatMediaCell.swift @@ -10,16 +10,19 @@ import Combine import MessageKit import SnapKit import UIKit +import CommonKit final class ChatMediaCell: MessageContentCell, ChatModelView { private let containerMediaView = ChatMediaContainerView() private let cellContainerView = UIView() private lazy var swipeWrapper = ChatSwipeWrapper(cellContainerView) - private var taskManager = TaskManager() - private var didCopy = false var subscription: AnyCancellable? var copyAction: ((String) -> Void)? + + // MARK: Gesture Helper + var GestureTaskManager: TaskManager = TaskManager() + var didPerformLongPressAction: Bool = false var model: ChatMediaContainerView.Model = .default { didSet { @@ -92,9 +95,7 @@ final class ChatMediaCell: MessageContentCell, ChatModelView { containerMediaView ) } -} - -extension ChatMediaCell { + fileprivate func configure() { contentView.addSubview(swipeWrapper) swipeWrapper.snp.makeConstraints { @@ -104,49 +105,62 @@ extension ChatMediaCell { } private func configureLongPressGesture() { - let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressToCopy(_:))) + let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:))) longPress.minimumPressDuration = 0.2 cellContainerView.addGestureRecognizer(longPress) cellContainerView.isUserInteractionEnabled = true } - - @objc private func handleLongPressToCopy(_ gesture: UILongPressGestureRecognizer) { - switch gesture.state { - case .began: - didCopy = false - cellContainerView.animatePressDown() - - Task { [weak self] in - try? await Task.sleep(nanoseconds: UInt64(1.5) * 1_000_000_000) - - guard let self = self, - gesture.state == .began || gesture.state == .changed else { return } - - await MainActor.run { - self.longPressCopyAction() - self.didCopy = true - } - }.stored(in: taskManager) - - case .ended: - if !didCopy { - taskManager.clean() - longPressCopyAction() - } - - case .cancelled, .failed: - cellContainerView.animatePressUp() - - default: - break +} + +extension ChatMediaCell: GestureHelper { + @objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) { + if !isMacOS, model.status == .failed { + handleLongPressToShowFailed(gesture) + return + } + if isMacOS { + handleLongPressToCopy(gesture) + return } + handleLongPressToOpenMenu(gesture) + } + + private func handleLongPressToCopy(_ gesture: UILongPressGestureRecognizer) { + processLongPress( + gesture: gesture, + perform: { [weak self] in + guard let text = self?.model.content.comment.string else { return } + self?.copyAction?(text) }, + onGestureBegan: { [weak self] in + self?.messageContainerView.animatePressDown() }, + onGestureEnded: { [weak self] in self?.messageContainerView.animatePressUp() } + ) } - private func longPressCopyAction() { - cellContainerView.animatePressUp() - if model.content.comment.string != "" { - copyAction?(model.content.comment.string) - } + private func handleLongPressToShowFailed(_ gesture: UILongPressGestureRecognizer) { + processLongPress( + gesture: gesture, + perform: { [weak self] in + guard let id = self?.model.id else { return } + self?.actionHandler(.showFailedMessageAlert(id: id)) }, + onGestureBegan: { [weak self] in + self?.messageContainerView.animatePressDown() }, + onGestureEnded: { [weak self] in self?.messageContainerView.animatePressUp() } + ) + } + + private func handleLongPressToOpenMenu(_ gesture: UILongPressGestureRecognizer) { + processLongPress( + gesture: gesture, + touchDuration: 0.2, + perform: { [weak self] in + guard let view = self?.cellContainerView else { return } + self?.containerMediaView.presentMenuProgrammatically(for: view) + }, + onGestureBegan: { [weak self] in + self?.messageContainerView.animatePressDown() }, + onGestureEnded: { [weak self] in self?.messageContainerView.animatePressUp() } + ) } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Container/ChatMediaContainerView.swift b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Container/ChatMediaContainerView.swift index 26862761b..d3b2e0708 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Container/ChatMediaContainerView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Container/ChatMediaContainerView.swift @@ -255,6 +255,14 @@ extension ChatMediaContainerView { } extension ChatMediaContainerView: ChatMenuManagerDelegate { + var isFailedMessage: Bool { + model.status == .failed + } + + func showFailedMenu() { + self.actionHandler(.showFailedMessageAlert(id: model.id)) + } + func getCopyView() -> UIView? { copy(with: model)?.contentView } @@ -278,6 +286,10 @@ extension ChatMediaContainerView: ChatMenuManagerDelegate { ) actionHandler(.presentMenu(arg: arguments)) } + + func presentMenuProgrammatically(for contentView: UIView) { + chatMenuManager.presentMenuProgrammatically(for: contentView) + } } extension ChatMediaContainerView { diff --git a/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift b/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift index 114fd59f8..9ec7e71f0 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift @@ -194,8 +194,10 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { private let opponentReactionSize = CGSize(width: 55, height: 27) private let opponentReactionImageSize = CGSize(width: 12, height: 12) private var layoutAttributes: MessagesCollectionViewLayoutAttributes? - private var taskManager = TaskManager() - private var didCopy = false + + // MARK: Gesture Helper + var GestureTaskManager: TaskManager = TaskManager() + var didPerformLongPressAction: Bool = false // MARK: - Methods @@ -589,48 +591,69 @@ private extension ChatMessageReplyCell { @objc func handleReplyTap() { actionHandler(.scrollTo(message: model)) } - - @objc private func handleLongPressToCopy(_ gesture: UILongPressGestureRecognizer) { - switch gesture.state { - case .began: - didCopy = false - messageContainerView.animatePressDown() - - Task { [weak self] in - try? await Task.sleep(nanoseconds: UInt64(1.5) * 1_000_000_000) - - guard let self = self, - gesture.state == .began || gesture.state == .changed else { return } - - await MainActor.run { - self.longPressCopyAction() - self.didCopy = true - } - }.stored(in: taskManager) - - case .ended: - if !didCopy { - taskManager.clean() - longPressCopyAction() - } - - case .cancelled, .failed: - messageContainerView.animatePressUp() - - default: - break +} + +extension ChatMessageReplyCell: GestureHelper { + @objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) { + if !isMacOS, model.backgroundColor == .failed { + handleLongPressToShowFailed(gesture) + return } + if isMacOS { + handleLongPressToCopy(gesture) + return + } + handleLongPressToOpenMenu(gesture) } - private func longPressCopyAction() { - messageContainerView.animatePressUp() - if model.message.string != "" { - copyAction?(model.message.string) - } + private func handleLongPressToCopy(_ gesture: UILongPressGestureRecognizer) { + processLongPress( + gesture: gesture, + perform: { [weak self] in + guard let text = self?.model.message.string else { return } + self?.copyAction?(text) }, + onGestureBegan: { [weak self] in + self?.messageContainerView.animatePressDown() }, + onGestureEnded: { [weak self] in self?.messageContainerView.animatePressUp() } + ) + } + + private func handleLongPressToShowFailed(_ gesture: UILongPressGestureRecognizer) { + processLongPress( + gesture: gesture, + perform: { [weak self] in + guard let id = self?.model.id else { return } + self?.actionHandler(.showFailedMessageAlert(id: id)) }, + onGestureBegan: { [weak self] in + self?.messageContainerView.animatePressDown() }, + onGestureEnded: { [weak self] in self?.messageContainerView.animatePressUp() } + ) + } + + private func handleLongPressToOpenMenu(_ gesture: UILongPressGestureRecognizer) { + processLongPress( + gesture: gesture, + touchDuration: 0.2, + perform: { [weak self] in + guard let view = self?.containerView else { return } + self?.chatMenuManager.presentMenuProgrammatically(for: view) + }, + onGestureBegan: { [weak self] in + self?.messageContainerView.animatePressDown() }, + onGestureEnded: { [weak self] in self?.messageContainerView.animatePressUp() } + ) } } extension ChatMessageReplyCell: ChatMenuManagerDelegate { + var isFailedMessage: Bool { + model.backgroundColor == .failed + } + + func showFailedMenu() { + self.actionHandler(.showFailedMessageAlert(id: model.id)) + } + func getCopyView() -> UIView? { copy( with: model, @@ -698,7 +721,7 @@ extension ChatMessageReplyCell { } func configureLongPressGesture() { - let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressToCopy(_:))) + let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:))) longPress.minimumPressDuration = 0.2 cellContainerView.addGestureRecognizer(longPress) cellContainerView.isUserInteractionEnabled = true diff --git a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift index cbb7e5fbd..3741dfae0 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift @@ -128,9 +128,11 @@ final class ChatTransactionContainerView: UIView { contentView.isSelected = isSelected } } - private var taskManager = TaskManager() - private var didCopy = false + // MARK: Gesture Helper + var GestureTaskManager: TaskManager = TaskManager() + var didPerformLongPressAction: Bool = false + override init(frame: CGRect) { super.init(frame: frame) configure() @@ -161,7 +163,7 @@ extension ChatTransactionContainerView { } fileprivate func configureLongPressGesture() { - let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressToCopy(_:))) + let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:))) longPress.minimumPressDuration = 0.2 contentView.addGestureRecognizer(longPress) contentView.isUserInteractionEnabled = true @@ -199,45 +201,6 @@ extension ChatTransactionContainerView { @objc fileprivate func onStatusButtonTap() { actionHandler(.forceUpdateTransactionStatus(id: model.id)) } - - @objc private func handleLongPressToCopy(_ gesture: UILongPressGestureRecognizer) { - switch gesture.state { - case .began: - didCopy = false - contentView.animatePressDown() - - Task { [weak self] in - try? await Task.sleep(nanoseconds: UInt64(1.5) * 1_000_000_000) - - guard let self = self, - gesture.state == .began || gesture.state == .changed else { return } - - await MainActor.run { - self.longPressCopyAction() - self.didCopy = true - } - }.stored(in: taskManager) - - case .ended: - if !didCopy { - taskManager.clean() - longPressCopyAction() - } - - case .cancelled, .failed: - contentView.animatePressUp() - - default: - break - } - } - - fileprivate func longPressCopyAction() { - contentView.animatePressUp() - if let comment = model.content.comment, !comment.isEmpty { - copyAction?(comment) - } - } fileprivate func updateOwnReaction() { ownReactionLabel.text = getReaction(for: model.address) @@ -361,6 +324,14 @@ extension ChatTransactionContainerView { } extension ChatTransactionContainerView: ChatMenuManagerDelegate { + var isFailedMessage: Bool { + model.status == .failed + } + + func showFailedMenu() { + self.actionHandler(.showFailedMessageAlert(id: model.id)) + } + func getCopyView() -> UIView? { copy(with: model)?.contentView } @@ -397,6 +368,58 @@ extension ChatTransactionContainerView { } } +extension ChatTransactionContainerView: GestureHelper { + @objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) { + if !isMacOS, isFailedMessage { + handleLongPressToShowFailed(gesture) + return + } + if isMacOS { + handleLongPressToCopy(gesture) + return + } + handleLongPressToOpenMenu(gesture) + } + + private func handleLongPressToCopy(_ gesture: UILongPressGestureRecognizer) { + processLongPress( + gesture: gesture, + perform: { [weak self] in + guard let text = self?.model.content.comment else { return } + self?.copyAction?(text) }, + onGestureBegan: { [weak self] in + self?.contentView.animatePressDown() }, + onGestureEnded: { [weak self] in self?.contentView.animatePressUp() } + ) + } + + private func handleLongPressToShowFailed(_ gesture: UILongPressGestureRecognizer) { + processLongPress( + gesture: gesture, + perform: { [weak self] in + guard let id = self?.model.id else { return } + self?.actionHandler(.showFailedMessageAlert(id: id)) }, + onGestureBegan: { [weak self] in + self?.contentView.animatePressDown() }, + onGestureEnded: { [weak self] in self?.contentView.animatePressUp() } + ) + } + + private func handleLongPressToOpenMenu(_ gesture: UILongPressGestureRecognizer) { + processLongPress( + gesture: gesture, + touchDuration: 0.2, + perform: { [weak self] in + guard let view = self?.contentView else { return } + self?.chatMenuManager.presentMenuProgrammatically(for: view) + }, + onGestureBegan: { [weak self] in + self?.contentView.animatePressDown() }, + onGestureEnded: { [weak self] in self?.contentView.animatePressUp() } + ) + } +} + extension ChatTransactionContainerView { func animateTransactionHighlight() { contentView.animateHighlightOverlay() diff --git a/CommonKit/Sources/CommonKit/Helpers/GestureHelper.swift b/CommonKit/Sources/CommonKit/Helpers/GestureHelper.swift new file mode 100644 index 000000000..60d9455e9 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Helpers/GestureHelper.swift @@ -0,0 +1,57 @@ +// +// GestureHelper.swift +// CommonKit +// +// Created by Dmitrij Meidus on 23.04.25. +// + +import UIKit + +public protocol GestureHelper: NSObject { + var GestureTaskManager: TaskManager { get set } + var didPerformLongPressAction: Bool { get set } +} + +public extension GestureHelper { + func processLongPress( + gesture: UILongPressGestureRecognizer, + touchDuration: TimeInterval = 1.5, + perform action: @escaping () -> Void, + onGestureBegan onBegan: @escaping () -> Void, + onGestureEnded onEnded: @escaping () -> Void + ) { + switch gesture.state { + case .began: + didPerformLongPressAction = false + onBegan() + + Task { [weak self] in + try? await Task.sleep(interval: touchDuration) + let state = await gesture.state + guard let self = self, + state == .began || state == .changed + else { return } + await MainActor.run { + action() + onEnded() + self.didPerformLongPressAction = true + } + }.stored(in: GestureTaskManager) + + case .ended: + onEnded() + GestureTaskManager.clean() + + if !didPerformLongPressAction { + action() + } + + case .cancelled, .failed: + onEnded() + GestureTaskManager.clean() + + default: + break + } + } +} diff --git a/Adamant/Helpers/TaskManager.swift b/CommonKit/Sources/CommonKit/Helpers/TaskManager.swift similarity index 50% rename from Adamant/Helpers/TaskManager.swift rename to CommonKit/Sources/CommonKit/Helpers/TaskManager.swift index 0bfa637ad..dc9dc600c 100644 --- a/Adamant/Helpers/TaskManager.swift +++ b/CommonKit/Sources/CommonKit/Helpers/TaskManager.swift @@ -1,21 +1,20 @@ // // TaskManager.swift -// Adamant +// CommonKit // -// Created by Stanislav Jelezoglo on 18.02.2023. -// Copyright © 2023 Adamant. All rights reserved. +// Created by Dmitrij Meidus on 23.04.25. // -import Foundation - -final class TaskManager { +public final class TaskManager { private var tasks = Set>() + + public init() {} - func insert(_ task: Task<(), Never>) { + public func insert(_ task: Task<(), Never>) { tasks.insert(task) } - func clean() { + public func clean() { tasks.forEach { $0.cancel() } } @@ -24,7 +23,7 @@ final class TaskManager { } } -extension Task where Success == Void, Failure == Never { +public extension Task where Success == Void, Failure == Never { func stored(in taskManager: TaskManager) { taskManager.insert(self) } From 782533b800f8277e4dfb477154e827f78387f142 Mon Sep 17 00:00:00 2001 From: Dmitrij Meidus Date: Thu, 24 Apr 2025 15:07:00 +0300 Subject: [PATCH 5/6] [trello.com/c/4FomJzhO] Update interval duration from merged changes --- Adamant/Modules/Chat/View/Subviews/ChatModelView.swift | 3 --- CommonKit/Sources/CommonKit/Helpers/GestureHelper.swift | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Adamant/Modules/Chat/View/Subviews/ChatModelView.swift b/Adamant/Modules/Chat/View/Subviews/ChatModelView.swift index a3965ee6e..22f2386e6 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatModelView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatModelView.swift @@ -36,6 +36,3 @@ extension ChatModelView { } } } - -let quickCopyInterval = 1.0 - diff --git a/CommonKit/Sources/CommonKit/Helpers/GestureHelper.swift b/CommonKit/Sources/CommonKit/Helpers/GestureHelper.swift index 60d9455e9..e1957e8bf 100644 --- a/CommonKit/Sources/CommonKit/Helpers/GestureHelper.swift +++ b/CommonKit/Sources/CommonKit/Helpers/GestureHelper.swift @@ -15,7 +15,7 @@ public protocol GestureHelper: NSObject { public extension GestureHelper { func processLongPress( gesture: UILongPressGestureRecognizer, - touchDuration: TimeInterval = 1.5, + touchDuration: TimeInterval = 1.0, perform action: @escaping () -> Void, onGestureBegan onBegan: @escaping () -> Void, onGestureEnded onEnded: @escaping () -> Void From 1d280994529ab3f82d5a30aedee300f79c4d19cf Mon Sep 17 00:00:00 2001 From: Dmitrij Meidus Date: Thu, 24 Apr 2025 18:09:04 +0300 Subject: [PATCH 6/6] [trello.com/c/4FomJzhO] Fix media message animation --- .../Chat/View/Subviews/ChatMedia/ChatMediaCell.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Adamant/Modules/Chat/View/Subviews/ChatMedia/ChatMediaCell.swift b/Adamant/Modules/Chat/View/Subviews/ChatMedia/ChatMediaCell.swift index 35c39d8a3..130353c43 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatMedia/ChatMediaCell.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatMedia/ChatMediaCell.swift @@ -132,8 +132,8 @@ extension ChatMediaCell: GestureHelper { guard let text = self?.model.content.comment.string else { return } self?.copyAction?(text) }, onGestureBegan: { [weak self] in - self?.messageContainerView.animatePressDown() }, - onGestureEnded: { [weak self] in self?.messageContainerView.animatePressUp() } + self?.cellContainerView.animatePressDown() }, + onGestureEnded: { [weak self] in self?.cellContainerView.animatePressUp() } ) } @@ -144,8 +144,8 @@ extension ChatMediaCell: GestureHelper { guard let id = self?.model.id else { return } self?.actionHandler(.showFailedMessageAlert(id: id)) }, onGestureBegan: { [weak self] in - self?.messageContainerView.animatePressDown() }, - onGestureEnded: { [weak self] in self?.messageContainerView.animatePressUp() } + self?.cellContainerView.animatePressDown() }, + onGestureEnded: { [weak self] in self?.cellContainerView.animatePressUp() } ) } @@ -158,7 +158,7 @@ extension ChatMediaCell: GestureHelper { self?.containerMediaView.presentMenuProgrammatically(for: view) }, onGestureBegan: { [weak self] in - self?.messageContainerView.animatePressDown() }, + self?.cellContainerView.animatePressDown() }, onGestureEnded: { [weak self] in self?.messageContainerView.animatePressUp() } ) }