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/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) + } + } + } + } + +} diff --git a/SignalServiceKit/Attachments/SignalAttachment.swift b/SignalServiceKit/Attachments/SignalAttachment.swift index e71446feb16..f4162a6799a 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 imageAttachment(dataSource: dataSource, dataUTI: typeIdentifier, isBorderless: dataSource?.hasStickerLikeProperties ?? false) as! Self + } + +}