Message List: Update behaviour (#6969)

This commit is contained in:
zubiden 2026-06-01 01:16:12 +02:00 committed by Alexander Zinchuk
parent 1162804e9d
commit 8edd3fe4ba
5 changed files with 138 additions and 16 deletions

View File

@ -88,7 +88,7 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
document.documentElement.style.setProperty( document.documentElement.style.setProperty(
'--composer-text-size', `${Math.max(newSize, IS_IOS ? 16 : 15)}px`, '--composer-text-size', `${Math.max(newSize, IS_IOS ? 16 : 15)}px`,
); );
document.documentElement.style.setProperty('--message-meta-height', `${Math.floor(newSize * 1.3125)}px`); document.documentElement.style.setProperty('--message-meta-height', `${Math.floor(newSize * 1.25)}px`);
document.documentElement.style.setProperty('--message-text-size', `${newSize}px`); document.documentElement.style.setProperty('--message-text-size', `${newSize}px`);
document.documentElement.setAttribute('data-message-text-size', newSize.toString()); document.documentElement.setAttribute('data-message-text-size', newSize.toString());

View File

@ -61,7 +61,8 @@
} }
.live-tail { .live-tail {
min-height: max(0rem, calc(100cqh - var(--middle-header-panes-height) - 3rem)); overflow-anchor: none;
min-height: max(0rem, calc(100cqh - var(--middle-header-panes-height) - 2.5rem));
.message-list-item { .message-list-item {
animation: live-tail-message-mount 0.2s ease-out; animation: live-tail-message-mount 0.2s ease-out;

View File

@ -288,6 +288,7 @@ const MessageList = ({
const isLiveTailAutoScrollingRef = useRef(false); const isLiveTailAutoScrollingRef = useRef(false);
const liveTailReleaseTimerRef = useRef<number>(); const liveTailReleaseTimerRef = useRef<number>();
const liveTailStartOriginalIdRef = useRef<number>(); const liveTailStartOriginalIdRef = useRef<number>();
const scrollTopBeforeUpdateRef = useRef<number>();
const [releasedLiveTailStartOriginalId, setReleasedLiveTailStartOriginalId] = useState<number>(); const [releasedLiveTailStartOriginalId, setReleasedLiveTailStartOriginalId] = useState<number>();
const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId); const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
@ -307,25 +308,58 @@ const MessageList = ({
} }
const previousLiveTailStartOriginalId = liveTailStartOriginalIdRef.current; const previousLiveTailStartOriginalId = liveTailStartOriginalIdRef.current;
const hasActiveLiveTail = previousLiveTailStartOriginalId !== undefined
&& previousLiveTailStartOriginalId !== releasedLiveTailStartOriginalId;
let renderedLiveTailStartOriginalId: number | undefined; let renderedLiveTailStartOriginalId: number | undefined;
for (let i = messageIds.length - 1; i >= 0; i--) { for (let i = messageIds.length - 1; i >= 0; i--) {
const message = messagesById[messageIds[i]]; const message = messagesById[messageIds[i]];
if (message?.isTypingDraft && !message.isOutgoing) { if (!message) {
return getMessageOriginalId(message); continue;
}
const originalId = getMessageOriginalId(message);
if (
hasActiveLiveTail
&& message.isOutgoing
&& originalId >= previousLiveTailStartOriginalId
) {
return originalId;
}
if (message.isTypingDraft && !message.isOutgoing) {
if (
hasActiveLiveTail
&& originalId === previousLiveTailStartOriginalId
) {
renderedLiveTailStartOriginalId = previousLiveTailStartOriginalId;
continue;
}
if (hasActiveLiveTail) {
continue;
}
// Start new live tail from our message to keep consistency with in-tail focusing
const previousMessage = i > 0 ? messagesById[messageIds[i - 1]] : undefined;
if (previousMessage?.isOutgoing) {
return getMessageOriginalId(previousMessage);
}
return originalId;
} }
if ( if (
previousLiveTailStartOriginalId !== undefined previousLiveTailStartOriginalId !== undefined
&& message?.wasTypingDraft && message.wasTypingDraft
&& getMessageOriginalId(message) === previousLiveTailStartOriginalId && originalId === previousLiveTailStartOriginalId
) { ) {
renderedLiveTailStartOriginalId = previousLiveTailStartOriginalId; renderedLiveTailStartOriginalId = previousLiveTailStartOriginalId;
} }
} }
return renderedLiveTailStartOriginalId; return renderedLiveTailStartOriginalId;
}, [messageIds, messagesById]); }, [messageIds, messagesById, releasedLiveTailStartOriginalId]);
liveTailStartOriginalIdRef.current = liveTailStartOriginalId; liveTailStartOriginalIdRef.current = liveTailStartOriginalId;
@ -463,6 +497,9 @@ const MessageList = ({
const isCurrentLastMessageTypingDraft = Boolean( const isCurrentLastMessageTypingDraft = Boolean(
currentLastMessage?.isTypingDraft || currentLastMessage?.wasTypingDraft, currentLastMessage?.isTypingDraft || currentLastMessage?.wasTypingDraft,
); );
const isCurrentLastMessageIncomingTypingDraft = Boolean(
currentLastMessage?.isTypingDraft && !currentLastMessage.isOutgoing,
);
useInterval(() => { useInterval(() => {
if (!messageIds || !messagesById || type === 'scheduled' || isAccountFrozen || !isActive) return; if (!messageIds || !messagesById || type === 'scheduled' || isAccountFrozen || !isActive) return;
@ -698,7 +735,10 @@ const MessageList = ({
useSyncEffect( useSyncEffect(
() => { () => {
isReplacingHistoryRef.current = true; isReplacingHistoryRef.current = true;
forceMeasure(() => rememberScrollPositionRef.current()); forceMeasure(() => {
scrollTopBeforeUpdateRef.current = containerRef.current?.scrollTop;
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, effectiveLiveTailStartOriginalId, rememberScrollPositionRef], [messageIds, isViewportNewest, effectiveLiveTailStartOriginalId, rememberScrollPositionRef],
@ -709,7 +749,12 @@ const MessageList = ({
[getContainerHeight, rememberScrollPositionRef], [getContainerHeight, rememberScrollPositionRef],
); );
// Handles updated message list, takes care of scroll repositioning /* Handles updated message list, takes care of scroll repositioning
Live tail mode:
- When a new typing draft is received, the live tail is revealed
- New messages attach to it. New outgoing message kick older out
- If outgoing message is tall, we should show at least one line of typing draft that replies to it
*/
useLayoutEffectWithPrevDeps(([ useLayoutEffectWithPrevDeps(([
prevMessageIds, prevIsViewportNewest, prevCurrentLastMessageOriginalId, prevLiveTailStartOriginalId, prevMessageIds, prevIsViewportNewest, prevCurrentLastMessageOriginalId, prevLiveTailStartOriginalId,
]) => { ]) => {
@ -749,7 +794,14 @@ const MessageList = ({
&& effectiveLiveTailStartOriginalId !== prevLiveTailStartOriginalId, && effectiveLiveTailStartOriginalId !== prevLiveTailStartOriginalId,
); );
const hasLiveTail = effectiveLiveTailStartOriginalId !== undefined; const hasLiveTail = effectiveLiveTailStartOriginalId !== undefined;
if (wasLiveTailCreated) { const shouldRevealLiveTailTypingDraft = Boolean(
wasMessageAdded
&& hasLiveTail
&& !wasLiveTailCreated
&& isCurrentLastMessageIncomingTypingDraft,
);
if (wasLiveTailCreated || shouldRevealLiveTailTypingDraft) {
isLiveTailBottomSnapSuppressedRef.current = true; isLiveTailBottomSnapSuppressedRef.current = true;
} else if (!hasLiveTail) { } else if (!hasLiveTail) {
isLiveTailBottomSnapSuppressedRef.current = false; isLiveTailBottomSnapSuppressedRef.current = false;
@ -804,15 +856,26 @@ const MessageList = ({
const scrollOffset = scrollOffsetRef.current; const scrollOffset = scrollOffsetRef.current;
let bottomOffset = scrollOffset - (prevContainerHeight || offsetHeight); let bottomOffset = scrollOffset - (prevContainerHeight || offsetHeight);
const lastItemHeight = wasMessageAdded && lastItemElement ? lastItemElement.offsetHeight : 0;
if (wasMessageAdded) { if (wasMessageAdded) {
// If two new messages come at once (e.g. when bot responds) then the first message will update `scrollOffset` // If two new messages come at once (e.g. when bot responds) then the first message will update `scrollOffset`
// right away (before animation) which creates inconsistency until the animation completes. To work around that, // right away (before animation) which creates inconsistency until the animation completes. To work around that,
// we calculate `isAtBottom` with a "buffer" of the latest message height (this is approximate). // we calculate `isAtBottom` with a "buffer" of the latest message height (this is approximate).
const lastItemHeight = lastItemElement ? lastItemElement.offsetHeight : 0;
bottomOffset -= lastItemHeight; bottomOffset -= lastItemHeight;
} }
const isAtBottom = isViewportNewest && prevIsViewportNewest && bottomOffset <= BOTTOM_THRESHOLD; const isAtBottom = isViewportNewest && prevIsViewportNewest && bottomOffset <= BOTTOM_THRESHOLD;
const wasAtBottomBeforeTypingDraft = Boolean(
shouldRevealLiveTailTypingDraft
&& isViewportNewest
&& prevIsViewportNewest
&& scrollHeight - lastItemHeight - scrollTop - offsetHeight <= BOTTOM_THRESHOLD,
);
const shouldFocusLiveTail = wasLiveTailCreated && isAtBottom; const shouldFocusLiveTail = wasLiveTailCreated && isAtBottom;
const shouldRevealTypingDraft = Boolean(
shouldRevealLiveTailTypingDraft
&& (isAtBottom || wasAtBottomBeforeTypingDraft),
);
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
@ -865,6 +928,53 @@ const MessageList = ({
shouldReturnMutationFn: true, shouldReturnMutationFn: true,
}) })
: undefined; : undefined;
const typingDraftTop = shouldRevealTypingDraft && lastItemElement
? getOffsetToContainer(lastItemElement, container).top
: undefined;
const typingDraftBottom = typingDraftTop !== undefined && lastItemElement
? typingDraftTop + lastItemElement.offsetHeight
: undefined;
const scrollTopBeforeUpdate = scrollTopBeforeUpdateRef.current;
const viewportBottomBeforeUpdate = (scrollTopBeforeUpdate ?? scrollTop) + offsetHeight;
const typingDraftElement = typingDraftBottom !== undefined && typingDraftBottom > viewportBottomBeforeUpdate
? lastItemElement
: undefined;
const typingDraftScrollTop = typingDraftElement && typingDraftTop !== undefined
? typingDraftTop + typingDraftElement.offsetHeight - offsetHeight
: undefined;
const shouldRestoreBeforeTypingDraftAnimation = Boolean(
typingDraftElement
&& scrollTopBeforeUpdate !== undefined
&& scrollTopBeforeUpdate < scrollTop
&& typingDraftScrollTop !== undefined
&& scrollTopBeforeUpdate < typingDraftScrollTop,
);
let animateTypingDraftScroll: NoneToVoidFunction | undefined;
if (typingDraftElement) {
animateTypingDraftScroll = shouldRestoreBeforeTypingDraftAnimation ? () => {
resetScroll(container, scrollTopBeforeUpdate);
requestMeasure(() => {
const mutate = animateScroll({
container,
element: typingDraftElement,
position: 'end',
maxDistance: Number.MAX_SAFE_INTEGER,
forceDuration: noMessageSendingAnimation ? 0 : undefined,
shouldReturnMutationFn: true,
});
requestMutation(mutate!);
});
} : animateScroll({
container,
element: typingDraftElement,
position: 'end',
maxDistance: Number.MAX_SAFE_INTEGER,
forceDuration: noMessageSendingAnimation ? 0 : undefined,
shouldReturnMutationFn: true,
});
}
let newScrollTop!: number; let newScrollTop!: number;
if (liveTailElement) { if (liveTailElement) {
@ -872,6 +982,10 @@ const MessageList = ({
newScrollTop = liveTailOffset + liveTailElement.offsetHeight - offsetHeight; newScrollTop = liveTailOffset + liveTailElement.offsetHeight - offsetHeight;
} else if (shouldFocusLiveTail) { } else if (shouldFocusLiveTail) {
newScrollTop = scrollHeight - offsetHeight; newScrollTop = scrollHeight - offsetHeight;
} else if (typingDraftScrollTop !== undefined) {
newScrollTop = typingDraftScrollTop;
} else if (shouldRevealTypingDraft) {
newScrollTop = scrollTop;
} else if (isAtBottom && isResized) { } else if (isAtBottom && isResized) {
newScrollTop = scrollHeight - offsetHeight; newScrollTop = scrollHeight - offsetHeight;
} else if (anchor) { } else if (anchor) {
@ -887,12 +1001,17 @@ const MessageList = ({
} }
return () => { return () => {
if (animateLiveTailScroll) { const animateScrollMutation = animateLiveTailScroll || animateTypingDraftScroll;
if (Math.abs(newScrollTop - scrollTop) >= 1) { if (animateScrollMutation) {
const animationStartScrollTop = shouldRestoreBeforeTypingDraftAnimation && scrollTopBeforeUpdate !== undefined
? scrollTopBeforeUpdate
: scrollTop;
if (Math.abs(newScrollTop - animationStartScrollTop) >= 1) {
isLiveTailAutoScrollingRef.current = true; isLiveTailAutoScrollingRef.current = true;
} }
animateLiveTailScroll(); animateScrollMutation();
scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight); scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight);
requestMeasure(() => { requestMeasure(() => {
isReplacingHistoryRef.current = false; isReplacingHistoryRef.current = false;
@ -901,6 +1020,7 @@ const MessageList = ({
} }
resetScroll(container, Math.ceil(newScrollTop)); resetScroll(container, Math.ceil(newScrollTop));
requestMeasure(() => { requestMeasure(() => {
isReplacingHistoryRef.current = false; isReplacingHistoryRef.current = false;
}); });
@ -931,6 +1051,7 @@ const MessageList = ({
currentLastMessageOriginalId, currentLastMessageOriginalId,
effectiveLiveTailStartOriginalId, effectiveLiveTailStartOriginalId,
isCurrentLastMessageTypingDraft, isCurrentLastMessageTypingDraft,
isCurrentLastMessageIncomingTypingDraft,
getContainerHeight, getContainerHeight,
prevContainerHeightRef, prevContainerHeightRef,
noMessageSendingAnimation, noMessageSendingAnimation,

View File

@ -156,7 +156,7 @@ addCallback((global: GlobalState) => {
document.documentElement.style.setProperty( document.documentElement.style.setProperty(
'--composer-text-size', `${Math.max(messageTextSize, IS_IOS ? 16 : 15)}px`, '--composer-text-size', `${Math.max(messageTextSize, IS_IOS ? 16 : 15)}px`,
); );
document.documentElement.style.setProperty('--message-meta-height', `${Math.floor(messageTextSize * 1.3125)}px`); document.documentElement.style.setProperty('--message-meta-height', `${Math.floor(messageTextSize * 1.25)}px`);
document.documentElement.style.setProperty('--message-text-size', `${messageTextSize}px`); document.documentElement.style.setProperty('--message-text-size', `${messageTextSize}px`);
document.documentElement.setAttribute('data-message-text-size', messageTextSize.toString()); document.documentElement.setAttribute('data-message-text-size', messageTextSize.toString());
document.body.classList.add('initial'); document.body.classList.add('initial');

View File

@ -69,7 +69,7 @@ addCallback((global: GlobalState) => {
'--composer-text-size', `${Math.max(sharedSettings.messageTextSize, IS_IOS ? 16 : 15)}px`, '--composer-text-size', `${Math.max(sharedSettings.messageTextSize, IS_IOS ? 16 : 15)}px`,
); );
document.documentElement.style.setProperty('--message-meta-height', document.documentElement.style.setProperty('--message-meta-height',
`${Math.floor(sharedSettings.messageTextSize * 1.3125)}px`); `${Math.floor(sharedSettings.messageTextSize * 1.25)}px`);
document.documentElement.style.setProperty('--message-text-size', `${sharedSettings.messageTextSize}px`); document.documentElement.style.setProperty('--message-text-size', `${sharedSettings.messageTextSize}px`);
document.documentElement.setAttribute('data-message-text-size', sharedSettings.messageTextSize.toString()); document.documentElement.setAttribute('data-message-text-size', sharedSettings.messageTextSize.toString());
} }