mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
28
desktop/src/features/custom-emoji/ui/emojiMartPrewarm.ts
Normal file
28
desktop/src/features/custom-emoji/ui/emojiMartPrewarm.ts
Normal 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 };
|
||||
@@ -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",
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user