UNPKG

stream-chat-react

Version:

React components to create chat conversations or livestream style chat

100 lines (99 loc) 4.88 kB
import React, { useEffect, useRef, useState } from 'react'; import { deprecationAndReplacementWarning } from '../../utils/deprecationWarning'; import { DEFAULT_LOAD_PAGE_SCROLL_THRESHOLD } from '../../constants/limits'; /** * Prevents Chrome hangups * See: https://stackoverflow.com/questions/47524205/random-high-content-download-time-in-chrome/47684257#47684257 */ const mousewheelListener = (event) => { if (event instanceof WheelEvent && event.deltaY === 1) { event.preventDefault(); } }; /** * This component serves a single purpose - load more items on scroll inside the MessageList component * It is not a general purpose infinite scroll controller, because: * 1. It is re-rendered whenever queryInProgress, hasNext, hasPrev changes. This can lead to scrollListener to have stale data. * 2. It pretends to invoke scrollListener on resize event even though this event is emitted only on window resize. It should * rather use ResizeObserver. But then again, it ResizeObserver would invoke a stale version of scrollListener. * * In general, the infinite scroll controller should not aim for checking the loading state and whether there is more data to load. * That should be controlled by the loading function. */ export const InfiniteScroll = (props) => { const { children, element: Component = 'div', hasMore, hasMoreNewer, hasNextPage, hasPreviousPage, head, initialLoad = true, isLoading, listenToScroll, loader, loadMore, loadMoreNewer, loadNextPage, loadPreviousPage, threshold = DEFAULT_LOAD_PAGE_SCROLL_THRESHOLD, useCapture = false, ...elementProps } = props; const loadNextPageFn = loadNextPage || loadMoreNewer; const loadPreviousPageFn = loadPreviousPage || loadMore; const hasNextPageFlag = hasNextPage || hasMoreNewer; const hasPreviousPageFlag = hasPreviousPage || hasMore; const [scrollComponent, setScrollComponent] = useState(null); const previousOffset = useRef(undefined); const previousReverseOffset = useRef(undefined); const scrollListenerRef = useRef(undefined); scrollListenerRef.current = () => { const element = scrollComponent; if (!element || element.offsetParent === null) { return; } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const parentElement = element.parentElement; const offset = element.scrollHeight - parentElement.scrollTop - parentElement.clientHeight; const reverseOffset = parentElement.scrollTop; if (listenToScroll) { listenToScroll(offset, reverseOffset, threshold); } if (isLoading) return; if (previousOffset.current === offset && previousReverseOffset.current === reverseOffset) return; previousOffset.current = offset; previousReverseOffset.current = reverseOffset; // FIXME: this triggers loadMore call when a user types messages in thread and the scroll container expands if (reverseOffset < Number(threshold) && typeof loadPreviousPageFn === 'function' && hasPreviousPageFlag) { loadPreviousPageFn(); } if (offset < Number(threshold) && typeof loadNextPageFn === 'function' && hasNextPageFlag) { loadNextPageFn(); } }; useEffect(() => { deprecationAndReplacementWarning([ [{ hasMoreNewer }, { hasNextPage }], [{ loadMoreNewer }, { loadNextPage }], [{ hasMore }, { hasPreviousPage }], [{ loadMore }, { loadPreviousPage }], ], 'InfiniteScroll'); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { const scrollElement = scrollComponent?.parentNode; if (!scrollElement) return; const scrollListener = () => scrollListenerRef.current?.(); scrollElement.addEventListener('scroll', scrollListener, useCapture); scrollElement.addEventListener('resize', scrollListener, useCapture); scrollListener(); return () => { scrollElement.removeEventListener('scroll', scrollListener, useCapture); scrollElement.removeEventListener('resize', scrollListener, useCapture); }; }, [initialLoad, scrollComponent, useCapture]); useEffect(() => { const scrollElement = scrollComponent?.parentNode; if (!scrollElement) return; scrollElement.addEventListener('wheel', mousewheelListener, { passive: false }); return () => { scrollElement.removeEventListener('wheel', mousewheelListener, useCapture); }; }, [scrollComponent, useCapture]); return (React.createElement(Component, { ...elementProps, ref: setScrollComponent }, head, loader, children)); };