Signal-iOS/Signal/ConversationView/CVItemViewModelImpl.swift
2024-04-04 12:57:31 -07:00

307 lines
8.0 KiB
Swift

//
// 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: StickerMetadata? {
AssertIsOnMainThread()
return componentState.stickerMetadata
}
public var stickerAttachment: TSAttachmentStream? {
AssertIsOnMainThread()
return componentState.stickerAttachment
}
public var stickerInfo: StickerInfo? {
AssertIsOnMainThread()
return componentState.stickerInfo
}
public var linkPreview: OWSLinkPreview? {
AssertIsOnMainThread()
return componentState.linkPreviewModel
}
public var linkPreviewAttachment: TSAttachment? {
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: TSAttachmentStream? {
AssertIsOnMainThread()
return componentState.audioAttachmentStream
}
public var genericAttachmentStream: TSAttachmentStream? {
AssertIsOnMainThread()
return componentState.genericAttachmentStream
}
public var bodyMediaAttachmentStreams: [TSAttachmentStream] {
AssertIsOnMainThread()
return componentState.bodyMediaAttachmentStreams
}
public var hasUnloadedAttachments: Bool {
if componentState.bodyText == .oversizeTextDownloading {
return true
}
if componentState.audioAttachment?.attachment as? TSAttachmentPointer != nil {
return true
}
if componentState.genericAttachment?.attachmentPointer != nil {
return true
}
if componentState.sticker?.attachmentPointer != nil {
return true
}
guard let bodyMedia = componentState.bodyMedia else {
return false
}
return !bodyMedia.items.compactMap { $0.attachment as? TSAttachmentPointer }.isEmpty
}
}
// 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: [TSAttachmentStream] {
guard !isViewOnce else {
return []
}
if let attachment = self.audioAttachmentStream {
return [attachment]
} else if let attachment = self.genericAttachmentStream {
return [attachment]
} else {
return self.bodyMediaAttachmentStreams.filter { attachment in
guard attachment.isValidVisualMedia else {
return false
}
if attachment.isImageMimeType || attachment.getAnimatedMimeType() == .animated {
return true
}
if attachment.isVideoMimeType,
let filePath = attachment.originalFilePath,
UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(filePath) {
return true
}
return false
}
}
}
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: TSAttachmentStream? {
audioAttachment?.attachmentStream
}
var genericAttachmentStream: TSAttachmentStream? {
genericAttachment?.attachmentStream
}
var bodyMediaAttachmentStreams: [TSAttachmentStream] {
guard let bodyMedia = self.bodyMedia else {
return []
}
return bodyMedia.items.compactMap { $0.attachmentStream }
}
var contactShareModel: ContactShareViewModel? {
guard let contactShare = self.contactShare else {
return nil
}
return contactShare.state.contactShare
}
var stickerMetadata: StickerMetadata? {
sticker?.stickerMetadata
}
var stickerAttachment: TSAttachmentStream? {
sticker?.attachmentStream
}
var stickerInfo: StickerInfo? {
sticker?.stickerMetadata?.stickerInfo
}
var linkPreviewModel: OWSLinkPreview? {
linkPreview?.linkPreview
}
}