TM-SGNL-iOS/SignalServiceKit/Messages/Interactions/Quotes/DraftQuotedReplyModel.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

296 lines
11 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
// View model for a draft which has already fetched any attachments
// from the original message.
public class DraftQuotedReplyModel {
public let originalMessageTimestamp: UInt64?
public let originalMessageAuthorAddress: SignalServiceAddress
public let threadUniqueId: String
public let isOriginalMessageAuthorLocalUser: Bool
// MARK: Attachments
public indirect enum Content {
/// The original message had text with no attachment
case text(MessageBody)
// MARK: - "Special" types
/// The original message was a gift badge
case giftBadge
/// The original message was a payment.
/// String is the displayable text.
case payment(String)
/// The original message was view-once, so only
/// placeholder information should be shown.
case viewOnce
/// The original message was a contact share
case contactShare(OWSContact)
/// The original message is a story reaction emoji
case storyReactionEmoji(String)
// MARK: - Attachment types
/// The original message had an attachment, but it could not
/// be thumbnail-ed
case attachmentStub(
MessageBody?,
QuotedMessageAttachmentReference.Stub
)
/// The original message had an attachment that can be thumbnail-ed,
/// though it may not actually be thumbnail-ed *yet*.
///
/// - Note:
/// This includes sticker messages, which are thumbnailable attachments.
case attachment(
MessageBody?,
attachmentRef: AttachmentReference,
attachment: Attachment,
thumbnailImage: UIImage?
)
// MARK: - Edit
/// A draft of an edit applied to an _existing_ quoted reply, with
/// the existing quoted reply's information provided.
case edit(
TSMessage,
TSQuotedMessage,
content: Content
)
// MARK: - Convenience
public var isGiftBadge: Bool {
switch self {
case .giftBadge:
return true
default:
return false
}
}
public var isViewOnce: Bool {
switch self {
case .viewOnce:
return true
default:
return false
}
}
public var isRemotelySourced: Bool {
switch self {
case .edit(_, let quotedMessage, _):
// The only way we end up with a "remotely sourced"
// draft is if we edit a quoted reply that was initially
// created on a linked device.
return quotedMessage.bodySource == .remote
default:
return false
}
}
public var renderingFlag: AttachmentReference.RenderingFlag {
switch self {
case .attachment(_, let attachmentRef, _, _):
return attachmentRef.renderingFlag
case .edit(_, _, let content):
return content.renderingFlag
default:
return .default
}
}
}
public let content: Content
internal init(
originalMessageTimestamp: UInt64?,
originalMessageAuthorAddress: SignalServiceAddress,
isOriginalMessageAuthorLocalUser: Bool,
threadUniqueId: String,
content: Content
) {
self.originalMessageTimestamp = originalMessageTimestamp
self.originalMessageAuthorAddress = originalMessageAuthorAddress
self.isOriginalMessageAuthorLocalUser = isOriginalMessageAuthorLocalUser
self.threadUniqueId = threadUniqueId
self.content = content
}
public static func fromOriginalPaymentMessage(
_ originalMessage: TSMessage,
amountString: String,
tx: SDSAnyReadTransaction
) -> DraftQuotedReplyModel? {
let authorAddress: SignalServiceAddress? = {
if originalMessage.isOutgoing {
return DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx.asV2Read)?.aciAddress
} else if let incomingMessage = originalMessage as? TSIncomingMessage {
return incomingMessage.authorAddress
} else {
return nil
}
}()
guard let authorAddress else {
return nil
}
owsAssertDebug(originalMessage is OWSPaymentMessage)
return DraftQuotedReplyModel(
originalMessageTimestamp: originalMessage.timestamp,
originalMessageAuthorAddress: authorAddress,
isOriginalMessageAuthorLocalUser: originalMessage.isOutgoing,
threadUniqueId: originalMessage.uniqueThreadId,
content: .payment(amountString)
)
}
public static func forEditingOriginalPaymentMessage(
originalMessage: TSMessage,
replyMessage: TSMessage,
quotedReply: TSQuotedMessage,
amountString: String,
tx: SDSAnyReadTransaction
) -> DraftQuotedReplyModel? {
let authorAddress: SignalServiceAddress? = {
if originalMessage.isOutgoing {
return DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx.asV2Read)?.aciAddress
} else if let incomingMessage = originalMessage as? TSIncomingMessage {
return incomingMessage.authorAddress
} else {
return nil
}
}()
guard let authorAddress else {
return nil
}
owsAssertDebug(originalMessage is OWSPaymentMessage)
return DraftQuotedReplyModel(
originalMessageTimestamp: originalMessage.timestamp,
originalMessageAuthorAddress: authorAddress,
isOriginalMessageAuthorLocalUser: originalMessage.isOutgoing,
threadUniqueId: originalMessage.uniqueThreadId,
content: .edit(replyMessage, quotedReply, content: .payment(amountString))
)
}
public var bodyForSending: MessageBody? {
return Self.bodyForSending(content, isOriginalMessageAuthorLocalUser: isOriginalMessageAuthorLocalUser)
}
private static func bodyForSending(_ content: DraftQuotedReplyModel.Content, isOriginalMessageAuthorLocalUser: Bool) -> MessageBody? {
switch content {
case .attachmentStub(let body, _):
return body
case .attachment(let body, _, _, _):
return body
case .edit(_, _, let innerContent):
return bodyForSending(innerContent, isOriginalMessageAuthorLocalUser: isOriginalMessageAuthorLocalUser)
case .contactShare(let contact):
return MessageBody(
text: "👤 " + contact.name.displayName,
ranges: .empty
)
case .viewOnce:
return MessageBody(
text: OWSLocalizedString(
"PER_MESSAGE_EXPIRATION_NOT_VIEWABLE",
comment: "inbox cell and notification text for an already viewed view-once media message."
),
ranges: .empty
)
case .payment(let text):
return MessageBody(text: text, ranges: .empty)
case .text(let body):
return body
case .giftBadge:
return nil
case .storyReactionEmoji(let emoji):
let formatString: String
if isOriginalMessageAuthorLocalUser {
formatString = OWSLocalizedString(
"STORY_REACTION_QUOTE_FORMAT_SECOND_PERSON",
comment: "quote text for a reaction to a story by the user (the header on the bubble says \"You\"). Embeds {{reaction emoji}}"
)
} else {
formatString = OWSLocalizedString(
"STORY_REACTION_QUOTE_FORMAT_THIRD_PERSON",
comment: "quote text for a reaction to a story by some other user (the header on the bubble says their name, e.g. \"Bob\"). Embeds {{reaction emoji}}"
)
}
let text = String(
format: formatString,
emoji
)
return MessageBody(text: text, ranges: .empty)
}
}
}
// MARK: - Equatable
extension DraftQuotedReplyModel: Equatable {
public static func == (lhs: DraftQuotedReplyModel, rhs: DraftQuotedReplyModel) -> Bool {
return lhs.originalMessageTimestamp == rhs.originalMessageTimestamp
&& lhs.originalMessageAuthorAddress.isEqualToAddress(rhs.originalMessageAuthorAddress)
&& lhs.content == rhs.content
}
}
extension DraftQuotedReplyModel.Content: Equatable {
public static func == (lhs: DraftQuotedReplyModel.Content, rhs: DraftQuotedReplyModel.Content) -> Bool {
switch (lhs, rhs) {
case (.giftBadge, .giftBadge), (.viewOnce, .viewOnce):
return true
case let (.payment(lhsBody), .payment(rhsBody)):
return lhsBody == rhsBody
case let (.text(lhsBody), .text(rhsBody)):
return lhsBody == rhsBody
case let (.contactShare(lhsContact), .contactShare(rhsContact)):
return lhsContact == rhsContact
case let (.storyReactionEmoji(lhsEmoji), .storyReactionEmoji(rhsEmoji)):
return lhsEmoji == rhsEmoji
case let (.edit(lhsMessage, lhsQuotedReply, lhsContent), .edit(rhsMessage, rhsQuotedReply, rhsContent)):
return lhsMessage == rhsMessage
&& lhsQuotedReply == rhsQuotedReply
&& lhsContent == rhsContent
case let (.attachmentStub(lhsBody, lhsStub), .attachmentStub(rhsBody, rhsStub)):
return lhsBody == rhsBody && lhsStub == rhsStub
case let (
.attachment(lhsBody, _, lhsAttachment, lhsThumbnailImage),
.attachment(rhsBody, _, rhsAttachment, rhsThumbnailImage)
):
return lhsBody == rhsBody
&& lhsAttachment.id == rhsAttachment.id
&& lhsThumbnailImage == rhsThumbnailImage
case
(.giftBadge, _),
(.payment, _),
(.text, _),
(.viewOnce, _),
(.contactShare, _),
(.attachmentStub, _),
(.attachment, _),
(.edit, _),
(.storyReactionEmoji, _),
(_, .giftBadge),
(_, .payment),
(_, .text),
(_, .viewOnce),
(_, .contactShare),
(_, .attachmentStub),
(_, .attachment),
(_, .edit),
(_, .storyReactionEmoji):
return false
}
}
}