refactor: make PureStatusList the same as StatusList component (except without immutable, statusIds, etc...)
This commit is contained in:
parent
a9e31c0041
commit
b9ca1c7866
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue