Browse Source

Merge pull request #985 from deltachat/delete-galler-item-2

Gallery - context menu - 2nd Attempt
cyBerta 4 years ago
parent
commit
dd679656f2

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

@@ -109,6 +109,8 @@
 		AE4AEE3522B1030D000AA495 /* PreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE4AEE3422B1030D000AA495 /* PreviewController.swift */; };
 		AE4AEE3522B1030D000AA495 /* PreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE4AEE3422B1030D000AA495 /* PreviewController.swift */; };
 		AE52EA19229EB53C00C586C9 /* ContactDetailHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE52EA18229EB53C00C586C9 /* ContactDetailHeader.swift */; };
 		AE52EA19229EB53C00C586C9 /* ContactDetailHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE52EA18229EB53C00C586C9 /* ContactDetailHeader.swift */; };
 		AE52EA20229EB9F000C586C9 /* EditGroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE52EA1F229EB9F000C586C9 /* EditGroupViewController.swift */; };
 		AE52EA20229EB9F000C586C9 /* EditGroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE52EA1F229EB9F000C586C9 /* EditGroupViewController.swift */; };
+		AE57C0802552BBD0003CFE70 /* GalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE57C07F2552BBD0003CFE70 /* GalleryItem.swift */; };
+		AE57C084255310BB003CFE70 /* ContextMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE57C083255310BB003CFE70 /* ContextMenuController.swift */; };
 		AE6EC5242497663200A400E4 /* UIImageView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE6EC5232497663200A400E4 /* UIImageView+Extensions.swift */; };
 		AE6EC5242497663200A400E4 /* UIImageView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE6EC5232497663200A400E4 /* UIImageView+Extensions.swift */; };
 		AE6EC5282497B9B200A400E4 /* ThumbnailCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE6EC5272497B9B200A400E4 /* ThumbnailCache.swift */; };
 		AE6EC5282497B9B200A400E4 /* ThumbnailCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE6EC5272497B9B200A400E4 /* ThumbnailCache.swift */; };
 		AE728F15229D5C390047565B /* PhotoPickerAlertAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE728F14229D5C390047565B /* PhotoPickerAlertAction.swift */; };
 		AE728F15229D5C390047565B /* PhotoPickerAlertAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE728F14229D5C390047565B /* PhotoPickerAlertAction.swift */; };
@@ -351,6 +353,8 @@
 		AE4AEE3422B1030D000AA495 /* PreviewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewController.swift; sourceTree = "<group>"; };
 		AE4AEE3422B1030D000AA495 /* PreviewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewController.swift; sourceTree = "<group>"; };
 		AE52EA18229EB53C00C586C9 /* ContactDetailHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactDetailHeader.swift; sourceTree = "<group>"; };
 		AE52EA18229EB53C00C586C9 /* ContactDetailHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactDetailHeader.swift; sourceTree = "<group>"; };
 		AE52EA1F229EB9F000C586C9 /* EditGroupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditGroupViewController.swift; sourceTree = "<group>"; };
 		AE52EA1F229EB9F000C586C9 /* EditGroupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditGroupViewController.swift; sourceTree = "<group>"; };
+		AE57C07F2552BBD0003CFE70 /* GalleryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryItem.swift; sourceTree = "<group>"; };
+		AE57C083255310BB003CFE70 /* ContextMenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuController.swift; sourceTree = "<group>"; };
 		AE6EC5232497663200A400E4 /* UIImageView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Extensions.swift"; sourceTree = "<group>"; };
 		AE6EC5232497663200A400E4 /* UIImageView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Extensions.swift"; sourceTree = "<group>"; };
 		AE6EC5272497B9B200A400E4 /* ThumbnailCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailCache.swift; sourceTree = "<group>"; };
 		AE6EC5272497B9B200A400E4 /* ThumbnailCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailCache.swift; sourceTree = "<group>"; };
 		AE728F14229D5C390047565B /* PhotoPickerAlertAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerAlertAction.swift; sourceTree = "<group>"; };
 		AE728F14229D5C390047565B /* PhotoPickerAlertAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerAlertAction.swift; sourceTree = "<group>"; };
@@ -614,6 +618,7 @@
 		7A9FB1421FB061E2001FEA36 /* deltachat-ios */ = {
 		7A9FB1421FB061E2001FEA36 /* deltachat-ios */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				AE57C07E2552BBC0003CFE70 /* Model */,
 				304219D7244072E600516852 /* DC */,
 				304219D7244072E600516852 /* DC */,
 				AE1988AA23EB3C7600B4CD5F /* Assets */,
 				AE1988AA23EB3C7600B4CD5F /* Assets */,
 				AE19887623EB2BDA00B4CD5F /* Assets */,
 				AE19887623EB2BDA00B4CD5F /* Assets */,
@@ -693,6 +698,14 @@
 			path = Cell;
 			path = Cell;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
+		AE57C07E2552BBC0003CFE70 /* Model */ = {
+			isa = PBXGroup;
+			children = (
+				AE57C07F2552BBD0003CFE70 /* GalleryItem.swift */,
+			);
+			path = Model;
+			sourceTree = "<group>";
+		};
 		AE77838B23E32EAA0093EABD /* ViewModel */ = {
 		AE77838B23E32EAA0093EABD /* ViewModel */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -747,6 +760,7 @@
 				AED423D2249F578B00B6B2BB /* AddGroupMembersViewController.swift */,
 				AED423D2249F578B00B6B2BB /* AddGroupMembersViewController.swift */,
 				AED423D6249F580700B6B2BB /* BlockedContactsViewController.swift */,
 				AED423D6249F580700B6B2BB /* BlockedContactsViewController.swift */,
 				AE39D322249CFC1A007346A1 /* DocumentGalleryController.swift */,
 				AE39D322249CFC1A007346A1 /* DocumentGalleryController.swift */,
+				AE57C083255310BB003CFE70 /* ContextMenuController.swift */,
 			);
 			);
 			path = Controller;
 			path = Controller;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -1184,6 +1198,7 @@
 				AE9DAF0F22C278C6004C9591 /* ChatTitleView.swift in Sources */,
 				AE9DAF0F22C278C6004C9591 /* ChatTitleView.swift in Sources */,
 				AE4AEE3522B1030D000AA495 /* PreviewController.swift in Sources */,
 				AE4AEE3522B1030D000AA495 /* PreviewController.swift in Sources */,
 				7070FB9B2101ECBB000DC258 /* NewGroupController.swift in Sources */,
 				7070FB9B2101ECBB000DC258 /* NewGroupController.swift in Sources */,
