123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- import UIKit
- import DcCore
- public class BaseMessageCell: UITableViewCell {
- private var leadingConstraint: NSLayoutConstraint?
- private var trailingConstraint: NSLayoutConstraint?
- private var leadingConstraintCurrentSender: NSLayoutConstraint?
- private var trailingConstraintCurrentSender: NSLayoutConstraint?
- private var mainContentBelowTopLabelConstraint: NSLayoutConstraint?
- private var mainContentUnderTopLabelConstraint: NSLayoutConstraint?
- private var mainContentAboveBottomLabelConstraint: NSLayoutConstraint?
- private var mainContentUnderBottomLabelConstraint: NSLayoutConstraint?
- private var bottomLineLeftAlignedConstraint: [NSLayoutConstraint] = []
- private var bottomLineRightAlignedConstraint: [NSLayoutConstraint] = []
- private var mainContentViewLeadingConstraint: NSLayoutConstraint?
- private var mainContentViewTrailingConstraint: NSLayoutConstraint?
- public var mainContentViewHorizontalPadding: CGFloat {
- set {
- mainContentViewLeadingConstraint?.constant = newValue
- mainContentViewTrailingConstraint?.constant = -newValue
- }
- get {
- return mainContentViewLeadingConstraint?.constant ?? 0
- }
- }
- //aligns the bottomLabel to the left / right
- private var bottomLineLeftAlign: Bool {
- set {
- for constraint in bottomLineLeftAlignedConstraint {
- constraint.isActive = newValue
- }
- for constraint in bottomLineRightAlignedConstraint {
- constraint.isActive = !newValue
- }
- }
- get {
- return !bottomLineLeftAlignedConstraint.isEmpty && bottomLineLeftAlignedConstraint[0].isActive
- }
- }
- // if set to true topLabel overlaps the main content
- public var topCompactView: Bool {
- set {
- mainContentBelowTopLabelConstraint?.isActive = !newValue
- mainContentUnderTopLabelConstraint?.isActive = newValue
- topLabel.backgroundColor = newValue ?
- UIColor(alpha: 200, red: 50, green: 50, blue: 50) :
- UIColor(alpha: 0, red: 0, green: 0, blue: 0)
- }
- get {
- return mainContentUnderTopLabelConstraint?.isActive ?? false
- }
- }
- // if set to true bottomLabel overlaps the main content
- public var bottomCompactView: Bool {
- set {
- mainContentAboveBottomLabelConstraint?.isActive = !newValue
- mainContentUnderBottomLabelConstraint?.isActive = newValue
- bottomLabel.backgroundColor = newValue ?
- UIColor(alpha: 200, red: 50, green: 50, blue: 50) :
- UIColor(alpha: 0, red: 0, green: 0, blue: 0)
- }
- get {
- return mainContentUnderBottomLabelConstraint?.isActive ?? false
- }
- }
- public weak var baseDelegate: BaseMessageCellDelegate?
- lazy var avatarView: InitialsBadge = {
- let view = InitialsBadge(size: 28)
- view.setColor(UIColor.gray)
- view.translatesAutoresizingMaskIntoConstraints = false
- view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
- view.isHidden = true
- return view
- }()
- lazy var topLabel: UILabel = {
- let label = PaddingLabel(top: 0, left: 4, bottom: 0, right: 4)
- label.translatesAutoresizingMaskIntoConstraints = false
- label.text = "title"
- label.font = UIFont.preferredFont(for: .caption1, weight: .regular)
- label.layer.cornerRadius = 4
- label.clipsToBounds = true
- return label
- }()
- lazy var mainContentView: UIStackView = {
- let view = UIStackView()
- view.translatesAutoresizingMaskIntoConstraints = false
- view.axis = .vertical
- return view
- }()
- lazy var bottomLabel: UILabel = {
- let label = PaddingLabel(top: 0, left: 4, bottom: 0, right: 4)
- label.translatesAutoresizingMaskIntoConstraints = false
- label.font = UIFont.preferredFont(for: .caption1, weight: .regular)
- label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
- label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
- label.layer.cornerRadius = 4
- label.clipsToBounds = true
- return label
- }()
- private lazy var messageBackgroundContainer: BackgroundContainer = {
- let container = BackgroundContainer()
- container.image = UIImage(color: UIColor.blue)
- container.contentMode = .scaleToFill
- container.clipsToBounds = true
- container.translatesAutoresizingMaskIntoConstraints = false
- container.isUserInteractionEnabled = true
- return container
- }()
- override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
- clipsToBounds = false
- backgroundColor = .none
- setupSubviews()
- }
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- func setupSubviews() {
- contentView.addSubview(messageBackgroundContainer)
- messageBackgroundContainer.addSubview(mainContentView)
- messageBackgroundContainer.addSubview(topLabel)
- messageBackgroundContainer.addSubview(bottomLabel)
- contentView.addSubview(avatarView)
- contentView.addConstraints([
- avatarView.constraintAlignLeadingTo(contentView, paddingLeading: 6),
- avatarView.constraintAlignBottomTo(contentView, paddingBottom: -6),
- avatarView.constraintWidthTo(28, priority: .defaultHigh),
- avatarView.constraintHeightTo(28, priority: .defaultHigh),
- topLabel.constraintAlignTopTo(messageBackgroundContainer, paddingTop: 6),
- topLabel.constraintAlignLeadingTo(messageBackgroundContainer, paddingLeading: 6),
- topLabel.constraintAlignTrailingMaxTo(messageBackgroundContainer, paddingTrailing: 6),
- bottomLabel.constraintAlignBottomTo(messageBackgroundContainer, paddingBottom: 6),
- messageBackgroundContainer.constraintAlignTopTo(contentView, paddingTop: 6),
- messageBackgroundContainer.constraintAlignBottomTo(contentView),
- ])
- leadingConstraint = messageBackgroundContainer.constraintToTrailingOf(avatarView, paddingLeading: -8)
- trailingConstraint = messageBackgroundContainer.constraintAlignTrailingMaxTo(contentView, paddingTrailing: 36)
- leadingConstraintCurrentSender = messageBackgroundContainer.constraintAlignLeadingMaxTo(contentView, paddingLeading: 36)
- trailingConstraintCurrentSender = messageBackgroundContainer.constraintAlignTrailingTo(contentView, paddingTrailing: 6)
- mainContentViewLeadingConstraint = mainContentView.constraintAlignLeadingTo(messageBackgroundContainer)
- mainContentViewTrailingConstraint = mainContentView.constraintAlignTrailingTo(messageBackgroundContainer)
- mainContentViewLeadingConstraint?.isActive = true
- mainContentViewTrailingConstraint?.isActive = true
- mainContentBelowTopLabelConstraint = mainContentView.constraintToBottomOf(topLabel, paddingTop: 6)
- mainContentUnderTopLabelConstraint = mainContentView.constraintAlignTopTo(messageBackgroundContainer)
- mainContentAboveBottomLabelConstraint = bottomLabel.constraintToBottomOf(mainContentView, paddingTop: 6, priority: .defaultHigh)
- mainContentUnderBottomLabelConstraint = mainContentView.constraintAlignBottomTo(messageBackgroundContainer, paddingBottom: 0, priority: .defaultHigh)
- bottomLineRightAlignedConstraint = [bottomLabel.constraintAlignLeadingMaxTo(messageBackgroundContainer, paddingLeading: 6),
- bottomLabel.constraintAlignTrailingTo(messageBackgroundContainer, paddingTrailing: 6)]
- bottomLineLeftAlignedConstraint = [bottomLabel.constraintAlignLeadingTo(messageBackgroundContainer, paddingLeading: 6),
- bottomLabel.constraintAlignTrailingMaxTo(messageBackgroundContainer, paddingTrailing: 6)]
- topCompactView = false
- bottomCompactView = false
- selectionStyle = .none
- }
- // update classes inheriting BaseMessageCell first before calling super.update(...)
- func update(msg: DcMsg, messageStyle: UIRectCorner, isAvatarVisible: Bool, isGroup: Bool) {
- if msg.isFromCurrentSender {
- topLabel.text = nil
- leadingConstraintCurrentSender?.isActive = true
- trailingConstraintCurrentSender?.isActive = true
- leadingConstraint?.isActive = false
- trailingConstraint?.isActive = false
- bottomLineLeftAlign = false
- } else {
- topLabel.text = isGroup ? msg.fromContact.displayName : nil
- leadingConstraint?.isActive = true
- trailingConstraint?.isActive = true
- leadingConstraintCurrentSender?.isActive = false
- trailingConstraintCurrentSender?.isActive = false
- bottomLineLeftAlign = true
- }
- if isAvatarVisible {
- avatarView.isHidden = false
- avatarView.setName(msg.fromContact.displayName)
- avatarView.setColor(msg.fromContact.color)
- if let profileImage = msg.fromContact.profileImage {
- avatarView.setImage(profileImage)
- }
- } else {
- avatarView.isHidden = true
- }
- messageBackgroundContainer.update(rectCorners: messageStyle,
- color: msg.isFromCurrentSender ? DcColors.messagePrimaryColor : DcColors.messageSecondaryColor)
- if !msg.isInfo {
- bottomLabel.attributedText = getFormattedBottomLine(message: msg)
- }
- }
- func getFormattedBottomLine(message: DcMsg) -> NSAttributedString {
- var timestampAttributes: [NSAttributedString.Key: Any] = [
- .font: UIFont.preferredFont(for: .caption1, weight: .regular),
- .foregroundColor: DcColors.grayDateColor,
- .paragraphStyle: NSParagraphStyle()
- ]
- let text = NSMutableAttributedString()
- if message.fromContactId == Int(DC_CONTACT_ID_SELF) {
- if let style = NSMutableParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle {
- style.alignment = .right
- timestampAttributes[.paragraphStyle] = style
- }
- text.append(NSAttributedString(string: message.formattedSentDate(), attributes: timestampAttributes))
- if message.showPadlock() {
- attachPadlock(to: text)
- }
- attachSendingState(message.state, to: text)
- return text
- }
- text.append(NSAttributedString(string: message.formattedSentDate(), attributes: timestampAttributes))
- if message.showPadlock() {
- attachPadlock(to: text)
- }
- return text
- }
- private func attachPadlock(to text: NSMutableAttributedString) {
- let imageAttachment = NSTextAttachment()
- imageAttachment.image = UIImage(named: "ic_lock")
- imageAttachment.image?.accessibilityIdentifier = String.localized("encrypted_message")
- let imageString = NSMutableAttributedString(attachment: imageAttachment)
- imageString.addAttributes([NSAttributedString.Key.baselineOffset: -1], range: NSRange(location: 0, length: 1))
- text.append(NSAttributedString(string: " "))
- text.append(imageString)
- }
- private func attachSendingState(_ state: Int, to text: NSMutableAttributedString) {
- let imageAttachment = NSTextAttachment()
- var offset = -4
- switch Int32(state) {
- case DC_STATE_OUT_PENDING, DC_STATE_OUT_PREPARING:
- imageAttachment.image = #imageLiteral(resourceName: "ic_hourglass_empty_white_36pt").scaleDownImage(toMax: 16)?.maskWithColor(color: DcColors.grayDateColor)
- imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_sending")
- offset = -2
- case DC_STATE_OUT_DELIVERED:
- imageAttachment.image = #imageLiteral(resourceName: "ic_done_36pt").scaleDownImage(toMax: 18)
- imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_delivered")
- case DC_STATE_OUT_MDN_RCVD:
- imageAttachment.image = #imageLiteral(resourceName: "ic_done_all_36pt").scaleDownImage(toMax: 18)
- imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_read")
- text.append(NSAttributedString(string: " "))
- case DC_STATE_OUT_FAILED:
- imageAttachment.image = #imageLiteral(resourceName: "ic_error_36pt").scaleDownImage(toMax: 16)
- imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_error")
- offset = -2
- default:
- imageAttachment.image = nil
- }
- let imageString = NSMutableAttributedString(attachment: imageAttachment)
- imageString.addAttributes([.baselineOffset: offset],
- range: NSRange(location: 0, length: 1))
- text.append(imageString)
- }
- override public func prepareForReuse() {
- textLabel?.text = nil
- textLabel?.attributedText = nil
- topLabel.text = nil
- topLabel.attributedText = nil
- avatarView.reset()
- messageBackgroundContainer.prepareForReuse()
- bottomLabel.text = nil
- bottomLabel.attributedText = nil
- baseDelegate = nil
- }
- // MARK: - Context menu
- @objc func messageInfo(_ sender: Any?) {
- self.performAction(#selector(BaseMessageCell.messageInfo(_:)), with: sender)
- }
- @objc func messageDelete(_ sender: Any?) {
- self.performAction(#selector(BaseMessageCell.messageDelete(_:)), with: sender)
- }
- @objc func messageForward(_ sender: Any?) {
- self.performAction(#selector(BaseMessageCell.messageForward(_:)), with: sender)
- }
- func performAction(_ action: Selector, with sender: Any?) {
- if let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
- // Trigger action in tableView delegate (UITableViewController)
- tableView.delegate?.tableView?(tableView,
- performAction: action,
- forRowAt: indexPath,
- withSender: sender)
- }
- }
- }
- // MARK: - BaseMessageCellDelegate
- // this delegate contains possible events from base cells or from derived cells
- public protocol BaseMessageCellDelegate: class {
- func linkTapped(link: String) // link is eg. `https://foo.bar` or `/command`
- func imageTapped(indexPath: IndexPath)
- }
|