diff --git a/src/components/UniversalPostRenderer.tsx b/src/components/UniversalPostRenderer.tsx index cd7477b..b9c79ab 100644 --- a/src/components/UniversalPostRenderer.tsx +++ b/src/components/UniversalPostRenderer.tsx @@ -11,6 +11,7 @@ import { useQueryIdentity, useQueryPost, useQueryProfile, + yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks, } from "~/utils/useQuery"; function asTyped(obj: T): $Typed { @@ -33,7 +34,8 @@ export interface UniversalPostRendererATURILoaderProps { ref?: React.Ref; dataIndexPropPass?: number; nopics?: boolean; - lightboxCallback?: (d:LightboxProps) => void; + lightboxCallback?: (d: LightboxProps) => void; + maxReplies?: number; } // export async function cachedGetRecord({ @@ -143,6 +145,7 @@ export function UniversalPostRendererATURILoader({ dataIndexPropPass, nopics, lightboxCallback, + maxReplies, }: UniversalPostRendererATURILoaderProps) { // /*mass comment*/ console.log("atUri", atUri); //const { get, set } = usePersistentStore(); @@ -388,6 +391,88 @@ export function UniversalPostRendererATURILoader({ ); }, [links]); + // const { data: repliesData } = useQueryConstellation({ + // method: "/links", + // target: atUri, + // collection: "app.bsky.feed.post", + // path: ".reply.parent.uri", + // }); + + const infinitequeryresults = useInfiniteQuery({ + ...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks( + { + method: "/links", + target: atUri, + collection: "app.bsky.feed.post", + path: ".reply.parent.uri", + } + ), + enabled: !!atUri && !!maxReplies, + }); + + const { + data: repliesData, + // fetchNextPage, + // hasNextPage, + // isFetchingNextPage, + } = infinitequeryresults; + + // auto-fetch all pages + useEffect(() => { + if (!maxReplies) return; + if ( + infinitequeryresults.hasNextPage && + !infinitequeryresults.isFetchingNextPage + ) { + console.log("Fetching the next page..."); + infinitequeryresults.fetchNextPage(); + } + }, [infinitequeryresults]); + + const replyAturis = repliesData + ? repliesData.pages.flatMap((page) => + page + ? page.linking_records.map((record) => { + const aturi = `at://${record.did}/${record.collection}/${record.rkey}`; + return aturi; + }) + : [] + ) + : []; + + //const [oldestOpsReply, setOldestOpsReply] = useState(undefined); + + const { oldestOpsReply, oldestOpsReplyElseNewestNonOpsReply } = (() => { + if (!replyAturis || replyAturis.length === 0 || !maxReplies) + return { + oldestOpsReply: undefined, + oldestOpsReplyElseNewestNonOpsReply: undefined, + }; + + const opdid = new AtUri( + //postQuery?.value.reply?.root.uri ?? postQuery?.uri ?? atUri + atUri + ).host; + + const opReplies = replyAturis.filter( + (aturi) => new AtUri(aturi).host === opdid + ); + + if (opReplies.length > 0) { + const opreply = opReplies[opReplies.length - 1]; + //setOldestOpsReply(opreply); + return { + oldestOpsReply: opreply, + oldestOpsReplyElseNewestNonOpsReply: opreply, + }; + } else { + return { + oldestOpsReply: undefined, + oldestOpsReplyElseNewestNonOpsReply: replyAturis[0], + }; + } + })(); + // const navigateToProfile = (e: React.MouseEvent) => { // e.stopPropagation(); // if (resolved?.did) { @@ -403,26 +488,103 @@ export function UniversalPostRendererATURILoader({ } return ( - + <> + {/* uprrs {maxReplies} {!!maxReplies&&!!oldestOpsReplyElseNewestNonOpsReply ? "true" : "false"} */} + + {oldestOpsReplyElseNewestNonOpsReply && ( + <> + {/* hello {maxReplies} */} + 0} + topReplyLine={(maxReplies ?? 0) > 1} + bottomBorder={bottomBorder} + feedviewpost={feedviewpost} + repostedby={repostedby} + style={style} + ref={ref} + dataIndexPropPass={dataIndexPropPass} + nopics={nopics} + lightboxCallback={lightboxCallback} + maxReplies={ + maxReplies && maxReplies > 0 ? maxReplies - 1 : undefined + } + /> + {maxReplies && maxReplies - 1 === 0 && ( + + )} + + )} + + ); +} + +function MoreReplies({ atUri }: { atUri: string }) { + const navigate = useNavigate(); + const aturio = new AtUri(atUri); + return ( +
+ navigate({ + to: "/profile/$did/post/$rkey", + params: { did: aturio.host, rkey: aturio.rkey }, + }) + } + className="border-b border-gray-300 dark:border-gray-800 flex flex-row px-4 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors" + > +
+
+
+ +
+ More Replies +
+
); } @@ -451,6 +613,7 @@ export function UniversalPostRendererRawRecordShim({ dataIndexPropPass, nopics, lightboxCallback, + maxReplies, }: { postRecord: any; profileRecord: any; @@ -469,7 +632,8 @@ export function UniversalPostRendererRawRecordShim({ ref?: React.Ref; dataIndexPropPass?: number; nopics?: boolean; - lightboxCallback?: (d:LightboxProps) => void; + lightboxCallback?: (d: LightboxProps) => void; + maxReplies?: number; }) { // /*mass comment*/ console.log(`received aturi: ${aturi} of post content: ${postRecord}`); const navigate = useNavigate(); @@ -669,6 +833,7 @@ export function UniversalPostRendererRawRecordShim({ dataIndexPropPass={dataIndexPropPass} nopics={nopics} lightboxCallback={lightboxCallback} + maxReplies={maxReplies} /> ); @@ -982,6 +1147,7 @@ import type { PostView, //ThreadViewPost, } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; +import { useInfiniteQuery } from "@tanstack/react-query"; import { useEffect, useRef, useState } from "react"; import ReactPlayer from "react-player"; @@ -1115,7 +1281,8 @@ function UniversalPostRenderer({ ref, dataIndexPropPass, nopics, - lightboxCallback + lightboxCallback, + maxReplies, }: { post: PostView; // optional for now because i havent ported every use to this yet @@ -1139,7 +1306,8 @@ function UniversalPostRenderer({ ref?: React.Ref; dataIndexPropPass?: number; nopics?: boolean; - lightboxCallback?: (d:LightboxProps) => void; + lightboxCallback?: (d: LightboxProps) => void; + maxReplies?: number; }) { const parsed = new AtUri(post.uri); const navigate = useNavigate(); @@ -1496,7 +1664,8 @@ function UniversalPostRenderer({ > {fedi ? ( <> - void; postid?: { did: string; rkey: string }; nopics?: boolean; - lightboxCallback?: (d:LightboxProps) => void; + lightboxCallback?: (d: LightboxProps) => void; }) { //const [lightboxIndex, setLightboxIndex] = useState(null); - function setLightboxIndex(number:number) { + function setLightboxIndex(number: number) { navigate({ - to: "/profile/$did/post/$rkey/image/$i", - params: { - did: postid?.did, - rkey: postid?.rkey, - i: number.toString(), - }, - }); + to: "/profile/$did/post/$rkey/image/$i", + params: { + did: postid?.did, + rkey: postid?.rkey, + i: number.toString(), + }, + }); } if ( AppBskyEmbedRecordWithMedia.isView(embed) && @@ -1962,11 +2131,11 @@ function PostEmbeds({ src: img.fullsize, alt: img.alt, })); - console.log("rendering images") + console.log("rendering images"); if (lightboxCallback) { - lightboxCallback({images: lightboxImages}) - console.log("rendering images") - }; + lightboxCallback({ images: lightboxImages }); + console.log("rendering images"); + } if (nopics) return; diff --git a/src/routes/profile.$did/post.$rkey.tsx b/src/routes/profile.$did/post.$rkey.tsx index eebe94b..a53c057 100644 --- a/src/routes/profile.$did/post.$rkey.tsx +++ b/src/routes/profile.$did/post.$rkey.tsx @@ -1,15 +1,16 @@ -import { useQueryClient } from "@tanstack/react-query"; +import { AtUri } from "@atproto/api"; +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { createFileRoute, Outlet } from "@tanstack/react-router"; -import React, { useLayoutEffect } from "react"; +import React, { useEffect, useLayoutEffect } from "react"; import { Header } from "~/components/Header"; import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; //import { usePersistentStore } from '~/providers/PersistentStoreProvider'; import { constructPostQuery, - useQueryConstellation, useQueryIdentity, useQueryPost, + yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks, } from "~/utils/useQuery"; import type { LightboxProps } from "./post.$rkey.image.$i"; @@ -198,13 +199,67 @@ export function ProfilePostComponent({ const { data: mainPost } = useQueryPost(atUri); - const { data: repliesData } = useQueryConstellation({ - method: "/links", - target: atUri, - collection: "app.bsky.feed.post", - path: ".reply.parent.uri", + // const { data: repliesData } = useQueryConstellation({ + // method: "/links", + // target: atUri, + // collection: "app.bsky.feed.post", + // path: ".reply.parent.uri", + // }); + // const replies = repliesData?.linking_records.slice(0, 50) ?? []; + const infinitequeryresults = useInfiniteQuery({ + ...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks( + { + method: "/links", + target: atUri, + collection: "app.bsky.feed.post", + path: ".reply.parent.uri", + } + ), + enabled: !!atUri, }); - const replies = repliesData?.linking_records.slice(0, 50) ?? []; + + const { + data: repliesData, + // fetchNextPage, + // hasNextPage, + // isFetchingNextPage, + } = infinitequeryresults; + + // auto-fetch all pages + useEffect(() => { + if ( + infinitequeryresults.hasNextPage && + !infinitequeryresults.isFetchingNextPage + ) { + console.log("Fetching the next page..."); + infinitequeryresults.fetchNextPage(); + } + }, [infinitequeryresults]); + + const replyAturis = repliesData + ? repliesData.pages.flatMap((page) => + page + ? page.linking_records.map((record) => { + const aturi = `at://${record.did}/${record.collection}/${record.rkey}`; + return aturi; + }) + : [] + ) + : []; + + const opdid = new AtUri(atUri).host; + + // Find oldest OP reply + const oldestOpsIndex = replyAturis.findIndex( + (aturi) => new AtUri(aturi).host === opdid + ); + + // Reorder: move oldest OP reply to the front + if (oldestOpsIndex > 0) { + const [oldestOpsReply] = replyAturis.splice(oldestOpsIndex, 1); + replyAturis.unshift(oldestOpsReply); + } + const [parents, setParents] = React.useState([]); const [parentsLoading, setParentsLoading] = React.useState(false); @@ -351,7 +406,8 @@ export function ProfilePostComponent({ maxWidth: 600, //margin: "0px auto 0", padding: 0, - minHeight: "100dvh", + minHeight: "80dvh", + paddingBottom: "20dvh" }} >
- {replies.length > 0 && - replies.map((reply) => { - const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`; + {replyAturis.length > 0 && + replyAturis.map((reply) => { + //const replyAtUri = `at://${reply.did}/app.bsky.feed.post/${reply.rkey}`; return ( ); })} diff --git a/src/utils/useQuery.ts b/src/utils/useQuery.ts index cdb17a7..dd3cc95 100644 --- a/src/utils/useQuery.ts +++ b/src/utils/useQuery.ts @@ -1,5 +1,6 @@ import * as ATPAPI from "@atproto/api"; import { + infiniteQueryOptions, type QueryFunctionContext, queryOptions, useInfiniteQuery, @@ -614,4 +615,58 @@ export function useInfiniteQueryFeedSkeleton(options: { refetchOnWindowFocus: false, enabled: !!options.feedUri && (options.isAuthed ? !!options.agent && !!options.pdsUrl && !!options.feedServiceDid : true), }); +} + + +export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(query?: { + method: '/links' + target?: string + collection: string + path: string +}) { + const constellationHost = 'constellation.microcosm.blue' + console.log( + 'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks', + query, + ) + + return infiniteQueryOptions({ + enabled: !!query?.target, + queryKey: [ + 'reddwarf_constellation', + query?.method, + query?.target, + query?.collection, + query?.path, + ] as const, + + queryFn: async ({pageParam}: {pageParam?: string}) => { + if (!query || !query?.target) return undefined + + const method = query.method + const target = query.target + const collection = query.collection + const path = query.path + const cursor = pageParam + + const res = await fetch( + `https://${constellationHost}${method}?target=${encodeURIComponent(target)}${ + collection ? `&collection=${encodeURIComponent(collection)}` : '' + }${path ? `&path=${encodeURIComponent(path)}` : ''}${ + cursor ? `&cursor=${encodeURIComponent(cursor)}` : '' + }`, + ) + + if (!res.ok) throw new Error('Failed to fetch') + + return (await res.json()) as linksRecordsResponse + }, + + getNextPageParam: lastPage => { + return (lastPage as any)?.cursor ?? undefined + }, + initialPageParam: undefined, + staleTime: 5 * 60 * 1000, + gcTime: 5 * 60 * 1000, + }) } \ No newline at end of file