+				AE57C084255310BB003CFE70 /* ContextMenuController.swift in Sources */,
 				304219D92440734A00516852 /* DcMsg+Extension.swift in Sources */,
 				304219D92440734A00516852 /* DcMsg+Extension.swift in Sources */,
 				303492CF2587C2DC00A523D0 /* ChatInputBar.swift in Sources */,
 				303492CF2587C2DC00A523D0 /* ChatInputBar.swift in Sources */,
 				AE52EA19229EB53C00C586C9 /* ContactDetailHeader.swift in Sources */,
 				AE52EA19229EB53C00C586C9 /* ContactDetailHeader.swift in Sources */,
@@ -1206,6 +1221,7 @@
 				AE77838F23E4276D0093EABD /* ContactCellViewModel.swift in Sources */,
 				AE77838F23E4276D0093EABD /* ContactCellViewModel.swift in Sources */,
 				B20462E62440C99600367A57 /* SettingsAutodelSetController.swift in Sources */,
 				B20462E62440C99600367A57 /* SettingsAutodelSetController.swift in Sources */,
 				3015634423A003BA00E9DEF4 /* AudioRecorderController.swift in Sources */,
 				3015634423A003BA00E9DEF4 /* AudioRecorderController.swift in Sources */,
+				AE57C0802552BBD0003CFE70 /* GalleryItem.swift in Sources */,
 				AE25F09022807AD800CDEA66 /* AvatarSelectionCell.swift in Sources */,
 				AE25F09022807AD800CDEA66 /* AvatarSelectionCell.swift in Sources */,
 				30A4149724F6EFBE00EC91EB /* InfoMessageCell.swift in Sources */,
 				30A4149724F6EFBE00EC91EB /* InfoMessageCell.swift in Sources */,
 				302B84C6239676F0001C261F /* AvatarHelper.swift in Sources */,
 				302B84C6239676F0001C261F /* AvatarHelper.swift in Sources */,

+ 10 - 4
deltachat-ios/Chat/ChatViewController.swift

@@ -566,6 +566,14 @@ class ChatViewController: UITableViewController {
         }
         }
     }
     }
 
 
