nicolium: move new timelines state to zustand
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -20,7 +20,7 @@ import {
|
||||
} from '@/queries/timelines/use-timelines';
|
||||
|
||||
import type { FilterContextType } from '@/queries/settings/use-filters';
|
||||
import type { TimelineEntry } from '@/queries/timelines/use-timeline';
|
||||
import type { TimelineEntry } from '@/stores/timelines';
|
||||
|
||||
interface ITimelineStatus {
|
||||
id: string;
|
||||
@ -86,7 +86,7 @@ interface ITimeline {
|
||||
}
|
||||
|
||||
const Timeline: React.FC<ITimeline> = ({ query, contextType = 'public' }) => {
|
||||
const { data, handleLoadMore, isLoading } = query;
|
||||
const { entries, fetchNextPage, isFetching, isPending } = query;
|
||||
|
||||
const renderEntry = (entry: TimelineEntry) => {
|
||||
if (entry.type === 'status') {
|
||||
@ -102,14 +102,13 @@ const Timeline: React.FC<ITimeline> = ({ query, contextType = 'public' }) => {
|
||||
// contextType={timelineId}
|
||||
// showGroup={showGroup}
|
||||
// variant={divideType === 'border' ? 'slim' : 'rounded'}
|
||||
// fromBookmarks={other.scrollKey === 'bookmarked_statuses'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (entry.type === 'page-end' || entry.type === 'page-start') {
|
||||
return (
|
||||
<div className='m-4'>
|
||||
<LoadMore key='load-more' onClick={() => handleLoadMore(entry)} disabled={isLoading} />
|
||||
<LoadMore key='load-more' onClick={fetchNextPage} disabled={isFetching} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -119,8 +118,8 @@ const Timeline: React.FC<ITimeline> = ({ query, contextType = 'public' }) => {
|
||||
<ScrollableList
|
||||
id='status-list'
|
||||
key='scrollable-list'
|
||||
isLoading={isLoading}
|
||||
showLoading={isLoading && !data}
|
||||
isLoading={isFetching}
|
||||
showLoading={isPending}
|
||||
placeholderComponent={() => <PlaceholderStatus variant={'slim'} />}
|
||||
placeholderCount={20}
|
||||
// className={className}
|
||||
@ -132,7 +131,7 @@ const Timeline: React.FC<ITimeline> = ({ query, contextType = 'public' }) => {
|
||||
// })}
|
||||
// {...other}
|
||||
>
|
||||
{(data || []).map(renderEntry)}
|
||||
{(entries || []).map(renderEntry)}
|
||||
</ScrollableList>
|
||||
);
|
||||
};
|
||||
|
||||
@ -7,7 +7,6 @@ import type { MinifiedInteractionRequest } from './statuses/use-interaction-requ
|
||||
import type { MinifiedContext } from './statuses/use-status';
|
||||
import type { MinifiedStatusEdit } from './statuses/use-status-history';
|
||||
import type { MinifiedEmojiReaction } from './statuses/use-status-interactions';
|
||||
import type { TimelineEntry } from './timelines/use-timeline';
|
||||
import type { MinifiedSuggestion } from './trends/use-suggested-accounts';
|
||||
import type {
|
||||
MinifiedAdminAccount,
|
||||
@ -34,10 +33,8 @@ import type {
|
||||
AdminRule,
|
||||
Announcement,
|
||||
Antenna,
|
||||
AntennaTimelineParams,
|
||||
Backup,
|
||||
BookmarkFolder,
|
||||
BubbleTimelineParams,
|
||||
Chat,
|
||||
Circle,
|
||||
CredentialAccount,
|
||||
@ -48,29 +45,21 @@ import type {
|
||||
Group,
|
||||
GroupRelationship,
|
||||
GroupRole,
|
||||
GroupTimelineParams,
|
||||
HashtagTimelineParams,
|
||||
HomeTimelineParams,
|
||||
InteractionPolicies,
|
||||
LinkTimelineParams,
|
||||
List,
|
||||
ListTimelineParams,
|
||||
Location,
|
||||
Marker,
|
||||
NotificationGroup,
|
||||
OauthToken,
|
||||
PaginatedResponse,
|
||||
PaginationParams,
|
||||
PlApiClient,
|
||||
Poll,
|
||||
PublicTimelineParams,
|
||||
Relationship,
|
||||
RssFeed,
|
||||
ScheduledStatus,
|
||||
Tag,
|
||||
Translation,
|
||||
TrendsLink,
|
||||
WrenchedTimelineParams,
|
||||
} from 'pl-api';
|
||||
|
||||
type TaggedKey<TKey extends readonly unknown[], TData> = DataTag<TKey, TData>;
|
||||
@ -429,46 +418,6 @@ const suggestions = {
|
||||
all: ['suggestions'] as TaggedKey<['suggestions'], Array<MinifiedSuggestion>>,
|
||||
};
|
||||
|
||||
const timelines = {
|
||||
root: ['timelines'] as const,
|
||||
home: (params?: Omit<HomeTimelineParams, keyof PaginationParams>) => {
|
||||
const key = ['timelines', 'home', params] as const;
|
||||
return key as TaggedKey<typeof key, Array<TimelineEntry>>;
|
||||
},
|
||||
public: (local?: boolean, params?: Omit<PublicTimelineParams, keyof PaginationParams>) => {
|
||||
const key = ['timelines', 'public', { local, ...params }] as const;
|
||||
return key as TaggedKey<typeof key, Array<TimelineEntry>>;
|
||||
},
|
||||
hashtag: (hashtag: string, params?: Omit<HashtagTimelineParams, keyof PaginationParams>) => {
|
||||
const key = ['timelines', 'hashtag', hashtag, params] as const;
|
||||
return key as TaggedKey<typeof key, Array<TimelineEntry>>;
|
||||
},
|
||||
link: (url: string, params?: Omit<LinkTimelineParams, keyof PaginationParams>) => {
|
||||
const key = ['timelines', 'link', url, params] as const;
|
||||
return key as TaggedKey<typeof key, Array<TimelineEntry>>;
|
||||
},
|
||||
list: (listId: string, params?: Omit<ListTimelineParams, keyof PaginationParams>) => {
|
||||
const key = ['timelines', 'list', listId, params] as const;
|
||||
return key as TaggedKey<typeof key, Array<TimelineEntry>>;
|
||||
},
|
||||
group: (groupId: string, params?: Omit<GroupTimelineParams, keyof PaginationParams>) => {
|
||||
const key = ['timelines', 'group', groupId, params] as const;
|
||||
return key as TaggedKey<typeof key, Array<TimelineEntry>>;
|
||||
},
|
||||
bubble: (params?: Omit<BubbleTimelineParams, keyof PaginationParams>) => {
|
||||
const key = ['timelines', 'bubble', params] as const;
|
||||
return key as TaggedKey<typeof key, Array<TimelineEntry>>;
|
||||
},
|
||||
antenna: (antennaId: string, params?: Omit<AntennaTimelineParams, keyof PaginationParams>) => {
|
||||
const key = ['timelines', 'antenna', antennaId, params] as const;
|
||||
return key as TaggedKey<typeof key, Array<TimelineEntry>>;
|
||||
},
|
||||
wrenched: (params?: Omit<WrenchedTimelineParams, keyof PaginationParams>) => {
|
||||
const key = ['timelines', 'wrenched', params] as const;
|
||||
return key as TaggedKey<typeof key, Array<TimelineEntry>>;
|
||||
},
|
||||
};
|
||||
|
||||
const timelineIds = {
|
||||
root: ['timelineIds'] as const,
|
||||
accountMedia: (accountId: string) => {
|
||||
@ -691,7 +640,6 @@ const queryKeys = {
|
||||
search,
|
||||
trends,
|
||||
suggestions,
|
||||
timelines,
|
||||
timelineIds,
|
||||
settings,
|
||||
interactionPolicies,
|
||||
|
||||
@ -1,103 +1,11 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { importEntities } from '@/actions/importer';
|
||||
import { useTimelineStream } from '@/api/hooks/streaming/use-timeline-stream';
|
||||
import { useTimeline as useStoreTimeline, useTimelinesActions } from '@/stores/timelines';
|
||||
|
||||
import type { DataTag } from '@tanstack/react-query';
|
||||
import type { PaginatedResponse, Status, StreamingParams } from 'pl-api';
|
||||
|
||||
type TimelineEntry =
|
||||
| {
|
||||
type: 'status';
|
||||
id: string;
|
||||
rebloggedBy: Array<string>;
|
||||
isConnectedTop?: boolean;
|
||||
isConnectedBottom?: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'pending-status';
|
||||
id: string;
|
||||
}
|
||||
| {
|
||||
type: 'gap';
|
||||
}
|
||||
| {
|
||||
type: 'page-start';
|
||||
maxId?: string;
|
||||
}
|
||||
| {
|
||||
type: 'page-end';
|
||||
minId?: string;
|
||||
};
|
||||
|
||||
const processPage = ({
|
||||
items: statuses,
|
||||
next,
|
||||
}: PaginatedResponse<Status>): Array<TimelineEntry> => {
|
||||
const timelinePage: Array<TimelineEntry> = [];
|
||||
|
||||
const processStatus = (status: Status): boolean => {
|
||||
if (timelinePage.some((entry) => entry.type === 'status' && entry.id === status.id))
|
||||
return false;
|
||||
|
||||
let isConnectedTop = false;
|
||||
const inReplyToId = (status.reblog || status).in_reply_to_id;
|
||||
|
||||
if (inReplyToId) {
|
||||
const foundStatus = statuses.find((s) => (s.reblog || s).id === inReplyToId);
|
||||
|
||||
if (foundStatus) {
|
||||
if (processStatus(foundStatus)) {
|
||||
const lastEntry = timelinePage.at(-1);
|
||||
// it's always of type status but doing this to satisfy ts
|
||||
if (lastEntry?.type === 'status') lastEntry.isConnectedBottom = true;
|
||||
isConnectedTop = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (status.reblog) {
|
||||
const existingEntry = timelinePage.find(
|
||||
(entry) => entry.type === 'status' && entry.id === status.reblog!.id,
|
||||
);
|
||||
|
||||
if (existingEntry?.type === 'status') {
|
||||
existingEntry.rebloggedBy.push(status.account.id);
|
||||
} else {
|
||||
timelinePage.push({
|
||||
type: 'status',
|
||||
id: status.reblog.id,
|
||||
rebloggedBy: [status.account.id],
|
||||
isConnectedTop,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
timelinePage.push({
|
||||
type: 'status',
|
||||
id: status.id,
|
||||
rebloggedBy: [],
|
||||
isConnectedTop,
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
for (const status of statuses) {
|
||||
processStatus(status);
|
||||
}
|
||||
|
||||
if (next)
|
||||
timelinePage.push({
|
||||
type: 'page-end',
|
||||
minId: statuses.at(-1)?.id,
|
||||
});
|
||||
|
||||
return timelinePage;
|
||||
};
|
||||
|
||||
type PaginationParams = { max_id?: string; min_id?: string };
|
||||
type TimelineFetcher = (params?: PaginationParams) => Promise<PaginatedResponse<Status>>;
|
||||
|
||||
@ -106,65 +14,45 @@ interface StreamConfig {
|
||||
params?: StreamingParams;
|
||||
}
|
||||
|
||||
type TimelineQueryKey = DataTag<readonly unknown[], Array<TimelineEntry>>;
|
||||
|
||||
const useTimeline = (
|
||||
queryKey: TimelineQueryKey,
|
||||
fetcher: TimelineFetcher,
|
||||
streamConfig?: StreamConfig,
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
const useTimeline = (timelineId: string, fetcher: TimelineFetcher, streamConfig?: StreamConfig) => {
|
||||
const timeline = useStoreTimeline(timelineId);
|
||||
const timelineActions = useTimelinesActions();
|
||||
|
||||
useTimelineStream(streamConfig?.stream ?? '', streamConfig?.params, !!streamConfig?.stream);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey,
|
||||
queryFn: async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetcher();
|
||||
importEntities({ statuses: response.items });
|
||||
return processPage(response);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!timeline.isPending) return;
|
||||
fetchInitial();
|
||||
}, []);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(query.isPending);
|
||||
const fetchInitial = useCallback(async () => {
|
||||
timelineActions.setLoading(timelineId, true);
|
||||
try {
|
||||
const response = await fetcher();
|
||||
importEntities({ statuses: response.items });
|
||||
timelineActions.expandTimeline(timelineId, response.items, !!response.next, true);
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}, [timelineId]);
|
||||
|
||||
const handleLoadMore = useCallback(
|
||||
async (entry: TimelineEntry) => {
|
||||
if (isLoading) return;
|
||||
if (entry.type !== 'page-end' && entry.type !== 'page-start') return;
|
||||
const fetchNextPage = useCallback(async () => {
|
||||
timelineActions.setLoading(timelineId, true);
|
||||
const lastEntry = timeline.entries.at(-1);
|
||||
if (!lastEntry || lastEntry.type !== 'page-end') return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetcher(
|
||||
entry.type === 'page-end' ? { max_id: entry.minId } : { min_id: entry.maxId },
|
||||
);
|
||||
try {
|
||||
const response = await fetcher({ max_id: lastEntry.minId });
|
||||
|
||||
importEntities({ statuses: response.items });
|
||||
importEntities({ statuses: response.items });
|
||||
|
||||
const timelinePage = processPage(response);
|
||||
timelineActions.expandTimeline(timelineId, response.items, !!response.next, false);
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}, [timelineId, timeline.entries]);
|
||||
|
||||
queryClient.setQueryData(queryKey, (oldData) => {
|
||||
if (!oldData) return timelinePage;
|
||||
const index = oldData.indexOf(entry);
|
||||
return oldData.toSpliced(index, 1, ...timelinePage);
|
||||
});
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
[isLoading, fetcher, queryKey, queryClient],
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({ ...query, handleLoadMore, isLoading }),
|
||||
[query, handleLoadMore, isLoading],
|
||||
);
|
||||
return useMemo(() => ({ ...timeline, fetchNextPage }), [timeline, fetchNextPage]);
|
||||
};
|
||||
|
||||
export { useTimeline, type TimelineEntry };
|
||||
export { useTimeline };
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { useClient } from '@/hooks/use-client';
|
||||
|
||||
import { queryKeys } from '../keys';
|
||||
|
||||
import { useTimeline } from './use-timeline';
|
||||
|
||||
import type {
|
||||
@ -22,7 +20,7 @@ const useHomeTimeline = (params?: Omit<HomeTimelineParams, keyof PaginationParam
|
||||
const stream = 'home';
|
||||
|
||||
return useTimeline(
|
||||
queryKeys.timelines.home(params),
|
||||
'home',
|
||||
(paginationParams) => client.timelines.homeTimeline({ ...params, ...paginationParams }),
|
||||
{ stream },
|
||||
);
|
||||
@ -33,7 +31,7 @@ const usePublicTimeline = (params?: Omit<PublicTimelineParams, keyof PaginationP
|
||||
const stream = params?.local ? 'public:local' : params?.instance ? `public:remote` : 'public';
|
||||
|
||||
return useTimeline(
|
||||
queryKeys.timelines.public(params?.local, params),
|
||||
`public${params?.local ? ':local' : params?.instance ? `:remote:` + params.instance : ''}`,
|
||||
(paginationParams) => client.timelines.publicTimeline({ ...params, ...paginationParams }),
|
||||
{ stream },
|
||||
);
|
||||
@ -46,7 +44,7 @@ const useHashtagTimeline = (
|
||||
const client = useClient();
|
||||
|
||||
return useTimeline(
|
||||
queryKeys.timelines.hashtag(hashtag, params),
|
||||
`hashtag:${hashtag}`,
|
||||
(paginationParams) =>
|
||||
client.timelines.hashtagTimeline(hashtag, { ...params, ...paginationParams }),
|
||||
{ stream: 'hashtag', params: { tag: hashtag } },
|
||||
@ -59,7 +57,7 @@ const useLinkTimeline = (
|
||||
) => {
|
||||
const client = useClient();
|
||||
|
||||
return useTimeline(queryKeys.timelines.link(url, params), (paginationParams) =>
|
||||
return useTimeline(`link:${url}`, (paginationParams) =>
|
||||
client.timelines.linkTimeline(url, { ...params, ...paginationParams }),
|
||||
);
|
||||
};
|
||||
@ -71,7 +69,7 @@ const useListTimeline = (
|
||||
const client = useClient();
|
||||
|
||||
return useTimeline(
|
||||
queryKeys.timelines.list(listId, params),
|
||||
`list:${listId}`,
|
||||
(paginationParams) => client.timelines.listTimeline(listId, { ...params, ...paginationParams }),
|
||||
{ stream: 'list', params: { list: listId } },
|
||||
);
|
||||
@ -84,7 +82,7 @@ const useGroupTimeline = (
|
||||
const client = useClient();
|
||||
|
||||
return useTimeline(
|
||||
queryKeys.timelines.group(groupId, params),
|
||||
`group:${groupId}`,
|
||||
(paginationParams) =>
|
||||
client.timelines.groupTimeline(groupId, { ...params, ...paginationParams }),
|
||||
{ stream: 'group', params: { group: groupId } },
|
||||
@ -95,7 +93,7 @@ const useBubbleTimeline = (params?: Omit<BubbleTimelineParams, keyof PaginationP
|
||||
const client = useClient();
|
||||
|
||||
return useTimeline(
|
||||
queryKeys.timelines.bubble(params),
|
||||
`bubble`,
|
||||
(paginationParams) => client.timelines.bubbleTimeline({ ...params, ...paginationParams }),
|
||||
{ stream: 'bubble' },
|
||||
);
|
||||
@ -107,7 +105,7 @@ const useAntennaTimeline = (
|
||||
) => {
|
||||
const client = useClient();
|
||||
|
||||
return useTimeline(queryKeys.timelines.antenna(antennaId, params), (paginationParams) =>
|
||||
return useTimeline(`antenna:${antennaId}`, (paginationParams) =>
|
||||
client.timelines.antennaTimeline(antennaId, { ...params, ...paginationParams }),
|
||||
);
|
||||
};
|
||||
@ -115,7 +113,7 @@ const useAntennaTimeline = (
|
||||
const useWrenchedTimeline = (params?: Omit<WrenchedTimelineParams, keyof PaginationParams>) => {
|
||||
const client = useClient();
|
||||
|
||||
return useTimeline(queryKeys.timelines.wrenched(params), (paginationParams) =>
|
||||
return useTimeline(`wrenched`, (paginationParams) =>
|
||||
client.timelines.wrenchedTimeline({ ...params, ...paginationParams }),
|
||||
);
|
||||
};
|
||||
|
||||
157
packages/nicolium/src/stores/timelines.ts
Normal file
157
packages/nicolium/src/stores/timelines.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import { create } from 'zustand';
|
||||
import { mutative } from 'zustand-mutative';
|
||||
|
||||
import type { Status } from 'pl-api';
|
||||
|
||||
type TimelineEntry =
|
||||
| {
|
||||
type: 'status';
|
||||
id: string;
|
||||
rebloggedBy: Array<string>;
|
||||
isConnectedTop?: boolean;
|
||||
isConnectedBottom?: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'pending-status';
|
||||
id: string;
|
||||
}
|
||||
| {
|
||||
type: 'gap';
|
||||
sinceId: string;
|
||||
maxId: string;
|
||||
}
|
||||
| {
|
||||
type: 'page-start';
|
||||
maxId?: string;
|
||||
}
|
||||
| {
|
||||
type: 'page-end';
|
||||
minId?: string;
|
||||
};
|
||||
|
||||
interface TimelineData {
|
||||
entries: Array<TimelineEntry>;
|
||||
isFetching: boolean;
|
||||
isPending: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
timelines: Record<string, TimelineData>;
|
||||
actions: {
|
||||
expandTimeline: (
|
||||
timelineId: string,
|
||||
statuses: Array<Status>,
|
||||
hasMore: boolean,
|
||||
initialFetch: boolean,
|
||||
) => void;
|
||||
setLoading: (timelineId: string, isFetching: boolean) => void;
|
||||
};
|
||||
}
|
||||
|
||||
const processPage = (statuses: Array<Status>, hasMore: boolean): Array<TimelineEntry> => {
|
||||
const timelinePage: Array<TimelineEntry> = [];
|
||||
|
||||
const processStatus = (status: Status): boolean => {
|
||||
if (timelinePage.some((entry) => entry.type === 'status' && entry.id === status.id))
|
||||
return false;
|
||||
|
||||
let isConnectedTop = false;
|
||||
const inReplyToId = (status.reblog || status).in_reply_to_id;
|
||||
|
||||
if (inReplyToId) {
|
||||
const foundStatus = statuses.find((s) => (s.reblog || s).id === inReplyToId);
|
||||
|
||||
if (foundStatus) {
|
||||
if (processStatus(foundStatus)) {
|
||||
const lastEntry = timelinePage.at(-1);
|
||||
// it's always of type status but doing this to satisfy ts
|
||||
if (lastEntry?.type === 'status') lastEntry.isConnectedBottom = true;
|
||||
isConnectedTop = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (status.reblog) {
|
||||
const existingEntry = timelinePage.find(
|
||||
(entry) => entry.type === 'status' && entry.id === status.reblog!.id,
|
||||
);
|
||||
|
||||
if (existingEntry?.type === 'status') {
|
||||
existingEntry.rebloggedBy.push(status.account.id);
|
||||
} else {
|
||||
timelinePage.push({
|
||||
type: 'status',
|
||||
id: status.reblog.id,
|
||||
rebloggedBy: [status.account.id],
|
||||
isConnectedTop,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
timelinePage.push({
|
||||
type: 'status',
|
||||
id: status.id,
|
||||
rebloggedBy: [],
|
||||
isConnectedTop,
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
for (const status of statuses) {
|
||||
processStatus(status);
|
||||
}
|
||||
|
||||
if (hasMore)
|
||||
timelinePage.push({
|
||||
type: 'page-end',
|
||||
minId: statuses.at(-1)?.id,
|
||||
});
|
||||
|
||||
return timelinePage;
|
||||
};
|
||||
|
||||
const useTimelinesStore = create<State>()(
|
||||
mutative((set) => ({
|
||||
timelines: {} as Record<string, TimelineData>,
|
||||
actions: {
|
||||
expandTimeline: (timelineId, statuses, hasMore, initialFetch) =>
|
||||
set((state) => {
|
||||
const timeline = state.timelines[timelineId] ?? createEmptyTimeline();
|
||||
const entries = processPage(statuses, hasMore);
|
||||
|
||||
if (initialFetch) timeline.entries = [];
|
||||
else if (timeline.entries.at(-1)?.type === 'page-end') timeline.entries.pop();
|
||||
timeline.entries.push(...entries);
|
||||
timeline.isPending = false;
|
||||
timeline.isFetching = false;
|
||||
state.timelines[timelineId] = timeline;
|
||||
}),
|
||||
setLoading: (timelineId, isFetching) =>
|
||||
set((state) => {
|
||||
const timeline = state.timelines[timelineId];
|
||||
|
||||
if (!timeline) return;
|
||||
|
||||
timeline.isFetching = isFetching;
|
||||
if (!isFetching) timeline.isPending = false;
|
||||
}),
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
const createEmptyTimeline = (): TimelineData => ({
|
||||
entries: [],
|
||||
isFetching: false,
|
||||
isPending: true,
|
||||
});
|
||||
|
||||
const emptyTimeline = createEmptyTimeline();
|
||||
|
||||
const useTimelinesActions = () => useTimelinesStore((state) => state.actions);
|
||||
|
||||
const useTimeline = (timelineId: string) =>
|
||||
useTimelinesStore((state) => state.timelines[timelineId] ?? emptyTimeline);
|
||||
|
||||
export { useTimelinesStore, useTimelinesActions, useTimeline, type TimelineEntry };
|
||||
Reference in New Issue
Block a user