Skip to main content

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 thisAdmins & sub-admins with moderation permission
Where it livesAdmin panel → /dashboard/school/:id/moderation
Triggers notifications?No (silent moderation actions)
Related featuresMessages & 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:

TabWhat's in it
Auto-flaggedMessages caught by the term watchlist. Badge shows count of open flags.
ReportsMessages a user has manually reported. Badge shows count of open reports.
All chatsA 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, or Teacher Name ↔ Parent Name for 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 message columns. 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 Restricted red 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

  1. Land on /dashboard/school/:id/moderation. Tab Auto-flagged is selected by default.
  2. Skim the rows. The badge counter on the tab is the number of open flags.
  3. Pick a row with high severity → View.
  4. 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.
  5. 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.
  6. If the offender is a chronic problem, suspend them from the chat window header.

Step-by-step: handling a report

  1. Open the Reports tab. Sort by When if needed.
  2. Expand a row to read the referenced message and any admin notes.
  3. Click Resolve (took action) or Dismiss (no action needed). Add a short note for the audit trail.
  4. 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, not reviewed or dismissed.
  • Severity tag renderinglow should be grey, medium warning, high red. Test all three.
  • Matched terms with regex special chars — flags whose matchedTerms contain ., *, ( 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 notes field 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 showSchoolColumn flag adds a School column. 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.