+    func scrollToMessage(msgId: Int, animated: Bool = true) {
+        guard let index = messageIds.firstIndex(of: msgId) else {
+            return
+        }
+        let indexPath = IndexPath(row: index, section: 0)
+        tableView.scrollToRow(at: indexPath, at: .top, animated: animated)
+    }
+
     private func showEmptyStateView(_ show: Bool) {
     private func showEmptyStateView(_ show: Bool) {
         if show {
         if show {
             let dcChat = dcContext.getChat(chatId: chatId)
             let dcChat = dcContext.getChat(chatId: chatId)
@@ -1108,10 +1116,8 @@ extension ChatViewController: BaseMessageCellDelegate {
     @objc func quoteTapped(indexPath: IndexPath) {
     @objc func quoteTapped(indexPath: IndexPath) {
         _ = handleUIMenu()
         _ = handleUIMenu()
         let msg = DcMsg(id: messageIds[indexPath.row])
         let msg = DcMsg(id: messageIds[indexPath.row])
-        if let quoteMsg = msg.quoteMessage,
-           let index = messageIds.firstIndex(of: quoteMsg.id) {
-            let indexPath = IndexPath(row: index, section: 0)
-            tableView.scrollToRow(at: indexPath, at: .top, animated: true)
+        if let quoteMsg = msg.quoteMessage {
+            scrollToMessage(msgId: quoteMsg.id)
         }
         }
     }
     }
 
 

+ 2 - 2
deltachat-ios/Controller/ContactDetailViewController.swift

@@ -390,13 +390,13 @@ class ContactDetailViewController: UITableViewController {
 
 
     private func showDocuments() {
     private func showDocuments() {
         let messageIds: [Int] = viewModel.documentItemMessageIds.reversed()
         let messageIds: [Int] = viewModel.documentItemMessageIds.reversed()
-        let fileGalleryController = DocumentGalleryController(fileMessageIds: messageIds)
+        let fileGalleryController = DocumentGalleryController(context: viewModel.context, fileMessageIds: messageIds)
         navigationController?.pushViewController(fileGalleryController, animated: true)
         navigationController?.pushViewController(fileGalleryController, animated: true)
     }
     }
 
 
     private func showGallery() {
     private func showGallery() {
         let messageIds: [Int] = viewModel.galleryItemMessageIds.reversed()
         let messageIds: [Int] = viewModel.galleryItemMessageIds.reversed()
-        let galleryController = GalleryViewController(mediaMessageIds: messageIds)
+        let galleryController = GalleryViewController(context: viewModel.context, mediaMessageIds: messageIds)
         navigationController?.pushViewController(galleryController, animated: true)
         navigationController?.pushViewController(galleryController, animated: true)
     }
     }
 
 

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

@@ -0,0 +1,119 @@
+import AVKit
+import AVFoundation
+import SDWebImage
+import DcCore
+
+
+protocol ContextMenuItem {
+    var msg: DcMsg { get set }
+    var thumbnailImage: UIImage? { get set  }
+}
+
+// MARK: - ContextMenuController
+class ContextMenuController: UIViewController {
+
+    let item: ContextMenuItem
+
+    var msg: DcMsg {
+        return item.msg
+    }
+
+    init(item: ContextMenuItem) {
+        self.item = item
+        super.init(nibName: nil, bundle: nil)
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    // MARK: - lifecycle
+    override func viewDidLoad() {
+        super.viewDidLoad()
+
+        let viewType = msg.viewtype
+        var thumbnailView: UIView?
+        switch viewType {
+        case .image:
+            thumbnailView = makeImageView(image: msg.image)
+        case .video:
+            thumbnailView = makeVideoView(videoUrl: msg.fileURL)
+        case .gif:
+            thumbnailView = makeGifView(gifImage: item.thumbnailImage)
+        default:
+            return
+        }
+
+        guard let contentView = thumbnailView else {
+            return
+        }
+
+        view.addSubview(contentView)
+        contentView.translatesAutoresizingMaskIntoConstraints = false
+
+        NSLayoutConstraint.activate([
+            contentView.leftAnchor.constraint(equalTo: view.leftAnchor),
+            contentView.rightAnchor.constraint(equalTo: view.rightAnchor),
+            contentView.topAnchor.constraint(equalTo: view.topAnchor),
+            contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+        ])
+    }
+
+    // MARK: - thumbnailView creation
+    private func makeGifView(gifImage: UIImage?) -> UIView? {
+        let view = SDAnimatedImageView()
+        view.contentMode = .scaleAspectFill
+        view.clipsToBounds = true
+        view.backgroundColor = DcColors.defaultBackgroundColor
+        if let image = gifImage {
+            setPreferredContentSize(for: image)
+        }
+        view.image = gifImage
+        return view
+    }
+
+    private func makeImageView(image: UIImage?) -> UIView? {
+        guard let image = image else {
+            safe_fatalError("unexpected nil value")
+            return nil
+        }
+
+        let imageView = UIImageView()
+        imageView.clipsToBounds = true
+        imageView.contentMode = .scaleAspectFill
+        imageView.image = image
+        setPreferredContentSize(for: image)
+        return imageView
+    }
+
+    private func makeVideoView(videoUrl: URL?) -> UIView? {
+        guard let videoUrl = videoUrl, let videoSize = item.thumbnailImage?.size else { return nil }
+        let player = AVPlayer(url: videoUrl)
+        let playerController = AVPlayerViewController()
+        addChild(playerController)
+        view.addSubview(playerController.view)
+        playerController.didMove(toParent: self)
+        playerController.view.backgroundColor = .darkGray
+        playerController.view.clipsToBounds = true
+        playerController.player = player
+        playerController.showsPlaybackControls = false
+        player.play()
+
+        // truncate edges on top/bottom or sides
+        let resizedHeightFactor = view.frame.height / videoSize.height
+        let resizedWidthFactor = view.frame.width / videoSize.width
+        let effectiveResizeFactor = min(resizedWidthFactor, resizedHeightFactor)
+        let maxHeight = videoSize.height * effectiveResizeFactor
+        let maxWidth = videoSize.width * effectiveResizeFactor
+        let size = CGSize(width: maxWidth, height: maxHeight)
+        preferredContentSize = size
+
+        return playerController.view
+    }
+
+    private func setPreferredContentSize(for image: UIImage) {
+        let width = view.bounds.width
+        let height = image.size.height * (width / image.size.width)
+        self.preferredContentSize = CGSize(width: width, height: height)
+    }
+}

+ 107 - 9
deltachat-ios/Controller/DocumentGalleryController.swift

@@ -3,9 +3,10 @@ import DcCore
 
 
 class DocumentGalleryController: UIViewController {
 class DocumentGalleryController: UIViewController {
 
 
-    private let fileMessageIds: [Int]
+    private var fileMessageIds: [Int]
+    private let dcContext: DcContext
 
 
-    private lazy var tableViews: UITableView = {
+    private lazy var tableView: UITableView = {
         let table = UITableView(frame: .zero, style: .grouped)
         let table = UITableView(frame: .zero, style: .grouped)
         table.register(DocumentGalleryFileCell.self, forCellReuseIdentifier: DocumentGalleryFileCell.reuseIdentifier)
         table.register(DocumentGalleryFileCell.self, forCellReuseIdentifier: DocumentGalleryFileCell.reuseIdentifier)
         table.dataSource = self
         table.dataSource = self
@@ -21,8 +22,33 @@ class DocumentGalleryController: UIViewController {
         return label
         return label
     }()
     }()
 
 
+    private lazy var contextMenu: ContextMenuProvider = {
+        let deleteItem = ContextMenuProvider.ContextMenuItem(
+            title: String.localized("delete"),
+            imageNames: ("trash", nil),
+            option: .delete,
+            action: #selector(DocumentGalleryFileCell.itemDelete(_:)),
+            onPerform: { [weak self] indexPath in
+                self?.askToDeleteItem(at: indexPath)
+            }
+        )
+        let showInChatItem = ContextMenuProvider.ContextMenuItem(
+            title: String.localized("show_in_chat"),
+            imageNames: ("doc.text.magnifyingglass", nil),
+            option: .showInChat,
+            action: #selector(DocumentGalleryFileCell.showInChat(_:)),
+            onPerform: { [weak self] indexPath in
+                self?.redirectToMessage(of: indexPath)
+            }
+        )
 
 
-    init(fileMessageIds: [Int]) {
+        let menu = ContextMenuProvider()
+        menu.setMenu([showInChatItem, deleteItem])
+        return menu
+    }()
+
+    init(context: DcContext, fileMessageIds: [Int]) {
+        self.dcContext = context
         self.fileMessageIds = fileMessageIds
         self.fileMessageIds = fileMessageIds
         super.init(nibName: nil, bundle: nil)
         super.init(nibName: nil, bundle: nil)
         self.title = String.localized("files")
         self.title = String.localized("files")
@@ -41,14 +67,18 @@ class DocumentGalleryController: UIViewController {
         }
         }
     }
     }
 
 
+    override func viewWillAppear(_ animated: Bool) {
+        setupContextMenuIfNeeded()
+    }
+
     // MARK: - layout
     // MARK: - layout
     private func setupSubviews() {
     private func setupSubviews() {
-        view.addSubview(tableViews)
-        tableViews.translatesAutoresizingMaskIntoConstraints = false
-        tableViews.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
-        tableViews.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
-        tableViews.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
-        tableViews.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
+        view.addSubview(tableView)
+        tableView.translatesAutoresizingMaskIntoConstraints = false
+        tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
+        tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
+        tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
+        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
 
 
         view.addSubview(emptyStateView)
         view.addSubview(emptyStateView)
         emptyStateView.translatesAutoresizingMaskIntoConstraints = false
         emptyStateView.translatesAutoresizingMaskIntoConstraints = false
@@ -57,6 +87,30 @@ class DocumentGalleryController: UIViewController {
         emptyStateView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor).isActive = true
         emptyStateView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor).isActive = true
         emptyStateView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
         emptyStateView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
     }
     }
+
+    private func setupContextMenuIfNeeded() {
+        UIMenuController.shared.menuItems = contextMenu.menuItems
+        UIMenuController.shared.update()
+    }
+
+    // MARK: - actions
+    private func askToDeleteItem(at indexPath: IndexPath) {
+        let title = String.localized(stringID: "ask_delete_messages", count: 1)
+        let alertController =  UIAlertController(title: title, message: nil, preferredStyle: .safeActionSheet)
+        let okAction = UIAlertAction(title: String.localized("delete"), style: .destructive, handler: { [weak self] _ in
+            self?.deleteItem(at: indexPath)
+        })
+        let cancelAction = UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil)
+        alertController.addAction(okAction)
+        alertController.addAction(cancelAction)
+        present(alertController, animated: true, completion: nil)
+    }
+
+    private func deleteItem(at indexPath: IndexPath) {
+        let msgId = fileMessageIds.remove(at: indexPath.row)
+        self.dcContext.deleteMessage(msgId: msgId)
+        self.tableView.deleteRows(at: [indexPath], with: .automatic)
+    }
 }
 }
 
 
 // MARK: - UITableViewDelegate, UITableViewDataSource
 // MARK: - UITableViewDelegate, UITableViewDataSource
@@ -80,6 +134,33 @@ extension DocumentGalleryController: UITableViewDelegate, UITableViewDataSource
         showPreview(msgId: msgId)
         showPreview(msgId: msgId)
         tableView.deselectRow(at: indexPath, animated: false)
         tableView.deselectRow(at: indexPath, animated: false)
     }
     }
+
+    // MARK: - context menu
+    // context menu for iOS 11, 12
+    func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
+        return true
+    }
+
+    func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
+        let action = contextMenu.canPerformAction(action: action)
+        return action
+    }
+
+    func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) {
+        contextMenu.performAction(action: action, indexPath: indexPath)
+    }
+
+    // context menu for iOS 13+
+    @available(iOS 13, *)
+    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)
+            }
+        )
+    }
 }
 }
 
 
 // MARK: - coordinator
 // MARK: - coordinator
