import { escapeRegExp } from '@rocket.chat/tools'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import type { EmojiCategory, EmojiItem } from '.'; import { emoji, emojiEmitter } from './lib'; export const CUSTOM_CATEGORY = 'rocket'; export const CUSTOM_CATEGORY_CLASSNAME = 'emojipicker--custom'; const MIXED_TONE_SUFFIX = /_tone([1-5])-[1-5]$/; type RowItem = Array; type RowDivider = { category: string; i18n: TranslationKey }; type LoadMoreItem = { loadMore: true }; export type EmojiPickerItem = RowItem | RowDivider | LoadMoreItem; export type CategoriesIndexes = { key: string; index: number }[]; export const isRowDivider = (item: EmojiPickerItem): item is RowDivider => 'i18n' in item; export const isLoadMore = (item: EmojiPickerItem): item is LoadMoreItem => 'loadMore' in item; export const createEmojiListByCategorySubscription = ( customItemsLimit: number, actualTone: number, recentEmojis: string[], setRecentEmojis: (emojis: string[]) => void, setQuickReactions: () => void, ): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ReturnType] => { let result: ReturnType = [[], []]; updateRecent(recentEmojis); const sub = (cb: () => void) => { result = createPickerEmojis(customItemsLimit, actualTone, recentEmojis, setRecentEmojis); setQuickReactions(); return emojiEmitter.on('updated', () => { result = createPickerEmojis(customItemsLimit, actualTone, recentEmojis, setRecentEmojis); setQuickReactions(); cb(); }); }; return [sub, () => result]; }; export const createPickerEmojis = ( customItemsLimit: number, actualTone: number, recentEmojis: string[], setRecentEmojis: (emojis: string[]) => void, ): [EmojiPickerItem[], CategoriesIndexes] => { const categories = getCategoriesList(); const categoriesIndexes: CategoriesIndexes = []; const mappedCategories = categories.reduce((acc, category) => { categoriesIndexes.push({ key: category.key, index: acc.length }); acc.push({ category: category.key, i18n: category.i18n }); acc.push(...createEmojiList(customItemsLimit, category.key, actualTone, recentEmojis, setRecentEmojis)); return acc; }, []); return [mappedCategories, categoriesIndexes]; }; export const createEmojiList = ( customItemsLimit: number, category: string, actualTone: number | null, recentEmojis: string[], setRecentEmojis: (emojis: string[]) => void, ): (RowItem | LoadMoreItem)[] => { const items: RowItem = []; const emojiPackages = Object.entries(emoji.packages); let count = 0; let limited = false; emojiPackages.forEach(([packageName, emojiPackage]) => { if (!emojiPackage.emojisByCategory?.[category]) { return; } const _total = emojiPackage.emojisByCategory[category].length; const total = category === CUSTOM_CATEGORY ? customItemsLimit - count : _total; for (let i = 0; i < total; i++) { const current = emojiPackage.emojisByCategory[category][i]; const tone = actualTone && actualTone > 0 && emojiPackage.toneList.hasOwnProperty(current) ? `_tone${actualTone}` : ''; const emojiToRender = `:${current}${tone}:`; const actualEmoji = emoji.list[emojiToRender]; if (!actualEmoji) { removeFromRecent(emojiToRender, recentEmojis, setRecentEmojis); return; } // A custom emoji with the same name overrides the native one (including its tones), so skip the native duplicate if (packageName === 'native' && emoji.list[`:${current}:`]?.emojiPackage === 'emojiCustom') { continue; } const actualPackage = actualEmoji.emojiPackage; const image = emoji.packages[actualPackage].renderPicker(emojiToRender); if (!image) { continue; } items.push({ emoji: current, image, category }); count++; } if (_total > total) { limited = true; } }); const rowCount = 9; const rowList: Array = Array.from({ length: Math.ceil(items.length / rowCount) }).map(() => []); for (let i = 0; i < rowList.length; i++) { const row = items.slice(i * rowCount, i * rowCount + rowCount); rowList[i] = row; } if (rowList.length === 0) { rowList.push([]); } if (limited) { rowList.push({ loadMore: true }); } return rowList; }; export const getCategoriesList = () => { let categoriesList: EmojiCategory[] = []; for (const emojiPackage of Object.values(emoji.packages)) { if (emojiPackage.emojiCategories) { if (typeof emojiPackage.categoryIndex !== 'undefined') { categoriesList.splice(emojiPackage.categoryIndex, 0, ...emojiPackage.emojiCategories); } else { categoriesList = categoriesList.concat(emojiPackage.emojiCategories); } } } const rocketPosition = categoriesList.findIndex((category) => category.key === CUSTOM_CATEGORY); const rocketCategory = categoriesList.splice(rocketPosition, 1)[0]; categoriesList.push(rocketCategory); return categoriesList; }; export const getEmojisBySearchTerm = ( searchTerm: string, actualTone: number, recentEmojis: string[], setRecentEmojis: (emojis: string[]) => void, ) => { const emojis = []; const seenEmojis = new Set<(typeof emoji.list)[string]>(); const searchRegExp = new RegExp(escapeRegExp(searchTerm.replace(/:/g, '')), 'i'); for (let current in emoji.list) { if (!emoji.list.hasOwnProperty(current) || !searchRegExp.test(current)) { continue; } const emojiObject = emoji.list[current]; // Skip duplicates (same emoji in different packages) if (seenEmojis.has(emojiObject)) { continue; } const { emojiPackage, shortnames = [], name } = emojiObject; let tone = ''; current = current.replace(/:/g, ''); if (actualTone > 0 && emoji.packages[emojiPackage].toneList.hasOwnProperty(name ?? current)) { tone = `_tone${actualTone}`; } const isNative = emojiPackage === 'native'; if (isNative) { const mixedTones = current.match(MIXED_TONE_SUFFIX); if (mixedTones && !(actualTone > 0 && Number(mixedTones[1]) === actualTone)) { continue; } } const categoryName = isNative ? current.replace(MIXED_TONE_SUFFIX, '') : current; const isCategoryEmoji = Object.values(emoji.packages[emojiPackage].emojisByCategory).some( (contents) => contents.indexOf(categoryName) !== -1, ); if (!isCategoryEmoji && shortnames.length === 0) { continue; } const emojiToRender = `:${current}${tone}:`; const actualEmoji = emoji.list[emojiToRender]; if (!actualEmoji) { removeFromRecent(emojiToRender, recentEmojis, setRecentEmojis); continue; } seenEmojis.add(emojiObject); const actualPackage = actualEmoji.emojiPackage; emojis.push({ emoji: `${current}${tone}`, image: emoji.packages[actualPackage].renderPicker(emojiToRender) }); } return emojis; }; export const removeFromRecent = (emoji: string, recentEmojis: string[], setRecentEmojis?: (emojis: string[]) => void) => { const _emoji = emoji.replace(/(^:|:$)/g, ''); const pos = recentEmojis.indexOf(_emoji); if (pos === -1) { return; } recentEmojis.splice(pos, 1); setRecentEmojis?.(recentEmojis); }; // There's no need to dispatchUpdate here. This helper is called before the list is generated. // This means that the recent list will always be up to date by the time it is used. export const updateRecent = (recentList: string[]) => { const recentPkgList: string[] = emoji.packages.base.emojisByCategory.recent; recentList?.forEach((_emoji) => { !recentPkgList.includes(_emoji) && recentPkgList.push(_emoji); }); }; export const replaceEmojiInRecent = ({ oldEmoji, newEmoji }: { oldEmoji: string; newEmoji: string }) => { const recentPkgList: string[] = emoji.packages.base.emojisByCategory.recent; const pos = recentPkgList.indexOf(oldEmoji); if (pos !== -1) { recentPkgList[pos] = newEmoji; emoji.dispatchUpdate(); } }; const getEmojiRender = (emojiName: string) => { const emojiPackageName = emoji.list[emojiName]?.emojiPackage; const emojiPackage = emoji.packages[emojiPackageName]; return emojiPackage?.render(emojiName); }; export const getFrequentEmoji = (frequentEmoji: string[]) => { return frequentEmoji?.map((frequentEmoji) => { return { emoji: frequentEmoji, image: getEmojiRender(`:${frequentEmoji}:`) }; }); };