Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/ConversationView/CVItemViewModelImpl.swift
1 views
//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

public import SignalServiceKit
public import SignalUI

public class CVComponentStateWrapper: NSObject, CVItemViewModel {
    public var interaction: TSInteraction
    public var componentState: CVComponentState

    init(interaction: TSInteraction, componentState: CVComponentState) {
        self.interaction = interaction
        self.componentState = componentState
    }

    public var contactShare: ContactShareViewModel? {
        AssertIsOnMainThread()

        return componentState.contactShareModel
    }

    public var isGiftBadge: Bool {
        AssertIsOnMainThread()

        return componentState.giftBadge != nil
    }

    public var stickerMetadata: (any StickerMetadata)? {
        AssertIsOnMainThread()

        return componentState.stickerMetadata
    }

    public var stickerAttachment: AttachmentStream? {
        AssertIsOnMainThread()

        return componentState.stickerAttachment
    }

    public var linkPreview: OWSLinkPreview? {
        AssertIsOnMainThread()

        return componentState.linkPreviewModel
    }

    public var hasRenderableContent: Bool {
        return componentState.hasRenderableContent
    }
}

// This class should only be accessed on the main thread.
public class CVItemViewModelImpl: CVComponentStateWrapper {
    public let renderItem: CVRenderItem

    public init(renderItem: CVRenderItem) {
        AssertIsOnMainThread()

        self.renderItem = renderItem

        super.init(interaction: renderItem.interaction, componentState: renderItem.componentState)
    }

    // MARK: -

    public var thread: TSThread {
        AssertIsOnMainThread()

        return renderItem.itemModel.thread
    }

    public var messageCellType: CVMessageCellType {
        AssertIsOnMainThread()

        return componentState.messageCellType
    }

    public var paymentAttachment: CVComponentState.PaymentAttachment? {
        AssertIsOnMainThread()

        return componentState.paymentAttachment
    }

    public var archivedPaymentAttachment: CVComponentState.ArchivedPaymentAttachment? {
        AssertIsOnMainThread()

        return componentState.archivedPaymentAttachment
    }

    public var reactionState: InteractionReactionState? {
        AssertIsOnMainThread()

        return componentState.reactions?.reactionState
    }

    public var displayableBodyText: DisplayableText? {
        AssertIsOnMainThread()

        return componentState.displayableBodyText
    }

    public var isViewOnce: Bool {
        AssertIsOnMainThread()

        guard let message = interaction as? TSMessage else {
            return false
        }
        return message.isViewOnceMessage
    }

    public var isSmsMessageRestoredFromBackup: Bool {
        AssertIsOnMainThread()

        guard let message = interaction as? TSMessage else {
            return false
        }

        return message.isSmsMessageRestoredFromBackup
    }

    public var wasRemotelyDeleted: Bool {
        AssertIsOnMainThread()

        return (interaction as? TSMessage)?.wasRemotelyDeleted == true
    }

    public var audioAttachmentStream: AttachmentStream? {
        AssertIsOnMainThread()

        return componentState.audioAttachmentStream?.attachmentStream
    }

    public var genericAttachmentStream: AttachmentStream? {
        AssertIsOnMainThread()

        return componentState.genericAttachmentStream?.attachmentStream
    }

    public var bodyMediaAttachmentStreams: [AttachmentStream] {
        AssertIsOnMainThread()

        return componentState.bodyMediaAttachmentStreams.map(\.attachmentStream)
    }

    private var hasUnloadedAttachments: Bool {

        if componentState.bodyText == .oversizeTextDownloading {
            return true
        }
        if
            let audioAttachment = componentState.audioAttachment?.attachment,
            audioAttachment.asStream() == nil
        {
            return true
        }
        if
            let genericAttachment = componentState.genericAttachment?.attachment.attachment,
            genericAttachment.attachment.asStream() == nil
        {
            return true
        }
        if
            let sticker = componentState.sticker,
            sticker.attachmentStream == nil
        {
            return true
        }
        guard let bodyMedia = componentState.bodyMedia else {
            return false
        }
        return bodyMedia.items.contains(where: { $0.attachment.attachment.attachment.asStream() == nil })
    }
}

