ChatFileVideoMessageCell.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2019-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. import Foundation
  21. import CocoaLumberjackSwift
  22. @objc open class ChatFileVideoMessageCell: ChatBlobTextMessageCell {
  23. override open var message: BaseMessage! {
  24. didSet {
  25. setBaseMessage(newMessage: message)
  26. }
  27. }
  28. private var _thumbnailView: UIImageView?
  29. private var _durationBackground: UIImageView?
  30. private var _downloadBackground: UIImageView?
  31. private var _playImageView: UIImageView?
  32. private var _durationLabel: UILabel?
  33. private var _downloadSizeLabel: UILabel?
  34. private var _observedMessages:[Data] = [Data]()
  35. @objc override public init!(style: UITableViewCell.CellStyle, reuseIdentifier: String!, transparent: Bool) {
  36. super.init(style: style, reuseIdentifier: reuseIdentifier, transparent: transparent)
  37. _thumbnailView = UIImageView.init()
  38. _thumbnailView!.clearsContextBeforeDrawing = false
  39. contentView.addSubview(_thumbnailView!)
  40. _durationBackground = UIImageView.init(image: UIImage.init(named: "VideoDurationBg")?.resizableImage(withCapInsets: UIEdgeInsets.init(top: 0, left: 32, bottom: 0, right: 0)))
  41. _durationBackground?.isOpaque = false
  42. _thumbnailView!.addSubview(_durationBackground!)
  43. _downloadBackground = UIImageView.init(image: UIImage.init(named: "VideoDownloadBg")?.resizableImage(withCapInsets: UIEdgeInsets.init(top: 0, left: 32, bottom: 0, right: 0)))
  44. _downloadBackground!.isOpaque = false
  45. _thumbnailView!.addSubview(_downloadBackground!)
  46. _durationLabel = UILabel.init()
  47. _durationLabel!.backgroundColor = .clear
  48. _durationLabel!.isOpaque = false
  49. _durationLabel!.font = UIFont.boldSystemFont(ofSize: 12)
  50. _durationLabel!.textColor = Colors.white()
  51. _durationLabel!.textAlignment = .right
  52. _durationBackground?.addSubview(_durationLabel!)
  53. _downloadSizeLabel = UILabel.init()
  54. _downloadSizeLabel!.backgroundColor = .clear
  55. _downloadSizeLabel!.isOpaque = false
  56. _downloadSizeLabel!.font = UIFont.boldSystemFont(ofSize: 12)
  57. _downloadSizeLabel!.textColor = Colors.white()
  58. _downloadSizeLabel!.textAlignment = .right
  59. _downloadSizeLabel!.adjustsFontSizeToFitWidth = true
  60. _downloadBackground!.addSubview(_downloadSizeLabel!)
  61. if #available(iOS 11.0, *) {
  62. _thumbnailView?.accessibilityIgnoresInvertColors = true
  63. }
  64. if #available(iOS 13.0, *) {
  65. _playImageView = UIImageView.init(image: BundleUtil.imageNamed("Play")?.withTintColor(Colors.white()))
  66. } else {
  67. _playImageView = UIImageView.init(image: BundleUtil.imageNamed("Play")?.withTint(Colors.white()))
  68. }
  69. _thumbnailView!.addSubview(_playImageView!)
  70. _captionLabel = ChatTextMessageCell.makeAttributedLabel(withFrame: self.bounds)
  71. _captionLabel?.tapDelegate = self
  72. _captionLabel?.longPressDelegate = self
  73. contentView.addSubview(_captionLabel!)
  74. setupColors()
  75. }
  76. required public init?(coder: NSCoder) {
  77. fatalError("init(coder:) has not been implemented")
  78. }
  79. }
  80. extension ChatFileVideoMessageCell {
  81. // MARK: Override functions
  82. @objc override open class func height(for message: BaseMessage!, forTableWidth tableWidth: CGFloat) -> CGFloat {
  83. let fileMessage = message as! FileMessage
  84. var cellHeight: CGFloat = 40.0
  85. let imageInsets = UIEdgeInsets.init(top: 5, left: 5, bottom: 5, right: 5)
  86. var scaledSize = CGSize.init()
  87. if fileMessage.thumbnail != nil {
  88. if fileMessage.thumbnail.data != nil && fileMessage.thumbnail.height.floatValue > 0 {
  89. let width: CGFloat = CGFloat(fileMessage.thumbnail.width.floatValue)
  90. let height: CGFloat = CGFloat(fileMessage.thumbnail.height.floatValue)
  91. let size: CGSize = CGSize.init(width: width, height: height)
  92. scaledSize = ChatFileVideoMessageCell.scaleImageSize(toCell: size, forTableWidth: tableWidth)
  93. if scaledSize.height != scaledSize.height || scaledSize.height < 0 {
  94. scaledSize.height = 120.0
  95. }
  96. cellHeight = scaledSize.height - 17.0
  97. }
  98. }
  99. if let caption = fileMessage.getCaption(), caption.count > 0 {
  100. let x: CGFloat = 30.0
  101. let maxSize = CGSize.init(width: scaledSize.width - x, height: CGFloat.greatestFiniteMagnitude)
  102. var textSize: CGSize?
  103. let captionTextNSString = NSString.init(string: caption)
  104. if UserSettings.shared().disableBigEmojis && captionTextNSString.isOnlyEmojisMaxCount(3) {
  105. var dummyLabelEmoji: ZSWTappableLabel? = nil
  106. if dummyLabelEmoji == nil {
  107. dummyLabelEmoji = ChatTextMessageCell.makeAttributedLabel(withFrame: CGRect.init(x: (x/2), y: 0.0, width: maxSize.width, height: maxSize.height))
  108. }
  109. dummyLabelEmoji!.font = ChatTextMessageCell.emojiFont()
  110. dummyLabelEmoji?.attributedText = NSAttributedString.init(string: caption, attributes: [NSAttributedString.Key.font: ChatMessageCell.emojiFont()!])
  111. textSize = dummyLabelEmoji?.sizeThatFits(maxSize)
  112. textSize!.height = textSize!.height + 18.0
  113. } else {
  114. var dummyLabel: ZSWTappableLabel? = nil
  115. if dummyLabel == nil {
  116. dummyLabel = ChatTextMessageCell.makeAttributedLabel(withFrame: CGRect.init(x: (x/2), y: 0.0, width: maxSize.width, height: maxSize.height))
  117. }
  118. dummyLabel!.font = ChatTextMessageCell.textFont()
  119. let attributed = TextStyleUtils.makeAttributedString(from: caption, with: dummyLabel!.font, textColor: Colors.fontNormal(), isOwn: true, application: UIApplication.shared)
  120. let formattedAttributeString = NSMutableAttributedString.init(attributedString: (dummyLabel!.applyMarkup(for: attributed))!)
  121. dummyLabel?.attributedText = TextStyleUtils.makeMentionsAttributedString(for: formattedAttributeString, textFont: dummyLabel!.font!, at: dummyLabel!.textColor.withAlphaComponent(0.4), messageInfo: Int32(message.isOwn!.intValue), application: UIApplication.shared)
  122. textSize = dummyLabel?.sizeThatFits(maxSize)
  123. textSize!.height = textSize!.height + 18.0
  124. }
  125. cellHeight = cellHeight + textSize!.height
  126. } else {
  127. cellHeight += imageInsets.top + imageInsets.bottom
  128. }
  129. return cellHeight
  130. }
  131. override public func layoutSubviews() {
  132. let fileMessage = message as! FileMessage
  133. let imageInsets = UIEdgeInsets.init(top: 5, left: 5, bottom: 5, right: 5)
  134. if (fileMessage.thumbnail != nil) {
  135. var size = CGSize.init(width: CGFloat(fileMessage.thumbnail.width.floatValue), height: CGFloat(fileMessage.thumbnail.height.floatValue))
  136. var textSize: CGSize = CGSize.init(width: 0.0, height: 0.0)
  137. let x: CGFloat = 30.0
  138. /* scale to fit maximum cell size */
  139. size = ChatFileVideoMessageCell.scaleImageSize(toCell: size, forTableWidth: frame.size.width)
  140. if size.height != size.height {
  141. size.height = 120.0
  142. }
  143. if size.width != size.width {
  144. size.width = 120.0
  145. }
  146. if let caption = fileMessage.getCaption(), caption.count > 0 {
  147. textSize = _captionLabel!.sizeThatFits(CGSize.init(width: size.width - x, height: CGFloat.greatestFiniteMagnitude))
  148. textSize.height = textSize.height + 12.0
  149. }
  150. let bubbleSize = CGSize.init(width: size.width + imageInsets.left + imageInsets.right, height: size.height + imageInsets.top + imageInsets.bottom + textSize.height)
  151. setBubble(bubbleSize)
  152. super.layoutSubviews()
  153. _thumbnailView!.frame = CGRect.init(x: msgBackground.frame.origin.x + imageInsets.left, y: msgBackground.frame.origin.y + imageInsets.top, width: size.width, height: size.height)
  154. _captionLabel!.frame = CGRect.init(x:ceil(msgBackground.frame.origin.x + (x/2)), y: ceil(_thumbnailView!.frame.origin.y + _thumbnailView!.frame.size.height), width: ceil(textSize.width), height: ceil(textSize.height))
  155. let mask: CALayer = bubbleMaskWithoutArrow(forImageSize: CGSize.init(width: _thumbnailView!.frame.size.width, height: _thumbnailView!.frame.size.height))
  156. _thumbnailView?.layer.mask = mask
  157. _thumbnailView?.layer.masksToBounds = true
  158. if fileMessage.isOwn != nil, fileMessage.isOwn.boolValue {
  159. resendButton.frame = CGRect.init(x: _thumbnailView!.frame.origin.x - kMessageScreenMargin, y: _thumbnailView!.frame.origin.y + (_thumbnailView!.frame.size.height - 32) / 2, width: 114, height: 32)
  160. }
  161. progressBar.frame = CGRect.init(x: _thumbnailView!.frame.origin.x + 16.0, y: _thumbnailView!.frame.origin.y + _thumbnailView!.frame.size.height - 40.0, width: size.width - 32.0, height: 16.0)
  162. /* duration label */
  163. _durationBackground!.frame = CGRect.init(x: 0, y: _thumbnailView!.frame.size.height - 22.0, width: _thumbnailView!.frame.size.width + 1, height: 18.0)
  164. _durationLabel!.frame = CGRect.init(x: _durationBackground!.frame.size.width / 2, y: 0, width: _durationBackground!.frame.size.width / 2 - 12, height: 16.0)
  165. /* download size label */
  166. _downloadBackground!.frame = CGRect.init(x: 0, y: 1, width: _thumbnailView!.frame.size.width + 1, height: 18.0)
  167. _downloadSizeLabel!.frame = CGRect.init(x: _downloadBackground!.frame.size.width / 2, y: 1, width: _downloadBackground!.frame.size.width / 2 - 12, height: 16.0)
  168. if bubbleSize.height > 44.0 && bubbleSize.width > 44.0 {
  169. _playImageView!.frame = CGRect.init(x: (_thumbnailView!.frame.size.width / 2) - 22.0, y: (_thumbnailView!.frame.size.height / 2) - 22.0 - 2.0, width: 44.0, height: 44.0)
  170. } else {
  171. var min = Swift.min(bubbleSize.width, bubbleSize.height)
  172. min = min - 20.0
  173. _playImageView!.frame = CGRect.init(x: (bubbleSize.width / 2) - (min/2), y: (bubbleSize.height / 2) - (min/2) - 2.0, width: min, height: min)
  174. }
  175. } else {
  176. var textSize: CGSize = CGSize.init(width: 0.0, height: 0.0)
  177. let size = CGSize.init(width: 80.0, height: 40.0)
  178. let x: CGFloat = 30.0
  179. if let caption = fileMessage.getCaption(), caption.count > 0 {
  180. textSize = _captionLabel!.sizeThatFits(CGSize.init(width: size.width - x, height: CGFloat.greatestFiniteMagnitude))
  181. textSize.height = textSize.height + 12.0
  182. }
  183. setBubbleContentSize(CGSize.init(width: size.width, height: size.height + textSize.height))
  184. }
  185. }
  186. public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
  187. super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
  188. if let objerveObject = object as? BaseMessage {
  189. if objerveObject == message && keyPath == "data" {
  190. updateDownloadSize()
  191. }
  192. }
  193. }
  194. override open func accessibilityLabelForContent() -> String! {
  195. let fileMessage = message as! FileMessage
  196. let preText = "\(BundleUtil.localizedString(forKey: "video")!), \(fileMessage.getDuration()?.intValue ?? 0) \(BundleUtil.localizedString(forKey: "seconds")!)"
  197. if _captionLabel?.text != nil {
  198. return "\(preText). \(_captionLabel!.text!))"
  199. }
  200. return preText
  201. }
  202. override open func messageTapped(_ sender: Any!) {
  203. let fileMessage = message as! FileMessage
  204. chatVc.fileVideoMessageTapped(fileMessage)
  205. }
  206. override open func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
  207. let fileMessage = message as! FileMessage
  208. if action == #selector(resendMessage(_:)) && fileMessage.isOwn.boolValue && fileMessage.sendFailed.boolValue {
  209. return true
  210. }
  211. else if action == #selector(deleteMessage(_:)) && fileMessage.isOwn.boolValue && fileMessage.progress != nil {
  212. /* don't allow messages in progress to be deleted */
  213. return false
  214. }
  215. else if action == #selector(copyMessage(_:)) && _captionLabel?.text != nil {
  216. return true
  217. }
  218. else if action == #selector(shareMessage(_:)) {
  219. if #available(iOS 13.0, *) {
  220. let mdmSetup = MDMSetup.init(setup: false)
  221. if mdmSetup?.disableShareMedia() == true {
  222. return false;
  223. }
  224. }
  225. return fileMessage.data != nil
  226. }
  227. else if action == #selector(forwardMessage(_:)) {
  228. if #available(iOS 13.0, *) {
  229. return fileMessage.data != nil
  230. } else {
  231. return false;
  232. }
  233. }
  234. else if action == #selector(speakMessage(_:)) && _captionLabel?.text != nil {
  235. return true
  236. }
  237. else {
  238. return super.canPerformAction(action, withSender: sender)
  239. }
  240. }
  241. @objc override open func copyMessage(_ menuController: UIMenuController!) {
  242. let fileMessage = message as! FileMessage
  243. if let caption = fileMessage.getCaption(), caption.count > 0 {
  244. UIPasteboard.general.string = fileMessage.getCaption()
  245. }
  246. }
  247. open override func performPlayActionForAccessibility() -> Bool {
  248. messageTapped(self)
  249. return true
  250. }
  251. open override func previewViewController() -> UIViewController! {
  252. return chatVc.headerView.getPhotoBrowser(at: message, forPeeking: true)
  253. }
  254. open override func previewViewController(for previewingContext: UIViewControllerPreviewing!, viewControllerForLocation location: CGPoint) -> UIViewController! {
  255. if let controller = super.previewViewController(for: previewingContext, viewControllerForLocation: location) {
  256. return controller
  257. }
  258. return chatVc.headerView.getPhotoBrowser(at: message, forPeeking: true)
  259. }
  260. open override func setupColors() {
  261. super.setupColors()
  262. _captionLabel?.textColor = Colors.fontNormal()
  263. }
  264. @available(iOS 13.0, *)
  265. open override func getContextMenu(_ indexPath: IndexPath!, point: CGPoint) -> UIContextMenuConfiguration! {
  266. if self.isEditing == true {
  267. return nil
  268. }
  269. // returns nil if there is no link tapped
  270. if let menu = contextMenuForLink(indexPath, point: point) {
  271. return menu
  272. }
  273. let fileMessage = message as! FileMessage
  274. if fileMessage.data != nil {
  275. if fileMessage.data.data != nil {
  276. let conf = UIContextMenuConfiguration.init(identifier: indexPath as NSIndexPath, previewProvider: { () -> UIViewController? in
  277. return self.previewViewController()
  278. }) { (suggestedActions) -> UIMenu? in
  279. var menuItems = super.contextMenuItems()!
  280. let saveImage = UIImage.init(systemName: "square.and.arrow.down.fill", compatibleWith: self.traitCollection)
  281. let saveAction = UIAction.init(title: BundleUtil.localizedString(forKey: "save"), image: saveImage, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { (action) in
  282. let fileName = String.init(format: "%f.%@", Date().timeIntervalSinceReferenceDate, MEDIA_EXTENSION_VIDEO)
  283. let tmpurl = URL.init(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
  284. do {
  285. try fileMessage.data.data.write(to: tmpurl)
  286. AlbumManager.shared.saveMovieToLibrary(movieURL: tmpurl) { (success) in
  287. do {
  288. try FileManager.default.removeItem(atPath: tmpurl.path)
  289. } catch {
  290. DDLogWarn("Remove moviefile to temporary file failed")
  291. }
  292. }
  293. } catch {
  294. DDLogWarn("Writing moviefile to temporary file failed")
  295. }
  296. }
  297. if self.message.isOwn.boolValue == true || self.chatVc.conversation.isGroup() == true {
  298. menuItems.insert(saveAction, at: 0)
  299. } else {
  300. menuItems.insert(saveAction, at: 1)
  301. }
  302. return UIMenu.init(title: "", image: nil, identifier: nil, options: .displayInline, children: menuItems as! [UIMenuElement])
  303. }
  304. return conf
  305. } else {
  306. return super.getContextMenu(indexPath, point: point)
  307. }
  308. } else {
  309. return super.getContextMenu(indexPath, point: point)
  310. }
  311. }
  312. }
  313. extension ChatFileVideoMessageCell {
  314. // MARK: Public functions
  315. func setBaseMessage(newMessage: BaseMessage) {
  316. if message != nil {
  317. if let index = _observedMessages.firstIndex(of: message.id) {
  318. message.removeObserver(self, forKeyPath: "data")
  319. _observedMessages.remove(at: index)
  320. }
  321. }
  322. let fileMessage = newMessage as! FileMessage
  323. super.message = newMessage
  324. if !chatVc.isOpenWithForceTouch {
  325. _observedMessages.append(message.id)
  326. message.addObserver(self, forKeyPath: "data", options: [], context: nil)
  327. }
  328. if fileMessage.thumbnail != nil, let thumb = fileMessage.thumbnail.uiImage {
  329. _thumbnailView?.image = thumb
  330. }
  331. var autoresizingMask: AutoresizingMask = .flexibleRightMargin
  332. if fileMessage.isOwn.boolValue {
  333. autoresizingMask = .flexibleLeftMargin
  334. }
  335. _thumbnailView?.autoresizingMask = autoresizingMask
  336. _durationBackground?.autoresizingMask = autoresizingMask
  337. _durationLabel?.autoresizingMask = autoresizingMask
  338. _downloadBackground?.autoresizingMask = autoresizingMask
  339. _downloadSizeLabel?.autoresizingMask = autoresizingMask
  340. if let seconds = fileMessage.getDuration()?.intValue {
  341. _durationLabel?.text = DateFormatter.timeFormatted(seconds)
  342. } else {
  343. _durationLabel?.text = nil
  344. }
  345. _downloadSizeLabel!.text = Utils.formatDataLength(CGFloat(fileMessage.fileSize!.floatValue))
  346. updateDownloadSize()
  347. if let captionText = fileMessage.getCaption(), captionText.count > 0, fileMessage.shouldShowCaption() {
  348. let attributed = TextStyleUtils.makeAttributedString(from: captionText, with: _captionLabel!.font, textColor: Colors.fontNormal(), isOwn: true, application: UIApplication.shared)
  349. let formattedAttributeString = NSMutableAttributedString.init(attributedString: (_captionLabel!.applyMarkup(for: attributed))!)
  350. _captionLabel?.attributedText = TextStyleUtils.makeMentionsAttributedString(for: formattedAttributeString, textFont: _captionLabel!.font!, at: _captionLabel!.textColor.withAlphaComponent(0.4), messageInfo: Int32(message.isOwn!.intValue), application: UIApplication.shared)
  351. _captionLabel?.isHidden = false
  352. }
  353. else {
  354. _captionLabel?.text = nil
  355. _captionLabel?.isHidden = true
  356. }
  357. setupColors()
  358. self.setNeedsLayout()
  359. }
  360. @objc func resendMessage(_ menuController: UIMenuController) {
  361. let fileMessage = message as! FileMessage
  362. let sender: FileMessageSender = FileMessageSender.init()
  363. sender.retryMessage(fileMessage)
  364. }
  365. @objc func speakMessage(_ menuController: UIMenuController) {
  366. if _captionLabel?.text != nil {
  367. let speakText = "\(BundleUtil.localizedString(forKey: "image") ?? "Image"). \(_captionLabel!.text!)"
  368. let utterance: AVSpeechUtterance = AVSpeechUtterance.init(string: speakText)
  369. let syn = AVSpeechSynthesizer.init()
  370. syn.speak(utterance)
  371. }
  372. }
  373. }
  374. extension ChatFileVideoMessageCell {
  375. // MARK: Private functions
  376. private func updateDownloadSize() {
  377. let fileMessage = message as! FileMessage
  378. if fileMessage.data != nil {
  379. self._downloadBackground?.image = UIImage.init(named: "VideoDownloadBgDownloaded")?.resizableImage(withCapInsets: UIEdgeInsets.init(top: 0, left: 32, bottom: 0, right: 0))
  380. } else {
  381. self._downloadBackground?.image = UIImage.init(named: "VideoDownloadBg")?.resizableImage(withCapInsets: UIEdgeInsets.init(top: 0, left: 32, bottom: 0, right: 0))
  382. }
  383. }
  384. }