mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			283 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			283 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Swift
		
	
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | 
						|
 | 
						|
import UIKit
 | 
						|
import SessionUIKit
 | 
						|
import SessionMessagingKit
 | 
						|
import SessionUtilitiesKit
 | 
						|
 | 
						|
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 onCancel: (() -> ())?
 | 
						|
 | 
						|
    // MARK: - Lifecycle
 | 
						|
    
 | 
						|
    init(
 | 
						|
        for mode: Mode,
 | 
						|
        authorId: String,
 | 
						|
        quotedText: String?,
 | 
						|
        threadVariant: SessionThread.Variant,
 | 
						|
        currentUserPublicKey: String?,
 | 
						|
        currentUserBlinded15PublicKey: String?,
 | 
						|
        currentUserBlinded25PublicKey: String?,
 | 
						|
        direction: Direction,
 | 
						|
        attachment: Attachment?,
 | 
						|
        onCancel: (() -> ())? = nil
 | 
						|
    ) {
 | 
						|
        self.onCancel = onCancel
 | 
						|
        
 | 
						|
        super.init(frame: CGRect.zero)
 | 
						|
        
 | 
						|
        setUpViewHierarchy(
 | 
						|
            mode: mode,
 | 
						|
            authorId: authorId,
 | 
						|
            quotedText: quotedText,
 | 
						|
            threadVariant: threadVariant,
 | 
						|
            currentUserPublicKey: currentUserPublicKey,
 | 
						|
            currentUserBlinded15PublicKey: currentUserBlinded15PublicKey,
 | 
						|
            currentUserBlinded25PublicKey: currentUserBlinded25PublicKey,
 | 
						|
            direction: direction,
 | 
						|
            attachment: attachment
 | 
						|
        )
 | 
						|
    }
 | 
						|
 | 
						|
    override init(frame: CGRect) {
 | 
						|
        preconditionFailure("Use init(for:maxMessageWidth:) instead.")
 | 
						|
    }
 | 
						|
 | 
						|
    required init?(coder: NSCoder) {
 | 
						|
        preconditionFailure("Use init(for:maxMessageWidth:) instead.")
 | 
						|
    }
 | 
						|
 | 
						|
    private func setUpViewHierarchy(
 | 
						|
        mode: Mode,
 | 
						|
        authorId: String,
 | 
						|
        quotedText: String?,
 | 
						|
        threadVariant: SessionThread.Variant,
 | 
						|
        currentUserPublicKey: String?,
 | 
						|
        currentUserBlinded15PublicKey: String?,
 | 
						|
        currentUserBlinded25PublicKey: String?,
 | 
						|
        direction: Direction,
 | 
						|
        attachment: Attachment?
 | 
						|
    ) {
 | 
						|
        // 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
 | 
						|
        var body: String? = quotedText
 | 
						|
        
 | 
						|
        // 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(to: self)
 | 
						|
        
 | 
						|
        if let attachment: Attachment = attachment {
 | 
						|
            let isAudio: Bool = MimeTypeUtil.isAudio(attachment.contentType)
 | 
						|
            let fallbackImageName: String = (isAudio ? "attachment_audio" : "actionsheet_document_black") // stringlint:disable
 | 
						|
            let imageView: UIImageView = UIImageView(
 | 
						|
                image: UIImage(named: fallbackImageName)?
 | 
						|
                    .resized(to: CGSize(width: iconSize, height: iconSize))?
 | 
						|
                    .withRenderingMode(.alwaysTemplate)
 | 
						|
            )
 | 
						|
            
 | 
						|
            imageView.themeTintColor = {
 | 
						|
                switch mode {
 | 
						|
                    case .regular: return (direction == .outgoing ?
 | 
						|
                        .messageBubble_outgoingText :
 | 
						|
                        .messageBubble_incomingText
 | 
						|
                    )
 | 
						|
                    case .draft: return .textPrimary
 | 
						|
                }
 | 
						|
            }()
 | 
						|
            imageView.contentMode = .center
 | 
						|
            imageView.themeBackgroundColor = .messageBubble_overlay
 | 
						|
            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 = attachment.shortDescription
 | 
						|
            }
 | 
						|
            
 | 
						|
            // Generate the thumbnail if needed
 | 
						|
            if attachment.isVisualMedia {
 | 
						|
                attachment.thumbnail(
 | 
						|
                    size: .small,
 | 
						|
                    success: { [imageView] image, _ in
 | 
						|
                        guard Thread.isMainThread else {
 | 
						|
                            DispatchQueue.main.async {
 | 
						|
                                imageView.image = image
 | 
						|
                                imageView.contentMode = .scaleAspectFill
 | 
						|
                            }
 | 
						|
                            return
 | 
						|
                        }
 | 
						|
                        
 | 
						|
                        imageView.image = image
 | 
						|
                        imageView.contentMode = .scaleAspectFill
 | 
						|
                    },
 | 
						|
                    failure: {}
 | 
						|
                )
 | 
						|
            }
 | 
						|
        }
 | 
						|
        else {
 | 
						|
            // Line view
 | 
						|
            let lineColor: ThemeValue = {
 | 
						|
                switch mode {
 | 
						|
                    case .regular: return (direction == .outgoing ? .messageBubble_outgoingText : .primary)
 | 
						|
                    case .draft: return .primary
 | 
						|
                }
 | 
						|
            }()
 | 
						|
            let lineView = UIView()
 | 
						|
            lineView.themeBackgroundColor = 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 = TappableLabel()
 | 
						|
        bodyLabel.lineBreakMode = .byTruncatingTail
 | 
						|
        bodyLabel.numberOfLines = 2
 | 
						|
        
 | 
						|
        let targetThemeColor: ThemeValue = {
 | 
						|
            switch mode {
 | 
						|
                case .regular: return (direction == .outgoing ?
 | 
						|
                    .messageBubble_outgoingText :
 | 
						|
                    .messageBubble_incomingText
 | 
						|
                )
 | 
						|
                case .draft: return .textPrimary
 | 
						|
            }
 | 
						|
        }()
 | 
						|
        bodyLabel.font = .systemFont(ofSize: Values.smallFontSize)
 | 
						|
        
 | 
						|
        ThemeManager.onThemeChange(observer: bodyLabel) { [weak bodyLabel] theme, primaryColor in
 | 
						|
            guard let textColor: UIColor = theme.color(for: targetThemeColor) else { return }
 | 
						|
            
 | 
						|
            bodyLabel?.attributedText = body
 | 
						|
                .map {
 | 
						|
                    MentionUtilities.highlightMentions(
 | 
						|
                        in: $0,
 | 
						|
                        threadVariant: threadVariant,
 | 
						|
                        currentUserPublicKey: currentUserPublicKey,
 | 
						|
                        currentUserBlinded15PublicKey: currentUserBlinded15PublicKey,
 | 
						|
                        currentUserBlinded25PublicKey: currentUserBlinded25PublicKey,
 | 
						|
                        location: {
 | 
						|
                            switch (mode, direction) {
 | 
						|
                                case (.draft, _): return .quoteDraft
 | 
						|
                                case (_, .outgoing): return .outgoingQuote
 | 
						|
                                case (_, .incoming): return .incomingQuote
 | 
						|
                            }
 | 
						|
                        }(),
 | 
						|
                        textColor: textColor,
 | 
						|
                        theme: theme,
 | 
						|
                        primaryColor: primaryColor,
 | 
						|
                        attributes: [
 | 
						|
                            .foregroundColor: textColor
 | 
						|
                        ]
 | 
						|
                    )
 | 
						|
                }
 | 
						|
                .defaulting(
 | 
						|
                    to: attachment.map {
 | 
						|
                        NSAttributedString(string: $0.shortDescription, attributes: [ .foregroundColor: textColor ])
 | 
						|
                    }
 | 
						|
                )
 | 
						|
                .defaulting(to: NSAttributedString(string: "messageErrorOriginal".localized(), attributes: [ .foregroundColor: textColor ]))
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Label stack view
 | 
						|
        let isCurrentUser: Bool = [
 | 
						|
            currentUserPublicKey,
 | 
						|
            currentUserBlinded15PublicKey,
 | 
						|
            currentUserBlinded25PublicKey
 | 
						|
        ]
 | 
						|
        .compactMap { $0 }
 | 
						|
        .asSet()
 | 
						|
        .contains(authorId)
 | 
						|
        
 | 
						|
        let authorLabel = UILabel()
 | 
						|
        authorLabel.font = .boldSystemFont(ofSize: Values.smallFontSize)
 | 
						|
        authorLabel.text = {
 | 
						|
            guard !isCurrentUser else { return "you".localized() }
 | 
						|
            guard body != nil else {
 | 
						|
                // When we can't find the quoted message we want to hide the author label
 | 
						|
                return Profile.displayNameNoFallback(
 | 
						|
                    id: authorId,
 | 
						|
                    threadVariant: threadVariant
 | 
						|
                )
 | 
						|
            }
 | 
						|
            
 | 
						|
            return Profile.displayName(
 | 
						|
                id: authorId,
 | 
						|
                threadVariant: threadVariant
 | 
						|
            )
 | 
						|
        }()
 | 
						|
        authorLabel.themeTextColor = targetThemeColor
 | 
						|
        authorLabel.lineBreakMode = .byTruncatingTail
 | 
						|
        authorLabel.isHidden = (authorLabel.text == nil)
 | 
						|
        authorLabel.numberOfLines = 1
 | 
						|
        
 | 
						|
        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)
 | 
						|
        mainStackView.addArrangedSubview(labelStackView)
 | 
						|
        
 | 
						|
        // Constraints
 | 
						|
        contentView.addSubview(mainStackView)
 | 
						|
        mainStackView.pin(to: contentView)
 | 
						|
        
 | 
						|
        if mode == .draft {
 | 
						|
            // Cancel button
 | 
						|
            let cancelButton = UIButton(type: .custom)
 | 
						|
            cancelButton.setImage(UIImage(named: "X")?.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)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Interaction
 | 
						|
    
 | 
						|
    @objc private func cancel() {
 | 
						|
        onCancel?()
 | 
						|
    }
 | 
						|
}
 |