Skip to content

Commit c6b60f3

Browse files
authored
Merge pull request #778 from Adamant-im/trello.com/c/46S4GSZ5
[trello.com/c/46S4GSZ5] hold left click now copy text of any messages
2 parents 8d4b2bd + 0e1e192 commit c6b60f3

File tree

11 files changed

+216
-20
lines changed

11 files changed

+216
-20
lines changed

Adamant/Modules/Chat/View/ChatViewController.swift

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,23 @@ final class ChatViewController: MessagesViewController {
212212
}
213213
super.collectionView(collectionView, willDisplay: cell, forItemAt: indexPath)
214214
}
215-
215+
216+
override func collectionView(
217+
_ collectionView: UICollectionView,
218+
canPerformAction action: Selector,
219+
forItemAt indexPath: IndexPath,
220+
withSender sender: Any?
221+
) -> Bool {
222+
return false
223+
}
224+
225+
override func collectionView(
226+
_ collectionView: UICollectionView,
227+
shouldShowMenuForItemAt indexPath: IndexPath
228+
) -> Bool {
229+
return false
230+
}
231+
216232
override func scrollViewDidEndDecelerating(_: UIScrollView) {
217233
scrollDidStop()
218234
}
@@ -765,6 +781,9 @@ extension ChatViewController {
765781
chatMessagesCollectionView.reloadData(newIds: viewModel.messages.map { $0.id }, isOnBottom: isScrollPositionNearlyTheBottom)
766782
scrollDownOnNewMessageIfNeeded(previousBottomMessageId: bottomMessageId)
767783
bottomMessageId = viewModel.messages.last?.messageId
784+
if !messagesLoaded {
785+
viewModel.startPosition.map { scrollToPosition($0) }
786+
}
768787
}
769788

770789
fileprivate func updateMessagesPosition() {
@@ -773,9 +792,7 @@ extension ChatViewController {
773792
if viewModel.messageIdToShow == nil {
774793
if let unreadMessage = viewModel.unreadMessagesIds?.first {
775794
scrollToPosition(.messageId(unreadMessage), setExtraOffset: true)
776-
} else if let position = viewModel.startPosition {
777-
scrollToPosition(position)
778-
}
795+
}
779796
}
780797
}
781798

Adamant/Modules/Chat/View/Helpers/AdamantCellAnimation.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,18 @@ extension UIView {
3434
layer.masksToBounds = masksToBounds
3535
layer.cornerRadius = cornerRadius
3636
}
37+
38+
func animatePressDown(duration: TimeInterval = 0.1) {
39+
UIView.animate(withDuration: duration) {
40+
self.transform = CGAffineTransform(scaleX: 0.96, y: 0.96)
41+
self.alpha = 0.5
42+
}
43+
}
44+
45+
func animatePressUp(duration: TimeInterval = 0.1) {
46+
UIView.animate(withDuration: duration) {
47+
self.transform = .identity
48+
self.alpha = 1.0
49+
}
50+
}
3751
}

Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ final class ChatDataSourceManager: MessagesDataSource {
9292
cell.model = model.value
9393
cell.configure(with: message, at: indexPath, and: messagesCollectionView)
9494
cell.setSubscription(publisher: publisher, collection: messagesCollectionView)
95+
cell.copyNotification = { [weak self] in
96+
self?.viewModel.dialog.send(.toast(.adamant.alert.copiedToPasteboardNotification))
97+
}
9598
return cell
9699
}
97100

@@ -114,6 +117,9 @@ final class ChatDataSourceManager: MessagesDataSource {
114117
cell.model = model.value
115118
cell.configure(with: message, at: indexPath, and: messagesCollectionView)
116119
cell.setSubscription(publisher: publisher, collection: messagesCollectionView)
120+
cell.copyNotification = { [weak self] in
121+
self?.viewModel.dialog.send(.toast(.adamant.alert.copiedToPasteboardNotification))
122+
}
117123
return cell
118124
}
119125

