From b9ca1c78662fe541edfd188051647c927afd15b2 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 2 Dec 2024 20:43:10 -0300 Subject: [PATCH] refactor: make PureStatusList the same as StatusList component (except without immutable, statusIds, etc...) --- src/components/pure-status-list.tsx | 159 ++++++++++++++++++---------- 1 file changed, 106 insertions(+), 53 deletions(-) diff --git a/src/components/pure-status-list.tsx b/src/components/pure-status-list.tsx index 4440506b1..6a8aa9064 100644 --- a/src/components/pure-status-list.tsx +++ b/src/components/pure-status-list.tsx @@ -1,29 +1,48 @@ - -import { useRef } from 'react'; -import { VirtuosoHandle } from 'react-virtuoso'; +import clsx from 'clsx'; +import { debounce } from 'es-toolkit'; +import { useRef, useCallback } from 'react'; +import { FormattedMessage } from 'react-intl'; import LoadGap from 'soapbox/components/load-gap.tsx'; import PureStatus from 'soapbox/components/pure-status.tsx'; -import ScrollableList, { IScrollableList } from 'soapbox/components/scrollable-list.tsx'; +import ScrollableList from 'soapbox/components/scrollable-list.tsx'; import StatusContainer from 'soapbox/containers/status-container.tsx'; import { EntityTypes, Entities } from 'soapbox/entity-store/entities.ts'; import FeedSuggestions from 'soapbox/features/feed-suggestions/feed-suggestions.tsx'; +import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder-status.tsx'; import PendingStatus from 'soapbox/features/ui/components/pending-status.tsx'; import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts'; +import type { VirtuosoHandle } from 'react-virtuoso'; +import type { IScrollableList } from 'soapbox/components/scrollable-list.tsx'; interface IPureStatusList extends Omit{ + /** Unique key to preserve the scroll position when navigating back. */ + scrollKey: string; /** List of statuses to display. */ - statuses: readonly EntityTypes[Entities.STATUSES][]|null; + statuses: readonly EntityTypes[Entities.STATUSES][]; + /** Last _unfiltered_ status ID (maxId) for pagination. */ + lastStatusId?: string; /** Pinned statuses to show at the top of the feed. */ - featuredStatusIds?: Set; - /** Whether the data is currently being fetched. */ - isLoading: boolean; + featuredStatuses?: readonly EntityTypes[Entities.STATUSES][]; /** Pagination callback when the end of the list is reached. */ onLoadMore?: (lastStatusId: string) => void; - - - [key: string]: any; + /** 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; + /** Message to display when the list is loaded but empty. */ + emptyMessage: React.ReactNode; + /** ID of the timeline in Redux. */ + timelineId?: string; + /** Whether to display a gap or border between statuses in the list. */ + divideType?: 'space' | 'border'; + /** Whether to display ads. */ + showAds?: boolean; + /** Whether to show group information. */ + showGroup?: boolean; } /** @@ -31,31 +50,28 @@ interface IPureStatusList extends Omit = ({ statuses, - featuredStatusIds, - isLoading, + lastStatusId, + featuredStatuses, + divideType = 'border', onLoadMore, + timelineId, + isLoading, + isPartial, + showAds = false, + showGroup = true, + className, + ...other }) => { - const node = useRef(null); const soapboxConfig = useSoapboxConfig(); + const node = useRef(null); const getFeaturedStatusCount = () => { - return featuredStatusIds?.size || 0; - }; - - const selectChild = (index: number) => { - node.current?.scrollIntoView({ - index, - behavior: 'smooth', - done: () => { - const element = document.querySelector(`#status-list [data-index="${index}"] .focusable`); - element?.focus(); - }, - }); + return featuredStatuses?.length || 0; }; const getCurrentStatusIndex = (id: string, featured: boolean): number => { if (featured) { - return Array.from(featuredStatusIds?.keys() ?? []).findIndex(key => key === id) || 0; + return (featuredStatuses ?? []).findIndex(key => key.id === id) || 0; } else { return ( (statuses?.map(status => status.id) ?? []).findIndex(key => key === id) + @@ -69,6 +85,29 @@ const PureStatusList: React.FC = ({ selectChild(elementIndex); }; + const handleMoveDown = (id: string, featured: boolean = false) => { + const elementIndex = getCurrentStatusIndex(id, featured) + 1; + selectChild(elementIndex); + }; + + const handleLoadOlder = useCallback(debounce(() => { + const maxId = lastStatusId || statuses.slice(-1)[0].id; + if (onLoadMore && maxId) { + onLoadMore(maxId.replace('末suggestions-', '')); + } + }, 300, { edges: ['leading'] }), [onLoadMore, lastStatusId, statuses.slice(-1)[0].id]); + + const selectChild = (index: number) => { + node.current?.scrollIntoView({ + index, + behavior: 'smooth', + done: () => { + const element = document.querySelector(`#status-list [data-index="${index}"] .focusable`); + element?.focus(); + }, + }); + }; + const renderLoadGap = (index: number) => { const ids = statuses?.map(status => status.id) ?? []; const nextId = ids[index + 1]; @@ -93,10 +132,9 @@ const PureStatusList: React.FC = ({ key={status.id} id={status.id} onMoveUp={handleMoveUp} - // onMoveDown={handleMoveDown} - // contextType={timelineId} - // showGroup={showGroup} - // variant={divideType === 'border' ? 'slim' : 'rounded'} + onMoveDown={handleMoveDown} + showGroup={showGroup} + variant={divideType === 'border' ? 'slim' : 'rounded'} /> ); }; @@ -113,18 +151,18 @@ const PureStatusList: React.FC = ({ }; const renderFeaturedStatuses = (): React.ReactNode[] => { - if (!featuredStatusIds) return []; + if (!featuredStatuses) return []; - return Array.from(featuredStatusIds).map(statusId => ( + return (featuredStatuses ?? []).map(status => ( )); }; @@ -135,7 +173,7 @@ const PureStatusList: React.FC = ({ key='suggestions' statusId={statusId} onMoveUp={handleMoveUp} - // onMoveDown={handleMoveDown} + onMoveDown={handleMoveDown} /> ); }; @@ -177,23 +215,38 @@ const PureStatusList: React.FC = ({ } }; + if (isPartial) { + return ( +
+
+
+ + + + +
+
+
+ ); + } + return ( } - // placeholderCount={20} - // ref={node} - // listClassName={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', { - // 'divide-none': divideType !== 'border', - // }, className)} - // itemClassName={clsx({ - // 'pb-3': divideType !== 'border', - // })} - // {...other} + isLoading={isLoading} + showLoading={isLoading && statuses.length === 0} + onLoadMore={handleLoadOlder} + placeholderComponent={() => } + placeholderCount={20} + ref={node} + listClassName={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', { + 'divide-none': divideType !== 'border', + }, className)} + itemClassName={clsx({ + 'pb-3': divideType !== 'border', + })} + {...other} > {renderScrollableContent()}