snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,196 @@
import type { Emoji } from 'emojibase';
import data from 'emojibase-data/en/data.json';
import emojibaseShortcodes from 'emojibase-data/en/shortcodes/emojibase.json';
import joypixelsShortcodes from 'emojibase-data/en/shortcodes/joypixels.json';
import type { EmojiEntry } from './generateEmojiData';
import { getEmojiData } from './generateEmojiData';
import { legacyEmojioneMap } from './legacyEmojioneMap';
import { shortnameToUnicode } from './shortnameToUnicode';
const asArray = (entry: string | string[] | undefined): string[] => {
if (!entry) return [];
return Array.isArray(entry) ? entry : [entry];
};
/**
* The emoji we expose shortcodes for, derived independently from `generateEmojiData` so this suite
* fails if the two ever drift apart.
*/
const emittedEmojis = (() => {
const emojis = new Map<string, string>();
for (const emojiData of data as Emoji[]) {
if (emojiData.group === 2) continue;
const { hexcode } = emojiData;
const codepoint = hexcode.includes('-') ? NaN : parseInt(hexcode, 16);
const isRegional = codepoint >= 0x1f1e6 && codepoint <= 0x1f1ff;
if (emojiData.group === undefined && !isRegional) continue;
emojis.set(hexcode, emojiData.emoji);
for (const skin of emojiData.skins ?? []) {
if (!skin.tone) continue;
emojis.set(skin.hexcode, skin.emoji);
}
}
return emojis;
})();
describe('native emoji shortcodes', () => {
const { emojiList, emojisByCategory } = getEmojiData();
const unicodeOf = (shortcode: string) => emojiList[`:${shortcode}:`]?.unicode;
// Shortcodes both presets define, pointing at different emoji. Reaction keys and message
// shortcodes stored before the native emoji migration — and everything the mobile clients
// send today — use the joypixels/emojione meaning, so these must not follow emojibase.
describe.each([
['up', '🆙', 'arrow_up_small', '🔼'],
['cat', '🐱', 'cat2', '🐈️'],
['dog', '🐶', 'dog2', '🐕️'],
['o', '⭕️', 'o2', '🅾️'],
['point_up', '☝️', 'point_up_2', '👆️'],
['sunglasses', '😎', 'dark_sunglasses', '🕶️'],
['pencil', '📝', 'pencil2', '✏️'],
['train', '🚋', 'train2', '🚆'],
['whale', '🐳', 'whale2', '🐋'],
['umbrella', '☔️', 'umbrella2', '☂️'],
['computer', '💻️', 'desktop_computer', '🖥️'],
['satellite', '📡', 'satellite_orbital', '🛰️'],
['tiger', '🐯', 'tiger2', '🐅'],
['no', '🇳🇴', 'thumbsdown', '👎️'],
['om', '🇴🇲', 'om_symbol', '🕉️'],
['sunny', '☀️', 'white_sun_small_cloud', '🌤️'],
['japan', '🗾', 'flag_jp', '🇯🇵'],
])('contested shortcode :%s:', (contested, expected, displacedShortcode, displaced) => {
it(`resolves to ${expected}, not ${displaced}`, () => {
expect(unicodeOf(contested)).toBe(expected);
});
it(`leaves ${displaced} reachable as :${displacedShortcode}:`, () => {
expect(unicodeOf(displacedShortcode)).toBe(displaced);
});
});
it('falls back to emojibase for emoji joypixels never named', () => {
// joypixels stopped shipping, so everything added to Unicode since then only has an
// emojibase name — without the fallback these emoji disappear from the picker entirely
expect(unicodeOf('fingerprint')).toBe('🫆');
expect(unicodeOf('face_with_eye_bags')).toBe('🫩');
expect(unicodeOf('leafless_tree')).toBe('🪾');
expect(unicodeOf('root_vegetable')).toBe('🫜');
});
it('only backfills legacy shortcodes the presets do not provide', () => {
// the map is hand-generated; if a preset already names a shortcode, the entry is dead weight
// and, worse, hides which name is authoritative
const redundant = Object.keys(legacyEmojioneMap).filter((shortcode) => emojiList[`:${shortcode}:`]);
expect(redundant).toEqual([]);
});
it('resolves the legacy shortcodes that survived', () => {
expect(shortnameToUnicode(':bride_with_veil:')).toBe('👰');
expect(shortnameToUnicode(':bride_with_veil_tone3:')).toBe('👰🏽');
expect(shortnameToUnicode(':digit_zero:')).toBe('0️⃣');
expect(shortnameToUnicode(':tone3:')).toBe('🏽');
});
it('keeps joypixels aliases of contested emoji working', () => {
expect(unicodeOf('point_up_2')).toBe('👆️');
expect(unicodeOf('cat_face')).toBe('🐱');
expect(unicodeOf('tram_car')).toBe('🚋');
});
it('applies the same precedence to skin tone variants', () => {
expect(unicodeOf('point_up_tone1')).toBe('☝🏻');
expect(unicodeOf('point_up_2_tone1')).toBe('👆🏻');
});
it('resolves contested shortcodes to unicode for shortnameToUnicode consumers', () => {
expect(shortnameToUnicode(':up:')).toBe('🆙');
expect(shortnameToUnicode(':cat:')).toBe('🐱');
expect(shortnameToUnicode(':o:')).toBe('⭕️');
});
it('gives joypixels precedence for every shortcode the two presets contest', () => {
const contested: string[] = [];
for (const [hexcode, emoji] of emittedEmojis) {
for (const shortcode of asArray((joypixelsShortcodes as Record<string, string | string[]>)[hexcode])) {
const entry = emojiList[`:${shortcode}:`];
if (entry && entry.unicode !== emoji) {
contested.push(`:${shortcode}: is ${entry.unicode}, joypixels says ${emoji}`);
}
}
}
expect(contested).toEqual([]);
});
it('never strands an emoji without a shortcode', () => {
const stranded: string[] = [];
const reachable = new Set(Object.values(emojiList).map((entry) => entry.unicode));
for (const [hexcode, emoji] of emittedEmojis) {
const hadShortcode =
asArray((emojibaseShortcodes as Record<string, string | string[]>)[hexcode]).length > 0 ||
asArray((joypixelsShortcodes as Record<string, string | string[]>)[hexcode]).length > 0;
if (hadShortcode && !reachable.has(emoji)) {
stranded.push(`${emoji} (${hexcode})`);
}
}
expect(stranded).toEqual([]);
});
it('keeps every picker entry resolvable', () => {
const unresolvable = Object.values(emojisByCategory)
.flat()
.filter((shortcode) => !emojiList[`:${shortcode}:`]);
expect(unresolvable).toEqual([]);
});
it('keeps every picker entry pointing at its own emoji', () => {
// a picker entry whose name resolves to a *different* emoji would insert the wrong emoji
const misdirected = Object.values(emojisByCategory)
.flat()
.filter((shortcode) => emojiList[`:${shortcode}:`]?.name !== shortcode);
expect(misdirected).toEqual([]);
});
it('registers the primary shortcode before any of its aliases', () => {
// picker search and unicodeToShortcodeMap both take the first registered name for an entry,
// so an alias registered ahead of the primary surfaces the wrong name to users
const firstSeen = new Map<EmojiEntry, string>();
for (const [shortcode, entry] of Object.entries(emojiList)) {
if (!firstSeen.has(entry)) firstSeen.set(entry, shortcode);
}
const outOfOrder = Object.values(emojisByCategory)
.flat()
.filter((shortcode) => firstSeen.get(emojiList[`:${shortcode}:`]) !== `:${shortcode}:`);
expect(outOfOrder).toEqual([]);
});
it('never lists a shortname that resolves to another emoji', () => {
// emoji picker search reads `shortnames`, so a stale alias surfaces the wrong result
const stale: string[] = [];
for (const entry of Object.values(emojiList)) {
for (const shortname of entry.shortnames) {
if (emojiList[shortname] !== entry) {
stale.push(`${entry.unicode} lists ${shortname}, which resolves to ${emojiList[shortname]?.unicode}`);
}
}
}
expect(stale).toEqual([]);
});
});

