feat(reactions): full categorized, searchable emoji picker (LC-389) #400

Merged
longjacksonle merged 1 commit from feat/lc-389-full-emoji-picker into main 2026-06-20 05:15:38 +02:00

Implements LC-389. The reaction emoji picker offered only a hardcoded six glyphs (👍 🖤 😂 🎉 😮 😢) under the search box. This expands it to the full, browsable Unicode set.

What changed

  • reactions::get_picker now renders the complete emojis-crate set grouped into the eight standard categories (Smileys & Emotion, People & Body, Animals & Nature, Food & Drink, Travel & Places, Activities, Objects, Symbols, Flags), each as a labeled section, in Unicode order. The room's enclave custom emojis follow as a Custom section. DMs / non-enclave rooms get the Unicode set with no Custom section.
  • A horizontally-scrollable category tab strip sits above the grid; each tab is a representative glyph that scrolls the grid to its section and takes an accent underline when active.
  • Search now matches human name plus every shortcode across the whole set (so "heart" surfaces the heart family, "+1" / "thumbsup" find 👍). While a query is active the tab strip hides and any section with no surviving cell collapses, so results read as one flat list; clearing the box restores the browsable category view.
  • The LC-288 Recent row is unchanged and still sits on top.

Why it needed almost no JS

The data was already on hand: the emojis crate is an existing dependency and the :-shortcode composer autocomplete already iterates the full set with .name() / .shortcodes() / .group(). Every react button keeps the exact LC-384 markup (data-lc-emoji-name filter hook + the hx-post / hx-target / hx-swap the recorder and close-on-react handlers key on), so the LC-274 filter, the LC-288 recent recorder, and the react/close flow all work without modification. The only new JS is the tab-scroll handler and the search's section-collapse pass.

Architecture note

The grid is server-rendered per open (the buttons are message-specific via the baked hx-post). At ~1900 cells this is a meaningful but on-demand, gzip-friendly payload, and it keeps the change entirely in the server-rendered-fragment grain with zero new client state. If picker-open latency ever shows up on low-end devices, the follow-up is a message-independent, browser-cached grid fragment with a delegated react handler (sketched in the ticket). Out of scope here: skin-tone variants, and unifying the composer picker (LC-316) onto the same machinery.

Testing

  • just check (fmt + clippy, standalone and saas) clean.
  • just test and just test-saas both green (exit 0, zero failures). routes_reactions_authz still passes: the picker continues to ship data-lc-emoji-filter, data-lc-emoji-name=, and data-lc-emoji-recent.
  • just build-css regenerated (w-72 utility now emitted).

No operator-visible change (UI only): no env var, config, or API-contract change, so no [operator-action] marker.

Implements LC-389. The reaction emoji picker offered only a hardcoded six glyphs (👍 🖤 😂 🎉 😮 😢) under the search box. This expands it to the full, browsable Unicode set. ## What changed - `reactions::get_picker` now renders the complete `emojis`-crate set grouped into the eight standard categories (Smileys & Emotion, People & Body, Animals & Nature, Food & Drink, Travel & Places, Activities, Objects, Symbols, Flags), each as a labeled section, in Unicode order. The room's enclave custom emojis follow as a Custom section. DMs / non-enclave rooms get the Unicode set with no Custom section. - A horizontally-scrollable category tab strip sits above the grid; each tab is a representative glyph that scrolls the grid to its section and takes an accent underline when active. - Search now matches human name plus every shortcode across the whole set (so "heart" surfaces the heart family, "+1" / "thumbsup" find 👍). While a query is active the tab strip hides and any section with no surviving cell collapses, so results read as one flat list; clearing the box restores the browsable category view. - The LC-288 Recent row is unchanged and still sits on top. ## Why it needed almost no JS The data was already on hand: the `emojis` crate is an existing dependency and the `:`-shortcode composer autocomplete already iterates the full set with `.name()` / `.shortcodes()` / `.group()`. Every react button keeps the exact LC-384 markup (`data-lc-emoji-name` filter hook + the `hx-post` / `hx-target` / `hx-swap` the recorder and close-on-react handlers key on), so the LC-274 filter, the LC-288 recent recorder, and the react/close flow all work without modification. The only new JS is the tab-scroll handler and the search's section-collapse pass. ## Architecture note The grid is server-rendered per open (the buttons are message-specific via the baked `hx-post`). At ~1900 cells this is a meaningful but on-demand, gzip-friendly payload, and it keeps the change entirely in the server-rendered-fragment grain with zero new client state. If picker-open latency ever shows up on low-end devices, the follow-up is a message-independent, browser-cached grid fragment with a delegated react handler (sketched in the ticket). Out of scope here: skin-tone variants, and unifying the composer picker (LC-316) onto the same machinery. ## Testing - `just check` (fmt + clippy, standalone and saas) clean. - `just test` and `just test-saas` both green (exit 0, zero failures). `routes_reactions_authz` still passes: the picker continues to ship `data-lc-emoji-filter`, `data-lc-emoji-name=`, and `data-lc-emoji-recent`. - `just build-css` regenerated (w-72 utility now emitted). No operator-visible change (UI only): no env var, config, or API-contract change, so no `[operator-action]` marker.
feat(reactions): full categorized, searchable emoji picker (LC-389)
All checks were successful
check-secrets / TruffleHog (push) Successful in 3s
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 8s
check-secrets / Nosey parker (pull_request) Successful in 9s
check-secrets / Kingfisher (pull_request) Successful in 9s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 5m11s
e075476653
Replace the hardcoded six-emoji reaction picker with the complete Unicode set organized into the eight standard categories plus the room's enclave custom emojis.

