BaseMessageCell.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  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 trailingConstraintCurrentSender: NSLayoutConstraint?
  8. private var mainContentBelowTopLabelConstraint: NSLayoutConstraint?
  9. private var mainContentUnderTopLabelConstraint: NSLayoutConstraint?
  10. private var mainContentAboveBottomLabelConstraint: NSLayoutConstraint?
  11. private var mainContentUnderBottomLabelConstraint: NSLayoutConstraint?
  12. private var bottomLineLeftAlignedConstraint: [NSLayoutConstraint] = []
  13. private var bottomLineRightAlignedConstraint: [NSLayoutConstraint] = []
  14. private var mainContentViewLeadingConstraint: NSLayoutConstraint?
  15. private var mainContentViewTrailingConstraint: NSLayoutConstraint?
  16. public var mainContentViewHorizontalPadding: CGFloat {
  17. set {
  18. mainContentViewLeadingConstraint?.constant = newValue
  19. mainContentViewTrailingConstraint?.constant = -newValue
  20. }
  21. get {
  22. return mainContentViewLeadingConstraint?.constant ?? 0
  23. }
  24. }
  25. //aligns the bottomLabel to the left / right
  26. private var bottomLineLeftAlign: Bool {
  27. set {
  28. for constraint in bottomLineLeftAlignedConstraint {
  29. constraint.isActive = newValue
  30. }
  31. for constraint in bottomLineRightAlignedConstraint {
  32. constraint.isActive = !newValue
  33. }
  34. }
  35. get {
  36. return !bottomLineLeftAlignedConstraint.isEmpty && bottomLineLeftAlignedConstraint[0].isActive
  37. }
  38. }
  39. // if set to true topLabel overlaps the main content
  40. public var topCompactView: Bool {
  41. set {
  42. mainContentBelowTopLabelConstraint?.isActive = !newValue
  43. mainContentUnderTopLabelConstraint?.isActive = newValue
  44. topLabel.backgroundColor = newValue ?
  45. UIColor(alpha: 200, red: 50, green: 50, blue: 50) :
  46. UIColor(alpha: 0, red: 0, green: 0, blue: 0)
  47. }
  48. get {
  49. return mainContentUnderTopLabelConstraint?.isActive ?? false
  50. }
  51. }
  52. // if set to true bottomLabel overlaps the main content
  53. public var bottomCompactView: Bool {
  54. set {
  55. mainContentAboveBottomLabelConstraint?.isActive = !newValue
  56. mainContentUnderBottomLabelConstraint?.isActive = newValue
  57. bottomLabel.backgroundColor = newValue ?
  58. UIColor(alpha: 200, red: 50, green: 50, blue: 50) :
  59. UIColor(alpha: 0, red: 0, green: 0, blue: 0)
  60. }
  61. get {
  62. return mainContentUnderBottomLabelConstraint?.isActive ?? false
  63. }
  64. }
  65. public weak var baseDelegate: BaseMessageCellDelegate?
  66. lazy var avatarView: InitialsBadge = {
  67. let view = InitialsBadge(size: 28)
  68. view.setColor(UIColor.gray)
  69. view.translatesAutoresizingMaskIntoConstraints = false
  70. view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  71. view.isHidden = true
  72. return view
  73. }()
  74. lazy var topLabel: UILabel = {
  75. let label = PaddingLabel(top: 0, left: 4, bottom: 0, right: 4)
  76. label.translatesAutoresizingMaskIntoConstraints = false
  77. label.text = "title"
  78. label.font = UIFont.preferredFont(for: .caption1, weight: .regular)
  79. label.layer.cornerRadius = 4
  80. label.clipsToBounds = true
  81. return label
  82. }()
  83. lazy var mainContentView: UIStackView = {
  84. let view = UIStackView()
  85. view.translatesAutoresizingMaskIntoConstraints = false
  86. view.axis = .vertical
  87. return view
  88. }()
  89. lazy var bottomLabel: UILabel = {
  90. let label = PaddingLabel(top: 0, left: 4, bottom: 0, right: 4)
  91. label.translatesAutoresizingMaskIntoConstraints = false
  92. label.font = UIFont.preferredFont(for: .caption1, weight: .regular)
  93. label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  94. label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
  95. label.layer.cornerRadius = 4
  96. label.clipsToBounds = true
  97. return label
  98. }()
  99. private lazy var messageBackgroundContainer: BackgroundContainer = {
  100. let container = BackgroundContainer()
  101. container.image = UIImage(color: UIColor.blue)
  102. container.contentMode = .scaleToFill
  103. container.clipsToBounds = true
  104. container.translatesAutoresizingMaskIntoConstraints = false
  105. container.isUserInteractionEnabled = true
  106. return container
  107. }()
  108. override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
  109. super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
  110. clipsToBounds = false
  111. backgroundColor = .none
  112. setupSubviews()
  113. }
  114. required init?(coder: NSCoder) {
  115. fatalError("init(coder:) has not been implemented")
  116. }
  117. func setupSubviews() {
  118. contentView.addSubview(messageBackgroundContainer)
  119. messageBackgroundContainer.addSubview(mainContentView)
  120. messageBackgroundContainer.addSubview(topLabel)
  121. messageBackgroundContainer.addSubview(bottomLabel)
  122. contentView.addSubview(avatarView)
  123. contentView.addConstraints([
  124. avatarView.constraintAlignLeadingTo(contentView, paddingLeading: 6),
  125. avatarView.constraintAlignBottomTo(contentView, paddingBottom: -6),
  126. avatarView.constraintWidthTo(28, priority: .defaultHigh),
  127. avatarView.constraintHeightTo(28, priority: .defaultHigh),
  128. topLabel.constraintAlignTopTo(messageBackgroundContainer, paddingTop: 6),
  129. topLabel.constraintAlignLeadingTo(messageBackgroundContainer, paddingLeading: 6),
  130. topLabel.constraintAlignTrailingMaxTo(messageBackgroundContainer, paddingTrailing: 6),
  131. bottomLabel.constraintAlignBottomTo(messageBackgroundContainer, paddingBottom: 6),
  132. messageBackgroundContainer.constraintAlignTopTo(contentView, paddingTop: 6),
  133. messageBackgroundContainer.constraintAlignBottomTo(contentView),
  134. ])
  135. leadingConstraint = messageBackgroundContainer.constraintToTrailingOf(avatarView, paddingLeading: -8)
  136. trailingConstraint = messageBackgroundContainer.constraintAlignTrailingMaxTo(contentView, paddingTrailing: 36)
  137. leadingConstraintCurrentSender = messageBackgroundContainer.constraintAlignLeadingMaxTo(contentView, paddingLeading: 36)
  138. trailingConstraintCurrentSender = messageBackgroundContainer.constraintAlignTrailingTo(contentView, paddingTrailing: 6)
  139. mainContentViewLeadingConstraint = mainContentView.constraintAlignLeadingTo(messageBackgroundContainer)
  140. mainContentViewTrailingConstraint = mainContentView.constraintAlignTrailingTo(messageBackgroundContainer)
  141. mainContentViewLeadingConstraint?.isActive = true
  142. mainContentViewTrailingConstraint?.isActive = true
  143. mainContentBelowTopLabelConstraint = mainContentView.constraintToBottomOf(topLabel, paddingTop: 6)
  144. mainContentUnderTopLabelConstraint = mainContentView.constraintAlignTopTo(messageBackgroundContainer)
  145. mainContentAboveBottomLabelConstraint = bottomLabel.constraintToBottomOf(mainContentView, paddingTop: 6, priority: .defaultHigh)
  146. mainContentUnderBottomLabelConstraint = mainContentView.constraintAlignBottomTo(messageBackgroundContainer, paddingBottom: 0, priority: .defaultHigh)
  147. bottomLineRightAlignedConstraint = [bottomLabel.constraintAlignLeadingMaxTo(messageBackgroundContainer, paddingLeading: 6),
  148. bottomLabel.constraintAlignTrailingTo(messageBackgroundContainer, paddingTrailing: 6)]
  149. bottomLineLeftAlignedConstraint = [bottomLabel.constraintAlignLeadingTo(messageBackgroundContainer, paddingLeading: 6),
  150. bottomLabel.constraintAlignTrailingMaxTo(messageBackgroundContainer, paddingTrailing: 6)]
  151. topCompactView = false
  152. bottomCompactView = false
  153. selectionStyle = .none
  154. }
  155. // update classes inheriting BaseMessageCell first before calling super.update(...)
  156. func update(msg: DcMsg, messageStyle: UIRectCorner, isAvatarVisible: Bool, isGroup: Bool) {
  157. if msg.isFromCurrentSender {
  158. topLabel.text = nil
  159. leadingConstraintCurrentSender?.isActive = true
  160. trailingConstraintCurrentSender?.isActive = true
  161. leadingConstraint?.isActive = false
  162. trailingConstraint?.isActive = false
  163. bottomLineLeftAlign = false
  164. } else {
  165. topLabel.text = isGroup ? msg.fromContact.displayName : nil
  166. leadingConstraint?.isActive = true
  167. trailingConstraint?.isActive = true
  168. leadingConstraintCurrentSender?.isActive = false
  169. trailingConstraintCurrentSender?.isActive = false
  170. bottomLineLeftAlign = true
  171. }
  172. if isAvatarVisible {
  173. avatarView.isHidden = false
  174. avatarView.setName(msg.fromContact.displayName)
  175. avatarView.setColor(msg.fromContact.color)
  176. if let profileImage = msg.fromContact.profileImage {
  177. avatarView.setImage(profileImage)
  178. }
  179. } else {
  180. avatarView.isHidden = true
  181. }
  182. messageBackgroundContainer.update(rectCorners: messageStyle,
  183. color: msg.isFromCurrentSender ? DcColors.messagePrimaryColor : DcColors.messageSecondaryColor)
  184. if !msg.isInfo {
  185. bottomLabel.attributedText = getFormattedBottomLine(message: msg)
  186. }
  187. }
  188. func getFormattedBottomLine(message: DcMsg) -> NSAttributedString {
  189. var timestampAttributes: [NSAttributedString.Key: Any] = [
  190. .font: UIFont.preferredFont(for: .caption1, weight: .regular),
  191. .foregroundColor: DcColors.grayDateColor,
  192. .paragraphStyle: NSParagraphStyle()
  193. ]
  194. let text = NSMutableAttributedString()
  195. if message.fromContactId == Int(DC_CONTACT_ID_SELF) {
  196. if let style = NSMutableParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle {
  197. style.alignment = .right
  198. timestampAttributes[.paragraphStyle] = style
  199. }
  200. text.append(NSAttributedString(string: message.formattedSentDate(), attributes: timestampAttributes))
  201. if message.showPadlock() {
  202. attachPadlock(to: text)
  203. }
  204. attachSendingState(message.state, to: text)
  205. return text
  206. }
  207. text.append(NSAttributedString(string: message.formattedSentDate(), attributes: timestampAttributes))
  208. if message.showPadlock() {
  209. attachPadlock(to: text)
  210. }
  211. return text
  212. }
  213. private func attachPadlock(to text: NSMutableAttributedString) {
  214. let imageAttachment = NSTextAttachment()
  215. imageAttachment.image = UIImage(named: "ic_lock")
  216. imageAttachment.image?.accessibilityIdentifier = String.localized("encrypted_message")
  217. let imageString = NSMutableAttributedString(attachment: imageAttachment)
  218. imageString.addAttributes([NSAttributedString.Key.baselineOffset: -1], range: NSRange(location: 0, length: 1))
  219. text.append(NSAttributedString(string: " "))
  220. text.append(imageString)
  221. }
  222. private func attachSendingState(_ state: Int, to text: NSMutableAttributedString) {
  223. let imageAttachment = NSTextAttachment()
  224. var offset = -4
  225. switch Int32(state) {
  226. case DC_STATE_OUT_PENDING, DC_STATE_OUT_PREPARING:
  227. imageAttachment.image = #imageLiteral(resourceName: "ic_hourglass_empty_white_36pt").scaleDownImage(toMax: 16)?.maskWithColor(color: DcColors.grayDateColor)
  228. imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_sending")
  229. offset = -2
  230. case DC_STATE_OUT_DELIVERED:
  231. imageAttachment.image = #imageLiteral(resourceName: "ic_done_36pt").scaleDownImage(toMax: 18)
  232. imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_delivered")
  233. case DC_STATE_OUT_MDN_RCVD:
  234. imageAttachment.image = #imageLiteral(resourceName: "ic_done_all_36pt").scaleDownImage(toMax: 18)
  235. imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_read")
  236. text.append(NSAttributedString(string: " "))
  237. case DC_STATE_OUT_FAILED:
  238. imageAttachment.image = #imageLiteral(resourceName: "ic_error_36pt").scaleDownImage(toMax: 16)
  239. imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_error")
  240. offset = -2
  241. default:
  242. imageAttachment.image = nil
  243. }
  244. let imageString = NSMutableAttributedString(attachment: imageAttachment)
  245. imageString.addAttributes([.baselineOffset: offset],
  246. range: NSRange(location: 0, length: 1))
  247. text.append(imageString)
  248. }
  249. override public func prepareForReuse() {
  250. textLabel?.text = nil
  251. textLabel?.attributedText = nil
  252. topLabel.text = nil
  253. topLabel.attributedText = nil
  254. avatarView.reset()
  255. messageBackgroundContainer.prepareForReuse()
  256. bottomLabel.text = nil
  257. bottomLabel.attributedText = nil
  258. baseDelegate = nil
  259. }
  260. // MARK: - Context menu
  261. @objc func messageInfo(_ sender: Any?) {
  262. self.performAction(#selector(BaseMessageCell.messageInfo(_:)), with: sender)
  263. }
  264. @objc func messageDelete(_ sender: Any?) {
  265. self.performAction(#selector(BaseMessageCell.messageDelete(_:)), with: sender)
  266. }
  267. @objc func messageForward(_ sender: Any?) {
  268. self.performAction(#selector(BaseMessageCell.messageForward(_:)), with: sender)
  269. }
  270. func performAction(_ action: Selector, with sender: Any?) {
  271. if let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
  272. // Trigger action in tableView delegate (UITableViewController)
  273. tableView.delegate?.tableView?(tableView,
  274. performAction: action,
  275. forRowAt: indexPath,
  276. withSender: sender)
  277. }
  278. }
  279. }
  280. // MARK: - BaseMessageCellDelegate
  281. // this delegate contains possible events from base cells or from derived cells
  282. public protocol BaseMessageCellDelegate: class {
  283. func linkTapped(link: String) // link is eg. `https://foo.bar` or `/command`
  284. func imageTapped(indexPath: IndexPath)
  285. }