GalleryViewController.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. import UIKit
  2. import DcCore
  3. import QuickLook
  4. class GalleryViewController: UIViewController {
  5. private let dcContext: DcContext
  6. // MARK: - data
  7. private let chatId: Int
  8. private var mediaMessageIds: [Int]
  9. private var items: [Int: GalleryItem] = [:]
  10. // MARK: - subview specs
  11. private let gridDefaultSpacing: CGFloat = 5
  12. private lazy var gridLayout: GridCollectionViewFlowLayout = {
  13. let layout = GridCollectionViewFlowLayout()
  14. layout.minimumLineSpacing = gridDefaultSpacing
  15. layout.minimumInteritemSpacing = gridDefaultSpacing
  16. layout.format = .square
  17. return layout
  18. }()
  19. private lazy var grid: UICollectionView = {
  20. let collection = UICollectionView(frame: .zero, collectionViewLayout: gridLayout)
  21. collection.dataSource = self
  22. collection.delegate = self
  23. collection.register(GalleryCell.self, forCellWithReuseIdentifier: GalleryCell.reuseIdentifier)
  24. collection.contentInset = UIEdgeInsets(top: gridDefaultSpacing, left: gridDefaultSpacing, bottom: gridDefaultSpacing, right: gridDefaultSpacing)
  25. collection.backgroundColor = DcColors.defaultBackgroundColor
  26. collection.delaysContentTouches = false
  27. collection.alwaysBounceVertical = true
  28. collection.isPrefetchingEnabled = true
  29. collection.prefetchDataSource = self
  30. return collection
  31. }()
  32. private lazy var timeLabel: GalleryTimeLabel = {
  33. let view = GalleryTimeLabel()
  34. view.hide(animated: false)
  35. return view
  36. }()
  37. private lazy var emptyStateView: EmptyStateLabel = {
  38. let label = EmptyStateLabel()
  39. label.text = String.localized(chatId == 0 ? "tab_all_media_empty_hint" : "tab_gallery_empty_hint")
  40. label.isHidden = true
  41. return label
  42. }()
  43. private lazy var contextMenu: ContextMenuProvider = {
  44. let deleteItem = ContextMenuProvider.ContextMenuItem(
  45. title: String.localized("delete"),
  46. imageName: "trash",
  47. isDestructive: true,
  48. action: #selector(GalleryCell.itemDelete(_:)),
  49. onPerform: { [weak self] indexPath in
  50. self?.askToDeleteItem(at: indexPath)
  51. }
  52. )
  53. let showInChatItem = ContextMenuProvider.ContextMenuItem(
  54. title: String.localized("show_in_chat"),
  55. imageName: "doc.text.magnifyingglass",
  56. action: #selector(GalleryCell.showInChat(_:)),
  57. onPerform: { [weak self] indexPath in
  58. self?.redirectToMessage(of: indexPath)
  59. }
  60. )
  61. let config = ContextMenuProvider()
  62. config.setMenu([showInChatItem, deleteItem])
  63. return config
  64. }()
  65. init(context: DcContext, chatId: Int, mediaMessageIds: [Int]) {
  66. self.dcContext = context
  67. self.chatId = chatId
  68. self.mediaMessageIds = mediaMessageIds
  69. super.init(nibName: nil, bundle: nil)
  70. }
  71. required init?(coder: NSCoder) {
  72. fatalError("init(coder:) has not been implemented")
  73. }
  74. // MARK: - lifecycle
  75. override func viewDidLoad() {
  76. super.viewDidLoad()
  77. setupSubviews()
  78. title = String.localized("images_and_videos")
  79. if mediaMessageIds.isEmpty {
  80. emptyStateView.isHidden = false
  81. }
  82. }
  83. override func viewWillAppear(_ animated: Bool) {
  84. grid.reloadData()
  85. setupContextMenuIfNeeded()
  86. }
  87. override func viewWillLayoutSubviews() {
  88. super.viewWillLayoutSubviews()
  89. self.reloadCollectionViewLayout()
  90. }
  91. // MARK: - setup
  92. private func setupSubviews() {
  93. view.addSubview(grid)
  94. grid.translatesAutoresizingMaskIntoConstraints = false
  95. grid.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
  96. grid.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
  97. grid.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
  98. grid.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
  99. view.addSubview(timeLabel)
  100. timeLabel.translatesAutoresizingMaskIntoConstraints = false
  101. timeLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10).isActive = true
  102. timeLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
  103. emptyStateView.addCenteredTo(parentView: view)
  104. }
  105. private func setupContextMenuIfNeeded() {
  106. UIMenuController.shared.menuItems = contextMenu.menuItems
  107. UIMenuController.shared.update()
  108. }
  109. // MARK: - updates
  110. private func updateFloatingTimeLabel() {
  111. if let indexPath = grid.indexPathsForVisibleItems.min() {
  112. let msgId = mediaMessageIds[indexPath.row]
  113. let msg = dcContext.getMessage(id: msgId)
  114. timeLabel.update(date: msg.sentDate)
  115. }
  116. }
  117. // MARK: - actions
  118. private func askToDeleteItem(at indexPath: IndexPath) {
  119. let chat = dcContext.getChat(chatId: chatId)
  120. let title = chat.isDeviceTalk ?
  121. String.localized(stringID: "ask_delete_messages_simple", count: 1) :
  122. String.localized(stringID: "ask_delete_messages", count: 1)
  123. let alertController = UIAlertController(title: title, message: nil, preferredStyle: .safeActionSheet)
  124. let okAction = UIAlertAction(title: String.localized("delete"), style: .destructive, handler: { [weak self] _ in
  125. self?.deleteItem(at: indexPath)
  126. })
  127. let cancelAction = UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil)
  128. alertController.addAction(okAction)
  129. alertController.addAction(cancelAction)
  130. present(alertController, animated: true, completion: nil)
  131. }
  132. private func deleteItem(at indexPath: IndexPath) {
  133. let msgId = mediaMessageIds.remove(at: indexPath.row)
  134. self.dcContext.deleteMessage(msgId: msgId)
  135. self.grid.deleteItems(at: [indexPath])
  136. }
  137. }
  138. extension GalleryViewController: UICollectionViewDataSourcePrefetching {
  139. func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
  140. indexPaths.forEach { if items[$0.row] == nil {
  141. let message = dcContext.getMessage(id: mediaMessageIds[$0.row])
  142. let item = GalleryItem(msg: message)
  143. items[$0.row] = item
  144. }}
  145. }
  146. }
  147. // MARK: - UICollectionViewDataSource, UICollectionViewDelegate
  148. extension GalleryViewController: UICollectionViewDataSource, UICollectionViewDelegate {
  149. func numberOfSections(in collectionView: UICollectionView) -> Int {
  150. return 1
  151. }
  152. func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  153. return mediaMessageIds.count
  154. }
  155. func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  156. guard let galleryCell = collectionView.dequeueReusableCell(
  157. withReuseIdentifier: GalleryCell.reuseIdentifier,
  158. for: indexPath) as? GalleryCell else {
  159. return UICollectionViewCell()
  160. }
  161. let msgId = mediaMessageIds[indexPath.row]
  162. var item: GalleryItem
  163. if let galleryItem = items[indexPath.row] {
  164. item = galleryItem
  165. } else {
  166. let message = dcContext.getMessage(id: msgId)
  167. let galleryItem = GalleryItem(msg: message)
  168. items[indexPath.row] = galleryItem
  169. item = galleryItem
  170. }
  171. galleryCell.update(item: item)
  172. UIMenuController.shared.setMenuVisible(false, animated: true)
  173. return galleryCell
  174. }
  175. func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  176. let previewController = PreviewController(dcContext: dcContext, type: .multi(mediaMessageIds, indexPath.row))
  177. previewController.delegate = self
  178. navigationController?.pushViewController(previewController, animated: true)
  179. collectionView.deselectItem(at: indexPath, animated: true)
  180. UIMenuController.shared.setMenuVisible(false, animated: true)
  181. }
  182. func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
  183. updateFloatingTimeLabel()
  184. timeLabel.show(animated: true)
  185. }
  186. func scrollViewDidScroll(_ scrollView: UIScrollView) {
  187. updateFloatingTimeLabel()
  188. }
  189. func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  190. timeLabel.hide(animated: true)
  191. }
  192. // MARK: - context menu
  193. // context menu for iOS 11, 12
  194. func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool {
  195. return true
  196. }
  197. func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
  198. return contextMenu.canPerformAction(action: action)
  199. }
  200. func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
  201. contextMenu.performAction(action: action, indexPath: indexPath)
  202. }
  203. // context menu for iOS 13+
  204. @available(iOS 13, *)
  205. func collectionView(
  206. _ collectionView: UICollectionView,
  207. contextMenuConfigurationForItemAt indexPath: IndexPath,
  208. point: CGPoint) -> UIContextMenuConfiguration? {
  209. guard let galleryCell = collectionView.cellForItem(at: indexPath) as? GalleryCell, let item = galleryCell.item else {
  210. return nil
  211. }
  212. return UIContextMenuConfiguration(
  213. identifier: nil,
  214. previewProvider: {
  215. let contextMenuController = ContextMenuController(item: item)
  216. return contextMenuController
  217. },
  218. actionProvider: { [weak self] _ in
  219. self?.contextMenu.actionProvider(indexPath: indexPath)
  220. }
  221. )
  222. }
  223. }
  224. // MARK: - grid layout + updates
  225. private extension GalleryViewController {
  226. func reloadCollectionViewLayout() {
  227. // columns specification
  228. let phonePortrait = 3
  229. let phoneLandscape = 4
  230. let padPortrait = 5
  231. let padLandscape = 8
  232. let orientation = UIApplication.shared.statusBarOrientation
  233. let deviceType = UIDevice.current.userInterfaceIdiom
  234. let gridDisplay: GridDisplay
  235. if deviceType == .phone {
  236. if orientation.isPortrait {
  237. gridDisplay = .grid(columns: phonePortrait)
  238. } else {
  239. gridDisplay = .grid(columns: phoneLandscape)
  240. }
  241. } else {
  242. if orientation.isPortrait {
  243. gridDisplay = .grid(columns: padPortrait)
  244. } else {
  245. gridDisplay = .grid(columns: padLandscape)
  246. }
  247. }
  248. gridLayout.display = gridDisplay
  249. let containerWidth = view.bounds.width - view.safeAreaInsets.left - view.safeAreaInsets.right - 2 * gridDefaultSpacing
  250. gridLayout.containerWidth = containerWidth
  251. }
  252. }
  253. // MARK: - coordinator
  254. private extension GalleryViewController {
  255. func redirectToMessage(of indexPath: IndexPath) {
  256. let msgId = mediaMessageIds[indexPath.row]
  257. let chatId = dcContext.getMessage(id: msgId).chatId
  258. if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
  259. appDelegate.appCoordinator.showChat(chatId: chatId, msgId: msgId, animated: false, clearViewControllerStack: true)
  260. }
  261. }
  262. }
  263. // MARK: - QLPreviewControllerDataSource
  264. extension GalleryViewController: QLPreviewControllerDelegate {
  265. func previewController(_ controller: QLPreviewController, transitionViewFor item: QLPreviewItem) -> UIView? {
  266. let indexPath = IndexPath(row: controller.currentPreviewItemIndex, section: 0)
  267. return grid.cellForItem(at: indexPath)
  268. }
  269. }