import React from 'react'; import { Virtuoso, Components } from 'react-virtuoso'; import PullToRefresh from 'soapbox/components/pull-to-refresh'; import { Spinner, Text } from './ui'; type Context = { itemClassName?: string, listClassName?: string, } // NOTE: It's crucial to space lists with **padding** instead of margin! // Pass an `itemClassName` like `pb-3`, NOT a `space-y-3` className // https://virtuoso.dev/troubleshooting#list-does-not-scroll-to-the-bottom--items-jump-around const Item: Components['Item'] = ({ context, ...rest }) => (
); // Ensure the className winds up here const List: Components['List'] = React.forwardRef((props, ref) => { const { context, ...rest } = props; return
; }); interface IScrollableList { scrollKey?: string, onLoadMore?: () => void, isLoading?: boolean, showLoading?: boolean, hasMore?: boolean, prepend?: React.ReactElement, alwaysPrepend?: boolean, emptyMessage?: React.ReactNode, children: Iterable, onScrollToTop?: () => void, onScroll?: () => void, placeholderComponent?: React.ComponentType, placeholderCount?: number, onRefresh?: () => Promise, className?: string, itemClassName?: string, } /** Legacy ScrollableList with Virtuoso for backwards-compatibility */ const ScrollableList: React.FC = ({ prepend = null, alwaysPrepend, children, isLoading, emptyMessage, showLoading, onRefresh, onScroll, onScrollToTop, onLoadMore, className, itemClassName, hasMore, placeholderComponent: Placeholder, placeholderCount = 0, }) => { /** Normalized children */ const elements = Array.from(children || []); const showPlaceholder = showLoading && Placeholder && placeholderCount > 0; // NOTE: We are doing some trickery to load a feed of placeholders // Virtuoso's `EmptyPlaceholder` unfortunately doesn't work for our use-case const data = showPlaceholder ? Array(placeholderCount).fill('') : elements; const isEmpty = data.length === 0; // Yes, if it has placeholders it isn't "empty" // Add a placeholder at the bottom for loading // (Don't use Virtuoso's `Footer` component because it doesn't preserve its height) if (hasMore && Placeholder) { data.push(); } else if (hasMore) { data.push(); } /* Render an empty state instead of the scrollable list */ const renderEmpty = (): JSX.Element => { return (
{alwaysPrepend && prepend}
{isLoading ? ( ) : ( {emptyMessage} )}
); }; /** Render a single item */ const renderItem = (_i: number, element: JSX.Element): JSX.Element => { if (showPlaceholder) { return ; } else { return element; } }; const handleEndReached = () => { if (hasMore && onLoadMore) { onLoadMore(); } }; /** Render the actual Virtuoso list */ const renderFeed = (): JSX.Element => ( isScrolling && onScroll && onScroll()} itemContent={renderItem} context={{ listClassName: className, itemClassName, }} components={{ Header: () => prepend, ScrollSeekPlaceholder: Placeholder as any, EmptyPlaceholder: () => renderEmpty(), List, Item, }} /> ); /** Conditionally render inner elements */ const renderBody = (): JSX.Element => { if (isEmpty) { return renderEmpty(); } else { return renderFeed(); } }; return ( {renderBody()} ); }; export default ScrollableList;