Parcourir la source

Merge pull request #1682 from deltachat/drag-and-drop

drag-and-drop into chats
cyBerta il y a 2 ans
Parent
commit
ddb4d1b6de

+ 5 - 1
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>"; };
@@ -971,6 +973,7 @@
 		AE851AC2227C695000ED86F0 /* Helper */ = {
 			isa = PBXGroup;
 			children = (
+				30238CFE28A5554C00EF14AC /* FileHelper.swift */,
 				3067AAC52667F3FE00525036 /* ImageFormat.swift */,
 				305702A024C6453700D84EFC /* TypeAlias.swift */,
 				AEACE2E21FB32B5C00DCDD78 /* Constants.swift */,
@@ -986,8 +989,8 @@
 				21D6C9392606190600D0755A /* NotificationManager.swift */,
 				302D544F268B6B2300A8B271 /* MessageUtils.swift */,
 				3011E8042787365D00214221 /* KeychainManager.swift */,
-				30238CFE28A5554C00EF14AC /* FileHelper.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 */,

+ 60 - 9
deltachat-ios/Chat/ChatViewController.swift

@@ -5,10 +5,8 @@ import AVFoundation
 import DcCore
 import SDWebImage
 
-class ChatViewController: UITableViewController {
+class ChatViewController: UITableViewController, UITableViewDropDelegate {
     var dcContext: DcContext
-    let outgoingAvatarOverlap: CGFloat = 17.5
-    let loadCount = 30
     let chatId: Int
     var messageIds: [Int] = []
 
@@ -30,6 +28,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 +397,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 +1273,7 @@ class ChatViewController: UITableViewController {
         messageInputBar.inputTextView.delegate = self
         messageInputBar.inputTextView.imagePasteDelegate = self
         messageInputBar.onScrollDownButtonPressed = scrollToBottom
+        messageInputBar.inputTextView.setDropInteractionDelegate(delegate: self)
     }
 
     private func evaluateInputBar(draft: DraftModel) {
@@ -1686,9 +1693,13 @@ class ChatViewController: UITableViewController {
 
     private func stageDocument(url: NSURL) {
         keepKeyboard = true
-        self.draft.setAttachment(viewType: url.pathExtension == "xdc" ? DC_MSG_WEBXDC : DC_MSG_FILE, path: url.relativePath)
-        self.configureDraftArea(draft: self.draft)
-        self.focusInputTextView()
+        DispatchQueue.main.async { [weak self] in
+            guard let self = self else { return }
+            self.draft.setAttachment(viewType: url.pathExtension == "xdc" ? DC_MSG_WEBXDC : DC_MSG_FILE, path: url.relativePath)
+            self.configureDraftArea(draft: self.draft)
+            self.focusInputTextView()
+            FileHelper.deleteFile(atPath: url.relativePath)
+        }
     }
 
     private func stageVideo(url: NSURL) {
@@ -1698,6 +1709,7 @@ class ChatViewController: UITableViewController {
             self.draft.setAttachment(viewType: DC_MSG_VIDEO, path: url.relativePath)
             self.configureDraftArea(draft: self.draft)
             self.focusInputTextView()
+            FileHelper.deleteFile(atPath: url.relativePath)
         }
     }
 
@@ -1722,7 +1734,7 @@ class ChatViewController: UITableViewController {
                     }
                     self.configureDraftArea(draft: self.draft)
                     self.focusInputTextView()
-                    ImageFormat.deleteImage(atPath: pathInCachesDir)
+                    FileHelper.deleteFile(atPath: pathInCachesDir)
                 }
             }
         }
@@ -1733,7 +1745,7 @@ class ChatViewController: UITableViewController {
             guard let self = self else { return }
             if let path = ImageFormat.saveImage(image: image, directory: .cachesDirectory) {
                 self.sendAttachmentMessage(viewType: DC_MSG_IMAGE, filePath: path, message: message)
-                ImageFormat.deleteImage(atPath: path)
+                FileHelper.deleteFile(atPath: path)
             }
         }
     }
@@ -1743,7 +1755,7 @@ class ChatViewController: UITableViewController {
             guard let self = self else { return }
             if let path = ImageFormat.saveImage(image: image, directory: .cachesDirectory) {
                 self.sendAttachmentMessage(viewType: DC_MSG_STICKER, filePath: path, message: nil)
-                ImageFormat.deleteImage(atPath: path)
+                FileHelper.deleteFile(atPath: path)
             }
         }
     }
@@ -1795,6 +1807,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)
     }
@@ -2437,3 +2464,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
+        }
+    }
+}

+ 2 - 0
deltachat-ios/Chat/InputBarAccessoryView/InputBarAccessoryView.swift

@@ -147,6 +147,8 @@ open class InputBarAccessoryView: UIView {
         let inputTextView = ChatInputTextView()
         inputTextView.translatesAutoresizingMaskIntoConstraints = false
         inputTextView.inputBarAccessoryView = self
+        let dropInteraction = UIDropInteraction(delegate: inputTextView)
+        inputTextView.addInteraction(dropInteraction)
         return inputTextView
     }()
     

+ 22 - 0
deltachat-ios/Chat/Views/ChatInputTextView.swift

@@ -1,9 +1,18 @@
 import Foundation
 import UIKit
+import MobileCoreServices
+import UniformTypeIdentifiers
 
 public class ChatInputTextView: InputTextView {
 
     public weak var imagePasteDelegate: 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 {
@@ -21,6 +30,19 @@ public class ChatInputTextView: InputTextView {
     }
 }
 
+extension ChatInputTextView: UIDropInteractionDelegate {
+    public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
+        return dropInteraction.dropInteraction(canHandle: session)
+    }
+
+    public func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
+        return dropInteraction.dropInteraction(sessionDidUpdate: session)
+    }
+
+    public func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
+        dropInteraction.dropInteraction(performDrop: session)
+    }
+}
 
 public protocol ChatInputTextViewPasteDelegate: class {
     func onImagePasted(image: UIImage)

+ 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)
+}

+ 27 - 0
deltachat-ios/Helper/FileHelper.swift

@@ -56,4 +56,31 @@ public class FileHelper {
             return nil
         }
     }
+
+    public static func deleteFile(atPath: String?) {
+        if Thread.isMainThread {
+            DispatchQueue.global(qos: .background).async {
+                deleteFile(atPath)
+            }
+        } else {
+            deleteFile(atPath)
+        }
+    }
+
+    private static func deleteFile(_ atPath: String?) {
+        guard let atPath = atPath else {
+            return
+        }
+
+        let fileManager = FileManager.default
+        if !fileManager.fileExists(atPath: atPath) {
+            return
+        }
+
+        do {
+            try fileManager.removeItem(atPath: atPath)
+        } catch {
+            print("err: \(error.localizedDescription)")
+        }
+    }
 }

+ 0 - 23
deltachat-ios/Helper/ImageFormat.swift

@@ -106,27 +106,4 @@ extension ImageFormat {
         }
         return nil
     }
-
-    public static func deleteImage(atPath: String) {
-        if Thread.isMainThread {
-            DispatchQueue.global(qos: .background).async {
-                deleteFile(atPath: atPath)
-            }
-        } else {
-            deleteFile(atPath: atPath)
-        }
-    }
-
-    private static func deleteFile(atPath: String) {
-        let fileManager = FileManager.default
-        if !fileManager.fileExists(atPath: atPath) {
-            return
-        }
-
-        do {
-            try fileManager.removeItem(atPath: atPath)
-        } catch {
-            print("err: \(error.localizedDescription)")
-        }
-    }
 }