// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation import GRDB import SessionUtilitiesKit public struct Quote: Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "quote" } public static let interactionForeignKey = ForeignKey([Columns.interactionId], to: [Interaction.Columns.id]) internal static let originalInteractionForeignKey = ForeignKey( [Columns.timestampMs, Columns.authorId], to: [Interaction.Columns.timestampMs, Interaction.Columns.authorId] ) internal static let profileForeignKey = ForeignKey([Columns.authorId], to: [Profile.Columns.id]) internal static let interaction = belongsTo(Interaction.self, using: interactionForeignKey) private static let profile = hasOne(Profile.self, using: profileForeignKey) private static let quotedInteraction = hasOne(Interaction.self, using: originalInteractionForeignKey) public static let attachment = hasOne(Attachment.self, using: Attachment.quoteForeignKey) public typealias Columns = CodingKeys public enum CodingKeys: String, CodingKey, ColumnExpression { case interactionId case authorId case timestampMs case body case attachmentId } /// The id for the interaction this Quote belongs to public let interactionId: Int64 /// The id for the author this Quote belongs to public let authorId: String /// The timestamp in milliseconds since epoch when the quoted interaction was sent public let timestampMs: Int64 /// The body of the quoted message if the user is quoting a text message or an attachment with a caption public let body: String? /// The id for the attachment this Quote is associated with public let attachmentId: String? // MARK: - Relationships public var interaction: QueryInterfaceRequest { request(for: Quote.interaction) } public var profile: QueryInterfaceRequest { request(for: Quote.profile) } public var attachment: QueryInterfaceRequest { request(for: Quote.attachment) } public var originalInteraction: QueryInterfaceRequest { request(for: Quote.quotedInteraction) } // MARK: - Interaction public init( interactionId: Int64, authorId: String, timestampMs: Int64, body: String?, attachmentId: String? ) { self.interactionId = interactionId self.authorId = authorId self.timestampMs = timestampMs self.body = body self.attachmentId = attachmentId } } // MARK: - Protobuf public extension Quote { init?(_ db: Database, proto: SNProtoDataMessage, interactionId: Int64, thread: SessionThread) throws { guard let quoteProto = proto.quote, quoteProto.id != 0, !quoteProto.author.isEmpty else { return nil } self.interactionId = interactionId self.timestampMs = Int64(quoteProto.id) self.authorId = quoteProto.author // Prefer to generate the text snippet locally if available. let quotedInteraction: Interaction? = try? thread .interactions .filter(Interaction.Columns.authorId == quoteProto.author) .filter(Interaction.Columns.timestampMs == Double(quoteProto.id)) .fetchOne(db) if let quotedInteraction: Interaction = quotedInteraction, quotedInteraction.body?.isEmpty == false { self.body = quotedInteraction.body } else if let body: String = quoteProto.text, !body.isEmpty { self.body = body } else { self.body = nil } // We only use the first attachment if let attachment = quoteProto.attachments.first(where: { $0.thumbnail != nil })?.thumbnail { self.attachmentId = try quotedInteraction .map { quotedInteraction -> Attachment? in // If the quotedInteraction has an attachment then try clone it if let attachment: Attachment = try? quotedInteraction.attachments.fetchOne(db) { return attachment.cloneAsQuoteThumbnail() } // Otherwise if the quotedInteraction has a link preview, try clone that return try? quotedInteraction.linkPreview .fetchOne(db)? .attachment .fetchOne(db)? .cloneAsQuoteThumbnail() } .defaulting(to: Attachment(proto: attachment)) .inserted(db) .id } else { self.attachmentId = nil } // Make sure the quote is valid before completing if self.body == nil && self.attachmentId == nil { return nil } } }