123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- import UIKit
- import DcCore
- public class BaseMessageCell: UITableViewCell {
- private var leadingConstraint: NSLayoutConstraint?
- private var trailingConstraint: NSLayoutConstraint?
- private var leadingConstraintCurrentSender: NSLayoutConstraint?
- private var leadingConstraintGroup: NSLayoutConstraint?
- private var trailingConstraintCurrentSender: NSLayoutConstraint?
- private var mainContentBelowTopLabelConstraint: NSLayoutConstraint?
- private var mainContentUnderTopLabelConstraint: NSLayoutConstraint?
- private var mainContentAboveBottomLabelConstraint: NSLayoutConstraint?
- private var mainContentUnderBottomLabelConstraint: 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
- }
- }
- // 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: 20, green: 20, blue: 20) :
- 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?
- public lazy var quoteView: QuoteView = {
- let view = QuoteView()
- view.translatesAutoresizingMaskIntoConstraints = false
- view.isUserInteractionEnabled = true
- view.isHidden = true
- return view
- }()
- public lazy var messageLabel: PaddingTextView = {
- let view = PaddingTextView()
- view.translatesAutoresizingMaskIntoConstraints = false
- view.setContentHuggingPriority(.defaultLow, for: .vertical)
- view.font = UIFont.preferredFont(for: .body, weight: .regular)
- view.delegate = self
- view.enabledDetectors = [.url, .phoneNumber]
- let attributes: [NSAttributedString.Key: Any] = [
- NSAttributedString.Key.foregroundColor: DcColors.defaultTextColor,
- NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
- NSAttributedString.Key.underlineColor: DcColors.defaultTextColor ]
- view.label.setAttributes(attributes, detector: .url)
- view.label.setAttributes(attributes, detector: .phoneNumber)
- view.isUserInteractionEnabled = true
- return view
- }()
- lazy var avatarView: InitialsBadge = {
- let view = InitialsBadge(size: 28)
- view.setColor(UIColor.gray)
- view.translatesAutoresizingMaskIntoConstraints = false
- view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
- view.isHidden = true
- view.isUserInteractionEnabled = true
- return view
- }()
- lazy var topLabel: PaddingTextView = {
- let view = PaddingTextView()
- view.translatesAutoresizingMaskIntoConstraints = false
- view.font = UIFont.preferredFont(for: .caption1, weight: .bold)
- view.layer.cornerRadius = 4
- view.numberOfLines = 1
- view.label.lineBreakMode = .byTruncatingTail
- view.clipsToBounds = true
- view.paddingLeading = 4
- view.paddingTrailing = 4
- return view
- }()
- lazy var mainContentView: UIStackView = {
- let view = UIStackView(arrangedSubviews: [quoteView])
- view.translatesAutoresizingMaskIntoConstraints = false
- view.axis = .vertical
- return view
- }()
- lazy var bottomLabel: PaddingTextView = {
- let label = PaddingTextView()
- 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.paddingLeading = 4
- label.paddingTrailing = 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: 2),
- avatarView.constraintAlignBottomTo(contentView),
- avatarView.constraintWidthTo(28, priority: .defaultHigh),
- avatarView.constraintHeightTo(28, priority: .defaultHigh),
- topLabel.constraintAlignTopTo(messageBackgroundContainer, paddingTop: 6),
- topLabel.constraintAlignLeadingTo(messageBackgroundContainer, paddingLeading: 8),
- topLabel.constraintAlignTrailingMaxTo(messageBackgroundContainer, paddingTrailing: 8),
- bottomLabel.constraintAlignBottomTo(messageBackgroundContainer, paddingBottom: 6),
- messageBackgroundContainer.constraintAlignTopTo(contentView, paddingTop: 3),
- messageBackgroundContainer.constraintAlignBottomTo(contentView, paddingBottom: 3),
- bottomLabel.constraintAlignLeadingMaxTo(messageBackgroundContainer, paddingLeading: 8),
- bottomLabel.constraintAlignTrailingTo(messageBackgroundContainer, paddingTrailing: 8)
- ])
- leadingConstraint = messageBackgroundContainer.constraintAlignLeadingTo(contentView, paddingLeading: 6)
- leadingConstraintGroup = messageBackgroundContainer.constraintToTrailingOf(avatarView, paddingLeading: 2)
- 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: -2, priority: .defaultHigh)
- mainContentUnderBottomLabelConstraint = mainContentView.constraintAlignBottomTo(messageBackgroundContainer, paddingBottom: 0, priority: .defaultHigh)
- topCompactView = false
- bottomCompactView = false
-
- let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onAvatarTapped))
- gestureRecognizer.numberOfTapsRequired = 1
- avatarView.addGestureRecognizer(gestureRecognizer)
- let messageLabelGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
- messageLabelGestureRecognizer.numberOfTapsRequired = 1
- messageLabel.addGestureRecognizer(messageLabelGestureRecognizer)
- let quoteViewGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onQuoteTapped))
- quoteViewGestureRecognizer.numberOfTapsRequired = 1
- quoteView.addGestureRecognizer(quoteViewGestureRecognizer)
- }
- @objc
- open func handleTapGesture(_ gesture: UIGestureRecognizer) {
- guard gesture.state == .ended else { return }
- let touchLocation = gesture.location(in: messageLabel)
- let isHandled = messageLabel.label.handleGesture(touchLocation)
- if !isHandled, let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
- self.baseDelegate?.textTapped(indexPath: indexPath)
- }
- }
- @objc func onAvatarTapped() {
- if let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
- baseDelegate?.avatarTapped(indexPath: indexPath)
- }
- }
- @objc func onQuoteTapped() {
- if let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
- baseDelegate?.quoteTapped(indexPath: indexPath)
- }
- }
- // update classes inheriting BaseMessageCell first before calling super.update(...)
- func update(msg: DcMsg, messageStyle: UIRectCorner, isAvatarVisible: Bool, isGroup: Bool) {
- if msg.isFromCurrentSender {
- topLabel.text = msg.isForwarded ? String.localized("forwarded_message") : nil
- topLabel.textColor = msg.isForwarded ? DcColors.grayDateColor : DcColors.defaultTextColor
- leadingConstraint?.isActive = false
- leadingConstraintGroup?.isActive = false
- trailingConstraint?.isActive = false
- leadingConstraintCurrentSender?.isActive = true
- trailingConstraintCurrentSender?.isActive = true
- } else {
- topLabel.text = msg.isForwarded ? String.localized("forwarded_message") :
- isGroup ? msg.fromContact.displayName : nil
- topLabel.textColor = msg.isForwarded ? DcColors.grayDateColor :
- isGroup ? msg.fromContact.color : DcColors.defaultTextColor
- leadingConstraintCurrentSender?.isActive = false
- trailingConstraintCurrentSender?.isActive = false
- if isGroup {
- leadingConstraint?.isActive = false
- leadingConstraintGroup?.isActive = true
- } else {
- leadingConstraintGroup?.isActive = false
- leadingConstraint?.isActive = true
- }
- trailingConstraint?.isActive = 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)
- }
- if let quoteText = msg.quoteText {
- quoteView.isHidden = false
- quoteView.quote.text = quoteText
- if let quoteMsg = msg.quoteMessage {
- quoteView.imagePreview.image = quoteMsg.image
- if quoteMsg.isForwarded {
- quoteView.senderTitle.text = String.localized("forwarded_message")
- quoteView.senderTitle.textColor = DcColors.grayDateColor
- quoteView.citeBar.backgroundColor = DcColors.grayDateColor
- } else {
- let contact = quoteMsg.fromContact
- quoteView.senderTitle.text = contact.displayName
- quoteView.senderTitle.textColor = contact.color
- quoteView.citeBar.backgroundColor = contact.color
- }
- }
- } else {
- quoteView.isHidden = true
- }
- messageLabel.delegate = self
- }
- func getFormattedBottomLine(message: DcMsg) -> NSAttributedString {
- var paragraphStyle = NSParagraphStyle()
- if let style = NSMutableParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle {
- style.minimumLineHeight = 22
- paragraphStyle = style
- }
- var timestampAttributes: [NSAttributedString.Key: Any] = [
- .font: UIFont.preferredFont(for: .caption1, weight: .regular),
- .foregroundColor: DcColors.grayDateColor,
- .paragraphStyle: paragraphStyle,
- ]
- let text = NSMutableAttributedString()
- if message.fromContactId == Int(DC_CONTACT_ID_SELF) {
- if let style = NSMutableParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle {
- style.alignment = .right
- style.minimumLineHeight = 22
- timestampAttributes[.paragraphStyle] = style
- if !bottomCompactView {
- timestampAttributes[.foregroundColor] = DcColors.checkmarkGreen
- }
- }
- text.append(NSAttributedString(string: message.formattedSentDate(), attributes: timestampAttributes))
- if message.showPadlock() {
- attachPadlock(to: text, color: bottomCompactView ? nil : DcColors.checkmarkGreen)
- }
- 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, color: UIColor? = nil) {
- let imageAttachment = NSTextAttachment()
- if let color = color {
- imageAttachment.image = UIImage(named: "ic_lock")?.maskWithColor(color: color)
- } else {
- 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 = -2
- switch Int32(state) {
- case DC_STATE_OUT_PENDING, DC_STATE_OUT_PREPARING:
- imageAttachment.image = #imageLiteral(resourceName: "ic_hourglass_empty_white_36pt").scaleDownImage(toMax: 14)?.maskWithColor(color: DcColors.grayDateColor)
- imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_sending")
- case DC_STATE_OUT_DELIVERED:
- imageAttachment.image = #imageLiteral(resourceName: "ic_done_36pt").scaleDownImage(toMax: 18)
- imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_delivered")
- offset = -3
- 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: " "))
- offset = -3
- case DC_STATE_OUT_FAILED:
- imageAttachment.image = #imageLiteral(resourceName: "ic_error_36pt").scaleDownImage(toMax: 17)
- imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_error")
- 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
- messageLabel.text = nil
- messageLabel.attributedText = nil
- messageLabel.delegate = nil
- quoteView.prepareForReuse()
- }
- // 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)
- }
- @objc func messageReply(_ sender: Any?) {
- self.performAction(#selector(BaseMessageCell.messageReply(_:)), with: sender)
- }
- @objc func messageCopy(_ sender: Any?) {
- self.performAction(#selector(BaseMessageCell.messageCopy(_:)), with: sender)
- }
- @objc func messageSelectMore(_ sender: Any?) {
- self.performAction(#selector(BaseMessageCell.messageSelectMore(_:)), 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)
- }
- }
- }
- extension BaseMessageCell: MessageLabelDelegate {
- public func didSelectAddress(_ addressComponents: [String: String]) {}
- public func didSelectDate(_ date: Date) {}
- public func didSelectPhoneNumber(_ phoneNumber: String) {
- if let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
- baseDelegate?.phoneNumberTapped(number: phoneNumber, indexPath: indexPath)
- }
- }
- public func didSelectURL(_ url: URL) {
- if let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
- logger.debug("did select URL")
- baseDelegate?.urlTapped(url: url, indexPath: indexPath)
- }
- }
- public func didSelectTransitInformation(_ transitInformation: [String: String]) {}
- public func didSelectMention(_ mention: String) {}
- public func didSelectHashtag(_ hashtag: String) {}
- public func didSelectCustom(_ pattern: String, match: String?) {}
- }
- // MARK: - BaseMessageCellDelegate
- // this delegate contains possible events from base cells or from derived cells
- public protocol BaseMessageCellDelegate: class {
- func commandTapped(command: String, indexPath: IndexPath) // `/command`
- func phoneNumberTapped(number: String, indexPath: IndexPath)
- func urlTapped(url: URL, indexPath: IndexPath) // url is eg. `https://foo.bar`
- func imageTapped(indexPath: IndexPath)
- func avatarTapped(indexPath: IndexPath)
- func textTapped(indexPath: IndexPath)
- func quoteTapped(indexPath: IndexPath)
- }
|