feat(receipts): group-room 'Seen by' read receipts (LC-489) #488

Merged
longjacksonle merged 4 commits from feat/LC-489-group-read-receipts into main 2026-06-29 01:49:26 +02:00

LC-489 - Group-room "Seen by" read receipts

Extends read receipts (previously DM-only) to small group rooms with an avatar-stack "Seen by" bar, reusing the existing dm_read_state watermark, the DmRead event rooms already broadcast on mark-read, and the read_receipts_enabled opt-in.

Behavior

A "Seen by [avatars]" bar sits below the message list, showing members who have read up to the room's latest message (Messenger-style bottom-of-thread indicator). It is gated by room_seen_members_if_applicable:

  • Small rooms only: member count <= SEEN_BY_MAX_MEMBERS (8). Caps per-event fan-out and avatar-row width. Larger rooms show nothing.
  • Symmetric consent: the viewer must have read_receipts_enabled to see the bar, and only members who themselves enabled receipts appear in it. The viewer is always excluded.
  • DMs are unaffected (they keep the existing single "Seen HH:MM" caption).

Data + render

  • db::chat::room_caught_up_member_ids(room, viewer) - members (excluding the viewer) whose watermark has reached the latest non-deleted message. db::auth::read_receipts_enabled_ids(ids) filters to consenting members. Avatars resolve via /avatars/{id} (generated fallback), so only id + label are needed.
  • RoomSeenBar renders #lc-seen-{room_id}; empty:hidden keeps it invisible until someone is caught up.
  • get_room pre-renders the bar (after mark-read) into the page. The WS layer refreshes it per-viewer: render_dm_read swaps it when a group-room read advances a watermark (replacing the old DM-only no-op for non-DM rooms), and render_new_message appends the recomputed bar so a new message resets it (only the author is caught up to it). The id-keyed OOB swap drops on connections not viewing the room.

Tests

db_read_receipts::room_caught_up_member_ids_tracks_latest (latest-watermark tracking, viewer exclusion). just check, just test, just test-saas all green; i18n parity covers the new room-seen-by (en+es); Tailwind rebuilt. Rebased onto current main (LC-487) - clean.

Not operator-visible (no env/config/contract change; reuses the existing per-user opt-in).

🤖 Generated with Claude Code

## LC-489 - Group-room "Seen by" read receipts Extends read receipts (previously DM-only) to small group rooms with an avatar-stack "Seen by" bar, reusing the existing `dm_read_state` watermark, the `DmRead` event rooms already broadcast on mark-read, and the `read_receipts_enabled` opt-in. ### Behavior A "Seen by [avatars]" bar sits below the message list, showing members who have read up to the room's latest message (Messenger-style bottom-of-thread indicator). It is gated by `room_seen_members_if_applicable`: - **Small rooms only**: member count <= `SEEN_BY_MAX_MEMBERS` (8). Caps per-event fan-out and avatar-row width. Larger rooms show nothing. - **Symmetric consent**: the viewer must have `read_receipts_enabled` to see the bar, and only members who themselves enabled receipts appear in it. The viewer is always excluded. - **DMs are unaffected** (they keep the existing single "Seen HH:MM" caption). ### Data + render - `db::chat::room_caught_up_member_ids(room, viewer)` - members (excluding the viewer) whose watermark has reached the latest non-deleted message. `db::auth::read_receipts_enabled_ids(ids)` filters to consenting members. Avatars resolve via `/avatars/{id}` (generated fallback), so only id + label are needed. - `RoomSeenBar` renders `#lc-seen-{room_id}`; `empty:hidden` keeps it invisible until someone is caught up. - `get_room` pre-renders the bar (after mark-read) into the page. The WS layer refreshes it per-viewer: `render_dm_read` swaps it when a group-room read advances a watermark (replacing the old DM-only no-op for non-DM rooms), and `render_new_message` appends the recomputed bar so a new message resets it (only the author is caught up to it). The id-keyed OOB swap drops on connections not viewing the room. ### Tests `db_read_receipts::room_caught_up_member_ids_tracks_latest` (latest-watermark tracking, viewer exclusion). `just check`, `just test`, `just test-saas` all green; i18n parity covers the new `room-seen-by` (en+es); Tailwind rebuilt. Rebased onto current main (LC-487) - clean. Not operator-visible (no env/config/contract change; reuses the existing per-user opt-in). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
db::chat::room_caught_up_member_ids returns members (excluding the viewer) whose dm_read_state watermark has reached the room's latest non-deleted message. db::auth::read_receipts_enabled_ids returns the subset of ids who opted in, so only consenting members appear in a "Seen by" stack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
room_seen_members_if_applicable gates the feature: non-DM rooms with <= 8 members where the viewer has read receipts on (symmetric consent), excluding the viewer, listing only consenting caught-up members. RoomSeenBar renders the avatar stack into #lc-seen-{room_id}. New i18n room-seen-by (en+es).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get_room pre-renders the bar (after mark-read) into the page below the message list. The WS layer refreshes it per-viewer: render_dm_read swaps it when a group-room read advances a watermark (replacing the old DM-only no-op), and render_new_message appends the recomputed bar so a new message resets it. Reuses the existing DmRead broadcast rooms already emit on mark-read.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test(receipts): room_caught_up_member_ids latest-watermark + viewer exclusion (LC-489)
All checks were successful
check-secrets / Nosey parker (push) Successful in 3s
check-secrets / TruffleHog (push) Successful in 4s
check-secrets / Kingfisher (push) Successful in 5s
check-secrets / Nosey parker (pull_request) Successful in 8s
check-secrets / TruffleHog (pull_request) Successful in 9s
check-secrets / Kingfisher (pull_request) Successful in 11s
Check / clippy + fmt + tests (pull_request) Successful in 5m39s
Create release / Create release from merged PR (pull_request) Has been skipped
ba2f7dc7aa
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/LC-489-group-read-receipts 2026-06-29 01:49:26 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!488
No description provided.