From 4889e0a0f0525d809b68245b365ec622c6661a9a Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Tue, 17 Dec 2024 23:45:42 +0100 Subject: [PATCH 01/11] factor our gesture detection --- deltachat-ios/Chat/ChatViewController.swift | 9 +++++++ deltachat-ios/Chat/Views/MessageLabel.swift | 28 +++++++++++++-------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/deltachat-ios/Chat/ChatViewController.swift b/deltachat-ios/Chat/ChatViewController.swift index bd412610e..ecf2d6706 100644 --- a/deltachat-ios/Chat/ChatViewController.swift +++ b/deltachat-ios/Chat/ChatViewController.swift @@ -2028,6 +2028,15 @@ extension ChatViewController { return nil } + // Check if the long tap is on a link (or other message text element with custom long tap behavior) + if let msgcell = tableView.cellForRow(at: indexPath) as? BaseMessageCell { + let label = msgcell.messageLabel.label + let localTouchLocation = tableView.convert(point, to: label) + if let (detectorType, value) = label.detectGesture(localTouchLocation) { + print("url: \(detectorType) -- \(value) -- ") + } + } + return UIContextMenuConfiguration( identifier: NSString(string: "\(messageId)"), previewProvider: nil, diff --git a/deltachat-ios/Chat/Views/MessageLabel.swift b/deltachat-ios/Chat/Views/MessageLabel.swift index f23956e40..2f3617354 100644 --- a/deltachat-ios/Chat/Views/MessageLabel.swift +++ b/deltachat-ios/Chat/Views/MessageLabel.swift @@ -452,17 +452,23 @@ open class MessageLabel: UILabel { } - open func handleGesture(_ touchLocation: CGPoint) -> Bool { - - guard let index = stringIndex(at: touchLocation) else { return false } - - for (detectorType, ranges) in rangesForDetectors { - for (range, value) in ranges { - if range.contains(index) { - handleGesture(for: detectorType, value: value) - return true - } - } + internal func detectGesture(_ touchLocation: CGPoint) -> (DetectorType, NewMessageTextCheckingType)? { + guard let index = stringIndex(at: touchLocation) else { return nil } + + for (detectorType, ranges) in rangesForDetectors { + for (range, value) in ranges { + if range.contains(index) { + return (detectorType, value) + } + } + } + return nil + } + + open func handleGesture(_ touchLocation: CGPoint) -> Bool { + if let (detectorType, value) = detectGesture(touchLocation) { + handleGesture(for: detectorType, value: value) + return true } return false } From f8e994c074e29852a576889c56a646f92fd9eecb Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Tue, 17 Dec 2024 23:53:38 +0100 Subject: [PATCH 02/11] get link text to copy --- deltachat-ios/Chat/ChatViewController.swift | 26 +++++---- deltachat-ios/Chat/Views/MessageLabel.swift | 64 ++++++++++++--------- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/deltachat-ios/Chat/ChatViewController.swift b/deltachat-ios/Chat/ChatViewController.swift index ecf2d6706..dc160c170 100644 --- a/deltachat-ios/Chat/ChatViewController.swift +++ b/deltachat-ios/Chat/ChatViewController.swift @@ -2021,6 +2021,15 @@ extension ChatViewController { menuElements.append(action) } + private func isLinkTapped(indexPath: IndexPath, point: CGPoint) -> String? { + if let cell = tableView.cellForRow(at: indexPath) as? BaseMessageCell { + let label = cell.messageLabel.label + let localTouchLocation = tableView.convert(point, to: label) + return label.getCopyableLinkText(localTouchLocation) + } + return nil + } + // context menu for iOS 13+ override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { let messageId = messageIds[indexPath.row] @@ -2028,15 +2037,6 @@ extension ChatViewController { return nil } - // Check if the long tap is on a link (or other message text element with custom long tap behavior) - if let msgcell = tableView.cellForRow(at: indexPath) as? BaseMessageCell { - let label = msgcell.messageLabel.label - let localTouchLocation = tableView.convert(point, to: label) - if let (detectorType, value) = label.detectGesture(localTouchLocation) { - print("url: \(detectorType) -- \(value) -- ") - } - } - return UIContextMenuConfiguration( identifier: NSString(string: "\(messageId)"), previewProvider: nil, @@ -2076,7 +2076,13 @@ extension ChatViewController { UIAction.menuAction(localizationKey: "forward", image: image, indexPath: indexPath, action: { self.forward(at: $0 ) }) ) - if let text = message.text, !text.isEmpty { + if let link = isLinkTapped(indexPath: indexPath, point: point) { + children.append( + UIAction.menuAction(localizationKey: "menu_copy_link_to_clipboard", systemImageName: "link", indexPath: indexPath, action: { _ in + UIPasteboard.general.string = link + }) + ) + } else if let text = message.text, !text.isEmpty { let copyTitle = message.file == nil ? "global_menu_edit_copy_desktop" : "menu_copy_text_to_clipboard" children.append( UIAction.menuAction(localizationKey: copyTitle, systemImageName: "doc.on.doc", indexPath: indexPath, action: { self.copyToClipboard(at: $0 ) }) diff --git a/deltachat-ios/Chat/Views/MessageLabel.swift b/deltachat-ios/Chat/Views/MessageLabel.swift index 2f3617354..6ef0f9168 100644 --- a/deltachat-ios/Chat/Views/MessageLabel.swift +++ b/deltachat-ios/Chat/Views/MessageLabel.swift @@ -452,55 +452,65 @@ open class MessageLabel: UILabel { } - internal func detectGesture(_ touchLocation: CGPoint) -> (DetectorType, NewMessageTextCheckingType)? { - guard let index = stringIndex(at: touchLocation) else { return nil } - - for (detectorType, ranges) in rangesForDetectors { - for (range, value) in ranges { - if range.contains(index) { - return (detectorType, value) - } - } - } - return nil - } - - open func handleGesture(_ touchLocation: CGPoint) -> Bool { - if let (detectorType, value) = detectGesture(touchLocation) { - handleGesture(for: detectorType, value: value) - return true + internal func detectLink(_ touchLocation: CGPoint) -> (DetectorType, NewMessageTextCheckingType)? { + guard let index = stringIndex(at: touchLocation) else { return nil } + + for (detectorType, ranges) in rangesForDetectors { + for (range, value) in ranges { + if range.contains(index) { + return (detectorType, value) + } + } + } + + return nil + } + + internal func getCopyableLinkText(_ touchLocation: CGPoint) -> String? { + guard let (detectorType, value) = detectLink(touchLocation) else { return nil } + + switch value { + case let .link(url): + guard let url = url else { return nil } + return url.absoluteString + case let .phoneNumber(phoneNumber): + return phoneNumber + case let .custom(pattern, match): + return match + case .addressComponents, .date, .transitInfoComponents: + return nil } - return false } - /// swiftlint:disable cyclomatic_complexity - private func handleGesture(for detectorType: DetectorType, value: NewMessageTextCheckingType) { + internal func handleGesture(_ touchLocation: CGPoint) -> Bool { + guard let (detectorType, value) = detectLink(touchLocation) else { return false } + switch value { case let .addressComponents(addressComponents): var transformedAddressComponents = [String: String]() - guard let addressComponents = addressComponents else { return } + guard let addressComponents = addressComponents else { return false } addressComponents.forEach { (key, value) in transformedAddressComponents[key.rawValue] = value } handleAddress(transformedAddressComponents) case let .phoneNumber(phoneNumber): - guard let phoneNumber = phoneNumber else { return } + guard let phoneNumber = phoneNumber else { return false } handlePhoneNumber(phoneNumber) case let .date(date): - guard let date = date else { return } + guard let date = date else { return false } handleDate(date) case let .link(url): - guard let url = url else { return } + guard let url = url else { return false } handleURL(url) case let .transitInfoComponents(transitInformation): var transformedTransitInformation = [String: String]() - guard let transitInformation = transitInformation else { return } + guard let transitInformation = transitInformation else { return false } transitInformation.forEach { (key, value) in transformedTransitInformation[key.rawValue] = value } handleTransitInformation(transformedTransitInformation) case let .custom(pattern, match): - guard let match = match else { return } + guard let match = match else { return false } switch detectorType { case .hashtag: handleHashtag(match) @@ -512,6 +522,8 @@ open class MessageLabel: UILabel { handleCustom(pattern, match: match) } } + + return true } private func handleAddress(_ addressComponents: [String: String]) { From 81977a5e6ccd58efb5ac62cefc49bc32c5855583 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Wed, 18 Dec 2024 01:01:38 +0100 Subject: [PATCH 03/11] make linter happy --- deltachat-ios/Chat/Views/MessageLabel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deltachat-ios/Chat/Views/MessageLabel.swift b/deltachat-ios/Chat/Views/MessageLabel.swift index 6ef0f9168..6da4fb607 100644 --- a/deltachat-ios/Chat/Views/MessageLabel.swift +++ b/deltachat-ios/Chat/Views/MessageLabel.swift @@ -467,7 +467,7 @@ open class MessageLabel: UILabel { } internal func getCopyableLinkText(_ touchLocation: CGPoint) -> String? { - guard let (detectorType, value) = detectLink(touchLocation) else { return nil } + guard let (_, value) = detectLink(touchLocation) else { return nil } switch value { case let .link(url): @@ -475,7 +475,7 @@ open class MessageLabel: UILabel { return url.absoluteString case let .phoneNumber(phoneNumber): return phoneNumber - case let .custom(pattern, match): + case let .custom(_, match): return match case .addressComponents, .date, .transitInfoComponents: return nil From b203ad7d21101762a427d545f75a242e8713a713 Mon Sep 17 00:00:00 2001 From: bjoern Date: Wed, 18 Dec 2024 10:39:35 +0100 Subject: [PATCH 04/11] Update deltachat-ios/Chat/Views/MessageLabel.swift Co-authored-by: zeitschlag --- deltachat-ios/Chat/Views/MessageLabel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-ios/Chat/Views/MessageLabel.swift b/deltachat-ios/Chat/Views/MessageLabel.swift index 6da4fb607..6bb41a6c7 100644 --- a/deltachat-ios/Chat/Views/MessageLabel.swift +++ b/deltachat-ios/Chat/Views/MessageLabel.swift @@ -471,7 +471,7 @@ open class MessageLabel: UILabel { switch value { case let .link(url): - guard let url = url else { return nil } + guard let url else { return nil } return url.absoluteString case let .phoneNumber(phoneNumber): return phoneNumber From c9ca1fd2d690d09cd24d9ebe7156918be60d18cb Mon Sep 17 00:00:00 2001 From: bjoern Date: Wed, 18 Dec 2024 10:39:53 +0100 Subject: [PATCH 05/11] Update deltachat-ios/Chat/Views/MessageLabel.swift Co-authored-by: zeitschlag --- deltachat-ios/Chat/Views/MessageLabel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-ios/Chat/Views/MessageLabel.swift b/deltachat-ios/Chat/Views/MessageLabel.swift index 6bb41a6c7..4b77ba713 100644 --- a/deltachat-ios/Chat/Views/MessageLabel.swift +++ b/deltachat-ios/Chat/Views/MessageLabel.swift @@ -488,7 +488,7 @@ open class MessageLabel: UILabel { switch value { case let .addressComponents(addressComponents): var transformedAddressComponents = [String: String]() - guard let addressComponents = addressComponents else { return false } + guard let addressComponents else { return false } addressComponents.forEach { (key, value) in transformedAddressComponents[key.rawValue] = value } From 4a76e2c16604b0d68401f09b7393e2debce9f0b2 Mon Sep 17 00:00:00 2001 From: bjoern Date: Wed, 18 Dec 2024 10:40:05 +0100 Subject: [PATCH 06/11] Update deltachat-ios/Chat/Views/MessageLabel.swift Co-authored-by: zeitschlag --- deltachat-ios/Chat/Views/MessageLabel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-ios/Chat/Views/MessageLabel.swift b/deltachat-ios/Chat/Views/MessageLabel.swift index 4b77ba713..97b82b4e3 100644 --- a/deltachat-ios/Chat/Views/MessageLabel.swift +++ b/deltachat-ios/Chat/Views/MessageLabel.swift @@ -494,7 +494,7 @@ open class MessageLabel: UILabel { } handleAddress(transformedAddressComponents) case let .phoneNumber(phoneNumber): - guard let phoneNumber = phoneNumber else { return false } + guard let phoneNumber else { return false } handlePhoneNumber(phoneNumber) case let .date(date): guard let date = date else { return false } From a3414ff06497357959d39e06a1a6cc96f8d37b66 Mon Sep 17 00:00:00 2001 From: bjoern Date: Wed, 18 Dec 2024 10:40:21 +0100 Subject: [PATCH 07/11] Update deltachat-ios/Chat/Views/MessageLabel.swift Co-authored-by: zeitschlag --- deltachat-ios/Chat/Views/MessageLabel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-ios/Chat/Views/MessageLabel.swift b/deltachat-ios/Chat/Views/MessageLabel.swift index 97b82b4e3..ba72d4f64 100644 --- a/deltachat-ios/Chat/Views/MessageLabel.swift +++ b/deltachat-ios/Chat/Views/MessageLabel.swift @@ -497,7 +497,7 @@ open class MessageLabel: UILabel { guard let phoneNumber else { return false } handlePhoneNumber(phoneNumber) case let .date(date): - guard let date = date else { return false } + guard let date else { return false } handleDate(date) case let .link(url): guard let url = url else { return false } From 7ce6801b85b00a3e41b6d510cc0feee9ebb2a4b8 Mon Sep 17 00:00:00 2001 From: bjoern Date: Wed, 18 Dec 2024 10:40:37 +0100 Subject: [PATCH 08/11] Update deltachat-ios/Chat/Views/MessageLabel.swift Co-authored-by: zeitschlag --- deltachat-ios/Chat/Views/MessageLabel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-ios/Chat/Views/MessageLabel.swift b/deltachat-ios/Chat/Views/MessageLabel.swift index ba72d4f64..31ec60208 100644 --- a/deltachat-ios/Chat/Views/MessageLabel.swift +++ b/deltachat-ios/Chat/Views/MessageLabel.swift @@ -510,7 +510,7 @@ open class MessageLabel: UILabel { } handleTransitInformation(transformedTransitInformation) case let .custom(pattern, match): - guard let match = match else { return false } + guard let match else { return false } switch detectorType { case .hashtag: handleHashtag(match) From ec6b2e9606b3e59f698f5c9920b3181d6cd1056e Mon Sep 17 00:00:00 2001 From: bjoern Date: Wed, 18 Dec 2024 10:40:50 +0100 Subject: [PATCH 09/11] Update deltachat-ios/Chat/Views/MessageLabel.swift Co-authored-by: zeitschlag --- deltachat-ios/Chat/Views/MessageLabel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-ios/Chat/Views/MessageLabel.swift b/deltachat-ios/Chat/Views/MessageLabel.swift index 31ec60208..2e8e8aff5 100644 --- a/deltachat-ios/Chat/Views/MessageLabel.swift +++ b/deltachat-ios/Chat/Views/MessageLabel.swift @@ -504,7 +504,7 @@ open class MessageLabel: UILabel { handleURL(url) case let .transitInfoComponents(transitInformation): var transformedTransitInformation = [String: String]() - guard let transitInformation = transitInformation else { return false } + guard let transitInformation else { return false } transitInformation.forEach { (key, value) in transformedTransitInformation[key.rawValue] = value } From a26bcf79dd3b2b9489310930ae7e5d18d25c68d8 Mon Sep 17 00:00:00 2001 From: bjoern Date: Wed, 18 Dec 2024 10:41:03 +0100 Subject: [PATCH 10/11] Update deltachat-ios/Chat/Views/MessageLabel.swift Co-authored-by: zeitschlag --- deltachat-ios/Chat/Views/MessageLabel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-ios/Chat/Views/MessageLabel.swift b/deltachat-ios/Chat/Views/MessageLabel.swift index 2e8e8aff5..181ab1826 100644 --- a/deltachat-ios/Chat/Views/MessageLabel.swift +++ b/deltachat-ios/Chat/Views/MessageLabel.swift @@ -500,7 +500,7 @@ open class MessageLabel: UILabel { guard let date else { return false } handleDate(date) case let .link(url): - guard let url = url else { return false } + guard let url else { return false } handleURL(url) case let .transitInfoComponents(transitInformation): var transformedTransitInformation = [String: String]() From ba0c10751f65db46d1c1a6f354e7b0cb821c3dab Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Wed, 18 Dec 2024 11:17:56 +0100 Subject: [PATCH 11/11] add CHANGELOG entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 496269f84..97f17e967 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## Unreleased - Don't show message-input when forwarding (#2435) +- Long-tap links for copying to clipboard (#2445) + ## v1.50.3