diff --git a/app/reader/ReaderContent.tsx b/app/reader/ReaderContent.tsx index db42c558..e3e11df1 100644 --- a/app/reader/ReaderContent.tsx +++ b/app/reader/ReaderContent.tsx @@ -15,15 +15,82 @@ import { PubLeafletDocument, PubLeafletPublication } from "lexicons/api"; import { blobRefToSrc } from "src/utils/blobRefToSrc"; import { Json } from "supabase/database.types"; import type { Post } from "./getReaderFeed"; +import useSWRInfinite from "swr/infinite"; +import { getReaderFeed } from "./getReaderFeed"; +import { useEffect, useRef } from "react"; +import { useRouter } from "next/navigation"; export const ReaderContent = (props: { root_entity: string; posts: Post[]; + nextCursor: string | null; }) => { - if (props.posts.length === 0) return ; + const getKey = ( + pageIndex: number, + previousPageData: { posts: Post[]; nextCursor: string | null } | null, + ) => { + // Reached the end + if (previousPageData && !previousPageData.nextCursor) return null; + + // First page, we don't have previousPageData + if (pageIndex === 0) return ["reader-feed", null]; + + // Add the cursor to the key + return ["reader-feed", previousPageData?.nextCursor]; + }; + + const { data, error, size, setSize, isValidating } = useSWRInfinite( + getKey, + ([_, cursor]) => getReaderFeed(cursor), + { + fallbackData: [{ posts: props.posts, nextCursor: props.nextCursor }], + revalidateFirstPage: false, + }, + ); + + const loadMoreRef = useRef(null); + + // Set up intersection observer to load more when trigger element is visible + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && !isValidating) { + const hasMore = data && data[data.length - 1]?.nextCursor; + if (hasMore) { + setSize(size + 1); + } + } + }, + { threshold: 0.1 }, + ); + + if (loadMoreRef.current) { + observer.observe(loadMoreRef.current); + } + + return () => observer.disconnect(); + }, [data, size, setSize, isValidating]); + + const allPosts = data ? data.flatMap((page) => page.posts) : []; + + if (allPosts.length === 0 && !isValidating) return ; + return ( -
- {props.posts?.map((p) => )} +
+ {allPosts.map((p) => ( + + ))} + {/* Trigger element for loading more posts */} + ); }; @@ -69,11 +136,11 @@ const Post = (props: Post) => { `} >
- {props.publications?.map((p) => )} + {props.publications?.map((p) => )}
); }; diff --git a/app/reader/getReaderFeed.ts b/app/reader/getReaderFeed.ts index 75d18dbe..8aad4ecf 100644 --- a/app/reader/getReaderFeed.ts +++ b/app/reader/getReaderFeed.ts @@ -86,7 +86,7 @@ const idResolver = new IdResolver({ }); export async function getReaderFeed( - cursor?: string, + cursor?: string | null, ): Promise<{ posts: Post[]; nextCursor: string | null }> { let auth_res = await getIdentityData(); if (!auth_res?.atp_did) return { posts: [], nextCursor: null }; diff --git a/app/reader/page.tsx b/app/reader/page.tsx index c757f4c9..68bc5f70 100644 --- a/app/reader/page.tsx +++ b/app/reader/page.tsx @@ -79,6 +79,7 @@ export default async function Reader(props: {}) { content: ( ),