ThreadReplay — Project Story
Inspiration
Online communities live and die by how well their moderation scales. But moderation is almost always reactive — a mod sees a report, takes action, and moves on. There's rarely a way to look back at a thread and understand how it unraveled: which comment ignited the toxicity, when a brigade arrived, and whether the cleanup actually helped sentiment recover.
We kept thinking about threads we'd seen spiral in real time — a civil discussion that hit one bad-faith reply and collapsed into a pile-on within minutes. The pattern felt almost mechanical, like watching a phase transition. That intuition is actually grounded in research on opinion dynamics. A simple model of how a hostile comment $h$ spreads through a thread of $n$ participants looks something like:
$$\frac{dh}{dt} = \beta \cdot h \cdot \left(1 - \frac{h}{n}\right) - \gamma \cdot h$$
where $\beta$ is the contagion rate of hostile framing and $\gamma$ is the natural dampening from civil replies or mod action. Small changes in $\frac{\beta}{\gamma}$ — the basic toxicity number — determine whether a thread self-corrects or cascades.
We wanted to make that invisible dynamic visible. That became ThreadReplay.
What it does
ThreadReplay is a forensic moderation tool built as a Reddit Devvit app. Given any thread, it:
- Replays the comment timeline from $t = 0$ using an animated scrubber, so you can watch the conversation unfold chronologically
- Detects toxic comments via keyword heuristics and score thresholds
- Identifies brigade waves by finding clusters of $\geq 3$ low-score accounts posting within a 3-minute window — a pattern consistent with coordinated off-sub arrivals
- Marks pivot moments — the first positive comments to appear within 10 minutes after a mod removal, signaling sentiment recovery
- Shows per-root density maps so you can see at a glance which subtrees were noisy and when
- Exports a Markdown mod report summarizing each root's statistics
The interface organizes the thread as an indexed accordion — each top-level comment (root) gets its own numbered row that expands to reveal its full comment subtree, with color-coded annotations for toxic 🔴, brigade 🟡, removed 🟢, and pivot 🟣 signals.
How we built it
The stack is deliberately lightweight: vanilla HTML, CSS, and JavaScript, packaged as a Devvit web view. No framework dependencies. The core pieces:
Detection pipeline runs entirely client-side. For each root subtree independently:
- Toxic detection — flag comments where $\text{score} < -10$ or body matches a regex of hostile terms
- Brigade detection — sliding window across sorted timestamps; if $|{authors}| \geq 3$ with $\text{score} < 0$ within $\Delta t \leq 180\text{s}$, mark as brigade
- Pivot detection — after each mod removal event $r$, find comments in $[t_r,\ t_r + 600\text{s}]$ where $\bar{s} > 40$; the first two become pivot markers
Timeline rendering uses a percentage cursor $p \in [0, 100]$ mapped to real timestamps:
$$t(p) = t_0 + \frac{p}{100} \cdot (t_{end} - t_0)$$
Comments are injected into the DOM as $p$ advances, with CSS animation for the slide-in effect. The playback loop ticks every 80ms and increments $p$ by $0.35 \times \text{speed}$.
Density canvas renders a <canvas> heatmap for both the global view and each root's minimap, drawing bars at x-positions proportional to comment timestamps. The minicursor tracks $p$ live.
Devvit bridge listens for postMessage events from the host app carrying {type: 'load', payload: {comments, removals}}, falling back to demo data after 1.5 seconds if nothing arrives.
Challenges we ran into
Subtree isolation. Early builds ran detection globally — a brigade in root C would incorrectly flag comments in root A. The fix was computing TOXIC, BRIGADE, and PIVOT maps per root ID, so each subtree is fully independent. This also meant the root-level stat badges needed their own scoped lookups.
Canvas sizing on dynamic layouts. When roots are inside an accordion, the minimap <canvas> has zero width before the panel is expanded. We had to defer drawRootMinimap calls to requestAnimationFrame after the DOM is painted, and re-trigger on expand.
Accordion + live replay interaction. If a panel is closed while playback is running, comments still render into the hidden <div> — which is correct, because toggling the panel later should show the accumulated state. But feed.scrollTop = feed.scrollHeight on a hidden element is a no-op. We kept it simple and let the scroll snap on open.
Brigade false positives. The sliding window algorithm is $O(n^2)$ over comments and was flagging legitimate bursts of discussion as brigades. Tightening the conditions to require both negative scores and $\geq 3$ distinct authors brought precision up without hurting recall on genuine coordination patterns.
Accomplishments that we're proud of
- A zero-dependency forensics UI that loads instantly inside a Devvit web view
- The indexed accordion layout — replacing a crowded multi-column grid with a scannable numbered list that scales to any number of roots cleanly
- Per-subtree detection that is genuinely root-isolated, preventing cross-contamination of signals
- A density heatmap that makes the temporal shape of toxicity immediately legible — you can see a brigade as a tight cluster of amber bars before you read a single comment
- Clean Devvit message bridge that makes the demo data / real data switchover seamless
What we learned
Building this sharpened our thinking on a few things:
Moderation is a signal processing problem. The brigade detector is essentially a matched filter for a known waveform — coordinated posting has a distinctive temporal signature ($\Delta t$ tight, authors diverse, scores negative). Framing it that way made the algorithm cleaner.
Accordion beats grid at scale. A side-by-side column layout feels intuitive for 2–3 roots but breaks badly at 6+. An indexed list with expand-in-place panels is both more space-efficient and more scannable — users can read the summary row and decide whether to drill in.
Detection thresholds are social contracts, not math. The cutoffs ($\text{score} < -10$, window $\leq 180\text{s}$, $\bar{s} > 40$ for pivot) aren't derived from first principles — they're tuned to match moderator intuition on real data. Any deployment would need community-specific calibration.
What's next for ThreadReplay
- Real Devvit data integration — wire the message bridge to Reddit's actual comment API instead of demo JSON
- Configurable thresholds — let community mods tune the $\beta / \gamma$ detection parameters per-subreddit via a settings panel
- Author graph overlay — visualize reply-to relationships as a force-directed graph to expose reply-chain coordination that the current linear view misses
- Sentiment scoring — replace keyword matching with an embedding-based toxicity classifier for higher precision, ideally running via a lightweight WASM model client-side
- Export to mod queue — one-click to surface detected comments directly into the subreddit mod queue with pre-filled removal reasons
- Historical comparison — track $\frac{\beta}{\gamma}$ trends across threads over time to identify subreddits at elevated risk before a cascade starts
Built With
- css3
- devvit
- github
- html5
- javascript
- reddit-public-api
- typescript
Log in or sign up for Devpost to join the conversation.