Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gallery - context menu - 2nd Attempt #985

Merged
merged 25 commits into from
Jan 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b098684
manually re-integrated branch due to merge conflicts
nayooti Nov 4, 2020
5a60252
moved gallery item to separate file
nayooti Nov 4, 2020
ec7b643
xlpreviewcontroller now takes item as argument
nayooti Nov 4, 2020
2276acb
showing video player that plays video
nayooti Nov 4, 2020
44abc8f
tried play videos with AVVideoController
nayooti Nov 4, 2020
a6eaf56
intergrated new approach, so ContextMenuController uses AVVideoContro…
nayooti Nov 4, 2020
dc881ad
moved contextMenuController into seperate file
nayooti Nov 4, 2020
a75013b
fixed grid layout issue
nayooti Nov 4, 2020
6867f39
added gif support
nayooti Nov 12, 2020
a30c631
cleaning
nayooti Nov 12, 2020
cbbbbb9
confirm deletion - action red
nayooti Nov 12, 2020
cf241f7
cleaning
nayooti Nov 12, 2020
d777fb2
same delete message as chat
nayooti Nov 12, 2020
ceb6746
hit test attempt
nayooti Nov 20, 2020
0c81188
tap on menuContent now redirects to preview
nayooti Nov 20, 2020
994bbda
added scroll to msg in chat view
nayooti Nov 27, 2020
5ae8a76
redirect to message in context menu
nayooti Nov 27, 2020
c24ad32
handle iOS 12 context menu
nayooti Nov 27, 2020
6771a17
attempt to make context menu configurator
nayooti Nov 27, 2020
e3d379a
menu can be configured
nayooti Nov 30, 2020
a7e777b
cleaning
nayooti Nov 30, 2020
6bbb9db
added context menu logic to documentGalleryController
nayooti Dec 7, 2020
da92ef8
implemented context menu for documentGallery, ContextMenuController n…
nayooti Dec 7, 2020
e3762a3
context menu in document gallery now works
nayooti Dec 14, 2020
9e6a0a6
don't show playback controls in context menu for videos
cyBerta Jan 4, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions deltachat-ios.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@
AE4AEE3522B1030D000AA495 /* PreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE4AEE3422B1030D000AA495 /* PreviewController.swift */; };
AE52EA19229EB53C00C586C9 /* ContactDetailHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE52EA18229EB53C00C586C9 /* ContactDetailHeader.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 */; };
AE6EC5282497B9B200A400E4 /* ThumbnailCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE6EC5272497B9B200A400E4 /* ThumbnailCache.swift */; };
AE728F15229D5C390047565B /* PhotoPickerAlertAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE728F14229D5C390047565B /* PhotoPickerAlertAction.swift */; };
Expand Down Expand Up @@ -351,6 +353,8 @@
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>"; };
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>"; };
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>"; };
Expand Down Expand Up @@ -614,6 +618,7 @@
7A9FB1421FB061E2001FEA36 /* deltachat-ios */ = {
isa = PBXGroup;
children = (
AE57C07E2552BBC0003CFE70 /* Model */,
304219D7244072E600516852 /* DC */,
AE1988AA23EB3C7600B4CD5F /* Assets */,
AE19887623EB2BDA00B4CD5F /* Assets */,
Expand Down Expand Up @@ -693,6 +698,14 @@
path = Cell;
sourceTree = "<group>";
};
AE57C07E2552BBC0003CFE70 /* Model */ = {
isa = PBXGroup;
children = (
AE57C07F2552BBD0003CFE70 /* GalleryItem.swift */,
);
path = Model;
sourceTree = "<group>";
};
AE77838B23E32EAA0093EABD /* ViewModel */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -747,6 +760,7 @@
AED423D2249F578B00B6B2BB /* AddGroupMembersViewController.swift */,
AED423D6249F580700B6B2BB /* BlockedContactsViewController.swift */,
AE39D322249CFC1A007346A1 /* DocumentGalleryController.swift */,
AE57C083255310BB003CFE70 /* ContextMenuController.swift */,
);
path = Controller;
sourceTree = "<group>";
Expand Down Expand Up @@ -1184,6 +1198,7 @@
AE9DAF0F22C278C6004C9591 /* ChatTitleView.swift in Sources */,
AE4AEE3522B1030D000AA495 /* PreviewController.swift in Sources */,
7070FB9B2101ECBB000DC258 /* NewGroupController.swift in Sources */,
AE57C084255310BB003CFE70 /* ContextMenuController.swift in Sources */,
304219D92440734A00516852 /* DcMsg+Extension.swift in Sources */,
303492CF2587C2DC00A523D0 /* ChatInputBar.swift in Sources */,
AE52EA19229EB53C00C586C9 /* ContactDetailHeader.swift in Sources */,
Expand All @@ -1206,6 +1221,7 @@
AE77838F23E4276D0093EABD /* ContactCellViewModel.swift in Sources */,
B20462E62440C99600367A57 /* SettingsAutodelSetController.swift in Sources */,
3015634423A003BA00E9DEF4 /* AudioRecorderController.swift in Sources */,
AE57C0802552BBD0003CFE70 /* GalleryItem.swift in Sources */,
AE25F09022807AD800CDEA66 /* AvatarSelectionCell.swift in Sources */,
30A4149724F6EFBE00EC91EB /* InfoMessageCell.swift in Sources */,
302B84C6239676F0001C261F /* AvatarHelper.swift in Sources */,
Expand Down
14 changes: 10 additions & 4 deletions deltachat-ios/Chat/ChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
if show {
let dcChat = dcContext.getChat(chatId: chatId)
Expand Down Expand Up @@ -1108,10 +1116,8 @@ extension ChatViewController: BaseMessageCellDelegate {
@objc func quoteTapped(indexPath: IndexPath) {
_ = handleUIMenu()
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)
}
}