// MARK: - Actions

extension CVItemViewModelImpl {

    var canEditMessage: Bool {
        return DependenciesBridge.shared.editManager.canShowEditMenu(interaction: interaction, thread: thread)
    }

    var canCopyOrShareOrSpeakText: Bool {
        guard !isViewOnce else {
            return false
        }
        // TODO: We could hypothetically support copying other
        // items like contact shares.
        return displayableBodyText != nil
    }

    func copyTextAction() {
        guard !isViewOnce else {
            return
        }
        // TODO: We could hypothetically support copying other
        // items like contact shares.
        guard let displayableBodyText = self.displayableBodyText else {
            return
        }
        BodyRangesTextView.copyToPasteboard(displayableBodyText.fullTextValue)
    }

    var canShareMedia: Bool {
        !shareableAttachments.isEmpty
    }

    func shareMediaAction(sender: Any?) {
        let attachments = shareableAttachments
        guard !attachments.isEmpty else {
            return
        }

        AttachmentSharing.showShareUI(for: attachments, sender: sender)
    }

    private var shareableAttachments: [ShareableAttachment] {
        guard !isViewOnce else {
            return []
        }

        guard !wasRemotelyDeleted else {
            return []
        }

        if let attachment = self.componentState.audioAttachmentStream {
            return (try? [attachment].asShareableAttachments()) ?? []
        }

        if let attachment = self.componentState.genericAttachmentStream {
            return (try? [attachment].asShareableAttachments()) ?? []
        }

        return []
    }

    var canSaveMedia: Bool {
        !saveableAttachments.isEmpty
    }

    func saveMediaAction() {
        AttachmentSaving.saveToPhotoLibrary(
            referencedAttachmentStreams: saveableAttachments,
        )
    }

    private var saveableAttachments: [ReferencedAttachmentStream] {
        return self.componentState.bodyMediaAttachmentStreams
    }

    var canForwardMessage: Bool {
        guard !isViewOnce else {
            return false
        }

        guard !isSmsMessageRestoredFromBackup else {
            return false
        }

        guard !wasRemotelyDeleted else {
            return false
        }

        switch messageCellType {
        case .unknown, .dateHeader, .typingIndicator, .unreadIndicator, .threadDetails, .systemMessage, .unknownThreadWarning, .defaultDisappearingMessageTimer, .collapseSet:
            return false
        case .giftBadge:
            return false
        case .textOnlyMessage, .audio, .genericAttachment, .contactShare, .bodyMedia, .viewOnce, .stickerMessage, .quoteOnlyMessage:
            return !hasUnloadedAttachments
        case .paymentAttachment, .archivedPaymentAttachment, .undownloadableAttachment:
            return false
        case .poll:
            return false
        }
    }
}

// MARK: -

public extension CVComponentState {

    var displayableBodyText: DisplayableText? {
        bodyText?.displayableText
    }

    var audioAttachmentStream: ReferencedAttachmentStream? {
        audioAttachment?.attachmentStream
    }

    var genericAttachmentStream: ReferencedAttachmentStream? {
        guard
            let reference = genericAttachment?.attachment.attachment.reference,
            let stream = genericAttachment?.attachmentStream
        else {
            return nil
        }
        return .init(reference: reference, attachmentStream: stream)
    }

    var bodyMediaAttachmentStreams: [ReferencedAttachmentStream] {
        guard let bodyMedia = self.bodyMedia else {
            return []
        }
        return bodyMedia.items.compactMap { item -> ReferencedAttachmentStream? in
            guard let stream = item.attachmentStream else {
                return nil
            }
            return .init(reference: item.attachment.attachment.reference, attachmentStream: stream)
        }
    }

    var contactShareModel: ContactShareViewModel? {
        guard let contactShare = self.contactShare else {
            return nil
        }
        return contactShare.state.contactShare
    }

    var stickerMetadata: (any StickerMetadata)? {
        sticker?.stickerMetadata
    }

    var stickerAttachment: AttachmentStream? {
        sticker?.attachmentStream?.attachmentStream
    }

    var linkPreviewModel: OWSLinkPreview? {
        linkPreview?.linkPreview
    }
}