a tool for shared writing and social publishing

add infinite scrolling feed

+75 -7
+72 -5
app/reader/ReaderContent.tsx
··· 15 15 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 16 16 import { Json } from "supabase/database.types"; 17 17 import type { Post } from "./getReaderFeed"; 18 + import useSWRInfinite from "swr/infinite"; 19 + import { getReaderFeed } from "./getReaderFeed"; 20 + import { useEffect, useRef } from "react"; 21 + import { useRouter } from "next/navigation"; 18 22 19 23 export const ReaderContent = (props: { 20 24 root_entity: string; 21 25 posts: Post[]; 26 + nextCursor: string | null; 22 27 }) => { 23 - if (props.posts.length === 0) return <ReaderEmpty />; 28 + const getKey = ( 29 + pageIndex: number, 30 + previousPageData: { posts: Post[]; nextCursor: string | null } | null, 31 + ) => { 32 + // Reached the end 33 + if (previousPageData && !previousPageData.nextCursor) return null; 34 + 35 + // First page, we don't have previousPageData 36 + if (pageIndex === 0) return ["reader-feed", null]; 37 + 38 + // Add the cursor to the key 39 + return ["reader-feed", previousPageData?.nextCursor]; 40 + }; 41 + 42 + const { data, error, size, setSize, isValidating } = useSWRInfinite( 43 + getKey, 44 + ([_, cursor]) => getReaderFeed(cursor), 45 + { 46 + fallbackData: [{ posts: props.posts, nextCursor: props.nextCursor }], 47 + revalidateFirstPage: false, 48 + }, 49 + ); 50 + 51 + const loadMoreRef = useRef<HTMLDivElement>(null); 52 + 53 + // Set up intersection observer to load more when trigger element is visible 54 + useEffect(() => { 55 + const observer = new IntersectionObserver( 56 + (entries) => { 57 + if (entries[0].isIntersecting && !isValidating) { 58 + const hasMore = data && data[data.length - 1]?.nextCursor; 59 + if (hasMore) { 60 + setSize(size + 1); 61 + } 62 + } 63 + }, 64 + { threshold: 0.1 }, 65 + ); 66 + 67 + if (loadMoreRef.current) { 68 + observer.observe(loadMoreRef.current); 69 + } 70 + 71 + return () => observer.disconnect(); 72 + }, [data, size, setSize, isValidating]); 73 + 74 + const allPosts = data ? data.flatMap((page) => page.posts) : []; 75 + 76 + if (allPosts.length === 0 && !isValidating) return <ReaderEmpty />; 77 + 24 78 return ( 25 - <div className="flex flex-col gap-3"> 26 - {props.posts?.map((p) => <Post {...p} key={p.documents.uri} />)} 79 + <div className="flex flex-col gap-3 relative"> 80 + {allPosts.map((p) => ( 81 + <Post {...p} key={p.documents.uri} /> 82 + ))} 83 + {/* Trigger element for loading more posts */} 84 + <div 85 + ref={loadMoreRef} 86 + className="absolute bottom-0 left-0 w-full h-px pointer-events-none" 87 + aria-hidden="true" 88 + /> 89 + {isValidating && ( 90 + <div className="text-center text-tertiary py-4"> 91 + Loading more posts... 92 + </div> 93 + )} 27 94 </div> 28 95 ); 29 96 }; ··· 69 136 `} 70 137 > 71 138 <SpeedyLink 72 - className="h-full w-full absolute top-0 left-0 " 139 + className="h-full w-full absolute top-0 left-0" 73 140 href={`${props.publication.href}/${postUri.rkey}`} 74 141 /> 75 142 <div 76 - className={`${showPageBackground ? "bg-bg-page " : "bg-transparent"} rounded-md w-full px-[10px] pt-2 pb-2 z-1 `} 143 + className={`${showPageBackground ? "bg-bg-page " : "bg-transparent"} rounded-md w-full px-[10px] pt-2 pb-2`} 77 144 style={{ 78 145 backgroundColor: showPageBackground 79 146 ? "rgba(var(--bg-page), var(--bg-page-alpha))"
+1 -1
app/reader/SubscriptionsContent.tsx
··· 16 16 17 17 return ( 18 18 <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-3"> 19 - {props.publications?.map((p) => <PubListing {...p} />)} 19 + {props.publications?.map((p) => <PubListing key={p.uri} {...p} />)} 20 20 </div> 21 21 ); 22 22 };
+1 -1
app/reader/getReaderFeed.ts
··· 86 86 }); 87 87 88 88 export async function getReaderFeed( 89 - cursor?: string, 89 + cursor?: string | null, 90 90 ): Promise<{ posts: Post[]; nextCursor: string | null }> { 91 91 let auth_res = await getIdentityData(); 92 92 if (!auth_res?.atp_did) return { posts: [], nextCursor: null };
+1
app/reader/page.tsx
··· 79 79 content: ( 80 80 <ReaderContent 81 81 root_entity={root_entity} 82 + nextCursor={posts.nextCursor} 82 83 posts={posts.posts} 83 84 /> 84 85 ),