[Perf] Middle Column: Various optimizations for opening chat
This commit is contained in:
parent
2f63a0fcf0
commit
d872273def
@ -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) {
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
28
src/components/middle/hooks/useContainerHeight.ts
Normal file
28
src/components/middle/hooks/useContainerHeight.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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]',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user