feat(teams): the phase 5 surface — discussion, replies, reports, and two admin screens
241 client tests pass (224 before).
**The forum panel becomes a forum.** It was "Announcements" with one composer;
it now has two, because phase 5 split one server capability into two: `canPost`
means "may open a discussion" and every participant may — a granted guest with no
game character included, which is path 3 doing its job — while `canAnnounce` is
the leader-only half `canPost` used to carry alone. Threads gain replies, an edit
control, per-post moderation and a report control, all still inside the one slot
the module declares, still navigating by `?thread=`.
**Almost nothing here is the client's decision, and the file says so.** `canPost`,
`canAnnounce`, `canReply` and each post's `canEdit`/`editableUntil` are read, not
computed. The one local judgement is a ticking clock that WITHDRAWS an edit offer
whose deadline passed while the page sat open — it can never grant one, because a
time-bounded permission must not take its clock from the party it bounds. That
asymmetry is the first thing client/test/teamForum.test.js asserts.
The panel's pure parts moved to `lib/teamForum.js` so they can be tested without a
browser, following teamActivity.js and teamAdmin.js. Two of them are subtler than
they look:
* `stripToText` decodes entities AFTER stripping tags, and `&` last of all.
Decoding first turns an author's literal "<script>" into a real tag the
strip pass then deletes — silently losing text that was never dangerous.
* `threadSummary` counts REPLIES, which is one fewer than `postCount`. Showing
the raw count tells a reader a brand-new thread already has one reply.
**Three admin surfaces.** The forum settings screen gains the edit-window field
(0 = posts permanent once written). The reports queue is a new screen beside
Appeals — under moderation rather than under Teams, because a staffer working a
queue should have one place to work and `target_type` is deliberately open-ended,
so the next reportable thing arrives as a row rather than as another nav entry.
Its copy tells a member where a report lands and that reporting changes nothing,
because a member who expects a post to vanish and watches it stay reports it
again. There is no leader-facing view and there is not meant to be.
And the per-Team forum moderation ledger finally renders: the route and
`api.admin.teamForumModeration()` have both existed since phase 4 with nothing
calling them, which made `actor_role` — the column that keeps a leader's ordinary
housekeeping distinguishable from a staff intervention — readable only from a DB
client.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,8 @@ import { useEffect, useState } from 'react'
|
||||
import { api } from '../../../api/client.js'
|
||||
import { useSite } from '../../../contexts/SiteContext.jsx'
|
||||
|
||||
// The operator's two Team-forum controls (TEAMS.md §5.5), and the acknowledgement.
|
||||
// The operator's Team-forum controls (TEAMS.md §5.5, plus phase 5's edit window),
|
||||
// and the acknowledgement.
|
||||
//
|
||||
// Its own panel rather than two more rows in SettingsAdmin's FIELDS table, for the
|
||||
// same reason EmailDelivery is its own: one of these settings has a server-side
|
||||
@@ -68,6 +69,7 @@ export default function TeamForumSettings() {
|
||||
const [state, setState] = useState(null)
|
||||
const [enabled, setEnabled] = useState(false)
|
||||
const [mode, setMode] = useState('disabled')
|
||||
const [editWindow, setEditWindow] = useState('15')
|
||||
const [dialog, setDialog] = useState(null)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
@@ -79,6 +81,7 @@ export default function TeamForumSettings() {
|
||||
setState(s)
|
||||
setEnabled(s.enabled)
|
||||
setMode(s.imageMode)
|
||||
setEditWindow(String(s.editWindowMinutes ?? 15))
|
||||
} catch {
|
||||
setError('Could not load forum settings.')
|
||||
}
|
||||
@@ -97,6 +100,7 @@ export default function TeamForumSettings() {
|
||||
await api.admin.updateSettings({
|
||||
teams_forums_enabled: next.enabled ? '1' : '0',
|
||||
teams_forum_images: next.mode,
|
||||
teams_forum_edit_window_minutes: String(next.editWindow),
|
||||
...(acknowledge ? { acknowledge } : {}),
|
||||
})
|
||||
setSaved(true)
|
||||
@@ -115,14 +119,14 @@ export default function TeamForumSettings() {
|
||||
function save() {
|
||||
setSaved(false)
|
||||
if (mode === 'uploads' && (!state.acknowledgement?.given || stale || state.imageMode !== 'uploads')) {
|
||||
setDialog({ enabled, mode })
|
||||
setDialog({ enabled, mode, editWindow })
|
||||
return
|
||||
}
|
||||
if (stale) {
|
||||
setDialog({ enabled, mode })
|
||||
setDialog({ enabled, mode, editWindow })
|
||||
return
|
||||
}
|
||||
persist({ enabled, mode })
|
||||
persist({ enabled, mode, editWindow })
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -159,6 +163,25 @@ export default function TeamForumSettings() {
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'block', marginTop: 14 }}>
|
||||
<span className="field-label">Post edit window (minutes)</span>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
min={0}
|
||||
max={state.editWindowMax ?? 1440}
|
||||
value={editWindow}
|
||||
onChange={(e) => { setEditWindow(e.target.value); setSaved(false) }}
|
||||
style={{ maxWidth: 120 }}
|
||||
/>
|
||||
<span className="sans dim" style={{ display: 'block', marginTop: 6, fontSize: '0.76rem' }}>
|
||||
How long an author may edit their own post after writing it. Staff are not bound by it and
|
||||
may edit at any time. Set it to 0 to make posts permanent once written — a bound of some
|
||||
kind is what stops a post being rewritten out from under someone quoting it, or under a
|
||||
moderator about to act on a report.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div className="sans dim" style={{ marginTop: 8, fontSize: '0.76rem', lineHeight: 1.55 }}>
|
||||
{HELP_TEXT.map((line) => <p key={line} style={{ margin: '0 0 6px' }}>{line}</p>)}
|
||||
<ul style={{ margin: '0 0 6px 18px' }}>
|
||||
@@ -181,7 +204,12 @@ export default function TeamForumSettings() {
|
||||
{dialog && (
|
||||
<UploadsDialog
|
||||
version={state.acknowledgement.version}
|
||||
onCancel={() => { setDialog(null); setMode(state.imageMode); setEnabled(state.enabled) }}
|
||||
onCancel={() => {
|
||||
setDialog(null)
|
||||
setMode(state.imageMode)
|
||||
setEnabled(state.enabled)
|
||||
setEditWindow(String(state.editWindowMinutes ?? 15))
|
||||
}}
|
||||
onConfirm={async (version) => {
|
||||
setDialog(null)
|
||||
await persist(dialog, version)
|
||||
|
||||
Reference in New Issue
Block a user