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
customizable microcosm urls
rimar1337
4 months ago
2f2f25fa
ba24c311
+102
-10
5 changed files
expand all
collapse all
unified
split
src
components
Login.tsx
routes
notifications.tsx
settings.tsx
utils
atoms.ts
useQuery.ts
+1
-1
src/components/Login.tsx
···
161
onClick={onClick}
162
className={`px-4 py-2 text-sm font-medium transition-colors rounded-full flex-1 ${
163
active
164
-
? "text-gray-950 dark:text-gray-200 border-gray-500 bg-gray-400 dark:bg-gray-500"
165
: "text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200"
166
}`}
167
>
···
161
onClick={onClick}
162
className={`px-4 py-2 text-sm font-medium transition-colors rounded-full flex-1 ${
163
active
164
+
? "text-gray-50 dark:text-gray-200 border-gray-500 bg-gray-400 dark:bg-gray-500"
165
: "text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200"
166
}`}
167
>
+7
-3
src/routes/notifications.tsx
···
1
import { createFileRoute } from "@tanstack/react-router";
0
2
import React, { useEffect, useRef,useState } from "react";
3
4
import { useAuth } from "~/providers/UnifiedAuthProvider";
0
5
6
const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour
7
···
70
}
71
}
72
0
0
73
useEffect(() => {
74
if (!did) return;
75
setLoading(true);
76
setError(null);
77
const urls = [
78
-
`https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet%23mention].did`,
79
-
`https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[].features[app.bsky.richtext.facet%23mention].did`,
80
-
`https://constellation.microcosm.blue/links?target=${encodeURIComponent(did)}&collection=app.bsky.graph.follow&path=.subject`,
81
];
82
let ignore = false;
83
Promise.all(
···
1
import { createFileRoute } from "@tanstack/react-router";
2
+
import { useAtom } from "jotai";
3
import React, { useEffect, useRef,useState } from "react";
4
5
import { useAuth } from "~/providers/UnifiedAuthProvider";
6
+
import { constellationURLAtom } from "~/utils/atoms";
7
8
const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour
9
···
72
}
73
}
74
75
+
const [constellationURL] = useAtom(constellationURLAtom)
76
+
77
useEffect(() => {
78
if (!did) return;
79
setLoading(true);
80
setError(null);
81
const urls = [
82
+
`https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet%23mention].did`,
83
+
`https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.feed.post&path=.facets[].features[app.bsky.richtext.facet%23mention].did`,
84
+
`https://${constellationURL}/links?target=${encodeURIComponent(did)}&collection=app.bsky.graph.follow&path=.subject`,
85
];
86
let ignore = false;
87
Promise.all(
+70
src/routes/settings.tsx
···
1
import { createFileRoute } from "@tanstack/react-router";
0
2
3
import { Header } from "~/components/Header";
4
import Login from "~/components/Login";
0
0
0
0
0
0
5
6
export const Route = createFileRoute("/settings")({
7
component: Settings,
···
21
}}
22
/>
23
<Login />
0
0
0
0
0
0
0
0
0
0
0
0
0
0
24
</>
25
);
26
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
import { createFileRoute } from "@tanstack/react-router";
2
+
import { useAtom } from "jotai";
3
4
import { Header } from "~/components/Header";
5
import Login from "~/components/Login";
6
+
import {
7
+
constellationURLAtom,
8
+
defaultconstellationURL,
9
+
defaultslingshotURL,
10
+
slingshotURLAtom,
11
+
} from "~/utils/atoms";
12
13
export const Route = createFileRoute("/settings")({
14
component: Settings,
···
28
}}
29
/>
30
<Login />
31
+
<TextInputSetting
32
+
atom={constellationURLAtom}
33
+
title={"Constellation URL"}
34
+
description={
35
+
"customize the Constellation instance to be used by Red Dwarf"
36
+
}
37
+
init={defaultconstellationURL}
38
+
/>
39
+
<TextInputSetting
40
+
atom={slingshotURLAtom}
41
+
title={"Slingshot URL"}
42
+
description={"customize the Slingshot instance to be used by Red Dwarf"}
43
+
init={defaultslingshotURL}
44
+
/>
45
</>
46
);
47
}
48
+
49
+
export function TextInputSetting({
50
+
atom,
51
+
title,
52
+
description,
53
+
init,
54
+
}: {
55
+
atom: typeof constellationURLAtom;
56
+
title?: string;
57
+
description?: string;
58
+
init?: string;
59
+
}) {
60
+
const [value, setValue] = useAtom(atom);
61
+
return (
62
+
<div className="flex flex-col gap-2 p-4 rounded-2xl border border-gray-200 dark:border-gray-800 ">
63
+
<div>
64
+
{title && (
65
+
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
66
+
{title}
67
+
</h3>
68
+
)}
69
+
{description && (
70
+
<p className="text-sm text-gray-500 dark:text-gray-400">
71
+
{description}
72
+
</p>
73
+
)}
74
+
</div>
75
+
76
+
<div className="flex flex-row gap-2 items-center">
77
+
<input
78
+
type="text"
79
+
value={value}
80
+
onChange={(e) => setValue(e.target.value)}
81
+
className="flex-1 px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700
82
+
text-gray-900 dark:text-gray-100 placeholder:text-gray-500 dark:placeholder:text-gray-400
83
+
focus:outline-none focus:ring-2 focus:ring-gray-400 dark:focus:ring-gray-600"
84
+
placeholder="Enter value..."
85
+
/>
86
+
<button
87
+
onClick={() => setValue(init ?? "")}
88
+
className="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 bg-gray-100 dark:bg-gray-800
89
+
text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition"
90
+
>
91
+
Reset
92
+
</button>
93
+
</div>
94
+
</div>
95
+
);
96
+
}
+11
src/utils/atoms.ts
···
21
{}
22
);
23
0
0
0
0
0
0
0
0
0
0
0
24
export const isAtTopAtom = atom<boolean>(true);
25
26
type ComposerState =
···
21
{}
22
);
23
24
+
export const defaultconstellationURL = 'constellation.microcosm.blue'
25
+
export const constellationURLAtom = atomWithStorage<string>(
26
+
'constellationURL',
27
+
defaultconstellationURL
28
+
)
29
+
export const defaultslingshotURL = 'slingshot.microcosm.blue'
30
+
export const slingshotURLAtom = atomWithStorage<string>(
31
+
'slingshotURL',
32
+
defaultslingshotURL
33
+
)
34
+
35
export const isAtTopAtom = atom<boolean>(true);
36
37
type ComposerState =
+13
-6
src/utils/useQuery.ts
···
7
useQuery,
8
type UseQueryResult} from "@tanstack/react-query";
9
0
0
10
export function constructIdentityQuery(didorhandle?: string) {
11
return queryOptions({
12
queryKey: ["identity", didorhandle],
13
queryFn: async () => {
14
if (!didorhandle) return undefined as undefined
0
15
const res = await fetch(
16
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}`
17
);
18
if (!res.ok) throw new Error("Failed to fetch post");
19
try {
···
63
queryKey: ["post", uri],
64
queryFn: async () => {
65
if (!uri) return undefined as undefined
0
66
const res = await fetch(
67
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
68
);
69
let data: any;
70
try {
···
126
queryKey: ["profile", uri],
127
queryFn: async () => {
128
if (!uri) return undefined as undefined
0
129
const res = await fetch(
130
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
131
);
132
let data: any;
133
try {
···
249
const path = query?.path
250
const cursor = query.cursor
251
const dids = query?.dids
0
252
const res = await fetch(
253
-
`https://constellation.microcosm.blue${method}?target=${encodeURIComponent(target)}${collection ? `&collection=${encodeURIComponent(collection)}` : ""}${path ? `&path=${encodeURIComponent(path)}` : ""}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}${dids ? dids.map((did) => `&did=${encodeURIComponent(did)}`).join("") : ""}`
254
);
255
if (!res.ok) throw new Error("Failed to fetch post");
256
try {
···
450
queryKey: ["arbitrary", uri],
451
queryFn: async () => {
452
if (!uri) return undefined as undefined
0
453
const res = await fetch(
454
-
`https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
455
);
456
let data: any;
457
try {
···
624
collection: string
625
path: string
626
}) {
627
-
const constellationHost = 'constellation.microcosm.blue'
628
console.log(
629
'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks',
630
query,
···
7
useQuery,
8
type UseQueryResult} from "@tanstack/react-query";
9
10
+
import { constellationURLAtom, slingshotURLAtom, store } from "./atoms";
11
+
12
export function constructIdentityQuery(didorhandle?: string) {
13
return queryOptions({
14
queryKey: ["identity", didorhandle],
15
queryFn: async () => {
16
if (!didorhandle) return undefined as undefined
17
+
const slingshoturl = store.get(slingshotURLAtom)
18
const res = await fetch(
19
+
`https://${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}`
20
);
21
if (!res.ok) throw new Error("Failed to fetch post");
22
try {
···
66
queryKey: ["post", uri],
67
queryFn: async () => {
68
if (!uri) return undefined as undefined
69
+
const slingshoturl = store.get(slingshotURLAtom)
70
const res = await fetch(
71
+
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
72
);
73
let data: any;
74
try {
···
130
queryKey: ["profile", uri],
131
queryFn: async () => {
132
if (!uri) return undefined as undefined
133
+
const slingshoturl = store.get(slingshotURLAtom)
134
const res = await fetch(
135
+
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
136
);
137
let data: any;
138
try {
···
254
const path = query?.path
255
const cursor = query.cursor
256
const dids = query?.dids
257
+
const constellation = store.get(constellationURLAtom);
258
const res = await fetch(
259
+
`https://${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("") : ""}`
260
);
261
if (!res.ok) throw new Error("Failed to fetch post");
262
try {
···
456
queryKey: ["arbitrary", uri],
457
queryFn: async () => {
458
if (!uri) return undefined as undefined
459
+
const slingshoturl = store.get(slingshotURLAtom)
460
const res = await fetch(
461
+
`https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}`
462
);
463
let data: any;
464
try {
···
631
collection: string
632
path: string
633
}) {
634
+
const constellationHost = store.get(constellationURLAtom)
635
console.log(
636
'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks',
637
query,