+27
-4
src/components/Import.tsx
+27
-4
src/components/Import.tsx
···
1
1
import { AtUri } from "@atproto/api";
2
2
import { useNavigate, type UseNavigateResult } from "@tanstack/react-router";
3
+
import { useAtom } from "jotai";
3
4
import { useState } from "react";
4
5
6
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
7
+
import { lycanURLAtom } from "~/utils/atoms";
8
+
import { useQueryLycanStatus } from "~/utils/useQuery";
9
+
5
10
/**
6
11
* Basically the best equivalent to Search that i can do
7
12
*/
8
-
export function Import() {
9
-
const [textInput, setTextInput] = useState<string | undefined>();
13
+
export function Import({optionaltextstring}: {optionaltextstring?: string}) {
14
+
const [textInput, setTextInput] = useState<string | undefined>(optionaltextstring);
10
15
const navigate = useNavigate();
11
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
+
12
26
const handleEnter = () => {
13
27
if (!textInput) return;
14
28
handleImport({
15
29
text: textInput,
16
30
navigate,
31
+
lycanReady: lycanReady,
17
32
});
18
33
};
19
34
35
+
const placeholder = lycanReady ? "Search..." : "Import...";
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
-
placeholder="Import..."
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
+
lycanReady,
41
59
}: {
42
60
text: string;
43
61
navigate: UseNavigateResult<string>;
62
+
lycanReady?: boolean;
44
63
}) {
45
64
const trimmed = text.trim();
46
65
// parse text
···
147
166
// } catch {
148
167
// // continue
149
168
// }
150
-
}
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
1252
1253
1253
import defaultpfp from "~/../public/favicon.png";
1254
1254
import { useAuth } from "~/providers/UnifiedAuthProvider";
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
-
return `<a href="https://${undfediwafrnHost}/search/${encoded}" target="_blank">#${tag.replaceAll(' ','-')}</a>`;
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
+
renderSnack({
2017
+
title: "Copied to clipboard!",
2018
+
});
2015
2019
} catch (_e) {
2016
2020
// idk
2021
+
renderSnack({
2022
+
title: "Failed to copy link",
2023
+
});
2017
2024
}
2018
2025
}}
2019
2026
style={{
···
2022
2029
>
2023
2030
<MdiShareVariant />
2024
2031
</HitSlopButton>
2025
-
<span style={btnstyle}>
2026
-
<MdiMoreHoriz />
2027
-
</span>
2032
+
<HitSlopButton
2033
+
onClick={() => {
2034
+
renderSnack({
2035
+
title: "Not implemented yet...",
2036
+
});
2037
+
}}
2038
+
>
2039
+
<span style={btnstyle}>
2040
+
<MdiMoreHoriz />
2041
+
</span>
2042
+
</HitSlopButton>
2028
2043
</div>
2029
2044
</div>
2030
2045
)}
+189
-9
src/routes/search.tsx
+189
-9
src/routes/search.tsx
···
1
-
import { createFileRoute } from "@tanstack/react-router";
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";
2
6
3
7
import { Header } from "~/components/Header";
4
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";
5
24
6
25
export const Route = createFileRoute("/search")({
7
26
component: Search,
8
27
});
9
28
10
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
+
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
-
<Import />
101
+
<Import optionaltextstring={q} />
25
102
<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>
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
-
Bluesky URLs from supported clients (like{" "}
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
-
Plain handles (like{" "}
116
+
User Handles (like{" "}
43
117
<code className="text-sm">@username.bsky.social</code>).
44
118
</li>
45
119
<li>
46
-
Direct DIDs (Decentralized Identifiers, starting with{" "}
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
+
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
+
)}
54
149
</div>
55
150
</div>
151
+
{q ? <SearchTabs query={q} /> : <></>}
56
152
</>
57
153
);
58
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
10
defaultconstellationURL,
11
11
defaulthue,
12
12
defaultImgCDN,
13
+
defaultLycanURL,
13
14
defaultslingshotURL,
14
15
defaultVideoCDN,
15
16
enableBitesAtom,
···
17
18
enableWafrnTextAtom,
18
19
hueAtom,
19
20
imgCDNAtom,
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
+
/>
116
+
<TextInputSetting
117
+
atom={lycanURLAtom}
118
+
title={"Lycan Search"}
119
+
description={"Enable text search across posts you've interacted with"}
120
+
init={defaultLycanURL}
113
121
/>
114
122
115
123
<SettingHeading title="Experimental" />
+6
src/utils/atoms.ts
+6
src/utils/atoms.ts
···
92
92
defaultVideoCDN
93
93
);
94
94
95
+
export const defaultLycanURL = "";
96
+
export const lycanURLAtom = atomWithStorage<string>(
97
+
"lycanURL",
98
+
defaultLycanURL
99
+
);
100
+
95
101
export const defaulthue = 28;
96
102
export const hueAtom = atomWithStorage<number>("hue", defaulthue);
97
103
+380
-157
src/utils/useQuery.ts
+380
-157
src/utils/useQuery.ts
···
5
5
queryOptions,
6
6
useInfiniteQuery,
7
7
useQuery,
8
-
type UseQueryResult} from "@tanstack/react-query";
8
+
type UseQueryResult,
9
+
} from "@tanstack/react-query";
9
10
import { useAtom } from "jotai";
10
11
11
-
import { constellationURLAtom, slingshotURLAtom } from "./atoms";
12
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
13
+
14
+
import { constellationURLAtom, lycanURLAtom, slingshotURLAtom } from "./atoms";
12
15
13
-
export function constructIdentityQuery(didorhandle?: string, slingshoturl?: string) {
16
+
export function constructIdentityQuery(
17
+
didorhandle?: string,
18
+
slingshoturl?: string
19
+
) {
14
20
return queryOptions({
15
21
queryKey: ["identity", didorhandle],
16
22
queryFn: async () => {
17
-
if (!didorhandle) return undefined as undefined
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
-
gcTime: /*0//*/5 * 60 * 1000,
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
-
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
-
>
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
+
>;
60
63
export function useQueryIdentity(didorhandle?: string) {
61
-
const [slingshoturl] = useAtom(slingshotURLAtom)
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
-
if (!uri) return undefined as undefined
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
-
if (data?.error === "InvalidRequest" && data.message?.includes("Could not find repo")) {
83
+
if (
84
+
data?.error === "InvalidRequest" &&
85
+
data.message?.includes("Could not find repo")
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
-
return (data) as {
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
-
gcTime: /*0//*/5 * 60 * 1000,
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
-
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
-
>
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
+
>;
124
127
export function useQueryPost(uri?: string) {
125
-
const [slingshoturl] = useAtom(slingshotURLAtom)
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
-
if (!uri) return undefined as undefined
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
-
if (data?.error === "InvalidRequest" && data.message?.includes("Could not find repo")) {
147
+
if (
148
+
data?.error === "InvalidRequest" &&
149
+
data.message?.includes("Could not find repo")
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
-
return (data) as {
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
-
gcTime: /*0//*/5 * 60 * 1000,
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
-
export function useQueryProfile(): UseQueryResult<
176
-
undefined,
177
-
Error
178
-
>;
179
-
export function useQueryProfile(uri?: string):
180
-
UseQueryResult<
181
-
{
181
+
export function useQueryProfile(): UseQueryResult<undefined, Error>;
182
+
export function useQueryProfile(uri?: string): UseQueryResult<
183
+
| {
182
184
uri: string;
183
185
cid: string;
184
186
value: ATPAPI.AppBskyActorProfile.Record;
185
-
} | undefined,
186
-
Error
187
-
>
187
+
}
188
+
| undefined,
189
+
Error
190
+
>;
188
191
export function useQueryProfile(uri?: string) {
189
-
const [slingshoturl] = useAtom(slingshotURLAtom)
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
-
export function constructConstellationQuery(query?:{
226
-
constellation: string,
228
+
export function constructConstellationQuery(query?: {
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
-
| "undefined",
234
-
target: string,
235
-
collection?: string,
236
-
path?: string,
237
-
cursor?: string,
238
-
dids?: string[]
239
-
}
240
-
) {
236
+
| "undefined";
237
+
target: string;
238
+
collection?: string;
239
+
path?: string;
240
+
cursor?: string;
241
+
dids?: string[];
242
+
}) {
241
243
// : QueryOptions<
242
244
// | linksRecordsResponse
243
245
// | linksDidsResponse
···
247
249
// Error
248
250
// >
249
251
return queryOptions({
250
-
queryKey: ["constellation", query?.method, query?.target, query?.collection, query?.path, query?.cursor, query?.dids] as const,
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,
251
261
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
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;
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
-
gcTime: /*0//*/5 * 60 * 1000,
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
-
const [constellationurl] = useAtom(constellationURLAtom)
306
+
const [constellationurl] = useAtom(constellationURLAtom);
297
307
const queryres = useQuery(
298
-
constructConstellationQuery(query && {constellation: constellationurl, ...query})
308
+
constructConstellationQuery(
309
+
query && { constellation: constellationurl, ...query }
310
+
)
299
311
) as unknown as UseQueryResult<linksCountResponse, Error>;
300
312
if (!query) {
301
-
return undefined as undefined;
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
-
const [constellationurl] = useAtom(constellationURLAtom)
380
+
const [constellationurl] = useAtom(constellationURLAtom);
369
381
return useQuery(
370
-
constructConstellationQuery(query && {constellation: constellationurl, ...query})
382
+
constructConstellationQuery(
383
+
query && { constellation: constellationurl, ...query }
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
-
queryKey: ["feedSkeleton", options?.feedUri, { isAuthed: options?.isAuthed, did: options?.agent?.did }],
428
+
queryKey: [
429
+
"feedSkeleton",
430
+
options?.feedUri,
431
+
{ isAuthed: options?.isAuthed, did: options?.agent?.did },
432
+
],
415
433
queryFn: async () => {
416
-
if (!options) return undefined as undefined
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
-
throw new Error("Missing required info for authenticated feed fetch.");
439
+
throw new Error(
440
+
"Missing required info for authenticated feed fetch."
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
-
if (!res.ok) throw new Error(`Authenticated feed fetch failed: ${res.statusText}`);
451
+
if (!res.ok)
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
-
if (!res.ok) throw new Error(`Public feed fetch failed: ${res.statusText}`);
458
+
if (!res.ok)
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
-
export function constructPreferencesQuery(agent?: ATPAPI.Agent | undefined, pdsUrl?: string | undefined) {
477
+
export function constructPreferencesQuery(
478
+
agent?: ATPAPI.Agent | undefined,
479
+
pdsUrl?: string | undefined
480
+
) {
456
481
return queryOptions({
457
-
queryKey: ['preferences', agent?.did],
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
-
agent?: ATPAPI.Agent | undefined, pdsUrl?: string | undefined
493
+
agent?: ATPAPI.Agent | undefined;
494
+
pdsUrl?: string | undefined;
469
495
}) {
470
496
return useQuery(constructPreferencesQuery(options.agent, options.pdsUrl));
471
497
}
472
-
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
-
if (!uri) return undefined as undefined
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
-
if (data?.error === "InvalidRequest" && data.message?.includes("Could not find repo")) {
514
+
if (
515
+
data?.error === "InvalidRequest" &&
516
+
data.message?.includes("Could not find repo")
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
-
return (data) as {
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
-
gcTime: /*0//*/5 * 60 * 1000,
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
-
export function useQueryArbitrary(): UseQueryResult<
522
-
undefined,
523
-
Error
524
-
>;
548
+
export function useQueryArbitrary(): UseQueryResult<undefined, Error>;
525
549
export function useQueryArbitrary(uri?: string): UseQueryResult<
526
-
{
527
-
uri: string;
528
-
cid: string;
529
-
value: any;
530
-
} | undefined,
550
+
| {
551
+
uri: string;
552
+
cid: string;
553
+
value: any;
554
+
}
555
+
| undefined,
531
556
Error
532
557
>;
533
558
export function useQueryArbitrary(uri?: string) {
534
-
const [slingshoturl] = useAtom(slingshotURLAtom)
559
+
const [slingshoturl] = useAtom(slingshotURLAtom);
535
560
return useQuery(constructArbitraryQuery(uri, slingshoturl));
536
561
}
537
562
538
-
export function constructFallbackNothingQuery(){
563
+
export function constructFallbackNothingQuery() {
539
564
return queryOptions({
540
565
queryKey: ["nothing"],
541
566
queryFn: async () => {
542
-
return undefined
567
+
return undefined;
543
568
},
544
569
});
545
570
}
···
553
578
}[];
554
579
};
555
580
556
-
export function constructAuthorFeedQuery(did: string, pdsUrl: string, collection: string = "app.bsky.feed.post") {
581
+
export function constructAuthorFeedQuery(
582
+
did: string,
583
+
pdsUrl: string,
584
+
collection: string = "app.bsky.feed.post"
585
+
) {
557
586
return queryOptions({
558
-
queryKey: ['authorFeed', did, collection],
587
+
queryKey: ["authorFeed", did, collection],
559
588
queryFn: async ({ pageParam }: QueryFunctionContext) => {
560
589
const limit = 25;
561
-
590
+
562
591
const cursor = pageParam as string | undefined;
563
-
const cursorParam = cursor ? `&cursor=${cursor}` : '';
564
-
592
+
const cursorParam = cursor ? `&cursor=${cursor}` : "";
593
+
565
594
const url = `${pdsUrl}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=${collection}&limit=${limit}${cursorParam}`;
566
-
595
+
567
596
const res = await fetch(url);
568
597
if (!res.ok) throw new Error("Failed to fetch author's posts");
569
-
598
+
570
599
return res.json() as Promise<ListRecordsResponse>;
571
600
},
572
601
});
573
602
}
574
603
575
-
export function useInfiniteQueryAuthorFeed(did: string | undefined, pdsUrl: string | undefined, collection?: string) {
576
-
const { queryKey, queryFn } = constructAuthorFeedQuery(did!, pdsUrl!, collection);
577
-
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
+
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
-
const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid, unauthedfeedurl } = options;
599
-
635
+
const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid, unauthedfeedurl } =
636
+
options;
637
+
600
638
return queryOptions({
601
639
queryKey: ["feedSkeleton", feedUri, { isAuthed, did: agent?.did }],
602
-
603
-
queryFn: async ({ pageParam }: QueryFunctionContext): Promise<FeedSkeletonPage> => {
640
+
641
+
queryFn: async ({
642
+
pageParam,
643
+
}: QueryFunctionContext): Promise<FeedSkeletonPage> => {
604
644
const cursorParam = pageParam ? `&cursor=${pageParam}` : "";
605
-
645
+
606
646
if (isAuthed && !unauthedfeedurl) {
607
647
if (!agent || !pdsUrl || !feedServiceDid) {
608
-
throw new Error("Missing required info for authenticated feed fetch.");
648
+
throw new Error(
649
+
"Missing required info for authenticated feed fetch."
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
-
if (!res.ok) throw new Error(`Authenticated feed fetch failed: ${res.statusText}`);
660
+
if (!res.ok)
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
-
if (!res.ok) throw new Error(`Public feed fetch failed: ${res.statusText}`);
666
+
if (!res.ok)
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
-
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};
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
+
};
649
702
}
650
-
651
703
652
704
export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(query?: {
653
-
constellation: string,
654
-
method: '/links'
655
-
target?: string
656
-
collection: string
657
-
path: string,
658
-
staleMult?: number
705
+
constellation: string;
706
+
method: "/links";
707
+
target?: string;
708
+
collection: string;
709
+
path: string;
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
-
'reddwarf_constellation',
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
-
queryFn: async ({pageParam}: {pageParam?: string}) => {
677
-
if (!query || !query?.target) return undefined
728
+
queryFn: async ({ pageParam }: { pageParam?: string }) => {
729
+
if (!query || !query?.target) return undefined;
678
730
679
-
const method = query.method
680
-
const target = query.target
681
-
const collection = query.collection
682
-
const path = query.path
683
-
const cursor = pageParam
731
+
const method = query.method;
732
+
const target = query.target;
733
+
const collection = query.collection;
734
+
const path = query.path;
735
+
const cursor = pageParam;
684
736
685
737
const res = await fetch(
686
738
`https://${query.constellation}${method}?target=${encodeURIComponent(target)}${
687
-
collection ? `&collection=${encodeURIComponent(collection)}` : ''
688
-
}${path ? `&path=${encodeURIComponent(path)}` : ''}${
689
-
cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''
690
-
}`,
691
-
)
739
+
collection ? `&collection=${encodeURIComponent(collection)}` : ""
740
+
}${path ? `&path=${encodeURIComponent(path)}` : ""}${
741
+
cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""
742
+
}`
743
+
);
692
744
693
-
if (!res.ok) throw new Error('Failed to fetch')
745
+
if (!res.ok) throw new Error("Failed to fetch");
694
746
695
-
return (await res.json()) as linksRecordsResponse
747
+
return (await res.json()) as linksRecordsResponse;
696
748
},
697
749
698
-
getNextPageParam: lastPage => {
699
-
return (lastPage as any)?.cursor ?? undefined
750
+
getNextPageParam: (lastPage) => {
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
-
})
705
-
}
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
+
}