Skip to content

[trello.com/c/4FomJzhO] Add Copy action to Failed messages menu + Open menu on long tap #833

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
5 changes: 1 addition & 4 deletions Adamant/Modules/Chat/ChatFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions Adamant/Modules/Chat/View/Managers/ChatAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
21 changes: 12 additions & 9 deletions Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,14 @@ 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)
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
}
Expand All @@ -117,8 +118,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
}
Expand Down Expand Up @@ -152,8 +153,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
}
Expand All @@ -177,8 +178,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
}
Expand Down Expand Up @@ -223,6 +224,8 @@ extension ChatDataSourceManager {
)
case let .forceDownloadAllFiles(messageId, files):
viewModel.forceDownloadAllFiles(messageId: messageId, files: files)
case let .showFailedMessageAlert(id):
viewModel.showFailedMessageAlert(id: id)
}
}
}
9 changes: 9 additions & 0 deletions Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ extension ChatDialogManager {
showRenameAlert()
case .actionMenu:
showActionMenu()
case let .copy(text):
dialogService.copyToPasteboard(text: text, withNotification: true)
}
}

Expand Down Expand Up @@ -291,6 +293,7 @@ extension ChatDialogManager {
actions: [
makeRetryAction(id: id),
makeCancelSendingAction(id: id),
makeCopyAction(id: id),
makeCancelAction()
],
from: nil
Expand Down Expand Up @@ -459,6 +462,12 @@ extension ChatDialogManager {
viewModel?.cancelMessage(id: id)
}
}

fileprivate func makeCopyAction(id: String) -> UIAlertAction {
.init(title: .adamant.alert.copyToPasteboard, style: .default) { [weak viewModel] _ in
viewModel?.copyMessage(id: id)
}
}

fileprivate func setProgress(_ show: Bool) {
if show {
Expand Down
39 changes: 6 additions & 33 deletions Adamant/Modules/Chat/View/Managers/ChatMenuManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ protocol ChatMenuManagerDelegate: AnyObject {
tapLocation: CGPoint,
getPositionOnScreen: @escaping () -> CGPoint
)
var isFailedMessage: Bool { get }
func showFailedMenu()
}

@MainActor
Expand All @@ -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) {
Expand All @@ -70,39 +65,17 @@ 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 {
public func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint
) -> UIContextMenuConfiguration? {
if let delegate = delegate, delegate.isFailedMessage {
delegate.showFailedMenu()
return nil
}
presentMacOverlay(interaction, configurationForMenuAtLocation: location)
return nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@ final class ChatMessageCell: TextMessageCell, ChatModelView {
// MARK: Dependencies

var chatMessagesListViewModel: ChatMessagesListViewModel?

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])
Expand Down Expand Up @@ -88,8 +94,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
Expand Down Expand Up @@ -117,8 +121,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)
}
}
}
Expand All @@ -131,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 taskManager = TaskManager()
private var didCopy = false

// MARK: - Methods

Expand Down Expand Up @@ -173,7 +174,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
Expand Down Expand Up @@ -411,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)
}
}

Expand Down Expand Up @@ -491,47 +492,17 @@ 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(interval: quickCopyInterval)

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()
UIPasteboard.general.string = self.model.text.string
self.copyNotification?()
}
}

extension ChatMessageCell: ChatMenuManagerDelegate {
var isFailedMessage: Bool {
model.backgroundColor == .failed
}

func showFailedMenu(){
self.actionHandler(.showFailedMessageAlert(id: model.id))
}

func getCopyView() -> UIView? {
copy(
with: model,
Expand Down Expand Up @@ -599,6 +570,59 @@ extension ChatMessageCell: ChatCellProtocol {
}
}

// MARK: - Long Press
extension ChatMessageCell: GestureHelper {
@objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
if !isMacOS, model.backgroundColor == .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.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: 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() }
)
}
}

private let reactionsContanerVerticalSpace: CGFloat = 10
private let minReactionsSpacingToOwnBoundary: CGFloat = 60
private let minReactionsSpacingToOppositeBoundary: CGFloat = 15
Loading