session-ios/Session/Conversations/Message Cells/Content Views/QuoteView.swift

250 lines
11 KiB
Swift
Raw Normal View History

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionMessagingKit
2021-01-29 01:46:32 +01:00
final class QuoteView : UIView {
2021-02-10 04:43:57 +01:00
private let mode: Mode
private let thread: TSThread
2021-02-10 04:43:57 +01:00
private let direction: Direction
private let hInset: CGFloat
2021-02-10 05:33:39 +01:00
private let maxWidth: CGFloat
2021-02-10 07:04:26 +01:00
private let delegate: QuoteViewDelegate?
2021-02-10 05:33:39 +01:00
private var maxBodyLabelHeight: CGFloat {
switch mode {
case .regular: return 60
case .draft: return 40
}
}
2021-01-29 01:46:32 +01:00
2021-02-10 04:43:57 +01:00
private var attachments: [OWSAttachmentInfo] {
switch mode {
case .regular(let viewItem): return (viewItem.interaction as? TSMessage)?.quotedMessage!.quotedAttachments ?? []
case .draft(let model): return given(model.attachmentStream) { [ OWSAttachmentInfo(attachmentStream: $0) ] } ?? []
}
}
private var thumbnail: UIImage? {
switch mode {
case .regular(let viewItem): return viewItem.quotedReply!.thumbnailImage
case .draft(let model): return model.thumbnailImage
}
}
private var body: String? {
switch mode {
case .regular(let viewItem): return (viewItem.interaction as? TSMessage)?.quotedMessage!.body
case .draft(let model): return model.body
}
}
private var authorID: String {
switch mode {
case .regular(let viewItem): return viewItem.quotedReply!.authorId
case .draft(let model): return model.authorId
2021-01-29 01:46:32 +01:00
}
}
private var lineColor: UIColor {
2021-02-10 04:43:57 +01:00
switch (mode, AppModeManager.shared.currentAppMode) {
2021-02-10 05:33:39 +01:00
case (.regular, .light), (.draft, .light): return .black
case (.regular, .dark): return (direction == .outgoing) ? .black : Colors.accent
2021-02-10 04:43:57 +01:00
case (.draft, .dark): return Colors.accent
}
2021-01-29 01:46:32 +01:00
}
private var textColor: UIColor {
2021-02-10 04:43:57 +01:00
if case .draft = mode { return Colors.text }
2021-01-29 01:46:32 +01:00
switch (direction, AppModeManager.shared.currentAppMode) {
2021-02-10 04:43:57 +01:00
case (.outgoing, .dark), (.incoming, .light): return .black
default: return .white
2021-01-29 01:46:32 +01:00
}
}
2021-02-10 04:43:57 +01:00
// MARK: Mode
enum Mode {
case regular(ConversationViewItem)
case draft(OWSQuotedReplyModel)
2021-01-29 01:46:32 +01:00
}
// MARK: Direction
enum Direction { case incoming, outgoing }
// MARK: Settings
static let thumbnailSize: CGFloat = 48
static let iconSize: CGFloat = 24
static let labelStackViewSpacing: CGFloat = 2
2021-02-10 05:33:39 +01:00
static let labelStackViewVMargin: CGFloat = 4
2021-02-12 01:14:01 +01:00
static let cancelButtonSize: CGFloat = 33
2021-01-29 01:46:32 +01:00
// MARK: Lifecycle
init(for viewItem: ConversationViewItem, in thread: TSThread?, direction: Direction, hInset: CGFloat, maxWidth: CGFloat) {
2021-02-10 04:43:57 +01:00
self.mode = .regular(viewItem)
self.thread = thread ?? TSThread.fetch(uniqueId: viewItem.interaction.uniqueThreadId)!
2021-02-10 05:33:39 +01:00
self.maxWidth = maxWidth
2021-02-10 04:43:57 +01:00
self.direction = direction
self.hInset = hInset
2021-02-10 07:04:26 +01:00
self.delegate = nil
2021-02-10 04:43:57 +01:00
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
2021-02-10 07:04:26 +01:00
init(for model: OWSQuotedReplyModel, direction: Direction, hInset: CGFloat, maxWidth: CGFloat, delegate: QuoteViewDelegate) {
2021-02-10 04:43:57 +01:00
self.mode = .draft(model)
self.thread = TSThread.fetch(uniqueId: model.threadId)!
2021-02-10 05:33:39 +01:00
self.maxWidth = maxWidth
2021-02-10 04:43:57 +01:00
self.direction = direction
self.hInset = hInset
2021-02-10 07:04:26 +01:00
self.delegate = delegate
2021-01-29 01:46:32 +01:00
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
override init(frame: CGRect) {
preconditionFailure("Use init(for:maxMessageWidth:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(for:maxMessageWidth:) instead.")
}
private func setUpViewHierarchy() {
2021-03-02 00:18:08 +01:00
// There's quite a bit of calculation going on here. It's a bit complex so don't make changes
// if you don't need to. If you do then test:
// Quoted text in both private chats and group chats
// Quoted images and videos in both private chats and group chats
// Quoted voice messages and documents in both private chats and group chats
// All of the above in both dark mode and light mode
2021-02-10 04:43:57 +01:00
let hasAttachments = !attachments.isEmpty
2021-01-29 01:46:32 +01:00
let thumbnailSize = QuoteView.thumbnailSize
let iconSize = QuoteView.iconSize
let labelStackViewSpacing = QuoteView.labelStackViewSpacing
2021-02-10 05:33:39 +01:00
let labelStackViewVMargin = QuoteView.labelStackViewVMargin
2021-01-29 01:46:32 +01:00
let smallSpacing = Values.smallSpacing
2021-02-10 07:04:26 +01:00
let cancelButtonSize = QuoteView.cancelButtonSize
var availableWidth: CGFloat
2021-02-10 05:33:39 +01:00
// Subtract smallSpacing twice; once for the spacing in between the stack view elements and
// once for the trailing margin.
2021-01-29 01:46:32 +01:00
if !hasAttachments {
2021-02-10 05:33:39 +01:00
availableWidth = maxWidth - 2 * hInset - Values.accentLineThickness - 2 * smallSpacing
2021-01-29 01:46:32 +01:00
} else {
2021-02-10 05:33:39 +01:00
availableWidth = maxWidth - 2 * hInset - thumbnailSize - 2 * smallSpacing
2021-01-29 01:46:32 +01:00
}
2021-02-10 07:04:26 +01:00
if case .draft = mode {
availableWidth -= cancelButtonSize
}
2021-01-29 01:46:32 +01:00
let availableSpace = CGSize(width: availableWidth, height: .greatestFiniteMagnitude)
2021-02-10 04:43:57 +01:00
var body = self.body
2021-01-29 01:46:32 +01:00
// Main stack view
let mainStackView = UIStackView(arrangedSubviews: [])
mainStackView.axis = .horizontal
mainStackView.spacing = smallSpacing
mainStackView.isLayoutMarginsRelativeArrangement = true
mainStackView.layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: smallSpacing)
mainStackView.alignment = .center
// Content view
let contentView = UIView()
addSubview(contentView)
2021-02-10 04:43:57 +01:00
contentView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.VerticalEdge.bottom ], to: self)
contentView.rightAnchor.constraint(lessThanOrEqualTo: self.rightAnchor).isActive = true
2021-01-29 01:46:32 +01:00
// Line view
let lineView = UIView()
lineView.backgroundColor = lineColor
lineView.set(.width, to: Values.accentLineThickness)
if !hasAttachments {
mainStackView.addArrangedSubview(lineView)
} else {
let isAudio = MIMETypeUtil.isAudio(attachments.first!.contentType ?? "")
2021-02-10 04:43:57 +01:00
let fallbackImageName = isAudio ? "attachment_audio" : "actionsheet_document_black"
let fallbackImage = UIImage(named: fallbackImageName)?.withTint(.white)?.resizedImage(to: CGSize(width: iconSize, height: iconSize))
let imageView = UIImageView(image: thumbnail ?? fallbackImage)
imageView.contentMode = (thumbnail != nil) ? .scaleAspectFill : .center
2021-01-29 01:46:32 +01:00
imageView.backgroundColor = lineColor
2021-02-10 04:43:57 +01:00
imageView.layer.cornerRadius = VisibleMessageCell.smallCornerRadius
imageView.layer.masksToBounds = true
2021-01-29 01:46:32 +01:00
imageView.set(.width, to: thumbnailSize)
imageView.set(.height, to: thumbnailSize)
mainStackView.addArrangedSubview(imageView)
if (body ?? "").isEmpty {
body = (thumbnail != nil) ? "Image" : (isAudio ? "Audio" : "Document")
}
2021-01-29 01:46:32 +01:00
}
// Body label
let bodyLabel = UILabel()
bodyLabel.numberOfLines = 0
bodyLabel.lineBreakMode = .byTruncatingTail
2021-02-10 04:43:57 +01:00
let isOutgoing = (direction == .outgoing)
2021-02-10 05:33:39 +01:00
bodyLabel.font = .systemFont(ofSize: Values.smallFontSize)
bodyLabel.attributedText = given(body) { MentionUtilities.highlightMentions(in: $0, isOutgoingMessage: isOutgoing, threadID: thread.uniqueId!, attributes: [:]) } ?? given(attachments.first?.contentType) { NSAttributedString(string: MIMETypeUtil.isAudio($0) ? "Audio" : "Document") } ?? NSAttributedString(string: "Document")
2021-01-29 01:46:32 +01:00
bodyLabel.textColor = textColor
let bodyLabelSize = bodyLabel.systemLayoutSizeFitting(availableSpace)
// Label stack view
2021-02-10 23:53:27 +01:00
var authorLabelHeight: CGFloat?
if let groupThread = thread as? TSGroupThread {
2021-01-29 01:46:32 +01:00
let authorLabel = UILabel()
authorLabel.lineBreakMode = .byTruncatingTail
authorLabel.text = Profile.displayName(for: authorID, thread: groupThread)
2021-01-29 01:46:32 +01:00
authorLabel.textColor = textColor
authorLabel.font = .boldSystemFont(ofSize: Values.smallFontSize)
let authorLabelSize = authorLabel.systemLayoutSizeFitting(availableSpace)
2021-02-10 23:53:27 +01:00
authorLabel.set(.height, to: authorLabelSize.height)
authorLabelHeight = authorLabelSize.height
2021-01-29 01:46:32 +01:00
let labelStackView = UIStackView(arrangedSubviews: [ authorLabel, bodyLabel ])
labelStackView.axis = .vertical
labelStackView.spacing = labelStackViewSpacing
2021-03-29 06:15:25 +02:00
labelStackView.distribution = .equalCentering
2021-01-29 01:46:32 +01:00
labelStackView.set(.width, to: max(bodyLabelSize.width, authorLabelSize.width))
2021-02-10 05:33:39 +01:00
labelStackView.isLayoutMarginsRelativeArrangement = true
labelStackView.layoutMargins = UIEdgeInsets(top: labelStackViewVMargin, left: 0, bottom: labelStackViewVMargin, right: 0)
2021-01-29 01:46:32 +01:00
mainStackView.addArrangedSubview(labelStackView)
} else {
mainStackView.addArrangedSubview(bodyLabel)
}
2021-02-10 07:04:26 +01:00
// Cancel button
let cancelButton = UIButton(type: .custom)
2021-02-10 23:53:27 +01:00
let tint: UIColor = isLightMode ? .black : .white
cancelButton.setImage(UIImage(named: "X")?.withTint(tint), for: UIControl.State.normal)
2021-02-10 07:04:26 +01:00
cancelButton.set(.width, to: cancelButtonSize)
cancelButton.set(.height, to: cancelButtonSize)
cancelButton.addTarget(self, action: #selector(cancel), for: UIControl.Event.touchUpInside)
2021-01-29 01:46:32 +01:00
// Constraints
contentView.addSubview(mainStackView)
mainStackView.pin(to: contentView)
if !thread.isGroupThread() {
2021-01-29 01:46:32 +01:00
bodyLabel.set(.width, to: bodyLabelSize.width)
}
2021-02-10 04:43:57 +01:00
let bodyLabelHeight = bodyLabelSize.height.clamp(0, maxBodyLabelHeight)
2021-01-29 01:46:32 +01:00
let contentViewHeight: CGFloat
if hasAttachments {
2021-02-10 05:33:39 +01:00
contentViewHeight = thumbnailSize + 8 // Add a small amount of spacing above and below the thumbnail
2021-03-29 06:15:25 +02:00
bodyLabel.set(.height, to: 18) // Experimentally determined
2021-01-29 01:46:32 +01:00
} else {
2021-02-10 23:53:27 +01:00
if let authorLabelHeight = authorLabelHeight { // Group thread
2021-02-10 05:33:39 +01:00
contentViewHeight = bodyLabelHeight + (authorLabelHeight + labelStackViewSpacing) + 2 * labelStackViewVMargin
} else {
contentViewHeight = bodyLabelHeight + 2 * smallSpacing
}
2021-01-29 01:46:32 +01:00
}
contentView.set(.height, to: contentViewHeight)
2021-02-10 05:33:39 +01:00
lineView.set(.height, to: contentViewHeight - 8) // Add a small amount of spacing above and below the line
2021-02-10 07:04:26 +01:00
if case .draft = mode {
addSubview(cancelButton)
cancelButton.center(.vertical, in: self)
cancelButton.pin(.right, to: .right, of: self)
}
}
// MARK: Interaction
@objc private func cancel() {
delegate?.handleQuoteViewCancelButtonTapped()
2021-01-29 01:46:32 +01:00
}
}
2021-02-10 07:04:26 +01:00
// MARK: Delegate
protocol QuoteViewDelegate {
func handleQuoteViewCancelButtonTapped()
}