Expand Down
4 changes: 2 additions & 2 deletions deltachat-ios/Controller/ContactDetailViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -390,13 +390,13 @@ class ContactDetailViewController: UITableViewController {

private func showDocuments() {
let messageIds: [Int] = viewModel.documentItemMessageIds.reversed()
let fileGalleryController = DocumentGalleryController(fileMessageIds: messageIds)
let fileGalleryController = DocumentGalleryController(context: viewModel.context, fileMessageIds: messageIds)
navigationController?.pushViewController(fileGalleryController, animated: true)
}

private func showGallery() {
let messageIds: [Int] = viewModel.galleryItemMessageIds.reversed()
let galleryController = GalleryViewController(mediaMessageIds: messageIds)
let galleryController = GalleryViewController(context: viewModel.context, mediaMessageIds: messageIds)
navigationController?.pushViewController(galleryController, animated: true)
}

Expand Down
119 changes: 119 additions & 0 deletions deltachat-ios/Controller/ContextMenuController.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
116 changes: 107 additions & 9 deletions deltachat-ios/Controller/DocumentGalleryController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import DcCore

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)
table.register(DocumentGalleryFileCell.self, forCellReuseIdentifier: DocumentGalleryFileCell.reuseIdentifier)
table.dataSource = self
Expand All @@ -21,8 +22,33 @@ class DocumentGalleryController: UIViewController {
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
super.init(nibName: nil, bundle: nil)
self.title = String.localized("files")
Expand All @@ -41,14 +67,18 @@ class DocumentGalleryController: UIViewController {
}
}

override func viewWillAppear(_ animated: Bool) {
setupContextMenuIfNeeded()
}

// MARK: - layout
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)
emptyStateView.translatesAutoresizingMaskIntoConstraints = false
Expand All @@ -57,6 +87,30 @@ class DocumentGalleryController: UIViewController {
emptyStateView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor).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
Expand All @@ -80,6 +134,33 @@ extension DocumentGalleryController: UITableViewDelegate, UITableViewDataSource
showPreview(msgId: msgId)
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
Expand All @@ -91,4 +172,21 @@ extension DocumentGalleryController {
let previewController = PreviewController(type: .multi(fileMessageIds, index))
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)
}
}
Loading