an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm

OP thread safe pagination

rimar1337 5ed2c581 44b8e6c2

Changed files
+140 -31
src
routes
profile.$did
utils
+135 -27
src/routes/profile.$did/post.$rkey.tsx
··· 1 1 import { AtUri } from "@atproto/api"; 2 2 import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; 3 3 import { createFileRoute, Outlet } from "@tanstack/react-router"; 4 - import React, { useEffect, useLayoutEffect } from "react"; 4 + import React, { useLayoutEffect } from "react"; 5 5 6 6 import { Header } from "~/components/Header"; 7 7 import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 8 8 //import { usePersistentStore } from '~/providers/PersistentStoreProvider'; 9 9 import { 10 10 constructPostQuery, 11 + type linksAllResponse, 12 + type linksRecordsResponse, 13 + useQueryConstellation, 11 14 useQueryIdentity, 12 15 useQueryPost, 13 16 yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks, ··· 193 196 () => 194 197 resolvedDid 195 198 ? `at://${decodeURIComponent(resolvedDid)}/app.bsky.feed.post/${rkey}` 196 - : "", 199 + : undefined, 197 200 [resolvedDid, rkey] 198 201 ); 199 202 200 203 const { data: mainPost } = useQueryPost(atUri); 201 204 205 + console.log("atUri",atUri) 206 + 207 + const opdid = React.useMemo( 208 + () => 209 + atUri 210 + ? new AtUri(atUri).host 211 + : undefined, 212 + [atUri] 213 + ); 214 + 215 + // @ts-expect-error i hate overloads 216 + const { data: links } = useQueryConstellation(atUri?{ 217 + method: "/links/all", 218 + target: atUri, 219 + } : { 220 + method: "undefined", 221 + target: "" 222 + })as { data: linksAllResponse | undefined }; 223 + 224 + //const [likes, setLikes] = React.useState<number | null>(null); 225 + //const [reposts, setReposts] = React.useState<number | null>(null); 226 + const [replyCount, setReplyCount] = React.useState<number | null>(null); 227 + 228 + React.useEffect(() => { 229 + // /*mass comment*/ console.log(JSON.stringify(links, null, 2)); 230 + // setLikes( 231 + // links 232 + // ? links?.links?.["app.bsky.feed.like"]?.[".subject.uri"]?.records || 0 233 + // : null 234 + // ); 235 + // setReposts( 236 + // links 237 + // ? links?.links?.["app.bsky.feed.repost"]?.[".subject.uri"]?.records || 0 238 + // : null 239 + // ); 240 + setReplyCount( 241 + links 242 + ? links?.links?.["app.bsky.feed.post"]?.[".reply.parent.uri"] 243 + ?.records || 0 244 + : null 245 + ); 246 + }, [links]); 247 + 248 + const { data: opreplies } = useQueryConstellation( 249 + !!opdid && replyCount && replyCount >= 25 250 + ? { 251 + method: "/links", 252 + target: atUri, 253 + // @ts-expect-error overloading sucks so much 254 + collection: "app.bsky.feed.post", 255 + path: ".reply.parent.uri", 256 + //cursor?: string; 257 + dids: [opdid], 258 + } 259 + : { 260 + method: "undefined", 261 + target: "", 262 + } 263 + ) as { data: linksRecordsResponse | undefined }; 264 + 265 + const opReplyAturis = 266 + opreplies?.linking_records.map( 267 + (r) => `at://${r.did}/${r.collection}/${r.rkey}`, 268 + ) ?? []; 269 + 270 + 202 271 // const { data: repliesData } = useQueryConstellation({ 203 272 // method: "/links", 204 273 // target: atUri, ··· 219 288 }); 220 289 221 290 const { 222 - data: repliesData, 223 - // fetchNextPage, 224 - // hasNextPage, 225 - // isFetchingNextPage, 291 + data: infiniteRepliesData, 292 + fetchNextPage, 293 + hasNextPage, 294 + isFetchingNextPage, 226 295 } = infinitequeryresults; 227 296 228 - // auto-fetch all pages 229 - useEffect(() => { 230 - if ( 231 - infinitequeryresults.hasNextPage && 232 - !infinitequeryresults.isFetchingNextPage 233 - ) { 234 - console.log("Fetching the next page..."); 235 - infinitequeryresults.fetchNextPage(); 236 - } 237 - }, [infinitequeryresults]); 297 + // // auto-fetch all pages 298 + // useEffect(() => { 299 + // if ( 300 + // infinitequeryresults.hasNextPage && 301 + // !infinitequeryresults.isFetchingNextPage 302 + // ) { 303 + // console.log("Fetching the next page..."); 304 + // infinitequeryresults.fetchNextPage(); 305 + // } 306 + // }, [infinitequeryresults]); 238 307 239 - const replyAturis = repliesData 240 - ? repliesData.pages.flatMap((page) => 241 - page 242 - ? page.linking_records.map((record) => { 243 - const aturi = `at://${record.did}/${record.collection}/${record.rkey}`; 244 - return aturi; 245 - }) 246 - : [] 247 - ) 248 - : []; 308 + // const replyAturis = repliesData 309 + // ? repliesData.pages.flatMap((page) => 310 + // page 311 + // ? page.linking_records.map((record) => { 312 + // const aturi = `at://${record.did}/${record.collection}/${record.rkey}`; 313 + // return aturi; 314 + // }) 315 + // : [] 316 + // ) 317 + // : []; 249 318 250 - const opdid = new AtUri(atUri).host; 319 + const replyAturis = React.useMemo(() => { 320 + // Get all replies from the standard infinite query 321 + const allReplies = 322 + infiniteRepliesData?.pages.flatMap( 323 + (page) => 324 + page?.linking_records.map( 325 + (r) => `at://${r.did}/${r.collection}/${r.rkey}`, 326 + ) ?? [], 327 + ) ?? []; 328 + 329 + if (replyCount && (replyCount < 25)) { 330 + // If count is low, just use the standard list and find the oldest OP reply to move to the top 331 + const opdidFromUri = atUri ? new AtUri(atUri).host : undefined; 332 + const oldestOpsIndex = allReplies.findIndex( 333 + (aturi) => new AtUri(aturi).host === opdidFromUri, 334 + ); 335 + if (oldestOpsIndex > 0) { 336 + const [oldestOpsReply] = allReplies.splice(oldestOpsIndex, 1); 337 + allReplies.unshift(oldestOpsReply); 338 + } 339 + return allReplies; 340 + } else { 341 + // If count is high, prioritize OP replies from the special query 342 + // and filter them out from the main list to avoid duplication. 343 + const opReplySet = new Set(opReplyAturis); 344 + const otherReplies = allReplies.filter((uri) => !opReplySet.has(uri)); 345 + return [...opReplyAturis, ...otherReplies]; 346 + } 347 + }, [infiniteRepliesData, opReplyAturis, replyCount, atUri]); 251 348 252 349 // Find oldest OP reply 253 350 const oldestOpsIndex = replyAturis.findIndex( ··· 282 379 283 380 hasPerformedInitialLayout.current = true; 284 381 } 382 + 285 383 // todo idk what to do with this 384 + // eslint-disable-next-line react-hooks/set-state-in-effect 286 385 setLayoutReady(true); 287 386 } 288 387 }, [parents, layoutReady]); ··· 423 522 /> 424 523 ); 425 524 })} 525 + {hasNextPage && ( 526 + <button 527 + onClick={() => fetchNextPage()} 528 + disabled={isFetchingNextPage} 529 + className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold disabled:opacity-50" 530 + > 531 + {isFetchingNextPage ? "Loading..." : "Load More"} 532 + </button> 533 + )} 426 534 </div> 427 535 </div> 428 536 </>
+4 -3
src/utils/followState.ts
··· 1 - import { AtUri, type Agent } from "@atproto/api"; 2 - import { useQueryConstellation, type linksRecordsResponse } from "./useQuery"; 1 + import { type Agent,AtUri } from "@atproto/api"; 2 + import { TID } from "@atproto/common-web"; 3 3 import type { QueryClient } from "@tanstack/react-query"; 4 - import { TID } from "@atproto/common-web"; 4 + 5 + import { type linksRecordsResponse,useQueryConstellation } from "./useQuery"; 5 6 6 7 export function useGetFollowState({ 7 8 target,
+1 -1
src/utils/useQuery.ts
··· 362 362 type linksCountResponse = { 363 363 total: string; 364 364 }; 365 - type linksAllResponse = { 365 + export type linksAllResponse = { 366 366 links: Record< 367 367 string, 368 368 Record<