Skip to content

Room chat stage 3: chat in the room, anywhere in the app, and on the big screen (MAD-842..845) - #71

Merged
madeofpendletonwool merged 3 commits into
mainfrom
mad-836-room-chat
Oct 14, 2026
Merged

madeofpendletonwool merged 3 commits into
mainfrom
mad-836-room-chat

Conversation

@madeofpendletonwool

Copy link
Copy Markdown
Owner

Stage 3 of Phase 13 (MAD-836), its last stage: people in a room can chat. Messages show in a chat panel in the room, pop up wherever you are in the app, and float by on the TV.

The decisions are written up in ADR 0018 (docs/adr/0018-room-chat.md).

MAD-842: ADR, data model, API and realtime

  • New internal/chat package and migration 00033 (chat_messages).
  • Messages stay with their night. A night only gets a row when it ends, so a message can't name its night when it's sent. When a night ends (nights.end), every message in that room without a night, up to the end time, is assigned to it.
  • Retention: nothing is pruned. Deleting a room or an account deletes its messages. Chat isn't part of the Wrapped story, which is about the music, but night_id is kept in case a recap or export wants the messages later.
  • Each message remembers the song playing (queue_item_id), and the API returns that song's snapshot (track) with the message, so the panel can show the song without another request.
  • API:
    • POST /rooms/{id}/messages: plain text, trimmed, 1–500 characters (counted in runes).
    • GET /rooms/{id}/messages?before=&limit=: newest first, paged by ID (IDs are UUIDv7, so time-ordered). Deleted messages are left out.
    • DELETE /rooms/{id}/messages/{messageId}: your own message, or anyone's if you're the room owner or an admin. The text is cleared and the row is kept, so paging stays stable.
  • Who can do what: members and guests can read and post. Paired displays can read (listChatMessages is a display op). A room you can't open returns 404, as everywhere else.
  • Rate limit: it uses the same token bucket as reactions. That limiter is now userLimiter, which takes a rate each time it's used. Chat allows a burst of 5, then one every 2s.
  • Realtime: chat.message and chat.deleted are sent on the room topic. They're in the spec's RoomEvent enum, so they're in the generated TS types.
  • New room setting screens.chat (floating | log | off, default floating). If a screens update leaves it out, the current value is kept, so older clients don't reset it.

MAD-843: Chat panel

  • Opening it: a Chat button in the room header. It opens a bottom sheet on a phone and a panel down the right side on a computer. It's mounted once in the app shell, so a popup can open it from any screen. It closes when you navigate, the same way now playing does.
  • Messages:
    • Grouped by sender and time (5 minutes apart).
    • When the song changed, a "during · " chip appears, using ArtistLinks.
  • Scrolling:
    • Scrolling up loads older pages and keeps your place.
    • New messages stick to the bottom, unless you've scrolled up. Then a "N new messages" pill shows.
  • Unread counts: shown on the Chat button and on the Room tab in the bottom nav.
    • Read state is per device, in local storage.
    • A device that has never opened a room's chat starts at zero instead of showing a backlog.
    • RoomLive keeps the chat loaded on every screen, so the counts stay right.
  • Delete: a hover button on desktop, or a long press on touch.
  • Composer:
    • It follows visualViewport, so the phone keyboard doesn't cover it.
    • Enter sends on a fine pointer. On a phone, Enter adds a new line and the button sends.
    • A character counter appears near the 500 limit.

MAD-844: Popups anywhere

  • The popup: a small glass bubble near the top of every screen in the app shell. It shows the avatar, the name and two lines of the message, and leaves 5s after the latest message.
  • Interaction: tap it to open the chat; swipe it away to dismiss.
  • When it doesn't show:
    • for your own messages
    • while the chat panel is open
    • for a room other than the one you're in
  • Bursts collapse into one bubble ("Sam and 2 others").
  • Motion: no bounce and no sound.
  • Separate from toasts: it has its own slot at the top, so error toasts at the bottom stay visible.
  • Mute: a per-device switch on the Me page ("Chat popups"). Unread counts still update when popups are muted.

