Show an in-call indicator when a room has an active huddle (LC-612) #572

Merged
longjacksonle merged 1 commit from feat/lc612-sidebar-incall into main 2026-07-21 01:59:35 +02:00

Closes LC-612. The sidebar workstream of LC-596.

The blocker was routing, not markup

An active huddle was invisible from the sidebar because VoiceJoined/VoiceLeft are broadcast to the room topic - only sockets that already have the room open receive them. Those are exactly the people who do not need a sidebar hint. So the existing events could not drive this; it needed per-member fan-out.

What this adds

ChatEvent::HuddlePresence is addressed to one member (to_user_id) and fanned out over list_room_member_ids on every join and leave, carrying the post-change participant count. It renders as a small pulsing green dot + count on the room row (#incall-room-{id}), OOB-swapped exactly like the unread badge. A fresh page load seeds the same count from hub.voice_room_users, so the indicator is correct on load, not only after the next event - SidebarRoom gained an in_call field for that.

The fan-out lives in huddle_ring.rs (broadcast_presence), next to the LC-611 ring it shares a shape with.

Two deliberate differences from the ring

  1. It ignores mute. The ring is an interruption, so mute suppresses it; this dot is passive presence. Hiding it on a muted room would mean you could not see a call happening in a room you muted but still belong to. Tested explicitly.
  2. It fires on every roster change, not just the first join, because the count changes each time. The ring fires once per huddle.

Scope

Non-is_voice rooms only, matching the ring. A persistent voice channel is a place people go to deliberately, and its membership is enclave-scoped rather than in room_members, so the per-member fan-out here would not reach the right people for it. The is_voice seed is hard-coded to 0 and broadcast_presence early-returns on it.

Tests

Five, in huddle_ring.rs and stage_control.rs:

  • presence reaches a member who does NOT have the room open - proven load-bearing by reverting broadcast_presence to broadcast_to_room, which then misses them (the two membership-sensitive tests fail, the voice-channel one still passes because it asserts absence)
  • the count is right as the huddle grows (1 -> 2) and shrinks back to 0, which tears the indicator down
  • a muted member still gets it
  • a voice channel does not drive it
  • the sidebar seeds the pill on initial load, with an empty target span when nobody is in the huddle

Full suite green (182 test binaries), just check clean, en/es Fluent symmetry holds.

Note

The count is read from the live in-memory hub on both the seed and the fan-out, so it reflects true presence and does not depend on the huddle having sent any message. No new DB state.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FhDVMViNHqUbwmfe2aXTv5

Closes LC-612. The sidebar workstream of LC-596. ## The blocker was routing, not markup An active huddle was invisible from the sidebar because `VoiceJoined`/`VoiceLeft` are broadcast to the *room topic* - only sockets that already have the room open receive them. Those are exactly the people who do not need a sidebar hint. So the existing events could not drive this; it needed per-member fan-out. ## What this adds `ChatEvent::HuddlePresence` is addressed to one member (`to_user_id`) and fanned out over `list_room_member_ids` on every join and leave, carrying the post-change participant count. It renders as a small pulsing green dot + count on the room row (`#incall-room-{id}`), OOB-swapped exactly like the unread badge. A fresh page load seeds the same count from `hub.voice_room_users`, so the indicator is correct on load, not only after the next event - `SidebarRoom` gained an `in_call` field for that. The fan-out lives in `huddle_ring.rs` (`broadcast_presence`), next to the LC-611 ring it shares a shape with. ## Two deliberate differences from the ring 1. **It ignores mute.** The ring is an interruption, so mute suppresses it; this dot is passive presence. Hiding it on a muted room would mean you could not see a call happening in a room you muted but still belong to. Tested explicitly. 2. **It fires on every roster change, not just the first join,** because the count changes each time. The ring fires once per huddle. ## Scope Non-`is_voice` rooms only, matching the ring. A persistent voice channel is a place people go to deliberately, and its membership is enclave-scoped rather than in `room_members`, so the per-member fan-out here would not reach the right people for it. The `is_voice` seed is hard-coded to 0 and `broadcast_presence` early-returns on it. ## Tests Five, in `huddle_ring.rs` and `stage_control.rs`: - presence reaches a member who does NOT have the room open - **proven load-bearing** by reverting `broadcast_presence` to `broadcast_to_room`, which then misses them (the two membership-sensitive tests fail, the voice-channel one still passes because it asserts absence) - the count is right as the huddle grows (1 -> 2) and shrinks back to 0, which tears the indicator down - a muted member still gets it - a voice channel does not drive it - the sidebar seeds the pill on initial load, with an empty target span when nobody is in the huddle Full suite green (182 test binaries), `just check` clean, en/es Fluent symmetry holds. ## Note The count is read from the live in-memory hub on both the seed and the fan-out, so it reflects true presence and does not depend on the huddle having sent any message. No new DB state. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FhDVMViNHqUbwmfe2aXTv5
feat(sidebar): show an in-call indicator when a room has an active huddle (LC-612)
All checks were successful
check-secrets / TruffleHog (push) Successful in 4s
check-secrets / Kingfisher (push) Successful in 5s
check-secrets / Nosey parker (push) Successful in 6s
check-secrets / Nosey parker (pull_request) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 4s
check-secrets / Kingfisher (pull_request) Successful in 6s
Check / clippy + fmt + tests (pull_request) Successful in 5m48s
Create release / Create release from merged PR (pull_request) Has been skipped
47c40a405e
An ongoing group call was invisible from the sidebar: `VoiceJoined`/`VoiceLeft` are broadcast to the room topic, so only people who already have the room open ever heard about a huddle. The whole point of a sidebar hint is to reach the members who are looking somewhere else, so those events were the wrong primitive.

A new `ChatEvent::HuddlePresence` is addressed to one member (`to_user_id`) and fanned out over `list_room_member_ids` on every join and leave, carrying the post-change participant count. It renders as a small pulsing green dot + count on the room row (`#incall-room-{id}`), swapped OOB exactly like the unread badge. A fresh page load seeds the same count straight from `hub.voice_room_users`, so the indicator is correct on load, not only after the next event.

Two deliberate differences from the LC-611 ring, which shares the fan-out shape:
- It ignores the room mute mode. The ring is an interruption and mute suppresses it; this dot is passive presence, and hiding it on a muted room would mean you could not see a call happening in a room you muted but still belong to.
- It fires on every roster change, not just the first join, because the count itself changes each time.

Scoped to ad-hoc huddles (non-`is_voice`), matching the ring: a persistent voice channel is a place people go to deliberately, and its membership is enclave-scoped rather than in `room_members`, so the per-member fan-out here would not reach the right people for it anyway.

Tests: presence reaches a member who does NOT have the room open (proven load-bearing by reverting to `broadcast_to_room`, which then misses them), carries the right count as the huddle grows and shrinks to 0, ignores mute, and skips voice channels; plus a render test that the sidebar seeds the pill on initial load. New Fluent key `sidebar-in-call` in en + es.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhDVMViNHqUbwmfe2aXTv5
longjacksonle deleted branch feat/lc612-sidebar-incall 2026-07-21 01:59:35 +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!572
No description provided.