diff --git a/src/components/middle/ActionMessage.tsx b/src/components/middle/ActionMessage.tsx index 5dad8b2f2..f84478864 100644 --- a/src/components/middle/ActionMessage.tsx +++ b/src/components/middle/ActionMessage.tsx @@ -1,6 +1,6 @@ import type { FC } from '../../lib/teact/teact'; import React, { - memo, useCallback, useEffect, useMemo, useRef, + memo, useCallback, useEffect, useMemo, useRef, useUnmountCleanup, } from '../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../global'; @@ -10,7 +10,7 @@ import type { import type { MessageListType } from '../../global/types'; import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { FocusDirection, ThreadId } from '../../types'; -import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage'; +import type { OnIntersectPinnedMessage } from './hooks/usePinnedMessage'; import { getChatTitle, getMessageHtmlId, isJoinedChannelMessage } from '../../global/helpers'; import { getMessageReplyInfo } from '../../global/helpers/replies'; @@ -57,7 +57,7 @@ type OwnProps = { isLastInList?: boolean; isInsideTopic?: boolean; memoFirstUnreadIdRef?: { current: number | undefined }; - onPinnedIntersectionChange?: PinnedIntersectionChangedCallback; + onIntersectPinnedMessage?: OnIntersectPinnedMessage; }; type StateProps = { @@ -102,7 +102,7 @@ const ActionMessage: FC = ({ observeIntersectionForReading, observeIntersectionForLoading, observeIntersectionForPlaying, - onPinnedIntersectionChange, + onIntersectPinnedMessage, }) => { const { openPremiumModal, requestConfetti, checkGiftCode, getReceipt, openStarsTransactionFromGift, @@ -121,13 +121,11 @@ const ActionMessage: FC = ({ ); useFocusMessage(ref, message.chatId, isFocused, focusDirection, noFocusHighlight, isJustAdded); - useEffect(() => { - if (!message.isPinned) return undefined; - - return () => { - onPinnedIntersectionChange?.({ viewportPinnedIdsToRemove: [message.id], isUnmount: true }); - }; - }, [onPinnedIntersectionChange, message.isPinned, message.id]); + useUnmountCleanup(() => { + if (message.isPinned) { + onIntersectPinnedMessage?.({ viewportPinnedIdsToRemove: [message.id] }); + } + }); const noAppearanceAnimation = appearanceOrder <= 0; const [isShown, markShown] = useFlag(noAppearanceAnimation); @@ -287,10 +285,14 @@ const ActionMessage: FC = ({ {oldLang(isUnclaimed ? 'BoostingUnclaimedPrize' : 'BoostingCongratulations')} - {targetChat && renderText(oldLang(isFromGiveaway ? 'BoostingReceivedGiftFrom' : isUnclaimed - ? 'BoostingReceivedPrizeFrom' : 'BoostingYouHaveUnclaimedPrize', - getChatTitle(oldLang, targetChat)), - ['simple_markdown'])} + {targetChat && renderText( + oldLang( + isFromGiveaway ? 'BoostingReceivedGiftFrom' : isUnclaimed + ? 'BoostingReceivedPrizeFrom' : 'BoostingYouHaveUnclaimedPrize', + getChatTitle(oldLang, targetChat), + ), + ['simple_markdown'], + )} {renderText(oldLang( diff --git a/src/components/middle/HeaderPinnedMessage.tsx b/src/components/middle/HeaderPinnedMessage.tsx index b24ef9052..a55b93427 100644 --- a/src/components/middle/HeaderPinnedMessage.tsx +++ b/src/components/middle/HeaderPinnedMessage.tsx @@ -3,17 +3,16 @@ import React, { memo } from '../../lib/teact/teact'; import { getActions } from '../../global'; import type { ApiMessage } from '../../api/types'; +import type { Signal } from '../../util/signals'; -import { - getMessageIsSpoiler, - getMessageMediaHash, getMessageSingleInlineButton, -} from '../../global/helpers'; +import { getMessageIsSpoiler, getMessageMediaHash, getMessageSingleInlineButton } from '../../global/helpers'; import buildClassName from '../../util/buildClassName'; import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; import { getPictogramDimensions, REM } from '../common/helpers/mediaDimensions'; import renderText from '../common/helpers/renderText'; import renderKeyboardButtonText from './composer/helpers/renderKeyboardButtonText'; +import useDerivedState from '../../hooks/useDerivedState'; import { useFastClick } from '../../hooks/useFastClick'; import useFlag from '../../hooks/useFlag'; import useLastCallback from '../../hooks/useLastCallback'; @@ -46,13 +45,13 @@ type OwnProps = { onUnpinMessage?: (id: number) => void; onClick?: (e: React.MouseEvent) => void; onAllPinnedClick?: () => void; - isLoading?: boolean; + getLoadingPinnedId: Signal; isFullWidth?: boolean; }; const HeaderPinnedMessage: FC = ({ message, count, index, customTitle, className, onUnpinMessage, onClick, onAllPinnedClick, - isLoading, isFullWidth, + getLoadingPinnedId, isFullWidth, }) => { const { clickBotInlineButton } = getActions(); const lang = useOldLang(); @@ -60,6 +59,8 @@ const HeaderPinnedMessage: FC = ({ const mediaThumbnail = useThumbnail(message); const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'pictogram')); const isSpoiler = getMessageIsSpoiler(message); + + const isLoading = Boolean(useDerivedState(getLoadingPinnedId)); const canRenderLoader = useAsyncRendering([isLoading], SHOW_LOADER_DELAY); const shouldShowLoader = canRenderLoader && isLoading; diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 409eabce4..142dc7232 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -1,21 +1,15 @@ import type { FC } from '../../lib/teact/teact'; import React, { - beginHeavyAnimation, - memo, - useEffect, - useMemo, - useRef, + beginHeavyAnimation, memo, useEffect, useMemo, useRef, } from '../../lib/teact/teact'; import { addExtraClass, removeExtraClass } from '../../lib/teact/teact-dom'; import { getActions, getGlobal, withGlobal } from '../../global'; import type { - ApiChatFullInfo, - ApiMessage, ApiRestrictionReason, ApiTopic, + ApiChatFullInfo, ApiMessage, ApiRestrictionReason, ApiTopic, } from '../../api/types'; import type { MessageListType } from '../../global/types'; -import type { Signal } from '../../util/signals'; -import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage'; +import type { OnIntersectPinnedMessage } from './hooks/usePinnedMessage'; import { MAIN_THREAD_ID } from '../../api/types'; import { LoadMoreDirection, type ThreadId } from '../../types'; @@ -98,8 +92,7 @@ type OwnProps = { hasTools?: boolean; withBottomShift?: boolean; withDefaultBg: boolean; - onPinnedIntersectionChange: PinnedIntersectionChangedCallback; - getForceNextPinnedInHeader: Signal; + onIntersectPinnedMessage: OnIntersectPinnedMessage; isContactRequirePremium?: boolean; }; @@ -185,11 +178,10 @@ const MessageList: FC = ({ noMessageSendingAnimation, isServiceNotificationsChat, currentUserId, - getForceNextPinnedInHeader, isContactRequirePremium, areAdsEnabled, channelJoinInfo, - onPinnedIntersectionChange, + onIntersectPinnedMessage, onScrollDownToggle, onNotchToggle, }) => { @@ -418,9 +410,10 @@ const MessageList: FC = ({ runDebouncedForScroll(() => { const global = getGlobal(); - const forceNextPinnedInHeader = getForceNextPinnedInHeader() && !selectTabState(global).focusedMessage?.chatId; - if (forceNextPinnedInHeader) { - onPinnedIntersectionChange({ hasScrolled: true }); + + const isFocusing = Boolean(selectTabState(global).focusedMessage?.chatId); + if (!isFocusing) { + onIntersectPinnedMessage({ shouldCancelWaiting: true }); } if (!container.parentElement) { @@ -731,7 +724,7 @@ const MessageList: FC = ({ noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current} onScrollDownToggle={onScrollDownToggle} onNotchToggle={onNotchToggle} - onPinnedIntersectionChange={onPinnedIntersectionChange} + onIntersectPinnedMessage={onIntersectPinnedMessage} /> ) : ( diff --git a/src/components/middle/MessageListContent.tsx b/src/components/middle/MessageListContent.tsx index 7c664620d..4c8986224 100644 --- a/src/components/middle/MessageListContent.tsx +++ b/src/components/middle/MessageListContent.tsx @@ -7,7 +7,7 @@ import type { MessageListType } from '../../global/types'; import type { ThreadId } from '../../types'; import type { Signal } from '../../util/signals'; import type { MessageDateGroup } from './helpers/groupMessages'; -import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage'; +import type { OnIntersectPinnedMessage } from './hooks/usePinnedMessage'; import { MAIN_THREAD_ID } from '../../api/types'; import { SCHEDULED_WHEN_ONLINE } from '../../config'; @@ -62,7 +62,7 @@ interface OwnProps { isSavedDialog?: boolean; onScrollDownToggle: BooleanToVoidFunction; onNotchToggle: AnyToVoidFunction; - onPinnedIntersectionChange: PinnedIntersectionChangedCallback; + onIntersectPinnedMessage: OnIntersectPinnedMessage; } const UNREAD_DIVIDER_CLASS = 'unread-divider'; @@ -94,7 +94,7 @@ const MessageListContent: FC = ({ isSavedDialog, onScrollDownToggle, onNotchToggle, - onPinnedIntersectionChange, + onIntersectPinnedMessage, }) => { const { openHistoryCalendar } = getActions(); @@ -107,7 +107,7 @@ const MessageListContent: FC = ({ observeIntersectionForReading, observeIntersectionForLoading, observeIntersectionForPlaying, - } = useMessageObservers(type, containerRef, memoFirstUnreadIdRef, onPinnedIntersectionChange, chatId); + } = useMessageObservers(type, containerRef, memoFirstUnreadIdRef, onIntersectPinnedMessage, chatId); const { withHistoryTriggers, @@ -180,7 +180,7 @@ const MessageListContent: FC = ({ appearanceOrder={messageCountToAnimate - ++appearanceIndex} isJustAdded={isLastInList && isNewMessage} isLastInList={isLastInList} - onPinnedIntersectionChange={onPinnedIntersectionChange} + onIntersectPinnedMessage={onIntersectPinnedMessage} />, ]); } @@ -249,7 +249,7 @@ const MessageListContent: FC = ({ isLastInDocumentGroup={position.isLastInDocumentGroup} isLastInList={position.isLastInList} memoFirstUnreadIdRef={memoFirstUnreadIdRef} - onPinnedIntersectionChange={onPinnedIntersectionChange} + onIntersectPinnedMessage={onIntersectPinnedMessage} getIsMessageListReady={getIsReady} />, message.id === threadId && ( diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index d4bdd8b53..a9bdf4f49 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -154,7 +154,6 @@ type StateProps = { shouldJoinToSend?: boolean; shouldSendJoinRequest?: boolean; pinnedIds?: number[]; - topMessageId?: number; canUnpin?: boolean; canUnblock?: boolean; isSavedDialog?: boolean; @@ -216,7 +215,6 @@ function MiddleColumn({ shouldSendJoinRequest, shouldLoadFullChat, pinnedIds, - topMessageId, canUnpin, canUnblock, isSavedDialog, @@ -252,12 +250,11 @@ function MiddleColumn({ const [isUnpinModalOpen, setIsUnpinModalOpen] = useState(false); const { - onIntersectionChanged, - onFocusPinnedMessage, - getCurrentPinnedIndexes, + handleIntersectPinnedMessage, + handleFocusPinnedMessage, + getCurrentPinnedIndex, getLoadingPinnedId, - getForceNextPinnedInHeader, - } = usePinnedMessage(chatId, threadId, pinnedIds, topMessageId); + } = usePinnedMessage(chatId, threadId, pinnedIds); const closeAnimationDuration = isMobile ? LAYER_ANIMATION_DURATION_MS : undefined; const hasTools = hasPinned && ( @@ -287,8 +284,8 @@ function MiddleColumn({ const renderingIsChannel = usePrevDuringAnimation(isChannel, closeAnimationDuration); const renderingShouldJoinToSend = usePrevDuringAnimation(shouldJoinToSend, closeAnimationDuration); const renderingShouldSendJoinRequest = usePrevDuringAnimation(shouldSendJoinRequest, closeAnimationDuration); - const renderingOnPinnedIntersectionChange = usePrevDuringAnimation( - chatId ? onIntersectionChanged : undefined, + const renderingHandleIntersectPinnedMessage = usePrevDuringAnimation( + chatId ? handleIntersectPinnedMessage : undefined, closeAnimationDuration, ); @@ -538,9 +535,9 @@ function MiddleColumn({ isComments={isComments} isReady={isReady} isMobile={isMobile} - getCurrentPinnedIndexes={getCurrentPinnedIndexes} + getCurrentPinnedIndex={getCurrentPinnedIndex} getLoadingPinnedId={getLoadingPinnedId} - onFocusPinnedMessage={onFocusPinnedMessage} + onFocusPinnedMessage={handleFocusPinnedMessage} />
{renderingCanPost && ( @@ -812,7 +808,6 @@ export default memo(withGlobal( const canShowOpenChatButton = isSavedDialog && threadId !== ANONYMOUS_USER_ID; const isCommentThread = threadId !== MAIN_THREAD_ID && !isSavedDialog && !chat?.isForum; - const topMessageId = isCommentThread ? Number(threadId) : undefined; const canUnpin = chat && ( isPrivate || ( @@ -856,7 +851,6 @@ export default memo(withGlobal( shouldSendJoinRequest, shouldLoadFullChat, pinnedIds, - topMessageId, canUnpin, canUnblock, isSavedDialog, diff --git a/src/components/middle/MiddleHeader.tsx b/src/components/middle/MiddleHeader.tsx index b8c308981..c18e6769d 100644 --- a/src/components/middle/MiddleHeader.tsx +++ b/src/components/middle/MiddleHeader.tsx @@ -91,9 +91,9 @@ type OwnProps = { isComments?: boolean; isReady?: boolean; isMobile?: boolean; - getCurrentPinnedIndexes: Signal>; + getCurrentPinnedIndex: Signal; getLoadingPinnedId: Signal; - onFocusPinnedMessage: (messageId: number) => boolean; + onFocusPinnedMessage: (messageId: number) => void; }; type StateProps = { @@ -147,7 +147,7 @@ const MiddleHeader: FC = ({ isSyncing, isSynced, isFetchingDifference, - getCurrentPinnedIndexes, + getCurrentPinnedIndex, getLoadingPinnedId, emojiStatusSticker, isSavedDialog, @@ -173,9 +173,7 @@ const MiddleHeader: FC = ({ const isBackButtonActive = useRef(true); const { isTablet } = useAppLayout(); - const currentPinnedIndexes = useDerivedState(getCurrentPinnedIndexes); - const currentPinnedIndex = currentPinnedIndexes[`${chatId}_${threadId}`] || 0; - const waitingForPinnedId = useDerivedState(getLoadingPinnedId); + const currentPinnedIndex = useDerivedState(getCurrentPinnedIndex); const pinnedMessageId = Array.isArray(pinnedMessageIds) ? pinnedMessageIds[currentPinnedIndex] : pinnedMessageIds; const pinnedMessage = messagesById && pinnedMessageId ? messagesById[pinnedMessageId] : undefined; const pinnedMessagesCount = Array.isArray(pinnedMessageIds) @@ -233,10 +231,11 @@ const MiddleHeader: FC = ({ ? pinnedMessageIds[cycleRestrict(pinnedMessageIds.length, pinnedMessageIds.indexOf(pinnedMessageId!) - 2)] : pinnedMessageId!; - if (onFocusPinnedMessage(messageId)) { + if (!getLoadingPinnedId()) { focusMessage({ chatId, threadId, messageId, noForumTopicPanel: true, }); + onFocusPinnedMessage(messageId); } }); @@ -509,7 +508,7 @@ const MiddleHeader: FC = ({ onUnpinMessage={renderingCanUnpin ? handleUnpinMessage : undefined} onClick={handlePinnedMessageClick} onAllPinnedClick={handleAllPinnedClick} - isLoading={waitingForPinnedId !== undefined} + getLoadingPinnedId={getLoadingPinnedId} isFullWidth={isPinnedMessagesFullWidth} /> )} diff --git a/src/components/middle/hooks/useMessageObservers.ts b/src/components/middle/hooks/useMessageObservers.ts index b0251187d..7c68df00d 100644 --- a/src/components/middle/hooks/useMessageObservers.ts +++ b/src/components/middle/hooks/useMessageObservers.ts @@ -2,7 +2,7 @@ import type { RefObject } from 'react'; import { getActions } from '../../../global'; import type { MessageListType } from '../../../global/types'; -import type { PinnedIntersectionChangedCallback } from './usePinnedMessage'; +import type { OnIntersectPinnedMessage } from './usePinnedMessage'; import { IS_ANDROID } from '../../../util/windowEnvironment'; @@ -17,7 +17,7 @@ export default function useMessageObservers( type: MessageListType, containerRef: RefObject, memoFirstUnreadIdRef: { current: number | undefined }, - onPinnedIntersectionChange: PinnedIntersectionChangedCallback, + onIntersectPinnedMessage: OnIntersectPinnedMessage, chatId: string, ) { const { @@ -44,12 +44,9 @@ export default function useMessageObservers( const viewportPinnedIdsToAdd: number[] = []; const viewportPinnedIdsToRemove: number[] = []; const scheduledToUpdateViews: number[] = []; - let isReversed = false; entries.forEach((entry) => { - const { - isIntersecting, target, boundingClientRect, rootBounds, - } = entry; + const { isIntersecting, target } = entry; const { dataset } = target as HTMLDivElement; const messageId = Number(dataset.lastMessageId || dataset.messageId); @@ -58,9 +55,6 @@ export default function useMessageObservers( if (!isIntersecting) { if (dataset.isPinned) { - if (rootBounds && boundingClientRect.bottom < rootBounds.top) { - isReversed = true; - } viewportPinnedIdsToRemove.push(albumMainId || messageId); } return; @@ -100,7 +94,7 @@ export default function useMessageObservers( } if (viewportPinnedIdsToAdd.length || viewportPinnedIdsToRemove.length) { - onPinnedIntersectionChange({ viewportPinnedIdsToAdd, viewportPinnedIdsToRemove, isReversed }); + onIntersectPinnedMessage({ viewportPinnedIdsToAdd, viewportPinnedIdsToRemove }); } if (scheduledToUpdateViews.length) { diff --git a/src/components/middle/hooks/usePinnedMessage.ts b/src/components/middle/hooks/usePinnedMessage.ts index 913876ce9..875b855a2 100644 --- a/src/components/middle/hooks/usePinnedMessage.ts +++ b/src/components/middle/hooks/usePinnedMessage.ts @@ -1,171 +1,139 @@ -import { useEffect, useRef, useSignal } from '../../../lib/teact/teact'; +import { useEffect, useSignal } from '../../../lib/teact/teact'; import { getGlobal } from '../../../global'; import type { ThreadId } from '../../../types'; -import { - selectFocusedMessageId, - selectListedIds, - selectOutlyingListByMessageId, -} from '../../../global/selectors'; +import { selectFocusedMessageId, selectListedIds, selectOutlyingListByMessageId } from '../../../global/selectors'; import cycleRestrict from '../../../util/cycleRestrict'; import { unique } from '../../../util/iteratees'; -import { clamp } from '../../../util/math'; +import useDerivedSignal from '../../../hooks/useDerivedSignal'; import useLastCallback from '../../../hooks/useLastCallback'; -type PinnedIntersectionChangedParams = { +export type OnIntersectPinnedMessage = (params: { viewportPinnedIdsToAdd?: number[]; viewportPinnedIdsToRemove?: number[]; - isReversed?: boolean; - hasScrolled?: boolean; - isUnmount?: boolean; -}; + shouldCancelWaiting?: boolean; +}) => void; -export type PinnedIntersectionChangedCallback = (params: PinnedIntersectionChangedParams) => void; +let viewportPinnedIds: number[] | undefined; +let lastFocusedId: number | undefined; export default function usePinnedMessage( - chatId?: string, threadId?: ThreadId, pinnedIds?: number[], topMessageId?: number, + chatId?: string, threadId?: ThreadId, pinnedIds?: number[], ) { - const [getCurrentPinnedIndexes, setCurrentPinnedIndexes] = useSignal>({}); - const [getForceNextPinnedInHeader, setForceNextPinnedInHeader] = useSignal(); - const viewportPinnedIdsRef = useRef(); + const [getPinnedIndexByKey, setPinnedIndexByKey] = useSignal>({}); const [getLoadingPinnedId, setLoadingPinnedId] = useSignal(); - const key = chatId ? `${chatId}_${threadId}` : undefined; + const getCurrentPinnedIndex = useDerivedSignal( + () => (getPinnedIndexByKey()[key!] ?? 0), + [getPinnedIndexByKey, key], + ); // Reset when switching chat useEffect(() => { - setForceNextPinnedInHeader(undefined); - viewportPinnedIdsRef.current = undefined; + viewportPinnedIds = undefined; setLoadingPinnedId(undefined); }, [ - chatId, setCurrentPinnedIndexes, setForceNextPinnedInHeader, setLoadingPinnedId, threadId, + chatId, setPinnedIndexByKey, setLoadingPinnedId, threadId, ]); useEffect(() => { if (!key) return; - const currentPinnedIndex = getCurrentPinnedIndexes()[key]; + const currentPinnedIndex = getPinnedIndexByKey()[key]; const pinnedLength = pinnedIds?.length || 0; if (currentPinnedIndex >= pinnedLength) { - setCurrentPinnedIndexes({ - ...getCurrentPinnedIndexes(), + setPinnedIndexByKey({ + ...getPinnedIndexByKey(), [key]: Math.max(0, pinnedLength - 1), }); } - }, [getCurrentPinnedIndexes, key, pinnedIds?.length, setCurrentPinnedIndexes]); + }, [getPinnedIndexByKey, key, pinnedIds?.length, setPinnedIndexByKey]); - const onIntersectionChanged = useLastCallback(({ - viewportPinnedIdsToAdd = [], viewportPinnedIdsToRemove = [], isReversed, hasScrolled, isUnmount, - }: PinnedIntersectionChangedParams) => { - if (!chatId || !threadId || !key) return; + const handleIntersectPinnedMessage: OnIntersectPinnedMessage = useLastCallback(({ + viewportPinnedIdsToAdd = [], + viewportPinnedIdsToRemove = [], + shouldCancelWaiting, + }) => { + if (!chatId || !threadId || !key || !pinnedIds?.length) return; - const global = getGlobal(); + if (shouldCancelWaiting) { + lastFocusedId = undefined; + setLoadingPinnedId(undefined); + return; + } - const pinnedMessagesCount = pinnedIds?.length || 0; - - if (!pinnedMessagesCount || !pinnedIds) return; - - const waitingForPinnedId = getLoadingPinnedId(); - if (waitingForPinnedId && !hasScrolled) { - const newPinnedIndex = pinnedIds.indexOf(waitingForPinnedId); - setCurrentPinnedIndexes({ - ...getCurrentPinnedIndexes(), + const loadingPinnedId = getLoadingPinnedId(); + if (loadingPinnedId) { + const newPinnedIndex = pinnedIds.indexOf(loadingPinnedId); + setPinnedIndexByKey({ + ...getPinnedIndexByKey(), [key]: newPinnedIndex, }); setLoadingPinnedId(undefined); } - if (hasScrolled) { - setForceNextPinnedInHeader(undefined); - setLoadingPinnedId(undefined); - } - - const forceNextPinnedInHeader = getForceNextPinnedInHeader(); - - const currentViewportPinnedIds = viewportPinnedIdsRef.current; - - // Unmounting the Message component will fire this action, and if we've already marked the pin as - // outside the viewport, we don't need to do anything - if (isUnmount - && viewportPinnedIdsToAdd.length === 0 && viewportPinnedIdsToRemove.length === 1 - && !currentViewportPinnedIds?.includes(viewportPinnedIdsToRemove[0])) { - return; - } - - const newPinnedViewportIds = unique( - (currentViewportPinnedIds?.filter((id) => !viewportPinnedIdsToRemove.includes(id)) || []) + viewportPinnedIds = unique( + (viewportPinnedIds?.filter((id) => !viewportPinnedIdsToRemove.includes(id)) ?? []) .concat(viewportPinnedIdsToAdd), ); - viewportPinnedIdsRef.current = newPinnedViewportIds; + // Sometimes this callback is called after focus has been reset in global, so we leverage `lastFocusedId` + const focusedMessageId = selectFocusedMessageId(getGlobal(), chatId) || lastFocusedId; - const focusedMessageId = selectFocusedMessageId(global, chatId); - // Focused to some non-pinned message - if (!newPinnedViewportIds.length && isUnmount && focusedMessageId && !pinnedIds.includes(focusedMessageId)) { - const firstPinnedIdAfterFocused = pinnedIds.find((id) => id < focusedMessageId); - if (firstPinnedIdAfterFocused) { - const newIndex = pinnedIds.indexOf(firstPinnedIdAfterFocused); - setCurrentPinnedIndexes({ - ...getCurrentPinnedIndexes(), - [key]: newIndex, - }); - } + if (lastFocusedId && viewportPinnedIds.includes(lastFocusedId)) { + lastFocusedId = undefined; } - if (forceNextPinnedInHeader || isUnmount) { + if (focusedMessageId) { + const pinnedIndexAboveFocused = pinnedIds.findIndex((id) => id < focusedMessageId); + const newIndex = pinnedIndexAboveFocused !== -1 ? pinnedIndexAboveFocused : 0; + + setPinnedIndexByKey({ + ...getPinnedIndexByKey(), + [key]: newIndex, + }); + } else if (viewportPinnedIds.length) { + const maxViewportPinnedId = Math.max(...viewportPinnedIds); + const newIndex = pinnedIds.indexOf(maxViewportPinnedId); + + setPinnedIndexByKey({ + ...getPinnedIndexByKey(), + [key]: newIndex, + }); + } + }); + + const handleFocusPinnedMessage = useLastCallback((messageId: number) => { + // Focusing on a post in comments + if (!chatId || !threadId || !pinnedIds?.length) { return; } - const maxId = Math.max(...newPinnedViewportIds); - const maxIdIndex = pinnedIds.findIndex((id) => id === maxId); - const delta = isReversed ? 0 : 1; - const newIndex = newPinnedViewportIds.length ? maxIdIndex : ( - currentViewportPinnedIds?.length - ? clamp(pinnedIds.indexOf(currentViewportPinnedIds[0]) + delta, 0, pinnedIds.length - 1) - : 0 - ); - - setCurrentPinnedIndexes({ - ...getCurrentPinnedIndexes(), - [key]: newIndex, - }); - }); - - const onFocusPinnedMessage = useLastCallback((messageId: number): boolean => { - if (!chatId || !threadId || !key || getLoadingPinnedId()) return false; + lastFocusedId = messageId; const global = getGlobal(); - if (!pinnedIds?.length) { - // Focusing on a post in comments - return topMessageId === messageId; - } - - const index = pinnedIds.indexOf(messageId); - const newPinnedIndex = cycleRestrict(pinnedIds.length, index + 1); - setForceNextPinnedInHeader(true); - const listedIds = selectListedIds(global, chatId, threadId); const isMessageLoaded = listedIds?.includes(messageId) || selectOutlyingListByMessageId(global, chatId, threadId, messageId); + const currentIndex = pinnedIds.indexOf(messageId); + const newIndex = cycleRestrict(pinnedIds.length, currentIndex + 1); + if (isMessageLoaded) { - setCurrentPinnedIndexes({ - ...getCurrentPinnedIndexes(), - [key]: newPinnedIndex, + setPinnedIndexByKey({ + ...getPinnedIndexByKey(), + [key!]: newIndex, }); - return true; } else { - setLoadingPinnedId(pinnedIds[newPinnedIndex]); - return true; + setLoadingPinnedId(pinnedIds[newIndex]); } }); return { - onIntersectionChanged, - onFocusPinnedMessage, - getCurrentPinnedIndexes, + handleIntersectPinnedMessage, + handleFocusPinnedMessage, + getCurrentPinnedIndex, getLoadingPinnedId, - getForceNextPinnedInHeader, }; } diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 9a8f14ba1..b9e8be02e 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -1,7 +1,13 @@ import type { FC } from '../../../lib/teact/teact'; import React, { beginHeavyAnimation, - memo, useCallback, useEffect, useMemo, useRef, useState, + memo, + useCallback, + useEffect, + useMemo, + useRef, + useState, + useUnmountCleanup, } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; @@ -27,7 +33,7 @@ import type { FocusDirection, IAlbum, ISettings, ScrollTargetPosition, ThreadId, } from '../../../types'; import type { Signal } from '../../../util/signals'; -import type { PinnedIntersectionChangedCallback } from '../hooks/usePinnedMessage'; +import type { OnIntersectPinnedMessage } from '../hooks/usePinnedMessage'; import { MAIN_THREAD_ID } from '../../../api/types'; import { AudioOrigin } from '../../../types'; @@ -206,7 +212,7 @@ type OwnProps = isJustAdded: boolean; memoFirstUnreadIdRef: { current: number | undefined }; getIsMessageListReady: Signal; - onPinnedIntersectionChange: PinnedIntersectionChangedCallback; + onIntersectPinnedMessage: OnIntersectPinnedMessage; } & MessagePositionProperties; @@ -408,7 +414,7 @@ const Message: FC = ({ canTranscribeVoice, viaBusinessBot, effect, - onPinnedIntersectionChange, + onIntersectPinnedMessage, }) => { const { toggleMessageSelection, @@ -479,14 +485,12 @@ const Message: FC = ({ id: messageId, chatId, forwardInfo, viaBotId, isTranscriptionError, factCheck, } = message; - useEffect(() => { - if (!isPinned) return undefined; - const id = album ? album.mainMessage.id : messageId; - - return () => { - onPinnedIntersectionChange({ viewportPinnedIdsToRemove: [id], isUnmount: true }); - }; - }, [album, isPinned, messageId, onPinnedIntersectionChange]); + useUnmountCleanup(() => { + if (message.isPinned) { + const id = album ? album.mainMessage.id : messageId; + onIntersectPinnedMessage({ viewportPinnedIdsToRemove: [id] }); + } + }); const isLocal = isMessageLocal(message); const isOwn = isOwnMessage(message); @@ -1050,7 +1054,7 @@ const Message: FC = ({ noMediaCorners && 'no-media-corners', ); const hasCustomAppendix = isLastInGroup - && (!hasText || (isInvertedMedia && !hasFactCheck && !hasReactions)) && !asForwarded && !withCommentButton; + && (!hasText || (isInvertedMedia && !hasFactCheck && !hasReactions)) && !asForwarded && !withCommentButton; const textContentClass = buildClassName( 'text-content', 'clearfix',