an independent Bluesky client using Constellation, PDS Queries, and other services reddwarf.app
frontend spa bluesky reddwarf microcosm client app
99
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 24efdc839aa8e9a9a563f2c8ef1f7055a100787f 141 lines 4.9 kB view raw
1import { useInfiniteQuery } from "@tanstack/react-query"; 2import { createFileRoute } from "@tanstack/react-router"; 3import { useAtom } from "jotai"; 4import React from "react"; 5 6import { Header } from "~/components/Header"; 7import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 8import { constellationURLAtom } from "~/utils/atoms"; 9import { type linksRecord,useQueryIdentity, yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks } from "~/utils/useQuery"; 10 11import { 12 EmptyState, 13 ErrorState, 14 LoadingState, 15} from "../notifications"; 16 17export const Route = createFileRoute("/profile/$did/post/$rkey/quotes")({ 18 component: RouteComponent, 19}); 20 21function RouteComponent() { 22 const { did, rkey } = Route.useParams(); 23 const { data: identity } = useQueryIdentity(did); 24 const atUri = identity?.did && rkey ? `at://${decodeURIComponent(identity.did)}/app.bsky.feed.post/${rkey}` : ''; 25 26 const [constellationurl] = useAtom(constellationURLAtom); 27 const infinitequeryresultsWithoutMedia = useInfiniteQuery({ 28 ...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks( 29 { 30 constellation: constellationurl, 31 method: "/links", 32 target: atUri, 33 collection: "app.bsky.feed.post", 34 path: ".embed.record.uri", // embed.record.record.uri and embed.record.uri 35 } 36 ), 37 enabled: !!atUri, 38 }); 39 const infinitequeryresultsWithMedia = useInfiniteQuery({ 40 ...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks( 41 { 42 constellation: constellationurl, 43 method: "/links", 44 target: atUri, 45 collection: "app.bsky.feed.post", 46 path: ".embed.record.record.uri", // embed.record.record.uri and embed.record.uri 47 } 48 ), 49 enabled: !!atUri, 50 }); 51 52 const { 53 data: infiniteQuotesDataWithoutMedia, 54 fetchNextPage: fetchNextPageWithoutMedia, 55 hasNextPage: hasNextPageWithoutMedia, 56 isFetchingNextPage: isFetchingNextPageWithoutMedia, 57 isLoading: isLoadingWithoutMedia, 58 isError: isErrorWithoutMedia, 59 error: errorWithoutMedia, 60 } = infinitequeryresultsWithoutMedia; 61 const { 62 data: infiniteQuotesDataWithMedia, 63 fetchNextPage: fetchNextPageWithMedia, 64 hasNextPage: hasNextPageWithMedia, 65 isFetchingNextPage: isFetchingNextPageWithMedia, 66 isLoading: isLoadingWithMedia, 67 isError: isErrorWithMedia, 68 error: errorWithMedia, 69 } = infinitequeryresultsWithMedia; 70 71 const fetchNextPage = async () => { 72 await Promise.all([ 73 hasNextPageWithMedia && fetchNextPageWithMedia(), 74 hasNextPageWithoutMedia && fetchNextPageWithoutMedia(), 75 ]); 76 }; 77 78 const hasNextPage = hasNextPageWithMedia || hasNextPageWithoutMedia; 79 const isFetchingNextPage = isFetchingNextPageWithMedia || isFetchingNextPageWithoutMedia; 80 const isLoading = isLoadingWithMedia || isLoadingWithoutMedia; 81 82 const allQuotes = React.useMemo(() => { 83 const withPages = infiniteQuotesDataWithMedia?.pages ?? []; 84 const withoutPages = infiniteQuotesDataWithoutMedia?.pages ?? []; 85 const maxLen = Math.max(withPages.length, withoutPages.length); 86 const merged: linksRecord[] = []; 87 88 for (let i = 0; i < maxLen; i++) { 89 const a = withPages[i]?.linking_records ?? []; 90 const b = withoutPages[i]?.linking_records ?? []; 91 const mergedPage = [...a, ...b].sort((b, a) => a.rkey.localeCompare(b.rkey)); 92 merged.push(...mergedPage); 93 } 94 95 return merged; 96 }, [infiniteQuotesDataWithMedia?.pages, infiniteQuotesDataWithoutMedia?.pages]); 97 98 const quotesAturis = React.useMemo(() => { 99 return allQuotes.flatMap((r) => `at://${r.did}/${r.collection}/${r.rkey}`); 100 }, [allQuotes]); 101 102 return ( 103 <> 104 <Header 105 title={`Quotes`} 106 backButtonCallback={() => { 107 if (window.history.length > 1) { 108 window.history.back(); 109 } else { 110 window.location.assign("/"); 111 } 112 }} 113 /> 114 115 <> 116 {(() => { 117 if (isLoading) return <LoadingState text="Loading quotes..." />; 118 if (isErrorWithMedia) return <ErrorState error={errorWithMedia} />; 119 if (isErrorWithoutMedia) return <ErrorState error={errorWithoutMedia} />; 120 121 if (!quotesAturis?.length) 122 return <EmptyState text="No quotes yet." />; 123 })()} 124 </> 125 126 {quotesAturis.map((m) => ( 127 <UniversalPostRendererATURILoader key={m} atUri={m} /> 128 ))} 129 130 {hasNextPage && ( 131 <button 132 onClick={() => fetchNextPage()} 133 disabled={isFetchingNextPage} 134 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" 135 > 136 {isFetchingNextPage ? "Loading..." : "Load More"} 137 </button> 138 )} 139 </> 140 ); 141}