Message List: Fix scroll to bottom ignoring local messages (#6418)

This commit is contained in:
zubiden 2025-11-06 11:36:38 +01:00 committed by Alexander Zinchuk
parent cb6f814c36
commit b497a3c0ea
17 changed files with 211 additions and 179 deletions

View File

@ -181,7 +181,6 @@ const Chat: FC<OwnProps & StateProps> = ({
openChat,
openSavedDialog,
toggleChatInfo,
focusLastMessage,
focusMessage,
loadTopics,
openForumPanel,
@ -191,6 +190,7 @@ const Chat: FC<OwnProps & StateProps> = ({
openFrozenAccountModal,
updateChatMutedState,
openQuickPreview,
scrollMessageListToBottom,
} = getActions();
const { isMobile } = useAppLayout();
@ -293,7 +293,7 @@ const Chat: FC<OwnProps & StateProps> = ({
openChat({ id: chatId, noForumTopicPanel, shouldReplaceHistory: true }, { forceOnHeavyAnimation: true });
if (isSelected && canScrollDown) {
focusLastMessage();
scrollMessageListToBottom();
}
});

View File

@ -102,7 +102,7 @@ const Topic: FC<OwnProps & StateProps> = ({
const {
openThread,
deleteTopic,
focusLastMessage,
scrollMessageListToBottom,
setViewForumAsMessages,
updateTopicMutedState,
openQuickPreview,
@ -168,7 +168,7 @@ const Topic: FC<OwnProps & StateProps> = ({
setViewForumAsMessages({ chatId, isEnabled: false });
if (canScrollDown) {
focusLastMessage();
scrollMessageListToBottom();
}
});

View File

@ -6,7 +6,6 @@ import type { MessageListType, ThreadId } from '../../types';
import { MAIN_THREAD_ID } from '../../api/types';
import { selectChat, selectCurrentMessageList, selectCurrentMiddleSearch } from '../../global/selectors';
import animateScroll from '../../util/animateScroll';
import buildClassName from '../../util/buildClassName';
import useLastCallback from '../../hooks/useLastCallback';
@ -32,8 +31,6 @@ type StateProps = {
mentionsCount?: number;
};
const FOCUS_MARGIN = 20;
const FloatingActionButtons: FC<OwnProps & StateProps> = ({
withScrollDown,
canPost,
@ -49,7 +46,7 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
}) => {
const {
focusNextReply, focusNextReaction, focusNextMention, fetchUnreadReactions,
readAllMentions, readAllReactions, fetchUnreadMentions,
readAllMentions, readAllReactions, fetchUnreadMentions, scrollMessageListToBottom,
} = getActions();
const elementRef = useRef<HTMLDivElement>();
@ -99,21 +96,7 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
if (messageListType === 'thread') {
focusNextReply();
} else {
const messagesContainer = elementRef.current!.parentElement!.querySelector<HTMLDivElement>(
'.Transition_slide-active > .MessageList',
)!;
const messageElements = messagesContainer.querySelectorAll<HTMLDivElement>('.message-list-item');
const lastMessageElement = messageElements[messageElements.length - 1];
if (!lastMessageElement) {
return;
}
animateScroll({
container: messagesContainer,
element: lastMessageElement,
position: 'end',
margin: FOCUS_MARGIN,
});
scrollMessageListToBottom();
}
});

View File

@ -321,6 +321,11 @@
}
}
.list-bottom-marker.with-sponsored {
position: relative;
top: -1rem; // Prevent overlapping with the sponsored message
}
@media (pointer: coarse) {
user-select: none;

View File

@ -61,6 +61,7 @@ import { isLocalMessageId } from '../../util/keys/messageKey';
import resetScroll from '../../util/resetScroll';
import { debounce, onTickEnd } from '../../util/schedulers';
import getOffsetToContainer from '../../util/visibility/getOffsetToContainer';
import { REM } from '../common/helpers/mediaDimensions';
import { groupMessages } from './helpers/groupMessages';
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
@ -76,7 +77,7 @@ import useContainerHeight from './hooks/useContainerHeight';
import useStickyDates from './hooks/useStickyDates';
import Loading from '../ui/Loading';
import Transition from '../ui/Transition.tsx';
import Transition from '../ui/Transition';
import ContactGreeting from './ContactGreeting';
import MessageListAccountInfo from './MessageListAccountInfo';
import MessageListContent from './MessageListContent';
@ -144,6 +145,7 @@ type StateProps = {
translationLanguage?: string;
shouldAutoTranslate?: boolean;
isActive?: boolean;
shouldScrollToBottom?: boolean;
};
enum Content {
@ -168,7 +170,7 @@ const BOTTOM_THRESHOLD = 50;
const UNREAD_DIVIDER_TOP = 10;
const SCROLL_DEBOUNCE = 200;
const MESSAGE_ANIMATION_DURATION = 500;
const BOTTOM_FOCUS_MARGIN = 20;
const BOTTOM_FOCUS_MARGIN = 0.5 * REM;
const SELECT_MODE_ANIMATION_DURATION = 200;
const UNREAD_DIVIDER_CLASS = 'unread-divider';
@ -186,6 +188,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
canPost,
isSynced,
isActive,
shouldScrollToBottom,
// eslint-disable-next-line @typescript-eslint/no-shadow
isChatMonoforum,
isReady,
@ -622,11 +625,11 @@ const MessageList: FC<OwnProps & StateProps> = ({
if (wasMessageAdded && isAtBottom && !isAlreadyFocusing) {
// Break out of `forceLayout`
requestMeasure(() => {
const shouldScrollToBottom = !isBackgroundModeActive() || !firstUnreadElement;
const isScrollToBottom = !isBackgroundModeActive() || !firstUnreadElement;
animateScroll({
container,
element: shouldScrollToBottom ? lastItemElement : firstUnreadElement,
position: shouldScrollToBottom ? 'end' : 'start',
element: isScrollToBottom ? lastItemElement : firstUnreadElement,
position: isScrollToBottom ? 'end' : 'start',
margin: BOTTOM_FOCUS_MARGIN,
forceDuration: noMessageSendingAnimation ? 0 : undefined,
});
@ -800,10 +803,11 @@ const MessageList: FC<OwnProps & StateProps> = ({
photoChangeDate={photoChangeDate}
noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current}
isQuickPreview={isQuickPreview}
canPost={canPost}
shouldScrollToBottom={shouldScrollToBottom}
onScrollDownToggle={onScrollDownToggle}
onNotchToggle={onNotchToggle}
onIntersectPinnedMessage={onIntersectPinnedMessage}
canPost={canPost}
/>
) : (
<Loading color="white" backgroundColor="dark" />
@ -827,6 +831,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId, type }): Complete<StateProps> => {
const tabState = selectTabState(global);
const currentUserId = global.currentUserId!;
const chat = selectChat(global, chatId);
const userFullInfo = selectUserFullInfo(global, chatId);
@ -883,6 +888,13 @@ export default memo(withGlobal<OwnProps>(
const isActive = currentMessageList && currentMessageList.chatId === chatId
&& currentMessageList.threadId === threadId && currentMessageList.type === type;
const {
chatId: focusedChatId,
threadId: focusedThreadId,
messageId: focusedMessageId,
} = tabState.focusedMessage || {};
const shouldScrollToBottom = focusedChatId === chatId && focusedThreadId === threadId && !focusedMessageId;
return {
isActive,
areAdsEnabled,
@ -925,6 +937,7 @@ export default memo(withGlobal<OwnProps>(
canTranslate,
translationLanguage,
shouldAutoTranslate,
shouldScrollToBottom,
};
},
)(MessageList));

View File

@ -0,0 +1,28 @@
import { memo, useRef } from '@teact';
import buildClassName from '../../util/buildClassName';
import useFocusMessageListElement from './message/hooks/useFocusMessageListElement';
type OwnProps = {
isJustAdded?: boolean;
isFocused?: boolean;
className?: string;
};
const MessageListBottomMarker = ({ isJustAdded, isFocused, className }: OwnProps) => {
const ref = useRef<HTMLDivElement>();
useFocusMessageListElement({
elementRef: ref,
isJustAdded,
isFocused,
noFocusHighlight: true,
});
return (
<div ref={ref} className={buildClassName('list-bottom-marker', className)} />
);
};
export default memo(MessageListBottomMarker);

View File

@ -1,4 +1,4 @@
import type { ElementRef, FC } from '../../lib/teact/teact';
import type { ElementRef } from '../../lib/teact/teact';
import { getIsHeavyAnimating, memo } from '../../lib/teact/teact';
import { getActions, getGlobal } from '../../global';
@ -43,6 +43,7 @@ import Message from './message/Message';
import SenderGroupContainer from './message/SenderGroupContainer';
import SponsoredMessage from './message/SponsoredMessage';
import MessageListAccountInfo from './MessageListAccountInfo';
import MessageListBottomMarker from './MessageListBottomMarker';
import actionMessageStyles from './message/ActionMessage.module.scss';
@ -75,15 +76,16 @@ interface OwnProps {
noAppearanceAnimation: boolean;
isSavedDialog?: boolean;
isQuickPreview?: boolean;
canPost?: boolean;
shouldScrollToBottom?: boolean;
onScrollDownToggle?: BooleanToVoidFunction;
onNotchToggle?: AnyToVoidFunction;
onIntersectPinnedMessage?: OnIntersectPinnedMessage;
canPost?: boolean;
}
const UNREAD_DIVIDER_CLASS = 'unread-divider';
const MessageListContent: FC<OwnProps> = ({
const MessageListContent = ({
canShowAds,
chatId,
threadId,
@ -112,17 +114,19 @@ const MessageListContent: FC<OwnProps> = ({
noAppearanceAnimation,
isSavedDialog,
isQuickPreview,
shouldScrollToBottom,
canPost,
onScrollDownToggle,
onNotchToggle,
onIntersectPinnedMessage,
canPost,
}) => {
}: OwnProps) => {
const { openHistoryCalendar } = getActions();
const getIsHeavyAnimating2 = getIsHeavyAnimating;
const getIsReady = useDerivedSignal(() => isReady && !getIsHeavyAnimating2(), [isReady, getIsHeavyAnimating2]);
const areDatesClickable = !isSavedDialog && !isSchedule;
const shouldRenderSponsoredMessage = canShowAds && isViewportNewest;
const {
observeIntersectionForReading,
@ -135,17 +139,17 @@ const MessageListContent: FC<OwnProps> = ({
backwardsTriggerRef,
forwardsTriggerRef,
fabTriggerRef,
} = useScrollHooks(
} = useScrollHooks({
type,
containerRef,
messageIds,
getContainerHeight,
isViewportNewest,
isUnread,
isReady,
onScrollDownToggle,
onNotchToggle,
isReady,
);
});
const oldLang = useOldLang();
const lang = useLang();
@ -457,7 +461,15 @@ const MessageListContent: FC<OwnProps> = ({
key="fab-trigger"
className="fab-trigger"
/>
{canShowAds && isViewportNewest && (
{isViewportNewest && (
<MessageListBottomMarker
key="bottom-marker"
isJustAdded={isNewMessage}
isFocused={shouldScrollToBottom}
className={shouldRenderSponsoredMessage ? 'with-sponsored' : undefined}
/>
)}
{shouldRenderSponsoredMessage && (
<SponsoredMessage
key={chatId}
chatId={chatId}

View File

@ -21,17 +21,27 @@ const NOTCH_THRESHOLD = 1; // Notch has zero height so we at least need a 1px ma
const CONTAINER_HEIGHT_DEBOUNCE = 200;
const TOOLS_FREEZE_TIMEOUT = 350; // Approximate message sending animation duration
export default function useScrollHooks(
type: MessageListType,
containerRef: ElementRef<HTMLDivElement>,
messageIds: number[],
getContainerHeight: Signal<number | undefined>,
isViewportNewest: boolean,
isUnread: boolean,
onScrollDownToggle: BooleanToVoidFunction | undefined,
onNotchToggle: AnyToVoidFunction | undefined,
isReady: boolean,
) {
export default function useScrollHooks({
type,
containerRef,
messageIds,
getContainerHeight,
isViewportNewest,
isUnread,
isReady,
onScrollDownToggle,
onNotchToggle,
}: {
type: MessageListType;
containerRef: ElementRef<HTMLDivElement>;
messageIds: number[];
getContainerHeight: Signal<number | undefined>;
isViewportNewest: boolean;
isUnread: boolean;
isReady: boolean;
onScrollDownToggle: BooleanToVoidFunction | undefined;
onNotchToggle: AnyToVoidFunction | undefined;
}) {
const { loadViewportMessages } = getActions();
const [loadMoreBackwards, loadMoreForwards] = useMemo(
@ -136,7 +146,18 @@ export default function useScrollHooks(
if (isReady) {
toggleScrollTools();
}
}, [isReady, toggleScrollTools]);
}, [isReady]);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
container.addEventListener('scrollend', toggleScrollTools);
return () => {
container.removeEventListener('scrollend', toggleScrollTools);
};
}, [containerRef]);
const freezeShortly = useLastCallback(() => {
freezeForFab();

View File

@ -44,7 +44,7 @@ import useMessageResizeObserver from '../../../hooks/useResizeMessageObserver';
import useShowTransition from '../../../hooks/useShowTransition';
import { type OnIntersectPinnedMessage } from '../hooks/usePinnedMessage';
import useFluidBackgroundFilter from './hooks/useFluidBackgroundFilter';
import useFocusMessage from './hooks/useFocusMessage';
import useFocusMessageListElement from './hooks/useFocusMessageListElement';
import ActionMessageText from './ActionMessageText';
import ChannelPhoto from './actions/ChannelPhoto';
@ -176,9 +176,8 @@ const ActionMessage = ({
replyMessage,
id,
);
useFocusMessage({
useFocusMessageListElement({
elementRef: ref,
chatId,
isFocused,
focusDirection,
noFocusHighlight,

View File

@ -154,7 +154,7 @@ import useMessageResizeObserver from '../../../hooks/useResizeMessageObserver';
import useShowTransition from '../../../hooks/useShowTransition';
import useTextLanguage from '../../../hooks/useTextLanguage';
import useDetectChatLanguage from './hooks/useDetectChatLanguage';
import useFocusMessage from './hooks/useFocusMessage';
import useFocusMessageListElement from './hooks/useFocusMessageListElement';
import useInnerHandlers from './hooks/useInnerHandlers';
import useMessageTranslation from './hooks/useMessageTranslation';
import useOuterHandlers from './hooks/useOuterHandlers';
@ -462,7 +462,7 @@ const Message = ({
openSuggestedPostApprovalModal,
disableContextMenuHint,
animateUnreadReaction,
focusLastMessage,
focusMessage,
markMentionsRead,
} = getActions();
@ -698,15 +698,24 @@ const Message = ({
requestEffect();
});
const handleFocusSelf = useLastCallback(() => {
focusMessage({
chatId,
threadId,
messageId,
noHighlight: true,
});
});
useEffect(() => {
if (!isLastInList) {
return;
}
if (withVoiceTranscription && transcribedText) {
focusLastMessage();
handleFocusSelf();
}
}, [focusLastMessage, isLastInList, transcribedText, withVoiceTranscription]);
}, [isLastInList, transcribedText, withVoiceTranscription]);
useEffect(() => {
const element = ref.current;
@ -882,9 +891,8 @@ const Message = ({
replyStory,
);
useFocusMessage({
useFocusMessageListElement({
elementRef: ref,
chatId,
isFocused,
focusDirection,
noFocusHighlight,

View File

@ -9,15 +9,16 @@ import {
requestForcedReflow, requestMeasure, requestMutation,
} from '../../../../lib/fasterdom/fasterdom';
import animateScroll from '../../../../util/animateScroll';
import { REM } from '../../../common/helpers/mediaDimensions';
// This is used when the viewport was replaced.
const BOTTOM_FOCUS_OFFSET = 500;
const RELOCATED_FOCUS_OFFSET = SCROLL_MAX_DISTANCE;
const FOCUS_MARGIN = 20;
const FOCUS_MARGIN = 1.25 * REM;
const BOTTOM_FOCUS_MARGIN = 0.5 * REM;
export default function useFocusMessage({
export default function useFocusMessageListElement({
elementRef,
chatId,
isFocused,
focusDirection,
noFocusHighlight,
@ -27,7 +28,6 @@ export default function useFocusMessage({
scrollTargetPosition,
}: {
elementRef: ElementRef<HTMLDivElement>;
chatId: string;
isFocused?: boolean;
focusDirection?: FocusDirection;
noFocusHighlight?: boolean;
@ -43,10 +43,12 @@ export default function useFocusMessage({
isRelocatedRef.current = false;
if (isFocused && elementRef.current) {
const messagesContainer = elementRef.current.closest<HTMLDivElement>('.MessageList')!;
const messagesContainer = elementRef.current.closest<HTMLDivElement>('.MessageList');
if (!messagesContainer) return;
// `noFocusHighlight` is always called with “scroll-to-bottom” buttons
const isToBottom = noFocusHighlight;
const scrollPosition = scrollTargetPosition || isToBottom ? 'end' : 'centerOrTop';
const scrollPosition = scrollTargetPosition || (isToBottom ? 'end' : 'centerOrTop');
const exec = () => {
const maxDistance = focusDirection !== undefined
@ -56,7 +58,7 @@ export default function useFocusMessage({
container: messagesContainer,
element: elementRef.current!,
position: scrollPosition,
margin: FOCUS_MARGIN,
margin: isToBottom ? BOTTOM_FOCUS_MARGIN : FOCUS_MARGIN,
maxDistance,
forceDirection: focusDirection,
forceNormalContainerHeight: isResizingContainer,
@ -85,6 +87,6 @@ export default function useFocusMessage({
}
}
}, [
elementRef, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer, isQuote, scrollTargetPosition,
elementRef, isFocused, focusDirection, noFocusHighlight, isResizingContainer, isQuote, scrollTargetPosition,
]);
}

View File

@ -169,6 +169,7 @@ addActionHandler('loadViewportMessages', (global, actions, payload): ActionRetur
direction = LoadMoreDirection.Around,
isBudgetPreload = false,
shouldForceRender = false,
forceLastSlice = false,
onLoaded,
onError,
tabId = getCurrentTabId(),
@ -199,7 +200,9 @@ addActionHandler('loadViewportMessages', (global, actions, payload): ActionRetur
const listedIds = selectListedIds(global, chatId, threadId);
if (!viewportIds || !viewportIds.length || direction === LoadMoreDirection.Around) {
const offsetId = selectFocusedMessageId(global, chatId, tabId) || selectRealLastReadId(global, chatId, threadId);
const offsetId = !forceLastSlice ? (
selectFocusedMessageId(global, chatId, tabId) || selectRealLastReadId(global, chatId, threadId)
) : undefined;
const isOutlying = Boolean(offsetId && listedIds && !listedIds.includes(offsetId));
const historyIds = (isOutlying
? selectOutlyingListByMessageId(global, chatId, threadId, offsetId!)
@ -222,17 +225,19 @@ addActionHandler('loadViewportMessages', (global, actions, payload): ActionRetur
onLoaded?.();
}
} else {
const offsetId = direction === LoadMoreDirection.Backwards ? viewportIds[0] : viewportIds[viewportIds.length - 1];
const offsetId = !forceLastSlice ? (
direction === LoadMoreDirection.Backwards ? viewportIds[0] : viewportIds[viewportIds.length - 1]
) : undefined;
// Prevent requests with local offsets
if (isLocalMessageId(offsetId)) return;
if (offsetId && isLocalMessageId(offsetId)) return;
// Prevent unnecessary requests in threads
if (offsetId === threadId && direction === LoadMoreDirection.Backwards) return;
const isOutlying = Boolean(listedIds && !listedIds.includes(offsetId));
const isOutlying = Boolean(listedIds && offsetId && !listedIds.includes(offsetId));
const historyIds = (isOutlying
? selectOutlyingListByMessageId(global, chatId, threadId, offsetId) : listedIds)!;
? selectOutlyingListByMessageId(global, chatId, threadId, offsetId!) : listedIds)!;
if (historyIds?.length) {
const {
newViewportIds, areSomeLocal, areAllLocal,

View File

@ -21,7 +21,6 @@ import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
import { getServerTime } from '../../../util/serverTime';
import versionNotification from '../../../versionNotification.txt';
import {
getIsSavedDialog,
getMediaFilename,
getMediaFormat,
getMediaHash,
@ -41,7 +40,6 @@ import {
replaceTabThreadParam,
replaceThreadParam,
toggleMessageSelection,
updateFocusDirection,
updateFocusedMessage,
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
@ -60,7 +58,6 @@ import {
selectIsRightColumnShown,
selectIsViewportNewest,
selectMessageIdsByGroupId,
selectPinnedIds,
selectReplyStack,
selectRequestedChatTranslationLanguage,
selectRequestedMessageTranslationLanguage,
@ -323,52 +320,6 @@ addActionHandler('closePollResults', (global, actions, payload): ActionReturnTyp
}, tabId);
});
addActionHandler('focusLastMessage', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
const currentMessageList = selectCurrentMessageList(global, tabId);
if (!currentMessageList) {
return;
}
const { chatId, threadId, type } = currentMessageList;
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
let lastMessageId: number | undefined;
if (threadId === MAIN_THREAD_ID) {
if (type === 'pinned') {
const pinnedMessageIds = selectPinnedIds(global, chatId, MAIN_THREAD_ID);
if (!pinnedMessageIds?.length) {
return;
}
lastMessageId = pinnedMessageIds[pinnedMessageIds.length - 1];
} else {
lastMessageId = selectChatLastMessageId(global, chatId);
}
} else if (isSavedDialog) {
lastMessageId = selectChatLastMessageId(global, String(threadId), 'saved');
} else {
const threadInfo = selectThreadInfo(global, chatId, threadId);
lastMessageId = threadInfo?.lastMessageId;
}
if (!lastMessageId) {
return;
}
actions.focusMessage({
chatId,
threadId,
messageListType: type,
messageId: lastMessageId,
noHighlight: true,
noForumTopicPanel: true,
tabId,
});
});
addActionHandler('focusNextReply', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
const currentMessageList = selectCurrentMessageList(global, tabId);
@ -381,7 +332,7 @@ addActionHandler('focusNextReply', (global, actions, payload): ActionReturnType
const replyStack = selectReplyStack(global, chatId, threadId, tabId);
if (!replyStack || replyStack.length === 0) {
actions.focusLastMessage({ tabId });
actions.scrollMessageListToBottom({ tabId });
} else {
const messageId = replyStack.pop();
@ -441,13 +392,11 @@ addActionHandler('focusMessage', (global, actions, payload): ActionReturnType =>
}
blurTimeout = window.setTimeout(() => {
global = getGlobal();
global = updateFocusedMessage({ global }, tabId);
global = updateFocusDirection(global, undefined, tabId);
global = updateFocusedMessage(global, undefined, tabId);
setGlobal(global);
}, noHighlight ? FOCUS_NO_HIGHLIGHT_DURATION : FOCUS_DURATION);
global = updateFocusedMessage({
global,
global = updateFocusedMessage(global, {
chatId,
messageId,
threadId,
@ -456,8 +405,8 @@ addActionHandler('focusMessage', (global, actions, payload): ActionReturnType =>
quote,
quoteOffset,
scrollTargetPosition,
direction: undefined,
}, tabId);
global = updateFocusDirection(global, undefined, tabId);
if (replyMessageId) {
const replyStack = selectReplyStack(global, chatId, threadId, tabId) || [];
@ -465,7 +414,7 @@ addActionHandler('focusMessage', (global, actions, payload): ActionReturnType =>
}
if (shouldSwitchChat) {
global = updateFocusDirection(global, FocusDirection.Static, tabId);
global = updateFocusedMessage(global, { direction: FocusDirection.Static }, tabId);
}
const viewportIds = selectViewportIds(global, chatId, threadId, tabId);
@ -489,7 +438,7 @@ addActionHandler('focusMessage', (global, actions, payload): ActionReturnType =>
if (viewportIds && !shouldSwitchChat) {
const direction = messageId > viewportIds[0] ? FocusDirection.Down : FocusDirection.Up;
global = updateFocusDirection(global, direction, tabId);
global = updateFocusedMessage(global, { direction }, tabId);
}
if (isAnimatingScroll()) {
@ -516,6 +465,50 @@ addActionHandler('focusMessage', (global, actions, payload): ActionReturnType =>
return undefined;
});
addActionHandler('scrollMessageListToBottom', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
const currentMessageList = selectCurrentMessageList(global, tabId);
if (!currentMessageList) {
return;
}
const { chatId, threadId } = currentMessageList;
global = updateFocusedMessage(global, {
chatId,
threadId,
messageId: undefined,
scrollTargetPosition: 'end',
direction: FocusDirection.Down,
noHighlight: true,
}, tabId);
setGlobal(global, { forceOnHeavyAnimation: true });
// Reuse part of `focusMessage`
if (blurTimeout) {
clearTimeout(blurTimeout);
blurTimeout = undefined;
}
blurTimeout = window.setTimeout(() => {
global = getGlobal();
global = updateFocusedMessage(global, undefined, tabId);
setGlobal(global);
}, FOCUS_NO_HIGHLIGHT_DURATION);
if (isAnimatingScroll()) {
cancelScrollBlockingAnimation();
}
actions.loadViewportMessages({
chatId,
threadId,
tabId,
shouldForceRender: true,
forceLastSlice: true,
});
});
addActionHandler('setShouldPreventComposerAnimation', (global, actions, payload): ActionReturnType => {
const { shouldPreventComposerAnimation, tabId = getCurrentTabId() } = payload;
return updateTabState(global, {

View File

@ -4,10 +4,8 @@ import type {
ApiWebPageFull,
} from '../../api/types';
import type {
FocusDirection,
MessageList,
MessageListType,
ScrollTargetPosition,
TabThread,
Thread,
ThreadId,
@ -719,40 +717,16 @@ export function updateQuickReplyMessages<T extends GlobalState>(
}
export function updateFocusedMessage<T extends GlobalState>(
{
global,
chatId,
messageId,
threadId = MAIN_THREAD_ID,
noHighlight = false,
isResizingContainer = false,
quote,
quoteOffset,
scrollTargetPosition,
}: {
global: T;
chatId?: string;
messageId?: number;
threadId?: ThreadId;
noHighlight?: boolean;
isResizingContainer?: boolean;
quote?: string;
quoteOffset?: number;
scrollTargetPosition?: ScrollTargetPosition;
},
...[tabId = getCurrentTabId()]: TabArgs<T>
global: T, update: Partial<TabState['focusedMessage']> | undefined, ...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
if (!update) {
return updateTabState(global, { focusedMessage: undefined }, tabId);
}
return updateTabState(global, {
focusedMessage: {
...selectTabState(global, tabId).focusedMessage,
chatId,
threadId,
messageId,
noHighlight,
isResizingContainer,
quote,
quoteOffset,
scrollTargetPosition,
...update,
},
}, tabId);
}
@ -789,18 +763,6 @@ export function deleteSponsoredMessage<T extends GlobalState>(
};
}
export function updateFocusDirection<T extends GlobalState>(
global: T, direction?: FocusDirection,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
return updateTabState(global, {
focusedMessage: {
...selectTabState(global, tabId).focusedMessage,
direction,
},
}, tabId);
}
export function enterMessageSelectMode<T extends GlobalState>(
global: T,
chatId: string,

View File

@ -1,8 +1,8 @@
import type {
ApiChat, ApiChatFullInfo, ApiChatType,
} from '../../api/types';
import type { ChatListType } from '../../types';
import type { GlobalState, TabArgs } from '../types';
import {
type ApiChat, type ApiChatFullInfo, type ApiChatType,
} from '../../api/types';
import {
ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, MEMBERS_LOAD_SLICE, SAVED_FOLDER_ID, SERVICE_NOTIFICATIONS_USER_ID,

View File

@ -469,6 +469,7 @@ export interface ActionPayloads {
chatId?: string;
threadId?: ThreadId;
shouldForceRender?: boolean;
forceLastSlice?: boolean;
onLoaded?: NoneToVoidFunction;
onError?: NoneToVoidFunction;
} & WithTabId;
@ -1024,8 +1025,8 @@ export interface ActionPayloads {
scrollTargetPosition?: ScrollTargetPosition;
timestamp?: number;
} & WithTabId;
scrollMessageListToBottom: WithTabId | undefined;
focusLastMessage: WithTabId | undefined;
updateDraftReplyInfo: Partial<ApiInputMessageReplyInfo> & WithTabId;
resetDraftReplyInfo: WithTabId | undefined;
updateDraftSuggestedPostInfo: Partial<ApiInputSuggestedPostInfo> & WithTabId;

View File

@ -16,7 +16,7 @@ function useMessageResizeObserver(
shouldFocusOnResize = false,
) {
const {
focusLastMessage,
scrollMessageListToBottom,
} = getActions();
const messageHeightRef = useRef(0);
@ -40,7 +40,7 @@ function useMessageResizeObserver(
const previousScrollBottom = currentScrollBottom - resizeDiff;
if (previousScrollBottom <= BOTTOM_FOCUS_SCROLL_THRESHOLD) {
focusLastMessage();
scrollMessageListToBottom();
}
},
);