import type { CompactEmoji } from 'emojibase'; export type IEmoji = CompactEmoji & { shortcode: string; }; export enum EmojiGroupId { People = 'People', Nature = 'Nature', Food = 'Food', Activity = 'Activity', Travel = 'Travel', Object = 'Object', Symbol = 'Symbol', Flag = 'Flag', } export type IEmojiGroup = { id: EmojiGroupId; order: number; emojis: IEmoji[]; }; export type EmojiData = { emojis: IEmoji[]; emojiGroups: IEmojiGroup[]; }; type ShortcodeMap = Record; /** * PERF (lazy emojibase split): the heavy `emojibase-data` JSON (compact emoji * data + the joypixels/emojibase shortcode maps, ~965 KB combined) used to be * imported statically at module top-level. Because reaction/message rendering * (`Reaction`, `scaleSystemEmoji`) import this module eagerly, that dragged the * whole `emojibase` chunk into the initial (eager) bundle graph. * * It is now loaded on demand via `loadEmojiData()` (a memoized dynamic import). * Only lazy emoji surfaces (EmojiBoard, EmoticonAutocomplete, recent-emoji) * trigger the load. Anything that renders eagerly (reaction/emoji tooltips and * aria-labels via `getShortcodeFor`) gracefully degrades to `undefined` until * the data has been loaded — the visible emoji glyph itself never depended on * this data, so on-screen UX is unchanged; the shortcode label simply resolves * once emoji data is loaded. `getHexcodeForEmoji` is inlined below so it stays * synchronous WITHOUT pulling the `emojibase` runtime into the eager graph. */ // Inlined from emojibase's `fromUnicodeToHexcode` so this synchronous helper // does not import the `emojibase` package (and thus the emojibase chunk) into // the eager graph. Kept byte-for-byte behaviourally identical. const SEQUENCE_REMOVAL_PATTERN = /200D|FE0E|FE0F/g; export const getHexcodeForEmoji = (unicode: string, strip = true): string => { const hexcode: string[] = []; [...unicode].forEach((codepoint) => { let hex = codepoint.codePointAt(0)?.toString(16).toUpperCase() ?? ''; while (hex.length < 4) { hex = `0${hex}`; } if (!strip || !hex.match(SEQUENCE_REMOVAL_PATTERN)) { hexcode.push(hex); } }); return hexcode.join('-'); }; // Populated by loadEmojiData(); `undefined` until the data has been loaded. let joypixelsShortcodes: ShortcodeMap | undefined; let emojibaseShortcodes: ShortcodeMap | undefined; export const getShortcodesFor = (hexcode: string): string[] | string | undefined => { if (!joypixelsShortcodes || !emojibaseShortcodes) return undefined; return joypixelsShortcodes[hexcode] || emojibaseShortcodes[hexcode]; }; export const getShortcodeFor = (hexcode: string): string | undefined => { const shortcode = getShortcodesFor(hexcode); return Array.isArray(shortcode) ? shortcode[0] : shortcode; }; // Shared, stable array references. They start empty and are populated in place // the first time loadEmojiData() resolves (mirroring the previous eager module // side-effect). React consumers await loadEmojiData() and re-render to observe // the populated data; non-React consumers (recent-emoji) read them after load. export const emojiGroups: IEmojiGroup[] = [ { id: EmojiGroupId.People, order: 0, emojis: [] }, { id: EmojiGroupId.Nature, order: 1, emojis: [] }, { id: EmojiGroupId.Food, order: 2, emojis: [] }, { id: EmojiGroupId.Activity, order: 3, emojis: [] }, { id: EmojiGroupId.Travel, order: 4, emojis: [] }, { id: EmojiGroupId.Object, order: 5, emojis: [] }, { id: EmojiGroupId.Symbol, order: 6, emojis: [] }, { id: EmojiGroupId.Flag, order: 7, emojis: [] }, ]; export const emojis: IEmoji[] = []; function addEmojiToGroup(groupIndex: number, emoji: IEmoji) { emojiGroups[groupIndex].emojis.push(emoji); } function getGroupIndex(emoji: IEmoji): number | undefined { if (emoji.group === 0 || emoji.group === 1) return 0; if (emoji.group === 3) return 1; if (emoji.group === 4) return 2; if (emoji.group === 6) return 3; if (emoji.group === 5) return 4; if (emoji.group === 7) return 5; if (emoji.group === 8 || typeof emoji.group === 'undefined') return 6; if (emoji.group === 9) return 7; return undefined; } let emojiDataPromise: Promise | undefined; /** * Lazily load emojibase data (dynamic import → the `emojibase` chunk). Memoized: * the JSON is fetched/parsed and `emojis`/`emojiGroups` are built exactly once. */ export const loadEmojiData = (): Promise => { if (!emojiDataPromise) { emojiDataPromise = (async (): Promise => { const [emojisModule, joypixelsModule, emojibaseModule] = await Promise.all([ import('emojibase-data/en/compact.json'), import('emojibase-data/en/shortcodes/joypixels.json'), import('emojibase-data/en/shortcodes/emojibase.json'), ]); joypixelsShortcodes = joypixelsModule.default as ShortcodeMap; emojibaseShortcodes = emojibaseModule.default as ShortcodeMap; const emojisData = emojisModule.default as unknown as CompactEmoji[]; emojisData.forEach((emoji) => { const myShortCodes = getShortcodesFor(emoji.hexcode); if (!myShortCodes) return; if (Array.isArray(myShortCodes) && myShortCodes.length === 0) return; const em: IEmoji = { ...emoji, shortcode: Array.isArray(myShortCodes) ? myShortCodes[0] : myShortCodes, shortcodes: Array.isArray(myShortCodes) ? myShortCodes : emoji.shortcodes, }; const groupIndex = getGroupIndex(em); if (groupIndex !== undefined) { addEmojiToGroup(groupIndex, em); emojis.push(em); } }); return { emojis, emojiGroups }; })(); // Don't cache a rejection: a transient chunk-load failure (e.g. mid-deploy // 404) would otherwise permanently disable emoji data until a full reload. emojiDataPromise = emojiDataPromise.catch((err) => { emojiDataPromise = undefined; throw err; }); } return emojiDataPromise; };