BaseChatTableViewCell.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. //
  2. // SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
  3. // SPDX-License-Identifier: GPL-3.0-or-later
  4. //
  5. import MapKit
  6. import SwiftyGif
  7. protocol BaseChatTableViewCellDelegate: AnyObject {
  8. func cellWantsToScroll(to message: NCChatMessage)
  9. func cellWantsToReply(to message: NCChatMessage)
  10. func cellDidSelectedReaction(_ reaction: NCChatReaction!, for message: NCChatMessage)
  11. func cellWants(toDownloadFile fileParameter: NCMessageFileParameter, for message: NCChatMessage)
  12. func cellHasDownloadedImagePreview(withHeight height: CGFloat, for message: NCChatMessage)
  13. func cellWants(toOpenLocation geoLocationRichObject: GeoLocationRichObject)
  14. func cellWants(toPlayAudioFile fileParameter: NCMessageFileParameter)
  15. func cellWants(toPauseAudioFile fileParameter: NCMessageFileParameter)
  16. func cellWants(toChangeProgress progress: CGFloat, fromAudioFile fileParameter: NCMessageFileParameter)
  17. func cellWants(toOpenPoll poll: NCMessageParameter)
  18. }
  19. // Common elements
  20. public let chatMessageCellPreviewCornerRadius = 4.0
  21. // Message cell
  22. public let chatMessageCellIdentifier = "chatMessageCellIdentifier"
  23. public let chatGroupedMessageCellIdentifier = "chatGroupedMessageCellIdentifier"
  24. public let chatReplyMessageCellIdentifier = "chatReplyMessageCellIdentifier"
  25. public let chatMessageCellMinimumHeight = 45.0
  26. public let chatGroupedMessageCellMinimumHeight = 25.0
  27. // File cell
  28. public let fileMessageCellIdentifier = "fileMessageCellIdentifier"
  29. public let fileGroupedMessageCellIdentifier = "fileGroupedMessageCellIdentifier"
  30. public let fileMessageCellMinimumHeight = 50.0
  31. public let fileMessageCellFileMaxPreviewHeight = 120.0
  32. public let fileMessageCellFileMaxPreviewWidth = 230.0
  33. public let fileMessageCellMediaFilePreviewHeight = 230.0
  34. public let fileMessageCellMediaFileMaxPreviewWidth = 230.0
  35. public let fileMessageCellVideoPlayIconSize = 48.0
  36. // Location cell
  37. public let locationMessageCellIdentifier = "locationMessageCellIdentifier"
  38. public let locationGroupedMessageCellIdentifier = "locationGroupedMessageCellIdentifier"
  39. public let locationMessageCellMinimumHeight = 50.0
  40. public let locationMessageCellPreviewHeight = 120.0
  41. public let locationMessageCellPreviewWidth = 240.0
  42. // Voice message cell
  43. public let voiceMessageCellIdentifier = "voiceMessageCellIdentifier"
  44. public let voiceGroupedMessageCellIdentifier = "voiceGroupedMessageCellIdentifier"
  45. public let voiceMessageCellPlayerHeight = 52.0
  46. // Poll cell
  47. public let pollMessageCellIdentifier = "pollMessageCellIdentifier"
  48. public let pollGroupedMessageCellIdentifier = "pollGroupedMessageCellIdentifier"
  49. class BaseChatTableViewCell: UITableViewCell, AudioPlayerViewDelegate, ReactionsViewDelegate {
  50. public weak var delegate: BaseChatTableViewCellDelegate?
  51. @IBOutlet weak var avatarButton: AvatarButton!
  52. @IBOutlet weak var titleLabel: UILabel!
  53. @IBOutlet weak var dateLabel: UILabel!
  54. @IBOutlet weak var statusView: UIStackView!
  55. @IBOutlet weak var messageBodyView: UIView!
  56. @IBOutlet weak var headerPart: UIView!
  57. @IBOutlet weak var quotePart: UIView!
  58. @IBOutlet weak var reactionPart: UIView!
  59. @IBOutlet weak var referencePart: UIView!
  60. public var message: NCChatMessage?
  61. public var messageId: Int = 0
  62. internal var quotedMessageView: QuotedMessageView?
  63. internal var reactionView: ReactionsView?
  64. internal var referenceView: ReferenceView?
  65. internal var replyGestureRecognizer: DRCellSlideGestureRecognizer?
  66. // Message cell
  67. internal var messageTextView: MessageBodyTextView?
  68. // File cell
  69. internal var filePreviewImageView: UIImageView?
  70. internal var filePreviewImageViewHeightConstraint: NSLayoutConstraint?
  71. internal var filePreviewImageViewWidthConstraint: NSLayoutConstraint?
  72. internal var fileActivityIndicator: MDCActivityIndicator?
  73. internal var filePreviewActivityIndicator: MDCActivityIndicator?
  74. internal var filePreviewPlayIconImageView: UIImageView?
  75. internal var fileControllerWrapper: NCChatFileControllerWrapper?
  76. // Location cell
  77. internal var locationPreviewImageView: UIImageView?
  78. internal var locationMapSnapshooter: MKMapSnapshotter?
  79. internal var locationPreviewImageViewHeightConstraint: NSLayoutConstraint?
  80. internal var locationPreviewImageViewWidthConstraint: NSLayoutConstraint?
  81. // Audio cell
  82. internal var audioPlayerView: AudioPlayerView?
  83. // Poll cell
  84. internal var pollMessageView: PollMessageView?
  85. override func awakeFromNib() {
  86. super.awakeFromNib()
  87. self.commonInit()
  88. }
  89. func commonInit() {
  90. self.headerPart.isHidden = false
  91. self.quotePart.isHidden = true
  92. self.referencePart.isHidden = true
  93. self.reactionPart.isHidden = true
  94. }
  95. override func prepareForReuse() {
  96. super.prepareForReuse()
  97. self.message = nil
  98. self.avatarButton.cancelCurrentRequest()
  99. self.avatarButton.setImage(nil, for: .normal)
  100. self.quotedMessageView?.avatarView.cancelCurrentRequest()
  101. self.quotedMessageView?.avatarView.image = nil
  102. self.titleLabel.text = ""
  103. self.dateLabel.text = ""
  104. self.headerPart.isHidden = false
  105. self.quotePart.isHidden = true
  106. self.referencePart.isHidden = true
  107. self.reactionPart.isHidden = true
  108. self.statusView.isHidden = false
  109. self.statusView.subviews.forEach { $0.removeFromSuperview() }
  110. self.referenceView?.prepareForReuse()
  111. self.prepareForReuseMessageCell()
  112. self.prepareForReuseFileCell()
  113. self.prepareForReuseLocationCell()
  114. self.prepareForReuseAudioCell()
  115. self.prepareForReusePollCell()
  116. if let replyGestureRecognizer {
  117. self.removeGestureRecognizer(replyGestureRecognizer)
  118. self.replyGestureRecognizer = nil
  119. }
  120. }
  121. // swiftlint:disable:next cyclomatic_complexity
  122. public func setup(for message: NCChatMessage, withLastCommonReadMessage lastCommonRead: Int) {
  123. self.message = message
  124. self.messageId = message.messageId
  125. self.avatarButton.setActorAvatar(forMessage: message)
  126. self.avatarButton.menu = self.getDeferredUserMenu()
  127. self.avatarButton.showsMenuAsPrimaryAction = true
  128. let date = Date(timeIntervalSince1970: TimeInterval(message.timestamp))
  129. self.dateLabel.text = NCUtils.getTime(fromDate: date)
  130. let messageActor = message.actor
  131. let titleLabel = messageActor.attributedDisplayName
  132. if let lastEditActorDisplayName = message.lastEditActorDisplayName, message.lastEditTimestamp > 0 {
  133. var editedString = ""
  134. if message.lastEditActorId == message.actorId, message.lastEditActorType == "users" {
  135. editedString = NSLocalizedString("edited", comment: "A message was edited")
  136. editedString = " (\(editedString))"
  137. } else {
  138. editedString = NSLocalizedString("edited by", comment: "A message was edited by ...")
  139. editedString = " (\(editedString) \(lastEditActorDisplayName))"
  140. }
  141. let editedAttributedString = editedString.withTextColor(.tertiaryLabel)
  142. titleLabel.append(editedAttributedString)
  143. }
  144. self.titleLabel.attributedText = titleLabel
  145. let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
  146. var shouldShowDeliveryStatus = false
  147. var shouldShowReadStatus = false
  148. if let room = NCDatabaseManager.sharedInstance().room(withToken: message.token, forAccountId: activeAccount.accountId) {
  149. shouldShowDeliveryStatus = NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatReadStatus, for: room)
  150. if let roomCapabilities = NCDatabaseManager.sharedInstance().roomTalkCapabilities(for: room) {
  151. shouldShowReadStatus = !(roomCapabilities.readStatusPrivacy)
  152. }
  153. }
  154. // This check is just a workaround to fix the issue with the deleted parents returned by the API.
  155. if let parent = message.parent {
  156. self.showQuotePart()
  157. let quoteString = parent.parsedMarkdownForChat()?.string ?? ""
  158. self.quotedMessageView?.messageLabel.text = quoteString
  159. self.quotedMessageView?.actorLabel.attributedText = parent.actor.attributedDisplayName
  160. self.quotedMessageView?.highlighted = parent.isMessage(from: activeAccount.userId)
  161. self.quotedMessageView?.avatarView.setActorAvatar(forMessage: parent)
  162. }
  163. if message.isGroupMessage, message.parent == nil {
  164. self.headerPart.isHidden = true
  165. }
  166. // When `setDeliveryState` is not called, we still need to make sure the placeholder view is removed
  167. self.statusView.subviews.forEach { $0.removeFromSuperview() }
  168. if message.isDeleting {
  169. self.setDeliveryState(to: .deleting)
  170. } else if message.sendingFailed {
  171. self.setDeliveryState(to: .failed)
  172. } else if message.isTemporary {
  173. self.setDeliveryState(to: .sending)
  174. } else if message.isMessage(from: activeAccount.userId), shouldShowDeliveryStatus {
  175. if lastCommonRead >= message.messageId, shouldShowReadStatus {
  176. self.setDeliveryState(to: .read)
  177. } else {
  178. self.setDeliveryState(to: .sent)
  179. }
  180. } else if message.isSilent {
  181. self.setDeliveryState(to: .silent)
  182. }
  183. let reactionsArray = message.reactionsArray()
  184. if !reactionsArray.isEmpty {
  185. self.showReactionsPart()
  186. self.reactionView?.updateReactions(reactions: reactionsArray)
  187. }
  188. if message.containsURL() {
  189. self.showReferencePart()
  190. message.getReferenceData { message, referenceDataRaw, url in
  191. guard let cellMessage = self.message,
  192. let referenceMessage = message,
  193. cellMessage.isSameMessage(referenceMessage)
  194. else { return }
  195. if referenceDataRaw == nil, let deckCard = cellMessage.deckCard() {
  196. // In case we were unable to retrieve reference data (for example if the user has no permissions)
  197. // but the message is a shared deck card, we use the shared information to show the deck view
  198. self.referenceView?.update(for: deckCard)
  199. } else if let referenceData = referenceDataRaw as? [String: [String: AnyObject]], let url {
  200. self.referenceView?.update(for: referenceData, and: url)
  201. }
  202. }
  203. }
  204. if message.isReplyable, !message.isDeleting {
  205. self.addSlideToReplyGestureRecognizer(for: message)
  206. }
  207. if message.isVoiceMessage {
  208. // Audio message
  209. self.setupForAudioCell(with: message)
  210. } else if message.poll != nil {
  211. // Poll message
  212. self.setupForPollCell(with: message)
  213. } else if message.file() != nil {
  214. // File message
  215. self.setupForFileCell(with: message, with: activeAccount)
  216. } else if message.geoLocation() != nil {
  217. // Location message
  218. self.setupForLocationCell(with: message)
  219. } else {
  220. // Normal text message
  221. self.setupForMessageCell(with: message)
  222. }
  223. if message.isDeletedMessage {
  224. self.statusView.isHidden = true
  225. self.messageTextView?.textColor = .tertiaryLabel
  226. }
  227. NotificationCenter.default.addObserver(self, selector: #selector(didChangeIsDownloading(notification:)), name: NSNotification.Name.NCChatFileControllerDidChangeIsDownloading, object: nil)
  228. NotificationCenter.default.addObserver(self, selector: #selector(didChangeDownloadProgress(notification:)), name: NSNotification.Name.NCChatFileControllerDidChangeDownloadProgress, object: nil)
  229. }
  230. func addSlideToReplyGestureRecognizer(for message: NCChatMessage) {
  231. if let action = DRCellSlideAction(forFraction: 0.2) {
  232. action.behavior = .pullBehavior
  233. action.activeColor = .label
  234. action.inactiveColor = .placeholderText
  235. action.activeBackgroundColor = self.backgroundColor
  236. action.inactiveBackgroundColor = self.backgroundColor
  237. action.icon = UIImage(systemName: "arrowshape.turn.up.left")
  238. action.willTriggerBlock = { [unowned self] _, _ -> Void in
  239. self.delegate?.cellWantsToReply(to: message)
  240. }
  241. action.didChangeStateBlock = { _, active -> Void in
  242. if active {
  243. // Actuate `Peek` feedback (weak boom)
  244. AudioServicesPlaySystemSound(1519)
  245. }
  246. }
  247. let replyGestureRecognizer = DRCellSlideGestureRecognizer()
  248. self.replyGestureRecognizer = replyGestureRecognizer
  249. replyGestureRecognizer.leftActionStartPosition = 80
  250. replyGestureRecognizer.addActions(action)
  251. self.addGestureRecognizer(replyGestureRecognizer)
  252. }
  253. }
  254. func setDeliveryState(to deliveryState: ChatMessageDeliveryState) {
  255. self.statusView.subviews.forEach { $0.removeFromSuperview() }
  256. if deliveryState == .sending || deliveryState == .deleting {
  257. let activityIndicator = MDCActivityIndicator(frame: .init(x: 0, y: 0, width: 20, height: 20))
  258. activityIndicator.radius = 7.0
  259. activityIndicator.cycleColors = [.systemGray2]
  260. activityIndicator.startAnimating()
  261. activityIndicator.heightAnchor.constraint(equalToConstant: 20).isActive = true
  262. self.statusView.addArrangedSubview(activityIndicator)
  263. } else if deliveryState == .failed {
  264. let errorView = UIImageView(frame: .init(x: 0, y: 0, width: 20, height: 20))
  265. let errorImage = UIImage(systemName: "exclamationmark.circle")?.withTintColor(.red).withRenderingMode(.alwaysOriginal)
  266. errorView.image = errorImage
  267. errorView.contentMode = .scaleAspectFit
  268. errorView.heightAnchor.constraint(equalToConstant: 20).isActive = true
  269. self.statusView.addArrangedSubview(errorView)
  270. } else if deliveryState == .silent {
  271. let silentView = UIImageView(frame: .init(x: 0, y: 0, width: 20, height: 20))
  272. var silentImage = UIImage(systemName: "bell.slash")?.withTintColor(.systemGray2).withRenderingMode(.alwaysOriginal)
  273. silentImage = silentImage?.withConfiguration(UIImage.SymbolConfiguration(textStyle: .subheadline))
  274. silentView.image = silentImage
  275. silentView.contentMode = .center
  276. silentView.heightAnchor.constraint(equalToConstant: 20).isActive = true
  277. self.statusView.addArrangedSubview(silentView)
  278. } else if deliveryState == .sent || deliveryState == .read {
  279. var checkImageName = "check"
  280. if deliveryState == .read {
  281. checkImageName = "check-all"
  282. }
  283. let checkImage = UIImage(named: checkImageName)?.withRenderingMode(.alwaysTemplate)
  284. let checkView = UIImageView(frame: .init(x: 0, y: 0, width: 20, height: 20))
  285. checkView.image = checkImage
  286. checkView.contentMode = .scaleAspectFit
  287. checkView.tintColor = .systemGray2
  288. checkView.accessibilityIdentifier = "MessageSent"
  289. checkView.heightAnchor.constraint(equalToConstant: 20).isActive = true
  290. self.statusView.addArrangedSubview(checkView)
  291. }
  292. }
  293. // MARK: - QuotePart
  294. func showQuotePart() {
  295. self.quotePart.isHidden = false
  296. if self.quotedMessageView == nil {
  297. let quotedMessageView = QuotedMessageView()
  298. self.quotedMessageView = quotedMessageView
  299. quotedMessageView.translatesAutoresizingMaskIntoConstraints = false
  300. self.quotePart.addSubview(quotedMessageView)
  301. NSLayoutConstraint.activate([
  302. quotedMessageView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor),
  303. quotedMessageView.rightAnchor.constraint(equalTo: self.quotePart.rightAnchor, constant: -10),
  304. quotedMessageView.topAnchor.constraint(equalTo: self.quotePart.topAnchor),
  305. quotedMessageView.bottomAnchor.constraint(equalTo: self.quotePart.bottomAnchor)
  306. ])
  307. let quoteTap = UITapGestureRecognizer(target: self, action: #selector(quoteTapped(_:)))
  308. quotedMessageView.addGestureRecognizer(quoteTap)
  309. }
  310. }
  311. @objc func quoteTapped(_ sender: UITapGestureRecognizer?) {
  312. if let parent = self.message?.parent {
  313. self.delegate?.cellWantsToScroll(to: parent)
  314. }
  315. }
  316. // MARK: - ReferencePart
  317. func showReferencePart() {
  318. self.referencePart.isHidden = false
  319. if self.referenceView == nil {
  320. let referenceView = ReferenceView()
  321. self.referenceView = referenceView
  322. referenceView.translatesAutoresizingMaskIntoConstraints = false
  323. self.referencePart.addSubview(referenceView)
  324. NSLayoutConstraint.activate([
  325. referenceView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor),
  326. referenceView.rightAnchor.constraint(equalTo: self.referencePart.rightAnchor, constant: -10),
  327. referenceView.topAnchor.constraint(equalTo: self.referencePart.topAnchor),
  328. referenceView.bottomAnchor.constraint(equalTo: self.referencePart.bottomAnchor, constant: -5)
  329. ])
  330. }
  331. }
  332. // MARK: - ReactionsPart
  333. func showReactionsPart() {
  334. self.reactionPart.isHidden = false
  335. if self.reactionView == nil {
  336. let flowLayout = UICollectionViewFlowLayout()
  337. flowLayout.scrollDirection = .horizontal
  338. let reactionView = ReactionsView(frame: .init(x: 0, y: 0, width: 50, height: 40), collectionViewLayout: flowLayout)
  339. reactionView.reactionsDelegate = self
  340. self.reactionView = reactionView
  341. reactionView.translatesAutoresizingMaskIntoConstraints = false
  342. self.reactionPart.addSubview(reactionView)
  343. NSLayoutConstraint.activate([
  344. reactionView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor),
  345. reactionView.rightAnchor.constraint(equalTo: self.reactionPart.rightAnchor, constant: -10),
  346. reactionView.topAnchor.constraint(equalTo: self.reactionPart.topAnchor),
  347. reactionView.bottomAnchor.constraint(equalTo: self.reactionPart.bottomAnchor, constant: -10)
  348. ])
  349. }
  350. }
  351. // MARK: - ReactionsView Delegate
  352. func didSelectReaction(reaction: NCChatReaction) {
  353. if let message = self.message {
  354. self.delegate?.cellDidSelectedReaction(reaction, for: message)
  355. }
  356. }
  357. // MARK: - Avatar User Menu
  358. func getDeferredUserMenu() -> UIMenu? {
  359. guard let message = self.message else { return nil }
  360. let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
  361. if message.actorType != "users" || message.actorId == activeAccount.userId {
  362. return nil
  363. }
  364. // Use an uncached provider so local time is not cached
  365. let deferredMenuElement = UIDeferredMenuElement.uncached { completion in
  366. self.getMenuUserAction(for: message) { items in
  367. completion(items)
  368. }
  369. }
  370. return UIMenu(title: message.actorDisplayName, children: [deferredMenuElement])
  371. }
  372. func getMenuUserAction(for message: NCChatMessage, completionBlock: @escaping ([UIMenuElement]) -> Void) {
  373. let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
  374. NCAPIController.sharedInstance().getUserActions(forUser: message.actorId, using: activeAccount) { userActionsRaw, error in
  375. guard error == nil,
  376. let userActionsDict = userActionsRaw as? [String: AnyObject],
  377. let userActions = userActionsDict["actions"] as? [[String: String]],
  378. let userId = userActionsDict["userId"] as? String
  379. else {
  380. let errorAction = UIAction(title: NSLocalizedString("No actions available", comment: "")) { _ in }
  381. errorAction.attributes = .disabled
  382. completionBlock([errorAction])
  383. return
  384. }
  385. var menuItems: [UIMenuElement] = []
  386. for userAction in userActions {
  387. guard let appId = userAction["appId"],
  388. let title = userAction["title"],
  389. let link = userAction["hyperlink"],
  390. let linkEncoded = link.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
  391. else { continue }
  392. if appId == "spreed" {
  393. let talkAction = UIAction(title: title, image: UIImage(named: "talk-20")?.withRenderingMode(.alwaysTemplate)) { _ in
  394. NotificationCenter.default.post(name: NSNotification.Name.NCChatViewControllerTalkToUserNotification, object: self, userInfo: ["actorId": userId])
  395. }
  396. menuItems.append(talkAction)
  397. continue
  398. }
  399. let otherAction = UIAction(title: title) { _ in
  400. if let actionUrl = URL(string: linkEncoded) {
  401. UIApplication.shared.open(actionUrl)
  402. }
  403. }
  404. if appId == "profile" {
  405. otherAction.image = UIImage(systemName: "person")
  406. } else if appId == "email" {
  407. otherAction.image = UIImage(systemName: "envelope")
  408. } else if appId == "timezone" {
  409. otherAction.image = UIImage(systemName: "clock")
  410. } else if appId == "social" {
  411. otherAction.image = UIImage(systemName: "heart")
  412. }
  413. menuItems.append(otherAction)
  414. }
  415. completionBlock(menuItems)
  416. }
  417. }
  418. // MARK: - File status / activity indicator
  419. func clearFileStatusView() {
  420. self.fileActivityIndicator?.stopAnimating()
  421. self.fileActivityIndicator?.removeFromSuperview()
  422. self.fileActivityIndicator = nil
  423. }
  424. func addActivityIndicator(with progress: Float) {
  425. self.clearFileStatusView()
  426. let fileActivityIndicator = MDCActivityIndicator(frame: .init(x: 0, y: 0, width: 20, height: 20))
  427. self.fileActivityIndicator = fileActivityIndicator
  428. fileActivityIndicator.radius = 7
  429. fileActivityIndicator.cycleColors = [.systemGray2]
  430. if progress > 0 {
  431. fileActivityIndicator.indicatorMode = .determinate
  432. fileActivityIndicator.setProgress(progress, animated: false)
  433. }
  434. fileActivityIndicator.startAnimating()
  435. fileActivityIndicator.heightAnchor.constraint(equalToConstant: 20).isActive = true
  436. self.statusView.addArrangedSubview(fileActivityIndicator)
  437. }
  438. // MARK: - File notifications
  439. @objc func didChangeIsDownloading(notification: Notification) {
  440. DispatchQueue.main.async {
  441. // Make sure this notification is really for this cell
  442. guard let fileParameter = self.message?.file(),
  443. let receivedStatus = NCChatFileStatus.getStatus(from: notification, for: fileParameter)
  444. else { return }
  445. if receivedStatus.isDownloading, self.fileActivityIndicator == nil {
  446. // Immediately show an indeterminate indicator as long as we don't have a progress value
  447. self.addActivityIndicator(with: 0)
  448. } else if !receivedStatus.isDownloading, self.fileActivityIndicator != nil {
  449. self.clearFileStatusView()
  450. }
  451. }
  452. }
  453. @objc func didChangeDownloadProgress(notification: Notification) {
  454. DispatchQueue.main.async {
  455. // Make sure this notification is really for this cell
  456. guard let fileParameter = self.message?.file(),
  457. let receivedStatus = NCChatFileStatus.getStatus(from: notification, for: fileParameter)
  458. else { return }
  459. if self.fileActivityIndicator != nil {
  460. // Switch to determinate-mode and show progress
  461. if receivedStatus.canReportProgress {
  462. self.fileActivityIndicator?.indicatorMode = .determinate
  463. self.fileActivityIndicator?.setProgress(Float(receivedStatus.downloadProgress), animated: true)
  464. }
  465. } else {
  466. // Make sure we have an activity indicator added to this cell
  467. self.addActivityIndicator(with: Float(receivedStatus.downloadProgress))
  468. }
  469. }
  470. }
  471. }