Kaynağa Gözat

Merge pull request #1023 from deltachat/new_chat_context_menu

New chat context menu
bjoern 4 yıl önce
ebeveyn
işleme
bb01fede10

+ 1 - 1
deltachat-ios.xcodeproj/project.pbxproj

@@ -557,6 +557,7 @@
 			children = (
 				30FDB6B624D193DD0066C48D /* Cells */,
 				30E348DE24F3F819005C93D1 /* ChatTableView.swift */,
+				303492CE2587C2DC00A523D0 /* ChatInputBar.swift */,
 				302E1BB3252B5AB4008F4264 /* PlayButtonView.swift */,
 				30F8817524DA97DA0023780E /* BackgroundContainer.swift */,
 				3008CB7324F9436C00E6A617 /* AudioPlayerView.swift */,
@@ -570,7 +571,6 @@
 				303492AC2577CAC300A523D0 /* FileView.swift */,
 				303492B22577E40700A523D0 /* DocumentPreview.swift */,
 				303492CA257A814200A523D0 /* DraftArea.swift */,
-				303492CE2587C2DC00A523D0 /* ChatInputBar.swift */,
 			);
 			path = Views;
 			sourceTree = "<group>";

+ 26 - 0
deltachat-ios/Assets.xcassets/ic_content_copy_white_36pt.imageset/Contents.json

@@ -0,0 +1,26 @@
+{
+  "images" : [
+    {
+      "filename" : "baseline_content_copy_white_36pt_1x.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "baseline_content_copy_white_36pt_2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "baseline_content_copy_white_36pt_3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "template-rendering-intent" : "template"
+  }
+}

BIN
deltachat-ios/Assets.xcassets/ic_content_copy_white_36pt.imageset/baseline_content_copy_white_36pt_1x.png


BIN
deltachat-ios/Assets.xcassets/ic_content_copy_white_36pt.imageset/baseline_content_copy_white_36pt_2x.png


BIN
deltachat-ios/Assets.xcassets/ic_content_copy_white_36pt.imageset/baseline_content_copy_white_36pt_3x.png


+ 26 - 0
deltachat-ios/Assets.xcassets/ic_forward_white_36pt.imageset/Contents.json

@@ -0,0 +1,26 @@
+{
+  "images" : [
+    {
+      "filename" : "baseline_forward_white_36pt_1x.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "baseline_forward_white_36pt_2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "baseline_forward_white_36pt_3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "template-rendering-intent" : "template"
+  }
+}

BIN
deltachat-ios/Assets.xcassets/ic_forward_white_36pt.imageset/baseline_forward_white_36pt_1x.png


BIN
deltachat-ios/Assets.xcassets/ic_forward_white_36pt.imageset/baseline_forward_white_36pt_2x.png


BIN
deltachat-ios/Assets.xcassets/ic_forward_white_36pt.imageset/baseline_forward_white_36pt_3x.png


+ 3 - 0
deltachat-ios/Assets.xcassets/ic_reply.imageset/Contents.json

@@ -19,5 +19,8 @@
   "info" : {
     "author" : "xcode",
     "version" : 1
+  },
+  "properties" : {
+    "template-rendering-intent" : "template"
   }
 }

+ 112 - 54
deltachat-ios/Chat/ChatViewController.swift

@@ -93,6 +93,87 @@ class ChatViewController: UITableViewController {
         return UIBarButtonItem(customView: badge)
     }()
 
+    private lazy var contextMenu: ContextMenuProvider = {
+        let copyItem = ContextMenuProvider.ContextMenuItem(
+        title: String.localized("global_menu_edit_copy_desktop"),
+        imageName: "ic_content_copy_white_36pt",
+        action: #selector(BaseMessageCell.messageCopy),
+        onPerform: { [weak self] indexPath in
+                guard let self = self else { return }
+                let id = self.messageIds[indexPath.row]
+                let msg = DcMsg(id: id)
+
+                let pasteboard = UIPasteboard.general
+                if msg.type == DC_MSG_TEXT {
+                    pasteboard.string = msg.text
+                } else {
+                    pasteboard.string = msg.summary(chars: 10000000)
+                }
+            }
+        )
+
+        let infoItem = ContextMenuProvider.ContextMenuItem(
+            title: String.localized("info"),
+            imageName: "info",
+            action: #selector(BaseMessageCell.messageInfo),
+            onPerform: { [weak self] indexPath in
+                guard let self = self else { return }
+                let msg = DcMsg(id: self.messageIds[indexPath.row])
+                let msgViewController = MessageInfoViewController(dcContext: self.dcContext, message: msg)
+                if let ctrl = self.navigationController {
+                    ctrl.pushViewController(msgViewController, animated: true)
+                }
+            }
+        )
+
+        let deleteItem = ContextMenuProvider.ContextMenuItem(
+            title: String.localized("delete"),
+            imageName: "trash",
+            isDestructive: true,
+            action: #selector(BaseMessageCell.messageDelete),
+            onPerform: { [weak self] indexPath in
+                DispatchQueue.main.async { [weak self] in
+                    guard let self = self else { return }
+                    self.tableView.becomeFirstResponder()
+                    let msg = DcMsg(id: self.messageIds[indexPath.row])
+                    self.askToDeleteMessage(id: msg.id)
+                }
+            }
+        )
+
+        let forwardItem = ContextMenuProvider.ContextMenuItem(
+            title: String.localized("forward"),
+            imageName: "ic_forward_white_36pt",
+            action: #selector(BaseMessageCell.messageForward),
+            onPerform: { [weak self] indexPath in
+                guard let self = self else { return }
+                let msg = DcMsg(id: self.messageIds[indexPath.row])
+                RelayHelper.sharedInstance.setForwardMessage(messageId: msg.id)
+                self.navigationController?.popViewController(animated: true)
+            }
+        )
+
+        let replyItem = ContextMenuProvider.ContextMenuItem(
+            title: String.localized("notify_reply_button"),
+            imageName: "ic_reply",
+            action: #selector(BaseMessageCell.messageReply),
+            onPerform: { indexPath in
+                DispatchQueue.main.async { [weak self] in
+                    self?.replyToMessage(at: indexPath)
+                }
+            }
+        )
+
+        let config = ContextMenuProvider()
+        if #available(iOS 13.0, *), !disableWriting {
+            config.setMenu([replyItem, forwardItem, infoItem, copyItem, deleteItem])
+        } else {
+            config.setMenu([forwardItem, infoItem, copyItem, deleteItem])
+        }
+
+        return config
+    }()
+
     /// The `BasicAudioController` controll the AVAudioPlayer state (play, pause, stop) and update audio cell UI accordingly.
     private lazy var audioController = AudioController(dcContext: dcContext, chatId: chatId)
 
