Skip to content
Varad Rane
Go back

Stream Chat Organizer: Building a Chrome Extension for Live Chat Readability

If you’ve ever watched a live stream with an active chat, you know how quickly things spiral into noise. Messages fly past before you can read them, important conversations vanish, and the whole experience becomes more overwhelming than engaging.

I built Stream Chat Organizer to fix that — and keep it simple while doing it.

Table of contents

Open Table of contents

The Problem

Live stream chats have a core UX problem: they’re designed for volume, not clarity. Everything is equal. A question from a regular viewer looks exactly the same as a passing comment from someone who won’t interact again. There’s no hierarchy, no persistence, and no way to thread a conversation.

I wanted:

And critically: I didn’t want to build a backend for this. Extensions that phone home to a server add latency, require accounts, and break when the server goes down.

The Approach: Purely Client-Side

Everything in Stream Chat Organizer runs in the browser. No server. No database. No API keys.

State is stored in the browser’s local storage and DOM manipulation handles all the visual changes. This means:

The tradeoff is that state doesn’t persist across devices or sessions beyond what local storage can hold, which is fine for the use case.

How It Works

The extension injects a content script into the streaming platform’s page. It then:

  1. Observes the chat DOM using a MutationObserver to detect new messages as they arrive
  2. Augments each message with interactive controls (mark as read, pin, quote)
  3. Persists state for pinned/read messages using localStorage
  4. Re-renders visual state on page load so the UI remains consistent between refreshes
const observer = new MutationObserver(mutations => {
  mutations.forEach(mutation => {
    mutation.addedNodes.forEach(node => {
      if (isChatMessage(node)) {
        augmentMessage(node);
      }
    });
  });
});

observer.observe(chatContainer, { childList: true, subtree: true });

The CSS layer handles most of the visual differentiation — read messages get a muted opacity, pinned messages get a distinct highlight, quoted messages render an inline reference block.

Features

Mark as Read Click a message to toggle its “read” state. Read messages visually recede, letting unread ones stand out naturally.

Pin Messages Pin important messages to keep them docked at the top of your view — useful for questions you want to come back to, key timestamps, or running jokes you want to track.

Quote Previous Messages Inline quoting gives context to replies, making thread-style conversations much easier to follow in fast-moving chats.

Improved Styles Better visual separation between messages, better contrast ratios, and less visual noise overall. The default chat UI is tuned for content velocity, not readability.

What I Learned

DOM manipulation at scale is finicky. Streaming platforms update their markup frequently, and selectors that work today might break in a minor platform update. I had to build some resilience into the element selection logic — falling back through multiple selector strategies before giving up.

MutationObserver is powerful but noisy. You have to be careful about what you observe and how you filter events. Observing too broadly leads to performance problems; too narrowly and you miss messages. The sweet spot required some tuning.

Chrome extension architecture has friction. Content scripts, background scripts, and the popup all run in isolated contexts with constrained communication channels. Understanding this model up front saves a lot of debugging time.

LocalStorage is good enough for ephemeral UI state. I initially overthought the persistence layer. For something like “which messages have I read,” localStorage is perfectly adequate — it’s fast, synchronous, and doesn’t require any infrastructure.

Try It

The source code is on GitHub. If you watch live streams and want a more organized chat experience, give it a try.

The extension is intentionally minimal. I’d rather it do a few things well than accumulate features that dilute the core value.


Share this post on:

Previous Post
LazyCurl: The Terminal Interface for Curl and API Exploration
Next Post
Building a Multi-Location HomeLab: Self-Hosting Across Two Machines