Random chat has an ugly moment: you press search and nobody is there. On GrowsChat that used to end in one of two ways, a loading screen or an automatic chat with an AI. I wanted a third option that feels like a place, not a delay.
The idea: the Mood Room
If no match is found after 3 seconds, the user picks where to wait:
- Chat with the GrowsChat AI
- Wait in the Mood Room, a shared cinema hall
People who choose to wait without a limit never see the popup.
Inside the hall there are 24 chairs in three rows (6, 8 and 10). Everyone seated shows up with an avatar, a name and a gender symbol. A big screen plays random videos, and a switch turns the lights off. When a real match is found, the user gets an offer to join it or stay in the hall.
Decision 1: reuse the offer system
The app already had an AI fallback. The user stays in the queue with a flag, and when a real match appears, they get an offer instead of being connected automatically. The flag was a boolean, so I replaced it with a place:
waitingPlace?: "ai" | "mood";
Both places get top priority and receive offers the same way. Only the place changes, so the Mood Room needed no new matching logic at all.
Decision 2: the server owns the seats
Redis holds one hash per hall (seat number to user id) and one hash with the details of everyone seated. Seats are claimed atomically:
const claimed = await redis.hsetnx(hallKey(hallId), String(seatIndex), userId);
if (!claimed) continue; // someone took it a millisecond ago
Two people arriving at the same moment can never get the same chair. When hall 1 is full, hall 2 opens.
The public seat list has no user ids, so nobody can collect ids from the hall. The side effect is that a client cannot tell which seat is its own, so the server sends a private seated event to the person who just joined.
I also broadcast the full seat list on every change instead of separate join and leave events. With 24 seats it is tiny, and it can never drift out of sync.
Decision 3: a guard and strict cleanup
A user can only join the hall if their queue entry says they chose it. A seat is freed when the user:
- gets matched
- cancels the search
- starts a new search
- presses Leave hall
- closes their last tab
The server also clears every seat on startup, because a restart disconnects everyone and would otherwise leave ghost seats behind.
The bug that surprised me
The searching screen starts a new search by itself the moment it appears. So when someone left the hall and I showed that screen again, a new search began without a single click. The fix: leaving the hall ends the search and sends the user to the mode selector.
The screen
Videos are YouTube embeds through the IFrame API. They are not synced between users, since it is only a waiting room. Each person gets their own random video, never the same one twice in a row. A video that blocks embedding is skipped automatically, and if the browser blocks autoplay, the screen shows a tap to start button.
All video links live in one constants file. Every item has a kind, so ads for free users can be added later without rewriting the player.
Lights off
It is pure CSS and only affects your own screen. Chairs and decorations dim with a brightness filter, while the screen and your own chair stay lit.
Two layout lessons
- The screen is always exactly 16:9 with no tilt, so the video can never leave its frame or shift the layout when it loads.
- Every color comes from theme tokens, with
background.defaultas the dark base, instead of fixed hex values. The hall follows the app instead of fighting it.
What is next
- Reconnect handling for a dropped connection while seated
- A mute button on the screen
- Ads for free users
- Hosted AI generated videos
Try it
Search for a text chat on GrowsChat and wait 3 seconds: https://growschat.com
If you have built waiting rooms or shared presence with Socket.IO, I would love to hear what you did differently.
Top comments (0)