Skip to content

feat(mobile): beacon task presence to suppress cross-device push fanout#2338

Draft
dmarticus wants to merge 1 commit into
mainfrom
dmarticus/mobile-task-presence
Draft

feat(mobile): beacon task presence to suppress cross-device push fanout#2338
dmarticus wants to merge 1 commit into
mainfrom
dmarticus/mobile-task-presence

Conversation

@dmarticus
Copy link
Copy Markdown
Contributor

@dmarticus dmarticus commented May 24, 2026

Problem

After posthog/posthog#59313 (per-device presence beacon for push fanout), the server suppresses pushes about a task to any device with an active presence row for it. This PR is the mobile client half — it actually beacons.

Without this, the server-side filter has nothing to filter on and mobile keeps ringing for activity the user is already watching on another device.

Changes

  • registerPushToken (src/lib/api.ts) now parses the id (UserPushToken.id UUID) from the registration response. Defensive parse so older server versions don't crash.
  • pushTokenStore persists that UUID in SecureStore as deviceId alongside the existing token. Re-upload gate triggers when deviceId is missing, so existing users pull it on next foregrounding without re-registering.
  • postTaskPresence / deleteTaskPresence in src/features/tasks/api.ts against POST/DELETE /api/projects/{team_id}/tasks/{task_id}/presence/ with { device_id }.
  • useTaskPresence(taskId) hook in src/features/tasks/hooks/useTaskPresence.ts handles the lifecycle:
    • POST on mount when AppState === "active".
    • Refresh every 30s while mounted + foregrounded (server TTL is 60s).
    • DELETE on background/inactive, unmount, or task switch.
    • Waits for pushTokenStore.hydrate() so cold-start mounts don't miss the device_id.
    • Failures are intentionally swallowed — this is notification routing, not core functionality.
  • app/task/[id].tsx calls useTaskPresence(taskId) once.

Out of scope / not done / needs decision

Desktop (apps/code) beacon

Desktop uses Electron's native Notification API (see apps/code/src/main/platform-adapters/electron-notifier.ts) — it never registers a push token with the server. So the server-side presence model, which keys on UserPushToken.id, has no row to write for a desktop session.

This means the original "I'm on my Mac, stop ringing my phone" complaint isn't fully fixed yet. Options to discuss:

  • A. Server change: support free-form device_id on presence rows (decouple from UserPushToken). Desktop generates a stable UUID once, persists it in Electron's userData dir, and beacons normally. Cleanest model — presence shouldn't be a child of push token; one of those concepts is broader.
  • B. Server change: let desktop register a "no-delivery" push token (platform: \"electron\") that the server flags as presence-only. Smaller server diff than A, hackier shape.
  • C. Skip desktop presence entirely. The existing mobile-side mitigations from feat(mobile): hackathon mobile app #2237 (no awaitingPing arming from external user messages, 30s dedup, awaiting_user_input-only per-turn pings) already cover most desktop-driven activity. The leftover case is mobile-initiated work that completes while the user has moved to desktop.

My read is A is the right fix, but it's a server PR. Punting until we decide.

Inbox / new-task screens

  • Inbox detail isn't task-scoped → no presence to claim.
  • /task (new task composer) isn't viewing a task → no presence to claim.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant