// // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import SignalServiceKit 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: TSResourceStream? { AssertIsOnMainThread() return componentState.stickerAttachment } public var stickerInfo: StickerInfo? { AssertIsOnMainThread() return componentState.stickerInfo } public var linkPreview: OWSLinkPreview? { AssertIsOnMainThread() return componentState.linkPreviewModel } public var linkPreviewAttachment: TSResource? { AssertIsOnMainThread() return componentState.linkPreview?.linkPreviewAttachment } 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 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 wasRemotelyDeleted: Bool { AssertIsOnMainThread() return (interaction as? TSMessage)?.wasRemotelyDeleted == true } public var audioAttachmentStream: TSResourceStream? { AssertIsOnMainThread() return componentState.audioAttachmentStream?.attachmentStream } public var genericAttachmentStream: TSResourceStream? { AssertIsOnMainThread() return componentState.genericAttachmentStream?.attachmentStream } public var bodyMediaAttachmentStreams: [TSResourceStream] { AssertIsOnMainThread() return componentState.bodyMediaAttachmentStreams.map(\.attachmentStream) } public var hasUnloadedAttachments: Bool { if componentState.bodyText == .oversizeTextDownloading { return true } if let audioAttachment = componentState.audioAttachment?.attachment, audioAttachment.asResourceStream() == nil { return true } if let genericAttachment = componentState.genericAttachment?.attachment.attachment, genericAttachment.attachment.asResourceStream() == 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.asResourceStream() == 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 } private var shareableAttachments: [ShareableTSResource] { guard !isViewOnce else { return [] } if let attachment = self.audioAttachmentStream { return [try? attachment.asShareableResource()].compacted() } else if let attachment = self.genericAttachmentStream { return [try? attachment.asShareableResource()].compacted() } else { return self.bodyMediaAttachmentStreams.compactMap { attachment in return try? attachment.asShareableResource() } } } func shareMediaAction(sender: Any?) { guard !isViewOnce else { return } guard !wasRemotelyDeleted else { return } let attachments = shareableAttachments guard !attachments.isEmpty else { return } AttachmentSharing.showShareUI(for: attachments, sender: sender) } var canForwardMessage: Bool { guard !isViewOnce else { return false } guard !wasRemotelyDeleted else { return false } switch messageCellType { case .unknown, .dateHeader, .typingIndicator, .unreadIndicator, .threadDetails, .systemMessage, .unknownThreadWarning, .defaultDisappearingMessageTimer: return false case .giftBadge: return false case .textOnlyMessage, .audio, .genericAttachment, .contactShare, .bodyMedia, .viewOnce, .stickerMessage, .quoteOnlyMessage: return !hasUnloadedAttachments case .paymentAttachment: return false } } } // MARK: - public extension CVComponentState { var displayableBodyText: DisplayableText? { bodyText?.displayableText } var audioAttachmentStream: ReferencedTSResourceStream? { audioAttachment?.attachmentStream } var genericAttachmentStream: ReferencedTSResourceStream? { guard let reference = genericAttachment?.attachment.attachment.reference, let stream = genericAttachment?.attachmentStream else { return nil } return .init(reference: reference, attachmentStream: stream) } var bodyMediaAttachmentStreams: [ReferencedTSResourceStream] { guard let bodyMedia = self.bodyMedia else { return [] } return bodyMedia.items.compactMap { (item) -> ReferencedTSResourceStream? 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: TSResourceStream? { sticker?.attachmentStream?.attachmentStream } var stickerInfo: StickerInfo? { sticker?.stickerMetadata?.stickerInfo } var linkPreviewModel: OWSLinkPreview? { linkPreview?.linkPreview } }