The full set comes from the `emojis` crate (already a dependency, used by the `:`-shortcode autocomplete): `get_picker` now iterates each `emojis::Group` in Unicode order, emitting a labeled section per category and a horizontally-scrollable tab strip whose representative glyphs scroll the grid to each section. Per-enclave custom emojis render as a trailing Custom section/tab; DMs and non-enclave rooms see Unicode only.

Each react button keeps the exact LC-384 markup (`data-lc-emoji-name` filter hook + the `hx-post`/`hx-target`/`hx-swap` the recorder and close-on-react JS key on), so the LC-274 filter, the LC-288 recent row, and the react/close flow all work unchanged. The filter now searches name plus every shortcode across the whole set and, while a query is active, hides the tab strip and collapses any category section with no surviving cell so results read as one flat list.

New `.lc-emoji-scroll` / `.lc-emoji-cat-label` / `.lc-emoji-cat-grid` / `.lc-emoji-tabs` / `.lc-emoji-tab` styles in the raw main.css; ten new category labels added to the en and es locales. Picker card widened to w-72 for an 8-column grid.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/lc-389-full-emoji-picker 2026-06-20 05:15:38 +02:00
Author
Owner

Scope update: this PR now also applies the full categorized set to the composer's emoji button (the unification I had flagged as a follow-up above), per request.

Second commit (5a931fc):

  • The composer picker (GET /rooms/{id}/emoji-picker) replaces its curated ~48 "popular" glyphs with the full Unicode set in the same eight categories plus the room's custom emojis.
  • The category grouping (the (Group, slug, label-key, tab-glyph) table + the name-plus-shortcodes keyword builder) is factored into a shared crate::emoji_catalog, so the reaction picker and the composer picker are one source of truth and cannot drift.
  • The composer picker reuses the reaction picker's client hooks (root id already starts with picker-, now also carries data-lc-emoji-tabs / data-lc-emoji-cat), so the LC-274 filter, the LC-389 section-collapse, and the tab-scroll handler all apply with zero new JS. Its buttons keep data-lc-emoji-insert (cursor splice) rather than an hx-post.
  • EmojiPickerFragment moves from a flat list to Vec<EmojiCategory>; the Askama template renders the tab strip + labeled sections; panel widened to w-72.

The :shortcode: autocomplete (get_autocomplete) is unchanged. just check, just test, and just test-saas all green again (the renamed picker_lists_unicode_and_custom_with_insert_hooks test asserts the categorized contract).

Scope update: this PR now also applies the full categorized set to the composer's emoji button (the unification I had flagged as a follow-up above), per request. Second commit (`5a931fc`): - The composer picker (`GET /rooms/{id}/emoji-picker`) replaces its curated ~48 "popular" glyphs with the full Unicode set in the same eight categories plus the room's custom emojis. - The category grouping (the `(Group, slug, label-key, tab-glyph)` table + the name-plus-shortcodes keyword builder) is factored into a shared `crate::emoji_catalog`, so the reaction picker and the composer picker are one source of truth and cannot drift. - The composer picker reuses the reaction picker's client hooks (root id already starts with `picker-`, now also carries `data-lc-emoji-tabs` / `data-lc-emoji-cat`), so the LC-274 filter, the LC-389 section-collapse, and the tab-scroll handler all apply with zero new JS. Its buttons keep `data-lc-emoji-insert` (cursor splice) rather than an `hx-post`. - `EmojiPickerFragment` moves from a flat list to `Vec<EmojiCategory>`; the Askama template renders the tab strip + labeled sections; panel widened to `w-72`. The `:shortcode:` autocomplete (`get_autocomplete`) is unchanged. `just check`, `just test`, and `just test-saas` all green again (the renamed `picker_lists_unicode_and_custom_with_insert_hooks` test asserts the categorized contract).
Author
Owner

Folded in the fix for LC-391 (the "Filter emoji" box appearing to do nothing), found while testing this picker.

Commit dcbca36:

  • Root cause: the LC-274 filter hides non-matching cells via the hidden attribute, but .lc-emoji-cell { display: inline-flex } and .lc-emoji-tabs { display: flex } are author rules that override the UA [hidden] { display: none } regardless of specificity - so filtered-out cells and the hidden tab strip kept showing and the filter looked dead (typing "heart" narrowed nothing). Same class of bug as the LC-385 regression. The <section data-lc-emoji-cat> elements DID hide (default block display), so a matching category showed ALL its emoji rather than just the matches.
  • Fix: .lc-emoji-cell[hidden], .lc-emoji-tabs[hidden] { display: none } in main.css. The attribute-qualified selector (0,2,0) beats the bare class (0,1,0); no !important. Sections need no rule. Fixes both the reaction picker and the composer picker, all four themes.

just check, just test, just test-saas all green.

Folded in the fix for LC-391 (the "Filter emoji" box appearing to do nothing), found while testing this picker. Commit `dcbca36`: - **Root cause:** the LC-274 filter hides non-matching cells via the `hidden` attribute, but `.lc-emoji-cell { display: inline-flex }` and `.lc-emoji-tabs { display: flex }` are author rules that override the UA `[hidden] { display: none }` regardless of specificity - so filtered-out cells and the hidden tab strip kept showing and the filter looked dead (typing "heart" narrowed nothing). Same class of bug as the LC-385 regression. The `<section data-lc-emoji-cat>` elements DID hide (default block display), so a matching category showed ALL its emoji rather than just the matches. - **Fix:** `.lc-emoji-cell[hidden], .lc-emoji-tabs[hidden] { display: none }` in `main.css`. The attribute-qualified selector (0,2,0) beats the bare class (0,1,0); no `!important`. Sections need no rule. Fixes both the reaction picker and the composer picker, all four themes. `just check`, `just test`, `just test-saas` all green.
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!400
No description provided.