Message List: Fix scroll to bottom ignoring local messages (#6418)
This commit is contained in:
parent
cb6f814c36
commit
b497a3c0ea
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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));
|
||||
|
||||
28
src/components/middle/MessageListBottomMarker.tsx
Normal file
28
src/components/middle/MessageListBottomMarker.tsx
Normal 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);
|
||||
@ -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}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user