Przeglądaj źródła

implement reply privately (#1612)

* implement reply privately, dynamically filter menu items depending on message properties

* remove superfluous filter call

* adapt reply icons in chat menu

* don't show reply privately on pre iOS 13 devices

* reuse same reply icon from SF symbols for swipe-to-reply

* remove unused white reply icons

* hide 'reply' if one cannot send to a chat, tweak comments

Co-authored-by: B. Petersen <r10s@b44t.com>
cyBerta 3 lat temu
rodzic
commit
7f8a8edcc2

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

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

BIN
deltachat-ios/Assets.xcassets/ic_reply.imageset/baseline_reply_white_36pt_1x.png


BIN
deltachat-ios/Assets.xcassets/ic_reply.imageset/baseline_reply_white_36pt_2x.png


BIN
deltachat-ios/Assets.xcassets/ic_reply.imageset/baseline_reply_white_36pt_3x.png


+ 87 - 31
deltachat-ios/Chat/ChatViewController.swift

@@ -154,11 +154,29 @@ class ChatViewController: UITableViewController {
     }()
 
     private lazy var contextMenu: ContextMenuProvider = {
-        let copyItem = ContextMenuProvider.ContextMenuItem(
-        title: String.localized("global_menu_edit_copy_desktop"),
-        imageName: "doc.on.doc",
-        action: #selector(BaseMessageCell.messageCopy),
-        onPerform: { [weak self] indexPath in
+        let dcChat = dcContext.getChat(chatId: chatId)
+        let config = ContextMenuProvider()
+        if #available(iOS 13.0, *) {
+            if dcChat.canSend {
+                let mainMenu = ContextMenuProvider.ContextMenuItem(submenuitems: [replyItem, replyPrivatelyItem, forwardItem, infoItem, copyItem, deleteItem])
+                config.setMenu([mainMenu, selectMoreItem])
+            } else {
+                config.setMenu([replyPrivatelyItem, forwardItem, infoItem, copyItem, deleteItem])
+            }
+        } else if dcChat.canSend { // skips some options on iOS <13 because of limited horizontal space (reply is still available by swiping)
+            config.setMenu([forwardItem, infoItem, copyItem, deleteItem, selectMoreItem])
+        } else {
+            config.setMenu([forwardItem, infoItem, copyItem, deleteItem])
+        }
+        return config
+    }()
+
+    private lazy var copyItem: ContextMenuProvider.ContextMenuItem = {
+        return ContextMenuProvider.ContextMenuItem(
+            title: String.localized("global_menu_edit_copy_desktop"),
+            imageName: "doc.on.doc",
+            action: #selector(BaseMessageCell.messageCopy),
+            onPerform: { [weak self] indexPath in
                 guard let self = self else { return }
                 let id = self.messageIds[indexPath.row]
                 let msg = self.dcContext.getMessage(id: id)
@@ -171,8 +189,10 @@ class ChatViewController: UITableViewController {
                 }
             }
         )
+    }()
 
-        let infoItem = ContextMenuProvider.ContextMenuItem(
+    private lazy var infoItem: ContextMenuProvider.ContextMenuItem = {
+        return ContextMenuProvider.ContextMenuItem(
             title: String.localized("info"),
             imageName: "info",
             action: #selector(BaseMessageCell.messageInfo),
@@ -185,8 +205,10 @@ class ChatViewController: UITableViewController {
                 }
             }
         )
+    }()
 
-        let deleteItem = ContextMenuProvider.ContextMenuItem(
+    private lazy var deleteItem: ContextMenuProvider.ContextMenuItem = {
+        return ContextMenuProvider.ContextMenuItem(
             title: String.localized("delete"),
             imageName: "trash",
             isDestructive: true,
@@ -200,8 +222,10 @@ class ChatViewController: UITableViewController {
                 }
             }
         )
+    }()
 
-        let forwardItem = ContextMenuProvider.ContextMenuItem(
+    private lazy var forwardItem: ContextMenuProvider.ContextMenuItem = {
+        return ContextMenuProvider.ContextMenuItem(
             title: String.localized("forward"),
             imageName: "ic_forward_white_36pt",
             action: #selector(BaseMessageCell.messageForward),
@@ -212,10 +236,12 @@ class ChatViewController: UITableViewController {
                 self.navigationController?.popViewController(animated: true)
             }
         )
+    }()
 
-        let replyItem = ContextMenuProvider.ContextMenuItem(
+    private lazy var replyItem: ContextMenuProvider.ContextMenuItem = {
+        return ContextMenuProvider.ContextMenuItem(
             title: String.localized("notify_reply_button"),
-            imageName: "ic_reply",
+            imageName: "arrowshape.turn.up.left.fill",
             action: #selector(BaseMessageCell.messageReply),
             onPerform: { indexPath in
                 DispatchQueue.main.async { [weak self] in
@@ -223,8 +249,22 @@ class ChatViewController: UITableViewController {
                 }
             }
         )
+    }()
+
+    private lazy var replyPrivatelyItem: ContextMenuProvider.ContextMenuItem = {
+        return ContextMenuProvider.ContextMenuItem(
+            title: String.localized("reply_privately"),
+            imageName: "arrowshape.turn.up.left",
+            action: #selector(BaseMessageCell.messageReplyPrivately),
+            onPerform: { [weak self] indexPath in
+                guard let self = self else { return }
+                self.replyPrivatelyToMessage(at: indexPath)
+            }
+        )
+    }()
 
-        let selectMoreItem = ContextMenuProvider.ContextMenuItem(
+    private lazy var selectMoreItem: ContextMenuProvider.ContextMenuItem = {
+        return ContextMenuProvider.ContextMenuItem(
             title: String.localized("select_more"),
             imageName: "checkmark.circle",
             action: #selector(BaseMessageCell.messageSelectMore),
@@ -239,19 +279,6 @@ class ChatViewController: UITableViewController {
                 }
             }
         )
-
-        let dcChat = dcContext.getChat(chatId: chatId)
-        let config = ContextMenuProvider()
-        if #available(iOS 13.0, *), dcChat.canSend {
-            let mainContextMenu = ContextMenuProvider.ContextMenuItem(submenuitems: [replyItem, forwardItem, infoItem, copyItem, deleteItem])
-            config.setMenu([mainContextMenu, selectMoreItem])
-        } else if dcChat.canSend {
-            config.setMenu([forwardItem, infoItem, copyItem, deleteItem, selectMoreItem])
-        } else {
-            config.setMenu([forwardItem, infoItem, copyItem, deleteItem])
-        }
-
-        return config
     }()
 
     /// The `BasicAudioController` controll the AVAudioPlayer state (play, pause, stop) and update audio cell UI accordingly.
@@ -450,8 +477,6 @@ class ChatViewController: UITableViewController {
             messageInputBar.inputTextView.text = RelayHelper.shared.mailtoDraft
             RelayHelper.shared.finishMailto()
         }
-
-        prepareContextMenu()
     }
 
     override func viewDidAppear(_ animated: Bool) {
@@ -834,7 +859,7 @@ class ChatViewController: UITableViewController {
                                             completionHandler(true)
                                         })
         if #available(iOS 13.0, *) {
-            action.image = UIImage(named: traitCollection.userInterfaceStyle == .light ? "ic_reply_black" : "ic_reply")
+            action.image = UIImage(systemName: "arrowshape.turn.up.left.fill")?.sd_tintedImage(with: DcColors.defaultInverseColor)
             action.backgroundColor = DcColors.chatBackgroundColor.withAlphaComponent(0.25)
         } else {
             action.image = UIImage(named: "ic_reply_black")
@@ -854,6 +879,16 @@ class ChatViewController: UITableViewController {
         focusInputTextView()
     }
 
+    func replyPrivatelyToMessage(at indexPath: IndexPath) {
+        let msgId = self.messageIds[indexPath.row]
+        let message = dcContext.getMessage(id: msgId)
+        let privateChatId = dcContext.createChatByContactId(contactId: message.fromContactId)
+        let replyMsg: DcMsg = dcContext.newMessage(viewType: DC_MSG_TEXT)
+        replyMsg.quoteMessage = message
+        dcContext.setDraft(chatId: privateChatId, message: replyMsg)
+        showChat(chatId: privateChatId)
+    }
+
     func markSeenMessagesInVisibleArea() {
         if isVisibleToUser,
            let indexPaths = tableView.indexPathsForVisibleRows {
@@ -1673,14 +1708,27 @@ class ChatViewController: UITableViewController {
     }
 
     // MARK: - Context menu
-    private func prepareContextMenu() {
-        UIMenuController.shared.menuItems = contextMenu.menuItems
+    private func prepareContextMenu(for message: DcMsg, isHidden: Bool) {
+        if #available(iOS 13.0, *) {
+            return
+        }
+
+        if isHidden {
+            UIMenuController.shared.menuItems = nil
+        } else if isGroupChat && !message.isFromCurrentSender {
+            UIMenuController.shared.menuItems = contextMenu.menuItems
+        } else {
+            UIMenuController.shared.menuItems = contextMenu.getMenuItems(filters: [ { $0.action != self.replyPrivatelyItem.action } ])
+        }
         UIMenuController.shared.update()
     }
 
     override func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
         let messageId = messageIds[indexPath.row]
-        return !(dcContext.getMessage(id: messageId).isInfo || messageId == DC_MSG_ID_MARKER1 || messageId == DC_MSG_ID_DAYMARKER)
+        let message = dcContext.getMessage(id: messageId)
+        let isHidden = message.isInfo || messageId == DC_MSG_ID_MARKER1 || messageId == DC_MSG_ID_DAYMARKER
+        prepareContextMenu(for: message, isHidden: isHidden)
+        return !isHidden
     }
 
     override func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
@@ -1727,7 +1775,15 @@ class ChatViewController: UITableViewController {
             identifier: NSString(string: "\(messageId)"),
             previewProvider: nil,
             actionProvider: { [weak self] _ in
-                self?.contextMenu.actionProvider(indexPath: indexPath)
+                guard let self = self else {
+                    return nil
+                }
+                if self.isGroupChat && !self.dcContext.getMessage(id: messageId).isFromCurrentSender {
+                    return self.contextMenu.actionProvider(indexPath: indexPath)
+                } else {
+                    return self.contextMenu.actionProvider(indexPath: indexPath,
+                                                           filters: [ { $0.action != self.replyPrivatelyItem.action } ])
+                }
             }
         )
     }

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

@@ -542,6 +542,10 @@ public class BaseMessageCell: UITableViewCell {
         self.performAction(#selector(BaseMessageCell.messageReply(_:)), with: sender)
     }
 
+    @objc func messageReplyPrivately(_ sender: Any?) {
+        self.performAction(#selector(BaseMessageCell.messageReplyPrivately(_:)), with: sender)
+    }
+
     @objc func messageCopy(_ sender: Any?) {
         self.performAction(#selector(BaseMessageCell.messageCopy(_:)), with: sender)
     }

+ 31 - 4
deltachat-ios/Controller/ContextMenuController.swift

@@ -137,15 +137,42 @@ class ContextMenuProvider {
             .map({ return UIMenuItem(title: $0.title!, action: $0.action!) })
     }
 
+    private func filter(_ filters: [(Array<ContextMenuItem>.Element) throws -> Bool]?, in items: [ContextMenuItem]) -> [ContextMenuItem] {
+        guard let filters = filters else {
+            return items
+        }
+
+        var items = items
+        for filter in filters {
+            do {
+                items = try items.filter(filter)
+            } catch {
+                logger.warning("applied context menu item filter is invalid")
+            }
+        }
+        return items
+    }
+
+    public func getMenuItems(filters: [(Array<ContextMenuItem>.Element) throws -> Bool]) -> [UIMenuItem] {
+        return filter(filters, in: menu)
+            .filter({ $0.title != nil && $0.action != nil })
+            .map({ return 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 {
+    func actionProvider(title: String = "",
+                        image: UIImage? = nil,
+                        identifier: UIMenu.Identifier? = nil,
+                        indexPath: IndexPath,
+                        filters: [(Array<ContextMenuItem>.Element) throws -> Bool]? = nil) -> UIMenu {
 
         var children: [UIMenuElement] = []
-
-        for item in menu {
+        let menuItems = filter(filters, in: menu)
+        for item in menuItems {
             // we only support 1 submenu layer for now
-            if let subMenus = item.children {
+            if var subMenus = item.children {
+                subMenus = filter(filters, in: subMenus)
                 var submenuChildren: [UIMenuElement] = []
                 for submenuItem in subMenus {
                     submenuChildren.append(generateUIAction(item: submenuItem, indexPath: indexPath))