From 8fd267e5d720bab5c17c4e07c428411f488c6c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 16:42:42 +0100 Subject: [PATCH] nicolium: add the old WIP timeline thing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../queries/timelines/use-home-timeline.ts | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 packages/pl-fe/src/queries/timelines/use-home-timeline.ts diff --git a/packages/pl-fe/src/queries/timelines/use-home-timeline.ts b/packages/pl-fe/src/queries/timelines/use-home-timeline.ts new file mode 100644 index 000000000..8bb6f24ca --- /dev/null +++ b/packages/pl-fe/src/queries/timelines/use-home-timeline.ts @@ -0,0 +1,163 @@ +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useState } from 'react'; + +import { importEntities } from '@/actions/importer'; +import { useTimelineStream } from '@/api/hooks/streaming/use-timeline-stream'; +import { useAppDispatch } from '@/hooks/use-app-dispatch'; +import { useClient } from '@/hooks/use-client'; + +import type { PaginatedResponse, Status } from 'pl-api'; + +type TimelineEntry = + | { + type: 'status'; + id: string; + rebloggedBy: Array; + 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) => { + const timelinePage: Array = []; + + // if (previous) timelinePage.push({ + // type: 'page-start', + // maxId: statuses.at(0)?.id, + // }); + + const processStatus = (status: Status) => { + 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((status) => (status.reblog || status).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; +}; + +const useHomeTimeline = () => { + const client = useClient(); + const dispatch = useAppDispatch(); + const queryClient = useQueryClient(); + + useTimelineStream('home'); + + const [isLoading, setIsLoading] = useState(true); + + const queryKey = ['timelines', 'home']; + + const query = useQuery({ + queryKey, + queryFn: () => { + setIsLoading(true); + + return client.timelines + .homeTimeline() + .then((response) => { + dispatch(importEntities({ statuses: response.items })); + + return processPage(response); + }) + .catch(() => {}) + .finally(() => setIsLoading(false)); + }, + }); + + const handleLoadMore = (entry: TimelineEntry) => { + if (isLoading) return; + + setIsLoading(true); + if (entry.type !== 'page-end' && entry.type !== 'page-start') return; + + return client.timelines + .homeTimeline(entry.type === 'page-end' ? { max_id: entry.minId } : { min_id: entry.maxId }) + .then((response) => { + dispatch(importEntities({ statuses: response.items })); + + const timelinePage = processPage(response); + + queryClient.setQueryData>(['timelines', 'home'], (oldData) => { + if (!oldData) return timelinePage; + + const index = oldData.indexOf(entry); + return oldData.toSpliced(index, 1, ...timelinePage); + }); + }) + .catch(() => {}) + .finally(() => setIsLoading(false)); + }; + return { + ...query, + isLoading: isLoading, + handleLoadMore, + }; +}; + +export { useHomeTimeline, type TimelineEntry };