GalleryViewController.swift 12 KB

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