Chat Moderation
Every teacher↔parent direct chat and every class broadcast channel is moderatable from one place. Two queues feed it: an auto-flag stream (messages that match a watchlist of terms) and a report stream (a parent or teacher manually flagged a message). Admins can dismiss, mark reviewed, delete the message, restrict a teacher↔parent pair, or suspend a user from chat entirely.
At a glance
| Who can do this | Admins & sub-admins with moderation permission |
| Where it lives | Admin panel → /dashboard/school/:id/moderation |
| Triggers notifications? | No (silent moderation actions) |
| Related features | Messages & Chat · PTM |
How it flows
The three tabs
The page header reads "Chat moderation" with a tagline pinned to the school's name. Three top tabs:
| Tab | What's in it |
|---|---|
| Auto-flagged | Messages caught by the term watchlist. Badge shows count of open flags. |
| Reports | Messages a user has manually reported. Badge shows count of open reports. |
| All chats | A read-only browser of every chat in the school: teacher → parent threads, and class broadcast channels. |
Tab 1 — Auto-flagged
A table sorted by recency. Sub-tabs filter by status: Open (default) · Reviewed · Dismissed · All.
Columns: When · Sender · Thread · Severity tag (low/medium/high) · Excerpt of the message body · Status · Actions.
Per-row actions while status is open: View, Mark reviewed, Dismiss.
Clicking View opens a side drawer with:
- Matched terms (red tags) — the watchlist words that triggered the flag.
- The full message with attachment link (if any) and sender name + role + timestamp.
- The thread label —
Class X · Section Y (broadcast)for class channels, orTeacher Name ↔ Parent Namefor direct chats. - Mark reviewed / Dismiss buttons.
Tab 2 — Reports
Same shape, sub-tabs Open / Resolved / Dismissed / All.
Columns: When · Reporter · Reported · Reason tag · Details (free-text from the reporter) · Status · Actions.
Per-row, while open: Resolve or Dismiss. Either opens a small modal asking for optional admin notes ("Notes for the record", up to 2000 chars). On submit, the row flips to the chosen status with a resolvedAt timestamp.
The expandable row reveals: the thread name, a card with the referenced message (body + sender + timestamp), and any admin notes already on file.
Tab 3 — All chats
A drill-down browser for all chat content in the school, regardless of flagging.
Top sub-tabs:
- Teachers → list of teachers with
Active chats,Last messagecolumns. Click one to see all their direct parent threads, then click a thread to open the full chat window. - Class channels → list of broadcast channels (one per section). Click to open the channel chat window.
The chat window
When a thread is open, the admin sees a familiar messaging UI:
- Header: title (parent name or class label), subtitle (student names "Parent of X & Y" or "Class announcements"), a
Restrictedred tag if the pair is currently blocked. - Header actions (only for teacher↔parent direct chats):
- Restrict pair / Unblock pair — toggle whether teacher and parent can exchange messages. Restricting opens a confirm with optional reason. Unblocking is a one-step confirm.
- Suspend teacher — bans the teacher from sending in any chat (they can still read past convos). Optional reason.
- Suspend parent — same, for the parent.
- Body: scrollable message list, day-grouped, with bubbles colored by role (teacher in indigo, parent in white).
- Hover over any non-deleted message to reveal a small Delete button — confirm, and the bubble flips to "Message removed by admin" (italic, grey) on both sides.
Class broadcast windows show the same message list but no per-pair restrict actions (there is no pair).
Step-by-step: handling a flag
- Land on
/dashboard/school/:id/moderation. TabAuto-flaggedis selected by default. - Skim the rows. The badge counter on the tab is the number of
openflags. - Pick a row with high severity → View.
- Read the matched terms + message body in the drawer. If the flag was a false positive (e.g. innocuous use), click Dismiss. Otherwise click Mark reviewed.
- If the message itself needs to go, switch to the All chats tab, drill into the same thread, hover the offending bubble, and click Delete.
- If the offender is a chronic problem, suspend them from the chat window header.
Step-by-step: handling a report
- Open the Reports tab. Sort by When if needed.
- Expand a row to read the referenced message and any admin notes.
- Click Resolve (took action) or Dismiss (no action needed). Add a short note for the audit trail.
- To actually delete the reported message or restrict the participants, jump to All chats and use the chat window controls.
Edge cases & things to test
- Open vs Reviewed status — verify the badge counter on the Auto-flagged tab only counts
open, notreviewedordismissed. - Severity tag rendering —
lowshould be grey,mediumwarning,highred. Test all three. - Matched terms with regex special chars — flags whose
matchedTermscontain.,*,(should render as plain tags without breaking the drawer. - Reporter == reported — should not be possible to self-report; verify the form blocks it.
- Resolve modal
notesfield at max length — 2000 chars should save; longer should be capped client-side. - Restrict pair while one side is composing — the other party tries to send a message; they should see a friendly "this chat is restricted" message in the chat UI.
- Unblock then re-restrict — verify the audit trail keeps both events, not just the latest.
- Suspend a teacher who's in a live class — verify they can still attend but cannot post chat messages.
- Delete a message with an attachment — both the body and the attachment link should be hidden behind "Message removed by admin".
- Delete a message in a broadcast channel — verify all class members see the deletion (Socket.IO push), not just the next refresh.
- Class channel with no messages — list should still render the row with
(empty)preview. - Drill-down breadcrumbs — Teachers → Teacher → Thread breadcrumb each clickable to walk back without losing state.
- Pagination — every table is
pageSize: 20, showSizeChanger: false. Verify scrolling past 20 results works. - Cross-school (super admin) — at higher scope the
showSchoolColumnflag adds aSchoolcolumn. Confirm the per-school page hides it correctly. - Sub-admin without moderation permission — the moderation entry should not appear in the school sidebar at all.
Related
- Messages & Chat — the user-facing chat surfaces being moderated
- PTM — parent-teacher meetings often originate as chat threads
- Sub-admin access control — gating the moderation page