Create ArchiveRecords.swift
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
/// The single SwiftData container backing durable chat history. It lives on a
|
||||
/// plain (non-actor, non-`@MainActor`) type so it can be read from arbitrary
|
||||
/// isolation without hopping.
|
||||
enum LumaPersistence {
|
||||
static let archiveContainer: ModelContainer = {
|
||||
let schema = Schema([
|
||||
ConversationRecord.self,
|
||||
MessageRecord.self,
|
||||
ArchiveMetadataRecord.self,
|
||||
])
|
||||
do {
|
||||
return try ModelContainer(
|
||||
for: schema,
|
||||
configurations: [ModelConfiguration("LumaArchive", schema: schema)]
|
||||
)
|
||||
} catch {
|
||||
// A corrupted on-disk store must not prevent sign-in. Degrade to an
|
||||
// in-memory store for the current session instead.
|
||||
do {
|
||||
return try ModelContainer(
|
||||
for: schema,
|
||||
configurations: [ModelConfiguration(isStoredInMemoryOnly: true)]
|
||||
)
|
||||
} catch {
|
||||
fatalError("Unable to create the Luma SwiftData container: \(error)")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
/// Encodes small Codable value types (reactions, call-history metadata, MAM
|
||||
/// checkpoints, ID sets) into `Data` attributes so the `@Model` records remain
|
||||
/// composed only of SwiftData-native scalar types.
|
||||
enum ArchiveBlobCodec {
|
||||
private static let encoder: JSONEncoder = {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.dateEncodingStrategy = .millisecondsSince1970
|
||||
return encoder
|
||||
}()
|
||||
|
||||
private static let decoder: JSONDecoder = {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .millisecondsSince1970
|
||||
return decoder
|
||||
}()
|
||||
|
||||
static func encode<T: Encodable>(_ value: T) -> Data? {
|
||||
try? encoder.encode(value)
|
||||
}
|
||||
|
||||
static func decode<T: Decodable>(_ type: T.Type, from data: Data?) -> T? {
|
||||
guard let data else { return nil }
|
||||
return try? decoder.decode(type, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
@Model
|
||||
final class ConversationRecord {
|
||||
var accountJID: String
|
||||
var jid: String
|
||||
var displayName: String
|
||||
var lastMessage: String
|
||||
var lastActivity: Date
|
||||
var unreadCount: Int
|
||||
var isOnline: Bool
|
||||
var isPinned: Bool
|
||||
var encryptionPreferenceRaw: String
|
||||
var kindRaw: String
|
||||
var groupNickname: String?
|
||||
var isGroupJoined: Bool
|
||||
var shouldAutojoin: Bool
|
||||
var occupantCount: Int
|
||||
var invitedBy: String?
|
||||
|
||||
init(accountJID: String, conversation: Conversation) {
|
||||
self.accountJID = accountJID
|
||||
self.jid = conversation.jid
|
||||
self.displayName = conversation.displayName
|
||||
self.lastMessage = conversation.lastMessage
|
||||
self.lastActivity = conversation.lastActivity
|
||||
self.unreadCount = conversation.unreadCount
|
||||
self.isOnline = conversation.isOnline
|
||||
self.isPinned = conversation.isPinned
|
||||
self.encryptionPreferenceRaw = conversation.encryptionPreference.rawValue
|
||||
self.kindRaw = conversation.kind.rawValue
|
||||
self.groupNickname = conversation.groupNickname
|
||||
self.isGroupJoined = conversation.isGroupJoined
|
||||
self.shouldAutojoin = conversation.shouldAutojoin
|
||||
self.occupantCount = conversation.occupantCount
|
||||
self.invitedBy = conversation.invitedBy
|
||||
}
|
||||
|
||||
var value: Conversation {
|
||||
Conversation(
|
||||
jid: jid,
|
||||
displayName: displayName,
|
||||
lastMessage: lastMessage,
|
||||
lastActivity: lastActivity,
|
||||
unreadCount: unreadCount,
|
||||
isOnline: isOnline,
|
||||
isPinned: isPinned,
|
||||
encryptionPreference: EncryptionPreference(rawValue: encryptionPreferenceRaw) ?? .inheritGlobal,
|
||||
kind: Conversation.Kind(rawValue: kindRaw) ?? .direct,
|
||||
groupNickname: groupNickname,
|
||||
isGroupJoined: isGroupJoined,
|
||||
shouldAutojoin: shouldAutojoin,
|
||||
occupantCount: occupantCount,
|
||||
invitedBy: invitedBy
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Model
|
||||
final class MessageRecord {
|
||||
var accountJID: String
|
||||
var messageID: String
|
||||
var conversationID: String
|
||||
var senderJID: String
|
||||
var body: String
|
||||
var timestamp: Date
|
||||
var directionRaw: String
|
||||
var deliveryRaw: String
|
||||
var securityRaw: String
|
||||
var kindRaw: String
|
||||
var remoteAttachmentURL: String?
|
||||
var localFilename: String?
|
||||
var mimeType: String?
|
||||
var duration: Double?
|
||||
var byteCount: Int?
|
||||
var encryptionFingerprint: String?
|
||||
var editedAt: Date?
|
||||
var replyToID: String?
|
||||
var replyToJID: String?
|
||||
var replyPreview: String?
|
||||
var forwardedFrom: String?
|
||||
var retractedAt: Date?
|
||||
var originID: String?
|
||||
var stanzaID: String?
|
||||
var senderDisplayName: String?
|
||||
var isGroupMessage: Bool
|
||||
var callHistoryData: Data?
|
||||
var reactionsData: Data?
|
||||
|
||||
init(accountJID: String, message: ChatMessage) {
|
||||
self.accountJID = accountJID
|
||||
self.messageID = message.id
|
||||
self.conversationID = message.conversationID
|
||||
self.senderJID = message.senderJID
|
||||
self.body = message.body
|
||||
self.timestamp = message.timestamp
|
||||
self.directionRaw = message.direction.rawValue
|
||||
self.deliveryRaw = message.delivery.rawValue
|
||||
self.securityRaw = message.security.rawValue
|
||||
self.kindRaw = message.kind.rawValue
|
||||
self.remoteAttachmentURL = message.remoteAttachmentURL
|
||||
self.localFilename = message.localFilename
|
||||
self.mimeType = message.mimeType
|
||||
self.duration = message.duration
|
||||
self.byteCount = message.byteCount
|
||||
self.encryptionFingerprint = message.encryptionFingerprint
|
||||
self.editedAt = message.editedAt
|
||||
self.replyToID = message.replyToID
|
||||
self.replyToJID = message.replyToJID
|
||||
self.replyPreview = message.replyPreview
|
||||
self.forwardedFrom = message.forwardedFrom
|
||||
self.retractedAt = message.retractedAt
|
||||
self.originID = message.originID
|
||||
self.stanzaID = message.stanzaID
|
||||
self.senderDisplayName = message.senderDisplayName
|
||||
self.isGroupMessage = message.isGroupMessage
|
||||
self.callHistoryData = message.callHistory.flatMap { ArchiveBlobCodec.encode($0) }
|
||||
self.reactionsData = ArchiveBlobCodec.encode(message.reactions)
|
||||
}
|
||||
|
||||
var value: ChatMessage {
|
||||
ChatMessage(
|
||||
id: messageID,
|
||||
conversationID: conversationID,
|
||||
senderJID: senderJID,
|
||||
body: body,
|
||||
timestamp: timestamp,
|
||||
direction: ChatMessage.Direction(rawValue: directionRaw) ?? .incoming,
|
||||
delivery: ChatMessage.Delivery(rawValue: deliveryRaw) ?? .sent,
|
||||
security: ChatMessage.Security(rawValue: securityRaw) ?? .plaintext,
|
||||
kind: ChatMessage.Kind(rawValue: kindRaw) ?? .text,
|
||||
remoteAttachmentURL: remoteAttachmentURL,
|
||||
localFilename: localFilename,
|
||||
mimeType: mimeType,
|
||||
duration: duration,
|
||||
byteCount: byteCount,
|
||||
encryptionFingerprint: encryptionFingerprint,
|
||||
editedAt: editedAt,
|
||||
replyToID: replyToID,
|
||||
replyToJID: replyToJID,
|
||||
replyPreview: replyPreview,
|
||||
forwardedFrom: forwardedFrom,
|
||||
retractedAt: retractedAt,
|
||||
originID: originID,
|
||||
stanzaID: stanzaID,
|
||||
senderDisplayName: senderDisplayName,
|
||||
isGroupMessage: isGroupMessage,
|
||||
callHistory: ArchiveBlobCodec.decode(CallHistoryMetadata.self, from: callHistoryData),
|
||||
reactions: ArchiveBlobCodec.decode([MessageReaction].self, from: reactionsData) ?? []
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Model
|
||||
final class ArchiveMetadataRecord {
|
||||
var accountJID: String
|
||||
var schemaVersion: Int
|
||||
var locallyDeletedMessageIDsData: Data
|
||||
var rosterContactJIDsData: Data
|
||||
var lastSuccessfulMAMSync: Date?
|
||||
var lastSuccessfulMAMCursor: String?
|
||||
var mamCheckpointsData: Data
|
||||
|
||||
init(accountJID: String, snapshot: ChatArchive.Snapshot) {
|
||||
self.accountJID = accountJID
|
||||
self.schemaVersion = snapshot.schemaVersion
|
||||
self.locallyDeletedMessageIDsData = ArchiveBlobCodec.encode(Array(snapshot.locallyDeletedMessageIDs)) ?? Data()
|
||||
self.rosterContactJIDsData = ArchiveBlobCodec.encode(Array(snapshot.rosterContactJIDs)) ?? Data()
|
||||
self.lastSuccessfulMAMSync = snapshot.lastSuccessfulMAMSync
|
||||
self.lastSuccessfulMAMCursor = snapshot.lastSuccessfulMAMCursor
|
||||
self.mamCheckpointsData = ArchiveBlobCodec.encode(snapshot.mamCheckpoints) ?? Data()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user