refactor: make PureStatusList the same as StatusList component (except without immutable, statusIds, etc...)

This commit is contained in:
P. Reis 2024-12-02 20:43:10 -03:00
parent a9e31c0041
commit b9ca1c7866
1 changed files with 106 additions and 53 deletions

View File

@ -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<IScrollableList, 'onLoadMore' | 'children'>{
/** 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<string>;
/** 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<IScrollableList, 'onLoadMore' | 'children
*/
const PureStatusList: React.FC<IPureStatusList> = ({
statuses,
featuredStatusIds,
isLoading,
lastStatusId,
featuredStatuses,
divideType = 'border',
onLoadMore,
timelineId,
isLoading,
isPartial,
showAds = false,
showGroup = true,
className,
...other
}) => {
const node = useRef<VirtuosoHandle>(null);
const soapboxConfig = useSoapboxConfig();
const node = useRef<VirtuosoHandle>(null);
const getFeaturedStatusCount = () => {
return featuredStatusIds?.size || 0;
};
const selectChild = (index: number) => {
node.current?.scrollIntoView({
index,
behavior: 'smooth',
done: () => {
const element = document.querySelector<HTMLDivElement>(`#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<IPureStatusList> = ({
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<HTMLDivElement>(`#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<IPureStatusList> = ({
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<IPureStatusList> = ({
};
const renderFeaturedStatuses = (): React.ReactNode[] => {
if (!featuredStatusIds) return [];
if (!featuredStatuses) return [];
return Array.from(featuredStatusIds).map(statusId => (
return (featuredStatuses ?? []).map(status => (
<StatusContainer
key={`f-${statusId}`}
id={statusId}
key={`f-${status.id}`}
id={status.id}
featured
onMoveUp={handleMoveUp}
// onMoveDown={handleMoveDown}
// contextType={timelineId}
// showGroup={showGroup}
// variant={divideType === 'border' ? 'slim' : 'default'}
onMoveDown={handleMoveDown}
contextType={timelineId}
showGroup={showGroup}
variant={divideType === 'border' ? 'slim' : 'default'}
/>
));
};
@ -135,7 +173,7 @@ const PureStatusList: React.FC<IPureStatusList> = ({
key='suggestions'
statusId={statusId}
onMoveUp={handleMoveUp}
// onMoveDown={handleMoveDown}
onMoveDown={handleMoveDown}
/>
);
};
@ -177,23 +215,38 @@ const PureStatusList: React.FC<IPureStatusList> = ({
}
};
if (isPartial) {
return (
<div className='flex flex-1 cursor-default items-center justify-center rounded-lg p-5 text-center text-[16px] font-medium text-gray-900 sm:rounded-none'>
<div className='w-full bg-transparent pt-0'>
<div>
<strong className='mb-2.5 block text-gray-900'>
<FormattedMessage id='regeneration_indicator.label' defaultMessage='Loading…' />
</strong>
<FormattedMessage id='regeneration_indicator.sublabel' defaultMessage='Your home feed is being prepared!' />
</div>
</div>
</div>
);
}
return (
<ScrollableList
id='status-list'
key='scrollable-list'
// isLoading={isLoading}
// showLoading={isLoading && statusIds.size === 0}
// onLoadMore={handleLoadOlder}
// placeholderComponent={() => <PlaceholderStatus variant={divideType === 'border' ? 'slim' : 'rounded'} />}
// 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={() => <PlaceholderStatus variant={divideType === 'border' ? 'slim' : 'rounded'} />}
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()}
</ScrollableList>