Skip to content
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally speaking ,deciding on the state based on backgroundColor is not a future-proof solution. Would you consider improving this part a bit?

}

func showFailedMenu(){
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

showFailedMenuAlert ?

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