nicolium: add a generalized version of useHomeTimeline

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-03-02 23:22:55 +01:00
parent b99f097c19
commit 4c6132205c
4 changed files with 225 additions and 37 deletions

View File

@ -7,9 +7,10 @@ import Status from '@/components/statuses/status';
import Tombstone from '@/components/statuses/tombstone';
import PlaceholderStatus from '@/features/placeholder/components/placeholder-status';
import { useStatus } from '@/queries/statuses/use-status';
import { type TimelineEntry, useHomeTimeline } from '@/queries/timelines/use-home-timeline';
import { useHomeTimeline } from '@/queries/timelines/use-timelines';
import type { FilterContextType } from '@/queries/settings/use-filters';
import type { TimelineEntry } from '@/queries/timelines/use-timeline';
interface ITimelineStatus {
id: string;

View File

@ -7,7 +7,7 @@ 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-home-timeline';
import type { TimelineEntry } from './timelines/use-timeline';
import type { MinifiedSuggestion } from './trends/use-suggested-accounts';
import type {
MinifiedAdminAccount,
@ -34,8 +34,10 @@ import type {
AdminRule,
Announcement,
Antenna,
AntennaTimelineParams,
Backup,
BookmarkFolder,
BubbleTimelineParams,
Chat,
Circle,
CredentialAccount,
@ -46,21 +48,29 @@ 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>;
@ -421,7 +431,42 @@ const suggestions = {
const timelines = {
root: ['timelines'] as const,
home: ['timelines', 'home'] as TaggedKey<['timelines', 'home'], Array<TimelineEntry>>,
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 = {

View File

@ -3,11 +3,9 @@ import { useCallback, useMemo, useState } from 'react';
import { importEntities } from '@/actions/importer';
import { useTimelineStream } from '@/api/hooks/streaming/use-timeline-stream';
import { useClient } from '@/hooks/use-client';
import { queryKeys } from '../keys';
import type { PaginatedResponse, Status } from 'pl-api';
import type { DataTag } from '@tanstack/react-query';
import type { PaginatedResponse, Status, StreamingParams } from 'pl-api';
type TimelineEntry =
| {
@ -33,15 +31,13 @@ type TimelineEntry =
minId?: string;
};
const processPage = ({ items: statuses, next }: PaginatedResponse<Status>) => {
const processPage = ({
items: statuses,
next,
}: PaginatedResponse<Status>): Array<TimelineEntry> => {
const timelinePage: Array<TimelineEntry> = [];
// if (previous) timelinePage.push({
// type: 'page-start',
// maxId: statuses.at(0)?.id,
// });
const processStatus = (status: Status) => {
const processStatus = (status: Status): boolean => {
if (timelinePage.some((entry) => entry.type === 'status' && entry.id === status.id))
return false;
@ -49,14 +45,13 @@ const processPage = ({ items: statuses, next }: PaginatedResponse<Status>) => {
const inReplyToId = (status.reblog || status).in_reply_to_id;
if (inReplyToId) {
const foundStatus = statuses.find((status) => (status.reblog || status).id === 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;
}
}
@ -103,38 +98,49 @@ const processPage = ({ items: statuses, next }: PaginatedResponse<Status>) => {
return timelinePage;
};
const useHomeTimeline = () => {
const client = useClient();
type PaginationParams = { max_id?: string; min_id?: string };
type TimelineFetcher = (params?: PaginationParams) => Promise<PaginatedResponse<Status>>;
interface StreamConfig {
stream: string;
params?: StreamingParams;
}
type TimelineQueryKey = DataTag<readonly unknown[], Array<TimelineEntry>>;
const useTimeline = (
queryKey: TimelineQueryKey,
fetcher: TimelineFetcher,
streamConfig?: StreamConfig,
) => {
const queryClient = useQueryClient();
useTimelineStream('home');
useTimelineStream(streamConfig?.stream ?? '', streamConfig?.params, !!streamConfig?.stream);
const [isLoading, setIsLoading] = useState(true);
const queryKey = queryKeys.timelines.home;
const query = useQuery({
queryKey,
queryFn: async () => {
setIsLoading(true);
const response = await client.timelines.homeTimeline();
importEntities({ statuses: response.items });
return processPage(response);
try {
const response = await fetcher();
importEntities({ statuses: response.items });
return processPage(response);
} finally {
setIsLoading(false);
}
},
});
const handleLoadMore = useCallback(
async (entry: TimelineEntry) => {
if (isLoading) return;
if (entry.type !== 'page-end' && entry.type !== 'page-start') return;
setIsLoading(true);
try {
if (entry.type !== 'page-end' && entry.type !== 'page-start') return;
const response = await client.timelines.homeTimeline(
const response = await fetcher(
entry.type === 'page-end' ? { max_id: entry.minId } : { min_id: entry.maxId },
);
@ -142,20 +148,23 @@ const useHomeTimeline = () => {
const timelinePage = processPage(response);
queryClient.setQueryData(queryKeys.timelines.home, (oldData) => {
queryClient.setQueryData(queryKey, (oldData) => {
if (!oldData) return timelinePage;
const index = oldData.indexOf(entry);
return oldData.toSpliced(index, 1, ...timelinePage);
});
} finally {
setIsLoading(false);
} catch (error) {
//
}
setIsLoading(false);
},
[isLoading],
[isLoading, fetcher, queryKey, queryClient],
);
return useMemo(() => ({ ...query, handleLoadMore, isLoading }), [query, isLoading]);
return useMemo(
() => ({ ...query, handleLoadMore, isLoading }),
[query, handleLoadMore, isLoading],
);
};
export { useHomeTimeline, type TimelineEntry };
export { useTimeline, type TimelineEntry };

View File

@ -0,0 +1,133 @@
import { useClient } from '@/hooks/use-client';
import { queryKeys } from '../keys';
import { useTimeline } from './use-timeline';
import type {
AntennaTimelineParams,
BubbleTimelineParams,
GroupTimelineParams,
HashtagTimelineParams,
HomeTimelineParams,
LinkTimelineParams,
ListTimelineParams,
PaginationParams,
PublicTimelineParams,
WrenchedTimelineParams,
} from 'pl-api';
const useHomeTimeline = (params?: Omit<HomeTimelineParams, keyof PaginationParams>) => {
const client = useClient();
const stream = 'home';
return useTimeline(
queryKeys.timelines.home(params),
(paginationParams) => client.timelines.homeTimeline({ ...params, ...paginationParams }),
{ stream },
);
};
const usePublicTimeline = (params?: Omit<PublicTimelineParams, keyof PaginationParams>) => {
const client = useClient();
const stream = params?.local ? 'public:local' : params?.instance ? `public:remote` : 'public';
return useTimeline(
queryKeys.timelines.public(params?.local, params),
(paginationParams) => client.timelines.publicTimeline({ ...params, ...paginationParams }),
{ stream },
);
};
const useHashtagTimeline = (
hashtag: string,
params?: Omit<HashtagTimelineParams, keyof PaginationParams>,
) => {
const client = useClient();
return useTimeline(
queryKeys.timelines.hashtag(hashtag, params),
(paginationParams) =>
client.timelines.hashtagTimeline(hashtag, { ...params, ...paginationParams }),
{ stream: 'hashtag', params: { tag: hashtag } },
);
};
const useLinkTimeline = (
url: string,
params?: Omit<LinkTimelineParams, keyof PaginationParams>,
) => {
const client = useClient();
return useTimeline(queryKeys.timelines.link(url, params), (paginationParams) =>
client.timelines.linkTimeline(url, { ...params, ...paginationParams }),
);
};
const useListTimeline = (
listId: string,
params?: Omit<ListTimelineParams, keyof PaginationParams>,
) => {
const client = useClient();
return useTimeline(
queryKeys.timelines.list(listId, params),
(paginationParams) => client.timelines.listTimeline(listId, { ...params, ...paginationParams }),
{ stream: 'list', params: { list: listId } },
);
};
const useGroupTimeline = (
groupId: string,
params?: Omit<GroupTimelineParams, keyof PaginationParams>,
) => {
const client = useClient();
return useTimeline(
queryKeys.timelines.group(groupId, params),
(paginationParams) =>
client.timelines.groupTimeline(groupId, { ...params, ...paginationParams }),
{ stream: 'group', params: { group: groupId } },
);
};
const useBubbleTimeline = (params?: Omit<BubbleTimelineParams, keyof PaginationParams>) => {
const client = useClient();
return useTimeline(
queryKeys.timelines.bubble(params),
(paginationParams) => client.timelines.bubbleTimeline({ ...params, ...paginationParams }),
{ stream: 'bubble' },
);
};
const useAntennaTimeline = (
antennaId: string,
params?: Omit<AntennaTimelineParams, keyof PaginationParams>,
) => {
const client = useClient();
return useTimeline(queryKeys.timelines.antenna(antennaId, params), (paginationParams) =>
client.timelines.antennaTimeline(antennaId, { ...params, ...paginationParams }),
);
};
const useWrenchedTimeline = (params?: Omit<WrenchedTimelineParams, keyof PaginationParams>) => {
const client = useClient();
return useTimeline(queryKeys.timelines.wrenched(params), (paginationParams) =>
client.timelines.wrenchedTimeline({ ...params, ...paginationParams }),
);
};
export {
useHomeTimeline,
usePublicTimeline,
useHashtagTimeline,
useLinkTimeline,
useListTimeline,
useGroupTimeline,
useBubbleTimeline,
useAntennaTimeline,
useWrenchedTimeline,
};