[Perf] Middle Column: Various optimizations for opening chat

This commit is contained in:
Alexander Zinchuk 2023-04-15 13:50:57 +02:00
parent 2f63a0fcf0
commit d872273def
13 changed files with 186 additions and 106 deletions

View File

@ -1,6 +1,6 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import React, { import React, {
memo, useCallback, useEffect, useMemo, useRef, useState, memo, useCallback, useEffect, useMemo, useRef,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../global'; import { getActions, getGlobal, withGlobal } from '../../global';
@ -55,17 +55,17 @@ import resetScroll, { patchChromiumScroll } from '../../util/resetScroll';
import fastSmoothScroll, { isAnimatingScroll } from '../../util/fastSmoothScroll'; import fastSmoothScroll, { isAnimatingScroll } from '../../util/fastSmoothScroll';
import renderText from '../common/helpers/renderText'; import renderText from '../common/helpers/renderText';
import { useStateRef } from '../../hooks/useStateRef';
import useSyncEffect from '../../hooks/useSyncEffect'; import useSyncEffect from '../../hooks/useSyncEffect';
import useStickyDates from './hooks/useStickyDates'; import useStickyDates from './hooks/useStickyDates';
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useWindowSize from '../../hooks/useWindowSize';
import useInterval from '../../hooks/useInterval'; import useInterval from '../../hooks/useInterval';
import useNativeCopySelectedMessages from '../../hooks/useNativeCopySelectedMessages'; import useNativeCopySelectedMessages from '../../hooks/useNativeCopySelectedMessages';
import useMedia from '../../hooks/useMedia'; import useMedia from '../../hooks/useMedia';
import useLayoutEffectWithPrevDeps from '../../hooks/useLayoutEffectWithPrevDeps'; import useLayoutEffectWithPrevDeps from '../../hooks/useLayoutEffectWithPrevDeps';
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
import useResizeObserver from '../../hooks/useResizeObserver'; import useContainerHeight from './hooks/useContainerHeight';
import Loading from '../ui/Loading'; import Loading from '../ui/Loading';
import MessageListContent from './MessageListContent'; import MessageListContent from './MessageListContent';
@ -182,10 +182,11 @@ const MessageList: FC<OwnProps & StateProps> = ({
// We update local cached `scrollOffsetRef` when opening chat. // We update local cached `scrollOffsetRef` when opening chat.
// Then we update global version every second on scrolling. // Then we update global version every second on scrolling.
const scrollOffsetRef = useRef<number>((type === 'thread' const scrollOffsetRef = useRef<number>(
&& selectScrollOffset(getGlobal(), chatId, threadId)) (type === 'thread' && selectScrollOffset(getGlobal(), chatId, threadId))
|| selectLastScrollOffset(getGlobal(), chatId, threadId) || selectLastScrollOffset(getGlobal(), chatId, threadId)
|| 0); || 0,
);
const anchorIdRef = useRef<string>(); const anchorIdRef = useRef<string>();
const anchorTopRef = useRef<number>(); const anchorTopRef = useRef<number>();
@ -196,8 +197,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
const isScrollTopJustUpdatedRef = useRef(false); const isScrollTopJustUpdatedRef = useRef(false);
const shouldAnimateAppearanceRef = useRef(Boolean(lastMessage)); const shouldAnimateAppearanceRef = useRef(Boolean(lastMessage));
const [containerHeight, setContainerHeight] = useState<number | undefined>();
const botInfoPhotoUrl = useMedia(botInfo?.photo ? getBotCoverMediaHash(botInfo.photo) : undefined); const botInfoPhotoUrl = useMedia(botInfo?.photo ? getBotCoverMediaHash(botInfo.photo) : undefined);
const botInfoGifUrl = useMedia(botInfo?.gif ? getDocumentMediaHash(botInfo.gif) : undefined); const botInfoGifUrl = useMedia(botInfo?.gif ? getDocumentMediaHash(botInfo.gif) : undefined);
const botInfoDimensions = botInfo?.photo ? getPhotoFullDimensions(botInfo.photo) : botInfo?.gif const botInfoDimensions = botInfo?.photo ? getPhotoFullDimensions(botInfo.photo) : botInfo?.gif
@ -247,8 +246,11 @@ const MessageList: FC<OwnProps & StateProps> = ({
return undefined; return undefined;
} }
const viewportIds = threadTopMessageId && threadFirstMessageId !== threadTopMessageId const viewportIds = (
threadTopMessageId
&& threadFirstMessageId !== threadTopMessageId
&& (!messageIds[0] || threadFirstMessageId === messageIds[0]) && (!messageIds[0] || threadFirstMessageId === messageIds[0])
)
? [threadTopMessageId, ...messageIds] ? [threadTopMessageId, ...messageIds]
: messageIds; : messageIds;
@ -342,18 +344,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
updateStickyDates, hasTools, getForceNextPinnedInHeader, onPinnedIntersectionChange, type, chatId, threadId, updateStickyDates, hasTools, getForceNextPinnedInHeader, onPinnedIntersectionChange, type, chatId, threadId,
]); ]);
// Container resize observer (caused by Composer reply/webpage panels) const [getContainerHeight, prevContainerHeightRef] = useContainerHeight(containerRef, canPost && !isSelectModeActive);
const handleResize = useCallback((entry: ResizeObserverEntry) => {
setContainerHeight(entry.contentRect.height);
}, []);
useResizeObserver(containerRef, handleResize);
// Memorize height for scroll animation
const { height: windowHeight } = useWindowSize();
useEffect(() => {
containerRef.current!.dataset.normalHeight = String(containerRef.current!.offsetHeight);
}, [windowHeight, canPost]);
// Initial message loading // Initial message loading
useEffect(() => { useEffect(() => {
@ -377,8 +368,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
} }
}, [isChatLoaded, messageIds, loadMoreAround, focusingId, isRestricted]); }, [isChatLoaded, messageIds, loadMoreAround, focusingId, isRestricted]);
// Remember scroll position before repositioning it const rememberScrollPositionRef = useStateRef(() => {
useSyncEffect(() => {
if (!messageIds || !listItemElementsRef.current) { if (!messageIds || !listItemElementsRef.current) {
return; return;
} }
@ -395,13 +385,25 @@ const MessageList: FC<OwnProps & StateProps> = ({
anchorIdRef.current = anchor.id; anchorIdRef.current = anchor.id;
anchorTopRef.current = anchor.getBoundingClientRect().top; anchorTopRef.current = anchor.getBoundingClientRect().top;
// This should match deps for `useLayoutEffectWithPrevDeps` below });
}, [messageIds, isViewportNewest, containerHeight, hasTools]);
useSyncEffect(
() => rememberScrollPositionRef.current(),
// This will run before modifying content and should match deps for `useLayoutEffectWithPrevDeps` below
[messageIds, isViewportNewest, hasTools, rememberScrollPositionRef],
);
useEffect(
() => rememberScrollPositionRef.current(),
// This is only needed to react on signal updates
[getContainerHeight, rememberScrollPositionRef],
);
// Handles updated message list, takes care of scroll repositioning // Handles updated message list, takes care of scroll repositioning
useLayoutEffectWithPrevDeps(([ useLayoutEffectWithPrevDeps(([prevMessageIds, prevIsViewportNewest]) => {
prevMessageIds, prevIsViewportNewest, prevContainerHeight, const containerHeight = getContainerHeight();
]) => { const prevContainerHeight = prevContainerHeightRef.current;
prevContainerHeightRef.current = containerHeight;
const container = containerRef.current!; const container = containerRef.current!;
listItemElementsRef.current = Array.from(container.querySelectorAll<HTMLDivElement>('.message-list-item')); listItemElementsRef.current = Array.from(container.querySelectorAll<HTMLDivElement>('.message-list-item'));
@ -476,7 +478,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
console.time('scrollTop'); console.time('scrollTop');
} }
const isResized = prevContainerHeight !== undefined && prevContainerHeight !== containerHeight; const isResized = prevContainerHeight && prevContainerHeight !== containerHeight;
const anchor = anchorIdRef.current && container.querySelector(`#${anchorIdRef.current}`); const anchor = anchorIdRef.current && container.querySelector(`#${anchorIdRef.current}`);
const unreadDivider = ( const unreadDivider = (
!anchor !anchor
@ -523,7 +525,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
console.timeEnd('scrollTop'); console.timeEnd('scrollTop');
} }
// This should match deps for `useSyncEffect` above // This should match deps for `useSyncEffect` above
}, [messageIds, isViewportNewest, containerHeight, hasTools]); }, [messageIds, isViewportNewest, getContainerHeight, prevContainerHeightRef, hasTools]);
useEffectWithPrevDeps(([prevIsSelectModeActive]) => { useEffectWithPrevDeps(([prevIsSelectModeActive]) => {
if (prevIsSelectModeActive !== undefined) { if (prevIsSelectModeActive !== undefined) {

View File

@ -208,6 +208,14 @@ const MiddleColumn: FC<OwnProps & StateProps> = ({
const [isNotchShown, setIsNotchShown] = useState<boolean | undefined>(); const [isNotchShown, setIsNotchShown] = useState<boolean | undefined>();
const [isUnpinModalOpen, setIsUnpinModalOpen] = useState(false); const [isUnpinModalOpen, setIsUnpinModalOpen] = useState(false);
const {
onIntersectionChanged,
onFocusPinnedMessage,
getCurrentPinnedIndexes,
getLoadingPinnedId,
getForceNextPinnedInHeader,
} = usePinnedMessage(chatId, threadId, pinnedIds);
const isMobileSearchActive = isMobile && hasCurrentTextSearch; const isMobileSearchActive = isMobile && hasCurrentTextSearch;
const closeAnimationDuration = isMobile ? LAYER_ANIMATION_DURATION_MS : undefined; const closeAnimationDuration = isMobile ? LAYER_ANIMATION_DURATION_MS : undefined;
const hasTools = hasPinned && ( const hasTools = hasPinned && (
@ -235,6 +243,10 @@ const MiddleColumn: FC<OwnProps & StateProps> = ({
const renderingIsChannel = usePrevDuringAnimation(isChannel, closeAnimationDuration); const renderingIsChannel = usePrevDuringAnimation(isChannel, closeAnimationDuration);
const renderingShouldJoinToSend = usePrevDuringAnimation(shouldJoinToSend, closeAnimationDuration); const renderingShouldJoinToSend = usePrevDuringAnimation(shouldJoinToSend, closeAnimationDuration);
const renderingShouldSendJoinRequest = usePrevDuringAnimation(shouldSendJoinRequest, closeAnimationDuration); const renderingShouldSendJoinRequest = usePrevDuringAnimation(shouldSendJoinRequest, closeAnimationDuration);
const renderingOnPinnedIntersectionChange = usePrevDuringAnimation(
chatId ? onIntersectionChanged : undefined,
closeAnimationDuration,
);
const prevTransitionKey = usePrevious(currentTransitionKey); const prevTransitionKey = usePrevious(currentTransitionKey);
@ -356,14 +368,6 @@ const MiddleColumn: FC<OwnProps & StateProps> = ({
const customBackgroundValue = useCustomBackground(theme, customBackground); const customBackgroundValue = useCustomBackground(theme, customBackground);
const {
onIntersectionChanged,
onFocusPinnedMessage,
getCurrentPinnedIndexes,
getLoadingPinnedId,
getForceNextPinnedInHeader,
} = usePinnedMessage(chatId, threadId, pinnedIds);
const className = buildClassName( const className = buildClassName(
renderingHasTools && 'has-header-tools', renderingHasTools && 'has-header-tools',
MASK_IMAGE_DISABLED ? 'mask-image-disabled' : 'mask-image-enabled', MASK_IMAGE_DISABLED ? 'mask-image-disabled' : 'mask-image-enabled',
@ -449,13 +453,13 @@ const MiddleColumn: FC<OwnProps & StateProps> = ({
style={customBackgroundValue ? `--custom-background: ${customBackgroundValue}` : undefined} style={customBackgroundValue ? `--custom-background: ${customBackgroundValue}` : undefined}
/> />
<div id="middle-column-portals" /> <div id="middle-column-portals" />
{renderingChatId && renderingThreadId && ( {Boolean(renderingChatId && renderingThreadId) && (
<> <>
<div className="messages-layout" onDragEnter={renderingCanPost ? handleDragEnter : undefined}> <div className="messages-layout" onDragEnter={renderingCanPost ? handleDragEnter : undefined}>
<MiddleHeader <MiddleHeader
chatId={renderingChatId} chatId={renderingChatId!}
threadId={renderingThreadId} threadId={renderingThreadId!}
messageListType={renderingMessageListType} messageListType={renderingMessageListType!}
isReady={isReady} isReady={isReady}
isMobile={isMobile} isMobile={isMobile}
getCurrentPinnedIndexes={getCurrentPinnedIndexes} getCurrentPinnedIndexes={getCurrentPinnedIndexes}
@ -471,25 +475,25 @@ const MiddleColumn: FC<OwnProps & StateProps> = ({
> >
<MessageList <MessageList
key={`${renderingChatId}-${renderingThreadId}-${renderingMessageListType}`} key={`${renderingChatId}-${renderingThreadId}-${renderingMessageListType}`}
chatId={renderingChatId} chatId={renderingChatId!}
threadId={renderingThreadId} threadId={renderingThreadId!}
type={renderingMessageListType} type={renderingMessageListType!}
canPost={renderingCanPost} canPost={renderingCanPost!}
hasTools={renderingHasTools} hasTools={renderingHasTools}
onFabToggle={setIsFabShown} onFabToggle={setIsFabShown}
onNotchToggle={setIsNotchShown} onNotchToggle={setIsNotchShown}
isReady={isReady} isReady={isReady}
withBottomShift={withMessageListBottomShift} withBottomShift={withMessageListBottomShift}
withDefaultBg={Boolean(!customBackground && !backgroundColor)} withDefaultBg={Boolean(!customBackground && !backgroundColor)}
onPinnedIntersectionChange={onIntersectionChanged} onPinnedIntersectionChange={renderingOnPinnedIntersectionChange!}
getForceNextPinnedInHeader={getForceNextPinnedInHeader} getForceNextPinnedInHeader={getForceNextPinnedInHeader}
/> />
<div className={footerClassName}> <div className={footerClassName}>
{renderingCanPost && ( {renderingCanPost && (
<Composer <Composer
chatId={renderingChatId} chatId={renderingChatId!}
threadId={renderingThreadId} threadId={renderingThreadId!}
messageListType={renderingMessageListType} messageListType={renderingMessageListType!}
dropAreaState={dropAreaState} dropAreaState={dropAreaState}
onDropHide={handleHideDropArea} onDropHide={handleHideDropArea}
isReady={isReady} isReady={isReady}
@ -519,8 +523,9 @@ const MiddleColumn: FC<OwnProps & StateProps> = ({
</div> </div>
</div> </div>
)} )}
{isMobile {(
&& (renderingCanSubscribe || (renderingShouldJoinToSend && !renderingShouldSendJoinRequest)) && ( isMobile && (renderingCanSubscribe || (renderingShouldJoinToSend && !renderingShouldSendJoinRequest))
) && (
<div className="middle-column-footer-button-container" dir={lang.isRtl ? 'rtl' : undefined}> <div className="middle-column-footer-button-container" dir={lang.isRtl ? 'rtl' : undefined}>
<Button <Button
size="tiny" size="tiny"
@ -584,7 +589,7 @@ const MiddleColumn: FC<OwnProps & StateProps> = ({
</Transition> </Transition>
<FloatingActionButtons <FloatingActionButtons
isShown={renderingIsFabShown} isShown={renderingIsFabShown!}
canPost={renderingCanPost} canPost={renderingCanPost}
withExtraShift={withExtraShift} withExtraShift={withExtraShift}
/> />

View File

@ -0,0 +1,28 @@
import type { RefObject } from 'react';
import { useCallback, useEffect, useRef } from '../../../lib/teact/teact';
import useSignal from '../../../hooks/useSignal';
import useResizeObserver from '../../../hooks/useResizeObserver';
export default function useContainerHeight(containerRef: RefObject<HTMLDivElement>, isComposerVisible: boolean) {
const [getContainerHeight, setContainerHeight] = useSignal<number | undefined>();
// Container resize observer (caused by Composer reply/webpage panels)
const handleResize = useCallback((entry: ResizeObserverEntry) => {
setContainerHeight(entry.contentRect.height);
}, [setContainerHeight]);
useResizeObserver(containerRef, handleResize);
useEffect(() => {
const currentNormalHeight = Number(containerRef.current!.dataset.normalHeight) || 0;
const containerHeight = getContainerHeight();
if (containerHeight && containerHeight > currentNormalHeight && isComposerVisible) {
containerRef.current!.dataset.normalHeight = String(containerHeight);
}
}, [isComposerVisible, containerRef, getContainerHeight]);
const prevContainerHeight = useRef<number>();
return [getContainerHeight, prevContainerHeight] as const;
}

View File

@ -576,7 +576,7 @@ const Message: FC<OwnProps & StateProps> = ({
text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice, location, action, game, text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice, location, action, game,
} = getMessageContent(message); } = getMessageContent(message);
const { result: detectedLanguage } = useTextLanguage(areTranslationsEnabled ? text?.text : undefined); const detectedLanguage = useTextLanguage(areTranslationsEnabled ? text?.text : undefined);
const { isPending: isTranslationPending, translatedText } = useMessageTranslation( const { isPending: isTranslationPending, translatedText } = useMessageTranslation(
chatTranslations, chatId, messageId, requestedTranslationLanguage, chatTranslations, chatId, messageId, requestedTranslationLanguage,

View File

@ -43,6 +43,7 @@ import {
removeRequestedMessageTranslation, removeRequestedMessageTranslation,
replaceScheduledMessages, replaceScheduledMessages,
replaceThreadParam, replaceThreadParam,
safeReplacePinnedIds,
safeReplaceViewportIds, safeReplaceViewportIds,
updateChat, updateChat,
updateChatMessage, updateChatMessage,
@ -73,14 +74,16 @@ import {
selectLanguageCode, selectLanguageCode,
selectListedIds, selectListedIds,
selectNoWebPage, selectNoWebPage,
selectOutlyingListByMessageId, selectPinnedIds, selectOutlyingListByMessageId,
selectPinnedIds,
selectRealLastReadId, selectRealLastReadId,
selectReplyingToId, selectReplyingToId,
selectScheduledMessage, selectScheduledMessage,
selectSendAs, selectSendAs,
selectSponsoredMessage, selectSponsoredMessage,
selectTabState, selectTabState,
selectThreadIdFromMessage, selectThreadOriginChat, selectThreadIdFromMessage,
selectThreadOriginChat,
selectThreadTopMessageId, selectThreadTopMessageId,
selectUser, selectUser,
selectViewportIds, selectViewportIds,
@ -1175,7 +1178,7 @@ addActionHandler('loadPinnedMessages', async (global, actions, payload): Promise
global = getGlobal(); global = getGlobal();
global = addChatMessagesById(global, chat.id, byId); global = addChatMessagesById(global, chat.id, byId);
global = replaceThreadParam(global, chat.id, threadId, 'pinnedIds', ids); global = safeReplacePinnedIds(global, chat.id, threadId, ids);
global = addUsers(global, buildCollectionByKey(users, 'id')); global = addUsers(global, buildCollectionByKey(users, 'id'));
global = addChats(global, buildCollectionByKey(chats, 'id')); global = addChats(global, buildCollectionByKey(chats, 'id'));
setGlobal(global); setGlobal(global);

View File

@ -14,6 +14,7 @@ import { getUserFullName } from './users';
import { IS_OPUS_SUPPORTED, isWebpSupported } from '../../util/windowEnvironment'; import { IS_OPUS_SUPPORTED, isWebpSupported } from '../../util/windowEnvironment';
import { getChatTitle, isUserId } from './chats'; import { getChatTitle, isUserId } from './chats';
import { getGlobal } from '../index'; import { getGlobal } from '../index';
import { areSortedArraysIntersecting, unique } from '../../util/iteratees';
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i'); const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
@ -253,3 +254,44 @@ export function getMessageSingleInlineButton(message: ApiMessage) {
&& message.inlineButtons[0].length === 1 && message.inlineButtons[0].length === 1
&& message.inlineButtons[0][0]; && message.inlineButtons[0][0];
} }
export function orderHistoryIds(listedIds: number[]) {
return listedIds.sort((a, b) => a - b);
}
export function orderPinnedIds(pinnedIds: number[]) {
return pinnedIds.sort((a, b) => b - a);
}
export function mergeIdRanges(ranges: number[][], idsUpdate: number[]): number[][] {
let hasIntersection = false;
let newOutlyingLists = ranges.length ? ranges.map((list) => {
if (areSortedArraysIntersecting(list, idsUpdate) && !hasIntersection) {
hasIntersection = true;
return orderHistoryIds(unique(list.concat(idsUpdate)));
}
return list;
}) : [idsUpdate];
if (!hasIntersection) {
newOutlyingLists = newOutlyingLists.concat([idsUpdate]);
}
newOutlyingLists.sort((a, b) => a[0] - b[0]);
let length = newOutlyingLists.length;
for (let i = 0; i < length; i++) {
const array = newOutlyingLists[i];
const prevArray = newOutlyingLists[i - 1];
if (prevArray && (prevArray.includes(array[0]) || prevArray.includes(array[0] - 1))) {
newOutlyingLists[i - 1] = orderHistoryIds(unique(array.concat(prevArray)));
newOutlyingLists.splice(i, 1);
length--;
i--;
}
}
return newOutlyingLists;
}

View File

@ -27,11 +27,13 @@ import {
selectTabState, selectOutlyingLists, selectTabState, selectOutlyingLists,
} from '../selectors'; } from '../selectors';
import { import {
areSortedArraysEqual, mergeIdRanges, omit, orderHistoryIds, pickTruthy, unique, areSortedArraysEqual, omit, pickTruthy, unique,
} from '../../util/iteratees'; } from '../../util/iteratees';
import { updateTabState } from './tabs'; import { updateTabState } from './tabs';
import { getCurrentTabId } from '../../util/establishMultitabRole'; import { getCurrentTabId } from '../../util/establishMultitabRole';
import { isLocalMessageId } from '../helpers'; import {
isLocalMessageId, mergeIdRanges, orderHistoryIds, orderPinnedIds,
} from '../helpers';
type MessageStoreSections = { type MessageStoreSections = {
byId: Record<number, ApiMessage>; byId: Record<number, ApiMessage>;
@ -454,6 +456,24 @@ export function safeReplaceViewportIds<T extends GlobalState>(
); );
} }
export function safeReplacePinnedIds<T extends GlobalState>(
global: T,
chatId: string,
threadId: number,
newPinnedIds: number[],
): T {
const currentIds = selectPinnedIds(global, chatId, threadId) || [];
const newIds = orderPinnedIds(newPinnedIds);
return replaceThreadParam(
global,
chatId,
threadId,
'pinnedIds',
areSortedArraysEqual(currentIds, newIds) ? currentIds : newIds,
);
}
export function updateThreadInfo<T extends GlobalState>( export function updateThreadInfo<T extends GlobalState>(
global: T, chatId: string, threadId: number, update: Partial<ApiThreadInfo> | undefined, global: T, chatId: string, threadId: number, update: Partial<ApiThreadInfo> | undefined,
): T { ): T {

View File

@ -1,11 +1,14 @@
import { useEffect } from '../lib/teact/teact'; import { useEffect, useRef } from '../lib/teact/teact';
import usePrevious from './usePrevious';
const useEffectWithPrevDeps = <const T extends readonly any[]>( const useEffectWithPrevDeps = <const T extends readonly any[]>(
cb: (args: T | readonly []) => void, dependencies: T, debugKey?: string, cb: (args: T | readonly []) => void, dependencies: T, debugKey?: string,
) => { ) => {
const prevDeps = usePrevious<T>(dependencies); const prevDepsRef = useRef<T>();
return useEffect(() => { return useEffect(() => {
const prevDeps = prevDepsRef.current;
prevDepsRef.current = dependencies;
return cb(prevDeps || []); return cb(prevDeps || []);
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, dependencies, debugKey); }, dependencies, debugKey);

View File

@ -1,11 +1,14 @@
import { useLayoutEffect } from '../lib/teact/teact'; import { useLayoutEffect, useRef } from '../lib/teact/teact';
import usePrevious from './usePrevious';
const useLayoutEffectWithPrevDeps = <const T extends readonly any[]>( const useLayoutEffectWithPrevDeps = <const T extends readonly any[]>(
cb: (args: T | readonly []) => void, dependencies: T, debugKey?: string, cb: (args: T | readonly []) => void, dependencies: T, debugKey?: string,
) => { ) => {
const prevDeps = usePrevious<T>(dependencies); const prevDepsRef = useRef<T>();
return useLayoutEffect(() => { return useLayoutEffect(() => {
const prevDeps = prevDepsRef.current;
prevDepsRef.current = dependencies;
return cb(prevDeps || []); return cb(prevDeps || []);
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, dependencies, debugKey); }, dependencies, debugKey);

View File

@ -4,7 +4,7 @@ import usePrevious from './usePrevious';
import useForceUpdate from './useForceUpdate'; import useForceUpdate from './useForceUpdate';
import useSyncEffect from './useSyncEffect'; import useSyncEffect from './useSyncEffect';
export default function usePrevDuringAnimation(current: any, duration?: number) { export default function usePrevDuringAnimation<T>(current: T, duration?: number) {
const prev = usePrevious(current, true); const prev = usePrevious(current, true);
const timeoutRef = useRef<number>(); const timeoutRef = useRef<number>();
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();

View File

@ -1,7 +1,17 @@
import { useState } from '../lib/teact/teact';
import { detectLanguage } from '../util/languageDetection'; import { detectLanguage } from '../util/languageDetection';
import useAsync from './useAsync';
import useSyncEffect from './useSyncEffect';
export default function useTextLanguage(text?: string) { export default function useTextLanguage(text?: string) {
const language = useAsync(() => (text ? detectLanguage(text) : Promise.resolve(undefined)), [text], undefined); const [language, setLanguage] = useState<string>();
useSyncEffect(() => {
if (text) {
detectLanguage(text).then(setLanguage);
}
}, [text]);
return language; return language;
} }

View File

@ -734,6 +734,7 @@ export function useMemo<T extends any>(resolver: () => T, dependencies: any[], d
if ( if (
byCursor[cursor] === undefined byCursor[cursor] === undefined
|| dependencies.length !== byCursor[cursor].dependencies.length
|| dependencies.some((dependency, i) => dependency !== byCursor[cursor].dependencies[i]) || dependencies.some((dependency, i) => dependency !== byCursor[cursor].dependencies[i])
) { ) {
if (DEBUG && debugKey) { if (DEBUG && debugKey) {
@ -741,7 +742,7 @@ export function useMemo<T extends any>(resolver: () => T, dependencies: any[], d
console.log( console.log(
`[Teact.useMemo] ${renderingInstance.name} (${debugKey}): Update is caused by:`, `[Teact.useMemo] ${renderingInstance.name} (${debugKey}): Update is caused by:`,
byCursor[cursor] byCursor[cursor]
? getUnequalProps(dependencies, byCursor[cursor].dependencies).join(', ') ? getUnequalProps(byCursor[cursor].dependencies, dependencies).join(', ')
: '[first render]', : '[first render]',
); );
} }

View File

@ -148,43 +148,6 @@ function isObject(value: any): value is object {
return typeof value === 'object' && value !== null; return typeof value === 'object' && value !== null;
} }
export function orderHistoryIds(listedIds: number[]) {
return listedIds.sort((a, b) => a - b);
}
export function mergeIdRanges(ranges: number[][], idsUpdate: number[]): number[][] {
let hasIntersection = false;
let newOutlyingLists = ranges.length ? ranges.map((list) => {
if (areSortedArraysIntersecting(list, idsUpdate) && !hasIntersection) {
hasIntersection = true;
return orderHistoryIds(unique(list.concat(idsUpdate)));
}
return list;
}) : [idsUpdate];
if (!hasIntersection) {
newOutlyingLists = newOutlyingLists.concat([idsUpdate]);
}
newOutlyingLists.sort((a, b) => a[0] - b[0]);
let length = newOutlyingLists.length;
for (let i = 0; i < length; i++) {
const array = newOutlyingLists[i];
const prevArray = newOutlyingLists[i - 1];
if (prevArray && (prevArray.includes(array[0]) || prevArray.includes(array[0] - 1))) {
newOutlyingLists[i - 1] = orderHistoryIds(unique(array.concat(prevArray)));
newOutlyingLists.splice(i, 1);
length--;
i--;
}
}
return newOutlyingLists;
}
export function findLast<T>(array: Array<T>, predicate: (value: T, index: number, obj: T[]) => boolean): T | undefined { export function findLast<T>(array: Array<T>, predicate: (value: T, index: number, obj: T[]) => boolean): T | undefined {
let cursor = array.length; let cursor = array.length;