import { useEffect, useState } from 'react' import { api } from '../api/client.js' import { useAuth } from '../contexts/AuthContext.jsx' import { activityScopeNote, freshnessNote, groupByDay } from '../lib/teamActivity.js' // Core's Team activity feed, rendered into a slot a MODULE declares // (TEAMS.md Part 4, §3.4 as amended). // // **This is the inverted slot direction, and this component is why it exists.** // The feed is core's: core owns `team_activity`, writes the membership and rename // items into it, enforces the public/members split, and is the only thing that // can resolve whether this viewer is inside the Team. None of that is a module's // to reimplement. But the PAGE is the module's, because Teams is a contract // primitive and core does not own the word for one — a UO shard says guild, the // next game will say something else. So the module declares the place and core // puts the feed in it. // // The module passes the Team in ITS OWN vocabulary — `externalId` plus its module // id — and core resolves the slug. A module never learns core's Team id and never // needs to: it names the thing the way it already names it. // // Everything here degrades to rendering nothing. A slot that throws is contained // by core's own boundary (Slot.jsx), but a slot that renders an error box would // still be core putting a defect on a page it does not own — so a failed fetch is // silence, not a message. export default function TeamActivityFeed({ externalId, moduleId, limit = 25 }) { const { user } = useAuth() const [state, setState] = useState({ loading: true, feed: null, team: null }) useEffect(() => { let active = true if (!externalId || !moduleId) { setState({ loading: false, feed: null, team: null }) return undefined } // Two calls because the module names the Team its way and the feed is keyed // by core's slug. The lookup is core's job precisely so the module does not // have to hold core's identifiers. api.teamByExternalId(moduleId, externalId) .then(async (team) => { const feed = await api.teamActivity(team.slug, { limit }) if (active) setState({ loading: false, feed, team }) }) .catch(() => { if (active) setState({ loading: false, feed: null, team: null }) }) return () => { active = false } }, [externalId, moduleId, limit]) const { loading, feed, team } = state if (loading || !feed) return null const days = groupByDay(feed.items || []) const note = team ? freshnessNote(team) : null const scopeNote = activityScopeNote(feed, Boolean(user)) // Nothing has happened and nothing to explain: render nothing rather than an // empty heading on someone else's page. if (days.length === 0 && !scopeNote) return null return (

Recent activity

{note && (

{note.text}

)} {days.length === 0 && (

Nothing has happened here yet.

)} {days.map((day) => (

{day.label}

))} {scopeNote && (

{scopeNote}

)}
) }