mirror of
https://github.com/lux-tel/session-ios.git
synced 2026-10-30 20:38:32 +00:00
• Tweaked the way gesture recognisers were setup in the message bubbles so they can now be based on the message type rather than the cell type • Fixed an annoying duplicate symbols build issue caused by the libSession build script • Fixed a bug with the styling of quotes • Fixed a bug where quotes that were missing author information could incorrectly show "You" as the author • Fixed an issue where the "Session Pro" menu item was visible when the feature was disabled (not currently connected to anything) • Fixed an issue where the disabled message input box wouldn't show errors correctly • Fixed an issue where message requests received via a community wouldn't result in the message request banner appearing • Fixed an issue where long pressing on a link in a message would show both the open link modal and the long press menu • Fixed an issue where outgoing voice messages wouldn't play correctly (incorrectly considered invalid attachments) • Fixed an issue where you couldn't paste images directly into the message input
179 lines
6.9 KiB
Swift
179 lines
6.9 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import UniformTypeIdentifiers
|
|
import Lucide
|
|
|
|
public final class QuoteView: UIView {
|
|
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
|
|
|
|
enum Mode {
|
|
case regular
|
|
case draft
|
|
}
|
|
enum Direction { case incoming, outgoing }
|
|
|
|
// MARK: - Variables
|
|
|
|
private let viewModel: QuoteViewModel
|
|
private let dataManager: ImageDataManagerType
|
|
private var onCancel: (() -> Void)?
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
public init(
|
|
viewModel: QuoteViewModel,
|
|
dataManager: ImageDataManagerType,
|
|
onCancel: (() -> Void)? = nil
|
|
) {
|
|
self.viewModel = viewModel
|
|
self.dataManager = dataManager
|
|
self.onCancel = onCancel
|
|
|
|
super.init(frame: CGRect.zero)
|
|
|
|
setUpViewHierarchy(viewModel: viewModel)
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
preconditionFailure("Use init(for:maxMessageWidth:) instead.")
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
preconditionFailure("Use init(for:maxMessageWidth:) instead.")
|
|
}
|
|
|
|
private func setUpViewHierarchy(viewModel: QuoteViewModel) {
|
|
// 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 thumbnailSize = QuoteView.thumbnailSize
|
|
let iconSize = QuoteView.iconSize
|
|
let labelStackViewSpacing = QuoteView.labelStackViewSpacing
|
|
let labelStackViewVMargin = QuoteView.labelStackViewVMargin
|
|
let smallSpacing = Values.smallSpacing
|
|
let cancelButtonSize = QuoteView.cancelButtonSize
|
|
|
|
// Main stack view
|
|
let mainStackView = UIStackView(arrangedSubviews: [])
|
|
mainStackView.axis = .horizontal
|
|
mainStackView.spacing = smallSpacing
|
|
mainStackView.alignment = .center
|
|
mainStackView.setCompressionResistance(.vertical, to: .required)
|
|
|
|
// Content view
|
|
let contentView = UIView()
|
|
addSubview(contentView)
|
|
contentView.pin(to: self)
|
|
|
|
if viewModel.hasAttachment {
|
|
let imageContainerView: UIView = UIView()
|
|
imageContainerView.themeBackgroundColor = .messageBubble_overlay
|
|
imageContainerView.layer.cornerRadius = 4
|
|
imageContainerView.layer.masksToBounds = true
|
|
imageContainerView.set(.width, to: thumbnailSize)
|
|
imageContainerView.set(.height, to: thumbnailSize)
|
|
mainStackView.addArrangedSubview(imageContainerView)
|
|
|
|
let imageView: SessionImageView = SessionImageView(
|
|
image: viewModel.fallbackImage,
|
|
dataManager: dataManager
|
|
)
|
|
imageView.themeTintColor = viewModel.targetThemeColor
|
|
imageView.contentMode = .scaleAspectFit
|
|
imageView.set(.width, to: iconSize)
|
|
imageView.set(.height, to: iconSize)
|
|
imageContainerView.addSubview(imageView)
|
|
imageView.center(in: imageContainerView)
|
|
|
|
// Generate the thumbnail if needed
|
|
if let source: ImageDataManager.DataSource = viewModel.quotedAttachmentInfo?.thumbnailSource {
|
|
imageView.loadImage(source) { [weak imageView] buffer in
|
|
guard buffer != nil else { return }
|
|
|
|
imageView?.contentMode = .scaleAspectFill
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// Line view
|
|
let lineView = UIView()
|
|
lineView.themeBackgroundColor = viewModel.lineColor
|
|
mainStackView.addArrangedSubview(lineView)
|
|
|
|
lineView.pin(.top, to: .top, of: mainStackView)
|
|
lineView.pin(.bottom, to: .bottom, of: mainStackView)
|
|
lineView.set(.width, to: Values.accentLineThickness)
|
|
}
|
|
|
|
// Body label
|
|
let bodyLabel = LinkHighlightingLabel()
|
|
bodyLabel.lineBreakMode = .byTruncatingTail
|
|
bodyLabel.numberOfLines = 2
|
|
bodyLabel.themeAttributedText = viewModel.attributedText
|
|
|
|
// Label stack view
|
|
let authorLabel = SessionLabelWithProBadge(
|
|
proBadgeSize: .mini,
|
|
proBadgeThemeBackgroundColor: viewModel.proBadgeThemeColor
|
|
)
|
|
authorLabel.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
|
authorLabel.text = viewModel.author
|
|
authorLabel.themeTextColor = viewModel.targetThemeColor
|
|
authorLabel.lineBreakMode = .byTruncatingTail
|
|
authorLabel.numberOfLines = 1
|
|
authorLabel.isHidden = (authorLabel.text == nil)
|
|
authorLabel.isProBadgeHidden = !viewModel.showProBadge
|
|
authorLabel.setCompressionResistance(.vertical, to: .required)
|
|
|
|
let labelStackView = UIStackView(arrangedSubviews: [ authorLabel, bodyLabel ])
|
|
labelStackView.axis = .vertical
|
|
labelStackView.spacing = labelStackViewSpacing
|
|
labelStackView.distribution = .equalCentering
|
|
labelStackView.isLayoutMarginsRelativeArrangement = true
|
|
labelStackView.layoutMargins = UIEdgeInsets(top: labelStackViewVMargin, left: 0, bottom: labelStackViewVMargin, right: 0)
|
|
labelStackView.setCompressionResistance(.vertical, to: .required)
|
|
mainStackView.addArrangedSubview(labelStackView)
|
|
|
|
// Constraints
|
|
contentView.addSubview(mainStackView)
|
|
mainStackView.pin(to: contentView)
|
|
|
|
if viewModel.mode == .draft {
|
|
// Cancel button
|
|
let cancelButton = UIButton(type: .custom)
|
|
cancelButton.setImage(Lucide.image(icon: .x, size: 24)?.withRenderingMode(.alwaysTemplate), for: .normal)
|
|
cancelButton.themeTintColor = .textPrimary
|
|
cancelButton.set(.width, to: cancelButtonSize)
|
|
cancelButton.set(.height, to: cancelButtonSize)
|
|
cancelButton.addTarget(self, action: #selector(cancel), for: UIControl.Event.touchUpInside)
|
|
|
|
mainStackView.addArrangedSubview(cancelButton)
|
|
cancelButton.center(.vertical, in: self)
|
|
mainStackView.isLayoutMarginsRelativeArrangement = true
|
|
mainStackView.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 1)
|
|
}
|
|
}
|
|
|
|
// MARK: - Interaction
|
|
|
|
@objc private func cancel() {
|
|
onCancel?()
|
|
}
|
|
|
|
// MARK: - Functions
|
|
|
|
public func update(viewModel: QuoteViewModel) {
|
|
subviews.forEach { $0.removeFromSuperview() }
|
|
|
|
setUpViewHierarchy(viewModel: viewModel)
|
|
}
|
|
}
|