View File

@@ -0,0 +1,152 @@
import data from 'emojibase-data/en/data.json';
import emojibaseShortcodes from 'emojibase-data/en/shortcodes/emojibase.json';
import joypixelsShortcodes from 'emojibase-data/en/shortcodes/joypixels.json';
// Map emojibase group numbers to our category keys
const groupToCategory: Record<number, string> = {
0: 'people', // smileys & emotion
1: 'people', // people & body
2: 'people', // component - skin tones etc (we skip these)
3: 'nature', // animals & nature
4: 'food', // food & drink
5: 'travel', // travel & places
6: 'activity', // activities
7: 'objects', // objects
8: 'symbols', // symbols
9: 'flags', // flags
};
export type EmojiEntry = {
name: string;
uc_base: string;
uc_output: string;
uc_match: string;
uc_greedy: string;
shortnames: string[];
category: string;
emojiPackage: string;
unicode: string;
};
function hexFromEmoji(emoji: string): string {
return [...emoji].map((cp) => cp.codePointAt(0)!.toString(16)).join('-');
}
function isRegionalIndicator(hexcode: string): boolean {
if (hexcode.includes('-')) return false;
const cp = parseInt(hexcode, 16);
return cp >= 0x1f1e6 && cp <= 0x1f1ff;
}
function getShortcodes(hexcode: string): string[] {
const entry =
(joypixelsShortcodes as Record<string, string | string[]>)[hexcode] ??
(emojibaseShortcodes as Record<string, string | string[]>)[hexcode];
if (!entry) return [];
return Array.isArray(entry) ? entry : [entry];
}
function buildEmojiData() {
const emojiList: Record<string, EmojiEntry> = {};
const emojisByCategory: Record<string, string[]> = {
people: [],
nature: [],
food: [],
activity: [],
travel: [],
objects: [],
symbols: [],
flags: [],
};
const toneList: Record<string, number> = {};
const bareAliases: [string, string][] = [];
for (const emojiData of data) {
// Skip component group (skin tones, hair styles)
if (emojiData.group === 2) continue;
const isRegional = isRegionalIndicator(emojiData.hexcode);
const category = groupToCategory[emojiData.group ?? -1] ?? (isRegional ? 'flags' : undefined);
if (!category) continue;
const codes = getShortcodes(emojiData.hexcode);
if (codes.length === 0) continue;
const primaryShortcode = codes[0];
const altShortcodes = codes.slice(1).map((s) => `:${s}:`);
const hex = hexFromEmoji(emojiData.emoji);
const bare = emojiData.emoji.replace(/\uFE0F/g, '');
const entry: EmojiEntry = {
name: primaryShortcode,
uc_base: hex,
uc_output: hex,
uc_match: hex,
uc_greedy: hex,
shortnames: altShortcodes,
category,
emojiPackage: 'native',
unicode: emojiData.emoji,
};
const key = `:${primaryShortcode}:`;
emojiList[key] = entry;
if (emojiData.type === 1 && bare !== emojiData.emoji) {
bareAliases.push([bare, key]);
}
// Only add to category if it's NOT a skin tone variant
if (!emojiData.tone && !isRegional) {
emojisByCategory[category].push(primaryShortcode);
}
// Register alternate shortcodes
for (const alt of altShortcodes) {
emojiList[alt] = entry;
}
// Track skin tone support
if (emojiData.skins && emojiData.skins.length > 0) {
toneList[primaryShortcode] = 1;
// Register skin tone variants
for (const skin of emojiData.skins) {
if (!skin.tone) continue;
const tones = Array.isArray(skin.tone) ? skin.tone : [skin.tone];
const skinHex = hexFromEmoji(skin.emoji);
const skinEntry: EmojiEntry = {
name: primaryShortcode,
uc_base: skinHex,
uc_output: skinHex,
uc_match: skinHex,
uc_greedy: skinHex,
shortnames: [],
category,
emojiPackage: 'native',
unicode: skin.emoji,
};
const toneShortcodes = getShortcodes(skin.hexcode);
const fallbackShortcode = `${primaryShortcode}_tone${tones.join('-')}`;
for (const code of [...toneShortcodes, fallbackShortcode]) {
emojiList[`:${code}:`] = skinEntry;
}
}
}
}
return { emojiList, emojisByCategory, toneList, bareAliases };
}
// Build once and cache
let _cached: ReturnType<typeof buildEmojiData> | null = null;
export function getEmojiData() {
if (!_cached) {
_cached = buildEmojiData();
}
return _cached;
}

