RoomSharedItemsTableViewController.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. //
  2. // SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
  3. // SPDX-License-Identifier: GPL-3.0-or-later
  4. //
  5. import UIKit
  6. import QuickLook
  7. @objcMembers class RoomSharedItemsTableViewController: UITableViewController,
  8. NCChatFileControllerDelegate,
  9. QLPreviewControllerDelegate,
  10. QLPreviewControllerDataSource,
  11. VLCKitVideoViewControllerDelegate {
  12. let room: NCRoom
  13. let account: TalkAccount = NCDatabaseManager.sharedInstance().activeAccount()
  14. let itemsOverviewLimit: Int = 1
  15. let itemLimit: Int = 100
  16. var sharedItemsOverview: [String: [NCChatMessage]] = [:]
  17. var currentItems: [NCChatMessage] = []
  18. var currentItemType: String = "all"
  19. var currentLastItemId: Int = -1
  20. var sharedItemsBackgroundView: PlaceholderView = PlaceholderView(for: .insetGrouped)
  21. var previewControllerFilePath: String = ""
  22. var isPreviewControllerShown: Bool = false
  23. weak var previewChatViewController: ContextChatViewController?
  24. weak var previewNavigationChatViewController: NCNavigationController?
  25. init(room: NCRoom) {
  26. self.room = room
  27. super.init(nibName: "RoomSharedItemsTableViewController", bundle: nil)
  28. }
  29. required init?(coder: NSCoder) {
  30. fatalError("init(coder:) has not been implemented")
  31. }
  32. override func viewDidLoad() {
  33. super.viewDidLoad()
  34. self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: NCAppBranding.themeTextColor()]
  35. self.navigationController?.navigationBar.tintColor = NCAppBranding.themeTextColor()
  36. self.navigationController?.navigationBar.barTintColor = NCAppBranding.themeColor()
  37. self.navigationController?.navigationBar.isTranslucent = false
  38. self.navigationItem.title = NSLocalizedString("Shared items", comment: "")
  39. let appearance = UINavigationBarAppearance()
  40. appearance.configureWithOpaqueBackground()
  41. appearance.titleTextAttributes = [.foregroundColor: NCAppBranding.themeTextColor()]
  42. appearance.backgroundColor = NCAppBranding.themeColor()
  43. self.navigationItem.standardAppearance = appearance
  44. self.navigationItem.compactAppearance = appearance
  45. self.navigationItem.scrollEdgeAppearance = appearance
  46. self.tableView.separatorInset = UIEdgeInsets(top: 0, left: 64, bottom: 0, right: 0)
  47. self.tableView.register(UINib(nibName: DirectoryTableViewCell.nibName, bundle: nil), forCellReuseIdentifier: DirectoryTableViewCell.identifier)
  48. self.hideShowMoreButton()
  49. self.getItemsOverview()
  50. }
  51. func availableItemTypes() -> [String] {
  52. var availableItemTypes: [String] = []
  53. for itemType in sharedItemsOverview.keys {
  54. guard let items = sharedItemsOverview[itemType] else {continue}
  55. if !items.isEmpty {
  56. availableItemTypes.append(itemType)
  57. }
  58. }
  59. return availableItemTypes.sorted(by: { $0 < $1 })
  60. }
  61. func getItemsForItemType(itemType: String) {
  62. showFetchingItemsPlaceholderView()
  63. NCAPIController.sharedInstance()
  64. .getSharedItems(ofType: itemType, fromLastMessageId: currentLastItemId, withLimit: itemLimit,
  65. inRoom: room.token, for: account) { items, lastItemId, error, _ in
  66. if error == nil, let sharedItems = items as? [NCChatMessage] {
  67. // Remove deleted files
  68. var filteredItems: [NCChatMessage] = []
  69. for message in sharedItems {
  70. if message.systemMessage == "file_shared" && message.file() == nil {continue}
  71. filteredItems.append(message)
  72. }
  73. // Sort received items
  74. let sortedItems = filteredItems.sorted(by: { $0.messageId > $1.messageId })
  75. // Set or append items
  76. if self.currentLastItemId > 0 {
  77. self.currentItems.append(contentsOf: sortedItems)
  78. } else {
  79. self.currentItems = sortedItems
  80. }
  81. // Set new last item id
  82. self.currentLastItemId = lastItemId
  83. // Show ir hide "Show more" button
  84. if sharedItems.count == self.itemLimit {
  85. self.showShowMoreButton()
  86. } else {
  87. self.hideShowMoreButton()
  88. }
  89. // Load items
  90. self.tableView.reloadData()
  91. } else {
  92. self.hideShowMoreButton()
  93. }
  94. self.hideFetchingItemsPlaceholderView()
  95. }
  96. }
  97. func getItemsOverview() {
  98. showFetchingItemsPlaceholderView()
  99. NCAPIController.sharedInstance()
  100. .getSharedItemsOverview(inRoom: room.token, withLimit: itemsOverviewLimit, for: account) { itemsOverview, error, _ in
  101. if error == nil {
  102. self.sharedItemsOverview = itemsOverview as? [String: [NCChatMessage]] ?? [:]
  103. let availableItemTypes = self.availableItemTypes()
  104. if availableItemTypes.isEmpty {
  105. self.hideFetchingItemsPlaceholderView()
  106. } else if availableItemTypes.contains(kSharedItemTypeMedia) {
  107. self.setupViewForItemType(itemType: kSharedItemTypeMedia)
  108. } else if availableItemTypes.contains(kSharedItemTypeFile) {
  109. self.setupViewForItemType(itemType: kSharedItemTypeFile)
  110. } else if let firstItemType = availableItemTypes.first {
  111. self.setupViewForItemType(itemType: firstItemType)
  112. }
  113. } else {
  114. self.hideFetchingItemsPlaceholderView()
  115. }
  116. }
  117. }
  118. func setupViewForItemType(itemType: String) {
  119. currentItemType = itemType
  120. currentItems = []
  121. currentLastItemId = -1
  122. hideShowMoreButton()
  123. tableView.reloadData()
  124. setupTitleButtonForItemType(itemType: itemType)
  125. getItemsForItemType(itemType: itemType)
  126. }
  127. func setupTitleButtonForItemType(itemType: String) {
  128. let itemTypeSelectorButton = UIButton(type: .custom)
  129. let buttonTitle = nameForItemType(itemType: itemType) + " ▼"
  130. itemTypeSelectorButton.setTitle(buttonTitle, for: .normal)
  131. itemTypeSelectorButton.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .medium)
  132. itemTypeSelectorButton.setTitleColor(NCAppBranding.themeTextColor(), for: .normal)
  133. self.navigationItem.titleView = itemTypeSelectorButton
  134. var menuActions: [UIAction] = []
  135. for itemType in availableItemTypes() {
  136. let itemTypeName = nameForItemType(itemType: itemType)
  137. let action = UIAction(title: itemTypeName, image: nil) { [unowned self] _ in
  138. self.setupViewForItemType(itemType: itemType)
  139. }
  140. if itemType == currentItemType {
  141. action.state = .on
  142. }
  143. menuActions.append(action)
  144. }
  145. itemTypeSelectorButton.showsMenuAsPrimaryAction = true
  146. itemTypeSelectorButton.menu = UIMenu(children: menuActions)
  147. }
  148. func showFetchingItemsPlaceholderView() {
  149. sharedItemsBackgroundView.placeholderView.isHidden = true
  150. sharedItemsBackgroundView.setImage(UIImage(systemName: "photo.on.rectangle.angled"))
  151. sharedItemsBackgroundView.placeholderImage.contentMode = .scaleAspectFit
  152. sharedItemsBackgroundView.placeholderTextView.text = NSLocalizedString("No shared items", comment: "")
  153. sharedItemsBackgroundView.loadingView.startAnimating()
  154. sharedItemsBackgroundView.loadingView.isHidden = false
  155. self.tableView.backgroundView = sharedItemsBackgroundView
  156. }
  157. func hideFetchingItemsPlaceholderView() {
  158. sharedItemsBackgroundView.loadingView.stopAnimating()
  159. sharedItemsBackgroundView.loadingView.isHidden = true
  160. sharedItemsBackgroundView.placeholderView.isHidden = !currentItems.isEmpty
  161. }
  162. func showShowMoreButton() {
  163. let showMoreButton = UIButton(frame: CGRect(origin: .zero, size: CGSize(width: self.tableView.frame.width, height: 40)))
  164. showMoreButton.titleLabel?.font = .systemFont(ofSize: 15)
  165. showMoreButton.setTitleColor(.systemBlue, for: .normal)
  166. showMoreButton.setTitle(NSLocalizedString("Show more…", comment: ""), for: .normal)
  167. showMoreButton.addTarget(self, action: #selector(showMoreButtonClicked), for: .touchUpInside)
  168. self.tableView.tableFooterView = showMoreButton
  169. }
  170. func hideShowMoreButton() {
  171. self.tableView.tableFooterView = UIView()
  172. }
  173. func showMoreButtonClicked() {
  174. let loadingMoreView = UIActivityIndicatorView(frame: CGRect(origin: .zero, size: CGSize(width: 40, height: 40)))
  175. loadingMoreView.color = .darkGray
  176. loadingMoreView.startAnimating()
  177. self.tableView.tableFooterView = loadingMoreView
  178. getItemsForItemType(itemType: currentItemType)
  179. }
  180. func nameForItemType(itemType: String) -> String {
  181. switch itemType {
  182. case kSharedItemTypeAudio:
  183. return NSLocalizedString("Audios", comment: "")
  184. case kSharedItemTypeDeckcard:
  185. return NSLocalizedString("Deck cards", comment: "")
  186. case kSharedItemTypeFile:
  187. return NSLocalizedString("Files", comment: "")
  188. case kSharedItemTypeMedia:
  189. return NSLocalizedString("Media", comment: "")
  190. case kSharedItemTypeLocation:
  191. return NSLocalizedString("Locations", comment: "")
  192. case kSharedItemTypeOther:
  193. return NSLocalizedString("Others", comment: "")
  194. case kSharedItemTypeVoice:
  195. return NSLocalizedString("Voice messages", comment: "")
  196. case kSharedItemTypePoll:
  197. return NSLocalizedString("Polls", comment: "")
  198. case kSharedItemTypeRecording:
  199. return NSLocalizedString("Recordings", comment: "")
  200. default:
  201. return NSLocalizedString("Shared items", comment: "")
  202. }
  203. }
  204. func imageForMessage(message: NCChatMessage) -> UIImage {
  205. var image = UIImage(named: "file")
  206. if message.file() != nil {
  207. let imageName = NCUtils.previewImage(forMimeType: message.file().mimetype)
  208. image = UIImage(named: imageName)
  209. }
  210. if message.geoLocation() != nil {
  211. image = UIImage(systemName: "mappin")
  212. }
  213. if message.deckCard() != nil {
  214. image = UIImage(named: "deck-item")
  215. }
  216. if message.poll != nil {
  217. image = UIImage(systemName: "chart.bar")
  218. }
  219. return image ?? UIImage()
  220. }
  221. // MARK: - File downloader
  222. func downloadFileForCell(cell: DirectoryTableViewCell, file: NCMessageFileParameter) {
  223. cell.fileParameter = file
  224. let downloader = NCChatFileController()
  225. downloader.delegate = self
  226. downloader.downloadFile(fromMessage: file)
  227. }
  228. func fileControllerDidLoadFile(_ fileController: NCChatFileController, with fileStatus: NCChatFileStatus) {
  229. DispatchQueue.main.async {
  230. if self.isPreviewControllerShown {
  231. return
  232. }
  233. guard let fileLocalPath = fileStatus.fileLocalPath else { return }
  234. self.previewControllerFilePath = fileLocalPath
  235. self.isPreviewControllerShown = true
  236. let fileExtension = URL(fileURLWithPath: fileLocalPath).pathExtension.lowercased()
  237. if VLCKitVideoViewController.supportedFileExtensions.contains(fileExtension) {
  238. let vlcViewController = VLCKitVideoViewController(filePath: fileLocalPath)
  239. vlcViewController.delegate = self
  240. vlcViewController.modalPresentationStyle = .fullScreen
  241. self.present(vlcViewController, animated: true)
  242. return
  243. }
  244. let previewController = QLPreviewController()
  245. previewController.dataSource = self
  246. previewController.delegate = self
  247. self.present(previewController, animated: true)
  248. }
  249. }
  250. func fileControllerDidFailLoadingFile(_ fileController: NCChatFileController, withErrorDescription errorDescription: String) {
  251. let alertTitle = NSLocalizedString("Unable to load file", comment: "")
  252. let alert = UIAlertController(
  253. title: alertTitle,
  254. message: errorDescription,
  255. preferredStyle: .alert)
  256. let okAction = UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)
  257. alert.addAction(okAction)
  258. self.present(alert, animated: true, completion: nil)
  259. }
  260. func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
  261. return 1
  262. }
  263. func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
  264. return NSURL(fileURLWithPath: previewControllerFilePath)
  265. }
  266. func previewControllerDidDismiss(_ controller: QLPreviewController) {
  267. isPreviewControllerShown = false
  268. }
  269. func vlckitVideoViewControllerDismissed(_ controller: VLCKitVideoViewController) {
  270. isPreviewControllerShown = false
  271. }
  272. // MARK: - Locations
  273. func presentLocation(location: GeoLocationRichObject) {
  274. let mapViewController = MapViewController(geoLocationRichObject: location)
  275. let navigationViewController = NCNavigationController(rootViewController: mapViewController)
  276. self.present(navigationViewController, animated: true, completion: nil)
  277. }
  278. // MARK: - Polls
  279. func presentPoll(pollId: Int) {
  280. let pollViewController = PollVotingView(style: .insetGrouped)
  281. pollViewController.room = room
  282. let navigationViewController = NCNavigationController(rootViewController: pollViewController)
  283. self.present(navigationViewController, animated: true, completion: nil)
  284. let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
  285. NCAPIController.sharedInstance().getPollWithId(pollId, inRoom: room.token, for: activeAccount) { poll, error, _ in
  286. if let poll = poll, error == nil {
  287. pollViewController.updatePoll(poll: poll)
  288. }
  289. }
  290. }
  291. // MARK: - Other files
  292. func openLink(link: String) {
  293. NCUtils.openLinkInBrowser(link: link)
  294. }
  295. // MARK: - Table view data source
  296. override func numberOfSections(in tableView: UITableView) -> Int {
  297. return 1
  298. }
  299. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  300. return currentItems.count
  301. }
  302. override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  303. return DirectoryTableViewCell.cellHeight
  304. }
  305. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  306. let cell = tableView.dequeueReusableCell(withIdentifier: DirectoryTableViewCell.identifier) as? DirectoryTableViewCell ??
  307. DirectoryTableViewCell(style: .default, reuseIdentifier: DirectoryTableViewCell.identifier)
  308. let message = currentItems[indexPath.row]
  309. if let file = message.file() {
  310. cell.fileNameLabel?.text = file.name
  311. } else {
  312. cell.fileNameLabel?.text = message.parsedMessage().string
  313. }
  314. var infoLabelText = NCUtils.relativeTimeFromDate(date: Date(timeIntervalSince1970: Double(message.timestamp)))
  315. if !message.actorDisplayName.isEmpty {
  316. infoLabelText += " ⸱ " + message.actorDisplayName
  317. }
  318. if let file = message.file(), file.size > 0 {
  319. let formatter = ByteCountFormatter()
  320. formatter.countStyle = .file
  321. let sizeString = formatter.string(fromByteCount: Int64(file.size))
  322. infoLabelText += " ⸱ " + sizeString
  323. }
  324. cell.fileInfoLabel?.text = infoLabelText
  325. let image = imageForMessage(message: message)
  326. cell.fileImageView?.image = image
  327. cell.fileImageView?.tintColor = .secondaryLabel
  328. if message.file()?.previewAvailable != nil {
  329. cell.fileImageView?
  330. .setImageWith(NCAPIController.sharedInstance().createPreviewRequest(forFile: message.file().parameterId,
  331. width: 40, height: 40,
  332. using: NCDatabaseManager.sharedInstance().activeAccount()),
  333. placeholderImage: image, success: nil, failure: nil)
  334. }
  335. return cell
  336. }
  337. override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
  338. return UIContextMenuConfiguration(identifier: indexPath as NSCopying, previewProvider: {
  339. // Init the BaseChatViewController without message to directly show a preview
  340. if let chatViewController = ContextChatViewController(for: self.room, withMessage: [], withHighlightId: 0) {
  341. self.previewChatViewController = chatViewController
  342. // Fetch the context of the message and update the BaseChatViewController
  343. let message = self.currentItems[indexPath.row]
  344. NCChatController(for: self.room).getMessageContext(forMessageId: message.messageId, withLimit: 50) { messages in
  345. guard let messages else { return }
  346. chatViewController.appendMessages(messages: messages)
  347. chatViewController.reloadDataAndHighlightMessage(messageId: message.messageId)
  348. }
  349. let navController = NCNavigationController(rootViewController: chatViewController)
  350. self.previewNavigationChatViewController = navController
  351. return navController
  352. }
  353. return nil
  354. }, actionProvider: { _ in
  355. UIMenu(children: [UIAction(title: NSLocalizedString("Open", comment: "")) { _ in
  356. DispatchQueue.main.async {
  357. self.presentPreviewChatViewController()
  358. }
  359. }])
  360. })
  361. }
  362. override func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
  363. animator.addAnimations {
  364. self.presentPreviewChatViewController()
  365. }
  366. }
  367. func presentPreviewChatViewController() {
  368. guard let previewNavigationChatViewController = self.previewNavigationChatViewController,
  369. let previewChatViewController = self.previewChatViewController
  370. else { return }
  371. self.present(previewNavigationChatViewController, animated: false)
  372. previewChatViewController.navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .cancel, primaryAction: UIAction { [weak previewChatViewController] _ in
  373. previewChatViewController?.dismiss(animated: true)
  374. })
  375. }
  376. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  377. let cell = tableView.cellForRow(at: indexPath) as? DirectoryTableViewCell ?? DirectoryTableViewCell()
  378. let message = currentItems[indexPath.row]
  379. self.tableView.deselectRow(at: indexPath, animated: true)
  380. switch currentItemType {
  381. case kSharedItemTypeMedia, kSharedItemTypeFile, kSharedItemTypeVoice, kSharedItemTypeAudio, kSharedItemTypeRecording:
  382. if let file = message.file() {
  383. downloadFileForCell(cell: cell, file: file)
  384. }
  385. case kSharedItemTypeLocation:
  386. if let geoLocation = message.geoLocation() {
  387. presentLocation(location: GeoLocationRichObject(from: geoLocation))
  388. }
  389. case kSharedItemTypeDeckcard, kSharedItemTypeOther:
  390. if let link = message.objectShareLink() {
  391. openLink(link: link)
  392. }
  393. case kSharedItemTypePoll:
  394. if let poll = message.poll, let pollId = Int(poll.parameterId) {
  395. presentPoll(pollId: pollId)
  396. }
  397. default:
  398. return
  399. }
  400. }
  401. }