From 9170dc1863bdc29d9575f7f9cffe7d0467d17a07 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Sat, 21 Aug 2021 13:33:26 +0300 Subject: [PATCH] Message List: Prevent scroll jumps when selecting (again) --- src/components/middle/MessageList.tsx | 51 ++++++++++++++++----------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 9a43dcbd2..070ec4309 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -333,23 +333,30 @@ const MessageList: FC = ({ }, FOCUSING_DURATION); } - const { scrollTop, scrollHeight, offsetHeight } = container; - const scrollOffset = scrollOffsetRef.current!; - const lastItemElement = listItemElementsRef.current[listItemElementsRef.current.length - 1]; - - const isAtBottom = isViewportNewest && prevIsViewportNewest && ( - scrollOffset - (prevContainerHeight || offsetHeight) <= BOTTOM_THRESHOLD - ); - - let newScrollTop!: number; - const hasFirstMessageChanged = messageIds && prevMessageIds && messageIds[0] !== prevMessageIds[0]; const hasLastMessageChanged = ( messageIds && prevMessageIds && messageIds[messageIds.length - 1] !== prevMessageIds[prevMessageIds.length - 1] ); + const wasMessageAdded = hasLastMessageChanged && !hasFirstMessageChanged; const isAlreadyFocusing = messageIds && memoFocusingIdRef.current === messageIds[messageIds.length - 1]; - if (isAtBottom && hasLastMessageChanged && !hasFirstMessageChanged && !isAlreadyFocusing) { + const { scrollTop, scrollHeight, offsetHeight } = container; + const scrollOffset = scrollOffsetRef.current!; + const lastItemElement = listItemElementsRef.current[listItemElementsRef.current.length - 1]; + + let bottomOffset = scrollOffset - (prevContainerHeight || offsetHeight); + 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 workaround 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; + + let newScrollTop!: number; + + if (wasMessageAdded && isAtBottom && !isAlreadyFocusing) { if (lastItemElement) { fastRaf(() => { fastSmoothScroll( @@ -431,10 +438,12 @@ const MessageList: FC = ({ const withUsers = Boolean((!isPrivate && !isChannelChat) || isChatWithSelf); const noAvatars = Boolean(!withUsers || isChannelChat); const shouldRenderGreeting = isChatPrivate(chatId) && !isChatWithSelf && !isBot - && (( - !messageGroups && !lastMessage && messageIds - // Used to avoid flickering when deleting a greeting that has just been sent - && (!listItemElementsRef.current || listItemElementsRef.current.length === 0)) + && ( + ( + !messageGroups && !lastMessage && messageIds + // Used to avoid flickering when deleting a greeting that has just been sent + && (!listItemElementsRef.current || listItemElementsRef.current.length === 0) + ) || checkSingleMessageActionByType('contactSignUp', messageGroups) || (lastMessage && lastMessage.content.action && lastMessage.content.action.type === 'contactSignUp') ); @@ -508,12 +517,12 @@ const MessageList: FC = ({ function checkSingleMessageActionByType(type: ApiAction['type'], messageGroups?: MessageDateGroup[]) { return messageGroups - && messageGroups.length === 1 - && messageGroups[0].senderGroups.length === 1 - && messageGroups[0].senderGroups[0].length === 1 - && 'content' in messageGroups[0].senderGroups[0][0] - && messageGroups[0].senderGroups[0][0].content.action - && messageGroups[0].senderGroups[0][0].content.action.type === type; + && messageGroups.length === 1 + && messageGroups[0].senderGroups.length === 1 + && messageGroups[0].senderGroups[0].length === 1 + && 'content' in messageGroups[0].senderGroups[0][0] + && messageGroups[0].senderGroups[0][0].content.action + && messageGroups[0].senderGroups[0][0].content.action.type === type; } export default memo(withGlobal(