ChatImageMessageCell.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  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 ChatImageMessageCell: ChatBlobTextMessageCell{
  23. override open var message: BaseMessage! {
  24. didSet {
  25. setBaseMessage(newMessage: message)
  26. }
  27. }
  28. private var _imageView: UIImageView?
  29. private var _imageIcon: UIImageView?
  30. @objc override public init!(style: UITableViewCell.CellStyle, reuseIdentifier: String!, transparent: Bool) {
  31. super.init(style: style, reuseIdentifier: reuseIdentifier, transparent: transparent)
  32. _imageView = UIImageView.init()
  33. _imageView?.clearsContextBeforeDrawing = false
  34. _imageView?.contentMode = .scaleAspectFit
  35. contentView.addSubview(_imageView!)
  36. _imageIcon = UIImageView.init()
  37. _imageIcon?.clearsContextBeforeDrawing = false
  38. contentView.addSubview(_imageIcon!)
  39. _captionLabel = ChatTextMessageCell.makeAttributedLabel(withFrame: self.bounds)
  40. _captionLabel?.tapDelegate = self
  41. _captionLabel?.longPressDelegate = self
  42. contentView.addSubview(_captionLabel!)
  43. if #available(iOS 11.0, *) {
  44. _imageView?.accessibilityIgnoresInvertColors = true
  45. }
  46. setupColors()
  47. }
  48. required public init?(coder: NSCoder) {
  49. fatalError("init(coder:) has not been implemented")
  50. }
  51. }
  52. extension ChatImageMessageCell {
  53. // MARK: Override functions
  54. @objc override open class func height(for message: BaseMessage!, forTableWidth tableWidth: CGFloat) -> CGFloat {
  55. let imageMessage = message as! ImageMessage
  56. var cellHeight: CGFloat = 40.0
  57. let imageInsets = UIEdgeInsets.init(top: 5, left: 5, bottom: 5, right: 5)
  58. var scaledSize = CGSize.init()
  59. if imageMessage.thumbnail != nil {
  60. if imageMessage.thumbnail.data != nil && imageMessage.thumbnail.height.floatValue > 0 {
  61. let width: CGFloat = CGFloat(imageMessage.thumbnail.width.floatValue)
  62. let height: CGFloat = CGFloat(imageMessage.thumbnail.height.floatValue)
  63. let size: CGSize = CGSize.init(width: width, height: height)
  64. scaledSize = ChatImageMessageCell.scaleImageSize(toCell: size, forTableWidth: tableWidth)
  65. if scaledSize.height != scaledSize.height || scaledSize.height < 0 {
  66. scaledSize.height = 40.0
  67. }
  68. cellHeight = scaledSize.height - 17.0
  69. }
  70. }
  71. if let image = imageMessage.image, let caption = image.getCaption(), caption.count > 0 {
  72. let x: CGFloat = 30.0
  73. let maxSize = CGSize.init(width: scaledSize.width - x, height: CGFloat.greatestFiniteMagnitude)
  74. var textSize: CGSize?
  75. let captionTextNSString = NSString.init(string: caption)
  76. if UserSettings.shared().disableBigEmojis && captionTextNSString.isOnlyEmojisMaxCount(3) {
  77. var dummyLabelEmoji: ZSWTappableLabel? = nil
  78. if dummyLabelEmoji == nil {
  79. dummyLabelEmoji = ChatTextMessageCell.makeAttributedLabel(withFrame: CGRect.init(x: (x/2), y: 0.0, width: maxSize.width, height: maxSize.height))
  80. }
  81. dummyLabelEmoji!.font = ChatTextMessageCell.emojiFont()
  82. dummyLabelEmoji?.attributedText = NSAttributedString.init(string: caption, attributes: [NSAttributedString.Key.font: ChatMessageCell.emojiFont()!])
  83. textSize = dummyLabelEmoji?.sizeThatFits(maxSize)
  84. textSize!.height = textSize!.height + 23.0
  85. } else {
  86. var dummyLabel: ZSWTappableLabel? = nil
  87. if dummyLabel == nil {
  88. dummyLabel = ChatTextMessageCell.makeAttributedLabel(withFrame: CGRect.init(x: (x/2), y: 0.0, width: maxSize.width, height: maxSize.height))
  89. }
  90. dummyLabel!.font = ChatTextMessageCell.textFont()
  91. let attributed = TextStyleUtils.makeAttributedString(from: caption, with: dummyLabel!.font, textColor: Colors.fontNormal(), isOwn: true, application: UIApplication.shared)
  92. let formattedAttributeString = NSMutableAttributedString.init(attributedString: (dummyLabel!.applyMarkup(for: attributed))!)
  93. dummyLabel?.attributedText = TextStyleUtils.makeMentionsAttributedString(for: formattedAttributeString, textFont: dummyLabel!.font!, at: dummyLabel!.textColor.withAlphaComponent(0.4), messageInfo: Int32(message.isOwn!.intValue), application: UIApplication.shared)
  94. textSize = dummyLabel?.sizeThatFits(maxSize)
  95. textSize!.height = textSize!.height + 23.0
  96. }
  97. cellHeight = cellHeight + textSize!.height
  98. } else {
  99. cellHeight += imageInsets.top + imageInsets.bottom
  100. }
  101. return cellHeight
  102. }
  103. override public func layoutSubviews() {
  104. let imageMessage = message as! ImageMessage
  105. var textSize: CGSize = CGSize.init(width: 0.0, height: 0.0)
  106. var size = CGSize.init(width: 80.0, height: 40.0)
  107. let x: CGFloat = 30.0
  108. let imageInsets = UIEdgeInsets.init(top: 5, left: 5, bottom: 5, right: 5)
  109. if imageMessage.thumbnail != nil {
  110. if imageMessage.thumbnail.data != nil && imageMessage.thumbnail.height.floatValue > 0 {
  111. let width: CGFloat = CGFloat(imageMessage.thumbnail.width.floatValue)
  112. let height: CGFloat = CGFloat(imageMessage.thumbnail.height.floatValue)
  113. let imageMessageSize: CGSize = CGSize.init(width: width, height: height)
  114. size = ChatFileImageMessageCell.scaleImageSize(toCell: imageMessageSize, forTableWidth: frame.size.width)
  115. if let image = imageMessage.image, let caption = image.getCaption(), caption.count > 0 {
  116. textSize = _captionLabel!.sizeThatFits(CGSize.init(width: size.width - x, height: CGFloat.greatestFiniteMagnitude))
  117. textSize.height = textSize.height + 12.0
  118. }
  119. let bubbleSize = CGSize.init(width: size.width + imageInsets.left + imageInsets.right, height: size.height + imageInsets.top + imageInsets.bottom + textSize.height)
  120. setBubble(bubbleSize)
  121. } else {
  122. if let image = imageMessage.image, let caption = image.getCaption(), caption.count > 0 {
  123. textSize = _captionLabel!.sizeThatFits(CGSize.init(width: size.width - x, height: CGFloat.greatestFiniteMagnitude))
  124. textSize.height = textSize.height + 12.0
  125. }
  126. setBubbleContentSize(CGSize.init(width: size.width, height: size.height + textSize.height))
  127. }
  128. } else {
  129. setBubbleContentSize(CGSize.init(width: size.width, height: size.height + textSize.height))
  130. }
  131. super.layoutSubviews()
  132. _imageView?.frame = CGRect.init(x: msgBackground.frame.origin.x + imageInsets.left, y: msgBackground.frame.origin.y + imageInsets.top, width: size.width, height: size.height)
  133. _captionLabel!.frame = CGRect.init(x:ceil(msgBackground.frame.origin.x + (x/2)), y: ceil(_imageView!.frame.origin.y + _imageView!.frame.size.height), width: ceil(textSize.width), height: ceil(textSize.height))
  134. let mask: CALayer = bubbleMaskWithoutArrow(forImageSize: CGSize.init(width: _imageView!.frame.size.width, height: _imageView!.frame.size.height))
  135. _imageView?.layer.mask = mask
  136. _imageView?.layer.masksToBounds = true
  137. if message.isOwn != nil, message.isOwn.boolValue {
  138. progressBar.frame = CGRect.init(x: _imageView!.frame.origin.x + 16.0, y: _imageView!.frame.origin.y + _imageView!.frame.size.height - 24.0, width: size.width - 32.0, height: 16.0)
  139. resendButton.frame = CGRect.init(x: _imageView!.frame.origin.x - kMessageScreenMargin, y: _imageView!.frame.origin.y + (_imageView!.frame.size.height - 32.0) / 2, width: 114.0, height: 32.0)
  140. } else {
  141. if imageMessage.thumbnail == nil {
  142. activityIndicator.frame = CGRect.init(x: 72.0 + contentLeftOffset(), y: 17.0, width: 21.0, height: 21.0)
  143. _imageIcon!.frame = CGRect.init(x: 26.0 + contentLeftOffset(), y: 19.0, width: 24.0, height: 18.0)
  144. } else {
  145. if imageMessage.thumbnail.data == nil {
  146. activityIndicator.frame = CGRect.init(x: 72.0 + contentLeftOffset(), y: 17.0, width: 21.0, height: 21.0)
  147. _imageIcon!.frame = CGRect.init(x: 26.0 + contentLeftOffset(), y: 19.0, width: 24.0, height: 18.0)
  148. }
  149. }
  150. }
  151. }
  152. override open func accessibilityLabelForContent() -> String! {
  153. if _captionLabel?.text != nil {
  154. return "\(BundleUtil.localizedString(forKey: "image") ?? "Image"). \(_captionLabel!.text!))"
  155. } else {
  156. return BundleUtil.localizedString(forKey: "image")
  157. }
  158. }
  159. override open func showActivityIndicator() -> Bool {
  160. return showProgressBar() == false
  161. }
  162. override open func showProgressBar() -> Bool {
  163. if message != nil {
  164. if message.isOwn != nil {
  165. return message.isOwn.boolValue
  166. }
  167. }
  168. return false
  169. }
  170. override open func messageTapped(_ sender: Any!) {
  171. let imageMessage = message as! ImageMessage
  172. if imageMessage.image == nil {
  173. // Not loaded yet. Should we start loading again?
  174. if imageMessage.progress == nil {
  175. let loader: BlobMessageLoader = BlobMessageLoader.init()
  176. loader.start(with: imageMessage, onCompletion: { (baseMessage) in
  177. DDLogInfo("File image message blob load completed")
  178. }) { (error) in
  179. DDLogInfo("File image message blob load failed with error: \(error!)")
  180. }
  181. }
  182. }
  183. chatVc.imageMessageTapped(imageMessage)
  184. }
  185. override open func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
  186. let imageMessage = message as! ImageMessage
  187. let mdmSetup = MDMSetup.init(setup: false)
  188. if action == #selector(resendMessage(_:)) && imageMessage.isOwn.boolValue && imageMessage.sendFailed.boolValue {
  189. return true
  190. }
  191. else if action == #selector(deleteMessage(_:)) && imageMessage.isOwn.boolValue && imageMessage.progress != nil {
  192. /* don't allow messages in progress to be deleted */
  193. return false
  194. }
  195. else if action == #selector(shareMessage(_:)) {
  196. if #available(iOS 13.0, *) {
  197. let mdmSetup = MDMSetup.init(setup: false)
  198. if mdmSetup?.disableShareMedia() == true {
  199. return false;
  200. }
  201. }
  202. return imageMessage.image != nil
  203. }
  204. else if action == #selector(forwardMessage(_:)) {
  205. if #available(iOS 13.0, *) {
  206. return imageMessage.image != nil
  207. } else {
  208. return false;
  209. }
  210. }
  211. else if action == #selector(copyMessage(_:)) && mdmSetup?.disableShareMedia() == true {
  212. return false
  213. }
  214. else if action == #selector(speakMessage(_:)) && _captionLabel?.text != nil {
  215. return true
  216. }
  217. else {
  218. return super.canPerformAction(action, withSender: sender)
  219. }
  220. }
  221. @objc override open func copyMessage(_ menuController: UIMenuController!) {
  222. let imageMessage = message as! ImageMessage
  223. if let image = imageMessage.image, let caption = image.getCaption(), caption.count > 0 {
  224. UIPasteboard.general.string = caption
  225. } else {
  226. if imageMessage.image != nil, imageMessage.image.data != nil {
  227. UIPasteboard.general.image = imageMessage.image.uiImage
  228. } else {
  229. if imageMessage.thumbnail != nil, imageMessage.thumbnail.data != nil {
  230. UIPasteboard.general.image = imageMessage.thumbnail.uiImage
  231. }
  232. }
  233. }
  234. }
  235. open override func textForQuote() -> String! {
  236. return (_captionLabel?.text as? String ?? "")
  237. }
  238. open override func performPlayActionForAccessibility() -> Bool {
  239. messageTapped(self)
  240. return true
  241. }
  242. open override func shouldHideBubbleBackground() -> Bool {
  243. return false
  244. }
  245. open override func previewViewController() -> UIViewController! {
  246. return chatVc.headerView.getPhotoBrowser(at: message, forPeeking: true)
  247. }
  248. open override func previewViewController(for previewingContext: UIViewControllerPreviewing!, viewControllerForLocation location: CGPoint) -> UIViewController! {
  249. if let controller = super.previewViewController(for: previewingContext, viewControllerForLocation: location) {
  250. return controller
  251. }
  252. return chatVc.headerView.getPhotoBrowser(at: message, forPeeking: true)
  253. }
  254. open override func setupColors() {
  255. super.setupColors()
  256. _captionLabel?.textColor = Colors.fontNormal()
  257. }
  258. @available(iOS 13.0, *)
  259. open override func getContextMenu(_ indexPath: IndexPath!, point: CGPoint) -> UIContextMenuConfiguration! {
  260. if self.isEditing == true {
  261. return nil
  262. }
  263. // returns nil if there is no link tapped
  264. if let menu = contextMenuForLink(indexPath, point: point) {
  265. return menu
  266. }
  267. let imageMessage = message as! ImageMessage
  268. if imageMessage.image != nil {
  269. if imageMessage.image.data != nil {
  270. let conf = UIContextMenuConfiguration.init(identifier: indexPath as NSIndexPath, previewProvider: { () -> UIViewController? in
  271. return self.previewViewController()
  272. }) { (suggestedActions) -> UIMenu? in
  273. var menuItems = super.contextMenuItems()!
  274. let saveImage = UIImage.init(systemName: "square.and.arrow.down.fill", compatibleWith: self.traitCollection)
  275. let saveAction = UIAction.init(title: BundleUtil.localizedString(forKey: "save"), image: saveImage, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { (action) in
  276. AlbumManager.shared.save(image: imageMessage.image.uiImage)
  277. }
  278. if self.message.isOwn.boolValue == true || self.chatVc.conversation.isGroup() == true {
  279. menuItems.insert(saveAction, at: 0)
  280. } else {
  281. menuItems.insert(saveAction, at: 1)
  282. }
  283. return UIMenu.init(title: "", image: nil, identifier: nil, options: .displayInline, children: menuItems as! [UIMenuElement])
  284. }
  285. return conf
  286. } else {
  287. return super.getContextMenu(indexPath, point: point)
  288. }
  289. } else {
  290. return super.getContextMenu(indexPath, point: point)
  291. }
  292. }
  293. }
  294. extension ChatImageMessageCell {
  295. // MARK: Public functions
  296. func setBaseMessage(newMessage: BaseMessage) {
  297. let imageMessage = newMessage as! ImageMessage
  298. super.message = newMessage
  299. if let thumb = imageMessage.thumbnail {
  300. if let data = thumb.data {
  301. _imageView?.image = UIImage.init(data: data)
  302. }
  303. }
  304. var size = CGSize.init(width: 80.0, height: 40.0)
  305. if imageMessage.thumbnail != nil {
  306. if imageMessage.thumbnail.data != nil && imageMessage.thumbnail.height.floatValue > 0 {
  307. let width: CGFloat = CGFloat(imageMessage.thumbnail.width.floatValue)
  308. let height: CGFloat = CGFloat(imageMessage.thumbnail.height.floatValue)
  309. let imageMessageSize: CGSize = CGSize.init(width: width, height: height)
  310. size = ChatImageMessageCell.scaleImageSize(toCell: imageMessageSize, forTableWidth: frame.size.width)
  311. }
  312. _imageView?.frame.size = size
  313. }
  314. if imageMessage.isOwn != nil, imageMessage.isOwn.boolValue {
  315. _imageView?.autoresizingMask = .flexibleLeftMargin
  316. _imageView?.isHidden = false
  317. _imageIcon?.isHidden = true
  318. } else {
  319. if imageMessage.thumbnail != nil{
  320. if imageMessage.thumbnail.data != nil {
  321. _imageView?.autoresizingMask = .flexibleLeftMargin
  322. _imageView?.isHidden = false
  323. _imageIcon?.isHidden = true
  324. } else {
  325. _imageView?.isHidden = true
  326. let image = BundleUtil.imageNamed("Landscape")
  327. _imageIcon!.image = image!.withTint(Colors.fontLight())
  328. _imageIcon?.alpha = 0.7
  329. _imageIcon?.isHidden = false
  330. }
  331. } else {
  332. _imageView?.isHidden = true
  333. let image = BundleUtil.imageNamed("Landscape")
  334. _imageIcon!.image = image!.withTint(Colors.fontLight())
  335. _imageIcon?.alpha = 0.7
  336. _imageIcon?.isHidden = false
  337. }
  338. }
  339. if let image = imageMessage.image, let captionText = image.getCaption(), captionText.count > 0 {
  340. _captionLabel?.font = ChatMessageCell.textFont()
  341. let attributed = TextStyleUtils.makeAttributedString(from: captionText, with: _captionLabel!.font, textColor: Colors.fontNormal(), isOwn: true, application: UIApplication.shared)
  342. let formattedAttributeString = NSMutableAttributedString.init(attributedString: (_captionLabel!.applyMarkup(for: attributed))!)
  343. _captionLabel?.attributedText = TextStyleUtils.makeMentionsAttributedString(for: formattedAttributeString, textFont: _captionLabel!.font!, at: _captionLabel!.textColor.withAlphaComponent(0.4), messageInfo: Int32(message.isOwn!.intValue), application: UIApplication.shared)
  344. _captionLabel?.isHidden = false
  345. }
  346. else {
  347. _captionLabel?.text = nil
  348. _captionLabel?.isHidden = true
  349. }
  350. setupColors()
  351. self.setNeedsLayout()
  352. }
  353. @objc func resendMessage(_ menuController: UIMenuController) {
  354. DDLogError("ImageMessages can not be resent anymore.")
  355. }
  356. @objc func speakMessage(_ menuController: UIMenuController) {
  357. if _captionLabel?.text != nil {
  358. let speakText = "\(BundleUtil.localizedString(forKey: "image") ?? "Image"). \(_captionLabel!.text!)"
  359. let utterance: AVSpeechUtterance = AVSpeechUtterance.init(string: speakText)
  360. let syn = AVSpeechSynthesizer.init()
  361. syn.speak(utterance)
  362. }
  363. }
  364. }