Reactions that actually work, sidebar expand ribbon, huddle transcription #553

Merged
vas2000-work merged 7 commits from feat/sidebar-collapse-ribbon into main 2026-07-18 21:00:20 +02:00
Owner

Follow-ups to the redesign (#552). Mostly fixes for things that looked finished but were never wired up, plus the sidebar expand affordance and transcription in huddles.

Reactions

Reactions could not be added through the UI at all. The picker was authored around a shared #lc-reaction-popover host with tab / filter / recent / close hooks, but the host element did not exist and the script driving it was never in the tree, so the add-reaction control did nothing and only an already-present chip could be toggled. There was no way to create the first reaction.

  • New reactions.js restores that runtime: opens the picker into a fixed host (added to layout.html), positions it by the trigger, switches category tabs, filters by name, keeps a recent-emoji MRU, and closes on react / Escape / outside-click.
  • The primary emoji-select moved into the message hover bar, beside reply / thread / more, so that is where a reaction starts.
  • The under-message + renders only when a message already has at least one reaction, so others can pile onto an existing set; at zero reactions there is no + and no picker under the text.
  • Populated the one-tap quick-react bar. Every row carried a data-lc-quick-bar placeholder that shipped hidden with nothing to fill it; it is now seeded from /api/reactions/frequent (which existed but was never called) merged with the local MRU.
  • toggle_reaction was SELECT then a separate INSERT/DELETE with no transaction, so a double-tap could both see "absent", both insert, violate the primary key and 500 while leaving the reaction on. Each branch is now one atomic statement keyed on rows-affected, with INSERT OR IGNORE.
  • Fixed a duplicate id="reactions-{id}": the bar was nested inside a second element carrying the same id, so the first outerHTML toggle replaced the wrong node and dropped the spacing wrapper.
  • Aligned the WS OOB echo with the HTTP fragment. They rendered different markup, so the reactor (who also receives the broadcast) watched the rich bar flip to a degraded one right after reacting.

Sidebar

Collapsing hid the sidebar entirely and left only a tile inside the rail to bring it back, so it read as the sidebar vanishing into the rail. That tile is replaced by a half-round ribbon on the rail's outer seam carrying the double-chevron, shown only while collapsed on desktop, themed from the existing rail/sidebar tokens.

Transcription in huddles

A huddle is the same mesh as an enclave voice channel with the same per-participant mic capture, and was excluded only because its room is not flagged is_voice: the surface check accepted a DM or a voice room and 404'd everything else, and the huddle bar omitted the controls. Captions therefore worked in DMs and voice channels but silently failed in group huddles. Any room is now a call surface and authority moved to require_participant, which demands live mesh membership for anything that is not a DM, so a member who has not joined is forbidden rather than the room being hidden. Controls added to the huddle bar.

Other fixes

  • Thread reply box: typed text was invisible. It reuses .lc-mdinput, which sets color: transparent because in the main composer a .lc-mdback backdrop paints the text; the thread box has no such backdrop, so only the caret showed.
  • Settings: dismiss by clicking outside it or pressing Escape. It fills the main pane with no close control, so the only exits were a sidebar room link or the browser back button.

Verification

Driven in a real browser against seeded data: picker opens/positions/closes, the first reaction creates a chip and reveals the +, quick-react adds and toggles off, concurrent double-toggle returns no 5xx, the ribbon shows only when collapsed and re-expands on click, and the huddle bar carries the transcribe controls. Full server test suite passes, including the transcripts test updated for the new huddle contract.

Note: last_visited.rs has two pre-existing failures (home_renders_welcome_when_no_cookie, home_welcome_renders_quick_actions). Confirmed unrelated to this branch by reproducing them with these changes stashed.

Follow-ups filed

LC-594 (reactions on thread replies + the duplicate thread-panel msg id), LC-595 (make group video calls easy: ring/invite, discovery, SFU-backed huddles), LC-597 (Stage SFU transcription).

Follow-ups to the redesign (#552). Mostly fixes for things that looked finished but were never wired up, plus the sidebar expand affordance and transcription in huddles. ## Reactions Reactions could not be added through the UI at all. The picker was authored around a shared `#lc-reaction-popover` host with tab / filter / recent / close hooks, but the host element did not exist and the script driving it was never in the tree, so the add-reaction control did nothing and only an already-present chip could be toggled. There was no way to create the first reaction. - New `reactions.js` restores that runtime: opens the picker into a fixed host (added to `layout.html`), positions it by the trigger, switches category tabs, filters by name, keeps a recent-emoji MRU, and closes on react / Escape / outside-click. - The primary emoji-select moved into the message hover bar, beside reply / thread / more, so that is where a reaction starts. - The under-message `+` renders only when a message already has at least one reaction, so others can pile onto an existing set; at zero reactions there is no `+` and no picker under the text. - Populated the one-tap quick-react bar. Every row carried a `data-lc-quick-bar` placeholder that shipped hidden with nothing to fill it; it is now seeded from `/api/reactions/frequent` (which existed but was never called) merged with the local MRU. - `toggle_reaction` was `SELECT` then a separate `INSERT`/`DELETE` with no transaction, so a double-tap could both see "absent", both insert, violate the primary key and 500 while leaving the reaction on. Each branch is now one atomic statement keyed on rows-affected, with `INSERT OR IGNORE`. - Fixed a duplicate `id="reactions-{id}"`: the bar was nested inside a second element carrying the same id, so the first `outerHTML` toggle replaced the wrong node and dropped the spacing wrapper. - Aligned the WS OOB echo with the HTTP fragment. They rendered different markup, so the reactor (who also receives the broadcast) watched the rich bar flip to a degraded one right after reacting. ## Sidebar Collapsing hid the sidebar entirely and left only a tile inside the rail to bring it back, so it read as the sidebar vanishing into the rail. That tile is replaced by a half-round ribbon on the rail's outer seam carrying the double-chevron, shown only while collapsed on desktop, themed from the existing rail/sidebar tokens. ## Transcription in huddles A huddle is the same mesh as an enclave voice channel with the same per-participant mic capture, and was excluded only because its room is not flagged `is_voice`: the surface check accepted a DM or a voice room and 404'd everything else, and the huddle bar omitted the controls. Captions therefore worked in DMs and voice channels but silently failed in group huddles. Any room is now a call surface and authority moved to `require_participant`, which demands live mesh membership for anything that is not a DM, so a member who has not joined is forbidden rather than the room being hidden. Controls added to the huddle bar. ## Other fixes - Thread reply box: typed text was invisible. It reuses `.lc-mdinput`, which sets `color: transparent` because in the main composer a `.lc-mdback` backdrop paints the text; the thread box has no such backdrop, so only the caret showed. - Settings: dismiss by clicking outside it or pressing Escape. It fills the main pane with no close control, so the only exits were a sidebar room link or the browser back button. ## Verification Driven in a real browser against seeded data: picker opens/positions/closes, the first reaction creates a chip and reveals the `+`, quick-react adds and toggles off, concurrent double-toggle returns no 5xx, the ribbon shows only when collapsed and re-expands on click, and the huddle bar carries the transcribe controls. Full server test suite passes, including the transcripts test updated for the new huddle contract. Note: `last_visited.rs` has two pre-existing failures (`home_renders_welcome_when_no_cookie`, `home_welcome_renders_quick_actions`). Confirmed unrelated to this branch by reproducing them with these changes stashed. ## Follow-ups filed LC-594 (reactions on thread replies + the duplicate thread-panel `msg` id), LC-595 (make group video calls easy: ring/invite, discovery, SFU-backed huddles), LC-597 (Stage SFU transcription).
The thread reply textarea reuses .lc-mdinput for its chrome, but that class sets color:transparent because in the main composer the visible text is painted by a .lc-mdback highlight backdrop sitting behind it inside .lc-mdwrap. The thread box has no such backdrop, so typed text rendered fully transparent and only the caret was visible. Restore a solid text colour and a field background for the solo #thread-panel .lc-mdinput so replies are legible as you type.

#LC-553
feat(settings): dismiss the settings page by clicking outside it or Escape
All checks were successful
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / TruffleHog (push) Successful in 5s
check-secrets / Kingfisher (push) Successful in 6s
1b61c3cc31
Settings fills the #main pane with no close control, so the only way out was clicking a sidebar room link or the browser back button. Add document-level dismissal: a pointerdown on the rail/sidebar background (anything outside #main that is not itself an interactive control) or the Escape key steps back to whatever was behind settings, falling back to home when there is no in-app history. Clicks inside the settings pane, including its header, never dismiss, and real controls like sidebar room links keep navigating normally.

#LC-553
feat(reactions): working emoji picker from the hover bar; conditional add-more
All checks were successful
check-secrets / Nosey parker (push) Successful in 3s
check-secrets / TruffleHog (push) Successful in 8s
check-secrets / Kingfisher (push) Successful in 10s
0a153a1fa5
Reactions could not actually be added through the UI: the picker was authored around a shared #lc-reaction-popover host and a set of tab/filter/recent/close hooks, but the script that opened and drove it was never in the tree, and the host element did not exist. So the add-reaction control did nothing and only pre-existing chips could be toggled.

Restore the missing runtime and reshape the affordances per the requested UX:
- Add reactions.js: opens the picker into a fixed #lc-reaction-popover host (added to layout.html), positions it by the trigger, switches category tabs, filters by name, keeps a recent-emoji MRU, and closes on react / Escape / outside-click. Wired into base.html.
- Move the primary emoji-select into the message hover bar (next to reply / thread / more), so starting a reaction happens there.
- The under-message "+" now renders only when a message already has at least one reaction, so anyone can add to an existing set; at zero reactions there is no "+" and no picker under the text.
- Fix a duplicate id: the reaction bar was nested inside a second element also carrying id="reactions-{id}", so the first outerHTML toggle replaced the wrong node and dropped the spacing wrapper. reaction_bar.html's own root is now the sole #reactions-{id}, server-hidden when empty so no gap is reserved at rest.
- Align the WS OOB echo (ws/reaction_update.html) with reaction_bar.html so a live update no longer clobbers the rich bar with degraded markup for the reactor who also receives the broadcast.

#LC-553
toggle_reaction did SELECT-then-INSERT/DELETE with no transaction, so two concurrent toggles from the same user (a double-tap) could both see "absent" and both INSERT; the second violated the (message_id, user_id, emoji) primary key and surfaced as a 500, leaving the reaction on when the pair of taps meant on-then-off. Each branch is now a single atomic statement keyed on rows-affected, with INSERT OR IGNORE so a lost race is a no-op and the returned flag always matches the row's final presence.

Also populate the quick-react bar. Every message row carried a data-lc-quick-bar placeholder that shipped hidden with nothing to fill it, so the one-tap row never appeared. It is now seeded from the caller's frequency-ranked emoji (/api/reactions/frequent, which existed but was never called) merged with the device-local recent MRU and topped up with defaults, filled lazily on first hover, and a one-tap react feeds back into the MRU.

#LC-553
Collapsing the sidebar hid it entirely and left only a tile inside the rail to bring it back, so it read as the sidebar disappearing into the rail. Replace that in-rail tile with a half-round ribbon tab sitting on the rail's outer seam, carrying the double-chevron: it reads as "pull the sidebar back out" rather than another rail icon. Shown only while collapsed and only on desktop, where the collapse applies; fixed-positioned so the nav panel cannot clip it, and themed from the existing rail/sidebar tokens so it follows every palette.

#LC-553
feat(transcription): support transcription in huddles
Some checks failed
check-secrets / TruffleHog (push) Successful in 8s
check-secrets / Nosey parker (push) Successful in 9s
check-secrets / Kingfisher (push) Successful in 11s
check-secrets / Nosey parker (pull_request) Successful in 5s
check-secrets / Kingfisher (pull_request) Successful in 5s
check-secrets / TruffleHog (pull_request) Successful in 7s
Check / clippy + fmt + tests (pull_request) Failing after 11s
0b05fbcc20
A huddle is the same WebRTC mesh as an enclave voice channel, keyed to the group room's id, with the same per-participant mic capture transcription relies on. It was excluded only because its room is not flagged is_voice: fetch_call_room accepted a DM or a voice room and 404'd everything else, and the huddle bar deliberately omitted the controls. So captions worked in DMs and voice channels but silently failed in group huddles.

Treat any room as a call surface and move the authority to require_participant, which now demands live mesh membership for anything that is not a DM (the hub keys huddles by the text room's id, so voice_room_users covers both). A group room is therefore no longer hidden behind a 404; a member who has not joined the huddle is forbidden instead. Add the transcribe and transcript-panel controls to the huddle bar, revealed on join by the existing voice.js scoping.

Updates the transcripts test that encoded the old DM-only contract.

#LC-553
style(reactions): rustfmt the toggle_reaction insert
All checks were successful
check-secrets / TruffleHog (push) Successful in 5s
check-secrets / TruffleHog (pull_request) Successful in 5s
check-secrets / Kingfisher (pull_request) Successful in 5s
check-secrets / Kingfisher (push) Successful in 6s
Check / clippy + fmt + tests (pull_request) Successful in 4m29s
Create release / Create release from merged PR (pull_request) Has been skipped
check-secrets / Nosey parker (pull_request) Successful in 6s
check-secrets / Nosey parker (push) Successful in 6s
15edf7d654
CI runs cargo fmt --check; the INSERT OR IGNORE statement exceeded the line width and needed the wrapped form.

#LC-553
vas2000-work deleted branch feat/sidebar-collapse-ribbon 2026-07-18 21:00:21 +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!553
No description provided.