@@ -91,4 +172,21 @@ extension DocumentGalleryController {
         let previewController = PreviewController(type: .multi(fileMessageIds, index))
         let previewController = PreviewController(type: .multi(fileMessageIds, index))
         present(previewController, animated: true, completion: nil)
         present(previewController, animated: true, completion: nil)
     }
     }
+
+    func redirectToMessage(of indexPath: IndexPath) {
+        let msgId = fileMessageIds[indexPath.row]
+
+        guard
+            let chatViewController = navigationController?.viewControllers.filter ({ $0 is ChatViewController}).first as? ChatViewController,
+            let chatListController = navigationController?.viewControllers.filter({ $0 is ChatListController}).first as? ChatListController
+        else {
+            safe_fatalError("failt to retrieve chatViewController, chatListController in navigation stack")
+            return
+        }
+        self.navigationController?.viewControllers.remove(at: 1)
+
+        self.navigationController?.pushViewController(chatViewController, animated: true)
+        self.navigationController?.setViewControllers([chatListController, chatViewController], animated: false)
+        chatViewController.scrollToMessage(msgId: msgId)
+    }
 }
 }

+ 180 - 66
deltachat-ios/Controller/GalleryViewController.swift

@@ -1,11 +1,11 @@
 import UIKit
 import UIKit
 import DcCore
 import DcCore
-import SDWebImage
 
 
 class GalleryViewController: UIViewController {
 class GalleryViewController: UIViewController {
 
 
+    private let dcContext: DcContext
     // MARK: - data
     // MARK: - data
-    private let mediaMessageIds: [Int]
+    private var mediaMessageIds: [Int]
     private var items: [Int: GalleryItem] = [:]
     private var items: [Int: GalleryItem] = [:]
 
 
     // MARK: - subview specs
     // MARK: - subview specs
@@ -46,7 +46,33 @@ class GalleryViewController: UIViewController {
         return label
         return label
     }()
     }()
 
 
-    init(mediaMessageIds: [Int]) {
+    private lazy var contextMenu: ContextMenuProvider = {
+        let deleteItem = ContextMenuProvider.ContextMenuItem(
+            title: String.localized("delete"),
+            imageNames: ("trash", nil),
+            option: .delete,
+            action: #selector(GalleryCell.itemDelete(_:)),
+            onPerform: { [weak self] indexPath in
+                self?.askToDeleteItem(at: indexPath)
+            }
+        )
+        let showInChatItem = ContextMenuProvider.ContextMenuItem(
+            title: String.localized("show_in_chat"),
+            imageNames: ("doc.text.magnifyingglass", nil),
+            option: .showInChat,
+            action: #selector(GalleryCell.showInChat(_:)),
+            onPerform: { [weak self] indexPath in
+                self?.redirectToMessage(of: indexPath)
+            }
+        )
+
+        let config = ContextMenuProvider()
+        config.setMenu([showInChatItem, deleteItem])
+        return config
+    }()
+
+    init(context: DcContext, mediaMessageIds: [Int]) {
+        self.dcContext = context
         self.mediaMessageIds = mediaMessageIds
         self.mediaMessageIds = mediaMessageIds
         super.init(nibName: nil, bundle: nil)
         super.init(nibName: nil, bundle: nil)
     }
     }
@@ -67,6 +93,7 @@ class GalleryViewController: UIViewController {
 
 
     override func viewWillAppear(_ animated: Bool) {
     override func viewWillAppear(_ animated: Bool) {
         grid.reloadData()
         grid.reloadData()
+        setupContextMenuIfNeeded()
     }
     }
 
 
     override func viewWillLayoutSubviews() {
     override func viewWillLayoutSubviews() {
@@ -78,9 +105,9 @@ class GalleryViewController: UIViewController {
     private func setupSubviews() {
     private func setupSubviews() {
         view.addSubview(grid)
         view.addSubview(grid)
         grid.translatesAutoresizingMaskIntoConstraints = false
         grid.translatesAutoresizingMaskIntoConstraints = false
-        grid.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
+        grid.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
         grid.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
         grid.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
-        grid.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
+        grid.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
         grid.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
         grid.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
 
 
         view.addSubview(timeLabel)
         view.addSubview(timeLabel)
@@ -96,6 +123,11 @@ class GalleryViewController: UIViewController {
         emptyStateView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
         emptyStateView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
     }
     }
 
 
+    private func setupContextMenuIfNeeded() {
+        UIMenuController.shared.menuItems = contextMenu.menuItems
+        UIMenuController.shared.update()
+    }
+
     // MARK: - updates
     // MARK: - updates
     private func updateFloatingTimeLabel() {
     private func updateFloatingTimeLabel() {
         if let indexPath = grid.indexPathsForVisibleItems.min() {
         if let indexPath = grid.indexPathsForVisibleItems.min() {
@@ -104,11 +136,30 @@ class GalleryViewController: UIViewController {
             timeLabel.update(date: msg.sentDate)
             timeLabel.update(date: msg.sentDate)
         }
         }
     }
     }
+
+    // MARK: - actions
+    private func askToDeleteItem(at indexPath: IndexPath) {
+        let title = String.localized(stringID: "ask_delete_messages", count: 1)
+        let alertController =  UIAlertController(title: title, message: nil, preferredStyle: .safeActionSheet)
+        let okAction = UIAlertAction(title: String.localized("delete"), style: .destructive, handler: { [weak self] _ in
+            self?.deleteItem(at: indexPath)
+        })
+        let cancelAction = UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil)
+        alertController.addAction(okAction)
+        alertController.addAction(cancelAction)
+        present(alertController, animated: true, completion: nil)
+    }
+
+    private func deleteItem(at indexPath: IndexPath) {
+        let msgId = mediaMessageIds.remove(at: indexPath.row)
+        self.dcContext.deleteMessage(msgId: msgId)
+        self.grid.deleteItems(at: [indexPath])
+    }
 }
 }
 
 
 extension GalleryViewController: UICollectionViewDataSourcePrefetching {
 extension GalleryViewController: UICollectionViewDataSourcePrefetching {
     func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
     func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
-         indexPaths.forEach { if items[$0.row] == nil {
+        indexPaths.forEach { if items[$0.row] == nil {
             let item = GalleryItem(msgId: mediaMessageIds[$0.row])
             let item = GalleryItem(msgId: mediaMessageIds[$0.row])
             items[$0.row] = item
             items[$0.row] = item
         }}
         }}
@@ -128,8 +179,8 @@ extension GalleryViewController: UICollectionViewDataSource, UICollectionViewDel
 
 
     func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
     func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
         guard let galleryCell = collectionView.dequeueReusableCell(
         guard let galleryCell = collectionView.dequeueReusableCell(
-            withReuseIdentifier: GalleryCell.reuseIdentifier,
-            for: indexPath) as? GalleryCell else {
+                withReuseIdentifier: GalleryCell.reuseIdentifier,
+                for: indexPath) as? GalleryCell else {
             return UICollectionViewCell()
             return UICollectionViewCell()
         }
         }
 
 
@@ -143,6 +194,7 @@ extension GalleryViewController: UICollectionViewDataSource, UICollectionViewDel
             item = galleryItem
             item = galleryItem
         }
         }
         galleryCell.update(item: item)
         galleryCell.update(item: item)
+        UIMenuController.shared.setMenuVisible(false, animated: true)
         return galleryCell
         return galleryCell
     }
     }
 
 
@@ -150,6 +202,7 @@ extension GalleryViewController: UICollectionViewDataSource, UICollectionViewDel
         let msgId = mediaMessageIds[indexPath.row]
         let msgId = mediaMessageIds[indexPath.row]
         showPreview(msgId: msgId)
         showPreview(msgId: msgId)
         collectionView.deselectItem(at: indexPath, animated: true)
         collectionView.deselectItem(at: indexPath, animated: true)
+        UIMenuController.shared.setMenuVisible(false, animated: true)
     }
     }
 
 
     func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
     func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
