migrate persistence from JSON to SwiftData
Models: - Convert Conversation and ChatMessage to SwiftData @Model classes with a one-to-many relationship and cascade delete. - Rename ChatMessage.id to clientID and drop Conversation.id in favor of jid (the string id would clash with PersistentIdentifier). - Keep MessageReaction as a Codable value stored in an array attribute. Persistence: - Add ArchiveStore, a per-account ModelContainer/ModelContext that replaces the JSON ChatArchive with load/save/erase. - Add ArchiveMetadataRecord for the durable MAM checkpoints, cursor, locally deleted message IDs and roster contact JIDs. - Add LegacyArchiveImporter to import the old JSON snapshot once and delete the file afterwards, preserving existing history. AppModel: - Replace ChatArchive with ArchiveStore and persist via context.save(); insert new models and delete the replaced object on upsert merges so SwiftData never keeps an orphaned duplicate. Tests: - Replace ChatArchiveTests with ArchiveStoreTests (store round-trip and legacy import) and drop the now-obsolete Codable round-trip assertions.
This commit is contained in:
1 parent
0317277d46
commit
cbb820a86e
30 files changed
+726
-665
No files matched your search
@@ -1,13 +1,14 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
struct Conversation: Codable, Identifiable, Hashable, Sendable {
|
||||
enum Kind: String, Codable, Sendable {
|
||||
@Model
|
||||
final class Conversation {
|
||||
enum Kind: String, Codable {
|
||||
case direct
|
||||
case group
|
||||
}
|
||||
|
||||
let id: String
|
||||
var jid: String
|
||||
@Attribute(.unique) var jid: String
|
||||
var displayName: String
|
||||
var lastMessage: String
|
||||
var lastActivity: Date
|
||||
@@ -22,6 +23,9 @@ struct Conversation: Codable, Identifiable, Hashable, Sendable {
|
||||
var occupantCount: Int
|
||||
var invitedBy: String?
|
||||
|
||||
@Relationship(deleteRule: .cascade, inverse: \ChatMessage.conversation)
|
||||
var messages: [ChatMessage] = []
|
||||
|
||||
init(
|
||||
jid: String,
|
||||
displayName: String? = nil,
|
||||
@@ -39,7 +43,6 @@ struct Conversation: Codable, Identifiable, Hashable, Sendable {
|
||||
invitedBy: String? = nil
|
||||
) {
|
||||
let normalized = jid.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
self.id = normalized
|
||||
self.jid = normalized
|
||||
self.displayName = displayName?.nilIfBlank ?? normalized
|
||||
self.lastMessage = lastMessage
|
||||
@@ -56,46 +59,6 @@ struct Conversation: Codable, Identifiable, Hashable, Sendable {
|
||||
self.invitedBy = invitedBy?.nilIfBlank
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case jid
|
||||
case displayName
|
||||
case lastMessage
|
||||
case lastActivity
|
||||
case unreadCount
|
||||
case isOnline
|
||||
case isPinned
|
||||
case encryptionPreference
|
||||
case kind
|
||||
case groupNickname
|
||||
case isGroupJoined
|
||||
case shouldAutojoin
|
||||
case occupantCount
|
||||
case invitedBy
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
jid = try values.decode(String.self, forKey: .jid)
|
||||
id = try values.decodeIfPresent(String.self, forKey: .id) ?? jid.lowercased()
|
||||
displayName = try values.decode(String.self, forKey: .displayName)
|
||||
lastMessage = try values.decode(String.self, forKey: .lastMessage)
|
||||
lastActivity = try values.decode(Date.self, forKey: .lastActivity)
|
||||
unreadCount = try values.decode(Int.self, forKey: .unreadCount)
|
||||
isOnline = try values.decode(Bool.self, forKey: .isOnline)
|
||||
isPinned = try values.decode(Bool.self, forKey: .isPinned)
|
||||
encryptionPreference = try values.decodeIfPresent(
|
||||
EncryptionPreference.self,
|
||||
forKey: .encryptionPreference
|
||||
) ?? .inheritGlobal
|
||||
kind = try values.decodeIfPresent(Kind.self, forKey: .kind) ?? .direct
|
||||
groupNickname = try values.decodeIfPresent(String.self, forKey: .groupNickname)
|
||||
isGroupJoined = try values.decodeIfPresent(Bool.self, forKey: .isGroupJoined) ?? false
|
||||
shouldAutojoin = try values.decodeIfPresent(Bool.self, forKey: .shouldAutojoin) ?? false
|
||||
occupantCount = max(0, try values.decodeIfPresent(Int.self, forKey: .occupantCount) ?? 0)
|
||||
invitedBy = try values.decodeIfPresent(String.self, forKey: .invitedBy)
|
||||
}
|
||||
|
||||
var isGroup: Bool { kind == .group }
|
||||
|
||||
var initials: String {
|
||||
|
||||
Reference in new issue
Block a user