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(
'--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.setAttribute('data-message-text-size', newSize.toString());

View File

@ -61,7 +61,8 @@
}
.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 {
animation: live-tail-message-mount 0.2s ease-out;

View File

@ -288,6 +288,7 @@ const MessageList = ({
const isLiveTailAutoScrollingRef = useRef(false);
const liveTailReleaseTimerRef = useRef<number>();
const liveTailStartOriginalIdRef = useRef<number>();
const scrollTopBeforeUpdateRef = useRef<number>();
const [releasedLiveTailStartOriginalId, setReleasedLiveTailStartOriginalId] = useState<number>();
const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
@ -307,25 +308,58 @@ const MessageList = ({
}
const previousLiveTailStartOriginalId = liveTailStartOriginalIdRef.current;
const hasActiveLiveTail = previousLiveTailStartOriginalId !== undefined
&& previousLiveTailStartOriginalId !== releasedLiveTailStartOriginalId;
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 (!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 (
previousLiveTailStartOriginalId !== undefined
&& message?.wasTypingDraft
&& getMessageOriginalId(message) === previousLiveTailStartOriginalId
&& message.wasTypingDraft
&& originalId === previousLiveTailStartOriginalId
) {
renderedLiveTailStartOriginalId = previousLiveTailStartOriginalId;
}
}
return renderedLiveTailStartOriginalId;
}, [messageIds, messagesById]);
}, [messageIds, messagesById, releasedLiveTailStartOriginalId]);
liveTailStartOriginalIdRef.current = liveTailStartOriginalId;
@ -463,6 +497,9 @@ const MessageList = ({
const isCurrentLastMessageTypingDraft = Boolean(
currentLastMessage?.isTypingDraft || currentLastMessage?.wasTypingDraft,
);
const isCurrentLastMessageIncomingTypingDraft = Boolean(
currentLastMessage?.isTypingDraft && !currentLastMessage.isOutgoing,
);
useInterval(() => {
if (!messageIds || !messagesById || type === 'scheduled' || isAccountFrozen || !isActive) return;
@ -698,7 +735,10 @@ const MessageList = ({
useSyncEffect(
() => {
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
[messageIds, isViewportNewest, effectiveLiveTailStartOriginalId, rememberScrollPositionRef],
@ -709,7 +749,12 @@ const MessageList = ({
[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(([
prevMessageIds, prevIsViewportNewest, prevCurrentLastMessageOriginalId, prevLiveTailStartOriginalId,
]) => {
@ -749,7 +794,14 @@ const MessageList = ({
&& effectiveLiveTailStartOriginalId !== prevLiveTailStartOriginalId,
);
const hasLiveTail = effectiveLiveTailStartOriginalId !== undefined;
if (wasLiveTailCreated) {
const shouldRevealLiveTailTypingDraft = Boolean(
wasMessageAdded
&& hasLiveTail
&& !wasLiveTailCreated
&& isCurrentLastMessageIncomingTypingDraft,
);
if (wasLiveTailCreated || shouldRevealLiveTailTypingDraft) {
isLiveTailBottomSnapSuppressedRef.current = true;
} else if (!hasLiveTail) {
isLiveTailBottomSnapSuppressedRef.current = false;
@ -804,15 +856,26 @@ const MessageList = ({
const scrollOffset = scrollOffsetRef.current;
let bottomOffset = scrollOffset - (prevContainerHeight || offsetHeight);
const lastItemHeight = wasMessageAdded && lastItemElement ? lastItemElement.offsetHeight : 0;
if (wasMessageAdded) {
// 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,
// we calculate `isAtBottom` with a "buffer" of the latest message height (this is approximate).
const lastItemHeight = lastItemElement ? lastItemElement.offsetHeight : 0;
bottomOffset -= lastItemHeight;
}
const isAtBottom = isViewportNewest && prevIsViewportNewest && bottomOffset <= BOTTOM_THRESHOLD;
const wasAtBottomBeforeTypingDraft = Boolean(
shouldRevealLiveTailTypingDraft
&& isViewportNewest
&& prevIsViewportNewest
&& scrollHeight - lastItemHeight - scrollTop - offsetHeight <= BOTTOM_THRESHOLD,
);
const shouldFocusLiveTail = wasLiveTailCreated && isAtBottom;
const shouldRevealTypingDraft = Boolean(
shouldRevealLiveTailTypingDraft
&& (isAtBottom || wasAtBottomBeforeTypingDraft),
);
const isAlreadyFocusing = messageIds && memoFocusingIdRef.current === messageIds[messageIds.length - 1];
// Animate incoming message, but if app is in background mode, scroll to the first unread
@ -865,6 +928,53 @@ const MessageList = ({
shouldReturnMutationFn: true,
})
: 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;
if (liveTailElement) {
@ -872,6 +982,10 @@ const MessageList = ({
newScrollTop = liveTailOffset + liveTailElement.offsetHeight - offsetHeight;
} else if (shouldFocusLiveTail) {
newScrollTop = scrollHeight - offsetHeight;
} else if (typingDraftScrollTop !== undefined) {
newScrollTop = typingDraftScrollTop;
} else if (shouldRevealTypingDraft) {
newScrollTop = scrollTop;
} else if (isAtBottom && isResized) {
newScrollTop = scrollHeight - offsetHeight;
} else if (anchor) {
@ -887,12 +1001,17 @@ const MessageList = ({
}
return () => {
if (animateLiveTailScroll) {
if (Math.abs(newScrollTop - scrollTop) >= 1) {
const animateScrollMutation = animateLiveTailScroll || animateTypingDraftScroll;
if (animateScrollMutation) {
const animationStartScrollTop = shouldRestoreBeforeTypingDraftAnimation && scrollTopBeforeUpdate !== undefined
? scrollTopBeforeUpdate
: scrollTop;
if (Math.abs(newScrollTop - animationStartScrollTop) >= 1) {
isLiveTailAutoScrollingRef.current = true;
}
animateLiveTailScroll();
animateScrollMutation();
scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight);
requestMeasure(() => {
isReplacingHistoryRef.current = false;
@ -901,6 +1020,7 @@ const MessageList = ({
}
resetScroll(container, Math.ceil(newScrollTop));
requestMeasure(() => {
isReplacingHistoryRef.current = false;
});
@ -931,6 +1051,7 @@ const MessageList = ({
currentLastMessageOriginalId,
effectiveLiveTailStartOriginalId,
isCurrentLastMessageTypingDraft,
isCurrentLastMessageIncomingTypingDraft,
getContainerHeight,
prevContainerHeightRef,
noMessageSendingAnimation,

View File

@ -156,7 +156,7 @@ addCallback((global: GlobalState) => {
document.documentElement.style.setProperty(
'--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.setAttribute('data-message-text-size', messageTextSize.toString());
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`,
);
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.setAttribute('data-message-text-size', sharedSettings.messageTextSize.toString());
}