diff --git a/src/components/middle/MessageList.scss b/src/components/middle/MessageList.scss index 7e5855a1e..ffaa2c74c 100644 --- a/src/components/middle/MessageList.scss +++ b/src/components/middle/MessageList.scss @@ -5,7 +5,6 @@ width: 100%; margin-bottom: .5rem; - overflow-anchor: none; overflow: scroll; overflow-x: hidden; overflow-y: auto; diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index cb2295cb7..d78a88ed4 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -42,7 +42,7 @@ import { preventMessageInputBlur } from './helpers/preventMessageInputBlur'; import useOnChange from '../../hooks/useOnChange'; import useStickyDates from './hooks/useStickyDates'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; -import resetScroll from '../../util/resetScroll'; +import resetScroll, { patchChromiumScroll } from '../../util/resetScroll'; import fastSmoothScroll, { isAnimatingScroll } from '../../util/fastSmoothScroll'; import renderText from '../common/helpers/renderText'; import useLang from '../../hooks/useLang'; @@ -210,12 +210,17 @@ const MessageList: FC = ({ const { isScrolled, updateStickyDates } = useStickyDates(); + const isScrollingRef = useRef(); + const isScrollPatchNeededRef = useRef(); + const handleScroll = useCallback(() => { if (isScrollTopJustUpdatedRef.current) { isScrollTopJustUpdatedRef.current = false; return; } + isScrollingRef.current = true; + const container = containerRef.current!; if (!memoFocusingIdRef.current) { @@ -223,6 +228,8 @@ const MessageList: FC = ({ } runDebouncedForScroll(() => { + isScrollingRef.current = false; + fastRaf(() => { if (!container.parentElement) { return; @@ -320,7 +327,7 @@ const MessageList: FC = ({ const container = containerRef.current!; listItemElementsRef.current = Array.from(container.querySelectorAll('.message-list-item')); - // During animation + // TODO Consider removing if (!container.offsetParent) { return; } @@ -355,6 +362,7 @@ const MessageList: FC = ({ } const { scrollTop, scrollHeight, offsetHeight } = container; + // TODO Consider `scrollOffset = scrollHeight - scrollTop` const scrollOffset = scrollOffsetRef.current!; const lastItemElement = listItemElementsRef.current[listItemElementsRef.current.length - 1]; @@ -397,6 +405,7 @@ const MessageList: FC = ({ } const isResized = prevContainerHeight !== undefined && prevContainerHeight !== containerHeight; + // TODO Look up within active transition slide const anchor = anchorIdRef.current && document.getElementById(anchorIdRef.current); const unreadDivider = ( !anchor @@ -411,6 +420,11 @@ const MessageList: FC = ({ newScrollTop = scrollHeight - offsetHeight; } else if (anchor) { + if (isScrollPatchNeededRef.current) { + isScrollPatchNeededRef.current = false; + patchChromiumScroll(container); + } + const newAnchorTop = anchor.getBoundingClientRect().top; newScrollTop = scrollTop + (newAnchorTop - (anchorTopRef.current || 0)); } else if (unreadDivider) { @@ -515,6 +529,8 @@ const MessageList: FC = ({ threadId={threadId} type={type} isReady={isReady} + isScrollingRef={isScrollingRef} + isScrollPatchNeededRef={isScrollPatchNeededRef} threadTopMessageId={threadTopMessageId} hasLinkedChat={hasLinkedChat} isSchedule={messageGroups ? type === 'scheduled' : false} diff --git a/src/components/middle/MessageListContent.tsx b/src/components/middle/MessageListContent.tsx index 6cc18a92f..fa75fb46f 100644 --- a/src/components/middle/MessageListContent.tsx +++ b/src/components/middle/MessageListContent.tsx @@ -31,6 +31,8 @@ interface OwnProps { threadId: number; type: MessageListType; isReady: boolean; + isScrollingRef: { current: boolean | undefined }; + isScrollPatchNeededRef: { current: boolean | undefined }; threadTopMessageId: number | undefined; hasLinkedChat: boolean | undefined; isSchedule: boolean; @@ -56,6 +58,8 @@ const MessageListContent: FC = ({ threadId, type, isReady, + isScrollingRef, + isScrollPatchNeededRef, threadTopMessageId, hasLinkedChat, isSchedule, @@ -83,6 +87,8 @@ const MessageListContent: FC = ({ onFabToggle, onNotchToggle, isReady, + isScrollingRef, + isScrollPatchNeededRef, ); const lang = useLang(); diff --git a/src/components/middle/hooks/useScrollHooks.ts b/src/components/middle/hooks/useScrollHooks.ts index d8d45eb21..2a6a11aea 100644 --- a/src/components/middle/hooks/useScrollHooks.ts +++ b/src/components/middle/hooks/useScrollHooks.ts @@ -5,10 +5,11 @@ import { useMemo, useRef } from '../../../lib/teact/teact'; import { LoadMoreDirection } from '../../../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 { useIntersectionObserver, useOnIntersect } from '../../../hooks/useIntersectionObserver'; -import { LOCAL_MESSAGE_ID_BASE, MESSAGE_LIST_SENSITIVE_AREA } from '../../../config'; import resetScroll from '../../../util/resetScroll'; +import { useIntersectionObserver, useOnIntersect } from '../../../hooks/useIntersectionObserver'; import useOnChange from '../../../hooks/useOnChange'; const FAB_THRESHOLD = 50; @@ -24,6 +25,8 @@ export default function useScrollHooks( onFabToggle: AnyToVoidFunction, onNotchToggle: AnyToVoidFunction, isReady: boolean, + isScrollingRef: { current: boolean | undefined }, + isScrollPatchNeededRef: { current: boolean | undefined }, ) { const { loadViewportMessages } = getDispatch(); @@ -91,9 +94,17 @@ export default function useScrollHooks( const { target } = triggerEntry; 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!); loadMoreBackwards(); } else if (target.className === 'forwards-trigger') { + // TODO Consider removing resetScroll(containerRef.current!); loadMoreForwards(); } diff --git a/src/config.ts b/src/config.ts index 03a04820c..d40f1dcab 100644 --- a/src/config.ts +++ b/src/config.ts @@ -41,7 +41,6 @@ const isBigScreen = typeof window !== 'undefined' && window.innerHeight >= 900; 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_VIEWPORT_LIMIT = MESSAGE_LIST_SLICE * 2; diff --git a/src/util/environment.ts b/src/util/environment.ts index 84d4651ae..9d751eb0b 100644 --- a/src/util/environment.ts +++ b/src/util/environment.ts @@ -39,9 +39,11 @@ export const IS_MAC_OS = PLATFORM_ENV === 'macOS'; export const IS_IOS = PLATFORM_ENV === 'iOS'; export const IS_ANDROID = PLATFORM_ENV === 'Android'; export const IS_SAFARI = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); -export const IS_PWA = window.matchMedia('(display-mode: standalone)').matches -|| (window.navigator as any).standalone -|| document.referrer.includes('android-app://'); +export const IS_PWA = ( + window.matchMedia('(display-mode: standalone)').matches + || (window.navigator as any).standalone + || document.referrer.includes('android-app://') +); export const IS_TOUCH_ENV = window.matchMedia('(pointer: coarse)').matches; // 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 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; diff --git a/src/util/resetScroll.ts b/src/util/resetScroll.ts index 12acc3683..6f46d78c0 100644 --- a/src/util/resetScroll.ts +++ b/src/util/resetScroll.ts @@ -1,4 +1,5 @@ import { IS_IOS } from './environment'; +import forceReflow from './forceReflow'; export default (container: HTMLDivElement, scrollTop?: number) => { if (IS_IOS) { @@ -13,3 +14,10 @@ export default (container: HTMLDivElement, scrollTop?: number) => { 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 = ''; +}