Header Pinned Message: Refactor and fix getting stuck on the same message
This commit is contained in:
parent
cb6c1faa86
commit
bceec156b5
@ -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(
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user