commit 25df50d33fc67a4b869bdae65a2f623742a64081
parent acf0e0c1ffc822f1e0e00976380df56f25597edf
Author: Michael Camilleri <[email protected]>
Date: Fri, 22 May 2026 22:09:15 +0900
Withdraw a game's notifications when a sibling device opens it
Prior to this commit, the session-begin local notification — 'X is solving the
puzzle' — had no cross-device dismissal path. Opening the puzzle on one device
left that notification standing on the account's other devices. The only
cross-device withdrawal ran through directed-ping deletion (onPingDeleted
calling dismissDeliveredNotifications); session-begin notifications are
inferred from Player records, not Ping records, so nothing ever withdrew them.
Player.readAt synced the read horizon between a user's devices, but only fed
the unread-moves badge, never notification dismissal.
setOnIncomingReadCursor — which applies a sibling device's Player.readAt to the
local unread-moves cursor — now also calls dismissDeliveredNotifications for
the game when the incoming readAt is a future-dated active lease. A future
readAt means a sibling device is in that puzzle right now, so any session
notification already delivered for it is moot. A past readAt is only a
closed-session horizon bump and leaves delivered notifications untouched, so a
catch-up re-applying an old Player record cannot sweep a freshly delivered
notification.
dismissDeliveredNotifications makes no CKSyncEngine call — it only touches
UserNotifications — so it is safe to await directly from the delegate callback,
matching the existing onPingDeleted call site on the same stack.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Diffstat:
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/Crossmate/Services/AppServices.swift b/Crossmate/Services/AppServices.swift
@@ -284,10 +284,19 @@ final class AppServices {
// A sibling device of the same iCloud account has published its read
// horizon; apply it directly because SyncEngine has already accepted
- // the Player record under last-writer-wins freshness checks.
- await syncEngine.setOnIncomingReadCursor { [store] pairs in
+ // the Player record under last-writer-wins freshness checks. A
+ // future-dated readAt is an active-session lease — a sibling is in the
+ // puzzle right now — so withdraw any session notifications we already
+ // delivered for that game (e.g. "X is solving"); opening it here is no
+ // longer something to nudge for. A past readAt is just a closed-session
+ // horizon bump and leaves delivered notifications untouched.
+ await syncEngine.setOnIncomingReadCursor { [weak self, store] pairs in
+ let now = Date()
for (gameID, readAt) in pairs {
store.noteIncomingReadCursor(gameID: gameID, readAt: readAt)
+ if readAt > now {
+ await self?.dismissDeliveredNotifications(for: gameID)
+ }
}
}