Sfoglia il codice sorgente

allow to drag and drop directly into the chat table view, not only the message input bar

cyberta 2 anni fa
parent
commit
489576d4e3

+ 4 - 0
deltachat-ios.xcodeproj/project.pbxproj

@@ -103,6 +103,7 @@
 		30B2BD02278F1C1900889AA4 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3011E8042787365D00214221 /* KeychainManager.swift */; };
 		30C0D49D237C4908008E2A0E /* CertificateCheckController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C0D49C237C4908008E2A0E /* CertificateCheckController.swift */; };
 		30C2BFFE27032375005505DA /* ChatSearchAccessoryBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C2BFFD27032375005505DA /* ChatSearchAccessoryBar.swift */; };
+		30CE137828D9C40800158DF4 /* ChatDropInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CE137728D9C40700158DF4 /* ChatDropInteraction.swift */; };
 		30DAF71C275901610073C154 /* SettingsBackgroundSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DAF71B275901610073C154 /* SettingsBackgroundSelectionController.swift */; };
 		30E348DF24F3F819005C93D1 /* ChatTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E348DE24F3F819005C93D1 /* ChatTableView.swift */; };
 		30E348E124F53772005C93D1 /* ImageTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E348E024F53772005C93D1 /* ImageTextCell.swift */; };
@@ -380,6 +381,7 @@
 		30B0ACF924AB5B99004D5E29 /* SettingsEphemeralMessageController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsEphemeralMessageController.swift; sourceTree = "<group>"; };
 		30C0D49C237C4908008E2A0E /* CertificateCheckController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateCheckController.swift; sourceTree = "<group>"; };
 		30C2BFFD27032375005505DA /* ChatSearchAccessoryBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatSearchAccessoryBar.swift; sourceTree = "<group>"; };
+		30CE137728D9C40700158DF4 /* ChatDropInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDropInteraction.swift; sourceTree = "<group>"; };
 		30DAF71B275901610073C154 /* SettingsBackgroundSelectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsBackgroundSelectionController.swift; sourceTree = "<group>"; };
 		30E348DE24F3F819005C93D1 /* ChatTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTableView.swift; sourceTree = "<group>"; };
 		30E348E024F53772005C93D1 /* ImageTextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageTextCell.swift; sourceTree = "<group>"; };
@@ -988,6 +990,7 @@
 				302D544F268B6B2300A8B271 /* MessageUtils.swift */,
 				3011E8042787365D00214221 /* KeychainManager.swift */,
 				30E83EFC289BF32C0035614C /* ShortcutManager.swift */,
+				30CE137728D9C40700158DF4 /* ChatDropInteraction.swift */,
 			);
 			path = Helper;
 			sourceTree = "<group>";
@@ -1417,6 +1420,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				78ED839421D5AF8A00243125 /* QrCodeView.swift in Sources */,
+				30CE137828D9C40800158DF4 /* ChatDropInteraction.swift in Sources */,
 				3059620E234614E700C80F33 /* DcContact+Extension.swift in Sources */,
 				AED423D7249F580700B6B2BB /* BlockedContactsViewController.swift in Sources */,
 				303492B32577E40700A523D0 /* DocumentPreview.swift in Sources */,

+ 49 - 12
deltachat-ios/Chat/ChatViewController.swift

@@ -5,7 +5,7 @@ import AVFoundation
 import DcCore
 import SDWebImage
 
-class ChatViewController: UITableViewController {
+class ChatViewController: UITableViewController, UITableViewDropDelegate {
     var dcContext: DcContext
     let outgoingAvatarOverlap: CGFloat = 17.5
     let loadCount = 30
@@ -30,6 +30,12 @@ class ChatViewController: UITableViewController {
         return draft
     }()
 
+    private lazy var dropInteraction: ChatDropInteraction = {
+        let dropInteraction = ChatDropInteraction()
+        dropInteraction.delegate = self
+        return dropInteraction
+    }()
+
     // search related
     private var activateSearch: Bool = false
     private var searchMessageIds: [Int] = []
@@ -393,6 +399,8 @@ class ChatViewController: UITableViewController {
         messageInputBar.inputTextView.text = draft.text
         configureDraftArea(draft: draft, animated: false)
         tableView.allowsMultipleSelectionDuringEditing = true
+        tableView.dragInteractionEnabled = true
+        tableView.dropDelegate = self
     }
 
     private func getTopInsetHeight() -> CGFloat {
@@ -1267,6 +1275,7 @@ class ChatViewController: UITableViewController {
         messageInputBar.inputTextView.delegate = self
         messageInputBar.inputTextView.textViewPasteDelegate = self
         messageInputBar.onScrollDownButtonPressed = scrollToBottom
+        messageInputBar.inputTextView.setDropInteractionDelegate(delegate: self)
     }
 
     private func evaluateInputBar(draft: DraftModel) {
@@ -1800,6 +1809,21 @@ class ChatViewController: UITableViewController {
         return !isHidden
     }
 
+    @objc(tableView:canHandleDropSession:)
+    func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
+        return self.dropInteraction.dropInteraction(canHandle: session)
+    }
+
+    @objc
+    func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
+        return UITableViewDropProposal(operation: .copy)
+    }
+
+    @objc(tableView:performDropWithCoordinator:)
+    func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
+        return self.dropInteraction.dropInteraction(performDrop: coordinator.session)
+    }
+
     override func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
         return !tableView.isEditing && contextMenu.canPerformAction(action: action)
     }
@@ -2414,17 +2438,6 @@ extension ChatViewController: ChatInputTextViewPasteDelegate {
     func onImagePasted(image: UIImage) {
         sendSticker(image)
     }
-    func onImageDragAndDropped(image: UIImage) {
-        stageImage(image)
-    }
-
-    func onVideoDragAndDropped(url: NSURL) {
-        stageVideo(url: url)
-    }
-
-    func onFileDragAndDropped(url: NSURL) {
-        stageDocument(url: url)
-    }
 }
 
 
@@ -2453,3 +2466,27 @@ extension ChatViewController: WebxdcSelectorDelegate {
         }
     }
 }
+
+extension ChatViewController: ChatDropInteractionDelegate {
+    func onImageDragAndDropped(image: UIImage) {
+        stageImage(image)
+    }
+
+    func onVideoDragAndDropped(url: NSURL) {
+        stageVideo(url: url)
+    }
+
+    func onFileDragAndDropped(url: NSURL) {
+        stageDocument(url: url)
+    }
+
+    func onTextDragAndDropped(text: String) {
+        if messageInputBar.inputTextView.text.isEmpty {
+            messageInputBar.inputTextView.text = text
+        } else {
+            var updatedText = messageInputBar.inputTextView.text
+            updatedText?.append(" \(text) ")
+            messageInputBar.inputTextView.text = updatedText
+        }
+    }
+}

+ 10 - 91
deltachat-ios/Chat/Views/ChatInputTextView.swift

@@ -6,6 +6,13 @@ import UniformTypeIdentifiers
 public class ChatInputTextView: InputTextView {
 
     public weak var textViewPasteDelegate: ChatInputTextViewPasteDelegate?
+    private lazy var dropInteraction: ChatDropInteraction = {
+        return ChatDropInteraction()
+    }()
+
+    public func setDropInteractionDelegate(delegate: ChatDropInteractionDelegate) {
+        dropInteraction.delegate = delegate
+    }
 
     // MARK: - Image Paste Support
     open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
@@ -25,106 +32,18 @@ public class ChatInputTextView: InputTextView {
 
 extension ChatInputTextView: UIDropInteractionDelegate {
     public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
-        if #available(iOS 14.0, *) {
-            return  session.items.count == 1 && session.hasItemsConforming(toTypeIdentifiers: [
-                UTType.image.identifier,
-                UTType.video.identifier,
-                UTType.movie.identifier,
-                UTType.text.identifier,
-                UTType.item.identifier])
-        }
-        return session.items.count == 1 && session.hasItemsConforming(toTypeIdentifiers: [
-            kUTTypeImage as String,
-            kUTTypeText as String,
-            kUTTypeMovie as String,
-            kUTTypeVideo as String,
-            kUTTypeItem as String])
+        dropInteraction.dropInteraction(canHandle: session)
     }
 
     public func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
-            return UIDropProposal(operation: .copy)
+        dropInteraction.dropInteraction(sessionDidUpdate: session)
     }
 
     public func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
