BaseMessageCell.swift 22 KB

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