Message List: Fix scroll jumps

This commit is contained in:
Alexander Zinchuk 2021-12-25 12:41:02 +01:00
parent cdf291933d
commit ab9cef0f13
7 changed files with 55 additions and 9 deletions

View File

@ -5,7 +5,6 @@
width: 100%; width: 100%;
margin-bottom: .5rem; margin-bottom: .5rem;
overflow-anchor: none;
overflow: scroll; overflow: scroll;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;

View File

@ -42,7 +42,7 @@ import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
import useOnChange from '../../hooks/useOnChange'; import useOnChange from '../../hooks/useOnChange';
import useStickyDates from './hooks/useStickyDates'; import useStickyDates from './hooks/useStickyDates';
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
import resetScroll from '../../util/resetScroll'; import resetScroll, { patchChromiumScroll } from '../../util/resetScroll';
import fastSmoothScroll, { isAnimatingScroll } from '../../util/fastSmoothScroll'; import fastSmoothScroll, { isAnimatingScroll } from '../../util/fastSmoothScroll';
import renderText from '../common/helpers/renderText'; import renderText from '../common/helpers/renderText';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
@ -210,12 +210,17 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
const { isScrolled, updateStickyDates } = useStickyDates(); const { isScrolled, updateStickyDates } = useStickyDates();
const isScrollingRef = useRef<boolean>();
const isScrollPatchNeededRef = useRef<boolean>();
const handleScroll = useCallback(() => { const handleScroll = useCallback(() => {
if (isScrollTopJustUpdatedRef.current) { if (isScrollTopJustUpdatedRef.current) {
isScrollTopJustUpdatedRef.current = false; isScrollTopJustUpdatedRef.current = false;
return; return;
} }
isScrollingRef.current = true;
const container = containerRef.current!; const container = containerRef.current!;
if (!memoFocusingIdRef.current) { if (!memoFocusingIdRef.current) {
@ -223,6 +228,8 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
} }
runDebouncedForScroll(() => { runDebouncedForScroll(() => {
isScrollingRef.current = false;
fastRaf(() => { fastRaf(() => {
if (!container.parentElement) { if (!container.parentElement) {
return; return;
@ -320,7 +327,7 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
const container = containerRef.current!; const container = containerRef.current!;
listItemElementsRef.current = Array.from(container.querySelectorAll<HTMLDivElement>('.message-list-item')); listItemElementsRef.current = Array.from(container.querySelectorAll<HTMLDivElement>('.message-list-item'));
// During animation // TODO Consider removing
if (!container.offsetParent) { if (!container.offsetParent) {
return; return;
} }
@ -355,6 +362,7 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
} }
const { scrollTop, scrollHeight, offsetHeight } = container; const { scrollTop, scrollHeight, offsetHeight } = container;
// TODO Consider `scrollOffset = scrollHeight - scrollTop`
const scrollOffset = scrollOffsetRef.current!; const scrollOffset = scrollOffsetRef.current!;
const lastItemElement = listItemElementsRef.current[listItemElementsRef.current.length - 1]; const lastItemElement = listItemElementsRef.current[listItemElementsRef.current.length - 1];
@ -397,6 +405,7 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
} }
const isResized = prevContainerHeight !== undefined && prevContainerHeight !== containerHeight; const isResized = prevContainerHeight !== undefined && prevContainerHeight !== containerHeight;
// TODO Look up within active transition slide
const anchor = anchorIdRef.current && document.getElementById(anchorIdRef.current); const anchor = anchorIdRef.current && document.getElementById(anchorIdRef.current);
const unreadDivider = ( const unreadDivider = (
!anchor !anchor
@ -411,6 +420,11 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
newScrollTop = scrollHeight - offsetHeight; newScrollTop = scrollHeight - offsetHeight;
} else if (anchor) { } else if (anchor) {
if (isScrollPatchNeededRef.current) {
isScrollPatchNeededRef.current = false;
patchChromiumScroll(container);
}
const newAnchorTop = anchor.getBoundingClientRect().top; const newAnchorTop = anchor.getBoundingClientRect().top;
newScrollTop = scrollTop + (newAnchorTop - (anchorTopRef.current || 0)); newScrollTop = scrollTop + (newAnchorTop - (anchorTopRef.current || 0));
} else if (unreadDivider) { } else if (unreadDivider) {
@ -515,6 +529,8 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
threadId={threadId} threadId={threadId}
type={type} type={type}
isReady={isReady} isReady={isReady}
isScrollingRef={isScrollingRef}
isScrollPatchNeededRef={isScrollPatchNeededRef}
threadTopMessageId={threadTopMessageId} threadTopMessageId={threadTopMessageId}
hasLinkedChat={hasLinkedChat} hasLinkedChat={hasLinkedChat}
isSchedule={messageGroups ? type === 'scheduled' : false} isSchedule={messageGroups ? type === 'scheduled' : false}

View File

@ -31,6 +31,8 @@ interface OwnProps {
threadId: number; threadId: number;
type: MessageListType; type: MessageListType;
isReady: boolean; isReady: boolean;
isScrollingRef: { current: boolean | undefined };
isScrollPatchNeededRef: { current: boolean | undefined };
threadTopMessageId: number | undefined; threadTopMessageId: number | undefined;
hasLinkedChat: boolean | undefined; hasLinkedChat: boolean | undefined;
isSchedule: boolean; isSchedule: boolean;
@ -56,6 +58,8 @@ const MessageListContent: FC<OwnProps> = ({
threadId, threadId,
type, type,
isReady, isReady,
isScrollingRef,
isScrollPatchNeededRef,
threadTopMessageId, threadTopMessageId,
hasLinkedChat, hasLinkedChat,
isSchedule, isSchedule,
@ -83,6 +87,8 @@ const MessageListContent: FC<OwnProps> = ({
onFabToggle, onFabToggle,
onNotchToggle, onNotchToggle,
isReady, isReady,
isScrollingRef,
isScrollPatchNeededRef,
); );
const lang = useLang(); const lang = useLang();

View File

@ -5,10 +5,11 @@ import { useMemo, useRef } from '../../../lib/teact/teact';
import { LoadMoreDirection } from '../../../types'; import { LoadMoreDirection } from '../../../types';
import { MessageListType } from '../../../global/types'; import { MessageListType } from '../../../global/types';
import { LOCAL_MESSAGE_ID_BASE, MESSAGE_LIST_SLICE } from '../../../config';
import { IS_MAC_OS, IS_SCROLL_PATCH_NEEDED, MESSAGE_LIST_SENSITIVE_AREA } from '../../../util/environment';
import { debounce } from '../../../util/schedulers'; import { debounce } from '../../../util/schedulers';
import { useIntersectionObserver, useOnIntersect } from '../../../hooks/useIntersectionObserver';
import { LOCAL_MESSAGE_ID_BASE, MESSAGE_LIST_SENSITIVE_AREA } from '../../../config';
import resetScroll from '../../../util/resetScroll'; import resetScroll from '../../../util/resetScroll';
import { useIntersectionObserver, useOnIntersect } from '../../../hooks/useIntersectionObserver';
import useOnChange from '../../../hooks/useOnChange'; import useOnChange from '../../../hooks/useOnChange';
const FAB_THRESHOLD = 50; const FAB_THRESHOLD = 50;
@ -24,6 +25,8 @@ export default function useScrollHooks(
onFabToggle: AnyToVoidFunction, onFabToggle: AnyToVoidFunction,
onNotchToggle: AnyToVoidFunction, onNotchToggle: AnyToVoidFunction,
isReady: boolean, isReady: boolean,
isScrollingRef: { current: boolean | undefined },
isScrollPatchNeededRef: { current: boolean | undefined },
) { ) {
const { loadViewportMessages } = getDispatch(); const { loadViewportMessages } = getDispatch();
@ -91,9 +94,17 @@ export default function useScrollHooks(
const { target } = triggerEntry; const { target } = triggerEntry;
if (target.className === 'backwards-trigger') { if (target.className === 'backwards-trigger') {
if (
IS_SCROLL_PATCH_NEEDED && isScrollingRef.current && messageIds.length <= MESSAGE_LIST_SLICE
) {
isScrollPatchNeededRef.current = true;
}
// TODO Consider removing
resetScroll(containerRef.current!); resetScroll(containerRef.current!);
loadMoreBackwards(); loadMoreBackwards();
} else if (target.className === 'forwards-trigger') { } else if (target.className === 'forwards-trigger') {
// TODO Consider removing
resetScroll(containerRef.current!); resetScroll(containerRef.current!);
loadMoreForwards(); loadMoreForwards();
} }

View File

@ -41,7 +41,6 @@ const isBigScreen = typeof window !== 'undefined' && window.innerHeight >= 900;
export const MIN_PASSWORD_LENGTH = 1; export const MIN_PASSWORD_LENGTH = 1;
export const MESSAGE_LIST_SENSITIVE_AREA = 750;
export const MESSAGE_LIST_SLICE = isBigScreen ? 60 : 40; export const MESSAGE_LIST_SLICE = isBigScreen ? 60 : 40;
export const MESSAGE_LIST_VIEWPORT_LIMIT = MESSAGE_LIST_SLICE * 2; export const MESSAGE_LIST_VIEWPORT_LIMIT = MESSAGE_LIST_SLICE * 2;

View File

@ -39,9 +39,11 @@ export const IS_MAC_OS = PLATFORM_ENV === 'macOS';
export const IS_IOS = PLATFORM_ENV === 'iOS'; export const IS_IOS = PLATFORM_ENV === 'iOS';
export const IS_ANDROID = PLATFORM_ENV === 'Android'; export const IS_ANDROID = PLATFORM_ENV === 'Android';
export const IS_SAFARI = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); export const IS_SAFARI = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
export const IS_PWA = window.matchMedia('(display-mode: standalone)').matches export const IS_PWA = (
|| (window.navigator as any).standalone window.matchMedia('(display-mode: standalone)').matches
|| document.referrer.includes('android-app://'); || (window.navigator as any).standalone
|| document.referrer.includes('android-app://')
);
export const IS_TOUCH_ENV = window.matchMedia('(pointer: coarse)').matches; export const IS_TOUCH_ENV = window.matchMedia('(pointer: coarse)').matches;
// Keep in mind the landscape orientation // Keep in mind the landscape orientation
@ -82,3 +84,8 @@ if (IS_MOV_SUPPORTED) SUPPORTED_VIDEO_CONTENT_TYPES.add(VIDEO_MOV_TYPE);
export const DPR = window.devicePixelRatio || 1; export const DPR = window.devicePixelRatio || 1;
export const MASK_IMAGE_DISABLED = true; export const MASK_IMAGE_DISABLED = true;
export const IS_SCROLL_PATCH_NEEDED = !IS_MAC_OS && !IS_IOS && !IS_ANDROID;
// Smaller area reduces scroll jumps caused by `patchChromiumScroll`
export const MESSAGE_LIST_SENSITIVE_AREA = IS_SCROLL_PATCH_NEEDED ? 300 : 750;

View File

@ -1,4 +1,5 @@
import { IS_IOS } from './environment'; import { IS_IOS } from './environment';
import forceReflow from './forceReflow';
export default (container: HTMLDivElement, scrollTop?: number) => { export default (container: HTMLDivElement, scrollTop?: number) => {
if (IS_IOS) { if (IS_IOS) {
@ -13,3 +14,10 @@ export default (container: HTMLDivElement, scrollTop?: number) => {
container.style.overflow = ''; container.style.overflow = '';
} }
}; };
// Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=1264266
export function patchChromiumScroll(element: HTMLElement) {
element.style.display = 'none';
forceReflow(element);
element.style.display = '';
}