@@ -164,7 +245,6 @@ class ChatViewController: UITableViewController {
                                        name: UIApplication.willResignActiveNotification,
                                        object: nil)
         notificationCenter.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
-        prepareContextMenu()
     }
 
     @objc func keyboardWillShow(_ notification: Notification) {
@@ -220,6 +300,7 @@ class ChatViewController: UITableViewController {
             askToForwardMessage()
         }
 
+        prepareContextMenu()
     }
 
     override func viewDidAppear(_ animated: Bool) {
@@ -408,11 +489,8 @@ class ChatViewController: UITableViewController {
         }
 
         let action = UIContextualAction(style: .normal, title: nil,
-                                        handler: { (_, _, completionHandler) in
-                                            let message = DcMsg(id: self.messageIds[indexPath.row])
-                                            self.draft.setQuote(quotedMsg: message)
-                                            self.configureDraftArea(draft: self.draft)
-                                            self.messageInputBar.inputTextView.becomeFirstResponder()
+                                        handler: { [weak self] (_, _, completionHandler) in
+                                            self?.replyToMessage(at: indexPath)
                                             completionHandler(true)
                                         })
         if #available(iOS 12.0, *) {
@@ -428,6 +506,13 @@ class ChatViewController: UITableViewController {
         return configuration
     }
 
+    func replyToMessage(at indexPath: IndexPath) {
+        let message = DcMsg(id: self.messageIds[indexPath.row])
+        self.draft.setQuote(quotedMsg: message)
+        self.configureDraftArea(draft: self.draft)
+        self.messageInputBar.inputTextView.becomeFirstResponder()
+    }
+
     func markSeenMessagesInVisibleArea() {
         if let indexPaths = tableView.indexPathsForVisibleRows {
             let visibleMessagesIds = indexPaths.map { UInt32(messageIds[$0.row]) }
@@ -512,20 +597,15 @@ class ChatViewController: UITableViewController {
         navigationItem.rightBarButtonItems = rightBarButtonItems
     }
 
-    // TODO: is the delay of one second needed?
     @objc
     private func refreshMessages() {
-        DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) {
-            DispatchQueue.main.async { [weak self] in
-                guard let self = self else { return }
-                self.messageIds = self.getMessageIds()
-                self.tableView.reloadData()
-                if self.isLastRowVisible() {
-                    self.scrollToBottom(animated: true)
-                }
-                self.showEmptyStateView(self.messageIds.isEmpty)
-            }
+        self.messageIds = self.getMessageIds()
+        let wasLastSectionVisible = self.isLastRowVisible()
+        self.tableView.reloadData()
+        if wasLastSectionVisible {
+            self.scrollToBottom(animated: true)
         }
+        self.showEmptyStateView(self.messageIds.isEmpty)
     }
 
     private func loadMessages() {
@@ -761,7 +841,7 @@ class ChatViewController: UITableViewController {
         confirmationAlert(title: title, actionTitle: String.localized("delete"), actionStyle: .destructive,
                           actionHandler: { _ in
                             self.dcContext.deleteMessage(msgId: id)
-                            self.dismiss(animated: true, completion: nil)})
+                          })
     }
 
     private func askToForwardMessage() {
@@ -994,11 +1074,7 @@ class ChatViewController: UITableViewController {
 
     // MARK: - Context menu
     private func prepareContextMenu() {
-        UIMenuController.shared.menuItems = [
-            UIMenuItem(title: String.localized("info"), action: #selector(BaseMessageCell.messageInfo)),
-            UIMenuItem(title: String.localized("delete"), action: #selector(BaseMessageCell.messageDelete)),
-            UIMenuItem(title: String.localized("forward"), action: #selector(BaseMessageCell.messageForward))
-        ]
+        UIMenuController.shared.menuItems = contextMenu.menuItems
         UIMenuController.shared.update()
     }
 
@@ -1007,42 +1083,24 @@ class ChatViewController: UITableViewController {
     }
 
     override func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
-        return action == #selector(UIResponderStandardEditActions.copy(_:))
-            || action == #selector(BaseMessageCell.messageInfo)
-            || action == #selector(BaseMessageCell.messageDelete)
-            || action == #selector(BaseMessageCell.messageForward)
+        return contextMenu.canPerformAction(action: action)
     }
 
     override func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) {
         // handle standard actions here, but custom actions never trigger this. it still needs to be present for the menu to display, though.
-        switch action {
-        case #selector(copy(_:)):
-            let id = messageIds[indexPath.row]
-            let msg = DcMsg(id: id)
-
-            let pasteboard = UIPasteboard.general
-            if msg.type == DC_MSG_TEXT {
-                pasteboard.string = msg.text
-            } else {
-                pasteboard.string = msg.summary(chars: 10000000)
-            }
-        case #selector(BaseMessageCell.messageInfo(_:)):
-            let msg = DcMsg(id: messageIds[indexPath.row])
-            let msgViewController = MessageInfoViewController(dcContext: dcContext, message: msg)
-            if let ctrl = navigationController {
-                ctrl.pushViewController(msgViewController, animated: true)
+        contextMenu.performAction(action: action, indexPath: indexPath)
+    }
+
+    // context menu for iOS 13+
+    @available(iOS 13, *)
+    override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
+        return UIContextMenuConfiguration(
+            identifier: nil,
+            previewProvider: nil,
+            actionProvider: { [weak self] _ in
+                self?.contextMenu.actionProvider(indexPath: indexPath)
             }
-        case #selector(BaseMessageCell.messageDelete(_:)):
-            let msg = DcMsg(id: messageIds[indexPath.row])
-            askToDeleteMessage(id: msg.id)
-
-        case #selector(BaseMessageCell.messageForward(_:)):
-            let msg = DcMsg(id: messageIds[indexPath.row])
-            RelayHelper.sharedInstance.setForwardMessage(messageId: msg.id)
-            navigationController?.popViewController(animated: true)
-        default:
-            break
-        }
+        )
     }
 
     func showMediaGalleryFor(indexPath: IndexPath) {

+ 8 - 0
deltachat-ios/Chat/Views/Cells/BaseMessageCell.swift

@@ -409,6 +409,14 @@ public class BaseMessageCell: UITableViewCell {
         self.performAction(#selector(BaseMessageCell.messageForward(_:)), with: sender)
     }
 
+    @objc func messageReply(_ sender: Any?) {
+        self.performAction(#selector(BaseMessageCell.messageReply(_:)), with: sender)
+    }
+
+    @objc func messageCopy(_ sender: Any?) {
+        self.performAction(#selector(BaseMessageCell.messageCopy(_:)), with: sender)
+    }
+
     func performAction(_ action: Selector, with sender: Any?) {
         if let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
             // Trigger action in tableView delegate (UITableViewController)

+ 1 - 0
deltachat-ios/Chat/Views/DraftArea.swift

@@ -63,6 +63,7 @@ public class DraftArea: UIView, InputItem {
     public func setupSubviews() {
         addSubview(mainContentView)
         mainContentView.fillSuperview()
+        mainContentView.backgroundColor = DcColors.chatBackgroundColor
     }
 
     public func configure(draft: DraftModel) {

+ 78 - 0
deltachat-ios/Controller/ContextMenuController.swift

@@ -117,3 +117,81 @@ class ContextMenuController: UIViewController {
         self.preferredContentSize = CGSize(width: width, height: height)
     }
 }
+
+class ContextMenuProvider {
+
+    var menu: [ContextMenuItem] = []
+
+    init(menu: [ContextMenuItem] = []) {
+        self.menu = menu
+    }
+
+    func setMenu(_ menu: [ContextMenuItem]) {
+        self.menu = menu
+    }
+
+    // iOS 12- action menu
+    var menuItems: [UIMenuItem] {
+        return menu.map { UIMenuItem(title: $0.title, action: $0.action) }
+    }
+
+    // iOS13+ action menu
+    @available(iOS 13, *)
+    func actionProvider(title: String = "", image: UIImage? = nil, identifier: UIMenu.Identifier? = nil, indexPath: IndexPath) -> UIMenu {
+
+        var children: [UIMenuElement] = []
+
+        for item in menu {
+            let image = UIImage(systemName: item.imageName) ??
+                UIImage(named: item.imageName)
+
+            let action = UIAction(
+                title: item.title,
+                image: image,
+                handler: { _ in item.onPerform?(indexPath) }
+            )
+            if item.isDestructive {
+                action.attributes = [.destructive]
+            }
+            children.append(action)
+        }
+
+        return UIMenu(
+            title: title,
+            image: image,
+            identifier: identifier,
+            children: children
+        )
+    }
+
+    func canPerformAction(action: Selector) -> Bool {
+        return !menu.filter {
+            $0.action == action
+        }.isEmpty
+    }
+
+    func performAction(action: Selector, indexPath: IndexPath) {
+        menu.filter {
+            $0.action == action
+        }.first?.onPerform?(indexPath)
+    }
+
+}
+
+extension ContextMenuProvider {
+    struct ContextMenuItem {
+        var title: String
+        var imageName: String
+        let isDestructive: Bool
+        var action: Selector
+        var onPerform: ((IndexPath) -> Void)?
+
+        init(title: String, imageName: String, isDestructive: Bool = false, action: Selector, onPerform: ((IndexPath) -> Void)?) {
+            self.title = title
+            self.imageName = imageName
+            self.isDestructive = isDestructive
+            self.action = action
+            self.onPerform = onPerform
+        }
+    }
+}

+ 4 - 5
deltachat-ios/Controller/DocumentGalleryController.swift

@@ -23,10 +23,10 @@ class DocumentGalleryController: UIViewController {
     }()
 
     private lazy var contextMenu: ContextMenuProvider = {
-        let deleteItem = ContextMenuProvider.ContextMenuItem(
+        let deleteItem = ContextMenuProvider.ContextMenuItem.init(
             title: String.localized("delete"),
-            imageNames: ("trash", nil),
-            option: .delete,
+            imageName: "trash",
+            isDestructive: true,
             action: #selector(DocumentGalleryFileCell.itemDelete(_:)),
             onPerform: { [weak self] indexPath in
                 self?.askToDeleteItem(at: indexPath)
@@ -34,8 +34,7 @@ class DocumentGalleryController: UIViewController {
         )
         let showInChatItem = ContextMenuProvider.ContextMenuItem(
             title: String.localized("show_in_chat"),
-            imageNames: ("doc.text.magnifyingglass", nil),
-            option: .showInChat,
+            imageName: "doc.text.magnifyingglass",
             action: #selector(DocumentGalleryFileCell.showInChat(_:)),
             onPerform: { [weak self] indexPath in
                 self?.redirectToMessage(of: indexPath)

+ 4 - 88
deltachat-ios/Controller/GalleryViewController.swift

@@ -2,7 +2,7 @@ import UIKit
 import DcCore
 import QuickLook
 
-class GalleryViewController: UIViewController {
+class GalleryViewController: UIViewController, QLPreviewControllerDelegate {
 
     private let dcContext: DcContext
     // MARK: - data
@@ -50,8 +50,8 @@ class GalleryViewController: UIViewController {
     private lazy var contextMenu: ContextMenuProvider = {
         let deleteItem = ContextMenuProvider.ContextMenuItem(
             title: String.localized("delete"),
-            imageNames: ("trash", nil),
-            option: .delete,
+            imageName: "trash",
+            isDestructive: true,
             action: #selector(GalleryCell.itemDelete(_:)),
             onPerform: { [weak self] indexPath in
                 self?.askToDeleteItem(at: indexPath)
@@ -59,8 +59,7 @@ class GalleryViewController: UIViewController {
         )
         let showInChatItem = ContextMenuProvider.ContextMenuItem(
             title: String.localized("show_in_chat"),
-            imageNames: ("doc.text.magnifyingglass", nil),
-            option: .showInChat,
+            imageName: "doc.text.magnifyingglass",
             action: #selector(GalleryCell.showInChat(_:)),
             onPerform: { [weak self] indexPath in
                 self?.redirectToMessage(of: indexPath)
@@ -319,86 +318,3 @@ private extension GalleryViewController {
         chatViewController.scrollToMessage(msgId: msgId)
     }
 }
-
-class ContextMenuProvider {
-
-    var menu: [ContextMenuItem] = []
-
-    init(menu: [ContextMenuItem] = []) {
-        self.menu = menu
-    }
-
-    func setMenu(_ menu: [ContextMenuItem]) {
-        self.menu = menu
-    }
-
-    // iOS 12- action menu
-    var menuItems: [UIMenuItem] {
-        return menu.map { UIMenuItem(title: $0.title, action: $0.action) }
-    }
-
-    // iOS13+ action menu
-    @available(iOS 13, *)
-    func actionProvider(title: String = "", image: UIImage? = nil, identifier: UIMenu.Identifier? = nil, indexPath: IndexPath) -> UIMenu {
-
-        var children: [UIMenuElement] = []
-
-        for item in menu {
-            // some system images are not available in iOS 13
-            let image = UIImage(systemName: item.imageNames.0) ?? UIImage(systemName: item.imageNames.1 ?? "")
-
-            let action = UIAction(
-                title: item.title,
-                image: image,
-                handler: { _ in item.onPerform?(indexPath) }
-            )
-            if item.option == .delete {
-                action.attributes = [.destructive]
-            }
-            children.append(action)
-        }
-
-        return UIMenu(
-            title: title,
-            image: image,
-            identifier: identifier,
-            children: children
-        )
-    }
-
-    func canPerformAction(action: Selector) -> Bool {
-        return !menu.filter {
-            $0.action == action
-        }.isEmpty
-    }
-
-    func performAction(action: Selector, indexPath: IndexPath) {
-        menu.filter {
-            $0.action == action
-        }.first?.onPerform?(indexPath)
-    }
-
-    enum Option {
-        case showInChat
-        case delete
-    }
-}
-
-extension ContextMenuProvider {
-    typealias ImageSystemName = String
-    struct ContextMenuItem {
-        var title: String
-        var imageNames: (ImageSystemName, ImageSystemName?) // (0,1) -> define 1 as backup if 0 is not available in iOS 13
-        let option: Option
-        var action: Selector
-        var onPerform: ((IndexPath) -> Void)?
-    }
-}
-
-// MARK: - QLPreviewControllerDataSource
-extension GalleryViewController: QLPreviewControllerDelegate {
-    func previewController(_ controller: QLPreviewController, transitionViewFor item: QLPreviewItem) -> UIView? {
-        let indexPath = IndexPath(row: controller.currentPreviewItemIndex, section: 0)
-        return grid.cellForItem(at: indexPath)
-    }
-}