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