2021-07-15 03:02:48 +03:00

218 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<EmojiModule>;
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<string, string[]>,
emojiKeywords?: Record<string, string[]>,
isDisabled = false,
) {
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
const [byId, setById] = useState<Record<string, Emoji> | undefined>();
const [keywords, setKeywords] = useState<string[]>();
const [byKeyword, setByKeyword] = useState<Record<string, Emoji[]>>({});
const [names, setNames] = useState<string[]>();
const [byName, setByName] = useState<Record<string, Emoji[]>>({});
const [shouldForceInsertEmoji, setShouldForceInsertEmoji] = useState(false);
const [filteredEmojis, setFilteredEmojis] = useState<Emoji[]>(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<string, Emoji[]>);
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('<br>', '\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<EmojiModule>;
emojiRawData = (await emojiDataPromise).default;
emojiData = uncompressEmoji(emojiRawData);
}
return emojiDataPromise;
}