Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Signal/ConversationView/ConversationInputTextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)
}
}
}

}
13 changes: 13 additions & 0 deletions Signal/ConversationView/ConversationViewController+Delegates.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
32 changes: 32 additions & 0 deletions Signal/ConversationView/ConversationViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

public import SignalServiceKit
public import SignalUI
import UIKit

public enum ConversationUIMode: UInt {
case normal
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
}
}
}
}

}
14 changes: 14 additions & 0 deletions SignalServiceKit/Attachments/SignalAttachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

}