@@ -164,6 +217,50 @@ extension GalleryViewController: UICollectionViewDataSource, UICollectionViewDel
     func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
     func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
         timeLabel.hide(animated: true)
         timeLabel.hide(animated: true)
     }
     }
+
+    // MARK: - context menu
+    // context menu for iOS 11, 12
+    func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool {
+        return true
+    }
+
+    func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
+        return contextMenu.canPerformAction(action: action)
+    }
+
+    func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
+
+        contextMenu.performAction(action: action, indexPath: indexPath)
+    }
+
+    // context menu for iOS 13+
+    @available(iOS 13, *)
+    func collectionView(
+        _ collectionView: UICollectionView,
+        contextMenuConfigurationForItemAt indexPath: IndexPath,
+        point: CGPoint) -> UIContextMenuConfiguration? {
+        guard let galleryCell = collectionView.cellForItem(at: indexPath) as? GalleryCell, let item = galleryCell.item else {
+            return nil
+        }
+
+        return UIContextMenuConfiguration(
+            identifier: nil,
+            previewProvider: {
+                let contextMenuController = ContextMenuController(item: item)
+                return contextMenuController
+            },
+            actionProvider: { [weak self] _ in
+                self?.contextMenu.actionProvider(indexPath: indexPath)
+            }
+        )
+    }
+
+    @available(iOS 13, *)
+    func collectionView(_ collectionView: UICollectionView, willEndContextMenuInteraction configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
+        if let msgId = (animator?.previewViewController as? ContextMenuController)?.msg.id {
+            self.showPreview(msgId: msgId)
+        }
+    }
 }
 }
 
 
 // MARK: - grid layout + updates
 // MARK: - grid layout + updates
@@ -205,88 +302,105 @@ private extension GalleryViewController {
 }
 }
 
 
 // MARK: - coordinator
 // MARK: - coordinator
