fix(reactions): self-heal the Recent emoji row after a react (LC-390) #402

Merged
longjacksonle merged 1 commit from fix/lc-390-recent-emoji-refresh into main 2026-06-20 05:51:29 +02:00

Fixes LC-390. The reaction picker's "Recent" row did not reflect a just-used emoji until a full page reload.

Root cause

The Recent row is rendered empty by the server and filled entirely client-side from localStorage['lc-recent-emoji'] by the LC-288 IIFE in layout.html. fill() was a one-shot, open-time snapshot: it set data-lc-recent-done on first scan and read localStorage at that instant, then never refilled for the life of that picker instance. So any react performed after the row was filled stayed invisible until a reload re-ran the fill from scratch. (Confirmed not a service-worker issue: sw.js does not cache the picker fragment, so the row was always built from fresh markup; the staleness was purely in the one-shot fill.)

Fix

  • Idempotent rebuild. render() rebuilds the row from the current MRU on every scan, skipping only when this box's rendered set is unchanged (signature = the joined glyph list). The scan firing on unrelated afterSettle events (WS / sidebar swaps) is therefore a cheap no-op and never duplicates buttons, while a changed MRU always refills. The scan selector drops the :not([data-lc-recent-done]) gate.
  • Immediate same-tab refresh. record() now calls scan() after writing localStorage, so an already-open picker updates the instant you react (no reopen, no reload).
  • Cross-tab refresh. A storage listener refreshes the row when another tab reacts or clears the MRU.
  • Glyph canonicalization (secondary). Some emoji render text-style/tofu unless followed by VS-16, and the grid emits the fully-qualified form while older MRU entries and the LC-302 quick-bar defaults stored the bare code point. canon() appends VS-16 to the known bare emoji-presentation code points on both record and read (so legacy entries dedupe onto the qualified form), and the quick-bar heart default is now fully qualified. Recent now matches the grid and dedupes correctly.

Client-only change in server/templates/layout.html; the server still renders the Recent row empty (no Rust change).

Testing

  • ./dev/cargo check clean (Askama template compiles).
  • just test and just test-saas both green (exit 0, zero failures). No server contract changed; the fix is in the picker's client JS, which the integration tests don't exercise, so the validation is the compile + the manual repro below.
  • Manual: react -> reopen (no reload) shows the emoji first in Recent; react repeatedly across grid / recent / quick-react / pill stays current with no duplicates or text-style glyphs.

No operator-visible change (UI only): no [operator-action] marker.

Fixes LC-390. The reaction picker's "Recent" row did not reflect a just-used emoji until a full page reload. ## Root cause The Recent row is rendered empty by the server and filled entirely client-side from `localStorage['lc-recent-emoji']` by the LC-288 IIFE in `layout.html`. `fill()` was a one-shot, open-time snapshot: it set `data-lc-recent-done` on first scan and read localStorage at that instant, then never refilled for the life of that picker instance. So any react performed after the row was filled stayed invisible until a reload re-ran the fill from scratch. (Confirmed not a service-worker issue: `sw.js` does not cache the picker fragment, so the row was always built from fresh markup; the staleness was purely in the one-shot fill.) ## Fix - **Idempotent rebuild.** `render()` rebuilds the row from the current MRU on every scan, skipping only when this box's rendered set is unchanged (signature = the joined glyph list). The scan firing on unrelated `afterSettle` events (WS / sidebar swaps) is therefore a cheap no-op and never duplicates buttons, while a changed MRU always refills. The scan selector drops the `:not([data-lc-recent-done])` gate. - **Immediate same-tab refresh.** `record()` now calls `scan()` after writing localStorage, so an already-open picker updates the instant you react (no reopen, no reload). - **Cross-tab refresh.** A `storage` listener refreshes the row when another tab reacts or clears the MRU. - **Glyph canonicalization (secondary).** Some emoji render text-style/tofu unless followed by VS-16, and the grid emits the fully-qualified form while older MRU entries and the LC-302 quick-bar defaults stored the bare code point. `canon()` appends VS-16 to the known bare emoji-presentation code points on both record and read (so legacy entries dedupe onto the qualified form), and the quick-bar heart default is now fully qualified. Recent now matches the grid and dedupes correctly. Client-only change in `server/templates/layout.html`; the server still renders the Recent row empty (no Rust change). ## Testing - `./dev/cargo check` clean (Askama template compiles). - `just test` and `just test-saas` both green (exit 0, zero failures). No server contract changed; the fix is in the picker's client JS, which the integration tests don't exercise, so the validation is the compile + the manual repro below. - Manual: react -> reopen (no reload) shows the emoji first in Recent; react repeatedly across grid / recent / quick-react / pill stays current with no duplicates or text-style glyphs. No operator-visible change (UI only): no `[operator-action]` marker.
fix(reactions): self-heal the Recent emoji row after a react (LC-390)
All checks were successful
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 3s
check-secrets / Kingfisher (pull_request) Successful in 4s
check-secrets / TruffleHog (push) Successful in 3s
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / Nosey parker (pull_request) Successful in 4s
Check / clippy + fmt + tests (pull_request) Successful in 2m36s
Create release / Create release from merged PR (pull_request) Has been skipped
61a1b553bd
The picker's Recent row was a one-shot, open-time snapshot: fill() set data-lc-recent-done on first scan and read localStorage at that instant, so a react performed afterwards was not reflected until a full page reload re-ran the fill from scratch.

Make the fill idempotent. render() now rebuilds the row from the current MRU on every scan, skipping only when the box's rendered set is unchanged (signature = the joined glyph list), so the scan firing on unrelated afterSettle events (WS / sidebar swaps) never duplicates buttons while a changed MRU always refills. record() now triggers a scan after writing, so an open picker updates immediately; a storage listener refreshes the row when another tab reacts. The scan selector drops the :not([data-lc-recent-done]) gate.

Also canonicalize glyphs (LC-390 secondary): some emoji render text-style/tofu unless followed by VS-16, and the grid emits the fully-qualified form while older MRU entries and the quick-bar defaults stored the bare code point. canon() appends VS-16 to the known bare emoji-presentation code points on both record and read (legacy entries dedupe onto the qualified form), and the quick-bar heart default is now fully qualified, so Recent always matches the grid.

Client-only change in layout.html; the server still renders the Recent row empty. Verified the picker fragment is not service-worker cached, so this was never an SW-staleness issue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch fix/lc-390-recent-emoji-refresh 2026-06-20 05:51:29 +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!402
No description provided.