feat(notifications): the in-app inbox — cutover 6 of 7 (edgemain) #43

Merged
whitlocktech merged 4 commits from edge into main 2026-09-01 14:01:00 +00:00
Member

Engagement Phase 13, step 6 of 7. The app's half of the in-app channel.

Merge steps 1–5 first — the inbox is a client of routes that reach main in step 3.

What lands (4 commits)

  • the in-app inbox — a list, unread counts, mark-read and mark-all-read, and per-channel notification preferences, against the routes Phase 7 shipped
  • an offline snapshot in DataStore — 30 items, first page only, owner-scoped, cleared on sign-out. docs/android/PLAN.md §7 says "no Room, no offline cache in v1"; this is its one deliberate exception, settled with the org lead, and it is a JSON snapshot rather than a database
  • ci(pr-checks) — the gate now runs on PRs into edge as well as main

The defect the live rig found

An inbox item's url is site-relative by Phase 7's own contract (validated by pageUrlTemplate's character class). The app was treating it as absolute, so every link in the inbox was dead until the app resolved it against the configured server. Unit tests could not see this; the emulator against a live server could.

On the last CI run

android-build failed on run 66 (21b6ddc, the Phase 8 PR head) and #42 was merged anyway. I reproduced that exact sha locally, clean, with --no-build-cache --rerun-tasks: 508 unit tests, 0 failures, lintDebug and assembleDebug both green. Two earlier runs in this repo's history (58 and 62) also show a failure followed by a success at the same sha, so this looks like the same runner flake rather than a code problem — the job installs the JDK and the Android SDK per run over the network.

This PR's own run is the confirmation. If it fails, the log is not readable from here, so the next step is reproducing the failing step locally rather than re-running blind.

  • AI-assisted: written with Claude Code.
**Engagement Phase 13, step 6 of 7.** The app's half of the in-app channel. **Merge steps 1–5 first** — the inbox is a client of routes that reach `main` in step 3. ### What lands (4 commits) - **the in-app inbox** — a list, unread counts, mark-read and mark-all-read, and per-channel notification preferences, against the routes Phase 7 shipped - **an offline snapshot in DataStore** — 30 items, first page only, owner-scoped, cleared on sign-out. `docs/android/PLAN.md` §7 says "no Room, no offline cache in v1"; this is its one deliberate exception, settled with the org lead, and it is a JSON snapshot rather than a database - **`ci(pr-checks)`** — the gate now runs on PRs into `edge` as well as `main` ### The defect the live rig found An inbox item's `url` is **site-relative** by Phase 7's own contract (validated by `pageUrlTemplate`'s character class). The app was treating it as absolute, so **every link in the inbox was dead** until the app resolved it against the configured server. Unit tests could not see this; the emulator against a live server could. ### On the last CI run `android-build` **failed** on run 66 (`21b6ddc`, the Phase 8 PR head) and #42 was merged anyway. I reproduced that exact sha locally, clean, with `--no-build-cache --rerun-tasks`: **508 unit tests, 0 failures**, `lintDebug` and `assembleDebug` both green. Two earlier runs in this repo's history (58 and 62) also show a failure followed by a success at the *same* sha, so this looks like the same runner flake rather than a code problem — the job installs the JDK and the Android SDK per run over the network. This PR's own run is the confirmation. If it fails, the log is not readable from here, so the next step is reproducing the failing step locally rather than re-running blind. - [x] AI-assisted: written with Claude Code.
wtclaude added 4 commits 2026-09-01 13:46:57 +00:00
ENGAGEMENT.md §7.1 Q8. `pr-checks.yml` triggered only on PRs into `main`, so a
workstream that lands its phases on `edge` before one cutover PR got no CI at
all until the cutover — all nine M12 phase PRs merged without a single run, and
engagement Phase 8 was about to do the same. A phase should fail on its own PR.

Sonar is untouched: `sonarqube.yml` is a push-on-`main` analysis, not a PR gate,
so no phase PR was ever expected to run it.

Co-Authored-By: Claude <noreply@anthropic.com>
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.

The drawer's "Notifications" is the INBOX now, with the preferences one tap away
behind its gear — the arrangement Phase 7 shipped on the web, and what a person
means when they tap the word. The settings screen moved off
/notifications/subscriptions onto /notifications/channels: it renders a control
per channel that applies to each id (from the item's own `channels`, never a
hardcoded three) and per mode that channel accepts, which is how email's
`digest` reaches the app. The old endpoint is the push projection of the new
table server-side, so the shipped APK went on working the whole time.

A tapped tickle whose `ref` starts with `notification:` lands on the inbox
whatever its stream is — an engagement rule's stream id is a TRIGGER id in the
one namespace, and `forStream`'s fixed map would have sent most of them Home.
Every other tickle keeps the route it has always had. The ref is not decoded
beyond that prefix and never rendered: it is a hint that a row exists, and the
contract stays wake-and-pull.

PLAN.md §7's "no Room cache in v1" stands; the offline snapshot is its one named
exception, settled with the org lead. The inbox is a short, read-only,
newest-first list with a server-side cursor, so what "works offline" needs is the
newest page and the badge, not a database — one JSON blob in the DataStore the
push code already uses. Every snapshot is scoped to (base URL, user id) and only
handed back to that pair: that, not the clear-on-logout, is what stops a cache
surviving into another account on the paths that never reach a logout at all.

Co-Authored-By: Claude <noreply@anthropic.com>
fix(notifications): resolve an item's relative url, and document the CI trigger
Some checks failed
PR Checks / android-build (pull_request) Failing after 42m5s
21b6ddc29b
Two things the live rig found, and the README half of the trigger change.

Phase 7 specifies an inbox item's `url` is RELATIVE-ONLY and validates it as
such — right for a browser already on the site, a dead link on a phone. The
first cut here only opened `http(s)`-prefixed strings, so on the rig every link
in the inbox did nothing at all. `InboxViewModel.linkFor` now resolves against
the configured base with OkHttp's `HttpUrl.resolve`, which absolutises the path
and returns null for anything that would not end up http(s) — so a `javascript:`
or `intent:` url in a notification body opens nothing.

Co-Authored-By: Claude <noreply@anthropic.com>
Reviewed-on: #42
whitlocktech approved these changes 2026-09-01 13:57:42 +00:00
whitlocktech scheduled this pull request to auto merge when all checks succeed 2026-09-01 13:57:47 +00:00
whitlocktech merged commit 80441c3367 into main 2026-09-01 14:01:00 +00:00
whitlocktech deleted branch edge 2026-09-01 14:01:01 +00:00
Sign in to join this conversation.
No description provided.