session-ios/SessionMessagingKit/Database/Models/Quote.swift
Morgan Pretty f8b2f73f7b Fixed a few issues found during QA
Fixed an issue where quotes containing images wouldn't send
Fixed an issue where a MessageSend job could get stuck in an infinite retry loop if it had an attachment in an invalid state
Fixed an issue where quotes containing non-media files wouldn't contain the correct data
Fixed an issue where the quote thumbnail was getting the wrong content mode set
Fixed an issue where the local disappearing messages config wasn't getting generated correctly
Fixed an issue where the format parameters for the disappearing message info message were the wrong way around in one case
Updated the AttachmentUploadJob to try to support images which haven't completed downloading (untested as it's not supported via the UI)
2022-08-04 13:25:46 +10:00

140 lines
5 KiB
Swift

// 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<Interaction> {
request(for: Quote.interaction)
}
public var profile: QueryInterfaceRequest<Profile> {
request(for: Quote.profile)
}
public var attachment: QueryInterfaceRequest<Attachment> {
request(for: Quote.attachment)
}
public var originalInteraction: QueryInterfaceRequest<Interaction> {
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
}
}
}