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

Compare changes

Choose any two refs to compare.

Changed files
+102 -26
src
components
routes
utils
+16 -5
src/components/Import.tsx
··· 10 10 /** 11 11 * Basically the best equivalent to Search that i can do 12 12 */ 13 - export function Import({optionaltextstring}: {optionaltextstring?: string}) { 14 - const [textInput, setTextInput] = useState<string | undefined>(optionaltextstring); 13 + export function Import({ 14 + optionaltextstring, 15 + }: { 16 + optionaltextstring?: string; 17 + }) { 18 + const [textInput, setTextInput] = useState<string | undefined>( 19 + optionaltextstring 20 + ); 15 21 const navigate = useNavigate(); 16 22 17 23 const { status } = useAuth(); ··· 19 25 const lycanExists = lycandomain !== ""; 20 26 const { data: lycanstatusdata } = useQueryLycanStatus(); 21 27 const lycanIndexed = lycanstatusdata?.status === "finished" || false; 28 + const lycanIndexing = lycanstatusdata?.status === "in_progress" || false; 29 + const lycanIndexingProgress = lycanIndexing 30 + ? lycanstatusdata?.progress 31 + : undefined; 22 32 const authed = status === "signedIn"; 23 33 24 34 const lycanReady = lycanExists && lycanIndexed && authed; ··· 28 38 handleImport({ 29 39 text: textInput, 30 40 navigate, 31 - lycanReady: lycanReady, 41 + lycanReady: 42 + lycanReady || (!!lycanIndexingProgress && lycanIndexingProgress > 0), 32 43 }); 33 44 }; 34 45 ··· 168 179 // } 169 180 170 181 if (lycanReady) { 171 - navigate({ to: "/search", search: { q: text} }) 182 + navigate({ to: "/search", search: { q: text } }); 172 183 } 173 - } 184 + }
+48 -20
src/routes/search.tsx
··· 2 2 import { useQueryClient } from "@tanstack/react-query"; 3 3 import { createFileRoute, useSearch } from "@tanstack/react-router"; 4 4 import { useAtom } from "jotai"; 5 - import { useMemo } from "react"; 5 + import { useEffect,useMemo } from "react"; 6 6 7 7 import { Header } from "~/components/Header"; 8 8 import { Import } from "~/components/Import"; ··· 21 21 } from "~/utils/useQuery"; 22 22 23 23 import { renderSnack } from "./__root"; 24 + import { SliderPrimitive } from "./settings"; 24 25 25 26 export const Route = createFileRoute("/search")({ 26 27 component: Search, ··· 32 33 const { data: identity } = useQueryIdentity(agent?.did); 33 34 const [lycandomain] = useAtom(lycanURLAtom); 34 35 const lycanExists = lycandomain !== ""; 35 - const { data: lycanstatusdata } = useQueryLycanStatus(); 36 + const { data: lycanstatusdata, refetch } = useQueryLycanStatus(); 36 37 const lycanIndexed = lycanstatusdata?.status === "finished" || false; 38 + const lycanIndexing = lycanstatusdata?.status === "in_progress" || false; 39 + const lycanIndexingProgress = lycanIndexing 40 + ? lycanstatusdata?.progress 41 + : undefined; 42 + 37 43 const authed = status === "signedIn"; 38 44 39 45 const lycanReady = lycanExists && lycanIndexed && authed; 40 46 41 47 const { q }: { q: string } = useSearch({ from: "/search" }); 42 48 43 - //const lycanIndexed = useQuery(); 49 + // auto-refetch Lycan status until ready 50 + useEffect(() => { 51 + if (!lycanExists || !authed) return; 52 + if (lycanReady) return; 53 + 54 + const interval = setInterval(() => { 55 + refetch(); 56 + }, 3000); 57 + 58 + return () => clearInterval(interval); 59 + }, [lycanExists, authed, lycanReady, refetch]); 44 60 45 61 const maintext = !lycanExists 46 62 ? "Sorry we dont have search. But instead, you can load some of these types of content into Red Dwarf:" ··· 64 80 constructLycanRequestIndexQuery(opts) 65 81 ); 66 82 if ( 67 - response?.message !== "Import has already started" || 68 - response?.message !== "Import has already started" 83 + response?.message !== "Import has already started" && 84 + response?.message !== "Import has been scheduled" 69 85 ) { 70 86 renderSnack({ 71 87 title: "Registration failed!", ··· 76 92 title: "Succesfully sent registration request!", 77 93 description: "Please wait for the server to index your account", 78 94 }); 95 + refetch(); 79 96 } 80 97 } catch { 81 98 renderSnack({ ··· 127 144 </p> 128 145 129 146 {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 147 + !lycanIndexing ? ( 148 + <div className="mt-4 mx-auto"> 149 + <button 150 + onClick={() => 151 + index({ 152 + agent: agent || undefined, 153 + isAuthed: status === "signedIn", 154 + pdsUrl: identity?.pds, 155 + feedServiceDid: "did:web:" + lycandomain, 156 + }) 157 + } 158 + className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800 141 159 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> 160 + > 161 + Index my Account 162 + </button> 163 + </div> 164 + ) : ( 165 + <div className="mt-4 gap-2 flex flex-col"> 166 + <span>indexing...</span> 167 + <SliderPrimitive 168 + value={lycanIndexingProgress || 0} 169 + min={0} 170 + max={1} 171 + /> 172 + </div> 173 + ) 146 174 ) : ( 147 175 <></> 148 176 )}
+33
src/routes/settings.tsx
··· 325 325 </Slider.Root> 326 326 ); 327 327 }; 328 + 329 + 330 + interface SliderPProps { 331 + value: number; 332 + min?: number; 333 + max?: number; 334 + step?: number; 335 + } 336 + 337 + 338 + export const SliderPrimitive: React.FC<SliderPProps> = ({ 339 + value, 340 + min = 0, 341 + max = 100, 342 + step = 1, 343 + }) => { 344 + 345 + return ( 346 + <Slider.Root 347 + className="relative flex items-center w-full h-4" 348 + value={[value]} 349 + min={min} 350 + max={max} 351 + step={step} 352 + onValueChange={(v: number[]) => {}} 353 + > 354 + <Slider.Track className="relative flex-grow h-4 bg-gray-300 dark:bg-gray-700 rounded-full"> 355 + <Slider.Range className="absolute h-full bg-gray-500 dark:bg-gray-400 rounded-l-full rounded-r-none" /> 356 + </Slider.Track> 357 + <Slider.Thumb className=" hidden shadow-[0_0_0_8px_var(--color-white)] dark:shadow-[0_0_0_8px_var(--color-gray-950)] block w-[3px] h-12 bg-gray-500 dark:bg-gray-400 rounded-md focus:outline-none" /> 358 + </Slider.Root> 359 + ); 360 + };
+5 -1
src/utils/useQuery.ts
··· 806 806 [key: string]: unknown; 807 807 error?: "MethodNotImplemented"; 808 808 message?: "Method Not Implemented"; 809 - status?: "finished"; 809 + status?: "finished" | "in_progress"; 810 + position?: string, 811 + progress?: number, 812 + 810 813 }; 811 814 815 + //{"status":"in_progress","position":"2025-08-30T06:53:18Z","progress":0.0878319661441268} 812 816 type importtype = { 813 817 message?: "Import has already started" | "Import has been scheduled" 814 818 }