diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index 7f84e6fc5..70bde1d2c 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -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; diff --git a/packages/nicolium/src/queries/keys.ts b/packages/nicolium/src/queries/keys.ts index 1296d509a..c63aff3a8 100644 --- a/packages/nicolium/src/queries/keys.ts +++ b/packages/nicolium/src/queries/keys.ts @@ -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 = DataTag; @@ -421,7 +431,42 @@ const suggestions = { const timelines = { root: ['timelines'] as const, - home: ['timelines', 'home'] as TaggedKey<['timelines', 'home'], Array>, + home: (params?: Omit) => { + const key = ['timelines', 'home', params] as const; + return key as TaggedKey>; + }, + public: (local?: boolean, params?: Omit) => { + const key = ['timelines', 'public', { local, ...params }] as const; + return key as TaggedKey>; + }, + hashtag: (hashtag: string, params?: Omit) => { + const key = ['timelines', 'hashtag', hashtag, params] as const; + return key as TaggedKey>; + }, + link: (url: string, params?: Omit) => { + const key = ['timelines', 'link', url, params] as const; + return key as TaggedKey>; + }, + list: (listId: string, params?: Omit) => { + const key = ['timelines', 'list', listId, params] as const; + return key as TaggedKey>; + }, + group: (groupId: string, params?: Omit) => { + const key = ['timelines', 'group', groupId, params] as const; + return key as TaggedKey>; + }, + bubble: (params?: Omit) => { + const key = ['timelines', 'bubble', params] as const; + return key as TaggedKey>; + }, + antenna: (antennaId: string, params?: Omit) => { + const key = ['timelines', 'antenna', antennaId, params] as const; + return key as TaggedKey>; + }, + wrenched: (params?: Omit) => { + const key = ['timelines', 'wrenched', params] as const; + return key as TaggedKey>; + }, }; const timelineIds = { diff --git a/packages/nicolium/src/queries/timelines/use-home-timeline.ts b/packages/nicolium/src/queries/timelines/use-timeline.ts similarity index 65% rename from packages/nicolium/src/queries/timelines/use-home-timeline.ts rename to packages/nicolium/src/queries/timelines/use-timeline.ts index e12939036..ed4a8008f 100644 --- a/packages/nicolium/src/queries/timelines/use-home-timeline.ts +++ b/packages/nicolium/src/queries/timelines/use-timeline.ts @@ -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) => { +const processPage = ({ + items: statuses, + next, +}: PaginatedResponse): Array => { const timelinePage: Array = []; - // 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) => { 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) => { return timelinePage; }; -const useHomeTimeline = () => { - const client = useClient(); +type PaginationParams = { max_id?: string; min_id?: string }; +type TimelineFetcher = (params?: PaginationParams) => Promise>; + +interface StreamConfig { + stream: string; + params?: StreamingParams; +} + +type TimelineQueryKey = DataTag>; + +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 }; diff --git a/packages/nicolium/src/queries/timelines/use-timelines.ts b/packages/nicolium/src/queries/timelines/use-timelines.ts new file mode 100644 index 000000000..d20df8a87 --- /dev/null +++ b/packages/nicolium/src/queries/timelines/use-timelines.ts @@ -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) => { + const client = useClient(); + const stream = 'home'; + + return useTimeline( + queryKeys.timelines.home(params), + (paginationParams) => client.timelines.homeTimeline({ ...params, ...paginationParams }), + { stream }, + ); +}; + +const usePublicTimeline = (params?: Omit) => { + 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, +) => { + 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, +) => { + const client = useClient(); + + return useTimeline(queryKeys.timelines.link(url, params), (paginationParams) => + client.timelines.linkTimeline(url, { ...params, ...paginationParams }), + ); +}; + +const useListTimeline = ( + listId: string, + params?: Omit, +) => { + 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, +) => { + 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) => { + const client = useClient(); + + return useTimeline( + queryKeys.timelines.bubble(params), + (paginationParams) => client.timelines.bubbleTimeline({ ...params, ...paginationParams }), + { stream: 'bubble' }, + ); +}; + +const useAntennaTimeline = ( + antennaId: string, + params?: Omit, +) => { + const client = useClient(); + + return useTimeline(queryKeys.timelines.antenna(antennaId, params), (paginationParams) => + client.timelines.antennaTimeline(antennaId, { ...params, ...paginationParams }), + ); +}; + +const useWrenchedTimeline = (params?: Omit) => { + 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, +};