nicolium: experimental timelines: ux improvements
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -3,7 +3,6 @@ import clsx from 'clsx';
|
||||
import React, { useRef } from 'react';
|
||||
import { defineMessages, FormattedList, FormattedMessage } from 'react-intl';
|
||||
|
||||
import LoadMore from '@/components/load-more';
|
||||
import ScrollTopButton from '@/components/scroll-top-button';
|
||||
import ScrollableList from '@/components/scrollable-list';
|
||||
import Status, { StatusFollowedTagInfo } from '@/components/statuses/status';
|
||||
@ -177,7 +176,6 @@ const TimelineStatus: React.FC<ITimelineStatus> = (props): React.JSX.Element =>
|
||||
<div
|
||||
className={clsx('⁂-timeline-status relative', {
|
||||
'⁂-timeline-status--connected-bottom': isConnectedBottom,
|
||||
'border-b border-solid border-gray-200 dark:border-gray-800': !isConnectedBottom,
|
||||
'⁂-timeline-status--connected-top': isConnectedTop,
|
||||
})}
|
||||
>
|
||||
@ -206,8 +204,16 @@ interface ITimeline {
|
||||
const Timeline: React.FC<ITimeline> = ({ query, contextType = 'public' }) => {
|
||||
const node = useRef<VirtuosoHandle | null>(null);
|
||||
|
||||
const { timelineId, entries, queuedCount, fetchNextPage, dequeueEntries, isFetching, isPending } =
|
||||
query;
|
||||
const {
|
||||
timelineId,
|
||||
entries,
|
||||
queuedCount,
|
||||
fetchNextPage,
|
||||
dequeueEntries,
|
||||
isFetching,
|
||||
isPending,
|
||||
hasNextPage,
|
||||
} = query;
|
||||
|
||||
const handleMoveUp = (index: number) =>
|
||||
selectChild(index - 1, node, document.getElementById('status-list') ?? undefined);
|
||||
@ -239,13 +245,6 @@ const Timeline: React.FC<ITimeline> = ({ query, contextType = 'public' }) => {
|
||||
/>
|
||||
);
|
||||
}
|
||||
if ((entry.type === 'page-end' || entry.type === 'page-start') && !isFetching) {
|
||||
return (
|
||||
<div className='m-4'>
|
||||
<LoadMore key='load-more' onClick={fetchNextPage} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@ -266,7 +265,8 @@ const Timeline: React.FC<ITimeline> = ({ query, contextType = 'public' }) => {
|
||||
placeholderComponent={() => <PlaceholderTimelineStatus />}
|
||||
placeholderCount={20}
|
||||
ref={node}
|
||||
hasMore
|
||||
hasMore={hasNextPage}
|
||||
onLoadMore={fetchNextPage}
|
||||
>
|
||||
{(entries || []).map(renderEntry)}
|
||||
</ScrollableList>
|
||||
|
||||
@ -38,11 +38,9 @@ const useTimeline = (timelineId: string, fetcher: TimelineFetcher, streamConfig?
|
||||
|
||||
const fetchNextPage = useCallback(async () => {
|
||||
timelineActions.setLoading(timelineId, true);
|
||||
const lastEntry = timeline.entries.at(-1);
|
||||
if (!lastEntry || lastEntry.type !== 'page-end') return;
|
||||
|
||||
try {
|
||||
const response = await fetcher({ max_id: lastEntry.minId });
|
||||
const response = await fetcher({ max_id: timeline.oldestStatusId });
|
||||
|
||||
importEntities({ statuses: response.items });
|
||||
|
||||
@ -50,7 +48,7 @@ const useTimeline = (timelineId: string, fetcher: TimelineFetcher, streamConfig?
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}, [timelineId, timeline.entries]);
|
||||
}, [timelineId, timeline.oldestStatusId]);
|
||||
|
||||
const dequeueEntries = useCallback(() => {
|
||||
timelineActions.dequeueEntries(timelineId);
|
||||
|
||||
@ -19,14 +19,6 @@ type TimelineEntry =
|
||||
type: 'gap';
|
||||
sinceId: string;
|
||||
maxId: string;
|
||||
}
|
||||
| {
|
||||
type: 'page-start';
|
||||
maxId?: string;
|
||||
}
|
||||
| {
|
||||
type: 'page-end';
|
||||
minId?: string;
|
||||
};
|
||||
|
||||
interface TimelineData {
|
||||
@ -35,6 +27,8 @@ interface TimelineData {
|
||||
queuedCount: number;
|
||||
isFetching: boolean;
|
||||
isPending: boolean;
|
||||
hasNextPage: boolean;
|
||||
oldestStatusId?: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@ -43,8 +37,8 @@ interface State {
|
||||
expandTimeline: (
|
||||
timelineId: string,
|
||||
statuses: Array<Status>,
|
||||
hasMore: boolean,
|
||||
initialFetch: boolean,
|
||||
hasMore?: boolean,
|
||||
initialFetch?: boolean,
|
||||
) => void;
|
||||
receiveStreamingStatus: (timelineId: string, status: Status) => void;
|
||||
deleteStatus: (statusId: string) => void;
|
||||
@ -53,7 +47,7 @@ interface State {
|
||||
};
|
||||
}
|
||||
|
||||
const processPage = (statuses: Array<Status>, hasMore: boolean): Array<TimelineEntry> => {
|
||||
const processPage = (statuses: Array<Status>): Array<TimelineEntry> => {
|
||||
const timelinePage: Array<TimelineEntry> = [];
|
||||
|
||||
const processStatus = (status: Status): boolean => {
|
||||
@ -111,12 +105,6 @@ const processPage = (statuses: Array<Status>, hasMore: boolean): Array<TimelineE
|
||||
processStatus(status);
|
||||
}
|
||||
|
||||
if (hasMore)
|
||||
timelinePage.push({
|
||||
type: 'page-end',
|
||||
minId: statuses.at(-1)?.id,
|
||||
});
|
||||
|
||||
return timelinePage;
|
||||
};
|
||||
|
||||
@ -124,16 +112,20 @@ const useTimelinesStore = create<State>()(
|
||||
mutative((set) => ({
|
||||
timelines: {} as Record<string, TimelineData>,
|
||||
actions: {
|
||||
expandTimeline: (timelineId, statuses, hasMore, initialFetch) =>
|
||||
expandTimeline: (timelineId, statuses, hasMore, initialFetch = false) =>
|
||||
set((state) => {
|
||||
const timeline = state.timelines[timelineId] ?? createEmptyTimeline();
|
||||
const entries = processPage(statuses, hasMore);
|
||||
const entries = processPage(statuses);
|
||||
|
||||
if (initialFetch) timeline.entries = [];
|
||||
else if (timeline.entries.at(-1)?.type === 'page-end') timeline.entries.pop();
|
||||
timeline.entries.push(...entries);
|
||||
if (initialFetch) timeline.entries = entries;
|
||||
else timeline.entries.push(...entries);
|
||||
timeline.isPending = false;
|
||||
timeline.isFetching = false;
|
||||
if (typeof hasMore === 'boolean') {
|
||||
timeline.hasNextPage = hasMore;
|
||||
const oldestStatus = statuses.at(-1);
|
||||
if (oldestStatus) timeline.oldestStatusId = oldestStatus.id;
|
||||
}
|
||||
state.timelines[timelineId] = timeline;
|
||||
}),
|
||||
receiveStreamingStatus: (timelineId, status) => {
|
||||
@ -169,10 +161,11 @@ const useTimelinesStore = create<State>()(
|
||||
},
|
||||
setLoading: (timelineId, isFetching) =>
|
||||
set((state) => {
|
||||
const timeline = (state.timelines[timelineId] ??= createEmptyTimeline());
|
||||
const timeline = state.timelines[timelineId] ?? createEmptyTimeline();
|
||||
|
||||
timeline.isFetching = isFetching;
|
||||
if (!isFetching) timeline.isPending = false;
|
||||
state.timelines[timelineId] = timeline;
|
||||
}),
|
||||
dequeueEntries: (timelineId) =>
|
||||
set((state) => {
|
||||
@ -180,7 +173,7 @@ const useTimelinesStore = create<State>()(
|
||||
|
||||
if (!timeline || timeline.queuedEntries.length === 0) return;
|
||||
|
||||
const processedEntries = processPage(timeline.queuedEntries, false);
|
||||
const processedEntries = processPage(timeline.queuedEntries);
|
||||
|
||||
timeline.entries.unshift(...processedEntries);
|
||||
timeline.queuedEntries = [];
|
||||
@ -196,6 +189,8 @@ const createEmptyTimeline = (): TimelineData => ({
|
||||
queuedCount: 0,
|
||||
isFetching: false,
|
||||
isPending: true,
|
||||
hasNextPage: true,
|
||||
oldestStatusId: undefined,
|
||||
});
|
||||
|
||||
const emptyTimeline = createEmptyTimeline();
|
||||
|
||||
@ -307,6 +307,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.⁂-timeline-status--connected-bottom .status__content-wrapper {
|
||||
padding-left: 54px;
|
||||
.⁂-timeline-status {
|
||||
&--connected-bottom .status__content-wrapper {
|
||||
padding-left: 54px;
|
||||
}
|
||||
|
||||
&:not(.⁂-timeline-status--connected-bottom) {
|
||||
@apply border-b border-solid border-gray-200 dark:border-gray-800;
|
||||
}
|
||||
}
|
||||
|
||||
div:last-child > .⁂-timeline-status {
|
||||
@apply border-b-0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user