fix(desktop): restore emoji recents (#6263)

## Summary

- normalize emoji-mart's poisoned literal `{}` recents state before
initialization
- preserve the established idle emoji-index prewarm so first picker open
stays responsive
- keep the mounted quick-reaction tray behavior unchanged from main
- cover full-picker same-session recents, prewarm ordering/callback
behavior, and the production picker-to-prewarm import boundary

## Validation

- focused prewarm tests passed 2/2 at
`98188797e219db20670b46379a240493ff8f4cbe`
- mutation check passed: changing `EmojiPicker` back to direct
`@emoji-mart/data` consumption fails the new production-boundary
assertion
- pre-push branch-skew, file-size, Desktop check, typecheck, and full
Desktop unit suite passed (5,024/5,024) at exact pushed head
`98188797e219db20670b46379a240493ff8f4cbe`
- focused full-picker lifecycle plus existing custom-emoji/tray smoke
passed 2/2 on the preceding runtime-equivalent head
- independent Royal Court scope review and headless field test passed
the full-picker journey on the preceding runtime-equivalent head

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
Wes
2026-08-19 14:31:15 -06:00
committed by GitHub
parent 81567b76a5
commit f96f1883c5
5 changed files with 116 additions and 26 deletions

View File

@@ -1,32 +1,9 @@
import data from "@emoji-mart/data";
import Picker from "@emoji-mart/react";
import { init } from "emoji-mart";
import * as React from "react";
import { buildCustomEmojiCategory } from "@/features/custom-emoji/emojiMartCategory";
import { useCustomEmoji } from "@/features/custom-emoji/hooks";
// emoji-mart builds its searchable index synchronously inside `init`, which
// `<Picker>` calls on mount — so the first reaction popover open paid the full
// ~1.8k-emoji index build and froze the cursor. Warm `init({ data })` once at
// idle so the index is prebuilt; `init` is a no-op after the first call (its
// `Data` singleton guards the rebuild), so the Picker's mount-time `init` skips
// the heavy work. Search still reads the prebuilt index — no first-keystroke
// hitch. Module-level so it fires regardless of when a picker first mounts.
let warmStarted = false;
function warmEmojiIndex() {
if (warmStarted) {
return;
}
warmStarted = true;
const warm = () => void init({ data });
if (typeof window !== "undefined" && "requestIdleCallback" in window) {
window.requestIdleCallback(warm, { timeout: 1_500 });
} else {
globalThis.setTimeout(warm, 250);
}
}
warmEmojiIndex();
import { emojiMartData } from "@/features/custom-emoji/ui/emojiMartPrewarm";
/**
* Reach into the `em-emoji-picker` shadow root and disable spellcheck,
@@ -128,7 +105,7 @@ export const EmojiPicker = React.memo(function EmojiPicker({
<Picker
autoFocus={autoFocus}
custom={custom}
data={data}
data={emojiMartData}
maxFrequentRows={2}
onEmojiSelect={(emoji: { native?: string; id?: string }) => {
// Standard emoji carry a `native` glyph. Custom emoji don't — emit

View File

@@ -0,0 +1,50 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const pickerSource = await readFile(
new URL("./EmojiPicker.tsx", import.meta.url),
"utf8",
);
const entries = new Map([
["emoji-mart.frequently", "{}"],
["emoji-mart.last", "thumbsup"],
]);
const idleCalls = [];
const initCalls = [];
globalThis.window = {
localStorage: {
getItem: (key) => entries.get(key) ?? null,
removeItem: (key) => entries.delete(key),
},
requestIdleCallback: (callback, options) => {
idleCalls.push({ callback, options });
return 1;
},
};
globalThis.__BUZZ_TEST_EMOJI_MART_INIT__ = (...args) => initCalls.push(args);
const { emojiMartData } = await import("./emojiMartPrewarm.ts");
test("EmojiPicker consumes data through the prewarm module", () => {
assert.match(
pickerSource,
/import\s*{\s*emojiMartData\s*}\s*from\s*["']@\/features\/custom-emoji\/ui\/emojiMartPrewarm["'];/,
);
assert.match(pickerSource, /<Picker[\s\S]*?\bdata={emojiMartData}/);
assert.doesNotMatch(pickerSource, /from\s*["']@emoji-mart\/data["']/);
});
test("normalizes poisoned recents before scheduling the emoji index prewarm", () => {
assert.equal(entries.has("emoji-mart.frequently"), false);
assert.equal(entries.has("emoji-mart.last"), false);
assert.equal(initCalls.length, 0);
assert.equal(idleCalls.length, 1);
assert.deepEqual(idleCalls[0].options, { timeout: 1_500 });
idleCalls[0].callback();
assert.equal(initCalls.length, 1);
assert.equal(initCalls[0][0].data, emojiMartData);
});

View File

@@ -0,0 +1,28 @@
import data from "@emoji-mart/data";
import { init } from "emoji-mart";
// emoji-mart treats a persisted empty object differently from a missing index:
// a missing index gets its default Frequent row, while `{}` removes the entire
// category from the module-global picker data. Normalize that poisoned state
// before the first picker initializes.
try {
if (window.localStorage.getItem("emoji-mart.frequently") === "{}") {
window.localStorage.removeItem("emoji-mart.frequently");
window.localStorage.removeItem("emoji-mart.last");
}
} catch {
// emoji-mart also tolerates unavailable storage; picker selection still works.
}
// emoji-mart synchronously builds its search index inside `init`. Warm it at
// idle so the first picker open does not pay that cost. This module also owns
// the data passed to every Picker, making the prewarm part of that import path
// rather than a disconnected best-effort call.
const warm = () => void init({ data });
if (typeof window !== "undefined" && "requestIdleCallback" in window) {
window.requestIdleCallback(warm, { timeout: 1_500 });
} else {
globalThis.setTimeout(warm, 250);
}
export { data as emojiMartData };

View File

@@ -56,7 +56,7 @@ function resolveSourcePath(basePath) {
const stubModules = new Map([
[
"emoji-mart",
"export const init = () => {};\n" +
"export const init = (...args) => globalThis.__BUZZ_TEST_EMOJI_MART_INIT__?.(...args);\n" +
"export const SearchIndex = { search: async () => [] };\n" +
"export default {};\n",
],

View File

@@ -309,6 +309,41 @@ test("message quick reaction tray stays neutral after selecting a tray emoji", a
);
});
test("emoji picker keeps Frequently used live within the app session", async ({
page,
}) => {
await page.addInitScript(() => {
window.localStorage.setItem("emoji-mart.frequently", "{}");
window.localStorage.removeItem("emoji-mart.last");
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
const row = reactionTargetRow(page);
await expect(row).toBeVisible();
await row.hover();
await row.getByLabel("Open reactions").click();
let picker = page.locator("em-emoji-picker");
await expect(
picker.getByRole("button", { name: "Frequently used" }),
).toBeVisible();
await picker.locator("input[type='search']").fill("unicorn");
await picker.getByRole("button", { name: "🦄" }).first().click();
await expect(row.getByLabel("Toggle 🦄 reaction")).toBeVisible();
await row.hover();
await row.getByLabel("Open reactions").click();
picker = page.locator("em-emoji-picker");
await picker.getByRole("button", { name: "Frequently used" }).click();
await expect(
picker.getByRole("button", { name: "🦄" }).first(),
).toBeVisible();
});
test("reacting with a custom emoji renders via the loopback media proxy", async ({
page,
}) => {