Message List: Simplify pinned navigation check (#6649)
This commit is contained in:
parent
0142b6ac55
commit
6908e40e0f
@ -135,7 +135,16 @@ const MessageListContent = ({
|
||||
observeIntersectionForReading,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
} = useMessageObservers(type, containerRef, memoFirstUnreadIdRef, onIntersectPinnedMessage, chatId, isQuickPreview);
|
||||
onMessageUnmount,
|
||||
} = useMessageObservers({
|
||||
type,
|
||||
containerRef,
|
||||
memoFirstUnreadIdRef,
|
||||
chatId,
|
||||
threadId,
|
||||
isQuickPreview,
|
||||
onIntersectPinnedMessage,
|
||||
});
|
||||
|
||||
const {
|
||||
withHistoryTriggers,
|
||||
@ -304,7 +313,7 @@ const MessageListContent = ({
|
||||
isJustAdded={isLastInList && isNewMessage}
|
||||
isLastInList={isLastInList}
|
||||
getIsMessageListReady={getIsReady}
|
||||
onIntersectPinnedMessage={onIntersectPinnedMessage}
|
||||
onMessageUnmount={onMessageUnmount}
|
||||
/>,
|
||||
]);
|
||||
}
|
||||
@ -375,8 +384,8 @@ const MessageListContent = ({
|
||||
isLastInDocumentGroup={position.isLastInDocumentGroup}
|
||||
isLastInList={position.isLastInList}
|
||||
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
|
||||
onIntersectPinnedMessage={onIntersectPinnedMessage}
|
||||
getIsMessageListReady={getIsReady}
|
||||
onMessageUnmount={onMessageUnmount}
|
||||
/>,
|
||||
]);
|
||||
}).flat();
|
||||
|
||||
@ -1,26 +1,37 @@
|
||||
import type { ElementRef } from '../../../lib/teact/teact';
|
||||
import { type ElementRef, useEffect, useRef } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { MessageListType } from '../../../types';
|
||||
import type { MessageListType, ThreadId } from '../../../types';
|
||||
import type { OnIntersectPinnedMessage } from './usePinnedMessage';
|
||||
|
||||
import { IS_ANDROID } from '../../../util/browser/windowEnvironment';
|
||||
import { unique } from '../../../util/iteratees';
|
||||
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useBackgroundMode, { isBackgroundModeActive } from '../../../hooks/window/useBackgroundMode';
|
||||
|
||||
const INTERSECTION_THROTTLE_FOR_READING = 150;
|
||||
const INTERSECTION_THROTTLE_FOR_MEDIA = IS_ANDROID ? 1000 : 350;
|
||||
|
||||
export default function useMessageObservers(
|
||||
type: MessageListType,
|
||||
containerRef: ElementRef<HTMLDivElement>,
|
||||
memoFirstUnreadIdRef: { current: number | undefined },
|
||||
onIntersectPinnedMessage: OnIntersectPinnedMessage | undefined,
|
||||
chatId: string,
|
||||
isQuickPreview?: boolean,
|
||||
) {
|
||||
export default function useMessageObservers({
|
||||
type,
|
||||
containerRef,
|
||||
memoFirstUnreadIdRef,
|
||||
chatId,
|
||||
threadId,
|
||||
isQuickPreview,
|
||||
onIntersectPinnedMessage,
|
||||
}: {
|
||||
containerRef: ElementRef<HTMLDivElement>;
|
||||
memoFirstUnreadIdRef: { current: number | undefined };
|
||||
chatId: string;
|
||||
threadId: ThreadId;
|
||||
type: MessageListType;
|
||||
isQuickPreview?: boolean;
|
||||
onIntersectPinnedMessage: OnIntersectPinnedMessage | undefined;
|
||||
}) {
|
||||
const {
|
||||
markMessageListRead, markMentionsRead, animateUnreadReaction,
|
||||
scheduleForViewsIncrement,
|
||||
@ -29,11 +40,18 @@ export default function useMessageObservers(
|
||||
const { isMobile } = useAppLayout();
|
||||
const INTERSECTION_MARGIN_FOR_LOADING = isMobile ? 300 : 500;
|
||||
|
||||
const visibleViewportIdsRef = useRef<number[]>([]);
|
||||
useEffect(() => {
|
||||
visibleViewportIdsRef.current = [];
|
||||
}, [threadId, chatId, type]);
|
||||
|
||||
// Note: Targets bottom marker, not the message itself
|
||||
const {
|
||||
observe: observeIntersectionForReading, freeze: freezeForReading, unfreeze: unfreezeForReading,
|
||||
} = useIntersectionObserver({
|
||||
rootRef: containerRef,
|
||||
throttleMs: INTERSECTION_THROTTLE_FOR_READING,
|
||||
threshold: 0,
|
||||
// `memoFirstUnreadIdRef` is set after the first render, firing callback before that can skip some entries, like the last message
|
||||
shouldSkipFirst: true,
|
||||
}, (entries) => {
|
||||
@ -44,10 +62,12 @@ export default function useMessageObservers(
|
||||
let maxId = 0;
|
||||
const mentionIds: number[] = [];
|
||||
const reactionIds: number[] = [];
|
||||
const viewportPinnedIdsToAdd: number[] = [];
|
||||
const viewportPinnedIdsToRemove: number[] = [];
|
||||
const scheduledToUpdateViews: number[] = [];
|
||||
|
||||
const currentVisibleViewportIds = visibleViewportIdsRef.current;
|
||||
const hiddenViewportIds = new Set<number>();
|
||||
const newVisibleViewportIds: number[] = [];
|
||||
|
||||
entries.forEach((entry) => {
|
||||
const { isIntersecting, target } = entry;
|
||||
|
||||
@ -57,12 +77,12 @@ export default function useMessageObservers(
|
||||
const albumMainId = dataset.albumMainId ? Number(dataset.albumMainId) : undefined;
|
||||
|
||||
if (!isIntersecting) {
|
||||
if (dataset.isPinned) {
|
||||
viewportPinnedIdsToRemove.push(albumMainId || messageId);
|
||||
}
|
||||
hiddenViewportIds.add(messageId);
|
||||
return;
|
||||
}
|
||||
|
||||
newVisibleViewportIds.push(messageId);
|
||||
|
||||
if (messageId > maxId) {
|
||||
maxId = messageId;
|
||||
}
|
||||
@ -75,15 +95,15 @@ export default function useMessageObservers(
|
||||
reactionIds.push(messageId);
|
||||
}
|
||||
|
||||
if (dataset.isPinned) {
|
||||
viewportPinnedIdsToAdd.push(albumMainId || messageId);
|
||||
}
|
||||
|
||||
if (shouldUpdateViews) {
|
||||
scheduledToUpdateViews.push(albumMainId || messageId);
|
||||
}
|
||||
});
|
||||
|
||||
visibleViewportIdsRef.current = unique(currentVisibleViewportIds.concat(newVisibleViewportIds))
|
||||
.filter((id) => !hiddenViewportIds.has(id))
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
if (!isQuickPreview) {
|
||||
if (memoFirstUnreadIdRef.current && maxId && maxId >= memoFirstUnreadIdRef.current) {
|
||||
markMessageListRead({ maxId });
|
||||
@ -102,8 +122,8 @@ export default function useMessageObservers(
|
||||
animateUnreadReaction({ chatId, messageIds: reactionIds });
|
||||
}
|
||||
|
||||
if (viewportPinnedIdsToAdd.length || viewportPinnedIdsToRemove.length) {
|
||||
onIntersectPinnedMessage?.({ viewportPinnedIdsToAdd, viewportPinnedIdsToRemove });
|
||||
if (visibleViewportIdsRef.current.length) {
|
||||
onIntersectPinnedMessage?.({ firstViewportId: visibleViewportIdsRef.current[0] });
|
||||
}
|
||||
});
|
||||
|
||||
@ -122,9 +142,14 @@ export default function useMessageObservers(
|
||||
throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA,
|
||||
});
|
||||
|
||||
const onMessageUnmount = useLastCallback((messageId: number) => {
|
||||
visibleViewportIdsRef.current = visibleViewportIdsRef.current.filter((id) => id !== messageId);
|
||||
});
|
||||
|
||||
return {
|
||||
observeIntersectionForReading,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
onMessageUnmount,
|
||||
};
|
||||
}
|
||||
|
||||
@ -3,22 +3,17 @@ import { getGlobal } from '../../../global';
|
||||
|
||||
import type { ThreadId } from '../../../types';
|
||||
|
||||
import { selectFocusedMessageId, selectListedIds, selectOutlyingListByMessageId } from '../../../global/selectors';
|
||||
import { selectListedIds, selectOutlyingListByMessageId } from '../../../global/selectors';
|
||||
import cycleRestrict from '../../../util/cycleRestrict';
|
||||
import { unique } from '../../../util/iteratees';
|
||||
|
||||
import useDerivedSignal from '../../../hooks/useDerivedSignal';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
export type OnIntersectPinnedMessage = (params: {
|
||||
viewportPinnedIdsToAdd?: number[];
|
||||
viewportPinnedIdsToRemove?: number[];
|
||||
firstViewportId?: number;
|
||||
shouldCancelWaiting?: boolean;
|
||||
}) => void;
|
||||
|
||||
let viewportPinnedIds: number[] | undefined;
|
||||
let lastFocusedId: number | undefined;
|
||||
|
||||
export default function usePinnedMessage(
|
||||
chatId?: string, threadId?: ThreadId, pinnedIds?: number[],
|
||||
) {
|
||||
@ -32,10 +27,9 @@ export default function usePinnedMessage(
|
||||
|
||||
// Reset when switching chat
|
||||
useEffect(() => {
|
||||
viewportPinnedIds = undefined;
|
||||
setLoadingPinnedId(undefined);
|
||||
}, [
|
||||
chatId, setPinnedIndexByKey, setLoadingPinnedId, threadId,
|
||||
chatId, threadId, setPinnedIndexByKey, setLoadingPinnedId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -51,14 +45,12 @@ export default function usePinnedMessage(
|
||||
}, [getPinnedIndexByKey, key, pinnedIds?.length, setPinnedIndexByKey]);
|
||||
|
||||
const handleIntersectPinnedMessage: OnIntersectPinnedMessage = useLastCallback(({
|
||||
viewportPinnedIdsToAdd = [],
|
||||
viewportPinnedIdsToRemove = [],
|
||||
firstViewportId,
|
||||
shouldCancelWaiting,
|
||||
}) => {
|
||||
if (!chatId || !threadId || !key || !pinnedIds?.length) return;
|
||||
|
||||
if (shouldCancelWaiting) {
|
||||
lastFocusedId = undefined;
|
||||
setLoadingPinnedId(undefined);
|
||||
return;
|
||||
}
|
||||
@ -71,36 +63,22 @@ export default function usePinnedMessage(
|
||||
[key]: clampIndex(newPinnedIndex),
|
||||
});
|
||||
setLoadingPinnedId(undefined);
|
||||
|
||||
// We're still scrolling, prevent updating the index
|
||||
if (loadingPinnedId < (firstViewportId || 0)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
viewportPinnedIds = unique(
|
||||
(viewportPinnedIds?.filter((id) => !viewportPinnedIdsToRemove.includes(id)) ?? [])
|
||||
.concat(viewportPinnedIdsToAdd),
|
||||
);
|
||||
|
||||
// Sometimes this callback is called after focus has been reset in global, so we leverage `lastFocusedId`
|
||||
const focusedMessageId = selectFocusedMessageId(getGlobal(), chatId) || lastFocusedId;
|
||||
|
||||
if (lastFocusedId && viewportPinnedIds.includes(lastFocusedId)) {
|
||||
lastFocusedId = undefined;
|
||||
let newIndex = pinnedIds.findIndex((id) => id < (firstViewportId || 0));
|
||||
if (newIndex === -1) {
|
||||
newIndex = 0; // Pinned are sorted from newest to oldest
|
||||
}
|
||||
|
||||
if (focusedMessageId) {
|
||||
const pinnedIndexAboveFocused = pinnedIds.findIndex((id) => id < focusedMessageId);
|
||||
|
||||
setPinnedIndexByKey({
|
||||
...getPinnedIndexByKey(),
|
||||
[key]: clampIndex(pinnedIndexAboveFocused),
|
||||
});
|
||||
} else if (viewportPinnedIds.length) {
|
||||
const maxViewportPinnedId = Math.max(...viewportPinnedIds);
|
||||
const newIndex = pinnedIds.indexOf(maxViewportPinnedId);
|
||||
|
||||
setPinnedIndexByKey({
|
||||
...getPinnedIndexByKey(),
|
||||
[key]: clampIndex(newIndex),
|
||||
});
|
||||
}
|
||||
setPinnedIndexByKey({
|
||||
...getPinnedIndexByKey(),
|
||||
[key]: clampIndex(newIndex),
|
||||
});
|
||||
});
|
||||
|
||||
const handleFocusPinnedMessage = useLastCallback((messageId: number) => {
|
||||
@ -109,8 +87,6 @@ export default function usePinnedMessage(
|
||||
return;
|
||||
}
|
||||
|
||||
lastFocusedId = messageId;
|
||||
|
||||
const global = getGlobal();
|
||||
const listedIds = selectListedIds(global, chatId, threadId);
|
||||
const isMessageLoaded = listedIds?.includes(messageId)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
memo, useEffect, useMemo, useRef, useUnmountCleanup,
|
||||
memo, useEffect, useMemo, useRef,
|
||||
useUnmountCleanup,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
@ -51,7 +52,6 @@ import { type ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionOb
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
import { type OnIntersectPinnedMessage } from '../hooks/usePinnedMessage';
|
||||
import useFluidBackgroundFilter from './hooks/useFluidBackgroundFilter';
|
||||
import useFocusMessageListElement from './hooks/useFocusMessageListElement';
|
||||
|
||||
@ -82,10 +82,10 @@ type OwnProps = {
|
||||
isLastInList?: boolean;
|
||||
memoFirstUnreadIdRef?: { current: number | undefined };
|
||||
getIsMessageListReady?: Signal<boolean>;
|
||||
onIntersectPinnedMessage?: OnIntersectPinnedMessage;
|
||||
observeIntersectionForBottom?: ObserveFn;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
onMessageUnmount?: (messageId: number) => void;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -138,10 +138,10 @@ const ActionMessage = ({
|
||||
isResizingContainer,
|
||||
scrollTargetPosition,
|
||||
isAccountFrozen,
|
||||
onIntersectPinnedMessage,
|
||||
observeIntersectionForBottom,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
onMessageUnmount,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
requestConfetti,
|
||||
@ -249,12 +249,6 @@ const ActionMessage = ({
|
||||
scrollTargetPosition,
|
||||
});
|
||||
|
||||
useUnmountCleanup(() => {
|
||||
if (message.isPinned) {
|
||||
onIntersectPinnedMessage?.({ viewportPinnedIdsToRemove: [message.id] });
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuAnchor,
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
@ -291,6 +285,10 @@ const ActionMessage = ({
|
||||
ref,
|
||||
});
|
||||
|
||||
useUnmountCleanup(() => {
|
||||
onMessageUnmount?.(id);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const bottomMarker = ref.current;
|
||||
if (!bottomMarker || !isElementInViewport(bottomMarker)) return;
|
||||
|
||||
@ -42,7 +42,6 @@ import type {
|
||||
ThreadReadState,
|
||||
} from '../../../types';
|
||||
import type { Signal } from '../../../util/signals';
|
||||
import type { OnIntersectPinnedMessage } from '../hooks/usePinnedMessage';
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
import { AudioOrigin } from '../../../types';
|
||||
|
||||
@ -238,7 +237,7 @@ type OwnProps = {
|
||||
observeIntersectionForBottom?: ObserveFn;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
onIntersectPinnedMessage?: OnIntersectPinnedMessage;
|
||||
onMessageUnmount?: (messageId: number) => void;
|
||||
} & MessagePositionProperties;
|
||||
|
||||
type StateProps = {
|
||||
@ -353,9 +352,6 @@ const MAX_REASON_LENGTH = 200;
|
||||
|
||||
const Message = ({
|
||||
message,
|
||||
observeIntersectionForBottom,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
album,
|
||||
noAvatars,
|
||||
withAvatar,
|
||||
@ -462,7 +458,10 @@ const Message = ({
|
||||
minFutureTime,
|
||||
webPage,
|
||||
summary,
|
||||
onIntersectPinnedMessage,
|
||||
observeIntersectionForBottom,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
onMessageUnmount,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
toggleMessageSelection,
|
||||
@ -536,19 +535,16 @@ const Message = ({
|
||||
className: false,
|
||||
});
|
||||
|
||||
useUnmountCleanup(() => {
|
||||
onMessageUnmount?.(messageId);
|
||||
});
|
||||
|
||||
const {
|
||||
id: messageId, chatId, forwardInfo, viaBotId, isTranscriptionError, factCheck,
|
||||
isTypingDraft,
|
||||
} = message;
|
||||
const hasSummary = Boolean(message.summaryLanguageCode);
|
||||
|
||||
useUnmountCleanup(() => {
|
||||
if (message.isPinned) {
|
||||
const id = album ? album.mainMessage.id : messageId;
|
||||
onIntersectPinnedMessage?.({ viewportPinnedIdsToRemove: [id] });
|
||||
}
|
||||
});
|
||||
|
||||
const isLocal = isMessageLocal(message);
|
||||
const isOwn = isOwnMessage(message);
|
||||
const isScheduled = messageListType === 'scheduled' || message.isScheduled;
|
||||
|
||||
@ -364,6 +364,8 @@
|
||||
}
|
||||
|
||||
&.tiny {
|
||||
--emoji-size: 1rem;
|
||||
|
||||
height: 2.25rem;
|
||||
padding: 0.4375rem;
|
||||
border-radius: var(--border-radius-button-tiny);
|
||||
@ -380,6 +382,8 @@
|
||||
}
|
||||
|
||||
&.pill {
|
||||
--emoji-size: 1.25rem;
|
||||
|
||||
height: 1.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 1rem;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user