2026-07-02 00:19:16 -04:00
|
|
|
import type { CompactEmoji } from 'emojibase';
|
2023-06-12 21:15:23 +10:00
|
|
|
|
|
|
|
|
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[];
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-02 00:19:16 -04:00
|
|
|
export type EmojiData = {
|
|
|
|
|
emojis: IEmoji[];
|
|
|
|
|
emojiGroups: IEmojiGroup[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type ShortcodeMap = Record<string, string | string[]>;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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];
|
|
|
|
|
};
|
2023-10-06 13:44:06 +11:00
|
|
|
|
|
|
|
|
export const getShortcodeFor = (hexcode: string): string | undefined => {
|
2026-07-02 00:19:16 -04:00
|
|
|
const shortcode = getShortcodesFor(hexcode);
|
2023-10-06 13:44:06 +11:00
|
|
|
return Array.isArray(shortcode) ? shortcode[0] : shortcode;
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-02 00:19:16 -04:00
|
|
|
// 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.
|
2023-06-12 21:15:23 +10:00
|
|
|
export const emojiGroups: IEmojiGroup[] = [
|
2026-07-02 00:19:16 -04:00
|
|
|
{ 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: [] },
|
2023-06-12 21:15:23 +10:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-02 00:19:16 -04:00
|
|
|
let emojiDataPromise: Promise<EmojiData> | 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<EmojiData> => {
|
|
|
|
|
if (!emojiDataPromise) {
|
|
|
|
|
emojiDataPromise = (async (): Promise<EmojiData> => {
|
|
|
|
|
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;
|
|
|
|
|
});
|
2023-06-12 21:15:23 +10:00
|
|
|
}
|
2026-07-02 00:19:16 -04:00
|
|
|
return emojiDataPromise;
|
|
|
|
|
};
|