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

4 Commits

Author SHA1 Message Date
d3bf4853de Merge pull request 'feat(notifications): the in-app inbox, and per-channel preferences (engagement Phase 8)' (#42) from feature/engagement-inapp-android into edge
All checks were successful
PR Checks / android-build (pull_request) Successful in 13m50s
Reviewed-on: #42
2026-08-31 14:37:35 +00:00
21b6ddc29b 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
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>
2026-08-31 09:27:39 -05:00
d393cf022e feat(notifications): the in-app inbox, and per-channel preferences (engagement Phase 8)
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>
2026-08-31 08:59:55 -05:00
21e235a07f ci(pr-checks): run the gate on pull requests into edge too
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>
2026-08-31 02:40:39 -05:00