@@ -146,6 +152,9 @@ final class ChatDataSourceManager: MessagesDataSource {
146152
cell.model = model.value
147153
cell.setSubscription(publisher: publisher, collection: messagesCollectionView)
148154
cell.configure(with: message, at: indexPath, and: messagesCollectionView)
155+
cell.copyNotification = { [weak self] in
156+
self?.viewModel.dialog.send(.toast(.adamant.alert.copiedToPasteboardNotification))
157+
}
149158
return cell
150159
}
151160

@@ -168,6 +177,9 @@ final class ChatDataSourceManager: MessagesDataSource {
168177
cell.model = model.value
169178
cell.setSubscription(publisher: publisher, collection: messagesCollectionView)
170179
cell.configure(with: message, at: indexPath, and: messagesCollectionView)
180+
cell.copyNotification = { [weak self] in
181+
self?.viewModel.dialog.send(.toast(.adamant.alert.copiedToPasteboardNotification))
182+
}
171183
return cell
172184
}
173185

Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ final class ChatMessageCell: TextMessageCell, ChatModelView {
8787
layoutReactionLabel()
8888
}
8989
}
90+
91+
var copyNotification: (() -> Void)?
9092

9193
var reactionsContanerViewWidth: CGFloat {
9294
if getReaction(for: model.address) == nil && getReaction(for: model.opponentAddress) == nil {
@@ -154,15 +156,23 @@ final class ChatMessageCell: TextMessageCell, ChatModelView {
154156

155157
func configureMenu() {
156158
containerView.layer.cornerRadius = 10
159+
160+
configureLongPressGesture()
157161

158162
messageContainerView.removeFromSuperview()
159163
cellContainerView.addSubview(containerView)
160-
161164
containerView.addSubview(messageContainerView)
162165

163166
chatMenuManager.setup(for: containerView)
164167
}
165-
168+
169+
private func configureLongPressGesture() {
170+
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressToCopy(_:)))
171+
longPress.minimumPressDuration = 0.2
172+
messageContainerView.addGestureRecognizer(longPress)
173+
messageContainerView.isUserInteractionEnabled = true
174+
}
175+
166176
func updateOwnReaction() {
167177
ownReactionLabel.text = getReaction(for: model.address)
168178
ownReactionLabel.backgroundColor = .adamant.pickedReactionBackground
@@ -427,7 +437,7 @@ final class ChatMessageCell: TextMessageCell, ChatModelView {
427437
}
428438
}
429439

430-
extension ChatMessageCell {
440+
private extension ChatMessageCell {
431441
func makeContextMenu() -> AMenuSection {
432442
let remove = AMenuItem.action(
433443
title: .adamant.chat.remove,
@@ -475,6 +485,24 @@ extension ChatMessageCell {
475485
@objc func tapReactionAction() {
476486
chatMenuManager.presentMenuProgrammatically(for: containerView)
477487
}
488+
489+
@objc private func handleLongPressToCopy(_ gesture: UILongPressGestureRecognizer) {
490+
switch gesture.state {
491+
case .began:
492+
messageContainerView.animatePressDown()
493+
494+
case .ended:
495+
messageContainerView.animatePressUp()
496+
UIPasteboard.general.string = model.text.string
497+
copyNotification?()
498+
499+
case .cancelled, .failed:
500+
messageContainerView.animatePressUp()
501+
502+
default:
503+
break
504+
}
505+
}
478506
}
479507

480508
extension ChatMessageCell: ChatMenuManagerDelegate {

Adamant/Modules/Chat/View/Subviews/ChatMedia/ChatMediaCell.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ final class ChatMediaCell: MessageContentCell, ChatModelView {
1717
private lazy var swipeWrapper = ChatSwipeWrapper(cellContainerView)
1818

1919
var subscription: AnyCancellable?
20+
var copyNotification: (() -> Void)?
2021

2122
var model: ChatMediaContainerView.Model = .default {
2223
didSet {
@@ -94,5 +95,33 @@ extension ChatMediaCell {
9495
swipeWrapper.snp.makeConstraints {
9596
$0.directionalEdges.equalToSuperview()
9697
}
98+
configureLongPressGesture()
99+
}
100+
101+
private func configureLongPressGesture() {
102+
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressToCopy(_:)))
103+
longPress.minimumPressDuration = 0.2
104+
cellContainerView.addGestureRecognizer(longPress)
105+
cellContainerView.isUserInteractionEnabled = true
106+
}
107+
108+
@objc private func handleLongPressToCopy(_ gesture: UILongPressGestureRecognizer) {
109+
switch gesture.state {
110+
case .began:
111+
cellContainerView.animatePressDown()
112+
113+
case .ended:
114+
cellContainerView.animatePressUp()
115+
if model.content.comment.string != "" {
116+
UIPasteboard.general.string = model.content.comment.string
117+
copyNotification?()
118+
}
119+
120+
case .cancelled, .failed:
121+
cellContainerView.animatePressUp()
122+
123+
default:
124+
break
125+
}
97126
}
98127
}

Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView {
149149
layoutReactionLabel()
150150
}
151151
}
152-
152+
var copyNotification: (() -> Void)?
153+
153154
var reactionsContanerViewWidth: CGFloat {
154155
if getReaction(for: model.address) == nil && getReaction(for: model.opponentAddress) == nil {
155156
return .zero
@@ -217,6 +218,8 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView {
217218
messageContainerView.addSubview(verticalStack)
218219
messageLabel.numberOfLines = 0
219220
replyMessageLabel.numberOfLines = 1
221+
configureReplyMessageGesture()
222+
configureLongPressGesture()
220223

221224
let leading = model.isFromCurrentSender ? smallHInset : longHInset
222225
let trailing = model.isFromCurrentSender ? longHInset : smallHInset
@@ -230,7 +233,15 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView {
230233

231234
cellContainerView.addSubview(reactionsContanerView)
232235
}
233-
236+
237+
func configureReplyMessageGesture() {
238+
let tap = UITapGestureRecognizer(target: self, action: #selector(handleReplyTap))
239+
replyMessageLabel.isUserInteractionEnabled = true
240+
verticalStack.isUserInteractionEnabled = true
241+
messageContainerView.isUserInteractionEnabled = true
242+
replyMessageLabel.addGestureRecognizer(tap)
243+
}
244+
234245
func configureMenu() {
235246
containerView.layer.cornerRadius = 10
236247

@@ -501,7 +512,6 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView {
501512
switch true {
502513
case containerViewContains && canHandle:
503514
delegate?.didTapMessage(in: self)
504-
actionHandler(.scrollTo(message: model))
505515
case avatarView.frame.contains(touchLocation):
506516
delegate?.didTapAvatar(in: self)
507517
case cellTopLabel.frame.contains(touchLocation):
@@ -525,7 +535,7 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView {
525535
}
526536
}
527537

528-
extension ChatMessageReplyCell {
538+
private extension ChatMessageReplyCell {
529539
func makeContextMenu() -> AMenuSection {
530540
let remove = AMenuItem.action(
531541
title: .adamant.chat.remove,
@@ -569,6 +579,30 @@ extension ChatMessageReplyCell {
569579
@objc func tapReactionAction() {
570580
chatMenuManager.presentMenuProgrammatically(for: containerView)
571581
}
582+
583+
@objc func handleReplyTap() {
584+
actionHandler(.scrollTo(message: model))
585+
}
586+
587+
@objc private func handleLongPressToCopy(_ gesture: UILongPressGestureRecognizer) {
588+
switch gesture.state {
589+
case .began:
590+
messageContainerView.animatePressDown()
591+
592+
case .ended:
593+
messageContainerView.animatePressUp()
594+
if model.message.string != "" {
595+
UIPasteboard.general.string = model.message.string
596+
copyNotification?()
597+
}
598+
599+
case .cancelled, .failed:
600+
messageContainerView.animatePressUp()
601+
602+
default:
603+
break
604+
}
605+
}
572606
}
573607

574608
extension ChatMessageReplyCell: ChatMenuManagerDelegate {
@@ -637,6 +671,13 @@ extension ChatMessageReplyCell {
637671
)
638672
return cell
639673
}
674+
675+
func configureLongPressGesture() {
676+
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressToCopy(_:)))
677+
longPress.minimumPressDuration = 0.2
678+
cellContainerView.addGestureRecognizer(longPress)
679+
cellContainerView.isUserInteractionEnabled = true
680+
}
640681
}
641682

642683
private let reactionsContanerVerticalSpace: CGFloat = 10

Adamant/Modules/Chat/View/Subviews/ChatTransaction/ChatTransactionCell.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ final class ChatTransactionCell: MessageContentCell, ChatModelView {
1717
private lazy var swipeWrapper = ChatSwipeWrapper(cellContainerView)
1818

1919
var subscription: AnyCancellable?
20+
var copyNotification: (() -> Void)? {
21+
didSet {
22+
transactionView.copyNotification = copyNotification
23+
}
24+
}
2025

2126
var model: ChatTransactionContainerView.Model = .default {
2227
didSet {

Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ final class ChatTransactionContainerView: UIView {
2727
var actionHandler: (ChatAction) -> Void = { _ in } {
2828
didSet { contentView.actionHandler = actionHandler }
2929
}
30+
31+
var copyNotification: (() -> Void)?
3032

3133
private let contentView = ChatTransactionContentView()
3234

@@ -153,6 +155,14 @@ extension ChatTransactionContainerView {
153155
}
154156

155157
chatMenuManager.setup(for: contentView)
158+
configureLongPressGesture()
159+
}
160+
161+
fileprivate func configureLongPressGesture() {
162+
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressToCopy(_:)))
163+
longPress.minimumPressDuration = 0.2
164+
contentView.addGestureRecognizer(longPress)
165+
contentView.isUserInteractionEnabled = true
156166
}
157167

158168
fileprivate func update() {
@@ -187,6 +197,26 @@ extension ChatTransactionContainerView {
187197
@objc fileprivate func onStatusButtonTap() {
188198
actionHandler(.forceUpdateTransactionStatus(id: model.id))
189199
}
200+
201+
@objc fileprivate func handleLongPressToCopy(_ gesture: UILongPressGestureRecognizer) {
202+
switch gesture.state {
203+
case .began:
204+
contentView.animatePressDown()
205+
206+
case .ended:
207+
contentView.animatePressUp()
208+
if let comment = model.content.comment, !comment.isEmpty {
209+
UIPasteboard.general.string = comment
210+
copyNotification?()
211+
}
212+
213+
case .cancelled, .failed:
214+
contentView.animatePressUp()
215+
216+
default:
217+
break
218+
}
219+
}
190220

191221
fileprivate func updateOwnReaction() {
192222
ownReactionLabel.text = getReaction(for: model.address)
@@ -292,8 +322,20 @@ extension ChatTransactionContainerView {
292322
) { [actionHandler, model] in
293323
actionHandler(.reply(id: model.id))
294324
}
295-
296-
return AMenuSection([reply, report, remove])
325+
326+
let copy = AMenuItem.action(
327+
title: .adamant.chat.copy,
328+
systemImageName: "doc.on.doc"
329+
) { [actionHandler, model] in
330+
actionHandler(.copy(text: model.content.comment ?? ""))
331+
}
332+
333+
let actions: [AMenuItem] =
334+
model.content.comment == nil || model.content.comment == ""
335+
? [reply, report, remove]
336+
: [reply, copy, report, remove]
337+
338+
return AMenuSection(actions)
297339
}
298340
}
299341

0 commit comments

Comments
 (0)