ChatFileAudioMessageCell.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  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 UIKit
  22. import CocoaLumberjackSwift
  23. @objc open class ChatFileAudioMessageCell: ChatBlobTextMessageCell {
  24. override open var message: BaseMessage! {
  25. didSet {
  26. setBaseMessage(newMessage: message)
  27. }
  28. }
  29. private var _audioIcon: UIImageView?
  30. private var _durationLabel: UILabel?
  31. @objc override public init!(style: UITableViewCell.CellStyle, reuseIdentifier: String!, transparent: Bool) {
  32. super.init(style: style, reuseIdentifier: reuseIdentifier, transparent: transparent)
  33. _audioIcon = UIImageView.init(image: BundleUtil.imageNamed("Microphone"))
  34. contentView.addSubview(_audioIcon!)
  35. _durationLabel = UILabel.init()
  36. _durationLabel?.clearsContextBeforeDrawing = false
  37. _durationLabel?.backgroundColor = .clear
  38. _durationLabel?.numberOfLines = 0
  39. _durationLabel?.lineBreakMode = .byWordWrapping
  40. _durationLabel?.font = ChatFileAudioMessageCell.textFont()
  41. contentView.addSubview(_durationLabel!)
  42. _captionLabel = ChatTextMessageCell.makeAttributedLabel(withFrame: self.bounds)
  43. _captionLabel?.tapDelegate = self
  44. _captionLabel?.longPressDelegate = self
  45. contentView.addSubview(_captionLabel!)
  46. if #available(iOS 11.0, *) {
  47. _audioIcon?.accessibilityIgnoresInvertColors = true
  48. }
  49. setupColors()
  50. }
  51. required public init?(coder: NSCoder) {
  52. fatalError("init(coder:) has not been implemented")
  53. }
  54. public class func displayText(fileMessage: FileMessage) -> String {
  55. if let seconds = fileMessage.getDuration() {
  56. return Utils.timeString(forSeconds: seconds.intValue)
  57. }
  58. return "0:00"
  59. }
  60. }
  61. extension ChatFileAudioMessageCell {
  62. // MARK: Private functions
  63. private func updateView() {
  64. let fileMessage = message as! FileMessage
  65. let displayText = ChatFileAudioMessageCell.displayText(fileMessage: fileMessage)
  66. let autoresizingMask: UIView.AutoresizingMask = fileMessage.isOwn.boolValue ? .flexibleLeftMargin : .flexibleRightMargin
  67. _durationLabel?.autoresizingMask = autoresizingMask
  68. _audioIcon?.autoresizingMask = autoresizingMask
  69. _captionLabel?.autoresizingMask = autoresizingMask
  70. if var captionText = fileMessage.getCaption(), captionText.count > 0, fileMessage.shouldShowCaption() {
  71. captionText = TextStyleUtils.makeMentionsString(forText: captionText)
  72. _captionLabel?.text = captionText
  73. _captionLabel?.isHidden = false
  74. }
  75. else {
  76. _captionLabel?.text = nil
  77. _captionLabel?.isHidden = true
  78. }
  79. setupColors()
  80. setNeedsLayout()
  81. _durationLabel?.text = displayText
  82. }
  83. private func updateActivityIndicator() {
  84. let fileMessage = message as! FileMessage
  85. if fileMessage.isOwn != nil, fileMessage.isOwn.boolValue {
  86. if fileMessage.sent.boolValue || fileMessage.sendFailed.boolValue {
  87. activityIndicator.stopAnimating()
  88. _audioIcon?.isHidden = false
  89. } else {
  90. activityIndicator.startAnimating()
  91. _audioIcon?.isHidden = true
  92. }
  93. } else {
  94. if fileMessage.data != nil {
  95. activityIndicator.stopAnimating()
  96. _audioIcon?.isHidden = false
  97. } else {
  98. if fileMessage.progress != nil {
  99. activityIndicator.startAnimating()
  100. _audioIcon?.isHidden = true
  101. } else {
  102. activityIndicator.stopAnimating()
  103. _audioIcon?.isHidden = false
  104. }
  105. }
  106. }
  107. }
  108. }
  109. extension ChatFileAudioMessageCell {
  110. // MARK: Override functions
  111. @objc override open class func height(for message: BaseMessage!, forTableWidth tableWidth: CGFloat) -> CGFloat {
  112. let fileMessage = message as! FileMessage
  113. let text = ChatFileAudioMessageCell.displayText(fileMessage: fileMessage)
  114. let size = text.sizeOfString(maxWidth: ChatFileAudioMessageCell.maxContentWidth(forTableWidth: tableWidth) - 25, font: ChatFileAudioMessageCell.textFont())
  115. var cellHeight = CGFloat(ceilf(Float(size.height)))
  116. if let caption = fileMessage.getCaption(), caption.count > 0 {
  117. let x: CGFloat = 30.0
  118. let maxSize = CGSize.init(width: ChatFileAudioMessageCell.maxContentWidth(forTableWidth: tableWidth) - x, height: CGFloat.greatestFiniteMagnitude)
  119. var textSize: CGSize?
  120. let captionTextNSString = NSString.init(string: caption)
  121. if UserSettings.shared().disableBigEmojis && captionTextNSString.isOnlyEmojisMaxCount(3) {
  122. var dummyLabelEmoji: ZSWTappableLabel? = nil
  123. if dummyLabelEmoji == nil {
  124. dummyLabelEmoji = ChatTextMessageCell.makeAttributedLabel(withFrame: CGRect.init(x: (x/2), y: 0.0, width: maxSize.width, height: maxSize.height))
  125. }
  126. dummyLabelEmoji!.font = ChatTextMessageCell.emojiFont()
  127. dummyLabelEmoji?.attributedText = NSAttributedString.init(string: caption, attributes: [NSAttributedString.Key.font: ChatMessageCell.emojiFont()!])
  128. textSize = dummyLabelEmoji?.sizeThatFits(maxSize)
  129. textSize!.height = textSize!.height + 12.0
  130. } else {
  131. var dummyLabel: ZSWTappableLabel? = nil
  132. if dummyLabel == nil {
  133. dummyLabel = ChatTextMessageCell.makeAttributedLabel(withFrame: CGRect.init(x: (x/2), y: 0.0, width: maxSize.width, height: maxSize.height))
  134. }
  135. dummyLabel!.font = ChatTextMessageCell.textFont()
  136. let attributed = TextStyleUtils.makeAttributedString(from: caption, with: dummyLabel!.font, textColor: Colors.fontNormal(), isOwn: true, application: UIApplication.shared)
  137. let formattedAttributeString = NSMutableAttributedString.init(attributedString: (dummyLabel!.applyMarkup(for: attributed))!)
  138. dummyLabel?.attributedText = TextStyleUtils.makeMentionsAttributedString(for: formattedAttributeString, textFont: dummyLabel!.font!, at: dummyLabel!.textColor.withAlphaComponent(0.4), messageInfo: Int32(message.isOwn!.intValue), application: UIApplication.shared)
  139. textSize = dummyLabel?.sizeThatFits(maxSize)
  140. textSize!.height = textSize!.height + 12.0
  141. }
  142. cellHeight = cellHeight + textSize!.height
  143. }
  144. return max(cellHeight, 34.0)
  145. }
  146. override open func setupColors() {
  147. super.setupColors()
  148. if #available(iOS 13.0, *) {
  149. _audioIcon?.image = BundleUtil.imageNamed("Microphone")?.withTintColor(Colors.fontNormal())
  150. } else {
  151. _audioIcon?.image = BundleUtil.imageNamed("Microphone")?.withTint(Colors.fontNormal())
  152. }
  153. _durationLabel?.textColor = Colors.fontNormal()
  154. _captionLabel?.textColor = Colors.fontNormal()
  155. }
  156. override public func layoutSubviews() {
  157. let fileMessage = message as! FileMessage
  158. let x: CGFloat = 30.0
  159. var messageTextWidth: CGFloat = 0.0
  160. var captionTextSize: CGSize = CGSize.init(width: 0.0, height: 0.0)
  161. if #available(iOS 11.0, *) {
  162. messageTextWidth = ChatMessageCell.maxContentWidth(forTableWidth: safeAreaLayoutGuide.layoutFrame.size.width)
  163. } else {
  164. messageTextWidth = ChatMessageCell.maxContentWidth(forTableWidth: frame.size.width)
  165. }
  166. if let caption = fileMessage.getCaption(), caption.count > 0 {
  167. captionTextSize = _captionLabel!.sizeThatFits(CGSize.init(width: messageTextWidth - x, height: CGFloat.greatestFiniteMagnitude))
  168. }
  169. let textSize = _durationLabel?.text?.sizeOfString(maxWidth: messageTextWidth - 25, font: ChatMessageCell.textFont())
  170. var cellSize = CGSize.init(width: CGFloat(ceilf(Float(max(textSize!.width, captionTextSize.width)))), height: CGFloat(ceilf(Float(max(34.0, textSize!.height) + captionTextSize.height))))
  171. if fileMessage.getCaption() == nil {
  172. cellSize.width = cellSize.width + 25.0
  173. }
  174. let size = CGSize.init(width: cellSize.width, height: cellSize.height)
  175. setBubbleContentSize(size)
  176. super.layoutSubviews()
  177. var textY: CGFloat = 7.0
  178. if textSize!.height < 34.0 {
  179. textY += (34.0 - textSize!.height) / 2;
  180. }
  181. if fileMessage.isOwn != nil, fileMessage.isOwn.boolValue {
  182. _durationLabel?.frame = CGRect.init(x: ceil((msgBackground.frame.origin.x + (size.width / 2)) + 5.0), y: textY, width: floor(cellSize.width + 1), height: floor(textSize!.height + 1))
  183. _audioIcon!.frame = CGRect.init(x: ceil((msgBackground.frame.origin.x + (size.width / 2)) - _audioIcon!.frame.size.width - 5.0), y: (_durationLabel!.frame.origin.y + _durationLabel!.frame.size.height/2) - _audioIcon!.frame.size.height / 2, width: _audioIcon!.frame.size.width, height: _audioIcon!.frame.size.height)
  184. resendButton.frame = CGRect.init(x: contentView.frame.size.width - size.width - 160.0 - statusImage.frame.size.width, y: 7 + (size.height - 32) / 2, width: 114.0, height: 32.0)
  185. } else {
  186. _durationLabel?.frame = CGRect.init(x: 46.0 + contentLeftOffset(), y: textY, width: floor(textSize!.width + 1), height: floor(textSize!.height + 1))
  187. _audioIcon!.frame = CGRect.init(x: 23.0 + contentLeftOffset(), y: (_durationLabel!.frame.origin.y + _durationLabel!.frame.size.height/2) - _audioIcon!.frame.size.height / 2, width: _audioIcon!.frame.size.width, height: _audioIcon!.frame.size.height)
  188. }
  189. _captionLabel!.frame = CGRect.init(x:ceil(msgBackground.frame.origin.x + (x/2)), y: ceil(_durationLabel!.frame.origin.y + _durationLabel!.frame.size.height + 3.0), width: ceil(captionTextSize.width), height: ceil(captionTextSize.height))
  190. activityIndicator.frame = _audioIcon!.frame
  191. }
  192. override open func accessibilityLabelForContent() -> String! {
  193. let fileMessage = message as! FileMessage
  194. let duration = Utils.accessabilityTimeString(forSeconds: fileMessage.getDuration()!.intValue)
  195. let durationText = "\(BundleUtil.localizedString(forKey: "audio") ?? "Audio"), \(duration!)"
  196. if _captionLabel?.text != nil {
  197. return "\(durationText). \(_captionLabel!.text!)"
  198. } else {
  199. return durationText
  200. }
  201. }
  202. override open func showActivityIndicator() -> Bool {
  203. return showProgressBar() == false
  204. }
  205. override open func showProgressBar() -> Bool {
  206. return false
  207. }
  208. override open func updateProgress() {
  209. updateActivityIndicator()
  210. }
  211. override open func messageTapped(_ sender: Any!) {
  212. let fileMessage = message as! FileMessage
  213. if fileMessage.data == nil {
  214. // Not loaded yet. Should we start loading again?
  215. if fileMessage.progress == nil {
  216. let loader: BlobMessageLoader = BlobMessageLoader.init()
  217. loader.start(with: fileMessage, onCompletion: { (baseMessage) in
  218. DDLogInfo("File audio message blob load completed")
  219. }) { (error) in
  220. DDLogInfo("File audio message blob load failed with error: \(error!)")
  221. }
  222. }
  223. }
  224. chatVc.fileAudioMessageTapped(fileMessage)
  225. }
  226. override open func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
  227. let fileMessage = message as! FileMessage
  228. if action == #selector(resendMessage(_:)) && fileMessage.isOwn.boolValue && fileMessage.sendFailed.boolValue {
  229. return true
  230. }
  231. else if action == #selector(deleteMessage(_:)) && fileMessage.isOwn.boolValue && !fileMessage.sent.boolValue && !fileMessage.sendFailed.boolValue {
  232. /* don't allow messages in progress to be deleted */
  233. return false
  234. }
  235. else if action == #selector(shareMessage(_:)) {
  236. if #available(iOS 13.0, *) {
  237. let mdmSetup = MDMSetup.init(setup: false)
  238. if mdmSetup?.disableShareMedia() == true {
  239. return false;
  240. }
  241. }
  242. return fileMessage.data != nil
  243. }
  244. else if action == #selector(forwardMessage(_:)) {
  245. if #available(iOS 13.0, *) {
  246. return fileMessage.data != nil
  247. } else {
  248. return false;
  249. }
  250. }
  251. else if action == #selector(copyMessage(_:)) && _captionLabel?.text == nil {
  252. return false
  253. }
  254. else if action == #selector(speakMessage(_:)) && _captionLabel?.text != nil {
  255. return true
  256. }
  257. else {
  258. return super.canPerformAction(action, withSender: sender)
  259. }
  260. }
  261. @objc override open func copyMessage(_ menuController: UIMenuController!) {
  262. let fileMessage = message as! FileMessage
  263. if let caption = fileMessage.getCaption(), caption.count > 0 {
  264. UIPasteboard.general.string = fileMessage.getCaption()
  265. }
  266. }
  267. open override func textForQuote() -> String! {
  268. return (_captionLabel?.text as? String ?? "")
  269. }
  270. open override func performPlayActionForAccessibility() -> Bool {
  271. messageTapped(self)
  272. return true
  273. }
  274. open override func previewViewController(for previewingContext: UIViewControllerPreviewing!, viewControllerForLocation location: CGPoint) -> UIViewController! {
  275. if let controller = super.previewViewController(for: previewingContext, viewControllerForLocation: location) {
  276. return controller
  277. }
  278. return chatVc.headerView.getPhotoBrowser(at: message, forPeeking: true)
  279. }
  280. @available(iOS 13.0, *)
  281. open override func getContextMenu(_ indexPath: IndexPath!, point: CGPoint) -> UIContextMenuConfiguration! {
  282. if self.isEditing == true {
  283. return nil
  284. }
  285. // returns nil if there is no link tapped
  286. if let menu = contextMenuForLink(indexPath, point: point) {
  287. return menu
  288. }
  289. return super.getContextMenu(indexPath, point: point)
  290. }
  291. }
  292. extension ChatFileAudioMessageCell {
  293. // MARK: Public functions
  294. func setBaseMessage(newMessage: BaseMessage) {
  295. super.message = newMessage
  296. self.updateView()
  297. }
  298. @objc func resendMessage(_ menuController: UIMenuController) {
  299. let fileMessage = message as! FileMessage
  300. let sender: FileMessageSender = FileMessageSender.init()
  301. sender.retryMessage(fileMessage)
  302. }
  303. @objc func speakMessage(_ menuController: UIMenuController) {
  304. if _captionLabel?.text != nil {
  305. let speakText = "\(BundleUtil.localizedString(forKey: "image") ?? "Image"). \(_captionLabel!.text!)"
  306. let utterance: AVSpeechUtterance = AVSpeechUtterance.init(string: speakText)
  307. let syn = AVSpeechSynthesizer.init()
  308. syn.speak(utterance)
  309. }
  310. }
  311. }
  312. extension String {
  313. func sizeOfString(maxWidth:CGFloat, font: UIFont) -> CGSize {
  314. let tmp = NSMutableAttributedString(string: self, attributes:[NSAttributedString.Key.font:font])
  315. let limitSize = CGSize(width: maxWidth, height: CGFloat(MAXFLOAT))
  316. let contentSize = tmp.boundingRect(with: limitSize, options: .usesLineFragmentOrigin, context: nil)
  317. return contentSize.size
  318. }
  319. }