bigbuffet-rw/app/soapbox/components/status_list.tsx

239 lines
6.6 KiB
TypeScript
Raw Normal View History

2022-06-02 11:32:08 -07:00
import classNames from 'classnames';
import { debounce } from 'lodash';
import React, { useRef, useCallback } from 'react';
import { FormattedMessage, defineMessages } from 'react-intl';
import StatusContainer from 'soapbox/containers/status_container';
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
import PendingStatus from 'soapbox/features/ui/components/pending_status';
import LoadGap from './load_gap';
import ScrollableList from './scrollable_list';
import TimelineQueueButtonHeader from './timeline_queue_button_header';
import type {
Map as ImmutableMap,
OrderedSet as ImmutableOrderedSet,
} from 'immutable';
import type { VirtuosoHandle } from 'react-virtuoso';
2022-06-02 12:00:35 -07:00
import type { IScrollableList } from 'soapbox/components/scrollable_list';
2022-06-02 11:32:08 -07:00
const messages = defineMessages({
queue: { id: 'status_list.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {post} other {posts}}' },
});
2022-06-02 12:00:35 -07:00
interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
2022-06-02 11:32:08 -07:00
scrollKey: string,
statusIds: ImmutableOrderedSet<string>,
2022-06-02 12:00:35 -07:00
lastStatusId?: string,
2022-06-02 11:32:08 -07:00
featuredStatusIds?: ImmutableOrderedSet<string>,
2022-06-02 12:00:35 -07:00
onLoadMore?: (lastStatusId: string) => void,
2022-06-02 11:32:08 -07:00
isLoading: boolean,
2022-06-02 12:00:35 -07:00
isPartial?: boolean,
2022-06-02 11:32:08 -07:00
hasMore: boolean,
2022-06-02 12:00:35 -07:00
prepend?: React.ReactNode,
2022-06-02 11:32:08 -07:00
emptyMessage: React.ReactNode,
2022-06-02 12:00:35 -07:00
alwaysPrepend?: boolean,
timelineId?: string,
queuedItemSize?: number,
onDequeueTimeline?: (timelineId: string) => void,
totalQueuedItemsCount?: number,
2022-06-02 11:32:08 -07:00
group?: ImmutableMap<string, any>,
2022-06-02 12:00:35 -07:00
withGroupAdmin?: boolean,
onScrollToTop?: () => void,
onScroll?: () => void,
2022-06-02 11:32:08 -07:00
divideType: 'space' | 'border',
}
const StatusList: React.FC<IStatusList> = ({
statusIds,
lastStatusId,
featuredStatusIds,
divideType = 'border',
onLoadMore,
timelineId,
totalQueuedItemsCount,
onDequeueTimeline,
isLoading,
isPartial,
withGroupAdmin,
group,
...other
}) => {
const node = useRef<VirtuosoHandle>(null);
const getFeaturedStatusCount = () => {
return featuredStatusIds?.size || 0;
};
const getCurrentStatusIndex = (id: string, featured: boolean): number => {
if (featured) {
return featuredStatusIds?.keySeq().findIndex(key => key === id) || 0;
} else {
return statusIds.keySeq().findIndex(key => key === id) + getFeaturedStatusCount();
}
};
const handleMoveUp = (id: string, featured: boolean = false) => {
const elementIndex = getCurrentStatusIndex(id, featured) - 1;
selectChild(elementIndex);
};
const handleMoveDown = (id: string, featured: boolean = false) => {
const elementIndex = getCurrentStatusIndex(id, featured) + 1;
selectChild(elementIndex);
};
const handleLoadOlder = useCallback(debounce(() => {
const loadMoreID = lastStatusId || statusIds.last();
2022-06-02 12:00:35 -07:00
if (onLoadMore && loadMoreID) {
2022-06-02 11:32:08 -07:00
onLoadMore(loadMoreID);
}
}, 300, { leading: true }), []);
const selectChild = (index: number) => {
node.current?.scrollIntoView({
index,
behavior: 'smooth',
done: () => {
const element: HTMLElement | null = document.querySelector(`#status-list [data-index="${index}"] .focusable`);
element?.focus();
},
});
};
const handleDequeueTimeline = () => {
if (!onDequeueTimeline || !timelineId) return;
onDequeueTimeline(timelineId);
};
const renderLoadGap = (index: number) => {
const ids = statusIds.toList();
2022-06-02 12:00:35 -07:00
const nextId = ids.get(index + 1);
const prevId = ids.get(index - 1);
if (index < 1 || !nextId || !prevId || !onLoadMore) return null;
2022-06-02 11:32:08 -07:00
return (
<LoadGap
2022-06-02 12:00:35 -07:00
key={'gap:' + nextId}
2022-06-02 11:32:08 -07:00
disabled={isLoading}
2022-06-02 12:00:35 -07:00
maxId={prevId!}
2022-06-02 11:32:08 -07:00
onClick={onLoadMore}
/>
);
};
const renderStatus = (statusId: string) => {
return (
// @ts-ignore
<StatusContainer
key={statusId}
id={statusId}
onMoveUp={handleMoveUp}
onMoveDown={handleMoveDown}
contextType={timelineId}
/>
);
};
const renderPendingStatus = (statusId: string) => {
const idempotencyKey = statusId.replace(/^末pending-/, '');
return (
<PendingStatus
key={statusId}
idempotencyKey={idempotencyKey}
/>
);
};
2022-06-02 12:00:35 -07:00
const renderFeaturedStatuses = (): React.ReactNode[] => {
2022-06-02 11:32:08 -07:00
if (!featuredStatusIds) return [];
return featuredStatusIds.toArray().map(statusId => (
// @ts-ignore
<StatusContainer
key={`f-${statusId}`}
id={statusId}
featured
onMoveUp={handleMoveUp}
onMoveDown={handleMoveDown}
contextType={timelineId}
/>
));
};
2022-06-02 12:00:35 -07:00
const renderStatuses = (): React.ReactNode[] => {
2022-06-02 11:32:08 -07:00
if (isLoading || statusIds.size > 0) {
return statusIds.toArray().map((statusId, index) => {
if (statusId === null) {
return renderLoadGap(index);
} else if (statusId.startsWith('末pending-')) {
return renderPendingStatus(statusId);
} else {
return renderStatus(statusId);
}
});
} else {
return [];
}
};
const renderScrollableContent = () => {
const featuredStatuses = renderFeaturedStatuses();
const statuses = renderStatuses();
if (featuredStatuses && statuses) {
return featuredStatuses.concat(statuses);
} else {
return statuses;
}
};
if (isPartial) {
return (
<div className='regeneration-indicator'>
<div>
<div className='regeneration-indicator__label'>
<FormattedMessage id='regeneration_indicator.label' tagName='strong' defaultMessage='Loading&hellip;' />
<FormattedMessage id='regeneration_indicator.sublabel' defaultMessage='Your home feed is being prepared!' />
</div>
</div>
</div>
);
}
return (
<>
<TimelineQueueButtonHeader
key='timeline-queue-button-header'
onClick={handleDequeueTimeline}
count={totalQueuedItemsCount}
message={messages.queue}
/>
<ScrollableList
id='status-list'
key='scrollable-list'
isLoading={isLoading}
showLoading={isLoading && statusIds.size === 0}
onLoadMore={handleLoadOlder}
placeholderComponent={PlaceholderStatus}
placeholderCount={20}
ref={node}
className={classNames('divide-y divide-solid divide-gray-200 dark:divide-slate-700', {
'divide-none': divideType !== 'border',
})}
itemClassName={classNames({
'pb-3': divideType !== 'border',
})}
{...other}
>
{renderScrollableContent()}
</ScrollableList>,
</>
);
};
export default StatusList;