View File

@@ -0,0 +1,95 @@
import { getEmojiConfig } from './getEmojiConfig';
import type { EmojiPackages } from '../../emoji/lib/rocketchat';
const buildEmojiPackages = (ascii: boolean): EmojiPackages =>
({
packages: { native: { ascii } },
list: {},
}) as unknown as EmojiPackages;
describe('native emoji render', () => {
it('converts ascii emoticons when the ascii flag is enabled', () => {
const { render } = getEmojiConfig(buildEmojiPackages(true));
expect(render('hello :)')).toBe('hello <span class="emoji" title=":)">🙂</span>');
});
it('converts HTML-escaped ascii emoticons', () => {
const { render } = getEmojiConfig(buildEmojiPackages(true));
expect(render('hello &gt;:(')).toBe('hello <span class="emoji" title="&gt;:(">😠</span>');
expect(render('hello &lt;3')).toBe('hello <span class="emoji" title="&lt;3">❤</span>');
});
it('keeps ascii emoticons literal when the ascii flag is disabled', () => {
const { render } = getEmojiConfig(buildEmojiPackages(false));
expect(render('hello :)')).toBe('hello :)');
});
it('converts ascii emoticons adjacent to HTML tags', () => {
const { render } = getEmojiConfig(buildEmojiPackages(true));
expect(render('<p>hello :D</p>')).toBe('<p>hello <span class="emoji" title=":D">😄</span></p>');
expect(render('<p>:)</p>')).toBe('<p><span class="emoji" title=":)">🙂</span></p>');
});
it('does not convert ascii sequences inside words or URLs', () => {
const { render } = getEmojiConfig(buildEmojiPackages(true));
expect(render('https://rocket.chat')).toBe('https://rocket.chat');
expect(render('hello:)')).toBe('hello:)');
});
it('does not convert ascii sequences inside HTML tags', () => {
const { render } = getEmojiConfig(buildEmojiPackages(true));
expect(render('<a href="https://rocket.chat">rocket</a>')).toBe('<a href="https://rocket.chat">rocket</a>');
});
it('still renders shortcodes when the ascii flag is enabled', () => {
const { render } = getEmojiConfig(buildEmojiPackages(true));
expect(render(':smiley:')).toContain('<span class="emoji" title=":smiley:">');
});
it('renders bare (non-VS16) emoji-default characters', () => {
const { render } = getEmojiConfig(buildEmojiPackages(false));
expect(render('🐕')).toBe('<span class="emoji" title=":dog2:">🐕</span>');
expect(render('🐶')).toBe('<span class="emoji" title=":dog:">🐶</span>');
expect(render('⭐')).toBe('<span class="emoji" title=":star:">⭐</span>');
expect(render('👍')).toBe('<span class="emoji" title=":thumbsup:">👍</span>');
});
it('resolves the VS16-qualified form to the same shortcode', () => {
const { render } = getEmojiConfig(buildEmojiPackages(false));
expect(render('🐕\u{FE0F}')).toBe('<span class="emoji" title=":dog2:">🐕\u{FE0F}</span>');
expect(render('⭐\u{FE0F}')).toBe('<span class="emoji" title=":star:">⭐\u{FE0F}</span>');
});
it('only treats text-default symbols as emoji when VS16 is present', () => {
const { render } = getEmojiConfig(buildEmojiPackages(false));
expect(render('©')).toBe('©');
expect(render('™')).toBe('™');
expect(render('©\u{FE0F}')).toBe('<span class="emoji" title=":copyright:">©\u{FE0F}</span>');
});
it('renders shortcodes contested with emojibase using their emojione meaning', () => {
const { render } = getEmojiConfig(buildEmojiPackages(false));
expect(render(':up:')).toBe('<span class="emoji" title=":up:">🆙</span>');
expect(render(':arrow_up_small:')).toBe('<span class="emoji" title=":arrow_up_small:">🔼</span>');
expect(render(':cat:')).toBe('<span class="emoji" title=":cat:">🐱</span>');
expect(render(':cat2:')).toBe('<span class="emoji" title=":cat2:">🐈️</span>');
});
it('renders picker entries for contested shortcodes', () => {
const { renderPicker } = getEmojiConfig(buildEmojiPackages(false));
expect(renderPicker(':up:')).toBe('<span class="emoji" title=":up:">🆙</span>');
expect(renderPicker(':cat2:')).toBe('<span class="emoji" title=":cat2:">🐈️</span>');
});
});

