diff --git a/app/soapbox/components/scroll-top-button.tsx b/app/soapbox/components/scroll-top-button.tsx index 8fed6cc78f..698c927793 100644 --- a/app/soapbox/components/scroll-top-button.tsx +++ b/app/soapbox/components/scroll-top-button.tsx @@ -9,13 +9,19 @@ import { useSettings } from 'soapbox/hooks'; import { shortNumberFormat } from 'soapbox/utils/numbers'; interface IScrollTopButton { + /** Callback when clicked, and also when scrolled to the top. */ onClick: () => void, + /** Number of unread items. */ count: number, + /** Message to display in the button (should contain a `{count}` value). */ message: MessageDescriptor, + /** Distance from the top of the screen (scrolling down) before the button appears. */ threshold?: number, + /** Distance from the top of the screen (scrolling up) before the action is triggered. */ autoloadThreshold?: number, } +/** Floating new post counter above timelines, clicked to scroll to top. */ const ScrollTopButton: React.FC = ({ onClick, count, @@ -41,7 +47,7 @@ const ScrollTopButton: React.FC = ({ } else { setScrolled(false); } - }, 150, { trailing: true }), []); + }, 150, { trailing: true }), [autoload, threshold, autoloadThreshold]); const scrollUp = () => { window.scrollTo({ top: 0, behavior: 'smooth' }); diff --git a/app/soapbox/components/scrollable_list.tsx b/app/soapbox/components/scrollable_list.tsx index e546f2a454..8b803be661 100644 --- a/app/soapbox/components/scrollable_list.tsx +++ b/app/soapbox/components/scrollable_list.tsx @@ -9,6 +9,7 @@ import { useSettings } from 'soapbox/hooks'; import LoadMore from './load_more'; import { Spinner, Text } from './ui'; +/** Custom Viruoso component context. */ type Context = { itemClassName?: string, listClassName?: string, @@ -20,6 +21,7 @@ type SavedScrollPosition = { offset: number, } +/** Custom Virtuoso Item component representing a single scrollable item. */ // 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 @@ -27,6 +29,7 @@ const Item: Components['Item'] = ({ context, ...rest }) => (
); +/** Custom Virtuoso List component for the outer container. */ // Ensure the className winds up here const List: Components['List'] = React.forwardRef((props, ref) => { const { context, ...rest } = props; @@ -34,28 +37,47 @@ const List: Components['List'] = React.forwardRef((props, ref) => { }); interface IScrollableList extends VirtuosoProps { + /** Unique key to preserve the scroll position when navigating back. */ scrollKey?: string, + /** Pagination callback when the end of the list is reached. */ onLoadMore?: () => void, + /** Whether the data is currently being fetched. */ isLoading?: boolean, + /** Whether to actually display the loading state. */ showLoading?: boolean, + /** Whether we expect an additional page of data. */ hasMore?: boolean, + /** Additional element to display at the top of the list. */ prepend?: React.ReactNode, + /** Whether to display the prepended element. */ alwaysPrepend?: boolean, + /** Message to display when the list is loaded but empty. */ emptyMessage?: React.ReactNode, + /** Scrollable content. */ children: Iterable, + /** Callback when the list is scrolled to the top. */ onScrollToTop?: () => void, + /** Callback when the list is scrolled. */ onScroll?: () => void, + /** Placeholder component to render while loading. */ placeholderComponent?: React.ComponentType | React.NamedExoticComponent, + /** Number of placeholders to render while loading. */ placeholderCount?: number, + /** Pull to refresh callback. */ onRefresh?: () => Promise, + /** Extra class names on the Virtuoso element. */ className?: string, + /** Class names on each item container. */ itemClassName?: string, + /** `id` attribute on the Virtuoso element. */ id?: string, + /** CSS styles on the Virtuoso element. */ style?: React.CSSProperties, + /** Whether to use the window to scroll the content instead of Virtuoso's container. */ useWindowScroll?: boolean } -/** Legacy ScrollableList with Virtuoso for backwards-compatibility */ +/** Legacy ScrollableList with Virtuoso for backwards-compatibility. */ const ScrollableList = React.forwardRef(({ scrollKey, prepend = null, @@ -88,7 +110,7 @@ const ScrollableList = React.forwardRef(({ const topIndex = useRef(scrollData ? scrollData.index : 0); const topOffset = useRef(scrollData ? scrollData.offset : 0); - /** Normalized children */ + /** Normalized children. */ const elements = Array.from(children || []); const showPlaceholder = showLoading && Placeholder && placeholderCount > 0; @@ -129,7 +151,7 @@ const ScrollableList = React.forwardRef(({ }; }, []); - /* Render an empty state instead of the scrollable list */ + /* Render an empty state instead of the scrollable list. */ const renderEmpty = (): JSX.Element => { return (
@@ -146,7 +168,7 @@ const ScrollableList = React.forwardRef(({ ); }; - /** Render a single item */ + /** Render a single item. */ const renderItem = (_i: number, element: JSX.Element): JSX.Element => { if (showPlaceholder) { return ; @@ -192,7 +214,7 @@ const ScrollableList = React.forwardRef(({ return 0; }, [showLoading, initialTopMostItemIndex]); - /** Render the actual Virtuoso list */ + /** Render the actual Virtuoso list. */ const renderFeed = (): JSX.Element => ( (({ /> ); - /** Conditionally render inner elements */ + /** Conditionally render inner elements. */ const renderBody = (): JSX.Element => { if (isEmpty) { return renderEmpty(); diff --git a/app/soapbox/components/status_list.tsx b/app/soapbox/components/status_list.tsx index edfcd505af..37e37f59df 100644 --- a/app/soapbox/components/status_list.tsx +++ b/app/soapbox/components/status_list.tsx @@ -14,24 +14,31 @@ import type { VirtuosoHandle } from 'react-virtuoso'; import type { IScrollableList } from 'soapbox/components/scrollable_list'; interface IStatusList extends Omit { + /** Unique key to preserve the scroll position when navigating back. */ scrollKey: string, + /** List of status IDs to display. */ statusIds: ImmutableOrderedSet, + /** Last _unfiltered_ status ID (maxId) for pagination. */ lastStatusId?: string, + /** Pinned statuses to show at the top of the feed. */ featuredStatusIds?: ImmutableOrderedSet, + /** Pagination callback when the end of the list is reached. */ onLoadMore?: (lastStatusId: string) => void, + /** Whether the data is currently being fetched. */ isLoading: boolean, + /** Whether the server did not return a complete page. */ isPartial?: boolean, + /** Whether we expect an additional page of data. */ hasMore: boolean, - prepend?: React.ReactNode, + /** Message to display when the list is loaded but empty. */ emptyMessage: React.ReactNode, - alwaysPrepend?: boolean, + /** ID of the timeline in Redux. */ timelineId?: string, - queuedItemSize?: number, - onScrollToTop?: () => void, - onScroll?: () => void, + /** Whether to display a gap or border between statuses in the list. */ divideType: 'space' | 'border', } +/** Feed of statuses, built atop ScrollableList. */ const StatusList: React.FC = ({ statusIds, lastStatusId, diff --git a/app/soapbox/features/ui/components/timeline.tsx b/app/soapbox/features/ui/components/timeline.tsx index 74efaac2ec..af3ff8e8c1 100644 --- a/app/soapbox/features/ui/components/timeline.tsx +++ b/app/soapbox/features/ui/components/timeline.tsx @@ -15,9 +15,11 @@ const messages = defineMessages({ }); interface ITimeline extends Omit { + /** ID of the timeline in Redux. */ timelineId: string, } +/** Scrollable list of statuses from a timeline in the Redux store. */ const Timeline: React.FC = ({ timelineId, onLoadMore, @@ -55,6 +57,7 @@ const Timeline: React.FC = ({ />