BaseMessageCell.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. import UIKit
  2. import DcCore
  3. public class BaseMessageCell: UITableViewCell {
  4. private var leadingConstraint: NSLayoutConstraint?
  5. private var trailingConstraint: NSLayoutConstraint?
  6. private var leadingConstraintCurrentSender: NSLayoutConstraint?
  7. private var leadingConstraintGroup: NSLayoutConstraint?
  8. private var trailingConstraintCurrentSender: NSLayoutConstraint?
  9. private var mainContentBelowTopLabelConstraint: NSLayoutConstraint?
  10. private var mainContentUnderTopLabelConstraint: NSLayoutConstraint?
  11. private var mainContentAboveBottomLabelConstraint: NSLayoutConstraint?
  12. private var mainContentUnderBottomLabelConstraint: NSLayoutConstraint?
  13. private var mainContentViewLeadingConstraint: NSLayoutConstraint?
  14. private var mainContentViewTrailingConstraint: NSLayoutConstraint?
  15. public var mainContentViewHorizontalPadding: CGFloat {
  16. set {
  17. mainContentViewLeadingConstraint?.constant = newValue
  18. mainContentViewTrailingConstraint?.constant = -newValue
  19. }
  20. get {
  21. return mainContentViewLeadingConstraint?.constant ?? 0
  22. }
  23. }
  24. // if set to true topLabel overlaps the main content
  25. public var topCompactView: Bool {
  26. set {
  27. mainContentBelowTopLabelConstraint?.isActive = !newValue
  28. mainContentUnderTopLabelConstraint?.isActive = newValue
  29. topLabel.backgroundColor = newValue ?
  30. UIColor(alpha: 200, red: 20, green: 20, blue: 20) :
  31. UIColor(alpha: 0, red: 0, green: 0, blue: 0)
  32. }
  33. get {
  34. return mainContentUnderTopLabelConstraint?.isActive ?? false
  35. }
  36. }
  37. // if set to true bottomLabel overlaps the main content
  38. public var bottomCompactView: Bool {
  39. set {
  40. mainContentAboveBottomLabelConstraint?.isActive = !newValue
  41. mainContentUnderBottomLabelConstraint?.isActive = newValue
  42. bottomLabel.backgroundColor = newValue ?
  43. UIColor(alpha: 200, red: 50, green: 50, blue: 50) :
  44. UIColor(alpha: 0, red: 0, green: 0, blue: 0)
  45. }
  46. get {
  47. return mainContentUnderBottomLabelConstraint?.isActive ?? false
  48. }
  49. }
  50. public weak var baseDelegate: BaseMessageCellDelegate?
  51. public lazy var quoteView: QuoteView = {
  52. let view = QuoteView()
  53. view.translatesAutoresizingMaskIntoConstraints = false
  54. view.isUserInteractionEnabled = true
  55. view.isHidden = true
  56. return view
  57. }()
  58. public lazy var messageLabel: PaddingTextView = {
  59. let view = PaddingTextView()
  60. view.translatesAutoresizingMaskIntoConstraints = false
  61. view.setContentHuggingPriority(.defaultLow, for: .vertical)
  62. view.font = UIFont.preferredFont(for: .body, weight: .regular)
  63. view.delegate = self
  64. view.enabledDetectors = [.url, .phoneNumber]
  65. let attributes: [NSAttributedString.Key: Any] = [
  66. NSAttributedString.Key.foregroundColor: DcColors.defaultTextColor,
  67. NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
  68. NSAttributedString.Key.underlineColor: DcColors.defaultTextColor ]
  69. view.label.setAttributes(attributes, detector: .url)
  70. view.label.setAttributes(attributes, detector: .phoneNumber)
  71. view.isUserInteractionEnabled = true
  72. return view
  73. }()
  74. lazy var avatarView: InitialsBadge = {
  75. let view = InitialsBadge(size: 28)
  76. view.setColor(UIColor.gray)
  77. view.translatesAutoresizingMaskIntoConstraints = false
  78. view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  79. view.isHidden = true
  80. view.isUserInteractionEnabled = true
  81. return view
  82. }()
  83. lazy var topLabel: PaddingTextView = {
  84. let view = PaddingTextView()
  85. view.translatesAutoresizingMaskIntoConstraints = false
  86. view.font = UIFont.preferredFont(for: .caption1, weight: .bold)
  87. view.layer.cornerRadius = 4
  88. view.numberOfLines = 1
  89. view.label.lineBreakMode = .byTruncatingTail
  90. view.clipsToBounds = true
  91. view.paddingLeading = 4
  92. view.paddingTrailing = 4
  93. return view
  94. }()
  95. lazy var mainContentView: UIStackView = {
  96. let view = UIStackView(arrangedSubviews: [quoteView])
  97. view.translatesAutoresizingMaskIntoConstraints = false
  98. view.axis = .vertical
  99. return view
  100. }()
  101. lazy var bottomLabel: PaddingTextView = {
  102. let label = PaddingTextView()
  103. label.translatesAutoresizingMaskIntoConstraints = false
  104. label.font = UIFont.preferredFont(for: .caption1, weight: .regular)
  105. label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  106. label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
  107. label.layer.cornerRadius = 4
  108. label.paddingLeading = 4
  109. label.paddingTrailing = 4
  110. label.clipsToBounds = true
  111. return label
  112. }()
  113. private lazy var messageBackgroundContainer: BackgroundContainer = {
  114. let container = BackgroundContainer()
  115. container.image = UIImage(color: UIColor.blue)
  116. container.contentMode = .scaleToFill
  117. container.clipsToBounds = true
  118. container.translatesAutoresizingMaskIntoConstraints = false
  119. container.isUserInteractionEnabled = true
  120. return container
  121. }()
  122. override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
  123. super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
  124. clipsToBounds = false
  125. backgroundColor = .none
  126. setupSubviews()
  127. }
  128. required init?(coder: NSCoder) {
  129. fatalError("init(coder:) has not been implemented")
  130. }
  131. func setupSubviews() {
  132. contentView.addSubview(messageBackgroundContainer)
  133. messageBackgroundContainer.addSubview(mainContentView)
  134. messageBackgroundContainer.addSubview(topLabel)
  135. messageBackgroundContainer.addSubview(bottomLabel)
  136. contentView.addSubview(avatarView)
  137. contentView.addConstraints([
  138. avatarView.constraintAlignLeadingTo(contentView, paddingLeading: 2),
  139. avatarView.constraintAlignBottomTo(contentView),
  140. avatarView.constraintWidthTo(28, priority: .defaultHigh),
  141. avatarView.constraintHeightTo(28, priority: .defaultHigh),
  142. topLabel.constraintAlignTopTo(messageBackgroundContainer, paddingTop: 6),
  143. topLabel.constraintAlignLeadingTo(messageBackgroundContainer, paddingLeading: 8),
  144. topLabel.constraintAlignTrailingMaxTo(messageBackgroundContainer, paddingTrailing: 8),
  145. bottomLabel.constraintAlignBottomTo(messageBackgroundContainer, paddingBottom: 6),
  146. messageBackgroundContainer.constraintAlignTopTo(contentView, paddingTop: 3),
  147. messageBackgroundContainer.constraintAlignBottomTo(contentView, paddingBottom: 3),
  148. bottomLabel.constraintAlignLeadingMaxTo(messageBackgroundContainer, paddingLeading: 8),
  149. bottomLabel.constraintAlignTrailingTo(messageBackgroundContainer, paddingTrailing: 8)
  150. ])
  151. leadingConstraint = messageBackgroundContainer.constraintAlignLeadingTo(contentView, paddingLeading: 6)
  152. leadingConstraintGroup = messageBackgroundContainer.constraintToTrailingOf(avatarView, paddingLeading: 2)
  153. trailingConstraint = messageBackgroundContainer.constraintAlignTrailingMaxTo(contentView, paddingTrailing: 36)
  154. leadingConstraintCurrentSender = messageBackgroundContainer.constraintAlignLeadingMaxTo(contentView, paddingLeading: 36)
  155. trailingConstraintCurrentSender = messageBackgroundContainer.constraintAlignTrailingTo(contentView, paddingTrailing: 6)
  156. mainContentViewLeadingConstraint = mainContentView.constraintAlignLeadingTo(messageBackgroundContainer)
  157. mainContentViewTrailingConstraint = mainContentView.constraintAlignTrailingTo(messageBackgroundContainer)
  158. mainContentViewLeadingConstraint?.isActive = true
  159. mainContentViewTrailingConstraint?.isActive = true
  160. mainContentBelowTopLabelConstraint = mainContentView.constraintToBottomOf(topLabel, paddingTop: 6)
  161. mainContentUnderTopLabelConstraint = mainContentView.constraintAlignTopTo(messageBackgroundContainer)
  162. mainContentAboveBottomLabelConstraint = bottomLabel.constraintToBottomOf(mainContentView, paddingTop: -2, priority: .defaultHigh)
  163. mainContentUnderBottomLabelConstraint = mainContentView.constraintAlignBottomTo(messageBackgroundContainer, paddingBottom: 0, priority: .defaultHigh)
  164. topCompactView = false
  165. bottomCompactView = false
  166. let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onAvatarTapped))
  167. gestureRecognizer.numberOfTapsRequired = 1
  168. avatarView.addGestureRecognizer(gestureRecognizer)
  169. let messageLabelGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
  170. messageLabelGestureRecognizer.numberOfTapsRequired = 1
  171. messageLabel.addGestureRecognizer(messageLabelGestureRecognizer)
  172. let quoteViewGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onQuoteTapped))
  173. quoteViewGestureRecognizer.numberOfTapsRequired = 1
  174. quoteView.addGestureRecognizer(quoteViewGestureRecognizer)
  175. }
  176. @objc
  177. open func handleTapGesture(_ gesture: UIGestureRecognizer) {
  178. guard gesture.state == .ended else { return }
  179. let touchLocation = gesture.location(in: messageLabel)
  180. let isHandled = messageLabel.label.handleGesture(touchLocation)
  181. if !isHandled, let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
  182. self.baseDelegate?.textTapped(indexPath: indexPath)
  183. }
  184. }
  185. @objc func onAvatarTapped() {
  186. if let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
  187. baseDelegate?.avatarTapped(indexPath: indexPath)
  188. }
  189. }
  190. @objc func onQuoteTapped() {
  191. if let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
  192. baseDelegate?.quoteTapped(indexPath: indexPath)
  193. }
  194. }
  195. // update classes inheriting BaseMessageCell first before calling super.update(...)
  196. func update(msg: DcMsg, messageStyle: UIRectCorner, isAvatarVisible: Bool, isGroup: Bool) {
  197. if msg.isFromCurrentSender {
  198. topLabel.text = msg.isForwarded ? String.localized("forwarded_message") : nil
  199. topLabel.textColor = msg.isForwarded ? DcColors.grayDateColor : DcColors.defaultTextColor
  200. leadingConstraint?.isActive = false
  201. leadingConstraintGroup?.isActive = false
  202. trailingConstraint?.isActive = false
  203. leadingConstraintCurrentSender?.isActive = true
  204. trailingConstraintCurrentSender?.isActive = true
  205. } else {
  206. topLabel.text = msg.isForwarded ? String.localized("forwarded_message") :
  207. isGroup ? msg.fromContact.displayName : nil
  208. topLabel.textColor = msg.isForwarded ? DcColors.grayDateColor :
  209. isGroup ? msg.fromContact.color : DcColors.defaultTextColor
  210. leadingConstraintCurrentSender?.isActive = false
  211. trailingConstraintCurrentSender?.isActive = false
  212. if isGroup {
  213. leadingConstraint?.isActive = false
  214. leadingConstraintGroup?.isActive = true
  215. } else {
  216. leadingConstraintGroup?.isActive = false
  217. leadingConstraint?.isActive = true
  218. }
  219. trailingConstraint?.isActive = true
  220. }
  221. if isAvatarVisible {
  222. avatarView.isHidden = false
  223. avatarView.setName(msg.fromContact.displayName)
  224. avatarView.setColor(msg.fromContact.color)
  225. if let profileImage = msg.fromContact.profileImage {
  226. avatarView.setImage(profileImage)
  227. }
  228. } else {
  229. avatarView.isHidden = true
  230. }
  231. messageBackgroundContainer.update(rectCorners: messageStyle,
  232. color: msg.isFromCurrentSender ? DcColors.messagePrimaryColor : DcColors.messageSecondaryColor)
  233. if !msg.isInfo {
  234. bottomLabel.attributedText = getFormattedBottomLine(message: msg)
  235. }
  236. if let quoteText = msg.quoteText {
  237. quoteView.isHidden = false
  238. quoteView.quote.text = quoteText
  239. if let quoteMsg = msg.quoteMessage {
  240. quoteView.imagePreview.image = quoteMsg.image
  241. if quoteMsg.isForwarded {
  242. quoteView.senderTitle.text = String.localized("forwarded_message")
  243. quoteView.senderTitle.textColor = DcColors.grayDateColor
  244. quoteView.citeBar.backgroundColor = DcColors.grayDateColor
  245. } else {
  246. let contact = quoteMsg.fromContact
  247. quoteView.senderTitle.text = contact.displayName
  248. quoteView.senderTitle.textColor = contact.color
  249. quoteView.citeBar.backgroundColor = contact.color
  250. }
  251. }
  252. } else {
  253. quoteView.isHidden = true
  254. }
  255. messageLabel.delegate = self
  256. }
  257. func getFormattedBottomLine(message: DcMsg) -> NSAttributedString {
  258. var paragraphStyle = NSParagraphStyle()
  259. if let style = NSMutableParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle {
  260. style.minimumLineHeight = 22
  261. paragraphStyle = style
  262. }
  263. var timestampAttributes: [NSAttributedString.Key: Any] = [
  264. .font: UIFont.preferredFont(for: .caption1, weight: .regular),
  265. .foregroundColor: DcColors.grayDateColor,
  266. .paragraphStyle: paragraphStyle,
  267. ]
  268. let text = NSMutableAttributedString()
  269. if message.fromContactId == Int(DC_CONTACT_ID_SELF) {
  270. if let style = NSMutableParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle {
  271. style.alignment = .right
  272. style.minimumLineHeight = 22
  273. timestampAttributes[.paragraphStyle] = style
  274. if !bottomCompactView {
  275. timestampAttributes[.foregroundColor] = DcColors.checkmarkGreen
  276. }
  277. }
  278. text.append(NSAttributedString(string: message.formattedSentDate(), attributes: timestampAttributes))
  279. if message.showPadlock() {
  280. attachPadlock(to: text, color: bottomCompactView ? nil : DcColors.checkmarkGreen)
  281. }
  282. attachSendingState(message.state, to: text)
  283. return text
  284. }
  285. text.append(NSAttributedString(string: message.formattedSentDate(), attributes: timestampAttributes))
  286. if message.showPadlock() {
  287. attachPadlock(to: text)
  288. }
  289. return text
  290. }
  291. private func attachPadlock(to text: NSMutableAttributedString, color: UIColor? = nil) {
  292. let imageAttachment = NSTextAttachment()
  293. if let color = color {
  294. imageAttachment.image = UIImage(named: "ic_lock")?.maskWithColor(color: color)
  295. } else {
  296. imageAttachment.image = UIImage(named: "ic_lock")
  297. }
  298. imageAttachment.image?.accessibilityIdentifier = String.localized("encrypted_message")
  299. let imageString = NSMutableAttributedString(attachment: imageAttachment)
  300. imageString.addAttributes([NSAttributedString.Key.baselineOffset: -1], range: NSRange(location: 0, length: 1))
  301. text.append(NSAttributedString(string: " "))
  302. text.append(imageString)
  303. }
  304. private func attachSendingState(_ state: Int, to text: NSMutableAttributedString) {
  305. let imageAttachment = NSTextAttachment()
  306. var offset = -2
  307. switch Int32(state) {
  308. case DC_STATE_OUT_PENDING, DC_STATE_OUT_PREPARING:
  309. imageAttachment.image = #imageLiteral(resourceName: "ic_hourglass_empty_white_36pt").scaleDownImage(toMax: 14)?.maskWithColor(color: DcColors.grayDateColor)
  310. imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_sending")
  311. case DC_STATE_OUT_DELIVERED:
  312. imageAttachment.image = #imageLiteral(resourceName: "ic_done_36pt").scaleDownImage(toMax: 18)
  313. imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_delivered")
  314. offset = -3
  315. case DC_STATE_OUT_MDN_RCVD:
  316. imageAttachment.image = #imageLiteral(resourceName: "ic_done_all_36pt").scaleDownImage(toMax: 18)
  317. imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_read")
  318. text.append(NSAttributedString(string: " "))
  319. offset = -3
  320. case DC_STATE_OUT_FAILED:
  321. imageAttachment.image = #imageLiteral(resourceName: "ic_error_36pt").scaleDownImage(toMax: 17)
  322. imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_error")
  323. default:
  324. imageAttachment.image = nil
  325. }
  326. let imageString = NSMutableAttributedString(attachment: imageAttachment)
  327. imageString.addAttributes([.baselineOffset: offset],
  328. range: NSRange(location: 0, length: 1))
  329. text.append(imageString)
  330. }
  331. override public func prepareForReuse() {
  332. textLabel?.text = nil
  333. textLabel?.attributedText = nil
  334. topLabel.text = nil
  335. topLabel.attributedText = nil
  336. avatarView.reset()
  337. messageBackgroundContainer.prepareForReuse()
  338. bottomLabel.text = nil
  339. bottomLabel.attributedText = nil
  340. baseDelegate = nil
  341. messageLabel.text = nil
  342. messageLabel.attributedText = nil
  343. messageLabel.delegate = nil
  344. quoteView.prepareForReuse()
  345. }
  346. // MARK: - Context menu
  347. @objc func messageInfo(_ sender: Any?) {
  348. self.performAction(#selector(BaseMessageCell.messageInfo(_:)), with: sender)
  349. }
  350. @objc func messageDelete(_ sender: Any?) {
  351. self.performAction(#selector(BaseMessageCell.messageDelete(_:)), with: sender)
  352. }
  353. @objc func messageForward(_ sender: Any?) {
  354. self.performAction(#selector(BaseMessageCell.messageForward(_:)), with: sender)
  355. }
  356. @objc func messageReply(_ sender: Any?) {
  357. self.performAction(#selector(BaseMessageCell.messageReply(_:)), with: sender)
  358. }
  359. @objc func messageCopy(_ sender: Any?) {
  360. self.performAction(#selector(BaseMessageCell.messageCopy(_:)), with: sender)
  361. }
  362. @objc func messageSelectMore(_ sender: Any?) {
  363. self.performAction(#selector(BaseMessageCell.messageSelectMore(_:)), with: sender)
  364. }
  365. func performAction(_ action: Selector, with sender: Any?) {
  366. if let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
  367. // Trigger action in tableView delegate (UITableViewController)
  368. tableView.delegate?.tableView?(tableView,
  369. performAction: action,
  370. forRowAt: indexPath,
  371. withSender: sender)
  372. }
  373. }
  374. }
  375. extension BaseMessageCell: MessageLabelDelegate {
  376. public func didSelectAddress(_ addressComponents: [String: String]) {}
  377. public func didSelectDate(_ date: Date) {}
  378. public func didSelectPhoneNumber(_ phoneNumber: String) {
  379. if let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
  380. baseDelegate?.phoneNumberTapped(number: phoneNumber, indexPath: indexPath)
  381. }
  382. }
  383. public func didSelectURL(_ url: URL) {
  384. if let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
  385. logger.debug("did select URL")
  386. baseDelegate?.urlTapped(url: url, indexPath: indexPath)
  387. }
  388. }
  389. public func didSelectTransitInformation(_ transitInformation: [String: String]) {}
  390. public func didSelectMention(_ mention: String) {}
  391. public func didSelectHashtag(_ hashtag: String) {}
  392. public func didSelectCustom(_ pattern: String, match: String?) {}
  393. }
  394. // MARK: - BaseMessageCellDelegate
  395. // this delegate contains possible events from base cells or from derived cells
  396. public protocol BaseMessageCellDelegate: class {
  397. func commandTapped(command: String, indexPath: IndexPath) // `/command`
  398. func phoneNumberTapped(number: String, indexPath: IndexPath)
  399. func urlTapped(url: URL, indexPath: IndexPath) // url is eg. `https://foo.bar`
  400. func imageTapped(indexPath: IndexPath)
  401. func avatarTapped(indexPath: IndexPath)
  402. func textTapped(indexPath: IndexPath)
  403. func quoteTapped(indexPath: IndexPath)
  404. }