[Perf] Composer: Optimize message typing

This commit is contained in:
Alexander Zinchuk 2022-01-21 17:29:29 +01:00
parent fbda8e8094
commit fc0365d5b9
8 changed files with 78 additions and 38 deletions

View File

@ -59,7 +59,7 @@ const SettingsNotifications: FC<OwnProps & StateProps> = ({
loadNotificationSettings(); loadNotificationSettings();
}, [loadNotificationSettings]); }, [loadNotificationSettings]);
const runDebounced = useDebounce(500, false, true); const runDebounced = useDebounce(500, true);
const handleSettingsChange = useCallback(( const handleSettingsChange = useCallback((
e: ChangeEvent<HTMLInputElement>, e: ChangeEvent<HTMLInputElement>,

View File

@ -104,9 +104,9 @@ const MediaViewerSlides: FC<OwnProps> = ({
forceUpdate(); forceUpdate();
}, [forceUpdate]); }, [forceUpdate]);
const debounceSetMessage = useDebounce(DEBOUNCE_MESSAGE, false); const debounceSetMessage = useDebounce(DEBOUNCE_MESSAGE, true);
const debounceSwipe = useDebounce(DEBOUNCE_SWIPE, false); const debounceSwipe = useDebounce(DEBOUNCE_SWIPE, true);
const debounceActive = useDebounce(DEBOUNCE_ACTIVE, false); const debounceActive = useDebounce(DEBOUNCE_ACTIVE, true);
const handleToggleFooterVisibility = useCallback(() => { const handleToggleFooterVisibility = useCallback(() => {
if (IS_TOUCH_ENV && (isPhoto || isGif) && hasFooter) { if (IS_TOUCH_ENV && (isPhoto || isGif) && hasFooter) {

View File

@ -1,6 +1,4 @@
import React, { import React, { FC, memo, useEffect } from '../../../lib/teact/teact';
FC, memo, useEffect, useMemo,
} from '../../../lib/teact/teact';
import { getDispatch, withGlobal } from '../../../lib/teact/teactn'; import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
import { ApiMessage, ApiMessageEntityTypes, ApiWebPage } from '../../../api/types'; import { ApiMessage, ApiMessageEntityTypes, ApiWebPage } from '../../../api/types';
@ -12,6 +10,7 @@ import parseMessageInput from '../../../util/parseMessageInput';
import useOnChange from '../../../hooks/useOnChange'; import useOnChange from '../../../hooks/useOnChange';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useDebouncedMemo from '../../../hooks/useDebouncedMemo';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import WebPage from '../message/WebPage'; import WebPage from '../message/WebPage';
@ -32,6 +31,7 @@ type StateProps = {
theme: ISettings['theme']; theme: ISettings['theme'];
}; };
const DEBOUNCE_MS = 300;
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i'); const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
const WebPagePreview: FC<OwnProps & StateProps> = ({ const WebPagePreview: FC<OwnProps & StateProps> = ({
@ -49,7 +49,7 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
toggleMessageWebPage, toggleMessageWebPage,
} = getDispatch(); } = getDispatch();
const link = useMemo(() => { const link = useDebouncedMemo(() => {
const { text, entities } = parseMessageInput(messageText); const { text, entities } = parseMessageInput(messageText);
const linkEntity = entities && entities.find(({ type }) => type === ApiMessageEntityTypes.TextUrl); const linkEntity = entities && entities.find(({ type }) => type === ApiMessageEntityTypes.TextUrl);
@ -63,7 +63,7 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
} }
return undefined; return undefined;
}, [messageText]); }, DEBOUNCE_MS, [messageText]);
useEffect(() => { useEffect(() => {
if (link) { if (link) {

View File

@ -3,25 +3,29 @@ import { getDispatch } from '../../../../lib/teact/teactn';
import { InlineBotSettings } from '../../../../types'; import { InlineBotSettings } from '../../../../types';
import useFlag from '../../../../hooks/useFlag'; import useFlag from '../../../../hooks/useFlag';
import usePrevious from '../../../../hooks/usePrevious'; import usePrevious from '../../../../hooks/usePrevious';
import useDebouncedMemo from '../../../../hooks/useDebouncedMemo';
const tempEl = document.createElement('div'); const DEBOUNCE_MS = 300;
const INLINE_BOT_QUERY_REGEXP = /^@([a-z0-9_]{1,32})[\u00A0\u0020]+(.*)/i; const INLINE_BOT_QUERY_REGEXP = /^@([a-z0-9_]{1,32})[\u00A0\u0020]+(.*)/i;
const HAS_NEW_LINE = /^@([a-z0-9_]{1,32})[\u00A0\u0020]+\n{2,}/i; const HAS_NEW_LINE = /^@([a-z0-9_]{1,32})[\u00A0\u0020]+\n{2,}/i;
const tempEl = document.createElement('div');
export default function useInlineBotTooltip( export default function useInlineBotTooltip(
isAllowed: boolean, isAllowed: boolean,
chatId: string, chatId: string,
html: string, html: string,
inlineBots?: Record<string, false | InlineBotSettings>, inlineBots?: Record<string, false | InlineBotSettings>,
) { ) {
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
const text = getPlainText(html);
const { queryInlineBot, resetInlineBot } = getDispatch(); const { queryInlineBot, resetInlineBot } = getDispatch();
const { username, query, canShowHelp } = parseStartWithUsernameString(text);
const usernameLowered = username.toLowerCase(); const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
const {
username, query, canShowHelp, usernameLowered,
} = useDebouncedMemo(() => parseBotQuery(html), DEBOUNCE_MS, [html]) || {};
const prevQuery = usePrevious(query); const prevQuery = usePrevious(query);
const prevUsername = usePrevious(username); const prevUsername = usePrevious(username);
const inlineBotData = inlineBots?.[usernameLowered]; const inlineBotData = usernameLowered ? inlineBots?.[usernameLowered] : undefined;
const { const {
id: botId, id: botId,
switchPm, switchPm,
@ -63,14 +67,33 @@ export default function useInlineBotTooltip(
return { return {
isOpen, isOpen,
closeTooltip: unmarkIsOpen,
loadMore,
username,
id: botId, id: botId,
isGallery, isGallery,
switchPm, switchPm,
results, results,
closeTooltip: unmarkIsOpen,
help: canShowHelp && help ? `@${username} ${help}` : undefined, help: canShowHelp && help ? `@${username} ${help}` : undefined,
loadMore,
};
}
function parseBotQuery(html: string) {
const text = getPlainText(html);
const result = text.match(INLINE_BOT_QUERY_REGEXP);
if (!result) {
return {
username: '',
query: '',
canShowHelp: false,
usernameLowered: '',
};
}
return {
username: result[1],
query: result[2],
canShowHelp: result[2] === '' && !text.match(HAS_NEW_LINE),
usernameLowered: result[1].toLowerCase(),
}; };
} }
@ -79,16 +102,3 @@ function getPlainText(html: string) {
return tempEl.innerText; return tempEl.innerText;
} }
function parseStartWithUsernameString(text: string) {
const result = text.match(INLINE_BOT_QUERY_REGEXP);
if (!result) {
return { username: '', query: '', canShowHelp: false };
}
return {
username: result[1],
query: result[2],
canShowHelp: result[2] === '' && !text.match(HAS_NEW_LINE),
};
}

View File

@ -13,7 +13,7 @@ export default function useStickyDates() {
// so we will add `position: sticky` only after first scroll. There would be no animation on the first show though. // so we will add `position: sticky` only after first scroll. There would be no animation on the first show though.
const [isScrolled, markIsScrolled] = useFlag(false); const [isScrolled, markIsScrolled] = useFlag(false);
const runDebounced = useDebounce(DEBOUNCE, false); const runDebounced = useDebounce(DEBOUNCE, true);
const updateStickyDates = useCallback((container: HTMLDivElement, hasTools?: boolean) => { const updateStickyDates = useCallback((container: HTMLDivElement, hasTools?: boolean) => {
markIsScrolled(); markIsScrolled();

View File

@ -2,8 +2,8 @@ import { useMemo } from '../lib/teact/teact';
import { debounce } from '../util/schedulers'; import { debounce } from '../util/schedulers';
export default function useDebounce(ms: number, shouldRunFirst?: boolean, shouldRunLast?: boolean) { export default function useDebounce(ms: number, noFirst?: boolean, noLast?: boolean) {
return useMemo(() => { return useMemo(() => {
return debounce((cb) => cb(), ms, shouldRunFirst, shouldRunLast); return debounce((cb) => cb(), ms, !noFirst, !noLast);
}, [ms, shouldRunFirst, shouldRunLast]); }, [ms, noFirst, noLast]);
} }

View File

@ -0,0 +1,28 @@
import { useState } from '../lib/teact/teact';
import useDebounce from './useDebounce';
import useOnChange from './useOnChange';
import useHeavyAnimationCheck from './useHeavyAnimationCheck';
import useFlag from './useFlag';
export default function useDebouncedMemo<R extends any, D extends any[]>(
resolverFn: () => R, ms: number, dependencies: D,
): R | undefined {
const runDebounced = useDebounce(ms, true);
const [value, setValue] = useState<R>();
const [isFrozen, freeze, unfreeze] = useFlag();
useHeavyAnimationCheck(freeze, unfreeze);
useOnChange(() => {
if (isFrozen) {
return;
}
runDebounced(() => {
setValue(resolverFn());
});
}, [...dependencies, isFrozen]);
return value;
}

View File

@ -5,7 +5,9 @@ import useOnChange from './useOnChange';
import useHeavyAnimationCheck from './useHeavyAnimationCheck'; import useHeavyAnimationCheck from './useHeavyAnimationCheck';
import useFlag from './useFlag'; import useFlag from './useFlag';
export default <R extends any, D extends any[]>(resolverFn: () => R, ms: number, dependencies: D) => { export default function useThrottledMemo<R extends any, D extends any[]>(
resolverFn: () => R, ms: number, dependencies: D,
): R | undefined {
const runThrottled = useThrottle(ms, true); const runThrottled = useThrottle(ms, true);
const [value, setValue] = useState<R>(); const [value, setValue] = useState<R>();
const [isFrozen, freeze, unfreeze] = useFlag(); const [isFrozen, freeze, unfreeze] = useFlag();
@ -20,7 +22,7 @@ export default <R extends any, D extends any[]>(resolverFn: () => R, ms: number,
runThrottled(() => { runThrottled(() => {
setValue(resolverFn()); setValue(resolverFn());
}); });
}, dependencies.concat([isFrozen])); }, [...dependencies, isFrozen]);
return value; return value;
}; }