a tool for shared writing and social publishing

use compound cursors and fix subscriptions pagination

+44 -20
+5 -5
app/reader/ReaderContent.tsx
··· 14 14 import { PubLeafletDocument, PubLeafletPublication } from "lexicons/api"; 15 15 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 16 16 import { Json } from "supabase/database.types"; 17 - import type { Post } from "./getReaderFeed"; 17 + import type { Cursor, Post } from "./getReaderFeed"; 18 18 import useSWRInfinite from "swr/infinite"; 19 19 import { getReaderFeed } from "./getReaderFeed"; 20 20 import { useEffect, useRef } from "react"; ··· 23 23 export const ReaderContent = (props: { 24 24 root_entity: string; 25 25 posts: Post[]; 26 - nextCursor: string | null; 26 + nextCursor: Cursor | null; 27 27 }) => { 28 28 const getKey = ( 29 29 pageIndex: number, 30 - previousPageData: { posts: Post[]; nextCursor: string | null } | null, 30 + previousPageData: { posts: Post[]; nextCursor: Cursor | null } | null, 31 31 ) => { 32 32 // Reached the end 33 33 if (previousPageData && !previousPageData.nextCursor) return null; 34 34 35 35 // First page, we don't have previousPageData 36 - if (pageIndex === 0) return ["reader-feed", null]; 36 + if (pageIndex === 0) return ["reader-feed", null] as const; 37 37 38 38 // Add the cursor to the key 39 - return ["reader-feed", previousPageData?.nextCursor]; 39 + return ["reader-feed", previousPageData?.nextCursor] as const; 40 40 }; 41 41 42 42 const { data, error, size, setSize, isValidating } = useSWRInfinite(
+6 -5
app/reader/SubscriptionsContent.tsx
··· 6 6 import { PublicationSubscription, getSubscriptions } from "./getSubscriptions"; 7 7 import useSWRInfinite from "swr/infinite"; 8 8 import { useEffect, useRef } from "react"; 9 + import { Cursor } from "./getReaderFeed"; 9 10 10 11 export const SubscriptionsContent = (props: { 11 12 publications: PublicationSubscription[]; 12 - nextCursor: string | null; 13 + nextCursor: Cursor | null; 13 14 }) => { 14 15 const getKey = ( 15 16 pageIndex: number, 16 17 previousPageData: { 17 18 subscriptions: PublicationSubscription[]; 18 - nextCursor: string | null; 19 + nextCursor: Cursor | null; 19 20 } | null, 20 21 ) => { 21 22 // Reached the end 22 23 if (previousPageData && !previousPageData.nextCursor) return null; 23 24 24 25 // First page, we don't have previousPageData 25 - if (pageIndex === 0) return ["subscriptions", null]; 26 + if (pageIndex === 0) return ["subscriptions", null] as const; 26 27 27 28 // Add the cursor to the key 28 - return ["subscriptions", previousPageData?.nextCursor]; 29 + return ["subscriptions", previousPageData?.nextCursor] as const; 29 30 }; 30 31 31 32 const { data, error, size, setSize, isValidating } = useSWRInfinite( ··· 72 73 return ( 73 74 <div className="relative"> 74 75 <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-3"> 75 - {allPublications?.map((p) => <PubListing key={p.uri} {...p} />)} 76 + {allPublications?.map((p, index) => <PubListing key={p.uri} {...p} />)} 76 77 </div> 77 78 {/* Trigger element for loading more subscriptions */} 78 79 <div
+20 -4
app/reader/getReaderFeed.ts
··· 10 10 import { Json } from "supabase/database.types"; 11 11 import { idResolver } from "./idResolver"; 12 12 13 + export type Cursor = { 14 + timestamp: string; 15 + uri: string; 16 + }; 17 + 13 18 export async function getReaderFeed( 14 - cursor?: string | null, 15 - ): Promise<{ posts: Post[]; nextCursor: string | null }> { 19 + cursor?: Cursor | null, 20 + ): Promise<{ posts: Post[]; nextCursor: Cursor | null }> { 16 21 let auth_res = await getIdentityData(); 17 22 if (!auth_res?.atp_did) return { posts: [], nextCursor: null }; 18 23 let query = supabaseServerClient ··· 28 33 auth_res.atp_did, 29 34 ) 30 35 .order("indexed_at", { ascending: false }) 36 + .order("uri", { ascending: false }) 31 37 .limit(25); 32 - if (cursor) query.lt("indexed_at", cursor); 38 + if (cursor) { 39 + query = query.lt("indexed_at", cursor.timestamp).lte("uri", cursor.uri); 40 + } 33 41 let { data: feed, error } = await query; 34 42 35 43 let posts = await Promise.all( ··· 55 63 return p; 56 64 }) || [], 57 65 ); 66 + const nextCursor = 67 + posts.length > 0 68 + ? { 69 + timestamp: posts[posts.length - 1].documents.indexed_at, 70 + uri: posts[posts.length - 1].documents.uri, 71 + } 72 + : null; 73 + 58 74 return { 59 75 posts, 60 - nextCursor: posts[posts.length - 1]?.documents.indexed_at || null, 76 + nextCursor, 61 77 }; 62 78 } 63 79
+13 -6
app/reader/getSubscriptions.ts
··· 6 6 import { Json } from "supabase/database.types"; 7 7 import { supabaseServerClient } from "supabase/serverClient"; 8 8 import { idResolver } from "./idResolver"; 9 + import { Cursor } from "./getReaderFeed"; 9 10 10 - export async function getSubscriptions(cursor?: string | null): Promise<{ 11 - nextCursor: null | string; 11 + export async function getSubscriptions(cursor?: Cursor | null): Promise<{ 12 + nextCursor: null | Cursor; 12 13 subscriptions: PublicationSubscription[]; 13 14 }> { 14 15 let auth_res = await getIdentityData(); 15 16 if (!auth_res?.atp_did) return { subscriptions: [], nextCursor: null }; 16 17 let query = supabaseServerClient 17 18 .from("publication_subscriptions") 18 - .select(`publications(*, documents_in_publications(*, documents(*)))`) 19 + .select(`*, publications(*, documents_in_publications(*, documents(*)))`) 19 20 .order(`created_at`, { ascending: false }) 21 + .order(`uri`, { ascending: false }) 20 22 .order("indexed_at", { 21 23 referencedTable: "publications.documents_in_publications", 22 24 }) ··· 24 26 .limit(25) 25 27 .eq("identity", auth_res.atp_did); 26 28 27 - if (cursor) query.lt("indexed_at", cursor); 29 + if (cursor) { 30 + query = query.lt("created_at", cursor.timestamp).lte("uri", cursor.uri); 31 + } 28 32 let { data: pubs, error } = await query; 33 + console.log(cursor); 29 34 30 35 const actors: string[] = [ 31 36 ...new Set( ··· 46 51 47 52 const nextCursor = 48 53 pubs && pubs.length > 0 49 - ? pubs[pubs.length - 1].publications?.documents_in_publications?.[0] 50 - ?.indexed_at || null 54 + ? { 55 + timestamp: pubs[pubs.length - 1].created_at, 56 + uri: pubs[pubs.length - 1].uri, 57 + } 51 58 : null; 52 59 53 60 return {