an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm

customizable microcosm urls

rimar1337 2f2f25fa ba24c311

Changed files
+102 -10
src
+1 -1
src/components/Login.tsx
··· 161 161 onClick={onClick} 162 162 className={`px-4 py-2 text-sm font-medium transition-colors rounded-full flex-1 ${ 163 163 active 164 - ? "text-gray-950 dark:text-gray-200 border-gray-500 bg-gray-400 dark:bg-gray-500" 164 + ? "text-gray-50 dark:text-gray-200 border-gray-500 bg-gray-400 dark:bg-gray-500" 165 165 : "text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200" 166 166 }`} 167 167 >
+7 -3
src/routes/notifications.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { useAtom } from "jotai"; 2 3 import React, { useEffect, useRef,useState } from "react"; 3 4 4 5 import { useAuth } from "~/providers/UnifiedAuthProvider"; 6 + import { constellationURLAtom } from "~/utils/atoms"; 5 7 6 8 const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour 7 9 ··· 70 72 } 71 73 } 72 74 75 + const [constellationURL] = useAtom(constellationURLAtom) 76 + 73 77 useEffect(() => { 74 78 if (!did) return; 75 79 setLoading(true); 76 80 setError(null); 77 81 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`, 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`, 81 85 ]; 82 86 let ignore = false; 83 87 Promise.all(
+70
src/routes/settings.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { useAtom } from "jotai"; 2 3 3 4 import { Header } from "~/components/Header"; 4 5 import Login from "~/components/Login"; 6 + import { 7 + constellationURLAtom, 8 + defaultconstellationURL, 9 + defaultslingshotURL, 10 + slingshotURLAtom, 11 + } from "~/utils/atoms"; 5 12 6 13 export const Route = createFileRoute("/settings")({ 7 14 component: Settings, ··· 21 28 }} 22 29 /> 23 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 + /> 24 45 </> 25 46 ); 26 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 21 {} 22 22 ); 23 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 + 24 35 export const isAtTopAtom = atom<boolean>(true); 25 36 26 37 type ComposerState =
+13 -6
src/utils/useQuery.ts
··· 7 7 useQuery, 8 8 type UseQueryResult} from "@tanstack/react-query"; 9 9 10 + import { constellationURLAtom, slingshotURLAtom, store } from "./atoms"; 11 + 10 12 export function constructIdentityQuery(didorhandle?: string) { 11 13 return queryOptions({ 12 14 queryKey: ["identity", didorhandle], 13 15 queryFn: async () => { 14 16 if (!didorhandle) return undefined as undefined 17 + const slingshoturl = store.get(slingshotURLAtom) 15 18 const res = await fetch( 16 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}` 19 + `https://${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}` 17 20 ); 18 21 if (!res.ok) throw new Error("Failed to fetch post"); 19 22 try { ··· 63 66 queryKey: ["post", uri], 64 67 queryFn: async () => { 65 68 if (!uri) return undefined as undefined 69 + const slingshoturl = store.get(slingshotURLAtom) 66 70 const res = await fetch( 67 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 71 + `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 68 72 ); 69 73 let data: any; 70 74 try { ··· 126 130 queryKey: ["profile", uri], 127 131 queryFn: async () => { 128 132 if (!uri) return undefined as undefined 133 + const slingshoturl = store.get(slingshotURLAtom) 129 134 const res = await fetch( 130 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 135 + `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 131 136 ); 132 137 let data: any; 133 138 try { ··· 249 254 const path = query?.path 250 255 const cursor = query.cursor 251 256 const dids = query?.dids 257 + const constellation = store.get(constellationURLAtom); 252 258 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("") : ""}` 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("") : ""}` 254 260 ); 255 261 if (!res.ok) throw new Error("Failed to fetch post"); 256 262 try { ··· 450 456 queryKey: ["arbitrary", uri], 451 457 queryFn: async () => { 452 458 if (!uri) return undefined as undefined 459 + const slingshoturl = store.get(slingshotURLAtom) 453 460 const res = await fetch( 454 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 461 + `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 455 462 ); 456 463 let data: any; 457 464 try { ··· 624 631 collection: string 625 632 path: string 626 633 }) { 627 - const constellationHost = 'constellation.microcosm.blue' 634 + const constellationHost = store.get(constellationURLAtom) 628 635 console.log( 629 636 'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks', 630 637 query,