-extension GalleryViewController {
+private extension GalleryViewController {
     func showPreview(msgId: Int) {
     func showPreview(msgId: Int) {
         guard let index = mediaMessageIds.index(of: msgId) else {
         guard let index = mediaMessageIds.index(of: msgId) else {
             return
             return
         }
         }
+
         let previewController = PreviewController(type: .multi(mediaMessageIds, index))
         let previewController = PreviewController(type: .multi(mediaMessageIds, index))
         present(previewController, animated: true, completion: nil)
         present(previewController, animated: true, completion: nil)
     }
     }
-}
 
 
-class GalleryItem {
+    func redirectToMessage(of indexPath: IndexPath) {
+        let msgId = mediaMessageIds[indexPath.row]
+
+        guard
+            let chatViewController = navigationController?.viewControllers.filter ({ $0 is ChatViewController}).first as? ChatViewController,
+            let chatListController = navigationController?.viewControllers.filter({ $0 is ChatListController}).first as? ChatListController
+        else {
+            safe_fatalError("failt to retrieve chatViewController, chatListController in navigation stack")
+            return
+        }
+        self.navigationController?.viewControllers.remove(at: 1)
+
+        self.navigationController?.pushViewController(chatViewController, animated: true)
+        self.navigationController?.setViewControllers([chatListController, chatViewController], animated: false)
+        chatViewController.scrollToMessage(msgId: msgId)
+    }
+}
 
 
-    var onImageLoaded: ((UIImage?) -> Void)?
+class ContextMenuProvider {
 
 
-    var msg: DcMsg
+    var menu: [ContextMenuItem] = []
 
 
-    var fileUrl: URL? {
-        return msg.fileURL
+    init(menu: [ContextMenuItem] = []) {
+        self.menu = menu
     }
     }
 
 
-    var thumbnailImage: UIImage? {
-        willSet {
-           onImageLoaded?(newValue)
-        }
+    func setMenu(_ menu: [ContextMenuItem]) {
+        self.menu = menu
     }
     }
 
 
-    var showPlayButton: Bool {
-        switch msg.viewtype {
-        case .video:
-            return true
-        default:
-            return false
-        }
+    // iOS 12- action menu
+    var menuItems: [UIMenuItem] {
+        return menu.map { UIMenuItem(title: $0.title, action: $0.action) }
     }
     }
 
 
-    init(msgId: Int) {
-        self.msg = DcMsg(id: msgId)
+    // iOS13+ action menu
+    @available(iOS 13, *)
+    func actionProvider(title: String = "", image: UIImage? = nil, identifier: UIMenu.Identifier? = nil, indexPath: IndexPath) -> UIMenu {
 
 
-        if let key = msg.fileURL?.absoluteString, let image = ThumbnailCache.shared.restoreImage(key: key) {
-            self.thumbnailImage = image
-        } else {
-            loadThumbnail()
+        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
+        )
     }
     }
 
 
-    private func loadThumbnail() {
-        guard let viewtype = msg.viewtype, let url = msg.fileURL else {
-            return
-        }
-        switch viewtype {
-        case .image:
-            thumbnailImage = msg.image
-        case .video:
-            loadVideoThumbnail(from: url)
-        case .gif:
-            loadGifThumbnail(from: url)
-        default:
-           safe_fatalError("unsupported viewtype - viewtype \(viewtype) not supported.")
-        }
+    func canPerformAction(action: Selector) -> Bool {
+        return !menu.filter {
+            $0.action == action
+        }.isEmpty
     }
     }
 
 
-    private func loadGifThumbnail(from url: URL) {
-        DispatchQueue.global(qos: .userInteractive).async {
-            guard let imageData = try? Data(contentsOf: url) else {
-                return
-            }
-            let thumbnailImage = SDAnimatedImage(data: imageData)
-            DispatchQueue.main.async { [weak self] in
-                self?.thumbnailImage = thumbnailImage
-            }
-        }
+    func performAction(action: Selector, indexPath: IndexPath) {
+        menu.filter {
+            $0.action == action
+        }.first?.onPerform?(indexPath)
     }
     }
 
 
-    private func loadVideoThumbnail(from url: URL) {
-        DispatchQueue.global(qos: .userInteractive).async {
-            let thumbnailImage = DcUtils.generateThumbnailFromVideo(url: url)
-            DispatchQueue.main.async { [weak self] in
-                self?.thumbnailImage = thumbnailImage
-                if let image = thumbnailImage {
-                    ThumbnailCache.shared.storeImage(image: image, key: url.absoluteString)
-                }
-            }
-        }
+    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)?
     }
     }
 }
 }

+ 2 - 2
deltachat-ios/Controller/GroupChatDetailViewController.swift

