[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 AnyLiteral = Record<string, any>;
|
||||||
type AnyClass = new (...args: any[]) => any;
|
type AnyClass = new (...args: any[]) => any;
|
||||||
type AnyFunction = (...args: any) => any;
|
type AnyFunction = (...args: any[]) => any;
|
||||||
type AnyToVoidFunction = (...args: any) => void;
|
type AnyToVoidFunction = (...args: any[]) => void;
|
||||||
type NoneToVoidFunction = () => void;
|
type NoneToVoidFunction = () => void;
|
||||||
|
|
||||||
type EmojiCategory = {
|
type EmojiCategory = {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
useCallback, useEffect, useMemo, useState,
|
useCallback, useEffect, useState,
|
||||||
} from '../../../../lib/teact/teact';
|
} from '../../../../lib/teact/teact';
|
||||||
|
|
||||||
import { EDITABLE_INPUT_ID } from '../../../../config';
|
import { EDITABLE_INPUT_ID } from '../../../../config';
|
||||||
@ -12,8 +12,16 @@ import focusEditableElement from '../../../../util/focusEditableElement';
|
|||||||
import {
|
import {
|
||||||
buildCollectionByKey, flatten, mapValues, pickTruthy, unique,
|
buildCollectionByKey, flatten, mapValues, pickTruthy, unique,
|
||||||
} from '../../../../util/iteratees';
|
} from '../../../../util/iteratees';
|
||||||
|
import memoized from '../../../../util/memoized';
|
||||||
import useFlag from '../../../../hooks/useFlag';
|
import useFlag from '../../../../hooks/useFlag';
|
||||||
|
|
||||||
|
interface Library {
|
||||||
|
keywords: string[];
|
||||||
|
byKeyword: Record<string, Emoji[]>;
|
||||||
|
names: string[];
|
||||||
|
byName: Record<string, Emoji[]>;
|
||||||
|
}
|
||||||
|
|
||||||
let emojiDataPromise: Promise<EmojiModule>;
|
let emojiDataPromise: Promise<EmojiModule>;
|
||||||
let emojiRawData: EmojiRawData;
|
let emojiRawData: EmojiRawData;
|
||||||
let emojiData: EmojiData;
|
let emojiData: EmojiData;
|
||||||
@ -22,6 +30,10 @@ let RE_EMOJI_SEARCH: RegExp;
|
|||||||
const EMOJIS_LIMIT = 36;
|
const EMOJIS_LIMIT = 36;
|
||||||
const FILTER_MIN_LENGTH = 2;
|
const FILTER_MIN_LENGTH = 2;
|
||||||
|
|
||||||
|
const prepareRecentEmojisMemo = memoized(prepareRecentEmojis);
|
||||||
|
const prepareLibraryMemo = memoized(prepareLibrary);
|
||||||
|
const searchInLibraryMemo = memoized(searchInLibrary);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
RE_EMOJI_SEARCH = new RegExp('(^|\\s):[-+_:\\p{L}\\p{N}]*$', 'gui');
|
RE_EMOJI_SEARCH = new RegExp('(^|\\s):[-+_:\\p{L}\\p{N}]*$', 'gui');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -40,27 +52,10 @@ export default function useEmojiTooltip(
|
|||||||
isDisabled = false,
|
isDisabled = false,
|
||||||
) {
|
) {
|
||||||
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
|
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
|
||||||
|
|
||||||
const [byId, setById] = useState<Record<string, Emoji> | undefined>();
|
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 [shouldForceInsertEmoji, setShouldForceInsertEmoji] = useState(false);
|
||||||
|
|
||||||
const [filteredEmojis, setFilteredEmojis] = useState<Emoji[]>(MEMO_EMPTY_ARRAY);
|
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.
|
// Initialize data on first render.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDisabled) return;
|
if (isDisabled) return;
|
||||||
@ -77,44 +72,7 @@ export default function useEmojiTooltip(
|
|||||||
}, [isDisabled]);
|
}, [isDisabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!byId || isDisabled) {
|
if (!isAllowed || !html || !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();
|
unmarkIsOpen();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -128,34 +86,28 @@ export default function useEmojiTooltip(
|
|||||||
|
|
||||||
const forceSend = code.length > 2 && code.endsWith(':');
|
const forceSend = code.length > 2 && code.endsWith(':');
|
||||||
const filter = code.substr(1, forceSend ? code.length - 2 : undefined);
|
const filter = code.substr(1, forceSend ? code.length - 2 : undefined);
|
||||||
let matched: Emoji[] = [];
|
let matched: Emoji[] = MEMO_EMPTY_ARRAY;
|
||||||
|
|
||||||
setShouldForceInsertEmoji(forceSend);
|
setShouldForceInsertEmoji(forceSend);
|
||||||
|
|
||||||
if (!filter) {
|
if (!filter) {
|
||||||
matched = recentEmojis;
|
matched = prepareRecentEmojisMemo(byId, recentEmojiIds, EMOJIS_LIMIT);
|
||||||
} else if (filter.length >= FILTER_MIN_LENGTH) {
|
} else if (filter.length >= FILTER_MIN_LENGTH) {
|
||||||
const matchedKeywords = keywords.filter((keyword) => keyword.startsWith(filter)).sort();
|
const library = prepareLibraryMemo(byId, baseEmojiKeywords, emojiKeywords);
|
||||||
matched = matched.concat(flatten(Object.values(pickTruthy(byKeyword, matchedKeywords))));
|
matched = searchInLibraryMemo(library, filter, EMOJIS_LIMIT);
|
||||||
|
|
||||||
// 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 (matched.length) {
|
||||||
if (!forceSend) {
|
if (!forceSend) {
|
||||||
markIsOpen();
|
markIsOpen();
|
||||||
}
|
}
|
||||||
setFilteredEmojis(matched.slice(0, EMOJIS_LIMIT));
|
setFilteredEmojis(matched);
|
||||||
} else {
|
} else {
|
||||||
unmarkIsOpen();
|
unmarkIsOpen();
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
byId, byKeyword, keywords, byName, names, html, isAllowed, markIsOpen,
|
byId, html, isAllowed, markIsOpen, recentEmojiIds, unmarkIsOpen, setShouldForceInsertEmoji,
|
||||||
recentEmojis, unmarkIsOpen, setShouldForceInsertEmoji,
|
isDisabled, baseEmojiKeywords, emojiKeywords,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const insertEmoji = useCallback((textEmoji: string, isForce?: boolean) => {
|
const insertEmoji = useCallback((textEmoji: string, isForce?: boolean) => {
|
||||||
@ -201,3 +153,74 @@ async function ensureEmojiData() {
|
|||||||
|
|
||||||
return emojiDataPromise;
|
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);
|
clearTimeout(waitingTimeout);
|
||||||
waitingTimeout = undefined;
|
waitingTimeout = undefined;
|
||||||
} else if (shouldRunFirst) {
|
} else if (shouldRunFirst) {
|
||||||
// @ts-ignore
|
|
||||||
fn(...args);
|
fn(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-globals
|
// eslint-disable-next-line no-restricted-globals
|
||||||
waitingTimeout = self.setTimeout(() => {
|
waitingTimeout = self.setTimeout(() => {
|
||||||
if (shouldRunLast) {
|
if (shouldRunLast) {
|
||||||
// @ts-ignore
|
|
||||||
fn(...args);
|
fn(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +46,6 @@ export function throttle<F extends AnyToVoidFunction>(
|
|||||||
if (!interval) {
|
if (!interval) {
|
||||||
if (shouldRunFirst) {
|
if (shouldRunFirst) {
|
||||||
isPending = false;
|
isPending = false;
|
||||||
// @ts-ignore
|
|
||||||
fn(...args);
|
fn(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +59,6 @@ export function throttle<F extends AnyToVoidFunction>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
isPending = false;
|
isPending = false;
|
||||||
// @ts-ignore
|
|
||||||
fn(...args);
|
fn(...args);
|
||||||
}, ms);
|
}, ms);
|
||||||
}
|
}
|
||||||
@ -97,7 +93,6 @@ export function throttleWith<F extends AnyToVoidFunction>(schedulerFn: Scheduler
|
|||||||
|
|
||||||
schedulerFn(() => {
|
schedulerFn(() => {
|
||||||
waiting = false;
|
waiting = false;
|
||||||
// @ts-ignore
|
|
||||||
fn(...args);
|
fn(...args);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user