Header Pinned Message: Refactor and fix getting stuck on the same message

This commit is contained in:
Alexander Zinchuk 2024-09-24 14:48:35 +02:00
parent cb6c1faa86
commit bceec156b5
9 changed files with 152 additions and 197 deletions

View File

@ -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<OwnProps & StateProps> = ({
observeIntersectionForReading,
observeIntersectionForLoading,
observeIntersectionForPlaying,
onPinnedIntersectionChange,
onIntersectPinnedMessage,
}) => {
const {
openPremiumModal, requestConfetti, checkGiftCode, getReceipt, openStarsTransactionFromGift,
@ -121,13 +121,11 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
);
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<OwnProps & StateProps> = ({
{oldLang(isUnclaimed ? 'BoostingUnclaimedPrize' : 'BoostingCongratulations')}
</strong>
<span className="action-message-subtitle">
{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'],
)}
</span>
<span className="action-message-subtitle">
{renderText(oldLang(

View File

@ -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<HTMLDivElement>) => void;
onAllPinnedClick?: () => void;
isLoading?: boolean;
getLoadingPinnedId: Signal<number | undefined>;
isFullWidth?: boolean;
};
const HeaderPinnedMessage: FC<OwnProps> = ({
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<OwnProps> = ({
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;

View File

@ -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<boolean | undefined>;
onIntersectPinnedMessage: OnIntersectPinnedMessage;
isContactRequirePremium?: boolean;
};
@ -185,11 +178,10 @@ const MessageList: FC<OwnProps & StateProps> = ({
noMessageSendingAnimation,
isServiceNotificationsChat,
currentUserId,
getForceNextPinnedInHeader,
isContactRequirePremium,
areAdsEnabled,
channelJoinInfo,
onPinnedIntersectionChange,
onIntersectPinnedMessage,
onScrollDownToggle,
onNotchToggle,
}) => {
@ -418,9 +410,10 @@ const MessageList: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current}
onScrollDownToggle={onScrollDownToggle}
onNotchToggle={onNotchToggle}
onPinnedIntersectionChange={onPinnedIntersectionChange}
onIntersectPinnedMessage={onIntersectPinnedMessage}
/>
) : (
<Loading color="white" backgroundColor="dark" />

View File

@ -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<OwnProps> = ({
isSavedDialog,
onScrollDownToggle,
onNotchToggle,
onPinnedIntersectionChange,
onIntersectPinnedMessage,
}) => {
const { openHistoryCalendar } = getActions();
@ -107,7 +107,7 @@ const MessageListContent: FC<OwnProps> = ({
observeIntersectionForReading,
observeIntersectionForLoading,
observeIntersectionForPlaying,
} = useMessageObservers(type, containerRef, memoFirstUnreadIdRef, onPinnedIntersectionChange, chatId);
} = useMessageObservers(type, containerRef, memoFirstUnreadIdRef, onIntersectPinnedMessage, chatId);
const {
withHistoryTriggers,
@ -180,7 +180,7 @@ const MessageListContent: FC<OwnProps> = ({
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
isJustAdded={isLastInList && isNewMessage}
isLastInList={isLastInList}
onPinnedIntersectionChange={onPinnedIntersectionChange}
onIntersectPinnedMessage={onIntersectPinnedMessage}
/>,
]);
}
@ -249,7 +249,7 @@ const MessageListContent: FC<OwnProps> = ({
isLastInDocumentGroup={position.isLastInDocumentGroup}
isLastInList={position.isLastInList}
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
onPinnedIntersectionChange={onPinnedIntersectionChange}
onIntersectPinnedMessage={onIntersectPinnedMessage}
getIsMessageListReady={getIsReady}
/>,
message.id === threadId && (

View File

@ -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}
/>
<Transition
name={shouldSkipHistoryAnimations ? 'none' : withInterfaceAnimations ? 'slide' : 'fade'}
@ -563,8 +560,7 @@ function MiddleColumn({
isContactRequirePremium={isContactRequirePremium}
withBottomShift={withMessageListBottomShift}
withDefaultBg={Boolean(!customBackground && !backgroundColor)}
onPinnedIntersectionChange={renderingOnPinnedIntersectionChange!}
getForceNextPinnedInHeader={getForceNextPinnedInHeader}
onIntersectPinnedMessage={renderingHandleIntersectPinnedMessage!}
/>
<div className={footerClassName}>
{renderingCanPost && (
@ -812,7 +808,6 @@ export default memo(withGlobal<OwnProps>(
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<OwnProps>(
shouldSendJoinRequest,
shouldLoadFullChat,
pinnedIds,
topMessageId,
canUnpin,
canUnblock,
isSavedDialog,

View File

@ -91,9 +91,9 @@ type OwnProps = {
isComments?: boolean;
isReady?: boolean;
isMobile?: boolean;
getCurrentPinnedIndexes: Signal<Record<string, number>>;
getCurrentPinnedIndex: Signal<number>;
getLoadingPinnedId: Signal<number | undefined>;
onFocusPinnedMessage: (messageId: number) => boolean;
onFocusPinnedMessage: (messageId: number) => void;
};
type StateProps = {
@ -147,7 +147,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
isSyncing,
isSynced,
isFetchingDifference,
getCurrentPinnedIndexes,
getCurrentPinnedIndex,
getLoadingPinnedId,
emojiStatusSticker,
isSavedDialog,
@ -173,9 +173,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
? 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<OwnProps & StateProps> = ({
onUnpinMessage={renderingCanUnpin ? handleUnpinMessage : undefined}
onClick={handlePinnedMessageClick}
onAllPinnedClick={handleAllPinnedClick}
isLoading={waitingForPinnedId !== undefined}
getLoadingPinnedId={getLoadingPinnedId}
isFullWidth={isPinnedMessagesFullWidth}
/>
)}

View File

@ -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<HTMLDivElement>,
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) {

View File

@ -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<Record<string, number>>({});
const [getForceNextPinnedInHeader, setForceNextPinnedInHeader] = useSignal<boolean | undefined>();
const viewportPinnedIdsRef = useRef<number[] | undefined>();
const [getPinnedIndexByKey, setPinnedIndexByKey] = useSignal<Record<string, number>>({});
const [getLoadingPinnedId, setLoadingPinnedId] = useSignal<number | undefined>();
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,
};
}

View File

@ -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<boolean>;
onPinnedIntersectionChange: PinnedIntersectionChangedCallback;
onIntersectPinnedMessage: OnIntersectPinnedMessage;
}
& MessagePositionProperties;
@ -408,7 +414,7 @@ const Message: FC<OwnProps & StateProps> = ({
canTranscribeVoice,
viaBusinessBot,
effect,
onPinnedIntersectionChange,
onIntersectPinnedMessage,
}) => {
const {
toggleMessageSelection,
@ -479,14 +485,12 @@ const Message: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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',