View File

@@ -0,0 +1,147 @@
import { asciiList } from '@rocket.chat/message-parser';
import { escapeHTML, escapeRegExp, unescapeHTML } from '@rocket.chat/tools';
import type { EmojiEntry } from './generateEmojiData';
import { getEmojiData } from './generateEmojiData';
import { legacyEmojioneMap } from './legacyEmojioneMap';
import { shortnameToUnicode } from './shortnameToUnicode';
import type { EmojiPackages } from '../../emoji/lib/rocketchat';
const emojiCategories = [
{ key: 'people', i18n: 'Smileys_and_People' },
{ key: 'nature', i18n: 'Animals_and_Nature' },
{ key: 'food', i18n: 'Food_and_Drink' },
{ key: 'activity', i18n: 'Activity' },
{ key: 'travel', i18n: 'Travel_and_Places' },
{ key: 'objects', i18n: 'Objects' },
{ key: 'symbols', i18n: 'Symbols' },
{ key: 'flags', i18n: 'Flags' },
];
// Build a reverse mapping from unicode emoji to shortcode for detection
let unicodeToShortcodeMap: Map<string, string> | null = null;
let emojiRegex: RegExp | null = null;
function getUnicodeToShortcodeMap(): Map<string, string> {
if (!unicodeToShortcodeMap) {
const { emojiList, bareAliases } = getEmojiData();
unicodeToShortcodeMap = new Map();
for (const [shortcode, entry] of Object.entries(emojiList)) {
const emojiEntry = entry;
if (emojiEntry.unicode && !unicodeToShortcodeMap.has(emojiEntry.unicode)) {
unicodeToShortcodeMap.set(emojiEntry.unicode, shortcode);
}
}
for (const [bare, shortcode] of bareAliases) {
if (!unicodeToShortcodeMap.has(bare)) {
unicodeToShortcodeMap.set(bare, shortcode);
}
}
}
return unicodeToShortcodeMap;
}
function getEmojiRegex(): RegExp {
if (!emojiRegex) {
const unicodeMap = getUnicodeToShortcodeMap();
// Sort emojis by length (longest first) to match multi-codepoint emojis correctly
const unicodeEmojis = [...unicodeMap.keys()].sort((a, b) => b.length - a.length);
const unicodePattern = unicodeEmojis.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
// Combine shortcode and unicode emoji patterns
emojiRegex = new RegExp(`(:([a-zA-Z0-9_+-]+):)|(${unicodePattern})`, 'g');
}
return emojiRegex;
}
// HTML-escaped variants are needed because the renderer receives escaped HTML (e.g. `>:(` arrives as `&gt;:(`)
const asciiPattern = [...new Set(Object.keys(asciiList).flatMap((ascii) => [escapeHTML(ascii), ascii]))]
.sort((a, b) => b.length - a.length)
.map(escapeRegExp)
.join('|');
// `>` and `<` count as boundaries so emoticons touching tags still convert (e.g. `<p>hello :D</p>`)
const asciiRegex = new RegExp(
`<object[^>]*>.*?</object>|<span[^>]*>.*?</span>|<(?:object|embed|svg|img|div|span|p|a)[^>]*>|(?<=^|\\s|>)(${asciiPattern})(?=\\s|$|[!,.?<])`,
'g',
);
function renderAsciiEmoji(text: string): string {
return text.replace(asciiRegex, (entire, ascii) => {
if (ascii === undefined) {
return entire;
}
const unescaped = unescapeHTML(ascii);
const unicode = asciiList[unescaped];
if (!unicode) {
return entire;
}
return `<span class="emoji" title="${escapeHTML(unescaped)}">${unicode}</span>`;
});
}
function renderEmoji(text: string, emojiPackages: EmojiPackages): string {
const { emojiList } = getEmojiData();
const unicodeMap = getUnicodeToShortcodeMap();
const pattern = getEmojiRegex();
const rendered = text.replace(pattern, (match, shortcodeGroup, shortcodeName, unicodeGroup) => {
// If it's a shortcode pattern (:emoji:)
if (shortcodeGroup) {
if (emojiPackages.list[`:${shortcodeName}:`]?.emojiPackage === 'emojiCustom') {
return match; // Don't render custom emojis as native
}
const key = `:${shortcodeName}:`;
const emoji = emojiList[key] as EmojiEntry | undefined;
if (emoji?.unicode) {
return `<span class="emoji" title="${shortcodeGroup}">${emoji.unicode}</span>`;
}
// Fallback to legacy emojione shortcodes for backward compatibility
const legacy = legacyEmojioneMap[shortcodeName];
if (legacy) {
return `<span class="emoji" title="${shortcodeGroup}">${legacy}</span>`;
}
return match;
}
// If it's a unicode emoji character (unicodeGroup will be defined)
if (unicodeGroup) {
const shortcode = unicodeMap.get(unicodeGroup);
if (shortcode) {
return `<span class="emoji" title="${shortcode}">${unicodeGroup}</span>`;
}
}
return match;
});
return emojiPackages.packages.native?.ascii ? renderAsciiEmoji(rendered) : rendered;
}
function renderPicker(emojiToRender: string): string | undefined {
const { emojiList } = getEmojiData();
const emoji = emojiList[emojiToRender] as EmojiEntry | undefined;
if (!emoji?.unicode) return undefined;
return `<span class="emoji" title="${emojiToRender}">${emoji.unicode}</span>`;
}
export const getEmojiConfig = (emojiPackages: EmojiPackages) => {
const { emojiList, emojisByCategory, toneList } = getEmojiData();
return {
emojiList,
emojisByCategory,
emojiCategories,
toneList,
render: (text: string) => renderEmoji(text, emojiPackages),
renderPicker,
sprites: false,
shortnameToUnicode,
};
};

View File

@@ -0,0 +1,34 @@
// Auto-generated legacy emojione shortcode mapping
// These shortcodes existed in emojione but are absent from the joypixels preset that
// `generateEmojiData` reads, so they are backfilled here to keep old stored messages and
// reactions resolving. `digit_*`, `pound_symbol` and `asterisk_symbol` were themselves
// hand-patched into emojione by Rocket.Chat, and `tone1`-`tone5` name skin-tone components,
// which are not emoji in their own right.
const legacyEmojioneMapData: Record<string, string> = {
bride_with_veil_tone1: '👰🏻',
bride_with_veil_tone2: '👰🏼',
bride_with_veil_tone3: '👰🏽',
bride_with_veil_tone4: '👰🏾',
bride_with_veil_tone5: '👰🏿',
bride_with_veil: '👰',
digit_zero: '0️⃣',
digit_one: '1️⃣',
digit_two: '2️⃣',
digit_three: '3️⃣',
digit_four: '4️⃣',
digit_five: '5️⃣',
digit_six: '6️⃣',
digit_seven: '7️⃣',
digit_eight: '8️⃣',
digit_nine: '9️⃣',
pound_symbol: '#️⃣',
asterisk_symbol: '*️⃣',
tone1: '🏻',
tone2: '🏼',
tone3: '🏽',
tone4: '🏾',
tone5: '🏿',
};
export const legacyEmojioneMap: Record<string, string> = Object.assign(Object.create(null), legacyEmojioneMapData);

View File

@@ -0,0 +1,19 @@
import type { EmojiEntry } from './generateEmojiData';
import { getEmojiData } from './generateEmojiData';
import { legacyEmojioneMap } from './legacyEmojioneMap';
export function shortnameToUnicode(text: string): string {
const { emojiList } = getEmojiData();
return text.replace(/:([a-zA-Z0-9_+-]+):/g, (match, shortcode) => {
const key = `:${shortcode}:`;
const emoji = emojiList[key] as EmojiEntry | undefined;
if (emoji?.unicode) return emoji.unicode;
// Fallback to legacy emojione shortcodes for backward compatibility
const legacy = legacyEmojioneMap[shortcode];
if (legacy) return legacy;
return match;
});
}