[Perf] Composer: Avoid duplicated calculations for various instances of useEmojiTooltip
This commit is contained in:
parent
6baa636ef7
commit
49c0794789
4
src/@types/global.d.ts
vendored
4
src/@types/global.d.ts
vendored
@ -22,8 +22,8 @@ declare namespace React {
|
||||
|
||||
type AnyLiteral = Record<string, any>;
|
||||
type AnyClass = new (...args: any[]) => any;
|
||||
type AnyFunction = (...args: any) => any;
|
||||
type AnyToVoidFunction = (...args: any) => void;
|
||||
type AnyFunction = (...args: any[]) => any;
|
||||
type AnyToVoidFunction = (...args: any[]) => void;
|
||||
type NoneToVoidFunction = () => void;
|
||||
|
||||
type EmojiCategory = {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {
|
||||
useCallback, useEffect, useMemo, useState,
|
||||
useCallback, useEffect, useState,
|
||||
} from '../../../../lib/teact/teact';
|
||||
|
||||
import { EDITABLE_INPUT_ID } from '../../../../config';
|
||||
@ -12,8 +12,16 @@ import focusEditableElement from '../../../../util/focusEditableElement';
|
||||
import {
|
||||
buildCollectionByKey, flatten, mapValues, pickTruthy, unique,
|
||||
} from '../../../../util/iteratees';
|
||||
import memoized from '../../../../util/memoized';
|
||||
import useFlag from '../../../../hooks/useFlag';
|
||||
|
||||
interface Library {
|
||||
keywords: string[];
|
||||
byKeyword: Record<string, Emoji[]>;
|
||||
names: string[];
|
||||
byName: Record<string, Emoji[]>;
|
||||
}
|
||||
|
||||
let emojiDataPromise: Promise<EmojiModule>;
|
||||
let emojiRawData: EmojiRawData;
|
||||
let emojiData: EmojiData;
|
||||
@ -22,6 +30,10 @@ let RE_EMOJI_SEARCH: RegExp;
|
||||
const EMOJIS_LIMIT = 36;
|
||||
const FILTER_MIN_LENGTH = 2;
|
||||
|
||||
const prepareRecentEmojisMemo = memoized(prepareRecentEmojis);
|
||||
const prepareLibraryMemo = memoized(prepareLibrary);
|
||||
const searchInLibraryMemo = memoized(searchInLibrary);
|
||||
|
||||
try {
|
||||
RE_EMOJI_SEARCH = new RegExp('(^|\\s):[-+_:\\p{L}\\p{N}]*$', 'gui');
|
||||
} catch (e) {
|
||||
@ -40,27 +52,10 @@ export default function useEmojiTooltip(
|
||||
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;
|
||||
@ -77,44 +72,7 @@ export default function useEmojiTooltip(
|
||||
}, [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) {
|
||||
if (!isAllowed || !html || !byId || isDisabled) {
|
||||
unmarkIsOpen();
|
||||
return;
|
||||
}
|
||||
@ -128,34 +86,28 @@ export default function useEmojiTooltip(
|
||||
|
||||
const forceSend = code.length > 2 && code.endsWith(':');
|
||||
const filter = code.substr(1, forceSend ? code.length - 2 : undefined);
|
||||
let matched: Emoji[] = [];
|
||||
let matched: Emoji[] = MEMO_EMPTY_ARRAY;
|
||||
|
||||
setShouldForceInsertEmoji(forceSend);
|
||||
|
||||
if (!filter) {
|
||||
matched = recentEmojis;
|
||||
matched = prepareRecentEmojisMemo(byId, recentEmojiIds, EMOJIS_LIMIT);
|
||||
} 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);
|
||||
const library = prepareLibraryMemo(byId, baseEmojiKeywords, emojiKeywords);
|
||||
matched = searchInLibraryMemo(library, filter, EMOJIS_LIMIT);
|
||||
}
|
||||
|
||||
if (matched.length) {
|
||||
if (!forceSend) {
|
||||
markIsOpen();
|
||||
}
|
||||
setFilteredEmojis(matched.slice(0, EMOJIS_LIMIT));
|
||||
setFilteredEmojis(matched);
|
||||
} else {
|
||||
unmarkIsOpen();
|
||||
}
|
||||
}, [
|
||||
byId, byKeyword, keywords, byName, names, html, isAllowed, markIsOpen,
|
||||
recentEmojis, unmarkIsOpen, setShouldForceInsertEmoji,
|
||||
byId, html, isAllowed, markIsOpen, recentEmojiIds, unmarkIsOpen, setShouldForceInsertEmoji,
|
||||
isDisabled, baseEmojiKeywords, emojiKeywords,
|
||||
]);
|
||||
|
||||
const insertEmoji = useCallback((textEmoji: string, isForce?: boolean) => {
|
||||
@ -201,3 +153,74 @@ async function ensureEmojiData() {
|
||||
|
||||
return emojiDataPromise;
|
||||
}
|
||||
|
||||
function prepareRecentEmojis(byId: Record<string, Emoji>, recentEmojiIds: string[], limit: number) {
|
||||
if (!byId || !recentEmojiIds.length) {
|
||||
return MEMO_EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
return Object.values(pickTruthy(byId, recentEmojiIds)).slice(0, limit);
|
||||
}
|
||||
|
||||
function prepareLibrary(
|
||||
byId: Record<string, Emoji>,
|
||||
baseEmojiKeywords?: Record<string, string[]>,
|
||||
emojiKeywords?: Record<string, string[]>,
|
||||
): Library {
|
||||
const emojis = Object.values(byId);
|
||||
|
||||
const byNative = buildCollectionByKey<Emoji>(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));
|
||||
})
|
||||
: {};
|
||||
|
||||
const byKeyword = { ...baseEmojisByKeyword, ...emojisByKeyword };
|
||||
const keywords = ([] as string[]).concat(Object.keys(baseEmojisByKeyword), Object.keys(emojisByKeyword));
|
||||
|
||||
const byName = emojis.reduce((result, emoji) => {
|
||||
emoji.names.forEach((name) => {
|
||||
if (!result[name]) {
|
||||
result[name] = [];
|
||||
}
|
||||
|
||||
result[name].push(emoji);
|
||||
});
|
||||
|
||||
return result;
|
||||
}, {} as Record<string, Emoji[]>);
|
||||
|
||||
const names = Object.keys(byName);
|
||||
|
||||
return {
|
||||
byKeyword,
|
||||
keywords,
|
||||
byName,
|
||||
names,
|
||||
};
|
||||
}
|
||||
|
||||
function searchInLibrary(library: Library, filter: string, limit: number) {
|
||||
const {
|
||||
byKeyword, keywords, byName, names,
|
||||
} = library;
|
||||
|
||||
let matched: Emoji[] = MEMO_EMPTY_ARRAY;
|
||||
|
||||
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);
|
||||
|
||||
return matched.slice(0, limit);
|
||||
}
|
||||
|
||||
19
src/util/memoized.ts
Normal file
19
src/util/memoized.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { areSortedArraysEqual } from './iteratees';
|
||||
|
||||
const cache = new WeakMap<AnyFunction, {
|
||||
lastArgs: any[];
|
||||
lastResult: any;
|
||||
}>();
|
||||
|
||||
export default function memoized<T extends AnyFunction>(fn: T) {
|
||||
return (...args: Parameters<T>): ReturnType<T> => {
|
||||
const cached = cache.get(fn);
|
||||
if (cached && areSortedArraysEqual(cached.lastArgs, args)) {
|
||||
return cached.lastResult;
|
||||
}
|
||||
|
||||
const result = fn(...args);
|
||||
cache.set(fn, { lastArgs: args, lastResult: result });
|
||||
return result;
|
||||
};
|
||||
}
|
||||
@ -16,14 +16,12 @@ export function debounce<F extends AnyToVoidFunction>(
|
||||
clearTimeout(waitingTimeout);
|
||||
waitingTimeout = undefined;
|
||||
} else if (shouldRunFirst) {
|
||||
// @ts-ignore
|
||||
fn(...args);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
waitingTimeout = self.setTimeout(() => {
|
||||
if (shouldRunLast) {
|
||||
// @ts-ignore
|
||||
fn(...args);
|
||||
}
|
||||
|
||||
@ -48,7 +46,6 @@ export function throttle<F extends AnyToVoidFunction>(
|
||||
if (!interval) {
|
||||
if (shouldRunFirst) {
|
||||
isPending = false;
|
||||
// @ts-ignore
|
||||
fn(...args);
|
||||
}
|
||||
|
||||
@ -62,7 +59,6 @@ export function throttle<F extends AnyToVoidFunction>(
|
||||
}
|
||||
|
||||
isPending = false;
|
||||
// @ts-ignore
|
||||
fn(...args);
|
||||
}, ms);
|
||||
}
|
||||
@ -97,7 +93,6 @@ export function throttleWith<F extends AnyToVoidFunction>(schedulerFn: Scheduler
|
||||
|
||||
schedulerFn(() => {
|
||||
waiting = false;
|
||||
// @ts-ignore
|
||||
fn(...args);
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user