feat(push): M7 Part 2 — opt-in push notifications (embedded ntfy distributor) #15
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/m7-push-notifications"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What & why
The app side of M7 push notifications (docs/android/PLAN.md §11). The app embeds its own distributor — the self-hosted ntfy is only the relay server, no second app installed, no Google Play Services. Opt-in per stream; nothing is pushed unless the user turns it on. A new, additive feature slice — no existing screen's data flow changes.
How it works
The app mints its own random, unguessable ntfy topic, registers that topic's URL as its device endpoint (
POST /auth/me/devices), and a foreground service holds an OkHttp-SSE connection to<ntfy>/<topic>/sseopen in the background — reusing the M2ShardStreamClientreconnect/backoff pattern on a bare client. The backend POSTs a content-free{ stream, ref }tickle to that endpoint; the service receives it over SSE and posts a per-stream notification that deep-links to the relevant screen.Changes
core/push/—NtfyTopic(topic + endpoint/SSE URL builders),PushTickle(tickle parse over ntfy's SSE envelope),NtfyStreamClient(bare-client SSE),PushNotifier(channels + deep-link notification),PushService(foreground service),PushManager(register/unregister + start/stop, keyed to the session),PushPreferences(DataStore state),PushStreams(ids + localized titles).data/—NotificationsApi+ DTOs +NotificationsRepositoryover the merged/auth/me/devices+/auth/me/notifications/*contract; apushblock onSettingsDto.ui/notifications/— settings screen + VM: per-stream toggles bound toGET/PUT …/subscriptions, personal streams greyed until a game account is linked,POST_NOTIFICATIONSrequested on enable.Routes.NOTIFICATIONS+ stream→route deep-link map, menu entry,RunicApp+MainActivityintent handling; device teardown wired into logout + server switch (clean DELETE while the bearer is valid) and every sign-out (local, via a session-state observer), mirroring the M3 token teardown.POST_NOTIFICATIONS,FOREGROUND_SERVICE(_DATA_SYNC), and the service.Recorded deviation
Direct-ntfy transport, no UnifiedPush library — the plan's stated likely path (work item 1). The UnifiedPush distributor model assumes a separate app (which the project owner vetoed) and we already own the SSE machinery, so pulling in
org.unifiedpush.android:connectorwould add a dependency for no gain. APushManager.PushResult/ transport seam keeps a future FCM Play flavor cheap.Dependencies
RunicGateway/website#79(push.ntfyUrlin/public/settings) so the app can discover the relay.NTFY_PUBLIC_URL/NTFY_ALLOWED_ORIGINS(§13). Until then the Notifications screen shows push as unavailable.Testing
18 new JVM unit tests (notifications DTO decode, ntfy tickle parse incl. malformed/keepalive, topic + URL building, stream→route map + personal gating).
:app:testDebugUnitTest+:app:lintDebug+:app:assembleDebuggreen (JDK 21,-Pksp.incremental=false). On-device delivery against a live ntfy is the one open QA item.AI disclosure
Authored with Claude Code (Claude Opus). Commits carry a
Co-Authored-By: Claudetrailer per org policy.🤖 Generated with Claude Code
Implements the app side of M7 push (docs/android/PLAN.md §11). The app EMBEDS its own distributor — ntfy is only the relay server, no second app installed, no Google Play Services. New feature slice; no existing screen's data flow changes. - core/push: NtfyTopic (random unguessable topic + endpoint/SSE URL builders), PushTickle (content-free { stream, ref } parser over ntfy's SSE envelope), NtfyStreamClient (bare-client OkHttp SSE to <ntfy>/<topic>/sse, reconnect/ backoff cloned from ShardStreamClient), PushNotifier (channels + per-stream deep-link notification), PushService (foreground service holding the connection), PushManager (mint topic / register-unregister device / start-stop, keyed to the session), PushPreferences (DataStore state). - data: NotificationsApi + DTOs + NotificationsRepository over the merged /auth/me/devices + /auth/me/notifications/* contract; push block on SettingsDto. - ui/notifications: settings screen + VM — per-stream toggles, personal streams greyed until a game account is linked, POST_NOTIFICATIONS request on enable. - Navigation: Routes.NOTIFICATIONS + stream→route deep-link map, menu entry, RunicApp + MainActivity intent handling; teardown wired into logout + server switch (deregister while bearer valid) and every sign-out (local, via session observer). - Manifest: POST_NOTIFICATIONS + FOREGROUND_SERVICE(_DATA_SYNC) + the service. Deviation (recorded in PLAN.md): direct-ntfy transport, no UnifiedPush library — the plan's stated likely path; keeps the APK Google-free and dependency-light, with a PushResult/transport seam for a future FCM Play flavor. Requires the small companion push.ntfyUrl settings field (website#<pr>). 18 new JVM tests; :app:testDebugUnitTest + lintDebug + assembleDebug green. Co-Authored-By: Claude <noreply@anthropic.com>