-        if #available(iOS 15.0, *) {
-            if session.hasItemsConforming(toTypeIdentifiers: [UTType.image.identifier]) {
-               loadImageObjects(session: session)
-            } else if session.hasItemsConforming(toTypeIdentifiers: [UTType.movie.identifier, UTType.video.identifier]) {
-                loadFileObjects(session: session, isVideo: true)
-            } else if session.hasItemsConforming(toTypeIdentifiers: [UTType.item.identifier]) {
-                loadFileObjects(session: session)
-            } else if session.hasItemsConforming(toTypeIdentifiers: [UTType.text.identifier]) {
-                loadTextObjects(session: session)
-            }
-        } else {
-            if session.hasItemsConforming(toTypeIdentifiers: [kUTTypeImage as String]) {
-               loadImageObjects(session: session)
-            } else if session.hasItemsConforming(toTypeIdentifiers: [kUTTypeMovie as String, kUTTypeVideo as String]) {
-                loadFileObjects(session: session, isVideo: true)
-            } else if session.hasItemsConforming(toTypeIdentifiers: [kUTTypeItem as String]) {
-                loadFileObjects(session: session)
-            } else if session.hasItemsConforming(toTypeIdentifiers: [kUTTypeText as String]) {
-                loadTextObjects(session: session)
-            }
-        }
-    }
-
-    private func loadImageObjects(session: UIDropSession) {
-        session.loadObjects(ofClass: UIImage.self) { [weak self] imageItems in
-            if let images = imageItems as? [UIImage], !images.isEmpty {
-                self?.textViewPasteDelegate?.onImageDragAndDropped(image: images[0])
-            }
-        }
-    }
-
-    private func loadFileObjects(session: UIDropSession, isVideo: Bool = false) {
-        if session.items.isEmpty {
-            return
-        }
-        let item: UIDragItem = session.items[0]
-        item.itemProvider.loadFileRepresentation(forTypeIdentifier: kUTTypeItem as String) { [weak self] (url, error) in
-            guard let url = url else {
-                if let error = error {
-                    logger.error("error loading file \(error)")
-                }
-                return
-            }
-            DispatchQueue.global().async { [weak self] in
-                let nsdata = NSData(contentsOf: url)
-                guard let data = nsdata as? Data else { return }
-                let name = url.deletingPathExtension().lastPathComponent
-                guard let fileName = FileHelper.saveData(data: data,
-                                                         name: name,
-                                                         suffix: url.pathExtension,
-                                                         directory: .cachesDirectory) else { return }
-                DispatchQueue.main.async {
-                    if isVideo {
-                        self?.textViewPasteDelegate?.onVideoDragAndDropped(url: NSURL(fileURLWithPath: fileName))
-                    } else {
-                        self?.textViewPasteDelegate?.onFileDragAndDropped(url: NSURL(fileURLWithPath: fileName))
-                    }
-                }
-            }
-        }
-    }
-
-    private func loadTextObjects(session: UIDropSession) {
-        session.loadObjects(ofClass: String.self) { [weak self] stringItems in
-            guard !stringItems.isEmpty else { return }
-            if let isEmpty = self?.text.isEmpty, isEmpty {
-                self?.text = stringItems[0]
-            } else {
-                var updatedText = self?.text
-                updatedText?.append(" \(stringItems[0]) ")
-                self?.text = updatedText
-            }
-        }
+        dropInteraction.dropInteraction(performDrop: session)
     }
 }
 
 public protocol ChatInputTextViewPasteDelegate: class {
     func onImagePasted(image: UIImage)
-    func onImageDragAndDropped(image: UIImage)
-    func onVideoDragAndDropped(url: NSURL)
-    func onFileDragAndDropped(url: NSURL)
 }

+ 107 - 0
deltachat-ios/Helper/ChatDropInteraction.swift

