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 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"; 2 import React, { useEffect, useRef,useState } from "react"; 3 4 import { useAuth } from "~/providers/UnifiedAuthProvider"; 5 6 const HANDLE_DID_CACHE_TIMEOUT = 60 * 60 * 1000; // 1 hour 7 ··· 70 } 71 } 72 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"; 2 3 import { Header } from "~/components/Header"; 4 import Login from "~/components/Login"; 5 6 export const Route = createFileRoute("/settings")({ 7 component: Settings, ··· 21 }} 22 /> 23 <Login /> 24 </> 25 ); 26 }
··· 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 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 10 export function constructIdentityQuery(didorhandle?: string) { 11 return queryOptions({ 12 queryKey: ["identity", didorhandle], 13 queryFn: async () => { 14 if (!didorhandle) return undefined as undefined 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 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 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 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 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,