+27
-4
src/components/Import.tsx
+27
-4
src/components/Import.tsx
···
1
import { AtUri } from "@atproto/api";
2
import { useNavigate, type UseNavigateResult } from "@tanstack/react-router";
3
import { useState } from "react";
4
5
/**
6
* Basically the best equivalent to Search that i can do
7
*/
8
-
export function Import() {
9
-
const [textInput, setTextInput] = useState<string | undefined>();
10
const navigate = useNavigate();
11
12
const handleEnter = () => {
13
if (!textInput) return;
14
handleImport({
15
text: textInput,
16
navigate,
17
});
18
};
19
20
return (
21
<div className="w-full relative">
22
<IconMaterialSymbolsSearch className="w-5 h-5 absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500" />
23
24
<input
25
type="text"
26
-
placeholder="Import..."
27
value={textInput}
28
onChange={(e) => setTextInput(e.target.value)}
29
onKeyDown={(e) => {
···
38
function handleImport({
39
text,
40
navigate,
41
}: {
42
text: string;
43
navigate: UseNavigateResult<string>;
44
}) {
45
const trimmed = text.trim();
46
// parse text
···
147
// } catch {
148
// // continue
149
// }
150
-
}
···
1
import { AtUri } from "@atproto/api";
2
import { useNavigate, type UseNavigateResult } from "@tanstack/react-router";
3
+
import { useAtom } from "jotai";
4
import { useState } from "react";
5
6
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
7
+
import { lycanURLAtom } from "~/utils/atoms";
8
+
import { useQueryLycanStatus } from "~/utils/useQuery";
9
+
10
/**
11
* Basically the best equivalent to Search that i can do
12
*/
13
+
export function Import({optionaltextstring}: {optionaltextstring?: string}) {
14
+
const [textInput, setTextInput] = useState<string | undefined>(optionaltextstring);
15
const navigate = useNavigate();
16
17
+
const { status } = useAuth();
18
+
const [lycandomain] = useAtom(lycanURLAtom);
19
+
const lycanExists = lycandomain !== "";
20
+
const { data: lycanstatusdata } = useQueryLycanStatus();
21
+
const lycanIndexed = lycanstatusdata?.status === "finished" || false;
22
+
const authed = status === "signedIn";
23
+
24
+
const lycanReady = lycanExists && lycanIndexed && authed;
25
+
26
const handleEnter = () => {
27
if (!textInput) return;
28
handleImport({
29
text: textInput,
30
navigate,
31
+
lycanReady: lycanReady,
32
});
33
};
34
35
+
const placeholder = lycanReady ? "Search..." : "Import...";
36
+
37
return (
38
<div className="w-full relative">
39
<IconMaterialSymbolsSearch className="w-5 h-5 absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500" />
40
41
<input
42
type="text"
43
+
placeholder={placeholder}
44
value={textInput}
45
onChange={(e) => setTextInput(e.target.value)}
46
onKeyDown={(e) => {
···
55
function handleImport({
56
text,
57
navigate,
58
+
lycanReady,
59
}: {
60
text: string;
61
navigate: UseNavigateResult<string>;
62
+
lycanReady?: boolean;
63
}) {
64
const trimmed = text.trim();
65
// parse text
···
166
// } catch {
167
// // continue
168
// }
169
+
170
+
if (lycanReady) {
171
+
navigate({ to: "/search", search: { q: text} })
172
+
}
173
+
}
+19
-4
src/components/UniversalPostRenderer.tsx
+19
-4
src/components/UniversalPostRenderer.tsx
···
1252
1253
import defaultpfp from "~/../public/favicon.png";
1254
import { useAuth } from "~/providers/UnifiedAuthProvider";
1255
import {
1256
FeedItemRenderAturiLoader,
1257
FollowButton,
···
1491
? tags
1492
.map((tag) => {
1493
const encoded = encodeURIComponent(tag);
1494
-
return `<a href="https://${undfediwafrnHost}/search/${encoded}" target="_blank">#${tag.replaceAll(' ','-')}</a>`;
1495
})
1496
.join("<br>")
1497
: "";
···
2012
"/post/" +
2013
post.uri.split("/").pop()
2014
);
2015
} catch (_e) {
2016
// idk
2017
}
2018
}}
2019
style={{
···
2022
>
2023
<MdiShareVariant />
2024
</HitSlopButton>
2025
-
<span style={btnstyle}>
2026
-
<MdiMoreHoriz />
2027
-
</span>
2028
</div>
2029
</div>
2030
)}
···
1252
1253
import defaultpfp from "~/../public/favicon.png";
1254
import { useAuth } from "~/providers/UnifiedAuthProvider";
1255
+
import { renderSnack } from "~/routes/__root";
1256
import {
1257
FeedItemRenderAturiLoader,
1258
FollowButton,
···
1492
? tags
1493
.map((tag) => {
1494
const encoded = encodeURIComponent(tag);
1495
+
return `<a href="https://${undfediwafrnHost}/search/${encoded}" target="_blank">#${tag.replaceAll(" ", "-")}</a>`;
1496
})
1497
.join("<br>")
1498
: "";
···
2013
"/post/" +
2014
post.uri.split("/").pop()
2015
);
2016
+
renderSnack({
2017
+
title: "Copied to clipboard!",
2018
+
});
2019
} catch (_e) {
2020
// idk
2021
+
renderSnack({
2022
+
title: "Failed to copy link",
2023
+
});
2024
}
2025
}}
2026
style={{
···
2029
>
2030
<MdiShareVariant />
2031
</HitSlopButton>
2032
+
<HitSlopButton
2033
+
onClick={() => {
2034
+
renderSnack({
2035
+
title: "Not implemented yet...",
2036
+
});
2037
+
}}
2038
+
>
2039
+
<span style={btnstyle}>
2040
+
<MdiMoreHoriz />
2041
+
</span>
2042
+
</HitSlopButton>
2043
</div>
2044
</div>
2045
)}
+189
-9
src/routes/search.tsx
+189
-9
src/routes/search.tsx
···
1
-
import { createFileRoute } from "@tanstack/react-router";
2
3
import { Header } from "~/components/Header";
4
import { Import } from "~/components/Import";
5
6
export const Route = createFileRoute("/search")({
7
component: Search,
8
});
9
10
export function Search() {
11
return (
12
<>
13
<Header
···
21
}}
22
/>
23
<div className=" flex flex-col items-center mt-4 mx-4 gap-4">
24
-
<Import />
25
<div className="flex flex-col">
26
-
<p className="text-gray-600 dark:text-gray-400">
27
-
Sorry we dont have search. But instead, you can load some of these
28
-
types of content into Red Dwarf:
29
-
</p>
30
<ul className="list-disc list-inside mt-2 text-gray-600 dark:text-gray-400">
31
<li>
32
-
Bluesky URLs from supported clients (like{" "}
33
<code className="text-sm">bsky.app</code> or{" "}
34
<code className="text-sm">deer.social</code>).
35
</li>
···
39
).
40
</li>
41
<li>
42
-
Plain handles (like{" "}
43
<code className="text-sm">@username.bsky.social</code>).
44
</li>
45
<li>
46
-
Direct DIDs (Decentralized Identifiers, starting with{" "}
47
<code className="text-sm">did:</code>).
48
</li>
49
</ul>
···
51
Simply paste one of these into the import field above and press
52
Enter to load the content.
53
</p>
54
</div>
55
</div>
56
</>
57
);
58
}
···
1
+
import type { Agent } from "@atproto/api";
2
+
import { useQueryClient } from "@tanstack/react-query";
3
+
import { createFileRoute, useSearch } from "@tanstack/react-router";
4
+
import { useAtom } from "jotai";
5
+
import { useMemo } from "react";
6
7
import { Header } from "~/components/Header";
8
import { Import } from "~/components/Import";
9
+
import {
10
+
ReusableTabRoute,
11
+
useReusableTabScrollRestore,
12
+
} from "~/components/ReusableTabRoute";
13
+
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
14
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
15
+
import { lycanURLAtom } from "~/utils/atoms";
16
+
import {
17
+
constructLycanRequestIndexQuery,
18
+
useInfiniteQueryLycanSearch,
19
+
useQueryIdentity,
20
+
useQueryLycanStatus,
21
+
} from "~/utils/useQuery";
22
+
23
+
import { renderSnack } from "./__root";
24
25
export const Route = createFileRoute("/search")({
26
component: Search,
27
});
28
29
export function Search() {
30
+
const queryClient = useQueryClient();
31
+
const { agent, status } = useAuth();
32
+
const { data: identity } = useQueryIdentity(agent?.did);
33
+
const [lycandomain] = useAtom(lycanURLAtom);
34
+
const lycanExists = lycandomain !== "";
35
+
const { data: lycanstatusdata } = useQueryLycanStatus();
36
+
const lycanIndexed = lycanstatusdata?.status === "finished" || false;
37
+
const authed = status === "signedIn";
38
+
39
+
const lycanReady = lycanExists && lycanIndexed && authed;
40
+
41
+
const { q }: { q: string } = useSearch({ from: "/search" });
42
+
43
+
//const lycanIndexed = useQuery();
44
+
45
+
const maintext = !lycanExists
46
+
? "Sorry we dont have search. But instead, you can load some of these types of content into Red Dwarf:"
47
+
: authed
48
+
? lycanReady
49
+
? "Lycan Search is enabled and ready! Type to search posts you've interacted with in the past. You can also load some of these types of content into Red Dwarf:"
50
+
: "Sorry, while Lycan Search is enabled, you are not indexed. Index below please. You can load some of these types of content into Red Dwarf:"
51
+
: "Sorry, while Lycan Search is enabled, you are unauthed. Please log in to use Lycan. You can load some of these types of content into Red Dwarf:";
52
+
53
+
async function index(opts: {
54
+
agent?: Agent;
55
+
isAuthed: boolean;
56
+
pdsUrl?: string;
57
+
feedServiceDid?: string;
58
+
}) {
59
+
renderSnack({
60
+
title: "Registering account...",
61
+
});
62
+
try {
63
+
const response = await queryClient.fetchQuery(
64
+
constructLycanRequestIndexQuery(opts)
65
+
);
66
+
if (
67
+
response?.message !== "Import has already started" ||
68
+
response?.message !== "Import has already started"
69
+
) {
70
+
renderSnack({
71
+
title: "Registration failed!",
72
+
description: "Unknown server error (2)",
73
+
});
74
+
} else {
75
+
renderSnack({
76
+
title: "Succesfully sent registration request!",
77
+
description: "Please wait for the server to index your account",
78
+
});
79
+
}
80
+
} catch {
81
+
renderSnack({
82
+
title: "Registration failed!",
83
+
description: "Unknown server error (1)",
84
+
});
85
+
}
86
+
}
87
+
88
return (
89
<>
90
<Header
···
98
}}
99
/>
100
<div className=" flex flex-col items-center mt-4 mx-4 gap-4">
101
+
<Import optionaltextstring={q} />
102
<div className="flex flex-col">
103
+
<p className="text-gray-600 dark:text-gray-400">{maintext}</p>
104
<ul className="list-disc list-inside mt-2 text-gray-600 dark:text-gray-400">
105
<li>
106
+
Bluesky URLs (from supported clients) (like{" "}
107
<code className="text-sm">bsky.app</code> or{" "}
108
<code className="text-sm">deer.social</code>).
109
</li>
···
113
).
114
</li>
115
<li>
116
+
User Handles (like{" "}
117
<code className="text-sm">@username.bsky.social</code>).
118
</li>
119
<li>
120
+
DIDs (Decentralized Identifiers, starting with{" "}
121
<code className="text-sm">did:</code>).
122
</li>
123
</ul>
···
125
Simply paste one of these into the import field above and press
126
Enter to load the content.
127
</p>
128
+
129
+
{lycanExists && authed && !lycanReady ? (
130
+
<div className="mt-4 mx-auto">
131
+
<button
132
+
onClick={() =>
133
+
index({
134
+
agent: agent || undefined,
135
+
isAuthed: status === "signedIn",
136
+
pdsUrl: identity?.pds,
137
+
feedServiceDid: "did:web:" + lycandomain,
138
+
})
139
+
}
140
+
className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800
141
+
text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition"
142
+
>
143
+
Index my Account
144
+
</button>
145
+
</div>
146
+
) : (
147
+
<></>
148
+
)}
149
</div>
150
</div>
151
+
{q ? <SearchTabs query={q} /> : <></>}
152
</>
153
);
154
}
155
+
156
+
function SearchTabs({ query }: { query: string }) {
157
+
return (
158
+
<div>
159
+
<ReusableTabRoute
160
+
route={`search` + query}
161
+
tabs={{
162
+
Likes: <LycanTab query={query} type={"likes"} key={"likes"} />,
163
+
Reposts: <LycanTab query={query} type={"reposts"} key={"reposts"} />,
164
+
Quotes: <LycanTab query={query} type={"quotes"} key={"quotes"} />,
165
+
Pins: <LycanTab query={query} type={"pins"} key={"pins"} />,
166
+
}}
167
+
/>
168
+
</div>
169
+
);
170
+
}
171
+
172
+
function LycanTab({
173
+
query,
174
+
type,
175
+
}: {
176
+
query: string;
177
+
type: "likes" | "pins" | "reposts" | "quotes";
178
+
}) {
179
+
useReusableTabScrollRestore("search" + query);
180
+
181
+
const {
182
+
data: postsData,
183
+
fetchNextPage,
184
+
hasNextPage,
185
+
isFetchingNextPage,
186
+
isLoading: arePostsLoading,
187
+
} = useInfiniteQueryLycanSearch({ query: query, type: type });
188
+
189
+
const posts = useMemo(
190
+
() =>
191
+
postsData?.pages.flatMap((page) => {
192
+
if (page) {
193
+
return page.posts;
194
+
} else {
195
+
return [];
196
+
}
197
+
}) ?? [],
198
+
[postsData]
199
+
);
200
+
201
+
return (
202
+
<>
203
+
{/* <div className="text-gray-500 dark:text-gray-400 text-lg font-semibold my-3 mx-4">
204
+
Posts
205
+
</div> */}
206
+
<div>
207
+
{posts.map((post) => (
208
+
<UniversalPostRendererATURILoader
209
+
key={post}
210
+
atUri={post}
211
+
feedviewpost={true}
212
+
/>
213
+
))}
214
+
</div>
215
+
216
+
{/* Loading and "Load More" states */}
217
+
{arePostsLoading && posts.length === 0 && (
218
+
<div className="p-4 text-center text-gray-500">Loading posts...</div>
219
+
)}
220
+
{isFetchingNextPage && (
221
+
<div className="p-4 text-center text-gray-500">Loading more...</div>
222
+
)}
223
+
{hasNextPage && !isFetchingNextPage && (
224
+
<button
225
+
onClick={() => fetchNextPage()}
226
+
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"
227
+
>
228
+
Load More Posts
229
+
</button>
230
+
)}
231
+
{posts.length === 0 && !arePostsLoading && (
232
+
<div className="p-4 text-center text-gray-500">No posts found.</div>
233
+
)}
234
+
</>
235
+
);
236
+
237
+
return <></>;
238
+
}
+8
src/routes/settings.tsx
+8
src/routes/settings.tsx
···
10
defaultconstellationURL,
11
defaulthue,
12
defaultImgCDN,
13
defaultslingshotURL,
14
defaultVideoCDN,
15
enableBitesAtom,
···
17
enableWafrnTextAtom,
18
hueAtom,
19
imgCDNAtom,
20
slingshotURLAtom,
21
videoCDNAtom,
22
} from "~/utils/atoms";
···
110
title={"Video CDN"}
111
description={"Customize the Slingshot instance to be used by Red Dwarf"}
112
init={defaultVideoCDN}
113
/>
114
115
<SettingHeading title="Experimental" />
···
10
defaultconstellationURL,
11
defaulthue,
12
defaultImgCDN,
13
+
defaultLycanURL,
14
defaultslingshotURL,
15
defaultVideoCDN,
16
enableBitesAtom,
···
18
enableWafrnTextAtom,
19
hueAtom,
20
imgCDNAtom,
21
+
lycanURLAtom,
22
slingshotURLAtom,
23
videoCDNAtom,
24
} from "~/utils/atoms";
···
112
title={"Video CDN"}
113
description={"Customize the Slingshot instance to be used by Red Dwarf"}
114
init={defaultVideoCDN}
115
+
/>
116
+
<TextInputSetting
117
+
atom={lycanURLAtom}
118
+
title={"Lycan Search"}
119
+
description={"Enable text search across posts you've interacted with"}
120
+
init={defaultLycanURL}
121
/>
122
123
<SettingHeading title="Experimental" />
+6
src/utils/atoms.ts
+6
src/utils/atoms.ts
+380
-157
src/utils/useQuery.ts
+380
-157
src/utils/useQuery.ts
···
5
queryOptions,
6
useInfiniteQuery,
7
useQuery,
8
-
type UseQueryResult} from "@tanstack/react-query";
9
import { useAtom } from "jotai";
10
11
-
import { constellationURLAtom, slingshotURLAtom } from "./atoms";
12
13
-
export function constructIdentityQuery(didorhandle?: string, slingshoturl?: string) {
14
return queryOptions({
15
queryKey: ["identity", didorhandle],
16
queryFn: async () => {
17
-
if (!didorhandle) return undefined as undefined
18
const res = await fetch(
19
`https://${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}`
20
);
···
31
}
32
},
33
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
34
-
gcTime: /*0//*/5 * 60 * 1000,
35
});
36
}
37
export function useQueryIdentity(didorhandle: string): UseQueryResult<
···
43
},
44
Error
45
>;
46
-
export function useQueryIdentity(): UseQueryResult<
47
-
undefined,
48
-
Error
49
-
>
50
-
export function useQueryIdentity(didorhandle?: string):
51
-
UseQueryResult<
52
-
{
53
-
did: string;
54
-
handle: string;
55
-
pds: string;
56
-
signing_key: string;
57
-
} | undefined,
58
-
Error
59
-
>
60
export function useQueryIdentity(didorhandle?: string) {
61
-
const [slingshoturl] = useAtom(slingshotURLAtom)
62
return useQuery(constructIdentityQuery(didorhandle, slingshoturl));
63
}
64
···
66
return queryOptions({
67
queryKey: ["post", uri],
68
queryFn: async () => {
69
-
if (!uri) return undefined as undefined
70
const res = await fetch(
71
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
72
);
···
77
return undefined;
78
}
79
if (res.status === 400) return undefined;
80
-
if (data?.error === "InvalidRequest" && data.message?.includes("Could not find repo")) {
81
return undefined; // cache “not found”
82
}
83
try {
84
if (!res.ok) throw new Error("Failed to fetch post");
85
-
return (data) as {
86
uri: string;
87
cid: string;
88
value: any;
···
97
return failureCount < 2;
98
},
99
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
100
-
gcTime: /*0//*/5 * 60 * 1000,
101
});
102
}
103
export function useQueryPost(uri: string): UseQueryResult<
···
108
},
109
Error
110
>;
111
-
export function useQueryPost(): UseQueryResult<
112
-
undefined,
113
-
Error
114
-
>
115
-
export function useQueryPost(uri?: string):
116
-
UseQueryResult<
117
-
{
118
-
uri: string;
119
-
cid: string;
120
-
value: ATPAPI.AppBskyFeedPost.Record;
121
-
} | undefined,
122
-
Error
123
-
>
124
export function useQueryPost(uri?: string) {
125
-
const [slingshoturl] = useAtom(slingshotURLAtom)
126
return useQuery(constructPostQuery(uri, slingshoturl));
127
}
128
···
130
return queryOptions({
131
queryKey: ["profile", uri],
132
queryFn: async () => {
133
-
if (!uri) return undefined as undefined
134
const res = await fetch(
135
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
136
);
···
141
return undefined;
142
}
143
if (res.status === 400) return undefined;
144
-
if (data?.error === "InvalidRequest" && data.message?.includes("Could not find repo")) {
145
return undefined; // cache “not found”
146
}
147
try {
148
if (!res.ok) throw new Error("Failed to fetch post");
149
-
return (data) as {
150
uri: string;
151
cid: string;
152
value: any;
···
161
return failureCount < 2;
162
},
163
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
164
-
gcTime: /*0//*/5 * 60 * 1000,
165
});
166
}
167
export function useQueryProfile(uri: string): UseQueryResult<
···
172
},
173
Error
174
>;
175
-
export function useQueryProfile(): UseQueryResult<
176
-
undefined,
177
-
Error
178
-
>;
179
-
export function useQueryProfile(uri?: string):
180
-
UseQueryResult<
181
-
{
182
uri: string;
183
cid: string;
184
value: ATPAPI.AppBskyActorProfile.Record;
185
-
} | undefined,
186
-
Error
187
-
>
188
export function useQueryProfile(uri?: string) {
189
-
const [slingshoturl] = useAtom(slingshotURLAtom)
190
return useQuery(constructProfileQuery(uri, slingshoturl));
191
}
192
···
222
// method: "/links/all",
223
// target: string
224
// ): QueryOptions<linksAllResponse, Error>;
225
-
export function constructConstellationQuery(query?:{
226
-
constellation: string,
227
method:
228
| "/links"
229
| "/links/distinct-dids"
230
| "/links/count"
231
| "/links/count/distinct-dids"
232
| "/links/all"
233
-
| "undefined",
234
-
target: string,
235
-
collection?: string,
236
-
path?: string,
237
-
cursor?: string,
238
-
dids?: string[]
239
-
}
240
-
) {
241
// : QueryOptions<
242
// | linksRecordsResponse
243
// | linksDidsResponse
···
247
// Error
248
// >
249
return queryOptions({
250
-
queryKey: ["constellation", query?.method, query?.target, query?.collection, query?.path, query?.cursor, query?.dids] as const,
251
queryFn: async () => {
252
-
if (!query || query.method === "undefined") return undefined as undefined
253
-
const method = query.method
254
-
const target = query.target
255
-
const collection = query?.collection
256
-
const path = query?.path
257
-
const cursor = query.cursor
258
-
const dids = query?.dids
259
const res = await fetch(
260
`https://${query.constellation}${method}?target=${encodeURIComponent(target)}${collection ? `&collection=${encodeURIComponent(collection)}` : ""}${path ? `&path=${encodeURIComponent(path)}` : ""}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}${dids ? dids.map((did) => `&did=${encodeURIComponent(did)}`).join("") : ""}`
261
);
···
281
},
282
// enforce short lifespan
283
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
284
-
gcTime: /*0//*/5 * 60 * 1000,
285
});
286
}
287
// todo do more of these instead of overloads since overloads sucks so much apparently
···
293
cursor?: string;
294
}): UseQueryResult<linksCountResponse, Error> | undefined {
295
//if (!query) return;
296
-
const [constellationurl] = useAtom(constellationURLAtom)
297
const queryres = useQuery(
298
-
constructConstellationQuery(query && {constellation: constellationurl, ...query})
299
) as unknown as UseQueryResult<linksCountResponse, Error>;
300
if (!query) {
301
-
return undefined as undefined;
302
}
303
return queryres as UseQueryResult<linksCountResponse, Error>;
304
}
···
365
>
366
| undefined {
367
//if (!query) return;
368
-
const [constellationurl] = useAtom(constellationURLAtom)
369
return useQuery(
370
-
constructConstellationQuery(query && {constellation: constellationurl, ...query})
371
);
372
}
373
···
411
}) {
412
return queryOptions({
413
// The query key includes all dependencies to ensure it refetches when they change
414
-
queryKey: ["feedSkeleton", options?.feedUri, { isAuthed: options?.isAuthed, did: options?.agent?.did }],
415
queryFn: async () => {
416
-
if (!options) return undefined as undefined
417
const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid } = options;
418
if (isAuthed) {
419
// Authenticated flow
420
if (!agent || !pdsUrl || !feedServiceDid) {
421
-
throw new Error("Missing required info for authenticated feed fetch.");
422
}
423
const url = `${pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}`;
424
const res = await agent.fetchHandler(url, {
···
428
"Content-Type": "application/json",
429
},
430
});
431
-
if (!res.ok) throw new Error(`Authenticated feed fetch failed: ${res.statusText}`);
432
return (await res.json()) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema;
433
} else {
434
// Unauthenticated flow (using a public PDS/AppView)
435
const url = `https://discover.bsky.app/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}`;
436
const res = await fetch(url);
437
-
if (!res.ok) throw new Error(`Public feed fetch failed: ${res.statusText}`);
438
return (await res.json()) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema;
439
}
440
},
···
452
return useQuery(constructFeedSkeletonQuery(options));
453
}
454
455
-
export function constructPreferencesQuery(agent?: ATPAPI.Agent | undefined, pdsUrl?: string | undefined) {
456
return queryOptions({
457
-
queryKey: ['preferences', agent?.did],
458
queryFn: async () => {
459
if (!agent || !pdsUrl) throw new Error("Agent or PDS URL not available");
460
const url = `${pdsUrl}/xrpc/app.bsky.actor.getPreferences`;
···
465
});
466
}
467
export function useQueryPreferences(options: {
468
-
agent?: ATPAPI.Agent | undefined, pdsUrl?: string | undefined
469
}) {
470
return useQuery(constructPreferencesQuery(options.agent, options.pdsUrl));
471
}
472
-
473
-
474
475
export function constructArbitraryQuery(uri?: string, slingshoturl?: string) {
476
return queryOptions({
477
queryKey: ["arbitrary", uri],
478
queryFn: async () => {
479
-
if (!uri) return undefined as undefined
480
const res = await fetch(
481
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
482
);
···
487
return undefined;
488
}
489
if (res.status === 400) return undefined;
490
-
if (data?.error === "InvalidRequest" && data.message?.includes("Could not find repo")) {
491
return undefined; // cache “not found”
492
}
493
try {
494
if (!res.ok) throw new Error("Failed to fetch post");
495
-
return (data) as {
496
uri: string;
497
cid: string;
498
value: any;
···
507
return failureCount < 2;
508
},
509
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
510
-
gcTime: /*0//*/5 * 60 * 1000,
511
});
512
}
513
export function useQueryArbitrary(uri: string): UseQueryResult<
···
518
},
519
Error
520
>;
521
-
export function useQueryArbitrary(): UseQueryResult<
522
-
undefined,
523
-
Error
524
-
>;
525
export function useQueryArbitrary(uri?: string): UseQueryResult<
526
-
{
527
-
uri: string;
528
-
cid: string;
529
-
value: any;
530
-
} | undefined,
531
Error
532
>;
533
export function useQueryArbitrary(uri?: string) {
534
-
const [slingshoturl] = useAtom(slingshotURLAtom)
535
return useQuery(constructArbitraryQuery(uri, slingshoturl));
536
}
537
538
-
export function constructFallbackNothingQuery(){
539
return queryOptions({
540
queryKey: ["nothing"],
541
queryFn: async () => {
542
-
return undefined
543
},
544
});
545
}
···
553
}[];
554
};
555
556
-
export function constructAuthorFeedQuery(did: string, pdsUrl: string, collection: string = "app.bsky.feed.post") {
557
return queryOptions({
558
-
queryKey: ['authorFeed', did, collection],
559
queryFn: async ({ pageParam }: QueryFunctionContext) => {
560
const limit = 25;
561
-
562
const cursor = pageParam as string | undefined;
563
-
const cursorParam = cursor ? `&cursor=${cursor}` : '';
564
-
565
const url = `${pdsUrl}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=${collection}&limit=${limit}${cursorParam}`;
566
-
567
const res = await fetch(url);
568
if (!res.ok) throw new Error("Failed to fetch author's posts");
569
-
570
return res.json() as Promise<ListRecordsResponse>;
571
},
572
});
573
}
574
575
-
export function useInfiniteQueryAuthorFeed(did: string | undefined, pdsUrl: string | undefined, collection?: string) {
576
-
const { queryKey, queryFn } = constructAuthorFeedQuery(did!, pdsUrl!, collection);
577
-
578
return useInfiniteQuery({
579
queryKey,
580
queryFn,
···
595
// todo the hell is a unauthedfeedurl
596
unauthedfeedurl?: string;
597
}) {
598
-
const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid, unauthedfeedurl } = options;
599
-
600
return queryOptions({
601
queryKey: ["feedSkeleton", feedUri, { isAuthed, did: agent?.did }],
602
-
603
-
queryFn: async ({ pageParam }: QueryFunctionContext): Promise<FeedSkeletonPage> => {
604
const cursorParam = pageParam ? `&cursor=${pageParam}` : "";
605
-
606
if (isAuthed && !unauthedfeedurl) {
607
if (!agent || !pdsUrl || !feedServiceDid) {
608
-
throw new Error("Missing required info for authenticated feed fetch.");
609
}
610
const url = `${pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`;
611
const res = await agent.fetchHandler(url, {
···
615
"Content-Type": "application/json",
616
},
617
});
618
-
if (!res.ok) throw new Error(`Authenticated feed fetch failed: ${res.statusText}`);
619
return (await res.json()) as FeedSkeletonPage;
620
} else {
621
const url = `https://${unauthedfeedurl ? unauthedfeedurl : "discover.bsky.app"}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`;
622
const res = await fetch(url);
623
-
if (!res.ok) throw new Error(`Public feed fetch failed: ${res.statusText}`);
624
return (await res.json()) as FeedSkeletonPage;
625
}
626
},
···
636
unauthedfeedurl?: string;
637
}) {
638
const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options);
639
-
640
-
return {...useInfiniteQuery({
641
-
queryKey,
642
-
queryFn,
643
-
initialPageParam: undefined as never,
644
-
getNextPageParam: (lastPage) => lastPage.cursor as null | undefined,
645
-
staleTime: Infinity,
646
-
refetchOnWindowFocus: false,
647
-
enabled: !!options.feedUri && (options.isAuthed ? (!!options.agent && !!options.pdsUrl || !!options.unauthedfeedurl) && !!options.feedServiceDid : true),
648
-
}), queryKey: queryKey};
649
}
650
-
651
652
export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(query?: {
653
-
constellation: string,
654
-
method: '/links'
655
-
target?: string
656
-
collection: string
657
-
path: string,
658
-
staleMult?: number
659
}) {
660
const safemult = query?.staleMult ?? 1;
661
// console.log(
···
666
return infiniteQueryOptions({
667
enabled: !!query?.target,
668
queryKey: [
669
-
'reddwarf_constellation',
670
query?.method,
671
query?.target,
672
query?.collection,
673
query?.path,
674
] as const,
675
676
-
queryFn: async ({pageParam}: {pageParam?: string}) => {
677
-
if (!query || !query?.target) return undefined
678
679
-
const method = query.method
680
-
const target = query.target
681
-
const collection = query.collection
682
-
const path = query.path
683
-
const cursor = pageParam
684
685
const res = await fetch(
686
`https://${query.constellation}${method}?target=${encodeURIComponent(target)}${
687
-
collection ? `&collection=${encodeURIComponent(collection)}` : ''
688
-
}${path ? `&path=${encodeURIComponent(path)}` : ''}${
689
-
cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''
690
-
}`,
691
-
)
692
693
-
if (!res.ok) throw new Error('Failed to fetch')
694
695
-
return (await res.json()) as linksRecordsResponse
696
},
697
698
-
getNextPageParam: lastPage => {
699
-
return (lastPage as any)?.cursor ?? undefined
700
},
701
initialPageParam: undefined,
702
staleTime: 5 * 60 * 1000 * safemult,
703
gcTime: 5 * 60 * 1000 * safemult,
704
-
})
705
-
}
···
5
queryOptions,
6
useInfiniteQuery,
7
useQuery,
8
+
type UseQueryResult,
9
+
} from "@tanstack/react-query";
10
import { useAtom } from "jotai";
11
12
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
13
+
14
+
import { constellationURLAtom, lycanURLAtom, slingshotURLAtom } from "./atoms";
15
16
+
export function constructIdentityQuery(
17
+
didorhandle?: string,
18
+
slingshoturl?: string
19
+
) {
20
return queryOptions({
21
queryKey: ["identity", didorhandle],
22
queryFn: async () => {
23
+
if (!didorhandle) return undefined as undefined;
24
const res = await fetch(
25
`https://${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}`
26
);
···
37
}
38
},
39
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
40
+
gcTime: /*0//*/ 5 * 60 * 1000,
41
});
42
}
43
export function useQueryIdentity(didorhandle: string): UseQueryResult<
···
49
},
50
Error
51
>;
52
+
export function useQueryIdentity(): UseQueryResult<undefined, Error>;
53
+
export function useQueryIdentity(didorhandle?: string): UseQueryResult<
54
+
| {
55
+
did: string;
56
+
handle: string;
57
+
pds: string;
58
+
signing_key: string;
59
+
}
60
+
| undefined,
61
+
Error
62
+
>;
63
export function useQueryIdentity(didorhandle?: string) {
64
+
const [slingshoturl] = useAtom(slingshotURLAtom);
65
return useQuery(constructIdentityQuery(didorhandle, slingshoturl));
66
}
67
···
69
return queryOptions({
70
queryKey: ["post", uri],
71
queryFn: async () => {
72
+
if (!uri) return undefined as undefined;
73
const res = await fetch(
74
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
75
);
···
80
return undefined;
81
}
82
if (res.status === 400) return undefined;
83
+
if (
84
+
data?.error === "InvalidRequest" &&
85
+
data.message?.includes("Could not find repo")
86
+
) {
87
return undefined; // cache “not found”
88
}
89
try {
90
if (!res.ok) throw new Error("Failed to fetch post");
91
+
return data as {
92
uri: string;
93
cid: string;
94
value: any;
···
103
return failureCount < 2;
104
},
105
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
106
+
gcTime: /*0//*/ 5 * 60 * 1000,
107
});
108
}
109
export function useQueryPost(uri: string): UseQueryResult<
···
114
},
115
Error
116
>;
117
+
export function useQueryPost(): UseQueryResult<undefined, Error>;
118
+
export function useQueryPost(uri?: string): UseQueryResult<
119
+
| {
120
+
uri: string;
121
+
cid: string;
122
+
value: ATPAPI.AppBskyFeedPost.Record;
123
+
}
124
+
| undefined,
125
+
Error
126
+
>;
127
export function useQueryPost(uri?: string) {
128
+
const [slingshoturl] = useAtom(slingshotURLAtom);
129
return useQuery(constructPostQuery(uri, slingshoturl));
130
}
131
···
133
return queryOptions({
134
queryKey: ["profile", uri],
135
queryFn: async () => {
136
+
if (!uri) return undefined as undefined;
137
const res = await fetch(
138
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
139
);
···
144
return undefined;
145
}
146
if (res.status === 400) return undefined;
147
+
if (
148
+
data?.error === "InvalidRequest" &&
149
+
data.message?.includes("Could not find repo")
150
+
) {
151
return undefined; // cache “not found”
152
}
153
try {
154
if (!res.ok) throw new Error("Failed to fetch post");
155
+
return data as {
156
uri: string;
157
cid: string;
158
value: any;
···
167
return failureCount < 2;
168
},
169
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
170
+
gcTime: /*0//*/ 5 * 60 * 1000,
171
});
172
}
173
export function useQueryProfile(uri: string): UseQueryResult<
···
178
},
179
Error
180
>;
181
+
export function useQueryProfile(): UseQueryResult<undefined, Error>;
182
+
export function useQueryProfile(uri?: string): UseQueryResult<
183
+
| {
184
uri: string;
185
cid: string;
186
value: ATPAPI.AppBskyActorProfile.Record;
187
+
}
188
+
| undefined,
189
+
Error
190
+
>;
191
export function useQueryProfile(uri?: string) {
192
+
const [slingshoturl] = useAtom(slingshotURLAtom);
193
return useQuery(constructProfileQuery(uri, slingshoturl));
194
}
195
···
225
// method: "/links/all",
226
// target: string
227
// ): QueryOptions<linksAllResponse, Error>;
228
+
export function constructConstellationQuery(query?: {
229
+
constellation: string;
230
method:
231
| "/links"
232
| "/links/distinct-dids"
233
| "/links/count"
234
| "/links/count/distinct-dids"
235
| "/links/all"
236
+
| "undefined";
237
+
target: string;
238
+
collection?: string;
239
+
path?: string;
240
+
cursor?: string;
241
+
dids?: string[];
242
+
}) {
243
// : QueryOptions<
244
// | linksRecordsResponse
245
// | linksDidsResponse
···
249
// Error
250
// >
251
return queryOptions({
252
+
queryKey: [
253
+
"constellation",
254
+
query?.method,
255
+
query?.target,
256
+
query?.collection,
257
+
query?.path,
258
+
query?.cursor,
259
+
query?.dids,
260
+
] as const,
261
queryFn: async () => {
262
+
if (!query || query.method === "undefined") return undefined as undefined;
263
+
const method = query.method;
264
+
const target = query.target;
265
+
const collection = query?.collection;
266
+
const path = query?.path;
267
+
const cursor = query.cursor;
268
+
const dids = query?.dids;
269
const res = await fetch(
270
`https://${query.constellation}${method}?target=${encodeURIComponent(target)}${collection ? `&collection=${encodeURIComponent(collection)}` : ""}${path ? `&path=${encodeURIComponent(path)}` : ""}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}${dids ? dids.map((did) => `&did=${encodeURIComponent(did)}`).join("") : ""}`
271
);
···
291
},
292
// enforce short lifespan
293
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
294
+
gcTime: /*0//*/ 5 * 60 * 1000,
295
});
296
}
297
// todo do more of these instead of overloads since overloads sucks so much apparently
···
303
cursor?: string;
304
}): UseQueryResult<linksCountResponse, Error> | undefined {
305
//if (!query) return;
306
+
const [constellationurl] = useAtom(constellationURLAtom);
307
const queryres = useQuery(
308
+
constructConstellationQuery(
309
+
query && { constellation: constellationurl, ...query }
310
+
)
311
) as unknown as UseQueryResult<linksCountResponse, Error>;
312
if (!query) {
313
+
return undefined as undefined;
314
}
315
return queryres as UseQueryResult<linksCountResponse, Error>;
316
}
···
377
>
378
| undefined {
379
//if (!query) return;
380
+
const [constellationurl] = useAtom(constellationURLAtom);
381
return useQuery(
382
+
constructConstellationQuery(
383
+
query && { constellation: constellationurl, ...query }
384
+
)
385
);
386
}
387
···
425
}) {
426
return queryOptions({
427
// The query key includes all dependencies to ensure it refetches when they change
428
+
queryKey: [
429
+
"feedSkeleton",
430
+
options?.feedUri,
431
+
{ isAuthed: options?.isAuthed, did: options?.agent?.did },
432
+
],
433
queryFn: async () => {
434
+
if (!options) return undefined as undefined;
435
const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid } = options;
436
if (isAuthed) {
437
// Authenticated flow
438
if (!agent || !pdsUrl || !feedServiceDid) {
439
+
throw new Error(
440
+
"Missing required info for authenticated feed fetch."
441
+
);
442
}
443
const url = `${pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}`;
444
const res = await agent.fetchHandler(url, {
···
448
"Content-Type": "application/json",
449
},
450
});
451
+
if (!res.ok)
452
+
throw new Error(`Authenticated feed fetch failed: ${res.statusText}`);
453
return (await res.json()) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema;
454
} else {
455
// Unauthenticated flow (using a public PDS/AppView)
456
const url = `https://discover.bsky.app/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}`;
457
const res = await fetch(url);
458
+
if (!res.ok)
459
+
throw new Error(`Public feed fetch failed: ${res.statusText}`);
460
return (await res.json()) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema;
461
}
462
},
···
474
return useQuery(constructFeedSkeletonQuery(options));
475
}
476
477
+
export function constructPreferencesQuery(
478
+
agent?: ATPAPI.Agent | undefined,
479
+
pdsUrl?: string | undefined
480
+
) {
481
return queryOptions({
482
+
queryKey: ["preferences", agent?.did],
483
queryFn: async () => {
484
if (!agent || !pdsUrl) throw new Error("Agent or PDS URL not available");
485
const url = `${pdsUrl}/xrpc/app.bsky.actor.getPreferences`;
···
490
});
491
}
492
export function useQueryPreferences(options: {
493
+
agent?: ATPAPI.Agent | undefined;
494
+
pdsUrl?: string | undefined;
495
}) {
496
return useQuery(constructPreferencesQuery(options.agent, options.pdsUrl));
497
}
498
499
export function constructArbitraryQuery(uri?: string, slingshoturl?: string) {
500
return queryOptions({
501
queryKey: ["arbitrary", uri],
502
queryFn: async () => {
503
+
if (!uri) return undefined as undefined;
504
const res = await fetch(
505
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
506
);
···
511
return undefined;
512
}
513
if (res.status === 400) return undefined;
514
+
if (
515
+
data?.error === "InvalidRequest" &&
516
+
data.message?.includes("Could not find repo")
517
+
) {
518
return undefined; // cache “not found”
519
}
520
try {
521
if (!res.ok) throw new Error("Failed to fetch post");
522
+
return data as {
523
uri: string;
524
cid: string;
525
value: any;
···
534
return failureCount < 2;
535
},
536
staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes
537
+
gcTime: /*0//*/ 5 * 60 * 1000,
538
});
539
}
540
export function useQueryArbitrary(uri: string): UseQueryResult<
···
545
},
546
Error
547
>;
548
+
export function useQueryArbitrary(): UseQueryResult<undefined, Error>;
549
export function useQueryArbitrary(uri?: string): UseQueryResult<
550
+
| {
551
+
uri: string;
552
+
cid: string;
553
+
value: any;
554
+
}
555
+
| undefined,
556
Error
557
>;
558
export function useQueryArbitrary(uri?: string) {
559
+
const [slingshoturl] = useAtom(slingshotURLAtom);
560
return useQuery(constructArbitraryQuery(uri, slingshoturl));
561
}
562
563
+
export function constructFallbackNothingQuery() {
564
return queryOptions({
565
queryKey: ["nothing"],
566
queryFn: async () => {
567
+
return undefined;
568
},
569
});
570
}
···
578
}[];
579
};
580
581
+
export function constructAuthorFeedQuery(
582
+
did: string,
583
+
pdsUrl: string,
584
+
collection: string = "app.bsky.feed.post"
585
+
) {
586
return queryOptions({
587
+
queryKey: ["authorFeed", did, collection],
588
queryFn: async ({ pageParam }: QueryFunctionContext) => {
589
const limit = 25;
590
+
591
const cursor = pageParam as string | undefined;
592
+
const cursorParam = cursor ? `&cursor=${cursor}` : "";
593
+
594
const url = `${pdsUrl}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=${collection}&limit=${limit}${cursorParam}`;
595
+
596
const res = await fetch(url);
597
if (!res.ok) throw new Error("Failed to fetch author's posts");
598
+
599
return res.json() as Promise<ListRecordsResponse>;
600
},
601
});
602
}
603
604
+
export function useInfiniteQueryAuthorFeed(
605
+
did: string | undefined,
606
+
pdsUrl: string | undefined,
607
+
collection?: string
608
+
) {
609
+
const { queryKey, queryFn } = constructAuthorFeedQuery(
610
+
did!,
611
+
pdsUrl!,
612
+
collection
613
+
);
614
+
615
return useInfiniteQuery({
616
queryKey,
617
queryFn,
···
632
// todo the hell is a unauthedfeedurl
633
unauthedfeedurl?: string;
634
}) {
635
+
const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid, unauthedfeedurl } =
636
+
options;
637
+
638
return queryOptions({
639
queryKey: ["feedSkeleton", feedUri, { isAuthed, did: agent?.did }],
640
+
641
+
queryFn: async ({
642
+
pageParam,
643
+
}: QueryFunctionContext): Promise<FeedSkeletonPage> => {
644
const cursorParam = pageParam ? `&cursor=${pageParam}` : "";
645
+
646
if (isAuthed && !unauthedfeedurl) {
647
if (!agent || !pdsUrl || !feedServiceDid) {
648
+
throw new Error(
649
+
"Missing required info for authenticated feed fetch."
650
+
);
651
}
652
const url = `${pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`;
653
const res = await agent.fetchHandler(url, {
···
657
"Content-Type": "application/json",
658
},
659
});
660
+
if (!res.ok)
661
+
throw new Error(`Authenticated feed fetch failed: ${res.statusText}`);
662
return (await res.json()) as FeedSkeletonPage;
663
} else {
664
const url = `https://${unauthedfeedurl ? unauthedfeedurl : "discover.bsky.app"}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`;
665
const res = await fetch(url);
666
+
if (!res.ok)
667
+
throw new Error(`Public feed fetch failed: ${res.statusText}`);
668
return (await res.json()) as FeedSkeletonPage;
669
}
670
},
···
680
unauthedfeedurl?: string;
681
}) {
682
const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options);
683
+
684
+
return {
685
+
...useInfiniteQuery({
686
+
queryKey,
687
+
queryFn,
688
+
initialPageParam: undefined as never,
689
+
getNextPageParam: (lastPage) => lastPage.cursor as null | undefined,
690
+
staleTime: Infinity,
691
+
refetchOnWindowFocus: false,
692
+
enabled:
693
+
!!options.feedUri &&
694
+
(options.isAuthed
695
+
? ((!!options.agent && !!options.pdsUrl) ||
696
+
!!options.unauthedfeedurl) &&
697
+
!!options.feedServiceDid
698
+
: true),
699
+
}),
700
+
queryKey: queryKey,
701
+
};
702
}
703
704
export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(query?: {
705
+
constellation: string;
706
+
method: "/links";
707
+
target?: string;
708
+
collection: string;
709
+
path: string;
710
+
staleMult?: number;
711
}) {
712
const safemult = query?.staleMult ?? 1;
713
// console.log(
···
718
return infiniteQueryOptions({
719
enabled: !!query?.target,
720
queryKey: [
721
+
"reddwarf_constellation",
722
query?.method,
723
query?.target,
724
query?.collection,
725
query?.path,
726
] as const,
727
728
+
queryFn: async ({ pageParam }: { pageParam?: string }) => {
729
+
if (!query || !query?.target) return undefined;
730
731
+
const method = query.method;
732
+
const target = query.target;
733
+
const collection = query.collection;
734
+
const path = query.path;
735
+
const cursor = pageParam;
736
737
const res = await fetch(
738
`https://${query.constellation}${method}?target=${encodeURIComponent(target)}${
739
+
collection ? `&collection=${encodeURIComponent(collection)}` : ""
740
+
}${path ? `&path=${encodeURIComponent(path)}` : ""}${
741
+
cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""
742
+
}`
743
+
);
744
745
+
if (!res.ok) throw new Error("Failed to fetch");
746
747
+
return (await res.json()) as linksRecordsResponse;
748
},
749
750
+
getNextPageParam: (lastPage) => {
751
+
return (lastPage as any)?.cursor ?? undefined;
752
},
753
initialPageParam: undefined,
754
staleTime: 5 * 60 * 1000 * safemult,
755
gcTime: 5 * 60 * 1000 * safemult,
756
+
});
757
+
}
758
+
759
+
export function useQueryLycanStatus() {
760
+
const [lycanurl] = useAtom(lycanURLAtom);
761
+
const { agent, status } = useAuth();
762
+
const { data: identity } = useQueryIdentity(agent?.did);
763
+
return useQuery(
764
+
constructLycanStatusCheckQuery({
765
+
agent: agent || undefined,
766
+
isAuthed: status === "signedIn",
767
+
pdsUrl: identity?.pds,
768
+
feedServiceDid: "did:web:"+lycanurl,
769
+
})
770
+
);
771
+
}
772
+
773
+
export function constructLycanStatusCheckQuery(options: {
774
+
agent?: ATPAPI.Agent;
775
+
isAuthed: boolean;
776
+
pdsUrl?: string;
777
+
feedServiceDid?: string;
778
+
}) {
779
+
const { agent, isAuthed, pdsUrl, feedServiceDid } = options;
780
+
781
+
return queryOptions({
782
+
queryKey: ["lycanStatus", { isAuthed, did: agent?.did }],
783
+
784
+
queryFn: async () => {
785
+
if (isAuthed && agent && pdsUrl && feedServiceDid) {
786
+
const url = `${pdsUrl}/xrpc/blue.feeds.lycan.getImportStatus`;
787
+
const res = await agent.fetchHandler(url, {
788
+
method: "GET",
789
+
headers: {
790
+
"atproto-proxy": `${feedServiceDid}#lycan`,
791
+
"Content-Type": "application/json",
792
+
},
793
+
});
794
+
if (!res.ok)
795
+
throw new Error(
796
+
`Authenticated lycan status fetch failed: ${res.statusText}`
797
+
);
798
+
return (await res.json()) as statuschek;
799
+
}
800
+
return undefined;
801
+
},
802
+
});
803
+
}
804
+
805
+
type statuschek = {
806
+
[key: string]: unknown;
807
+
error?: "MethodNotImplemented";
808
+
message?: "Method Not Implemented";
809
+
status?: "finished";
810
+
};
811
+
812
+
type importtype = {
813
+
message?: "Import has already started" | "Import has been scheduled"
814
+
}
815
+
816
+
export function constructLycanRequestIndexQuery(options: {
817
+
agent?: ATPAPI.Agent;
818
+
isAuthed: boolean;
819
+
pdsUrl?: string;
820
+
feedServiceDid?: string;
821
+
}) {
822
+
const { agent, isAuthed, pdsUrl, feedServiceDid } = options;
823
+
824
+
return queryOptions({
825
+
queryKey: ["lycanIndex", { isAuthed, did: agent?.did }],
826
+
827
+
queryFn: async () => {
828
+
if (isAuthed && agent && pdsUrl && feedServiceDid) {
829
+
const url = `${pdsUrl}/xrpc/blue.feeds.lycan.startImport`;
830
+
const res = await agent.fetchHandler(url, {
831
+
method: "POST",
832
+
headers: {
833
+
"atproto-proxy": `${feedServiceDid}#lycan`,
834
+
"Content-Type": "application/json",
835
+
},
836
+
});
837
+
if (!res.ok)
838
+
throw new Error(
839
+
`Authenticated lycan status fetch failed: ${res.statusText}`
840
+
);
841
+
return await res.json() as importtype;
842
+
}
843
+
return undefined;
844
+
},
845
+
});
846
+
}
847
+
848
+
type LycanSearchPage = {
849
+
terms: string[];
850
+
posts: string[];
851
+
cursor?: string;
852
+
};
853
+
854
+
855
+
export function useInfiniteQueryLycanSearch(options: { query: string, type: "likes" | "pins" | "reposts" | "quotes"}) {
856
+
857
+
858
+
const [lycanurl] = useAtom(lycanURLAtom);
859
+
const { agent, status } = useAuth();
860
+
const { data: identity } = useQueryIdentity(agent?.did);
861
+
862
+
const { queryKey, queryFn } = constructLycanSearchQuery({
863
+
agent: agent || undefined,
864
+
isAuthed: status === "signedIn",
865
+
pdsUrl: identity?.pds,
866
+
feedServiceDid: "did:web:"+lycanurl,
867
+
query: options.query,
868
+
type: options.type,
869
+
})
870
+
871
+
return {
872
+
...useInfiniteQuery({
873
+
queryKey,
874
+
queryFn,
875
+
initialPageParam: undefined as never,
876
+
getNextPageParam: (lastPage) => lastPage?.cursor as null | undefined,
877
+
//staleTime: Infinity,
878
+
refetchOnWindowFocus: false,
879
+
// enabled:
880
+
// !!options.feedUri &&
881
+
// (options.isAuthed
882
+
// ? ((!!options.agent && !!options.pdsUrl) ||
883
+
// !!options.unauthedfeedurl) &&
884
+
// !!options.feedServiceDid
885
+
// : true),
886
+
}),
887
+
queryKey: queryKey,
888
+
};
889
+
}
890
+
891
+
892
+
export function constructLycanSearchQuery(options: {
893
+
agent?: ATPAPI.Agent;
894
+
isAuthed: boolean;
895
+
pdsUrl?: string;
896
+
feedServiceDid?: string;
897
+
type: "likes" | "pins" | "reposts" | "quotes";
898
+
query: string;
899
+
}) {
900
+
const { agent, isAuthed, pdsUrl, feedServiceDid, type, query } = options;
901
+
902
+
return infiniteQueryOptions({
903
+
queryKey: ["lycanSearch", query, type, { isAuthed, did: agent?.did }],
904
+
905
+
queryFn: async ({
906
+
pageParam,
907
+
}: QueryFunctionContext): Promise<LycanSearchPage | undefined> => {
908
+
if (isAuthed && agent && pdsUrl && feedServiceDid) {
909
+
const url = `${pdsUrl}/xrpc/blue.feeds.lycan.searchPosts?query=${query}&collection=${type}${pageParam ? `&cursor=${pageParam}` : ""}`;
910
+
const res = await agent.fetchHandler(url, {
911
+
method: "GET",
912
+
headers: {
913
+
"atproto-proxy": `${feedServiceDid}#lycan`,
914
+
"Content-Type": "application/json",
915
+
},
916
+
});
917
+
if (!res.ok)
918
+
throw new Error(
919
+
`Authenticated lycan status fetch failed: ${res.statusText}`
920
+
);
921
+
return (await res.json()) as LycanSearchPage;
922
+
}
923
+
return undefined;
924
+
},
925
+
initialPageParam: undefined as never,
926
+
getNextPageParam: (lastPage) => lastPage?.cursor as null | undefined,
927
+
});
928
+
}