import { useCallback, useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { api } from '../api/client.js' import { useAuth } from '../contexts/AuthContext.jsx' // Core's per-Team notification control, rendered into a THIRD slot a module // declares (TEAMS.md §6.3, phase 6). // // **Why this is a slot at all, and why it is the third one.** Teams have no core // page — the module that owns the vocabulary owns the page — so a control that // acts on one Team has nowhere of core's to live. The feed and the forum go below // the module's roster; this goes above it, because muting a guild is an action ON // the page rather than more content in it, and that is exactly the placement // decision a module cannot make if core stacks everything into one fill. // // **It renders nothing for a viewer who is not in the Team**, including anonymous // ones, and that is a privacy property rather than a tidiness one: whether a // notification preference EXISTS for a Team answers "is this person in it", and // the guild page is public. The server decides — the preference list only contains // Teams the caller may be notified about — and this file never infers membership // from anything it can see on the page. // // **Muting is per-Team and covers all four streams.** The per-stream on/off lives // on the account screen, where the catalog does; the thing that could not be // expressed before phase 6 is "I am in five Teams and want notifications from // one", and that is the only question this control asks. export default function TeamNotifyToggle({ externalId, moduleId }) { const { user } = useAuth() const [state, setState] = useState({ loading: true, team: null, pref: null }) const [busy, setBusy] = useState(false) const load = useCallback(async () => { // Anonymous viewers never fetch. The endpoint would 401 harmlessly, but a // guild page rendering a public roster should not put an authenticated // request on the wire for every visitor. if (!user) return setState({ loading: false, team: null, pref: null }) try { const team = await api.teamByExternalId(moduleId, externalId) const { teams } = await api.teamNotificationPrefs() const pref = (teams || []).find((t) => t.teamId === team.id) || null setState({ loading: false, team, pref }) } catch { // Same rule as the feed and the forum: this is core's content on a page // core does not own, so a failure renders nothing rather than putting an // error box on somebody else's surface. setState({ loading: false, team: null, pref: null }) } }, [externalId, moduleId, user]) useEffect(() => { load() }, [load]) const { loading, pref } = state if (loading || !pref) return null async function toggle() { setBusy(true) // Optimistic, and reconciled from the server's echo rather than assumed: a // PUT that silently dropped the entry (a Team left in another tab) must not // leave the control claiming a state the server does not hold. const next = { ...pref, muted: !pref.muted } setState((s) => ({ ...s, pref: next })) try { const { teams } = await api.setTeamNotificationPrefs([ { teamId: pref.teamId, muted: next.muted, emailMode: pref.emailMode }, ]) const echoed = (teams || []).find((t) => t.teamId === pref.teamId) if (echoed) setState((s) => ({ ...s, pref: echoed })) } catch { setState((s) => ({ ...s, pref })) } finally { setBusy(false) } } return (