Add unread message indicators for rooms (#38)

Shows a dot on rooms with unread messages in the sidebar, updated live
over WebSocket. Reuses the same offline-member audience computation
already used for push notifications: a member gets the real-time signal
whenever they aren't currently connected to that room's channel, which
correctly covers both "room not open" and "room open but tab
backgrounded" (the client leaves a room's channel while hidden).

Persisted server-side via a new room_memberships.last_read_at column so
state survives reload and stays consistent across devices, advanced by
an explicit mark-read call the frontend makes on room-open and on each
live message received while the room is genuinely visible -- gated on a
live visibility check, not a cached ref, so a backgrounded-but-open room
keeps accumulating unread instead of auto-marking-read the instant a
message arrives somewhere it can't be seen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 20:44:20 -06:00
co-authored by Claude Sonnet 5
parent 974d92ab4d
commit 68e487e5ec
14 changed files with 336 additions and 11 deletions
+4
View File
@@ -35,6 +35,10 @@ class RoomListItem(RoomRead):
class MyRoomItem(RoomRead):
role: RoomRole
# Whether this room has a message newer than the caller's last_read_at --
# computed by the router/service, not a stored column on Room itself
# (it's inherently per-viewer, unlike everything else on RoomRead).
has_unread: bool
class RoomMemberRead(BaseModel):