#12 sync call history between devices via carbons and MAM
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

- After a call ends, send a plaintext <call-history/> service message
  to the user's own bare JID with direction, status, duration, start
  time, peer and video flag plus the call id as origin-id.
- Intercept the payload on live, carbon and MAM paths and upsert the
  same system call card on every device; deduplication reuses the
  origin-id as clientID, and missed incoming calls bump unread.
- Cover payload parsing and round-trip in CallHistorySyncTests, guard
  the namespace and interception in verify.sh, and document the flow
  in ARCHITECTURE.md.
This commit is contained in:
wt committed 2026-08-29 20:51:28 +07:00
1 parent 0d78e02d2b
commit 1a26651daa
10 files changed
+269 -18

No files matched your search

+38
View File
@@ -2090,6 +2090,8 @@ final class AppModel: ObservableObject {
activeCall = call
case .callHistory(let entry):
recordCallHistory(entry)
case .callHistorySync(let envelope):
recordSyncedCallHistory(envelope)
case .callError(let message):
errorMessage = message
case .recoverableError(let message):
@@ -2295,6 +2297,42 @@ final class AppModel: ObservableObject {
&& entry.outcome == .missed
&& selectedConversationID != entry.peerJID.lowercased()
_ = upsertMessage(message, incrementUnread: isUnreadMissedCall)
// Sync the card to the user's other devices via Carbons/MAM.
xmpp.syncCallHistory(entry)
}
/// Applies a call card received from another of the user's devices. The
/// envelope id equals the origin-id of the sync message, which also equals
/// the originating device's local clientID, so Carbons and MAM copies
/// deduplicate into a single card.
private func recordSyncedCallHistory(_ entry: CallHistorySync.Envelope) {
guard let account else { return }
let direction: ChatMessage.Direction =
entry.direction == .outgoing
? .outgoing
: .incoming
let metadata = CallHistoryMetadata(
isVideo: entry.isVideo,
outcome: entry.outcome
)
let message = ChatMessage(
id: entry.id,
conversationID: entry.peerJID,
senderJID: direction == .outgoing ? account.normalizedJID : entry.peerJID,
body: entry.isVideo ? "Видеозвонок" : "Аудиозвонок",
timestamp: entry.startedAt,
direction: direction,
delivery: direction == .outgoing ? .sent : .delivered,
security: .plaintext,
kind: .system,
duration: entry.duration,
callHistory: metadata
)
let isUnreadMissedCall =
direction == .incoming
&& entry.outcome == .missed
&& selectedConversationID != entry.peerJID.lowercased()
_ = upsertMessage(message, incrementUnread: isUnreadMissedCall)
}
private func upsertConversation(jid: String, name: String?) {