feat(notifications): the in-app inbox, and per-channel preferences (engagement Phase 8) #42
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/engagement-inapp-android"
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?
The app's half of the in-app channel. Phase 7 shipped four inbox routes with no consumer on either platform; this is the Android one, plus the per-channel preferences Phase 3 added and the shipped screen could not express.
Design of record:
docs/website/ENGAGEMENT.mdPhase 8 (docs#190). Four decisions were settled by the org lead before any code.What changed
…/settingsthe preferences), and what a person means when they tap the word./notifications/channels. Controls are rendered from the wire: a control per channel in that item'schannels, its shape from that channel'smodes— a switch for two, chips for three, so email'sdigestreaches the app and a fourth channel would too, with no release. A trigger-only id shows no push control rather than a dead switch. Each change is one sparse PUT of one pair. The old endpoint is the push projection of the new table server-side, so the shipped APK kept working the whole time.ref, not its stream. An engagement rule's tickle carries the TRIGGER id asstream(§7.2's one namespace) andPushStreamsknows only the eight push streams, soteam.forum.postwould have landed on Home.Routes.forTicklesends anything whose ref starts withnotification:to the inbox and leaves every other tickle where it has always gone. The ref is never decoded past that prefix and never rendered — it is a hint that a row exists, and the contract stays wake-and-pull, exactly aspushChannel.jssays.PLAN.md§7's "no offline caching, no Room in v1" decision stands; this is its one named exception, settled with the org lead. An inbox is a short, read-only, newest-first list with a server-side cursor and no joins, so what "works offline" needs is the newest page and the badge — one JSON blob in the DataStore the push code already uses.pr-checks.ymlnow runs on PRs intoedgetoo (§7.1 Q8, answered "fix it"). All nine M12 phase PRs merged with zero CI; this PR is the first phase PR in the workstream that gets a real build.sonarqube.ymlis untouched — it is a push-on-mainanalysis, not a PR gate.The finding worth reading
Phase 7's contract says an item's
urlis relative-only — right for a browser already on the site, and a dead link on a phone. The first cut only openedhttp(s)-prefixed strings, so on the live rig every link in the inbox did nothing at all.InboxViewModel.linkFornow resolves against the configured base withHttpUrl.resolve, which absolutises the path and returns null for anything that would not end up http(s), so ajavascript:orintent:url in a notification body opens nothing. The contract did not change; the client half of it was simply never written down.The snapshot is scoped to (base URL, user id), and that is the security property — not the clear-on-logout beside the push deregistration. A cache is only handed back to the pair that wrote it, so the teardown paths that never reach a logout (a dead refresh token, a server switch) cannot surface one person's notifications under another's session. There is a test that fails if that scoping is removed.
Verified
./gradlew testDebugUnitTest lintDebug assembleDebuggreen — 508 tests, 0 failures.On the live rig (emulator + the real server on
edge, rows emitted throughctx.events.emit→ engine → outbox →inappChannel, never hand-written):(35, news.post, email, instant)— and disturbed nothing else;STREAM=team.forum.post,REF=notification:5, the exact extrasPushNotifierbuilds) landed on the inbox rather than Home.One thing deliberately NOT fixed
Offline works in a running app, not on a cold start — and the reason is the app shell, not the inbox:
MainActivitygates the whole ofRunicAppon loading the site's appearance (M12), so an offline launch shows "Can't reach the site / Retry" and never reaches the drawer. Widening this phase into the shell's startup model is a decision for the org lead, so it is written up inPLAN.md§7 andENGAGEMENT.mdrather than done here.🤖 Generated with Claude Code
edgetoo 21e235a07f