import { useCallback, useEffect, useMemo, useState, } from '../../../../lib/teact/teact'; import { EDITABLE_INPUT_ID } from '../../../../config'; import { IS_SINGLE_COLUMN_LAYOUT } from '../../../../util/environment'; import { MEMO_EMPTY_ARRAY } from '../../../../util/memo'; import { EmojiData, EmojiModule, EmojiRawData, uncompressEmoji, } from '../../../../util/emoji'; import focusEditableElement from '../../../../util/focusEditableElement'; import { buildCollectionByKey, flatten, mapValues, pickTruthy, unique, } from '../../../../util/iteratees'; import useFlag from '../../../../hooks/useFlag'; let emojiDataPromise: Promise; let emojiRawData: EmojiRawData; let emojiData: EmojiData; let RE_NOT_EMOJI_SEARCH: RegExp; const EMOJIS_LIMIT = 36; const FILTER_MIN_LENGTH = 2; try { RE_NOT_EMOJI_SEARCH = new RegExp('[^-+_:\\p{L}\\p{N}]+', 'iu'); } catch (e) { // Support for older versions of firefox RE_NOT_EMOJI_SEARCH = new RegExp('[^-+_:\\d\\wа-яё]+', 'i'); } export default function useEmojiTooltip( isAllowed: boolean, html: string, recentEmojiIds: string[], inputId = EDITABLE_INPUT_ID, onUpdateHtml: (html: string) => void, baseEmojiKeywords?: Record, emojiKeywords?: Record, isDisabled = false, ) { const [isOpen, markIsOpen, unmarkIsOpen] = useFlag(); const [byId, setById] = useState | undefined>(); const [keywords, setKeywords] = useState(); const [byKeyword, setByKeyword] = useState>({}); const [names, setNames] = useState(); const [byName, setByName] = useState>({}); const [shouldForceInsertEmoji, setShouldForceInsertEmoji] = useState(false); const [filteredEmojis, setFilteredEmojis] = useState(MEMO_EMPTY_ARRAY); const recentEmojis = useMemo( () => { if (!byId || !recentEmojiIds.length) { return []; } return Object.values(pickTruthy(byId, recentEmojiIds)); }, [byId, recentEmojiIds], ); // Initialize data on first render. useEffect(() => { if (isDisabled) return; const exec = () => { setById(emojiData.emojis); }; if (emojiData) { exec(); } else { ensureEmojiData() .then(exec); } }, [isDisabled]); useEffect(() => { if (!byId || isDisabled) { return; } const emojis = Object.values(byId); const byNative = buildCollectionByKey(emojis, 'native'); const baseEmojisByKeyword = baseEmojiKeywords ? mapValues(baseEmojiKeywords, (natives) => { return Object.values(pickTruthy(byNative, natives)); }) : {}; const emojisByKeyword = emojiKeywords ? mapValues(emojiKeywords, (natives) => { return Object.values(pickTruthy(byNative, natives)); }) : {}; setByKeyword({ ...baseEmojisByKeyword, ...emojisByKeyword }); setKeywords([...Object.keys(baseEmojisByKeyword), ...Object.keys(emojisByKeyword)]); const emojisByName = emojis.reduce((result, emoji) => { emoji.names.forEach((name) => { if (!result[name]) { result[name] = []; } result[name].push(emoji); }); return result; }, {} as Record); setByName(emojisByName); setNames(Object.keys(emojisByName)); }, [isDisabled, baseEmojiKeywords, byId, emojiKeywords]); useEffect(() => { if (!isAllowed || !html || !byId || !keywords || !keywords.length) { unmarkIsOpen(); return; } const code = html.includes(':') && getEmojiCode(html); if (!code) { setFilteredEmojis(MEMO_EMPTY_ARRAY); unmarkIsOpen(); return; } const forceSend = code.length > 2 && code.endsWith(':'); const filter = code.substr(1, forceSend ? code.length - 2 : undefined); let matched: Emoji[] = []; setShouldForceInsertEmoji(forceSend); if (!filter) { matched = recentEmojis; } else if (filter.length >= FILTER_MIN_LENGTH) { const matchedKeywords = keywords.filter((keyword) => keyword.startsWith(filter)).sort(); matched = matched.concat(flatten(Object.values(pickTruthy(byKeyword, matchedKeywords)))); // Also search by names, which is useful for non-English languages const matchedNames = names.filter((name) => name.startsWith(filter)); matched = matched.concat(flatten(Object.values(pickTruthy(byName, matchedNames)))); matched = unique(matched); } if (matched.length) { if (!forceSend) { markIsOpen(); } setFilteredEmojis(matched.slice(0, EMOJIS_LIMIT)); } else { unmarkIsOpen(); } }, [ byId, byKeyword, keywords, byName, names, html, isAllowed, markIsOpen, recentEmojis, unmarkIsOpen, setShouldForceInsertEmoji, ]); const insertEmoji = useCallback((textEmoji: string, isForce?: boolean) => { const atIndex = html.lastIndexOf(':', isForce ? -1 : undefined); if (atIndex !== -1) { onUpdateHtml(`${html.substr(0, atIndex)}${textEmoji}`); const messageInput = document.getElementById(inputId)!; if (!IS_SINGLE_COLUMN_LAYOUT) { requestAnimationFrame(() => { focusEditableElement(messageInput, true); }); } } unmarkIsOpen(); }, [html, inputId, onUpdateHtml, unmarkIsOpen]); useEffect(() => { if (isOpen && shouldForceInsertEmoji && filteredEmojis.length) { insertEmoji(filteredEmojis[0].native, true); } }, [filteredEmojis, insertEmoji, isOpen, shouldForceInsertEmoji]); return { isEmojiTooltipOpen: isOpen, closeEmojiTooltip: unmarkIsOpen, filteredEmojis, insertEmoji, }; } function getEmojiCode(html: string) { const tempEl = document.createElement('div'); tempEl.innerHTML = html.replace('
', '\n'); const text = tempEl.innerText.replace(/\n$/i, ''); const lastSymbol = text[text.length - 1]; const lastWord = text.split(RE_NOT_EMOJI_SEARCH).pop(); if ( !text.length || RE_NOT_EMOJI_SEARCH.test(lastSymbol) || !lastWord || !lastWord.startsWith(':') ) { return undefined; } return lastWord.toLowerCase(); } async function ensureEmojiData() { if (!emojiDataPromise) { emojiDataPromise = import('emoji-data-ios/emoji-data.json') as unknown as Promise; emojiRawData = (await emojiDataPromise).default; emojiData = uncompressEmoji(emojiRawData); } return emojiDataPromise; }