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:
@@ -187,6 +187,26 @@ export const api = {
|
||||
req(`/player/teams/${encodeURIComponent(slug)}/grants`, { method: 'POST', body }),
|
||||
teamGrantRevoke: (slug, userId) =>
|
||||
req(`/player/teams/${encodeURIComponent(slug)}/grants/${userId}`, { method: 'DELETE' }),
|
||||
|
||||
// ----- notifications (TEAMS.md Part 6) -----
|
||||
//
|
||||
// Under /auth/me rather than /player: these are role-agnostic self-service, the
|
||||
// same rule that put the forum under /player rather than behind a staff gate.
|
||||
// The streams catalog and the per-stream subscriptions were built for the app
|
||||
// and had no web consumer at all until phase 6 gave them one.
|
||||
notificationStreams: () => req('/auth/me/notifications/streams'),
|
||||
notificationSubscriptions: () => req('/auth/me/notifications/subscriptions'),
|
||||
// `streams` is always sent, empty array included — the endpoint requires the
|
||||
// field, so clearing the last subscription must not become an absent key.
|
||||
setNotificationSubscriptions: (streams) =>
|
||||
req('/auth/me/notifications/subscriptions', { method: 'PUT', body: { streams } }),
|
||||
teamNotificationPrefs: () => req('/auth/me/notifications/teams'),
|
||||
setTeamNotificationPrefs: (teams) =>
|
||||
req('/auth/me/notifications/teams', { method: 'PUT', body: { teams } }),
|
||||
// Unauthenticated, and the one write in the public tier: the caller is reading
|
||||
// their mail, not signed in. Always resolves 200 whatever the token was.
|
||||
unsubscribeTeam: (token) =>
|
||||
req(`/public/teams/unsubscribe/${encodeURIComponent(token)}`, { method: 'POST' }),
|
||||
wikiTags: () => req('/public/wiki/tags'),
|
||||
wikiPage: (slug) => req(`/public/wiki/${slug}`),
|
||||
// CMS pages (block-based). Published-only for the public; a draft-preview link
|
||||
|
||||
Reference in New Issue
Block a user