Skip to content

fix(desktop): give the shared emoji picker a panel, and open the inbox on unread - #27

Merged
mattbalza merged 1 commit into
mainfrom
fix/emoji-picker-surface
Aug 7, 2026
Merged

fix(desktop): give the shared emoji picker a panel, and open the inbox on unread#27
mattbalza merged 1 commit into
mainfrom
fix/emoji-picker-surface

Conversation

@mattbalza

Copy link
Copy Markdown
Owner

Two unrelated desktop reports: "the emoji/reactions panel has no background and doesn't scroll", and "the inbox should open on unread".

The emoji picker

EmojiPicker renders emoji-mart into a bare unclassed <div>. Three things that make the picker usable live in .buzz-emoji-mart + useEmojiMartThemeVars + useEmojiMartStyles, and only ProfileAvatarEditor and AgentCreationPreview applied them — those render emoji-mart directly. So the component that exists to be the emoji picker was the only one without a surface.

Measured before the fix rather than assumed:

  • --rgb-background unset does not make the panel transparent. It drops emoji-mart onto its own theme="auto" palette, which follows prefers-color-scheme and not Buzz's theme class. Probed live: rgb(255, 255, 255) on a white app, inside popovers that carry bg-transparent border-0 shadow-none because they assume the picker paints itself. That is the "no background".
  • A wheel event over a shadow-root list is retargeted to the host, so the browser does not scroll .scroll on its own. useEmojiMartStyles installs a non-passive handler on it; without that the list is stuck on its first category. This is the half the original report called "doesn't scroll".
  • w-[352px] on the host was already dead code — unlayered .buzz-emoji-mart { width: 100% } beats a layered Tailwind utility regardless of specificity. Removed with dynamicWidth; re-probed identical.

className became an override on top of the surface classes, so the two call sites that size the picker keep working.

The inbox

unreadOnly was React.useState(false) — every launch reset to "all", so the one unhandled thing had to be found by reading past everything already handled.

New useInboxUnreadOnly: defaults to unread-only, remembers the last choice in localStorage. Not sessionStorage (the list width next door is a per-window layout preference; which mail is outstanding is not). Anything but the exact string "false" reads as the default, so a corrupt value can never silently hide unread mail.

Flipping the default broke six specs that assert on specific fixture rows, most of which are read. Instead of rewriting them or faking a different default in tests, installMockBridge now seeds the preference as explicitly off — the state of a user who already switched it — with seedInboxUnreadOnly: false to opt out. One new spec opts out and covers the shipped default.

Test plan

  • node --test desktop/src/features/home/useInboxUnreadOnly.test.mjs — 6/6
  • desktop/tests/e2e/emoji-picker-surface.spec.ts — 3/3, two proven red before the fix
  • desktop/tests/e2e/inbox-unread-default.spec.ts — 3/3
  • emoji + reaction smoke batch — 27/27
  • inbox / drafts / unread / reminder / project-inbox / home-collapsed batch — 59/59
  • npx tsc --noEmit clean; biome check clean on the changed files

…x on unread

Two independent user reports, both in the desktop app.

**The emoji picker had no background and swallowed the wheel.**
`EmojiPicker` renders emoji-mart into a bare unclassed `<div>`. Three things
that make the picker usable live elsewhere and were never applied here:
the `buzz-emoji-mart` surface class, `useEmojiMartThemeVars()` (which supplies
`--rgb-background`), and `useEmojiMartStyles()` (which injects the shadow-root
CSS *and* installs the wheel-scroll handler). Only `ProfileAvatarEditor` and
`AgentCreationPreview` had them, because those render emoji-mart directly — so
the component that exists to be "the one emoji picker" was the only one without.

Measured, not assumed: unset `--rgb-background` does not make the panel
transparent, it drops emoji-mart onto its own `theme="auto"` palette, which
follows `prefers-color-scheme` instead of Buzz's theme class — a white panel on
a white app, inside popovers that carry `bg-transparent border-0 shadow-none`
precisely because they assume the picker paints itself. And a wheel event over a
shadow-root list is retargeted to the host, so without the handler the list is
stuck on its first category.

`className` is now an override rather than the whole style, so the two callers
that already size the picker keep working.

**The inbox reset to "all" on every launch.**
`unreadOnly` was `React.useState(false)`. The inbox is a queue: it should open
on what is still owed. New `useInboxUnreadOnly` defaults to unread-only and
remembers the last choice in `localStorage` — not `sessionStorage`, because
which mail you consider outstanding is not a per-window layout preference.
Anything but the exact string `"false"` reads as the default, so a corrupt value
can never silently hide unread mail.

Flipping the default broke six specs that assert on specific fixture rows (most
of which are read). Rather than rewrite them or fake a different default, the
bridge helper now seeds the preference as explicitly off — the state of a user
who already switched it — with `seedInboxUnreadOnly: false` to opt out. One new
spec opts out and covers the shipped default.

Tests: `useInboxUnreadOnly.test.mjs` (6), `emoji-picker-surface.spec.ts` (3, two
of which were proven red before the fix), `inbox-unread-default.spec.ts` (3).
@mattbalza
mattbalza merged commit 4797950 into main Aug 7, 2026
23 checks passed
@mattbalza
mattbalza deleted the fix/emoji-picker-surface branch August 7, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant