import { MutableRefObject } from 'react'; import React, { FC, useCallback, useRef } from '../../lib/teact/teact'; import { MESSAGE_LIST_SENSITIVE_AREA } from '../../config'; import resetScroll from '../../util/resetScroll'; import { useIntersectionObserver, useOnIntersect } from '../../hooks/useIntersectionObserver'; import useOnChange from '../../hooks/useOnChange'; type OwnProps = { containerRef: MutableRefObject; className: string; messageIds: number[]; focusingId?: number; loadMoreForwards?: NoneToVoidFunction; loadMoreBackwards?: NoneToVoidFunction; isViewportNewest?: boolean; firstUnreadId?: number; onFabToggle: AnyToVoidFunction; onNotchToggle: AnyToVoidFunction; children: any; }; const FAB_THRESHOLD = 50; const TOOLS_FREEZE_TIMEOUT = 1000; const MessageScroll: FC = ({ containerRef, className, messageIds, focusingId, loadMoreForwards, loadMoreBackwards, isViewportNewest, firstUnreadId, onFabToggle, onNotchToggle, children, }) => { // eslint-disable-next-line no-null/no-null const backwardsTriggerRef = useRef(null); // eslint-disable-next-line no-null/no-null const forwardsTriggerRef = useRef(null); // eslint-disable-next-line no-null/no-null const fabTriggerRef = useRef(null); const toggleScrollTools = useCallback(() => { if (!messageIds || !messageIds.length) { onFabToggle(false); onNotchToggle(false); return; } if (!isViewportNewest) { onFabToggle(true); onNotchToggle(true); return; } const { offsetHeight, scrollHeight, scrollTop } = containerRef.current!; const scrollBottom = scrollHeight - scrollTop - offsetHeight; const isNearBottom = scrollBottom <= FAB_THRESHOLD; const isAtBottom = scrollBottom <= 0; onFabToggle(firstUnreadId ? !isAtBottom : !isNearBottom); onNotchToggle(!isAtBottom); }, [messageIds, isViewportNewest, containerRef, onFabToggle, firstUnreadId, onNotchToggle]); const { observe: observeIntersection, freeze: freezeForLoadMore, unfreeze: unfreezeForLoadMore, } = useIntersectionObserver({ rootRef: containerRef, margin: MESSAGE_LIST_SENSITIVE_AREA, }, (entries) => { if (!loadMoreForwards || !loadMoreBackwards) { return; } const triggerEntry = entries.find(({ isIntersecting }) => isIntersecting); if (!triggerEntry) { return; } const { target } = triggerEntry; if (target.className === 'backwards-trigger') { resetScroll(containerRef.current!); loadMoreBackwards(); } else if (target.className === 'forwards-trigger') { resetScroll(containerRef.current!); loadMoreForwards(); } }); useOnIntersect(backwardsTriggerRef, observeIntersection); useOnIntersect(forwardsTriggerRef, observeIntersection); const { observe: observeIntersectionForFab, freeze: freezeForFab, unfreeze: unfreezeForFab, } = useIntersectionObserver({ rootRef: containerRef, margin: FAB_THRESHOLD, }, toggleScrollTools); useOnIntersect(fabTriggerRef, observeIntersectionForFab); const { observe: observeIntersectionForNotch, freeze: freezeForNotch, unfreeze: unfreezeForNotch, } = useIntersectionObserver({ rootRef: containerRef, }, toggleScrollTools); useOnIntersect(fabTriggerRef, observeIntersectionForNotch); useOnChange(() => { if (focusingId) { freezeForLoadMore(); } else { unfreezeForLoadMore(); } }, [focusingId]); // Workaround for FAB and notch flickering with tall incoming message useOnChange(() => { freezeForFab(); freezeForNotch(); setTimeout(() => { unfreezeForNotch(); unfreezeForFab(); }, TOOLS_FREEZE_TIMEOUT); }, [messageIds]); return (
{children}
); }; export default MessageScroll;