@@ -304,12 +304,12 @@ class GroupChatDetailViewController: UIViewController {
 
 
     private func showDocuments() {
     private func showDocuments() {
         let messageIds: [Int] = documentItemMessageIds.reversed()
         let messageIds: [Int] = documentItemMessageIds.reversed()
-        let fileGalleryController = DocumentGalleryController(fileMessageIds: messageIds)
+        let fileGalleryController = DocumentGalleryController(context: dcContext, fileMessageIds: messageIds)
         navigationController?.pushViewController(fileGalleryController, animated: true)    }
         navigationController?.pushViewController(fileGalleryController, animated: true)    }
 
 
     private func showGallery() {
     private func showGallery() {
         let messageIds: [Int] = galleryItemMessageIds.reversed()
         let messageIds: [Int] = galleryItemMessageIds.reversed()
-        let galleryController = GalleryViewController(mediaMessageIds: messageIds)
+        let galleryController = GalleryViewController(context: dcContext, mediaMessageIds: messageIds)
         navigationController?.pushViewController(galleryController, animated: true)
         navigationController?.pushViewController(galleryController, animated: true)
     }
     }
 
 

+ 79 - 0
deltachat-ios/Model/GalleryItem.swift

@@ -0,0 +1,79 @@
+import UIKit
+import DcCore
+import SDWebImage
+
+class GalleryItem: ContextMenuItem {
+
+    var onImageLoaded: ((UIImage?) -> Void)?
+
+    var msg: DcMsg
+
+    var fileUrl: URL? {
+        return msg.fileURL
+    }
+
+    var thumbnailImage: UIImage? {
+        willSet {
+            onImageLoaded?(newValue)
+        }
+    }
+
+    var showPlayButton: Bool {
+        switch msg.viewtype {
+        case .video:
+            return true
+        default:
+            return false
+        }
+    }
+
+    init(msgId: Int) {
+        self.msg = DcMsg(id: msgId)
+
+        if let key = msg.fileURL?.absoluteString, let image = ThumbnailCache.shared.restoreImage(key: key) {
+            self.thumbnailImage = image
+        } else {
+            loadThumbnail()
+        }
+    }
+
+    private func loadThumbnail() {
+        guard let viewtype = msg.viewtype, let url = msg.fileURL else {
+            return
+        }
+        switch viewtype {
+        case .image:
+            thumbnailImage = msg.image
+        case .video:
+            loadVideoThumbnail(from: url)
+        case .gif:
+            loadGifThumbnail(from: url)
+        default:
+            safe_fatalError("unsupported viewtype - viewtype \(viewtype) not supported.")
+        }
+    }
+
+    private func loadGifThumbnail(from url: URL) {
+        DispatchQueue.global(qos: .userInteractive).async {
+            guard let imageData = try? Data(contentsOf: url) else {
+                return
+            }
+            let thumbnailImage = SDAnimatedImage(data: imageData)
+            DispatchQueue.main.async { [weak self] in
+                self?.thumbnailImage = thumbnailImage
+            }
+        }
+    }
+
+    private func loadVideoThumbnail(from url: URL) {
+        DispatchQueue.global(qos: .userInteractive).async {
+            let thumbnailImage = DcUtils.generateThumbnailFromVideo(url: url)
+            DispatchQueue.main.async { [weak self] in
+                self?.thumbnailImage = thumbnailImage
+                if let image = thumbnailImage {
+                    ThumbnailCache.shared.storeImage(image: image, key: url.absoluteString)
+                }
+            }
+        }
+    }
+}

+ 19 - 0
deltachat-ios/View/Cell/DocumentGalleryFileCell.swift

@@ -84,6 +84,25 @@ class DocumentGalleryFileCell: UITableViewCell {
             let controller = UIDocumentInteractionController(url: url)
             let controller = UIDocumentInteractionController(url: url)
             fileImageView.image = controller.icons.first ?? placeholder
             fileImageView.image = controller.icons.first ?? placeholder
         }
         }
+    }
+
+    // needed for iOS 12 context men
+    @objc func itemDelete(_ sender: Any) {
+        self.performAction(#selector(DocumentGalleryFileCell.itemDelete(_:)), with: sender)
+    }
 
 
+    @objc func showInChat(_ sender: Any) {
+        self.performAction(#selector(DocumentGalleryFileCell.showInChat(_:)), with: sender)
+    }
+
+    func performAction(_ action: Selector, with sender: Any?) {
+        if let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
+            tableView.delegate?.tableView?(
+                tableView,
+                performAction: action,
+                forRowAt: indexPath,
+                withSender: sender
+            )
+        }
     }
     }
 }
 }

+ 19 - 0
deltachat-ios/View/Cell/GalleryCell.swift

@@ -66,4 +66,23 @@ class GalleryCell: UICollectionViewCell {
             imageView.alpha = newValue ? 0.75 : 1.0
             imageView.alpha = newValue ? 0.75 : 1.0
         }
         }
     }
     }
+
+    @objc func itemDelete(_ sender: Any) {
+        self.performAction(#selector(GalleryCell.itemDelete(_:)), with: sender)
+    }
+
+    @objc func showInChat(_ sender: Any) {
+        self.performAction(#selector(GalleryCell.showInChat(_:)), with: sender)
+    }
+
+    func performAction(_ action: Selector, with sender: Any?) {
+        if let collectionView = self.superview as? UICollectionView, let indexPath = collectionView.indexPath(for: self) {
+            collectionView.delegate?.collectionView?(
+                collectionView,
+                performAction: action,
+                forItemAt: indexPath,
+                withSender: sender
+            )
+        }
+    }
 }
 }