From d7b57013aa2e84707e99637710f58baac75af9c6 Mon Sep 17 00:00:00 2001 From: Talon Brown Date: Fri, 16 May 2025 12:18:31 -0500 Subject: [PATCH 1/3] Added drop functionality to ConversationInputTextView (cherry picked from commit b26e5a01c3e831a16c31435f9aa60356ff58f45c) --- .../ConversationInputTextView.swift | 30 +++++++++++++++++++ ...ConversationViewController+Delegates.swift | 13 ++++++++ .../Attachments/SignalAttachment.swift | 14 +++++++++ 3 files changed, 57 insertions(+) diff --git a/Signal/ConversationView/ConversationInputTextView.swift b/Signal/ConversationView/ConversationInputTextView.swift index d23596297a0..2696370c0b8 100644 --- a/Signal/ConversationView/ConversationInputTextView.swift +++ b/Signal/ConversationView/ConversationInputTextView.swift @@ -5,8 +5,12 @@ public import SignalServiceKit import SignalUI +import UIKit +import MobileCoreServices +import UniformTypeIdentifiers public protocol ConversationInputTextViewDelegate: AnyObject { + func didAttemptToDropAttachments(_ attachments: [SignalAttachment]) func didAttemptAttachmentPaste() func inputTextViewSendMessagePressed() func textViewDidChange(_ textView: UITextView) @@ -46,6 +50,10 @@ class ConversationInputTextView: BodyRangesTextView { contentMode = .redraw dataDetectorTypes = [] + + //Drop Interaction for images + let dropInteraction = UIDropInteraction(delegate: self) + addInteraction(dropInteraction) placeholderView.text = OWSLocalizedString( "INPUT_TOOLBAR_MESSAGE_PLACEHOLDER", @@ -221,3 +229,25 @@ class ConversationInputTextView: BodyRangesTextView { textViewToolbarDelegate?.textViewDidChange(self) } } + +extension ConversationInputTextView : UIDropInteractionDelegate { + + func dropInteraction(_ interaction: UIDropInteraction, canHandle session: any UIDropSession) -> Bool { + let inputUtiTypes = Array(MimeTypeUtil.supportedInputImageUtiTypes) + return session.hasItemsConforming(toTypeIdentifiers: inputUtiTypes) + } + + func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: any UIDropSession) -> UIDropProposal { + return UIDropProposal(operation: .copy) + } + + func dropInteraction(_ interaction: UIDropInteraction, performDrop session: any UIDropSession) { + //session.loadObject automatically runs on the main thread as per documentation + session.loadObjects(ofClass: SignalAttachment.self) { attachmentItems in + if let attachments = attachmentItems as? [SignalAttachment] { + self.inputTextViewDelegate?.didAttemptToDropAttachments(attachments) + } + } + } + +} diff --git a/Signal/ConversationView/ConversationViewController+Delegates.swift b/Signal/ConversationView/ConversationViewController+Delegates.swift index 632c9c44e1c..5e34cb291af 100644 --- a/Signal/ConversationView/ConversationViewController+Delegates.swift +++ b/Signal/ConversationView/ConversationViewController+Delegates.swift @@ -223,6 +223,19 @@ extension ConversationViewController: ConversationHeaderViewDelegate { // MARK: - extension ConversationViewController: ConversationInputTextViewDelegate { + + public func didAttemptToDropAttachments(_ attachments: [SignalAttachment]) { + //We only want one attachment (image for now) + if let firstAttachment = attachments.first { + if firstAttachment.isBorderless { + tryToSendAttachments([ firstAttachment ], messageBody: nil) + } else { + dismissKeyBoard() + showApprovalDialog(forAttachment: firstAttachment) + } + } + } + public func didAttemptAttachmentPaste() { ModalActivityIndicatorViewController.present(fromViewController: self) { modal in let attachment: SignalAttachment? = await SignalAttachment.attachmentFromPasteboard() diff --git a/SignalServiceKit/Attachments/SignalAttachment.swift b/SignalServiceKit/Attachments/SignalAttachment.swift index e71446feb16..9e08ca7d953 100644 --- a/SignalServiceKit/Attachments/SignalAttachment.swift +++ b/SignalServiceKit/Attachments/SignalAttachment.swift @@ -1387,3 +1387,17 @@ public class SignalAttachment: NSObject { return attachment } } + + +extension SignalAttachment : NSItemProviderReading { + + public static var readableTypeIdentifiersForItemProvider: [String] { + return Array(inputImageUTISet) //Only support images for now + } + + public static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Self { + let dataSource = DataSourceValue(data, utiType: typeIdentifier) + return SignalAttachment.attachment(dataSource: dataSource, dataUTI: typeIdentifier) as! Self + } + +} From f3cb18752af83f7de6fe7e85e051a00fdcfc004a Mon Sep 17 00:00:00 2001 From: Talon Brown Date: Fri, 16 May 2025 13:41:07 -0500 Subject: [PATCH 2/3] Added drag and drop functionality to conversation view controller (cherry picked from commit 913bb4ea2d42a6f6c1fec2792c18e5fd8a575e43) --- .../ConversationViewController.swift | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Signal/ConversationView/ConversationViewController.swift b/Signal/ConversationView/ConversationViewController.swift index 1b06b476b3f..f9773943c94 100644 --- a/Signal/ConversationView/ConversationViewController.swift +++ b/Signal/ConversationView/ConversationViewController.swift @@ -5,6 +5,7 @@ public import SignalServiceKit public import SignalUI +import UIKit public enum ConversationUIMode: UInt { case normal @@ -223,6 +224,9 @@ public final class ConversationViewController: OWSViewController { loadCoordinator.viewDidLoad() self.startReloadTimer() + + let dropInteraction = UIDropInteraction(delegate: self) + self.view.addInteraction(dropInteraction) } private func createContents() { @@ -683,3 +687,31 @@ extension ConversationViewController: ContactsViewHelperObserver { loadCoordinator.enqueueReload(canReuseInteractionModels: true, canReuseComponentStates: false) } } + +extension ConversationViewController: UIDropInteractionDelegate { + + public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: any UIDropSession) -> Bool { + let inputUtiTypes = Array(MimeTypeUtil.supportedInputImageUtiTypes) + return session.hasItemsConforming(toTypeIdentifiers: inputUtiTypes) && uiMode == .normal + } + + public func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: any UIDropSession) -> UIDropProposal { + return UIDropProposal(operation: .copy) + } + + public func dropInteraction(_ interaction: UIDropInteraction, performDrop session: any UIDropSession) { + //session.loadObject automatically runs on the main thread as per documentation + session.loadObjects(ofClass: SignalAttachment.self) { [weak self] attachmentItems in + if let attachments = attachmentItems as? [SignalAttachment], let firstAttachment = attachments.first { + //MARK: Perhaps if we're using this code block so much, it should be refactored into its own function somewhere. + if firstAttachment.isBorderless { + self?.tryToSendAttachments([ firstAttachment ], messageBody: nil) + } else { + self?.dismissKeyBoard() + self?.showApprovalDialog(forAttachment: firstAttachment) + } + } + } + } + +} From eade6b374379c5544633533a31714a73fba63b9a Mon Sep 17 00:00:00 2001 From: Talon Brown Date: Fri, 16 May 2025 14:43:50 -0500 Subject: [PATCH 3/3] Edge case for stickers (cherry picked from commit 91cc49b998acc42df26f6bb6e6a58eec6d97400f) --- SignalServiceKit/Attachments/SignalAttachment.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SignalServiceKit/Attachments/SignalAttachment.swift b/SignalServiceKit/Attachments/SignalAttachment.swift index 9e08ca7d953..f4162a6799a 100644 --- a/SignalServiceKit/Attachments/SignalAttachment.swift +++ b/SignalServiceKit/Attachments/SignalAttachment.swift @@ -1397,7 +1397,7 @@ extension SignalAttachment : NSItemProviderReading { public static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Self { let dataSource = DataSourceValue(data, utiType: typeIdentifier) - return SignalAttachment.attachment(dataSource: dataSource, dataUTI: typeIdentifier) as! Self + return imageAttachment(dataSource: dataSource, dataUTI: typeIdentifier, isBorderless: dataSource?.hasStickerLikeProperties ?? false) as! Self } }