MAD-845: Big screen

  • Floating (default): messages drift right to left across the top of the TV as glass bubbles, with the sender's name in their color. They use two lanes, with at most 4 on screen, and long messages are clipped to two lines. The top band avoids the lyrics column and the now-playing title.
  • Side log: up to 3 messages from the last 15 minutes, in the top-right corner.
  • Off: no chat on the TV.
  • The setting: it's in the Big screen dialog ("Chat on the big screen"), which the room owner or an admin can change.
  • Deleting a message removes it from the TV right away.
  • The app's full-screen visualizer shows chat in the room's chosen style too.

Testing

  • Automated: go test -race ./..., golangci-lint (0 issues), and web pnpm test (298), typecheck, lint and build all pass.
  • New tests:
    • TestChat: send (trimmed, remembers the song), on the socket, validation, paging, display read-only, delete permissions and chat.deleted, private-room 404, rate limit, and messages assigned to the night when it ends.
    • TestChatGuests: guests can read and post in their own room, and are blocked from other rooms.
    • chat package: send, list and delete against the store and bus.
    • TestRoomScreens covers screens.chat.
    • lib/chat.test.ts: grouping, unread counts, read marks, cache updates, popup collapsing and timing.
    • Component tests for the panel (render, send, delete), the popup (collapse, own messages, muted, panel open) and the big-screen chat (floating, delete, log, off).
  • In a browser: I ran the server with the fake provider and Vite, and drove two signed-in users with headless Chromium. I checked:
    • the phone and desktop panel
    • unread badges on the Chat button and Room tab
    • a popup on Search
    • Floating and Side log on /tv
  • Not checked: a real phone keyboard, the long press, and the visualizer overlay.

🤖 Generated with Claude Code

madeofpendletonwool and others added 3 commits October 14, 2026 10:50
A room has a chat. Messages are plain text, trimmed, 1-500 characters,
and remember the song that was playing. Members and guests read and
post; paired displays read along. You can delete your own, and the
room's owner or an admin can delete anyone's.

- Migration 00033 adds chat_messages. A night isn't a row until it
  ends, so ending one gives it the room's messages that have no night.
- POST/GET /rooms/{id}/messages (newest first, paged by ID) and
  DELETE /rooms/{id}/messages/{messageId}.
- chat.message and chat.deleted on the room socket.
- Sending is rate limited with the reactions' token bucket, now
  userLimiter with a rate per use: a burst of 5, then one every 2s.
- screens.chat (floating, log or off) says how the big screen shows it.
  Left out of an update, it stays as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…AD-844)

A Chat button on the room page opens the chat: a sheet on a phone, a
panel down the side on a computer. Messages are grouped by sender and
time, with a "during <song>" chip whenever the song changed, its
artists linked. Scrolling up loads older pages and keeps your place;
new messages stick to the bottom unless you've scrolled up, then a pill
says how many came in. You can delete your own (anyone's, as the host)
from a hover button or a long press. The composer follows the visual
viewport so the keyboard never covers it, and Enter sends on a computer.

RoomLive keeps the room's chat loaded on every screen. Unread counts,
per device in local storage, show on the Chat button and the Room tab.
A device that's never read a room's chat starts with nothing unread.

New messages pop up anywhere in the app as a small glass bubble near
the top, in their own slot away from the toaster. Never for your own,
or while the chat is open. A burst collapses ("Sam and 2 others").
Tap to open the chat, swipe to dismiss. The Me page can mute them on
this device; the unread count still counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The TV shows the room's chat in the look the room picks (screens.chat,
in the big screen settings): Floating drifts messages across the top of
the screen as glass bubbles with the sender's name in their color, clear
of the lyrics and the title, two lanes and four at most. Side log keeps
the last few from the past 15 minutes in the top right. Off shows none.
A deleted message leaves the screen at once. The app's visualizer shows
the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@madeofpendletonwool
madeofpendletonwool merged commit b33a58c into main Oct 14, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant