import { MutableRefObject } from 'react'; import React, { FC, useCallback, useEffect, 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[]; containerHeight?: number; listItemElementsRef: MutableRefObject; anchorIdRef: MutableRefObject; anchorTopRef: MutableRefObject; loadMoreForwards?: NoneToVoidFunction; loadMoreBackwards?: NoneToVoidFunction; isViewportNewest?: boolean; firstUnreadId?: number; focusingId?: number; onFabToggle: AnyToVoidFunction; children: any; }; const FAB_THRESHOLD = 50; const FAB_FREEZE_TIMEOUT = 100; // Local flag is used because `freeze/unfreeze` methods are controlled by heavy animation let isFabFrozen = false; const MessageScroll: FC = ({ containerRef, className, messageIds, containerHeight, listItemElementsRef, focusingId, anchorIdRef, anchorTopRef, loadMoreForwards, loadMoreBackwards, isViewportNewest, firstUnreadId, onFabToggle, 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 updateFabVisibility = useCallback(() => { if (isFabFrozen) { return; } if (!messageIds || !messageIds.length) { onFabToggle(false); return; } if (!isViewportNewest) { onFabToggle(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); }, [messageIds, isViewportNewest, containerRef, onFabToggle, firstUnreadId]); 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' && (target as HTMLDivElement).dataset.isActive) { resetScroll(containerRef.current!); loadMoreForwards(); } }); useOnIntersect(backwardsTriggerRef, observeIntersection); useOnIntersect(forwardsTriggerRef, observeIntersection); const { observe: observeIntersectionForFab, freeze: freezeForFab, unfreeze: unfreezeForFab, } = useIntersectionObserver({ rootRef: containerRef, margin: FAB_THRESHOLD, }, ([{ target }]) => { if ((target as HTMLDivElement).dataset.isActive) { updateFabVisibility(); } }); useOnIntersect(fabTriggerRef, observeIntersectionForFab); // Do not load more and show FAB when focusing useOnChange(() => { if (focusingId) { freezeForLoadMore(); freezeForFab(); } else { unfreezeForFab(); unfreezeForLoadMore(); } }, [focusingId]); // Remember scroll position before updating height useOnChange(() => { if (!listItemElementsRef.current) { return; } const preservedItemElements = listItemElementsRef.current .filter((element) => messageIds.includes(Number(element.dataset.messageId))); // We avoid the very first item as it may be a partly-loaded album // and also because it may be removed when messages limit is reached const anchor = preservedItemElements[1] || preservedItemElements[0]; if (!anchor) { return; } anchorIdRef.current = anchor.id; anchorTopRef.current = anchor.getBoundingClientRect().top; }, [messageIds, containerHeight]); // Workaround for FAB flickering with tall incoming message useOnChange(() => { isFabFrozen = true; setTimeout(() => { isFabFrozen = false; }, FAB_FREEZE_TIMEOUT); }, [messageIds]); // Workaround for stuck FAB when many unread messages useEffect(updateFabVisibility, [firstUnreadId]); return (
{children}
); }; export default MessageScroll;