an independent Bluesky client using Constellation, PDS Queries, and other services
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
client
app
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}