added omemo2
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

This commit is contained in:
wt committed 2026-08-30 05:40:44 +07:00
1 parent 97e8f4bab6
commit 7db327c588
36 files changed
+5042 -114

No files matched your search

+70 -38
View File
@@ -19,6 +19,24 @@ struct PhoneWatchSnapshot: Codable {
let chats: [Chat]
}
/// Plain-value snapshots of the SwiftData models, taken on the main actor
/// before the background queue builds the watch payload. Reading @Model
/// properties off the main actor crashes with EXC_BAD_ACCESS.
private struct PhoneWatchChatData {
let jid: String
let name: String
let unread: Int
}
private struct PhoneWatchMessageData {
let id: String
let conversationID: String
let body: String
let timestamp: Date
let outgoing: Bool
let encrypted: Bool
}
struct WatchVoiceMessage: Sendable {
let transferID: String
let jid: String
@@ -59,18 +77,33 @@ final class PhoneWatchBridge: NSObject, WCSessionDelegate {
session?.activate()
}
@MainActor
func update(conversations: [Conversation], messages: [ChatMessage]) {
guard let session else { return }
// Copy-on-write snapshots are cheap here; the filtering, sorting and
// JSON encoding are not. Keep that work away from SwiftUI's main run
// loop, especially immediately after a MAM page is committed.
// SwiftData models must never be read off the main actor: snapshot
// the fields the watch needs here, then hand plain values to the
// background queue for the filtering, sorting and JSON encoding.
let chats = conversations.map { conversation in
PhoneWatchChatData(
jid: conversation.jid,
name: conversation.displayName,
unread: conversation.unreadCount
)
}
let entries = messages.map { message in
PhoneWatchMessageData(
id: message.clientID,
conversationID: message.conversationID,
body: message.previewText,
timestamp: message.timestamp,
outgoing: message.direction == .outgoing,
encrypted: message.security == .omemo
)
}
snapshotQueue.async { [weak self, weak session] in
guard let self,
let session,
let data = Self.makeSnapshotData(
conversations: conversations,
messages: messages
),
let data = Self.makeSnapshotData(chats: chats, entries: entries),
data != self.latestSnapshot else { return }
self.latestSnapshot = data
self.publishLatestSnapshot(using: session)
@@ -78,57 +111,56 @@ final class PhoneWatchBridge: NSObject, WCSessionDelegate {
}
private static func makeSnapshotData(
conversations: [Conversation],
messages: [ChatMessage]
chats: [PhoneWatchChatData],
entries: [PhoneWatchMessageData]
) -> Data? {
let visibleConversations = Array(conversations.prefix(20))
let visibleConversationIDs = Set(visibleConversations.map(\.jid))
var recentMessagesByConversation: [String: [ChatMessage]] = [:]
let visibleChats = Array(chats.prefix(20))
let visibleChatIDs = Set(visibleChats.map(\.jid))
var recentMessagesByChat: [String: [PhoneWatchMessageData]] = [:]
// Keep a bounded, chronologically sorted window. MAM can append an old
// overlapping stanza after a newer live message, so array order alone
// cannot identify the latest messages reliably.
for message in messages where visibleConversationIDs.contains(message.conversationID) {
var recent = recentMessagesByConversation[message.conversationID] ?? []
let insertionIndex = Self.insertionIndex(for: message, in: recent)
recent.insert(message, at: insertionIndex)
for entry in entries where visibleChatIDs.contains(entry.conversationID) {
var recent = recentMessagesByChat[entry.conversationID] ?? []
let insertionIndex = Self.insertionIndex(for: entry, in: recent)
recent.insert(entry, at: insertionIndex)
if recent.count > 20 {
recent.removeFirst(recent.count - 20)
}
recentMessagesByConversation[message.conversationID] = recent
recentMessagesByChat[entry.conversationID] = recent
}
let chats = visibleConversations.map { conversation in
let snapshotChats = visibleChats.map { chat in
PhoneWatchSnapshot.Chat(
jid: conversation.jid,
name: conversation.displayName,
unread: conversation.unreadCount,
messages: (recentMessagesByConversation[conversation.jid] ?? [])
.map {
PhoneWatchSnapshot.Message(
id: $0.clientID,
body: $0.previewText,
timestamp: $0.timestamp,
outgoing: $0.direction == .outgoing,
encrypted: $0.security == .omemo
)
}
jid: chat.jid,
name: chat.name,
unread: chat.unread,
messages: (recentMessagesByChat[chat.jid] ?? []).map { entry in
PhoneWatchSnapshot.Message(
id: entry.id,
body: entry.body,
timestamp: entry.timestamp,
outgoing: entry.outgoing,
encrypted: entry.encrypted
)
}
)
}
return try? JSONEncoder.watchEncoder.encode(PhoneWatchSnapshot(chats: chats))
return try? JSONEncoder.watchEncoder.encode(PhoneWatchSnapshot(chats: snapshotChats))
}
private static func insertionIndex(
for message: ChatMessage,
in sortedMessages: [ChatMessage]
for entry: PhoneWatchMessageData,
in sorted: [PhoneWatchMessageData]
) -> Int {
var lowerBound = 0
var upperBound = sortedMessages.count
var upperBound = sorted.count
while lowerBound < upperBound {
let middle = lowerBound + (upperBound - lowerBound) / 2
let existing = sortedMessages[middle]
let existingComesFirst = existing.timestamp < message.timestamp
|| (existing.timestamp == message.timestamp && existing.clientID < message.clientID)
let existing = sorted[middle]
let existingComesFirst = existing.timestamp < entry.timestamp
|| (existing.timestamp == entry.timestamp && existing.id < entry.id)
if existingComesFirst {
lowerBound = middle + 1
} else {