// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation import GRDB public struct QuotedReplyModel { public let threadId: String public let authorId: String public let timestampMs: Int64 public let body: String? public let attachment: Attachment? public let contentType: String? public let sourceFileName: String? public let thumbnailDownloadFailed: Bool // MARK: - Initialization init( threadId: String, authorId: String, timestampMs: Int64, body: String?, attachment: Attachment?, contentType: String?, sourceFileName: String?, thumbnailDownloadFailed: Bool ) { self.attachment = attachment self.threadId = threadId self.authorId = authorId self.timestampMs = timestampMs self.body = body self.contentType = contentType self.sourceFileName = sourceFileName self.thumbnailDownloadFailed = thumbnailDownloadFailed } public static func quotedReplyForSending( threadId: String, authorId: String, variant: Interaction.Variant, body: String?, timestampMs: Int64, attachments: [Attachment]?, linkPreview: LinkPreview? ) -> QuotedReplyModel? { guard variant == .standardOutgoing || variant == .standardIncoming else { return nil } var quotedText: String? = body var quotedAttachment: Attachment? = attachments?.first // If the attachment is "oversize text", try the quote as a reply to text, not as // a reply to an attachment if quotedText?.isEmpty == true, let attachment: Attachment = quotedAttachment, attachment.contentType == OWSMimeTypeOversizeTextMessage, ( (variant == .standardIncoming && attachment.state == .downloaded) || attachment.state != .failed ), let originalFilePath: String = attachment.originalFilePath { quotedText = "" if let textData: Data = try? Data(contentsOf: URL(fileURLWithPath: originalFilePath)), let oversizeText: String = String(data: textData, encoding: .utf8) { // The attachment is going to be sent as text instead quotedAttachment = nil // We don't need to include the entire text body of the message, just // enough to render a snippet. kOversizeTextMessageSizeThreshold is our // limit on how long text should be in protos since they'll be stored in // the database. We apply this constant here for the same reasons. // // First, truncate to the rough max characters var truncatedText: String = oversizeText.substring(to: Int(Interaction.oversizeTextMessageSizeThreshold - 1)) // But kOversizeTextMessageSizeThreshold is in _bytes_, not characters, // so we need to continue to trim the string until it fits. while truncatedText.lengthOfBytes(using: .utf8) >= Interaction.oversizeTextMessageSizeThreshold { // A very coarse binary search by halving is acceptable, since // kOversizeTextMessageSizeThreshold is much longer than our target // length of "three short lines of text on any device we might // display this on. // // The search will always converge since in the worst case (namely // a single character which in utf-8 is >= 1024 bytes) the loop will // exit when the string is empty. truncatedText = truncatedText.substring(to: truncatedText.count / 2) } if truncatedText.lengthOfBytes(using: .utf8) < Interaction.oversizeTextMessageSizeThreshold { quotedText = truncatedText } } } return QuotedReplyModel( threadId: threadId, authorId: authorId, timestampMs: timestampMs, body: (quotedText == nil && quotedAttachment == nil ? "" : quotedText), attachment: quotedAttachment, contentType: quotedAttachment?.contentType, sourceFileName: quotedAttachment?.sourceFilename, thumbnailDownloadFailed: false ) } } // MARK: - Convenience public extension QuotedReplyModel { func generateAttachmentThumbnailIfNeeded(_ db: Database) throws -> String? { guard let sourceAttachment: Attachment = self.attachment else { return nil } return try sourceAttachment .cloneAsThumbnail()? .inserted(db) .id } }