Message List: Move streaming responses to the top (#6924)
This commit is contained in:
parent
a103f09dae
commit
2b6fca15bc
@ -800,6 +800,7 @@ export interface ApiMessage {
|
|||||||
fromRank?: string;
|
fromRank?: string;
|
||||||
|
|
||||||
isTypingDraft?: boolean; // Local field
|
isTypingDraft?: boolean; // Local field
|
||||||
|
wasTypingDraft?: boolean; // Local field
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiReactions {
|
export interface ApiReactions {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
@use "../../styles/mixins";
|
@use "../../styles/mixins";
|
||||||
|
|
||||||
.MessageList {
|
.MessageList {
|
||||||
|
container-type: size;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -59,6 +60,18 @@
|
|||||||
scroll-snap-align: end;
|
scroll-snap-align: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.live-tail {
|
||||||
|
min-height: max(0rem, calc(100cqh - var(--middle-header-panes-height) - 3rem));
|
||||||
|
|
||||||
|
.message-list-item {
|
||||||
|
animation: live-tail-message-mount 0.2s ease-out;
|
||||||
|
|
||||||
|
body.no-message-sending-animations & {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
// Patch for an issue on Android when rotating device
|
// Patch for an issue on Android when rotating device
|
||||||
@ -396,3 +409,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes live-tail-message-mount {
|
||||||
|
from { transform: translateY(2rem); }
|
||||||
|
to { transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { beginHeavyAnimation, memo, useEffect, useMemo, useRef } from '@teact';
|
import { beginHeavyAnimation, memo, useEffect, useMemo, useRef, useState, useUnmountCleanup } from '@teact';
|
||||||
import { addExtraClass, removeExtraClass } from '@teact/teact-dom';
|
import { addExtraClass, removeExtraClass } from '@teact/teact-dom';
|
||||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||||
|
|
||||||
@ -11,6 +11,7 @@ import {
|
|||||||
ANIMATION_END_DELAY,
|
ANIMATION_END_DELAY,
|
||||||
ANONYMOUS_USER_ID,
|
ANONYMOUS_USER_ID,
|
||||||
MESSAGE_LIST_SLICE,
|
MESSAGE_LIST_SLICE,
|
||||||
|
SCROLL_MAX_DURATION,
|
||||||
SERVICE_NOTIFICATIONS_USER_ID,
|
SERVICE_NOTIFICATIONS_USER_ID,
|
||||||
} from '../../config';
|
} from '../../config';
|
||||||
import { forceMeasure, requestMeasure, requestMutation } from '../../lib/fasterdom/fasterdom';
|
import { forceMeasure, requestMeasure, requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||||
@ -180,6 +181,7 @@ const BOTTOM_SNAP_THRESHOLD = 7;
|
|||||||
const UNREAD_DIVIDER_TOP = 10;
|
const UNREAD_DIVIDER_TOP = 10;
|
||||||
const SCROLL_DEBOUNCE = 200;
|
const SCROLL_DEBOUNCE = 200;
|
||||||
const MESSAGE_ANIMATION_DURATION = 500;
|
const MESSAGE_ANIMATION_DURATION = 500;
|
||||||
|
const SEND_FOCUS_DURATION = SCROLL_MAX_DURATION + ANIMATION_END_DELAY;
|
||||||
const BOTTOM_FOCUS_MARGIN = 0.5 * REM;
|
const BOTTOM_FOCUS_MARGIN = 0.5 * REM;
|
||||||
const SELECT_MODE_ANIMATION_DURATION = 200;
|
const SELECT_MODE_ANIMATION_DURATION = 200;
|
||||||
|
|
||||||
@ -189,6 +191,12 @@ const BOTTOM_SNAP_CLASS = 'with-bottom-snap';
|
|||||||
|
|
||||||
const runDebouncedForScroll = debounce((cb) => cb(), SCROLL_DEBOUNCE, false);
|
const runDebouncedForScroll = debounce((cb) => cb(), SCROLL_DEBOUNCE, false);
|
||||||
|
|
||||||
|
function getShouldReleaseLiveTail(liveTailElement: HTMLDivElement) {
|
||||||
|
const liveTailMinHeight = parseFloat(getComputedStyle(liveTailElement).minHeight);
|
||||||
|
|
||||||
|
return Boolean(liveTailMinHeight && liveTailElement.scrollHeight > liveTailMinHeight + 1);
|
||||||
|
}
|
||||||
|
|
||||||
const MessageList = ({
|
const MessageList = ({
|
||||||
chatId,
|
chatId,
|
||||||
threadId,
|
threadId,
|
||||||
@ -276,7 +284,11 @@ const MessageList = ({
|
|||||||
const isReplacingHistoryRef = useRef(false);
|
const isReplacingHistoryRef = useRef(false);
|
||||||
const shouldAnimateAppearanceRef = useRef(Boolean(lastMessage));
|
const shouldAnimateAppearanceRef = useRef(Boolean(lastMessage));
|
||||||
const scrollSnapDisabledTimerRef = useRef<number>();
|
const scrollSnapDisabledTimerRef = useRef<number>();
|
||||||
const typingDraftSnapTriggeredIdRef = useRef<number>();
|
const isLiveTailBottomSnapSuppressedRef = useRef(false);
|
||||||
|
const isLiveTailAutoScrollingRef = useRef(false);
|
||||||
|
const liveTailReleaseTimerRef = useRef<number>();
|
||||||
|
const liveTailStartOriginalIdRef = useRef<number>();
|
||||||
|
const [releasedLiveTailStartOriginalId, setReleasedLiveTailStartOriginalId] = useState<number>();
|
||||||
|
|
||||||
const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
|
const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
|
||||||
const hasOpenChatButton = isSavedDialog
|
const hasOpenChatButton = isSavedDialog
|
||||||
@ -289,6 +301,42 @@ const MessageList = ({
|
|||||||
const withUsers = Boolean((!isPrivate && !isChannelChat)
|
const withUsers = Boolean((!isPrivate && !isChannelChat)
|
||||||
|| isChatWithSelf || isSystemBotChat || isAnonymousForwards || isChannelWithAvatars);
|
|| isChatWithSelf || isSystemBotChat || isAnonymousForwards || isChannelWithAvatars);
|
||||||
|
|
||||||
|
const liveTailStartOriginalId = useMemo(() => {
|
||||||
|
if (!messageIds?.length || !messagesById) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousLiveTailStartOriginalId = liveTailStartOriginalIdRef.current;
|
||||||
|
let renderedLiveTailStartOriginalId: number | undefined;
|
||||||
|
|
||||||
|
for (let i = messageIds.length - 1; i >= 0; i--) {
|
||||||
|
const message = messagesById[messageIds[i]];
|
||||||
|
if (message?.isTypingDraft && !message.isOutgoing) {
|
||||||
|
return getMessageOriginalId(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
previousLiveTailStartOriginalId !== undefined
|
||||||
|
&& message?.wasTypingDraft
|
||||||
|
&& getMessageOriginalId(message) === previousLiveTailStartOriginalId
|
||||||
|
) {
|
||||||
|
renderedLiveTailStartOriginalId = previousLiveTailStartOriginalId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderedLiveTailStartOriginalId;
|
||||||
|
}, [messageIds, messagesById]);
|
||||||
|
|
||||||
|
liveTailStartOriginalIdRef.current = liveTailStartOriginalId;
|
||||||
|
|
||||||
|
const effectiveLiveTailStartOriginalId = liveTailStartOriginalId !== releasedLiveTailStartOriginalId
|
||||||
|
? liveTailStartOriginalId
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
useUnmountCleanup(() => {
|
||||||
|
clearTimeout(liveTailReleaseTimerRef.current);
|
||||||
|
});
|
||||||
|
|
||||||
useSyncEffect(() => {
|
useSyncEffect(() => {
|
||||||
// We only need it first time when message list appears
|
// We only need it first time when message list appears
|
||||||
if (areMessagesLoaded) {
|
if (areMessagesLoaded) {
|
||||||
@ -399,19 +447,22 @@ const MessageList = ({
|
|||||||
!isForum ? Number(threadId) : undefined,
|
!isForum ? Number(threadId) : undefined,
|
||||||
isChatWithSelf,
|
isChatWithSelf,
|
||||||
withUsers,
|
withUsers,
|
||||||
|
effectiveLiveTailStartOriginalId,
|
||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
}, [withUsers,
|
}, [withUsers,
|
||||||
messageIds, messagesById, type,
|
messageIds, messagesById, type,
|
||||||
isServiceNotificationsChat, isForum,
|
isServiceNotificationsChat, isForum,
|
||||||
threadId, isChatWithSelf, channelJoinInfo]);
|
threadId, isChatWithSelf, channelJoinInfo, effectiveLiveTailStartOriginalId]);
|
||||||
|
|
||||||
const currentLastMessageOriginalId = useMemo(() => {
|
const currentLastMessageId = messageIds?.[messageIds.length - 1];
|
||||||
const currentLastMessageId = messageIds?.[messageIds.length - 1];
|
const currentLastMessage = currentLastMessageId !== undefined ? messagesById?.[currentLastMessageId] : undefined;
|
||||||
const currentLastMessage = currentLastMessageId !== undefined ? messagesById?.[currentLastMessageId] : undefined;
|
const currentLastMessageOriginalId = currentLastMessage
|
||||||
|
? getMessageOriginalId(currentLastMessage)
|
||||||
return currentLastMessage ? getMessageOriginalId(currentLastMessage) : currentLastMessageId;
|
: currentLastMessageId;
|
||||||
}, [messageIds, messagesById]);
|
const isCurrentLastMessageTypingDraft = Boolean(
|
||||||
|
currentLastMessage?.isTypingDraft || currentLastMessage?.wasTypingDraft,
|
||||||
|
);
|
||||||
|
|
||||||
useInterval(() => {
|
useInterval(() => {
|
||||||
if (!messageIds || !messagesById || type === 'scheduled' || isAccountFrozen || !isActive) return;
|
if (!messageIds || !messagesById || type === 'scheduled' || isAccountFrozen || !isActive) return;
|
||||||
@ -494,6 +545,13 @@ const MessageList = ({
|
|||||||
const bottomTrigger = container?.querySelector<HTMLDivElement>('.fab-trigger');
|
const bottomTrigger = container?.querySelector<HTMLDivElement>('.fab-trigger');
|
||||||
if (!container || !bottomTrigger) return;
|
if (!container || !bottomTrigger) return;
|
||||||
|
|
||||||
|
if (effectiveLiveTailStartOriginalId !== undefined && isLiveTailBottomSnapSuppressedRef.current) {
|
||||||
|
requestMutation(() => {
|
||||||
|
removeExtraClass(container, BOTTOM_SNAP_CLASS);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if fab-trigger + threshold are entering the viewport
|
// Check if fab-trigger + threshold are entering the viewport
|
||||||
const viewportBottom = container.scrollTop + container.offsetHeight;
|
const viewportBottom = container.scrollTop + container.offsetHeight;
|
||||||
const triggerPosition = bottomTrigger.offsetTop;
|
const triggerPosition = bottomTrigger.offsetTop;
|
||||||
@ -517,29 +575,13 @@ const MessageList = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleTallTypingDraft = useLastCallback((messageId: number, isNearExit: boolean) => {
|
const allowLiveTailBottomSnap = useLastCallback(() => {
|
||||||
if (!isNearExit) {
|
if (effectiveLiveTailStartOriginalId === undefined || !isLiveTailBottomSnapSuppressedRef.current) {
|
||||||
if (typingDraftSnapTriggeredIdRef.current === messageId) {
|
|
||||||
typingDraftSnapTriggeredIdRef.current = undefined;
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typingDraftSnapTriggeredIdRef.current === messageId) {
|
isLiveTailBottomSnapSuppressedRef.current = false;
|
||||||
return;
|
updateBottomSnapClass();
|
||||||
}
|
|
||||||
|
|
||||||
const container = containerRef.current;
|
|
||||||
if (!container || !container.classList.contains(BOTTOM_SNAP_CLASS)) return;
|
|
||||||
|
|
||||||
typingDraftSnapTriggeredIdRef.current = messageId;
|
|
||||||
|
|
||||||
clearTimeout(scrollSnapDisabledTimerRef.current);
|
|
||||||
scrollSnapDisabledTimerRef.current = undefined;
|
|
||||||
|
|
||||||
requestMutation(() => {
|
|
||||||
removeExtraClass(container, BOTTOM_SNAP_CLASS);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleScroll = useLastCallback(() => {
|
const handleScroll = useLastCallback(() => {
|
||||||
@ -557,6 +599,16 @@ const MessageList = ({
|
|||||||
updateStickyDates(container);
|
updateStickyDates(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLiveTailAutoScrollingRef.current) {
|
||||||
|
if (!isAnimatingScroll()) {
|
||||||
|
requestMeasure(() => {
|
||||||
|
isLiveTailAutoScrollingRef.current = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
allowLiveTailBottomSnap();
|
||||||
|
}
|
||||||
|
|
||||||
// Check if scroll should be snapped, but only if there's no new message animation in progress
|
// Check if scroll should be snapped, but only if there's no new message animation in progress
|
||||||
if (scrollSnapDisabledTimerRef.current === undefined) {
|
if (scrollSnapDisabledTimerRef.current === undefined) {
|
||||||
updateBottomSnapClass();
|
updateBottomSnapClass();
|
||||||
@ -585,6 +637,11 @@ const MessageList = ({
|
|||||||
const [getContainerHeight, prevContainerHeightRef] = useContainerHeight(containerRef, canPost && !isSelectModeActive);
|
const [getContainerHeight, prevContainerHeightRef] = useContainerHeight(containerRef, canPost && !isSelectModeActive);
|
||||||
|
|
||||||
const handleWheel = useLastCallback((e: React.WheelEvent<HTMLDivElement>) => {
|
const handleWheel = useLastCallback((e: React.WheelEvent<HTMLDivElement>) => {
|
||||||
|
if (e.deltaY > 0) {
|
||||||
|
isLiveTailAutoScrollingRef.current = false;
|
||||||
|
allowLiveTailBottomSnap();
|
||||||
|
}
|
||||||
|
|
||||||
// Remove snap when scrolling up to avoid scroll bug
|
// Remove snap when scrolling up to avoid scroll bug
|
||||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1753188
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1753188
|
||||||
if (IS_FIREFOX && e.deltaY < 0) {
|
if (IS_FIREFOX && e.deltaY < 0) {
|
||||||
@ -644,7 +701,7 @@ const MessageList = ({
|
|||||||
forceMeasure(() => rememberScrollPositionRef.current());
|
forceMeasure(() => rememberScrollPositionRef.current());
|
||||||
},
|
},
|
||||||
// This will run before modifying content and should match deps for `useLayoutEffectWithPrevDeps` below
|
// This will run before modifying content and should match deps for `useLayoutEffectWithPrevDeps` below
|
||||||
[messageIds, isViewportNewest, rememberScrollPositionRef],
|
[messageIds, isViewportNewest, effectiveLiveTailStartOriginalId, rememberScrollPositionRef],
|
||||||
);
|
);
|
||||||
useEffect(
|
useEffect(
|
||||||
() => rememberScrollPositionRef.current(),
|
() => rememberScrollPositionRef.current(),
|
||||||
@ -653,7 +710,9 @@ const MessageList = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Handles updated message list, takes care of scroll repositioning
|
// Handles updated message list, takes care of scroll repositioning
|
||||||
useLayoutEffectWithPrevDeps(([prevMessageIds, prevIsViewportNewest, prevCurrentLastMessageOriginalId]) => {
|
useLayoutEffectWithPrevDeps(([
|
||||||
|
prevMessageIds, prevIsViewportNewest, prevCurrentLastMessageOriginalId, prevLiveTailStartOriginalId,
|
||||||
|
]) => {
|
||||||
if (process.env.APP_ENV === 'perf') {
|
if (process.env.APP_ENV === 'perf') {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.time('scrollTop');
|
console.time('scrollTop');
|
||||||
@ -685,11 +744,34 @@ const MessageList = ({
|
|||||||
messageIds?.[0] !== prevMessageIds?.[0] && messageIds?.length === (MESSAGE_LIST_SLICE / 2 + 1)
|
messageIds?.[0] !== prevMessageIds?.[0] && messageIds?.length === (MESSAGE_LIST_SLICE / 2 + 1)
|
||||||
);
|
);
|
||||||
const wasMessageAdded = hasLastMessageChanged && !hasViewportShifted;
|
const wasMessageAdded = hasLastMessageChanged && !hasViewportShifted;
|
||||||
|
const wasLiveTailCreated = Boolean(
|
||||||
|
effectiveLiveTailStartOriginalId !== undefined
|
||||||
|
&& effectiveLiveTailStartOriginalId !== prevLiveTailStartOriginalId,
|
||||||
|
);
|
||||||
|
const hasLiveTail = effectiveLiveTailStartOriginalId !== undefined;
|
||||||
|
if (wasLiveTailCreated) {
|
||||||
|
isLiveTailBottomSnapSuppressedRef.current = true;
|
||||||
|
} else if (!hasLiveTail) {
|
||||||
|
isLiveTailBottomSnapSuppressedRef.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldReleaseLiveTail = Boolean(
|
||||||
|
wasMessageAdded
|
||||||
|
&& currentLastMessageOriginalId !== undefined
|
||||||
|
&& hasLiveTail
|
||||||
|
&& !wasLiveTailCreated
|
||||||
|
&& !isCurrentLastMessageTypingDraft
|
||||||
|
&& forceMeasure(() => {
|
||||||
|
const liveTailElement = container.querySelector<HTMLDivElement>('.live-tail');
|
||||||
|
return liveTailElement ? getShouldReleaseLiveTail(liveTailElement) : false;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Add extra height when few messages to allow scroll animation
|
// Add extra height when few messages to allow scroll animation
|
||||||
if (
|
if (
|
||||||
isViewportNewest
|
isViewportNewest
|
||||||
&& wasMessageAdded
|
&& wasMessageAdded
|
||||||
|
&& !hasLiveTail
|
||||||
&& (messageIds && messageIds.length < MESSAGE_LIST_SLICE / 2)
|
&& (messageIds && messageIds.length < MESSAGE_LIST_SLICE / 2)
|
||||||
&& !container.parentElement!.classList.contains(FORCE_MESSAGES_SCROLL_CLASS)
|
&& !container.parentElement!.classList.contains(FORCE_MESSAGES_SCROLL_CLASS)
|
||||||
&& forceMeasure(() => (
|
&& forceMeasure(() => (
|
||||||
@ -730,10 +812,13 @@ const MessageList = ({
|
|||||||
bottomOffset -= lastItemHeight;
|
bottomOffset -= lastItemHeight;
|
||||||
}
|
}
|
||||||
const isAtBottom = isViewportNewest && prevIsViewportNewest && bottomOffset <= BOTTOM_THRESHOLD;
|
const isAtBottom = isViewportNewest && prevIsViewportNewest && bottomOffset <= BOTTOM_THRESHOLD;
|
||||||
|
const shouldFocusLiveTail = wasLiveTailCreated && isAtBottom;
|
||||||
const isAlreadyFocusing = messageIds && memoFocusingIdRef.current === messageIds[messageIds.length - 1];
|
const isAlreadyFocusing = messageIds && memoFocusingIdRef.current === messageIds[messageIds.length - 1];
|
||||||
|
|
||||||
// Animate incoming message, but if app is in background mode, scroll to the first unread
|
// Animate incoming message, but if app is in background mode, scroll to the first unread
|
||||||
if (wasMessageAdded && isAtBottom && !isAlreadyFocusing) {
|
if (wasMessageAdded && isAtBottom && (!isAlreadyFocusing || shouldReleaseLiveTail) && (
|
||||||
|
!hasLiveTail || shouldReleaseLiveTail
|
||||||
|
)) {
|
||||||
// Break out of `forceLayout`
|
// Break out of `forceLayout`
|
||||||
requestMeasure(() => {
|
requestMeasure(() => {
|
||||||
const isScrollToBottom = !isBackgroundModeActive() || !firstUnreadElement;
|
const isScrollToBottom = !isBackgroundModeActive() || !firstUnreadElement;
|
||||||
@ -744,6 +829,15 @@ const MessageList = ({
|
|||||||
margin: BOTTOM_FOCUS_MARGIN,
|
margin: BOTTOM_FOCUS_MARGIN,
|
||||||
forceDuration: noMessageSendingAnimation ? 0 : undefined,
|
forceDuration: noMessageSendingAnimation ? 0 : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (shouldReleaseLiveTail && effectiveLiveTailStartOriginalId !== undefined) {
|
||||||
|
clearTimeout(liveTailReleaseTimerRef.current);
|
||||||
|
|
||||||
|
liveTailReleaseTimerRef.current = window.setTimeout(() => {
|
||||||
|
liveTailReleaseTimerRef.current = undefined;
|
||||||
|
setReleasedLiveTailStartOriginalId(effectiveLiveTailStartOriginalId);
|
||||||
|
}, SEND_FOCUS_DURATION);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -758,9 +852,27 @@ const MessageList = ({
|
|||||||
&& memoUnreadDividerBeforeIdRef.current
|
&& memoUnreadDividerBeforeIdRef.current
|
||||||
&& container.querySelector<HTMLDivElement>(`.${UNREAD_DIVIDER_CLASS}`)
|
&& container.querySelector<HTMLDivElement>(`.${UNREAD_DIVIDER_CLASS}`)
|
||||||
);
|
);
|
||||||
|
const liveTailElement = shouldFocusLiveTail
|
||||||
|
? container.querySelector<HTMLDivElement>('.live-tail')
|
||||||
|
: undefined;
|
||||||
|
const animateLiveTailScroll = liveTailElement
|
||||||
|
? animateScroll({
|
||||||
|
container,
|
||||||
|
element: liveTailElement,
|
||||||
|
position: 'end',
|
||||||
|
maxDistance: Number.MAX_SAFE_INTEGER,
|
||||||
|
forceDuration: noMessageSendingAnimation ? 0 : undefined,
|
||||||
|
shouldReturnMutationFn: true,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
let newScrollTop!: number;
|
let newScrollTop!: number;
|
||||||
if (isAtBottom && isResized) {
|
if (liveTailElement) {
|
||||||
|
const liveTailOffset = getOffsetToContainer(liveTailElement, container).top;
|
||||||
|
newScrollTop = liveTailOffset + liveTailElement.offsetHeight - offsetHeight;
|
||||||
|
} else if (shouldFocusLiveTail) {
|
||||||
|
newScrollTop = scrollHeight - offsetHeight;
|
||||||
|
} else if (isAtBottom && isResized) {
|
||||||
newScrollTop = scrollHeight - offsetHeight;
|
newScrollTop = scrollHeight - offsetHeight;
|
||||||
} else if (anchor) {
|
} else if (anchor) {
|
||||||
const newAnchorTop = anchor.getBoundingClientRect().top;
|
const newAnchorTop = anchor.getBoundingClientRect().top;
|
||||||
@ -775,6 +887,19 @@ const MessageList = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (animateLiveTailScroll) {
|
||||||
|
if (Math.abs(newScrollTop - scrollTop) >= 1) {
|
||||||
|
isLiveTailAutoScrollingRef.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
animateLiveTailScroll();
|
||||||
|
scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight);
|
||||||
|
requestMeasure(() => {
|
||||||
|
isReplacingHistoryRef.current = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
resetScroll(container, Math.ceil(newScrollTop));
|
resetScroll(container, Math.ceil(newScrollTop));
|
||||||
requestMeasure(() => {
|
requestMeasure(() => {
|
||||||
isReplacingHistoryRef.current = false;
|
isReplacingHistoryRef.current = false;
|
||||||
@ -804,6 +929,8 @@ const MessageList = ({
|
|||||||
messageIds,
|
messageIds,
|
||||||
isViewportNewest,
|
isViewportNewest,
|
||||||
currentLastMessageOriginalId,
|
currentLastMessageOriginalId,
|
||||||
|
effectiveLiveTailStartOriginalId,
|
||||||
|
isCurrentLastMessageTypingDraft,
|
||||||
getContainerHeight,
|
getContainerHeight,
|
||||||
prevContainerHeightRef,
|
prevContainerHeightRef,
|
||||||
noMessageSendingAnimation,
|
noMessageSendingAnimation,
|
||||||
@ -916,6 +1043,7 @@ const MessageList = ({
|
|||||||
anchorIdRef={anchorIdRef}
|
anchorIdRef={anchorIdRef}
|
||||||
memoUnreadDividerBeforeIdRef={memoUnreadDividerBeforeIdRef}
|
memoUnreadDividerBeforeIdRef={memoUnreadDividerBeforeIdRef}
|
||||||
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
|
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
|
||||||
|
liveTailStartOriginalId={effectiveLiveTailStartOriginalId}
|
||||||
isReplacingHistoryRef={isReplacingHistoryRef}
|
isReplacingHistoryRef={isReplacingHistoryRef}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
type={type}
|
type={type}
|
||||||
@ -933,7 +1061,6 @@ const MessageList = ({
|
|||||||
onScrollDownToggle={onScrollDownToggle}
|
onScrollDownToggle={onScrollDownToggle}
|
||||||
onNotchToggle={onNotchToggle}
|
onNotchToggle={onNotchToggle}
|
||||||
onIntersectPinnedMessage={onIntersectPinnedMessage}
|
onIntersectPinnedMessage={onIntersectPinnedMessage}
|
||||||
onTallTypingDraft={handleTallTypingDraft}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Loading color="white" backgroundColor="dark" />
|
<Loading color="white" backgroundColor="dark" />
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { ElementRef } from '../../lib/teact/teact';
|
import type { ElementRef, TeactNode } from '../../lib/teact/teact';
|
||||||
import { getIsHeavyAnimating, memo } from '../../lib/teact/teact';
|
import { getIsHeavyAnimating, memo } from '../../lib/teact/teact';
|
||||||
import { getActions, getGlobal } from '../../global';
|
import { getActions, getGlobal } from '../../global';
|
||||||
|
|
||||||
@ -70,6 +70,7 @@ interface OwnProps {
|
|||||||
anchorIdRef: { current: string | undefined };
|
anchorIdRef: { current: string | undefined };
|
||||||
memoUnreadDividerBeforeIdRef: { current: number | undefined };
|
memoUnreadDividerBeforeIdRef: { current: number | undefined };
|
||||||
memoFirstUnreadIdRef: { current: number | undefined };
|
memoFirstUnreadIdRef: { current: number | undefined };
|
||||||
|
liveTailStartOriginalId?: number;
|
||||||
isReplacingHistoryRef: { current: boolean };
|
isReplacingHistoryRef: { current: boolean };
|
||||||
type: MessageListType;
|
type: MessageListType;
|
||||||
isReady: boolean;
|
isReady: boolean;
|
||||||
@ -86,11 +87,23 @@ interface OwnProps {
|
|||||||
onScrollDownToggle?: BooleanToVoidFunction;
|
onScrollDownToggle?: BooleanToVoidFunction;
|
||||||
onNotchToggle?: AnyToVoidFunction;
|
onNotchToggle?: AnyToVoidFunction;
|
||||||
onIntersectPinnedMessage?: OnIntersectPinnedMessage;
|
onIntersectPinnedMessage?: OnIntersectPinnedMessage;
|
||||||
onTallTypingDraft?: (messageId: number, isNearExit: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const UNREAD_DIVIDER_CLASS = 'unread-divider';
|
const UNREAD_DIVIDER_CLASS = 'unread-divider';
|
||||||
|
|
||||||
|
function senderGroupContainsOriginalId(
|
||||||
|
senderGroup: (ApiMessage | IAlbum | IDocumentGroup)[],
|
||||||
|
originalId: number,
|
||||||
|
) {
|
||||||
|
return senderGroup.some((messageOrAlbum) => {
|
||||||
|
if (isAlbum(messageOrAlbum) || isDocumentGroup(messageOrAlbum)) {
|
||||||
|
return messageOrAlbum.messages.some((message) => getMessageOriginalId(message) === originalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getMessageOriginalId(messageOrAlbum) === originalId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const MessageListContent = ({
|
const MessageListContent = ({
|
||||||
canShowAds,
|
canShowAds,
|
||||||
chatId,
|
chatId,
|
||||||
@ -111,6 +124,7 @@ const MessageListContent = ({
|
|||||||
anchorIdRef,
|
anchorIdRef,
|
||||||
memoUnreadDividerBeforeIdRef,
|
memoUnreadDividerBeforeIdRef,
|
||||||
memoFirstUnreadIdRef,
|
memoFirstUnreadIdRef,
|
||||||
|
liveTailStartOriginalId,
|
||||||
isReplacingHistoryRef,
|
isReplacingHistoryRef,
|
||||||
type,
|
type,
|
||||||
isReady,
|
isReady,
|
||||||
@ -127,7 +141,6 @@ const MessageListContent = ({
|
|||||||
onScrollDownToggle,
|
onScrollDownToggle,
|
||||||
onNotchToggle,
|
onNotchToggle,
|
||||||
onIntersectPinnedMessage,
|
onIntersectPinnedMessage,
|
||||||
onTallTypingDraft,
|
|
||||||
}: OwnProps) => {
|
}: OwnProps) => {
|
||||||
const { openHistoryCalendar } = getActions();
|
const { openHistoryCalendar } = getActions();
|
||||||
|
|
||||||
@ -159,7 +172,6 @@ const MessageListContent = ({
|
|||||||
backwardsTriggerRef,
|
backwardsTriggerRef,
|
||||||
forwardsTriggerRef,
|
forwardsTriggerRef,
|
||||||
fabTriggerRef,
|
fabTriggerRef,
|
||||||
observeIntersectionForTopExit,
|
|
||||||
} = useScrollHooks({
|
} = useScrollHooks({
|
||||||
type,
|
type,
|
||||||
containerRef,
|
containerRef,
|
||||||
@ -352,6 +364,7 @@ const MessageListContent = ({
|
|||||||
) {
|
) {
|
||||||
const isOwn = isOwnMessage(message);
|
const isOwn = isOwnMessage(message);
|
||||||
const originalId = getMessageOriginalId(message);
|
const originalId = getMessageOriginalId(message);
|
||||||
|
const isInLiveTail = liveTailStartOriginalId !== undefined && originalId >= liveTailStartOriginalId;
|
||||||
const key = isServiceNotificationMessage(message)
|
const key = isServiceNotificationMessage(message)
|
||||||
? `${message.date}_${originalId}` : originalId;
|
? `${message.date}_${originalId}` : originalId;
|
||||||
const shouldShowGuestAvatar = isPrivate && !withUsers && Boolean(message.guestChatViaId);
|
const shouldShowGuestAvatar = isPrivate && !withUsers && Boolean(message.guestChatViaId);
|
||||||
@ -384,11 +397,11 @@ const MessageListContent = ({
|
|||||||
isFirstInDocumentGroup={position.isFirstInDocumentGroup}
|
isFirstInDocumentGroup={position.isFirstInDocumentGroup}
|
||||||
isLastInDocumentGroup={position.isLastInDocumentGroup}
|
isLastInDocumentGroup={position.isLastInDocumentGroup}
|
||||||
isLastInList={position.isLastInList}
|
isLastInList={position.isLastInList}
|
||||||
|
shouldIgnoreSendFocus={isInLiveTail && isOwn}
|
||||||
|
isQuickPreview={isQuickPreview}
|
||||||
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
|
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
|
||||||
getIsMessageListReady={getIsReady}
|
getIsMessageListReady={getIsReady}
|
||||||
observeIntersectionForTopExit={observeIntersectionForTopExit}
|
|
||||||
onMessageUnmount={onMessageUnmount}
|
onMessageUnmount={onMessageUnmount}
|
||||||
onTallTypingDraft={onTallTypingDraft}
|
|
||||||
/>,
|
/>,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -504,43 +517,97 @@ const MessageListContent = ({
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}).flat();
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateGroups = messageGroups.map((
|
function renderDateHeader(dateGroup: MessageDateGroup) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={buildClassName('sticky-date', areDatesClickable && 'interactive')}
|
||||||
|
key="date-header"
|
||||||
|
onMouseDown={preventMessageInputBlur}
|
||||||
|
onClick={areDatesClickable ? () => openHistoryCalendar({ selectedAt: dateGroup.datetime }) : undefined}
|
||||||
|
>
|
||||||
|
<span dir="auto">
|
||||||
|
{isSchedule && dateGroup.originalDate === SCHEDULED_WHEN_ONLINE && (
|
||||||
|
oldLang('MessageScheduledUntilOnline')
|
||||||
|
)}
|
||||||
|
{isSchedule && dateGroup.originalDate !== SCHEDULED_WHEN_ONLINE && (
|
||||||
|
oldLang('MessageScheduledOn', formatHumanDate(oldLang, dateGroup.datetime, undefined, true))
|
||||||
|
)}
|
||||||
|
{!isSchedule && formatMessageListDate(lang, new Date(dateGroup.datetime))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDateGroup(
|
||||||
|
dateGroup: MessageDateGroup,
|
||||||
|
children: TeactNode[],
|
||||||
|
keySuffix: string,
|
||||||
|
shouldAddFirstClass: boolean,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={buildClassName('message-date-group', shouldAddFirstClass && 'first-message-date-group')}
|
||||||
|
key={`${dateGroup.datetime}-${keySuffix}`}
|
||||||
|
onMouseDown={preventMessageInputBlur}
|
||||||
|
teactFastList
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let isRenderingLiveTail = false;
|
||||||
|
const dateGroups: TeactNode[] = [];
|
||||||
|
const liveTailDateGroups: TeactNode[] = [];
|
||||||
|
|
||||||
|
messageGroups.forEach((
|
||||||
dateGroup: MessageDateGroup,
|
dateGroup: MessageDateGroup,
|
||||||
dateGroupIndex: number,
|
dateGroupIndex: number,
|
||||||
dateGroupsArray: MessageDateGroup[],
|
dateGroupsArray: MessageDateGroup[],
|
||||||
) => {
|
) => {
|
||||||
const senderGroups = calculateSenderGroups(dateGroup, dateGroupIndex, dateGroupsArray);
|
const senderGroups = calculateSenderGroups(dateGroup, dateGroupIndex, dateGroupsArray);
|
||||||
|
const beforeTailChildren: TeactNode[] = [];
|
||||||
|
const liveTailChildren: TeactNode[] = [];
|
||||||
|
|
||||||
return (
|
if (isRenderingLiveTail) {
|
||||||
<div
|
liveTailChildren.push(renderDateHeader(dateGroup));
|
||||||
className={buildClassName('message-date-group', !(nameChangeDate || photoChangeDate)
|
} else {
|
||||||
&& dateGroupIndex === 0 && 'first-message-date-group')}
|
beforeTailChildren.push(renderDateHeader(dateGroup));
|
||||||
key={dateGroup.datetime}
|
}
|
||||||
onMouseDown={preventMessageInputBlur}
|
|
||||||
teactFastList
|
senderGroups.forEach((senderGroupElements, senderGroupIndex) => {
|
||||||
>
|
const isLiveTailStart = (
|
||||||
<div
|
!isRenderingLiveTail
|
||||||
className={buildClassName('sticky-date', areDatesClickable && 'interactive')}
|
&& liveTailStartOriginalId !== undefined
|
||||||
key="date-header"
|
&& senderGroupContainsOriginalId(
|
||||||
onMouseDown={preventMessageInputBlur}
|
dateGroup.senderGroups[senderGroupIndex],
|
||||||
onClick={areDatesClickable ? () => openHistoryCalendar({ selectedAt: dateGroup.datetime }) : undefined}
|
liveTailStartOriginalId,
|
||||||
>
|
)
|
||||||
<span dir="auto">
|
);
|
||||||
{isSchedule && dateGroup.originalDate === SCHEDULED_WHEN_ONLINE && (
|
|
||||||
oldLang('MessageScheduledUntilOnline')
|
if (isLiveTailStart) {
|
||||||
)}
|
isRenderingLiveTail = true;
|
||||||
{isSchedule && dateGroup.originalDate !== SCHEDULED_WHEN_ONLINE && (
|
}
|
||||||
oldLang('MessageScheduledOn', formatHumanDate(oldLang, dateGroup.datetime, undefined, true))
|
|
||||||
)}
|
const target = isRenderingLiveTail ? liveTailChildren : beforeTailChildren;
|
||||||
{!isSchedule && formatMessageListDate(lang, new Date(dateGroup.datetime))}
|
target.push(...senderGroupElements);
|
||||||
</span>
|
});
|
||||||
</div>
|
|
||||||
{senderGroups.flat()}
|
const shouldAddFirstClass = !(nameChangeDate || photoChangeDate) && dateGroupIndex === 0;
|
||||||
</div>
|
if (beforeTailChildren.length) {
|
||||||
);
|
dateGroups.push(renderDateGroup(
|
||||||
|
dateGroup, beforeTailChildren, 'before-tail', shouldAddFirstClass,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (liveTailChildren.length) {
|
||||||
|
liveTailDateGroups.push(renderDateGroup(
|
||||||
|
dateGroup, liveTailChildren, 'live-tail', shouldAddFirstClass && !beforeTailChildren.length,
|
||||||
|
));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -548,7 +615,12 @@ const MessageListContent = ({
|
|||||||
{withHistoryTriggers && <div ref={backwardsTriggerRef} key="backwards-trigger" className="backwards-trigger" />}
|
{withHistoryTriggers && <div ref={backwardsTriggerRef} key="backwards-trigger" className="backwards-trigger" />}
|
||||||
{shouldRenderAccountInfo
|
{shouldRenderAccountInfo
|
||||||
&& <MessageListAccountInfo key={`account_info_${chatId}`} chatId={chatId} hasMessages />}
|
&& <MessageListAccountInfo key={`account_info_${chatId}`} chatId={chatId} hasMessages />}
|
||||||
{dateGroups.flat()}
|
{dateGroups}
|
||||||
|
{Boolean(liveTailDateGroups.length) && (
|
||||||
|
<div className="live-tail" key="live-tail" teactFastList>
|
||||||
|
{liveTailDateGroups}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{isViewportNewest && renderBotForumTopicAction()}
|
{isViewportNewest && renderBotForumTopicAction()}
|
||||||
{withHistoryTriggers && (
|
{withHistoryTriggers && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { ApiMessage } from '../../../api/types';
|
import type { ApiMessage } from '../../../api/types';
|
||||||
import type { IAlbum, IDocumentGroup } from '../../../types';
|
import type { IAlbum, IDocumentGroup } from '../../../types';
|
||||||
|
|
||||||
import { isActionMessage } from '../../../global/helpers';
|
import { getMessageOriginalId, isActionMessage } from '../../../global/helpers';
|
||||||
import { getDayStartAt } from '../../../util/dates/oldDateFormat';
|
import { getDayStartAt } from '../../../util/dates/oldDateFormat';
|
||||||
|
|
||||||
type SenderGroup = (ApiMessage | IAlbum | IDocumentGroup)[];
|
type SenderGroup = (ApiMessage | IAlbum | IDocumentGroup)[];
|
||||||
@ -26,6 +26,7 @@ export function isDocumentGroup(
|
|||||||
|
|
||||||
export function groupMessages(
|
export function groupMessages(
|
||||||
messages: ApiMessage[], firstUnreadId?: number, topMessageId?: number, isChatWithSelf?: boolean, withUsers?: boolean,
|
messages: ApiMessage[], firstUnreadId?: number, topMessageId?: number, isChatWithSelf?: boolean, withUsers?: boolean,
|
||||||
|
splitBeforeMessageId?: number,
|
||||||
) {
|
) {
|
||||||
const initDateGroup: MessageDateGroup = {
|
const initDateGroup: MessageDateGroup = {
|
||||||
originalDate: messages[0].date,
|
originalDate: messages[0].date,
|
||||||
@ -120,6 +121,7 @@ export function groupMessages(
|
|||||||
dateGroups.push(newDateGroup);
|
dateGroups.push(newDateGroup);
|
||||||
} else if (
|
} else if (
|
||||||
nextMessage.id === firstUnreadId
|
nextMessage.id === firstUnreadId
|
||||||
|
|| (splitBeforeMessageId !== undefined && getMessageOriginalId(nextMessage) === splitBeforeMessageId)
|
||||||
|| message.senderId !== nextMessage.senderId
|
|| message.senderId !== nextMessage.senderId
|
||||||
|| message.guestChatViaId !== nextMessage.guestChatViaId
|
|| message.guestChatViaId !== nextMessage.guestChatViaId
|
||||||
|| (!withUsers && message.paidMessageStars)
|
|| (!withUsers && message.paidMessageStars)
|
||||||
|
|||||||
@ -77,6 +77,10 @@ export default function useMessageObservers({
|
|||||||
const shouldUpdateViews = dataset.shouldUpdateViews === 'true';
|
const shouldUpdateViews = dataset.shouldUpdateViews === 'true';
|
||||||
const albumMainId = dataset.albumMainId ? Number(dataset.albumMainId) : undefined;
|
const albumMainId = dataset.albumMainId ? Number(dataset.albumMainId) : undefined;
|
||||||
|
|
||||||
|
if (!Number.isInteger(messageId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isIntersecting) {
|
if (!isIntersecting) {
|
||||||
hiddenViewportIds.add(messageId);
|
hiddenViewportIds.add(messageId);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import useSyncEffect from '../../../hooks/useSyncEffect';
|
|||||||
|
|
||||||
const FAB_THRESHOLD = 50;
|
const FAB_THRESHOLD = 50;
|
||||||
const NOTCH_THRESHOLD = 1; // Notch has zero height so we at least need a 1px margin to intersect
|
const NOTCH_THRESHOLD = 1; // Notch has zero height so we at least need a 1px margin to intersect
|
||||||
const TOP_EXIT_THRESHOLD = 50;
|
|
||||||
const CONTAINER_HEIGHT_DEBOUNCE = 200;
|
const CONTAINER_HEIGHT_DEBOUNCE = 200;
|
||||||
const SCROLL_TOOLS_DEBOUNCE = 100;
|
const SCROLL_TOOLS_DEBOUNCE = 100;
|
||||||
const TOOLS_FREEZE_TIMEOUT = 350; // Approximate message sending animation duration
|
const TOOLS_FREEZE_TIMEOUT = 350; // Approximate message sending animation duration
|
||||||
@ -157,14 +156,6 @@ export default function useScrollHooks({
|
|||||||
|
|
||||||
useOnIntersect(fabTriggerRef, observeIntersectionForNotch);
|
useOnIntersect(fabTriggerRef, observeIntersectionForNotch);
|
||||||
|
|
||||||
const {
|
|
||||||
observe: observeIntersectionForTopExit,
|
|
||||||
} = useIntersectionObserver({
|
|
||||||
rootRef: containerRef,
|
|
||||||
margin: `-${TOP_EXIT_THRESHOLD}px 0px 0px 0px`,
|
|
||||||
throttleScheduler: requestMeasure,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isReady) {
|
if (isReady) {
|
||||||
updateScrollTools();
|
updateScrollTools();
|
||||||
@ -204,6 +195,5 @@ export default function useScrollHooks({
|
|||||||
backwardsTriggerRef,
|
backwardsTriggerRef,
|
||||||
forwardsTriggerRef,
|
forwardsTriggerRef,
|
||||||
fabTriggerRef,
|
fabTriggerRef,
|
||||||
observeIntersectionForTopExit,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -809,17 +809,6 @@
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-marker {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.giveaway-result-content {
|
.giveaway-result-content {
|
||||||
min-width: 17rem;
|
min-width: 17rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -239,14 +239,14 @@ type OwnProps = {
|
|||||||
appearanceOrder: number;
|
appearanceOrder: number;
|
||||||
isJustAdded: boolean;
|
isJustAdded: boolean;
|
||||||
isThreadTop?: boolean;
|
isThreadTop?: boolean;
|
||||||
|
shouldIgnoreSendFocus?: boolean;
|
||||||
|
isQuickPreview?: boolean;
|
||||||
memoFirstUnreadIdRef?: { current: number | undefined };
|
memoFirstUnreadIdRef?: { current: number | undefined };
|
||||||
getIsMessageListReady?: Signal<boolean>;
|
getIsMessageListReady?: Signal<boolean>;
|
||||||
observeIntersectionForBottom?: ObserveFn;
|
observeIntersectionForBottom?: ObserveFn;
|
||||||
observeIntersectionForLoading?: ObserveFn;
|
observeIntersectionForLoading?: ObserveFn;
|
||||||
observeIntersectionForPlaying?: ObserveFn;
|
observeIntersectionForPlaying?: ObserveFn;
|
||||||
observeIntersectionForTopExit?: ObserveFn;
|
|
||||||
onMessageUnmount?: (messageId: number) => void;
|
onMessageUnmount?: (messageId: number) => void;
|
||||||
onTallTypingDraft?: (messageId: number, isNearExit: boolean) => void;
|
|
||||||
} & MessagePositionProperties;
|
} & MessagePositionProperties;
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -477,9 +477,8 @@ const Message = ({
|
|||||||
observeIntersectionForBottom,
|
observeIntersectionForBottom,
|
||||||
observeIntersectionForLoading,
|
observeIntersectionForLoading,
|
||||||
observeIntersectionForPlaying,
|
observeIntersectionForPlaying,
|
||||||
observeIntersectionForTopExit,
|
isQuickPreview,
|
||||||
onMessageUnmount,
|
onMessageUnmount,
|
||||||
onTallTypingDraft,
|
|
||||||
}: OwnProps & StateProps) => {
|
}: OwnProps & StateProps) => {
|
||||||
const {
|
const {
|
||||||
toggleMessageSelection,
|
toggleMessageSelection,
|
||||||
@ -491,6 +490,7 @@ const Message = ({
|
|||||||
animateUnreadReaction,
|
animateUnreadReaction,
|
||||||
focusMessage,
|
focusMessage,
|
||||||
markTypingDraftDone,
|
markTypingDraftDone,
|
||||||
|
markMessageListRead,
|
||||||
markMentionsRead,
|
markMentionsRead,
|
||||||
markPollVotesRead,
|
markPollVotesRead,
|
||||||
openThread,
|
openThread,
|
||||||
@ -498,7 +498,6 @@ const Message = ({
|
|||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>();
|
const ref = useRef<HTMLDivElement>();
|
||||||
const topMarkerRef = useRef<HTMLDivElement>();
|
|
||||||
const bottomMarkerRef = useRef<HTMLDivElement>();
|
const bottomMarkerRef = useRef<HTMLDivElement>();
|
||||||
const quickReactionRef = useRef<HTMLDivElement>();
|
const quickReactionRef = useRef<HTMLDivElement>();
|
||||||
|
|
||||||
@ -519,16 +518,7 @@ const Message = ({
|
|||||||
const [declineReason, setDeclineReason] = useState('');
|
const [declineReason, setDeclineReason] = useState('');
|
||||||
const { isMobile, isTouchScreen } = useAppLayout();
|
const { isMobile, isTouchScreen } = useAppLayout();
|
||||||
|
|
||||||
useOnIntersect(bottomMarkerRef, observeIntersectionForBottom);
|
useOnIntersect(bottomMarkerRef, isTypingDraft ? undefined : observeIntersectionForBottom);
|
||||||
|
|
||||||
const handleTypingDraftNearExit = useLastCallback(({ isIntersecting }: IntersectionObserverEntry) => {
|
|
||||||
onTallTypingDraft?.(messageId, !isIntersecting);
|
|
||||||
});
|
|
||||||
useOnIntersect(
|
|
||||||
topMarkerRef,
|
|
||||||
isTypingDraft && isLastInList ? observeIntersectionForTopExit : undefined,
|
|
||||||
handleTypingDraftNearExit,
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isContextMenuOpen,
|
isContextMenuOpen,
|
||||||
@ -993,9 +983,23 @@ const Message = ({
|
|||||||
|| undefined;
|
|| undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isTypingDraft) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const bottomMarker = bottomMarkerRef.current;
|
const bottomMarker = bottomMarkerRef.current;
|
||||||
if (!bottomMarker || !isElementInViewport(bottomMarker)) return;
|
if (!bottomMarker || !isElementInViewport(bottomMarker)) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
message.wasTypingDraft
|
||||||
|
&& !isQuickPreview
|
||||||
|
&& !isOwn
|
||||||
|
&& memoFirstUnreadIdRef?.current
|
||||||
|
&& messageId >= memoFirstUnreadIdRef.current
|
||||||
|
) {
|
||||||
|
markMessageListRead({ maxId: messageId });
|
||||||
|
}
|
||||||
|
|
||||||
if (hasUnreadReaction) {
|
if (hasUnreadReaction) {
|
||||||
animateUnreadReaction({ chatId, messageIds: [messageId] });
|
animateUnreadReaction({ chatId, messageIds: [messageId] });
|
||||||
}
|
}
|
||||||
@ -1021,10 +1025,16 @@ const Message = ({
|
|||||||
hasUnreadPollVote,
|
hasUnreadPollVote,
|
||||||
album,
|
album,
|
||||||
chatId,
|
chatId,
|
||||||
|
isQuickPreview,
|
||||||
|
isOwn,
|
||||||
|
isTypingDraft,
|
||||||
|
markMessageListRead,
|
||||||
messageId,
|
messageId,
|
||||||
|
memoFirstUnreadIdRef,
|
||||||
animateUnreadReaction,
|
animateUnreadReaction,
|
||||||
markPollVotesRead,
|
markPollVotesRead,
|
||||||
message.hasUnreadMention,
|
message.hasUnreadMention,
|
||||||
|
message.wasTypingDraft,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const albumLayout = useMemo(() => {
|
const albumLayout = useMemo(() => {
|
||||||
@ -1914,10 +1924,6 @@ const Message = ({
|
|||||||
onMouseMove={withQuickReactionButton ? handleMouseMove : undefined}
|
onMouseMove={withQuickReactionButton ? handleMouseMove : undefined}
|
||||||
onMouseLeave={(withQuickReactionButton || isInDocumentGroupNotLast) ? handleMouseLeave : undefined}
|
onMouseLeave={(withQuickReactionButton || isInDocumentGroupNotLast) ? handleMouseLeave : undefined}
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
ref={topMarkerRef}
|
|
||||||
className="top-marker"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
ref={bottomMarkerRef}
|
ref={bottomMarkerRef}
|
||||||
className="bottom-marker"
|
className="bottom-marker"
|
||||||
@ -2100,7 +2106,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
} = selectTabState(global);
|
} = selectTabState(global);
|
||||||
const {
|
const {
|
||||||
message, album, documentGroup, withSenderName, withAvatar, threadId, messageListType,
|
message, album, documentGroup, withSenderName, withAvatar, threadId, messageListType,
|
||||||
isLastInDocumentGroup, isFirstInGroup,
|
isLastInDocumentGroup, isFirstInGroup, shouldIgnoreSendFocus,
|
||||||
} = ownProps;
|
} = ownProps;
|
||||||
const {
|
const {
|
||||||
id, chatId, viaBotId, guestChatViaId, isOutgoing, forwardInfo, transcriptionId, isPinned,
|
id, chatId, viaBotId, guestChatViaId, isOutgoing, forwardInfo, transcriptionId, isPinned,
|
||||||
@ -2158,11 +2164,18 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const storySender = storyReplyPeerId ? selectPeer(global, storyReplyPeerId) : undefined;
|
const storySender = storyReplyPeerId ? selectPeer(global, storyReplyPeerId) : undefined;
|
||||||
|
|
||||||
const uploadProgress = selectUploadProgress(global, message);
|
const uploadProgress = selectUploadProgress(global, message);
|
||||||
const isFocused = messageListType === 'thread' && (
|
const isFocusTarget = messageListType === 'thread' && (
|
||||||
album
|
album
|
||||||
? album.messages.some((m) => selectIsMessageFocused(global, m, threadId))
|
? album.messages.some((m) => selectIsMessageFocused(global, m, threadId))
|
||||||
: selectIsMessageFocused(global, message, threadId)
|
: selectIsMessageFocused(global, message, threadId)
|
||||||
);
|
);
|
||||||
|
const shouldIgnoreFocus = Boolean(
|
||||||
|
isFocusTarget
|
||||||
|
&& shouldIgnoreSendFocus
|
||||||
|
&& focusedMessage?.noHighlight
|
||||||
|
&& focusedMessage.isResizingContainer,
|
||||||
|
);
|
||||||
|
const isFocused = isFocusTarget && !shouldIgnoreFocus;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
direction: focusDirection, noHighlight: noFocusHighlight, isResizingContainer,
|
direction: focusDirection, noHighlight: noFocusHighlight, isResizingContainer,
|
||||||
|
|||||||
@ -1287,6 +1287,7 @@ addActionHandler('reportChannelSpam', (global, actions, payload): ActionReturnTy
|
|||||||
addActionHandler('markMessageListRead', (global, actions, payload): ActionReturnType => {
|
addActionHandler('markMessageListRead', (global, actions, payload): ActionReturnType => {
|
||||||
if (selectIsCurrentUserFrozen(global)) return undefined;
|
if (selectIsCurrentUserFrozen(global)) return undefined;
|
||||||
const { maxId, tabId = getCurrentTabId() } = payload;
|
const { maxId, tabId = getCurrentTabId() } = payload;
|
||||||
|
if (isLocalMessageId(maxId)) return undefined;
|
||||||
|
|
||||||
const currentMessageList = selectCurrentMessageList(global, tabId);
|
const currentMessageList = selectCurrentMessageList(global, tabId);
|
||||||
if (!currentMessageList) {
|
if (!currentMessageList) {
|
||||||
|
|||||||
@ -213,6 +213,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
...message,
|
...message,
|
||||||
previousLocalId: matchedTypingDraftEntry.message.id,
|
previousLocalId: matchedTypingDraftEntry.message.id,
|
||||||
isTypingDraft: true,
|
isTypingDraft: true,
|
||||||
|
wasTypingDraft: true,
|
||||||
} : message;
|
} : message;
|
||||||
|
|
||||||
global = updateWithLocalMedia(global, chatId, id, true, nextMessage);
|
global = updateWithLocalMedia(global, chatId, id, true, nextMessage);
|
||||||
|
|||||||
@ -862,6 +862,7 @@ export function selectFirstUnreadId<T extends GlobalState>(
|
|||||||
return (
|
return (
|
||||||
(!lastReadId || id > lastReadId)
|
(!lastReadId || id > lastReadId)
|
||||||
&& byId[id]
|
&& byId[id]
|
||||||
|
&& !byId[id].isTypingDraft
|
||||||
&& (!byId[id].isOutgoing || byId[id].isFromScheduled)
|
&& (!byId[id].isOutgoing || byId[id].isFromScheduled)
|
||||||
&& id > lastReadServiceNotificationId
|
&& id > lastReadServiceNotificationId
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user