Push a live signal when a user is added to a room (#26)

Previously GET /api/rooms/mine was only ever fetched once at app mount,
so a room added mid-session stayed invisible until a full page reload
-- add_member had no way to reach an already-open client at all.

Backend: ConnectionManager and Broadcaster (renamed from RoomBroadcaster)
now support per-user channels alongside the existing per-room ones, so a
signal can reach a user's socket even for a room they haven't joined
(and by definition can't have, until this fires). add_member publishes
a room_added event on the target user's channel.

Frontend: the WebSocket connection is no longer scoped to whichever
room is open -- ChatShellPage now owns one persistent connection for
the whole session (including while no room is open, which is exactly
when this bug showed), and ChatPane joins/leaves rooms on top of it.
A room_added event triggers a room-list refetch with no reload needed.

Verified end-to-end in the browser: a user sitting on the empty room
list saw a newly-added room appear live, then chatted in it normally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 10:29:06 -06:00
co-authored by Claude Sonnet 5
parent c9d61c3d12
commit 7dcc7104df
11 changed files with 222 additions and 64 deletions
+13
View File
@@ -12,6 +12,7 @@ import { TopBar } from '../components/TopBar'
import { useAuth } from '../context/AuthContext'
import { MOBILE_BREAKPOINT, useWindowWidth } from '../hooks/useWindowWidth'
import type { MyRoomItem, RoomMember } from '../types'
import { useChatSocket } from '../ws/useChatSocket'
import './ChatShellPage.css'
type ModalKind = 'new' | 'browse' | null
@@ -56,6 +57,17 @@ export function ChatShellPage() {
refreshRooms().catch(() => {})
}, [refreshRooms])
const onSocketUnauthenticated = useCallback(() => navigate('/login'), [navigate])
const socket = useChatSocket({ onUnauthenticated: onSocketUnauthenticated })
useEffect(
() =>
socket.subscribe((envelope) => {
if (envelope.type === 'room_added') refreshRooms()
}),
[socket, refreshRooms],
)
useEffect(() => {
refreshMembers()
// Also re-run when the logged-in user's own profile changes (display
@@ -97,6 +109,7 @@ export function ChatShellPage() {
onBack={() => navigate('/rooms')}
onToggleInfo={() => setInfoOpen((v) => !v)}
infoOpen={infoOpen}
socket={socket}
/>
) : (
!isMobile && (