commit 8c6d0f8187fc517e00471d446516db3a00f59afa
parent 2ead503eff4fd82a92775537651ae3025dfbf0c0
Author: Michael Camilleri <[email protected]>
Date: Sat, 20 Jun 2026 09:31:40 +0900
Remove the dead .play presence notification
The session-begin push that announced 'X is solving the puzzle' was
retired when presence moved onto live engagement state, but its plumbing
lingered: the .play case in PushPayload.Event, the playBody text
builder, and a `kind === "play"` branch in the push worker that chose
the deliver-now-or-discard expiration.
This commit deletes that path end to end. The PushPayload codec drops
.play from its event enum, discriminator, and the encode, decode,
composedBody and marksUnread arms; because decoding is deliberately
tolerant, any `"play"` payload still in flight now resolves to .unknown
— no rebuilt body and no badge, matching the event's former behaviour.
PuzzleNotificationText loses the orphaned playBody, and the push worker
drops the dead expiration clause along with its play comment.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Diffstat:
5 files changed, 16 insertions(+), 42 deletions(-)
diff --git a/Shared/PushPayload.swift b/Shared/PushPayload.swift
@@ -54,8 +54,10 @@ struct PushPayload: Codable, Sendable, Equatable {
/// The sender's wall clock when the pause was computed — surfaces
/// clock skew against the recipient's own clock.
var senderNow: Date? = nil
- /// When the sender believes this play session began (the begin-push
- /// timestamp). `nil` if the session was never announced (e.g. solo).
+ /// When the sender believes the current solving session began. Now that
+ /// the begin push (which used to stamp this) is gone, no sender
+ /// populates it — it is always `nil` in practice and kept only so the
+ /// receipt log keeps a stable slot should a session-start signal return.
var sessionStart: Date? = nil
/// The recipient's `Player.readAt` *as the sender saw it* — the exact
/// cutoff the per-recipient diff used. A stale value here widens the
@@ -114,7 +116,6 @@ struct PushPayload: Codable, Sendable, Equatable {
}
enum Event: Sendable, Equatable {
- case play
/// A session-end summary, broken down by what the peer did since the
/// recipient last looked: net letter `fills` / `clears`, and the count
/// of `checks` / `reveals` *gestures* run. Letter counts are
@@ -143,7 +144,7 @@ struct PushPayload: Codable, Sendable, Equatable {
case .pause(let fills, let clears, let checks, let reveals):
return fills + clears + checks + reveals > 0
case .win, .resign: return true
- case .play, .replay, .nudge, .unknown: return false
+ case .replay, .nudge, .unknown: return false
}
}
}
@@ -164,11 +165,6 @@ extension PushPayload {
func composedBody(playerName: String) -> String? {
guard let puzzleTitle else { return nil }
switch event {
- case .play:
- return PuzzleNotificationText.playBody(
- playerName: playerName,
- puzzleTitle: puzzleTitle
- )
case .pause(let fills, let clears, let checks, let reveals):
return PuzzleNotificationText.pauseBody(
playerName: playerName,
@@ -223,15 +219,13 @@ extension PushPayload.Event: Codable {
}
private enum Discriminator: String {
- case play, pause, win, resign, replay, nudge
+ case pause, win, resign, replay, nudge
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let raw = try container.decode(String.self, forKey: .type)
switch Discriminator(rawValue: raw) {
- case .play:
- self = .play
case .pause:
let fills = try container.decodeIfPresent(Int.self, forKey: .fills) ?? 0
let clears = try container.decodeIfPresent(Int.self, forKey: .clears) ?? 0
@@ -255,8 +249,6 @@ extension PushPayload.Event: Codable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
- case .play:
- try container.encode(Discriminator.play.rawValue, forKey: .type)
case .pause(let fills, let clears, let checks, let reveals):
try container.encode(Discriminator.pause.rawValue, forKey: .type)
try container.encode(fills, forKey: .fills)
diff --git a/Shared/PuzzleNotificationText.swift b/Shared/PuzzleNotificationText.swift
@@ -14,11 +14,6 @@ enum PuzzleNotificationText {
return "\(title) – \(subtitle)"
}
- /// Body for a session-begin push: "Alice is solving the puzzle 'X'".
- static func playBody(playerName: String, puzzleTitle: String) -> String {
- "\(resolvedName(playerName)) is solving \(puzzleSuffix(puzzleTitle))"
- }
-
/// Body for a nudge push: "Alice nudged you to play the puzzle 'X'". A
/// deliberate rouse from the in-game players menu, so it always names an
/// action even when nothing in the grid changed.
diff --git a/Tests/Unit/PushPayloadTests.swift b/Tests/Unit/PushPayloadTests.swift
@@ -13,7 +13,6 @@ struct PushPayloadTests {
@Test("Events round-trip through the wire encoding")
func eventsRoundTrip() throws {
let cases: [PushPayload.Event] = [
- .play,
.pause(fills: 3, clears: 2, checks: 1, reveals: 0),
.pause(fills: 0, clears: 0, checks: 0, reveals: 0),
.pause(fills: 0, clears: 0, checks: 0, reveals: 2),
@@ -30,7 +29,7 @@ struct PushPayloadTests {
@Test("Puzzle title round-trips through the wire encoding")
func puzzleTitleRoundTrips() throws {
- let payload = PushPayload(event: .play, puzzleTitle: "Saturday Crossword")
+ let payload = PushPayload(event: .win, puzzleTitle: "Saturday Crossword")
let decoded = try roundTrip(payload)
#expect(decoded == payload)
#expect(decoded.puzzleTitle == "Saturday Crossword")
@@ -42,7 +41,6 @@ struct PushPayloadTests {
PushPayload(event: event, puzzleTitle: "Saturday")
.composedBody(playerName: "Mum")
}
- #expect(body(.play) == "Mum is solving the puzzle 'Saturday'")
#expect(body(.nudge) == "Mum nudged you to play the puzzle 'Saturday'")
#expect(body(.win) == "Mum solved the puzzle 'Saturday'")
#expect(body(.resign) == "Mum resigned the puzzle 'Saturday'.")
@@ -55,7 +53,7 @@ struct PushPayloadTests {
@Test("composedBody returns nil when the body can't be rebuilt")
func composedBodyNilWhenUncomposable() {
// No puzzle title (older sender) — can't faithfully rebuild.
- #expect(PushPayload(event: .play).composedBody(playerName: "Mum") == nil)
+ #expect(PushPayload(event: .win).composedBody(playerName: "Mum") == nil)
// Bodyless events carry no visible text to rebuild.
#expect(PushPayload(event: .replay, puzzleTitle: "Saturday")
.composedBody(playerName: "Mum") == nil)
@@ -162,7 +160,6 @@ struct PushPayloadTests {
#expect(PushPayload(event: .resign).marksUnread)
#expect(!PushPayload(event: .pause(fills: 0, clears: 0, checks: 0, reveals: 0)).marksUnread)
- #expect(!PushPayload(event: .play).marksUnread)
// A nudge is a manual presence ping — it never marks a game unread.
#expect(!PushPayload(event: .nudge).marksUnread)
#expect(!PushPayload(event: .replay).marksUnread)
diff --git a/Tests/Unit/PuzzleNotificationTextTests.swift b/Tests/Unit/PuzzleNotificationTextTests.swift
@@ -32,15 +32,6 @@ struct PuzzleNotificationTextTests {
#expect(InviteCoordinator.bodyText(for: ping) == "system-only ping should not be presented")
}
- @Test("playBody names the solver and puzzle")
- func playBodyNamesSolver() {
- #expect(PuzzleNotificationText.playBody(playerName: "Alice", puzzleTitle: "Saturday Puzzle")
- == "Alice is solving the puzzle 'Saturday Puzzle'")
- // Empty name and title fall back to neutral wording.
- #expect(PuzzleNotificationText.playBody(playerName: "", puzzleTitle: "")
- == "A player is solving the puzzle")
- }
-
@Test("nudgeBody names the nudger and puzzle")
func nudgeBodyNamesNudger() {
#expect(PuzzleNotificationText.nudgeBody(playerName: "Alice", puzzleTitle: "Saturday Puzzle")
diff --git a/Workers/push-worker.js b/Workers/push-worker.js
@@ -559,17 +559,16 @@ export class PushRegistry {
// app builds, which the extension handles by falling back to `kind`.
if (message.payload) apnsPayload.payload = message.payload;
- // "play" presence and a "nudge" rouse are ephemeral: deliver now or
- // discard, since "come play" delivered hours later is stale noise.
- // `accountSeen` is also background-only, but it withdraws already-read
- // notifications from sibling devices; give APNs a short store-and-forward
- // window so a briefly-unreachable device can still converge. Other alert
- // kinds (win/resign/pause) are one-time meaningful events and keep the
- // longer window so a recipient who is offline at send time still gets the
- // banner.
+ // A "nudge" rouse is ephemeral: deliver now or discard, since "come play"
+ // delivered hours later is stale noise. `accountSeen` is also
+ // background-only, but it withdraws already-read notifications from sibling
+ // devices; give APNs a short store-and-forward window so a briefly-
+ // unreachable device can still converge. Other alert kinds (win/resign/
+ // pause) are one-time meaningful events and keep the longer window so a
+ // recipient who is offline at send time still gets the banner.
const expirationSeconds =
message.kind === "accountSeen" ? 15 * 60 :
- message.background || message.kind === "play" || message.kind === "nudge" ? 0 :
+ message.background || message.kind === "nudge" ? 0 :
4 * 60 * 60;
const expiration = expirationSeconds === 0
? "0"