From f62840f276fac2eb047cf3866f6780a2992a550c Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 21:33:26 -0700 Subject: [PATCH] Push Notification Live Activity Redundancy Nucleic-Session: 73687ED9-C11C-438C-832B-979332B73893 Co-authored-by: Nucleic --- .../NucleicRemote/LiveActivityManager.swift | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/NucleicRemote/NucleicRemote/LiveActivityManager.swift b/NucleicRemote/NucleicRemote/LiveActivityManager.swift index fd39e6d..194eacf 100644 --- a/NucleicRemote/NucleicRemote/LiveActivityManager.swift +++ b/NucleicRemote/NucleicRemote/LiveActivityManager.swift @@ -209,9 +209,35 @@ final class LiveActivityManager { Date().timeIntervalSince($0) >= Self.churnRefreshInterval } ?? true guard signatureChanged || (state != lastState && churnRefreshDue) else { return } + // When a session newly needs the user while we're backgrounded, this update carries the alert + // (sound/haptic) — there's no banner then, we prefer the glance. Foreground, the app UI / a + // banner announces it, so the glance updates silently. Computed against the *previous* state + // before `lastState` is overwritten. + let alert = foreground ? nil : Self.alert(from: lastState, to: state) lastState = state lastPushAt = Date() - enqueue(state) + enqueue(state, alert: alert) + } + + /// The alert an update should carry when backgrounded: a session newly needs the user. An approval + /// (its Allow/Deny live on the glance) outranks a needs-input for the wording; a rise in neither ⇒ + /// nil (silent update). Reuses the same localized keys as the host's pushed alert so a local and a + /// pushed alert read identically. Approvals/inputs that merely persist (or resolve) don't re-alert. + private static func alert( + from previous: NucleicSessionAttributes.ContentState?, + to next: NucleicSessionAttributes.ContentState + ) -> AlertConfiguration? { + if next.approvalCount > (previous?.approvalCount ?? 0) { + return AlertConfiguration( + title: LocalizedStringResource("approval.title"), + body: LocalizedStringResource("approval.pending"), sound: .default) + } + if next.needsYouCount > (previous?.needsYouCount ?? 0) { + return AlertConfiguration( + title: LocalizedStringResource("input.title"), + body: LocalizedStringResource("input.pending"), sound: .default) + } + return nil } /// Start the process-lived observers that make push-to-start work: the device's push-to-start