mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Added migrations for contacts and started working through thread migration (have contact and closed group threads migrating) Deprecated usage of ECKeyPair in the migrations (want to be able to remove Curve25519Kit in the future)
250 lines
11 KiB
Swift
250 lines
11 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import SessionUIKit
|
|
import SessionMessagingKit
|
|
|
|
final class QuoteView : UIView {
|
|
private let mode: Mode
|
|
private let thread: TSThread
|
|
private let direction: Direction
|
|
private let hInset: CGFloat
|
|
private let maxWidth: CGFloat
|
|
private let delegate: QuoteViewDelegate?
|
|
|
|
private var maxBodyLabelHeight: CGFloat {
|
|
switch mode {
|
|
case .regular: return 60
|
|
case .draft: return 40
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
private var lineColor: UIColor {
|
|
switch (mode, AppModeManager.shared.currentAppMode) {
|
|
case (.regular, .light), (.draft, .light): return .black
|
|
case (.regular, .dark): return (direction == .outgoing) ? .black : Colors.accent
|
|
case (.draft, .dark): return Colors.accent
|
|
}
|
|
}
|
|
|
|
private var textColor: UIColor {
|
|
if case .draft = mode { return Colors.text }
|
|
switch (direction, AppModeManager.shared.currentAppMode) {
|
|
case (.outgoing, .dark), (.incoming, .light): return .black
|
|
default: return .white
|
|
}
|
|
}
|
|
|
|
// MARK: Mode
|
|
enum Mode {
|
|
case regular(ConversationViewItem)
|
|
case draft(OWSQuotedReplyModel)
|
|
}
|
|
|
|
// MARK: Direction
|
|
enum Direction { case incoming, outgoing }
|
|
|
|
// MARK: Settings
|
|
static let thumbnailSize: CGFloat = 48
|
|
static let iconSize: CGFloat = 24
|
|
static let labelStackViewSpacing: CGFloat = 2
|
|
static let labelStackViewVMargin: CGFloat = 4
|
|
static let cancelButtonSize: CGFloat = 33
|
|
|
|
// MARK: Lifecycle
|
|
init(for viewItem: ConversationViewItem, in thread: TSThread?, direction: Direction, hInset: CGFloat, maxWidth: CGFloat) {
|
|
self.mode = .regular(viewItem)
|
|
self.thread = thread ?? TSThread.fetch(uniqueId: viewItem.interaction.uniqueThreadId)!
|
|
self.maxWidth = maxWidth
|
|
self.direction = direction
|
|
self.hInset = hInset
|
|
self.delegate = nil
|
|
super.init(frame: CGRect.zero)
|
|
setUpViewHierarchy()
|
|
}
|
|
|
|
init(for model: OWSQuotedReplyModel, direction: Direction, hInset: CGFloat, maxWidth: CGFloat, delegate: QuoteViewDelegate) {
|
|
self.mode = .draft(model)
|
|
self.thread = TSThread.fetch(uniqueId: model.threadId)!
|
|
self.maxWidth = maxWidth
|
|
self.direction = direction
|
|
self.hInset = hInset
|
|
self.delegate = delegate
|
|
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() {
|
|
// 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
|
|
let hasAttachments = !attachments.isEmpty
|
|
let thumbnailSize = QuoteView.thumbnailSize
|
|
let iconSize = QuoteView.iconSize
|
|
let labelStackViewSpacing = QuoteView.labelStackViewSpacing
|
|
let labelStackViewVMargin = QuoteView.labelStackViewVMargin
|
|
let smallSpacing = Values.smallSpacing
|
|
let cancelButtonSize = QuoteView.cancelButtonSize
|
|
var availableWidth: CGFloat
|
|
// Subtract smallSpacing twice; once for the spacing in between the stack view elements and
|
|
// once for the trailing margin.
|
|
if !hasAttachments {
|
|
availableWidth = maxWidth - 2 * hInset - Values.accentLineThickness - 2 * smallSpacing
|
|
} else {
|
|
availableWidth = maxWidth - 2 * hInset - thumbnailSize - 2 * smallSpacing
|
|
}
|
|
if case .draft = mode {
|
|
availableWidth -= cancelButtonSize
|
|
}
|
|
let availableSpace = CGSize(width: availableWidth, height: .greatestFiniteMagnitude)
|
|
var body = self.body
|
|
// 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)
|
|
contentView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.VerticalEdge.bottom ], to: self)
|
|
contentView.rightAnchor.constraint(lessThanOrEqualTo: self.rightAnchor).isActive = true
|
|
// 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 ?? "")
|
|
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
|
|
imageView.backgroundColor = lineColor
|
|
imageView.layer.cornerRadius = VisibleMessageCell.smallCornerRadius
|
|
imageView.layer.masksToBounds = true
|
|
imageView.set(.width, to: thumbnailSize)
|
|
imageView.set(.height, to: thumbnailSize)
|
|
mainStackView.addArrangedSubview(imageView)
|
|
if (body ?? "").isEmpty {
|
|
body = (thumbnail != nil) ? "Image" : (isAudio ? "Audio" : "Document")
|
|
}
|
|
}
|
|
// Body label
|
|
let bodyLabel = UILabel()
|
|
bodyLabel.numberOfLines = 0
|
|
bodyLabel.lineBreakMode = .byTruncatingTail
|
|
let isOutgoing = (direction == .outgoing)
|
|
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")
|
|
bodyLabel.textColor = textColor
|
|
let bodyLabelSize = bodyLabel.systemLayoutSizeFitting(availableSpace)
|
|
// Label stack view
|
|
var authorLabelHeight: CGFloat?
|
|
if let groupThread = thread as? TSGroupThread {
|
|
let authorLabel = UILabel()
|
|
authorLabel.lineBreakMode = .byTruncatingTail
|
|
authorLabel.text = Profile.displayName(for: authorID, thread: groupThread)
|
|
authorLabel.textColor = textColor
|
|
authorLabel.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
|
let authorLabelSize = authorLabel.systemLayoutSizeFitting(availableSpace)
|
|
authorLabel.set(.height, to: authorLabelSize.height)
|
|
authorLabelHeight = authorLabelSize.height
|
|
let labelStackView = UIStackView(arrangedSubviews: [ authorLabel, bodyLabel ])
|
|
labelStackView.axis = .vertical
|
|
labelStackView.spacing = labelStackViewSpacing
|
|
labelStackView.distribution = .equalCentering
|
|
labelStackView.set(.width, to: max(bodyLabelSize.width, authorLabelSize.width))
|
|
labelStackView.isLayoutMarginsRelativeArrangement = true
|
|
labelStackView.layoutMargins = UIEdgeInsets(top: labelStackViewVMargin, left: 0, bottom: labelStackViewVMargin, right: 0)
|
|
mainStackView.addArrangedSubview(labelStackView)
|
|
} else {
|
|
mainStackView.addArrangedSubview(bodyLabel)
|
|
}
|
|
// Cancel button
|
|
let cancelButton = UIButton(type: .custom)
|
|
let tint: UIColor = isLightMode ? .black : .white
|
|
cancelButton.setImage(UIImage(named: "X")?.withTint(tint), for: UIControl.State.normal)
|
|
cancelButton.set(.width, to: cancelButtonSize)
|
|
cancelButton.set(.height, to: cancelButtonSize)
|
|
cancelButton.addTarget(self, action: #selector(cancel), for: UIControl.Event.touchUpInside)
|
|
// Constraints
|
|
contentView.addSubview(mainStackView)
|
|
mainStackView.pin(to: contentView)
|
|
if !thread.isGroupThread() {
|
|
bodyLabel.set(.width, to: bodyLabelSize.width)
|
|
}
|
|
let bodyLabelHeight = bodyLabelSize.height.clamp(0, maxBodyLabelHeight)
|
|
let contentViewHeight: CGFloat
|
|
if hasAttachments {
|
|
contentViewHeight = thumbnailSize + 8 // Add a small amount of spacing above and below the thumbnail
|
|
bodyLabel.set(.height, to: 18) // Experimentally determined
|
|
} else {
|
|
if let authorLabelHeight = authorLabelHeight { // Group thread
|
|
contentViewHeight = bodyLabelHeight + (authorLabelHeight + labelStackViewSpacing) + 2 * labelStackViewVMargin
|
|
} else {
|
|
contentViewHeight = bodyLabelHeight + 2 * smallSpacing
|
|
}
|
|
}
|
|
contentView.set(.height, to: contentViewHeight)
|
|
lineView.set(.height, to: contentViewHeight - 8) // Add a small amount of spacing above and below the line
|
|
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()
|
|
}
|
|
}
|
|
|
|
// MARK: Delegate
|
|
protocol QuoteViewDelegate {
|
|
|
|
func handleQuoteViewCancelButtonTapped()
|
|
}
|