feat(teams): the web surface — a notifications screen that did not exist

This is phase 6's first finding, and it changed the phase's shape.

TEAMS.md §6.3 says the per-Team mute list is surfaced "under the existing
notification settings screen". There was no such screen. `/auth/me/notifications/*`
was built for the Android app in M7 and had ZERO web consumers — a browser could
not see the stream catalog or its own subscriptions at all. That is tolerable
while push is the only sink, because push needs the app anyway. It is not
tolerable for email, whose entire argument is the web-only user who runs neither
the app nor Discord, so the sink and the screen to configure it had to ship
together.

`/account/notifications` carries all three: what to be told about, which Teams,
and whether any of it reaches a mailbox — in the order a user actually reasons
about them.

The mute toggle goes in a THIRD module-declared slot, above the roster, because
muting is an action ON the guild page while the feed and forum are content IN it.
It renders nothing for a viewer with no preference available, which is a privacy
property rather than a tidiness one: whether a preference EXISTS for a Team
answers "is this person in it", and the guild page is public.

`/unsubscribe/:token` is public and POSTs on mount — the link the user clicked was
a GET, and a GET that mutated would be triggered by every mail-client link scanner.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-18 14:35:08 -05:00
parent 2a56cbf22a
commit b458c1f46f
8 changed files with 563 additions and 0 deletions

View File

@@ -6,6 +6,7 @@ import { publishSharedDependencies } from './modules/shared.js'
import { declareSlot, applyCoreFills, fillModuleSlot } from './modules/registry.js'
import TeamActivityFeed from './modules/TeamActivityFeed.jsx'
import TeamForumPanel from './modules/TeamForumPanel.jsx'
import TeamNotifyToggle from './modules/TeamNotifyToggle.jsx'
import './styles/theme.css'
// Publish window.__rg BEFORE rendering and before any module chunk evaluates.
@@ -84,6 +85,12 @@ fillModuleSlot('uo.guild.detail', TeamActivityFeed)
// exactly as before.
fillModuleSlot('uo.guild.forum', TeamForumPanel)
// And the notification control, in a third place the module declares ABOVE its
// roster. A third slot rather than a corner of the feed for the same reason there
// were two: this is an action on the page and the other two are content in it,
// and only the module can say where each belongs on a page it owns.
fillModuleSlot('uo.guild.header', TeamNotifyToggle)
// Render on DOMContentLoaded rather than immediately, and that is the one line
// of core's boot the module system changes.
//