@@ -0,0 +1,107 @@
+import Foundation
+import UIKit
+import MobileCoreServices
+import UniformTypeIdentifiers
+
+public class ChatDropInteraction {
+
+    public weak var delegate: ChatDropInteractionDelegate?
+
+    public func dropInteraction(canHandle session: UIDropSession) -> Bool {
+        if #available(iOS 14.0, *) {
+            return  session.items.count == 1 && session.hasItemsConforming(toTypeIdentifiers: [
+                UTType.image.identifier,
+                UTType.video.identifier,
+                UTType.movie.identifier,
+                UTType.text.identifier,
+                UTType.item.identifier])
+        }
+        return session.items.count == 1 && session.hasItemsConforming(toTypeIdentifiers: [
+            kUTTypeImage as String,
+            kUTTypeText as String,
+            kUTTypeMovie as String,
+            kUTTypeVideo as String,
+            kUTTypeItem as String])
+    }
+
+    public func dropInteraction(sessionDidUpdate session: UIDropSession) -> UIDropProposal {
+            return UIDropProposal(operation: .copy)
+    }
+
+    public func dropInteraction(performDrop session: UIDropSession) {
+        if #available(iOS 15.0, *) {
+            if session.hasItemsConforming(toTypeIdentifiers: [UTType.image.identifier]) {
+               loadImageObjects(session: session)
+            } else if session.hasItemsConforming(toTypeIdentifiers: [UTType.movie.identifier, UTType.video.identifier]) {
+                loadFileObjects(session: session, isVideo: true)
+            } else if session.hasItemsConforming(toTypeIdentifiers: [UTType.item.identifier]) {
+                loadFileObjects(session: session)
+            } else if session.hasItemsConforming(toTypeIdentifiers: [UTType.text.identifier]) {
+                loadTextObjects(session: session)
+            }
+        } else {
+            if session.hasItemsConforming(toTypeIdentifiers: [kUTTypeImage as String]) {
+               loadImageObjects(session: session)
+            } else if session.hasItemsConforming(toTypeIdentifiers: [kUTTypeMovie as String, kUTTypeVideo as String]) {
+                loadFileObjects(session: session, isVideo: true)
+            } else if session.hasItemsConforming(toTypeIdentifiers: [kUTTypeItem as String]) {
+                loadFileObjects(session: session)
+            } else if session.hasItemsConforming(toTypeIdentifiers: [kUTTypeText as String]) {
+                loadTextObjects(session: session)
+            }
+        }
+    }
+
+    private func loadImageObjects(session: UIDropSession) {
+        session.loadObjects(ofClass: UIImage.self) { [weak self] imageItems in
+            if let images = imageItems as? [UIImage], !images.isEmpty {
+                self?.delegate?.onImageDragAndDropped(image: images[0])
+            }
+        }
+    }
+
+    private func loadFileObjects(session: UIDropSession, isVideo: Bool = false) {
+        if session.items.isEmpty {
+            return
+        }
+        let item: UIDragItem = session.items[0]
+        item.itemProvider.loadFileRepresentation(forTypeIdentifier: kUTTypeItem as String) { [weak self] (url, error) in
+            guard let url = url else {
+                if let error = error {
+                    logger.error("error loading file \(error)")
+                }
+                return
+            }
+            DispatchQueue.global().async { [weak self] in
+                let nsdata = NSData(contentsOf: url)
+                guard let data = nsdata as? Data else { return }
+                let name = url.deletingPathExtension().lastPathComponent
+                guard let fileName = FileHelper.saveData(data: data,
+                                                         name: name,
+                                                         suffix: url.pathExtension,
+                                                         directory: .cachesDirectory) else { return }
+                DispatchQueue.main.async {
+                    if isVideo {
+                        self?.delegate?.onVideoDragAndDropped(url: NSURL(fileURLWithPath: fileName))
+                    } else {
+                        self?.delegate?.onFileDragAndDropped(url: NSURL(fileURLWithPath: fileName))
+                    }
+                }
+            }
+        }
+    }
+
+    private func loadTextObjects(session: UIDropSession) {
+        session.loadObjects(ofClass: String.self) { [weak self] stringItems in
+            guard !stringItems.isEmpty else { return }
+            self?.delegate?.onTextDragAndDropped(text: stringItems[0])
+        }
+    }
+}
+
+public protocol ChatDropInteractionDelegate: class {
+    func onImageDragAndDropped(image: UIImage)
+    func onVideoDragAndDropped(url: NSURL)
+    func onFileDragAndDropped(url: NSURL)
+    func onTextDragAndDropped(text: String)
+}