migrate persistence from JSON to SwiftData
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s

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:
wt committed 2026-08-18 22:58:18 +07:00
1 parent 0317277d46
commit cbb820a86e
30 files changed
+726 -665

No files matched your search

@@ -24,7 +24,7 @@ enum ChatMediaImageCache {
for message: ChatMessage,
data: Data
) -> ChatMediaPlatformImage? {
let cacheKey = "\(message.conversationID)\u{1F}\(message.id)" as NSString
let cacheKey = "\(message.conversationID)\u{1F}\(message.clientID)" as NSString
if let cached = cache.object(forKey: cacheKey) {
return cached
}
@@ -82,7 +82,7 @@ final class PhoneWatchBridge: NSObject, WCSessionDelegate {
messages: [ChatMessage]
) -> Data? {
let visibleConversations = Array(conversations.prefix(20))
let visibleConversationIDs = Set(visibleConversations.map(\.id))
let visibleConversationIDs = Set(visibleConversations.map(\.jid))
var recentMessagesByConversation: [String: [ChatMessage]] = [:]
// Keep a bounded, chronologically sorted window. MAM can append an old
@@ -103,10 +103,10 @@ final class PhoneWatchBridge: NSObject, WCSessionDelegate {
jid: conversation.jid,
name: conversation.displayName,
unread: conversation.unreadCount,
messages: (recentMessagesByConversation[conversation.id] ?? [])
messages: (recentMessagesByConversation[conversation.jid] ?? [])
.map {
PhoneWatchSnapshot.Message(
id: $0.id,
id: $0.clientID,
body: $0.previewText,
timestamp: $0.timestamp,
outgoing: $0.direction == .outgoing,
@@ -128,7 +128,7 @@ final class PhoneWatchBridge: NSObject, WCSessionDelegate {
let middle = lowerBound + (upperBound - lowerBound) / 2
let existing = sortedMessages[middle]
let existingComesFirst = existing.timestamp < message.timestamp
|| (existing.timestamp == message.timestamp && existing.id < message.id)
|| (existing.timestamp == message.timestamp && existing.clientID < message.clientID)
if existingComesFirst {
lowerBound = middle + 1
} else {