nvrsion: Add: Chat: Chat View Refinement to modernize session chat interface by removing vertical space for selectors, Fix: Chat: Remove Face ID Permission Text, Refactor: Chat: Live Activity Content Density to open app directly into waiting sessions when tapped, Refactor: Chat: Live Activity Content Density to refresh live activity info significantly faster, Fix: Chat: Live Activity Content Density to refresh live activity info significantly faster, Fix: Chat: Live Activity Content Density to refresh live activity info significantly faster, Fix: Chat: Network Type Transition Optimization to reduce delay during network type switches, Feature Enhancement Planning: All devices in a mesh group should see and be aware of each other; “pair” reframed into “join”; scanning, Fix: Chat: Session Transcript Sync to show only current frame’s transcript when opening a session on another Mac, Build: Push-update path to stay fresh while locked.

Nucleic-Promote: 1
Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
2026-07-05 01:00:03 -07:00
co-authored by Nucleic
parent 368978bea8
commit 8b37b8576f
7 changed files with 205 additions and 78 deletions
@@ -282,6 +282,7 @@ final class RemoteStore: ObservableObject {
// MARK: - Lifecycle
func onAppear() {
setupLiveActivityBridge()
if demoMode { seedDemo(); return }
// Re-plan every connection the moment the network path changes (left Wi-Fi, joined a
// network, cellular⇄Wi-Fi) — the proactive half of "immediate switchover".
@@ -489,6 +490,9 @@ final class RemoteStore: ObservableObject {
if hostID == self.activeHostID { self.mirrorActive() }
self.refreshAggregate()
self.flushPendingNotificationDecision()
// A host that just connected (or reconnected) needs the current Live Activity token
// so it can push while the phone is away.
self.syncLiveActivityRegistration()
}
cb.openSnapshot = { [weak self] snap in
guard let self, hostID == self.activeHostID, snap.summary.sessionID == self.openSessionID else { return }
@@ -834,6 +838,49 @@ final class RemoteStore: ObservableObject {
activeConnection?.send(msg)
}
// MARK: - Live Activity push registration (UX_IOS §5.3)
/// The current Live Activity's APNS update token + id, shipped to every capable Mac so it can
/// refresh the lock-screen glance over APNs while the phone is backgrounded and its socket is
/// suspended. Sent to *all* connected hosts (not just the active one) — while the phone is
/// away, whichever Mac has work to report should be able to push.
private var liveActivityReg: (token: String, activityID: String)?
/// Host ids that already have the current token (re-sent to a host that (re)connects, and
/// re-sent to everyone when the token rotates).
private var liveActivitySentTo: Set<String> = []
private func setupLiveActivityBridge() {
LiveActivityManager.shared.onPushToken = { [weak self] token, activityID in
guard let self, self.liveActivityReg?.token != token else { return }
self.liveActivityReg = (token, activityID)
self.liveActivitySentTo.removeAll() // a fresh token must reach every host again
self.syncLiveActivityRegistration()
}
LiveActivityManager.shared.onActivityEnded = { [weak self] activityID in
guard let self else { return }
self.liveActivityReg = nil
self.liveActivitySentTo.removeAll()
for conn in self.connections.values where conn.capabilities.canPushLiveActivity {
conn.send(.endLiveActivity(activityID))
}
}
}
/// Send the current Live Activity token to every live, capable host that hasn't got it yet.
/// Called when the token changes and on every connection update, so a host that just connected
/// — or reconnected while the phone was away — learns the token it needs to push.
private func syncLiveActivityRegistration() {
guard let reg = liveActivityReg else { return }
// A host that dropped should re-register when it returns.
liveActivitySentTo = liveActivitySentTo.filter { connections[$0]?.connectivity.isLive == true }
for (id, conn) in connections {
guard conn.connectivity.isLive, conn.capabilities.canPushLiveActivity,
!liveActivitySentTo.contains(id) else { continue }
conn.send(.registerLiveActivity(reg.token, reg.activityID))
liveActivitySentTo.insert(id)
}
}
// MARK: - Demo simulator (offline, interactive)
//
// In demo mode there's no host, so writes can't go over the wire. Instead they mutate the
@@ -887,7 +934,11 @@ final class RemoteStore: ObservableObject {
case .setSessionShipBranch(let id, let branch):
demoUpdateSession(id) { $0.demoCopy(shipBranch: .some(branch)) }
case .hello, .listSessions, .listDashboard, .subscribe, .unsubscribe,
.ping, .cancelQueuedMessage, .fetchDiff, .listPeers, .addressUpdate, .meshRoster,
.ping, .cancelQueuedMessage, .fetchDiff, .fetchTranscript, .listPeers,
.addressUpdate, .meshRoster,
// Live Activity push registration is a real-connection concern (there's no host to
// push in demo), so it's inert here.
.registerLiveActivity, .endLiveActivity,
// Session transfer (mesh P5) is a Mac↔Mac flow — the phone never originates these,
// and demo has no peer Macs, so they're inert here.
.transferOffer, .transferChunk, .transferCommit, .transferCancel: