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

faster initial loading

rimar1337 69f8676a 78b41734

Changed files
+72 -66
src
providers
routes
utils
+26 -23
src/providers/UnifiedAuthProvider.tsx
··· 1 - // src/providers/UnifiedAuthProvider.tsx 2 - // Import both Agent and the (soon to be deprecated) AtpAgent 3 import { Agent, AtpAgent, type AtpSessionData } from "@atproto/api"; 4 import { 5 type OAuthSession, ··· 7 TokenRefreshError, 8 TokenRevokedError, 9 } from "@atproto/oauth-client-browser"; 10 import React, { 11 createContext, 12 use, ··· 15 useState, 16 } from "react"; 17 18 - import { oauthClient } from "../utils/oauthClient"; // Adjust path if needed 19 20 - // Define the unified status and authentication method 21 type AuthStatus = "loading" | "signedIn" | "signedOut"; 22 type AuthMethod = "password" | "oauth" | null; 23 24 interface AuthContextValue { 25 - agent: Agent | null; // The agent is typed as the base class `Agent` 26 status: AuthStatus; 27 authMethod: AuthMethod; 28 loginWithPassword: ( ··· 41 }: { 42 children: React.ReactNode; 43 }) => { 44 - // The state is typed as the base class `Agent`, which accepts both `Agent` and `AtpAgent` instances. 45 const [agent, setAgent] = useState<Agent | null>(null); 46 const [status, setStatus] = useState<AuthStatus>("loading"); 47 const [authMethod, setAuthMethod] = useState<AuthMethod>(null); 48 const [oauthSession, setOauthSession] = useState<OAuthSession | null>(null); 49 50 - // Unified Initialization Logic 51 const initialize = useCallback(async () => { 52 - // --- 1. Try OAuth initialization first --- 53 try { 54 const oauthResult = await oauthClient.init(); 55 if (oauthResult) { 56 // /*mass comment*/ console.log("OAuth session restored."); 57 - const apiAgent = new Agent(oauthResult.session); // Standard Agent 58 setAgent(apiAgent); 59 setOauthSession(oauthResult.session); 60 setAuthMethod("oauth"); 61 setStatus("signedIn"); 62 - return; // Success 63 } 64 } catch (e) { 65 console.error("OAuth init failed, checking password session.", e); 66 } 67 68 - // --- 2. If no OAuth, try password-based session using AtpAgent --- 69 try { 70 const service = localStorage.getItem("service"); 71 const sessionString = localStorage.getItem("sess"); 72 73 if (service && sessionString) { 74 // /*mass comment*/ console.log("Resuming password-based session using AtpAgent..."); 75 - // Use the original, working AtpAgent logic 76 const apiAgent = new AtpAgent({ service }); 77 const session: AtpSessionData = JSON.parse(sessionString); 78 await apiAgent.resumeSession(session); 79 80 // /*mass comment*/ console.log("Password-based session resumed successfully."); 81 - setAgent(apiAgent); // This works because AtpAgent is a subclass of Agent 82 setAuthMethod("password"); 83 setStatus("signedIn"); 84 - return; // Success 85 } 86 } catch (e) { 87 console.error("Failed to resume password-based session.", e); ··· 89 localStorage.removeItem("service"); 90 } 91 92 - // --- 3. If neither worked, user is signed out --- 93 // /*mass comment*/ console.log("No active session found."); 94 setStatus("signedOut"); 95 setAgent(null); 96 setAuthMethod(null); 97 - }, []); 98 99 useEffect(() => { 100 const handleOAuthSessionDeleted = ( ··· 105 setOauthSession(null); 106 setAuthMethod(null); 107 setStatus("signedOut"); 108 }; 109 110 oauthClient.addEventListener("deleted", handleOAuthSessionDeleted as EventListener); ··· 113 return () => { 114 oauthClient.removeEventListener("deleted", handleOAuthSessionDeleted as EventListener); 115 }; 116 - }, [initialize]); 117 118 - // --- Login Methods --- 119 const loginWithPassword = async ( 120 user: string, 121 password: string, ··· 125 setStatus("loading"); 126 try { 127 let sessionData: AtpSessionData | undefined; 128 - // Use the AtpAgent for its simple login and session persistence 129 const apiAgent = new AtpAgent({ 130 service, 131 persistSession: (_evt, sess) => { ··· 137 if (sessionData) { 138 localStorage.setItem("service", service); 139 localStorage.setItem("sess", JSON.stringify(sessionData)); 140 - setAgent(apiAgent); // Store the AtpAgent instance in our state 141 setAuthMethod("password"); 142 setStatus("signedIn"); 143 // /*mass comment*/ console.log("Successfully logged in with password."); 144 } else { 145 throw new Error("Session data not persisted after login."); ··· 147 } catch (e) { 148 console.error("Password login failed:", e); 149 setStatus("signedOut"); 150 throw e; 151 } 152 }; ··· 161 } 162 }, [status]); 163 164 - // --- Unified Logout --- 165 const logout = useCallback(async () => { 166 if (status !== "signedIn" || !agent) return; 167 setStatus("loading"); ··· 173 } else if (authMethod === "password") { 174 localStorage.removeItem("service"); 175 localStorage.removeItem("sess"); 176 - // AtpAgent has its own logout methods 177 await (agent as AtpAgent).com.atproto.server.deleteSession(); 178 // /*mass comment*/ console.log("Password-based session deleted."); 179 } ··· 184 setAuthMethod(null); 185 setOauthSession(null); 186 setStatus("signedOut"); 187 } 188 - }, [status, authMethod, agent, oauthSession]); 189 190 return ( 191 <AuthContext
··· 1 import { Agent, AtpAgent, type AtpSessionData } from "@atproto/api"; 2 import { 3 type OAuthSession, ··· 5 TokenRefreshError, 6 TokenRevokedError, 7 } from "@atproto/oauth-client-browser"; 8 + import { useAtom } from "jotai"; 9 import React, { 10 createContext, 11 use, ··· 14 useState, 15 } from "react"; 16 17 + import { quickAuthAtom } from "~/utils/atoms"; 18 + 19 + import { oauthClient } from "../utils/oauthClient"; 20 21 type AuthStatus = "loading" | "signedIn" | "signedOut"; 22 type AuthMethod = "password" | "oauth" | null; 23 24 interface AuthContextValue { 25 + agent: Agent | null; 26 status: AuthStatus; 27 authMethod: AuthMethod; 28 loginWithPassword: ( ··· 41 }: { 42 children: React.ReactNode; 43 }) => { 44 const [agent, setAgent] = useState<Agent | null>(null); 45 const [status, setStatus] = useState<AuthStatus>("loading"); 46 const [authMethod, setAuthMethod] = useState<AuthMethod>(null); 47 const [oauthSession, setOauthSession] = useState<OAuthSession | null>(null); 48 + const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom); 49 50 const initialize = useCallback(async () => { 51 try { 52 const oauthResult = await oauthClient.init(); 53 if (oauthResult) { 54 // /*mass comment*/ console.log("OAuth session restored."); 55 + const apiAgent = new Agent(oauthResult.session); 56 setAgent(apiAgent); 57 setOauthSession(oauthResult.session); 58 setAuthMethod("oauth"); 59 setStatus("signedIn"); 60 + setQuickAuth(apiAgent?.did || null); 61 + return; 62 } 63 } catch (e) { 64 console.error("OAuth init failed, checking password session.", e); 65 + if (!quickAuth) { 66 + // quickAuth restoration. if last used method is oauth we immediately call for oauth redo 67 + // (and set a persistent atom somewhere to not retry again if it failed) 68 + } 69 } 70 71 try { 72 const service = localStorage.getItem("service"); 73 const sessionString = localStorage.getItem("sess"); 74 75 if (service && sessionString) { 76 // /*mass comment*/ console.log("Resuming password-based session using AtpAgent..."); 77 const apiAgent = new AtpAgent({ service }); 78 const session: AtpSessionData = JSON.parse(sessionString); 79 await apiAgent.resumeSession(session); 80 81 // /*mass comment*/ console.log("Password-based session resumed successfully."); 82 + setAgent(apiAgent); 83 setAuthMethod("password"); 84 setStatus("signedIn"); 85 + setQuickAuth(apiAgent?.did || null); 86 + return; 87 } 88 } catch (e) { 89 console.error("Failed to resume password-based session.", e); ··· 91 localStorage.removeItem("service"); 92 } 93 94 // /*mass comment*/ console.log("No active session found."); 95 setStatus("signedOut"); 96 setAgent(null); 97 setAuthMethod(null); 98 + // do we want to null it here? 99 + setQuickAuth(null); 100 + }, [quickAuth, setQuickAuth]); 101 102 useEffect(() => { 103 const handleOAuthSessionDeleted = ( ··· 108 setOauthSession(null); 109 setAuthMethod(null); 110 setStatus("signedOut"); 111 + setQuickAuth(null); 112 }; 113 114 oauthClient.addEventListener("deleted", handleOAuthSessionDeleted as EventListener); ··· 117 return () => { 118 oauthClient.removeEventListener("deleted", handleOAuthSessionDeleted as EventListener); 119 }; 120 + }, [initialize, setQuickAuth]); 121 122 const loginWithPassword = async ( 123 user: string, 124 password: string, ··· 128 setStatus("loading"); 129 try { 130 let sessionData: AtpSessionData | undefined; 131 const apiAgent = new AtpAgent({ 132 service, 133 persistSession: (_evt, sess) => { ··· 139 if (sessionData) { 140 localStorage.setItem("service", service); 141 localStorage.setItem("sess", JSON.stringify(sessionData)); 142 + setAgent(apiAgent); 143 setAuthMethod("password"); 144 setStatus("signedIn"); 145 + setQuickAuth(apiAgent?.did || null); 146 // /*mass comment*/ console.log("Successfully logged in with password."); 147 } else { 148 throw new Error("Session data not persisted after login."); ··· 150 } catch (e) { 151 console.error("Password login failed:", e); 152 setStatus("signedOut"); 153 + setQuickAuth(null); 154 throw e; 155 } 156 }; ··· 165 } 166 }, [status]); 167 168 const logout = useCallback(async () => { 169 if (status !== "signedIn" || !agent) return; 170 setStatus("loading"); ··· 176 } else if (authMethod === "password") { 177 localStorage.removeItem("service"); 178 localStorage.removeItem("sess"); 179 await (agent as AtpAgent).com.atproto.server.deleteSession(); 180 // /*mass comment*/ console.log("Password-based session deleted."); 181 } ··· 186 setAuthMethod(null); 187 setOauthSession(null); 188 setStatus("signedOut"); 189 + setQuickAuth(null); 190 } 191 + }, [status, agent, authMethod, oauthSession, setQuickAuth]); 192 193 return ( 194 <AuthContext
+39 -40
src/routes/index.tsx
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 import { useAtom } from "jotai"; 3 import * as React from "react"; 4 - import { useEffect, useLayoutEffect } from "react"; 5 6 import { Header } from "~/components/Header"; 7 import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed"; 8 import { useAuth } from "~/providers/UnifiedAuthProvider"; 9 import { 10 - agentAtom, 11 - authedAtom, 12 feedScrollPositionsAtom, 13 isAtTopAtom, 14 selectedFeedUriAtom, 15 - store, 16 } from "~/utils/atoms"; 17 //import { usePersistentStore } from "~/providers/PersistentStoreProvider"; 18 import { ··· 107 } = useAuth(); 108 const authed = !!agent?.did; 109 110 - useEffect(() => { 111 - if (agent?.did) { 112 - store.set(authedAtom, true); 113 - } else { 114 - store.set(authedAtom, false); 115 - } 116 - }, [status, agent, authed]); 117 - useEffect(() => { 118 - if (agent) { 119 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment 120 - // @ts-ignore is it just me or is the type really weird here it should be Agent not AtpAgent 121 - store.set(agentAtom, agent); 122 - } else { 123 - store.set(agentAtom, null); 124 - } 125 - }, [status, agent, authed]); 126 127 //const { get, set } = usePersistentStore(); 128 // const [feed, setFeed] = React.useState<any[]>([]); ··· 162 163 // const savedFeeds = savedFeedsPref?.items || []; 164 165 - const identityresultmaybe = useQueryIdentity(agent?.did); 166 const identity = identityresultmaybe?.data; 167 168 const prefsresultmaybe = useQueryPreferences({ 169 - agent: agent ?? undefined, 170 - pdsUrl: identity?.pds, 171 }); 172 const prefs = prefsresultmaybe?.data; 173 ··· 178 return savedFeedsPref?.items || []; 179 }, [prefs]); 180 181 - const [persistentSelectedFeed, setPersistentSelectedFeed] = 182 - useAtom(selectedFeedUriAtom); // React.useState<string | null>(null); 183 - const [unauthedSelectedFeed, setUnauthedSelectedFeed] = React.useState( 184 - persistentSelectedFeed 185 - ); // React.useState<string | null>(null); 186 const selectedFeed = agent?.did 187 ? persistentSelectedFeed 188 : unauthedSelectedFeed; ··· 306 }, [scrollPositions]); 307 308 useLayoutEffect(() => { 309 const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0; 310 311 window.scrollTo({ top: savedPosition, behavior: "instant" }); 312 // eslint-disable-next-line react-hooks/exhaustive-deps 313 - }, [selectedFeed]); 314 315 useLayoutEffect(() => { 316 - if (!selectedFeed) return; 317 318 const handleScroll = () => { 319 scrollPositionsRef.current = { ··· 328 329 setScrollPositions(scrollPositionsRef.current); 330 }; 331 - }, [selectedFeed, setScrollPositions]); 332 333 - const feedGengetrecordquery = useQueryArbitrary(selectedFeed ?? undefined); 334 - const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did; 335 336 // const { 337 // data: feedData, ··· 347 348 // const feed = feedData?.feed || []; 349 350 - const isReadyForAuthedFeed = 351 - authed && agent && identity?.pds && feedServiceDid; 352 - const isReadyForUnauthedFeed = !authed && selectedFeed; 353 354 355 const [isAtTop] = useAtom(isAtTopAtom); ··· 358 <div 359 className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"}`} 360 > 361 - {savedFeeds.length > 0 ? ( 362 <div className={`flex items-center px-4 py-2 h-[52px] sticky top-0 bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] ${!isAtTop && "shadow-sm"} sm:shadow-none sm:bg-white sm:dark:bg-gray-950 z-10 border-0 sm:border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin`}> 363 {savedFeeds.map((item: any, idx: number) => { 364 const label = item.value.split("/").pop() || item.value; ··· 410 /> 411 ))} */} 412 413 - {authed && (!identity?.pds || !feedServiceDid) && ( 414 <div className="p-4 text-center text-gray-500"> 415 Preparing your feed... 416 </div> 417 )} 418 419 - {isReadyForAuthedFeed || isReadyForUnauthedFeed ? ( 420 <InfiniteCustomFeed 421 key={selectedFeed!} 422 feedUri={selectedFeed!} ··· 425 /> 426 ) : ( 427 <div className="p-4 text-center text-gray-500"> 428 - Select a feed to get started. 429 </div> 430 )} 431 {/* {false && restoringScrollPosition && (
··· 1 import { createFileRoute } from "@tanstack/react-router"; 2 import { useAtom } from "jotai"; 3 import * as React from "react"; 4 + import { useLayoutEffect, useState } from "react"; 5 6 import { Header } from "~/components/Header"; 7 import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed"; 8 import { useAuth } from "~/providers/UnifiedAuthProvider"; 9 import { 10 feedScrollPositionsAtom, 11 isAtTopAtom, 12 + quickAuthAtom, 13 selectedFeedUriAtom, 14 } from "~/utils/atoms"; 15 //import { usePersistentStore } from "~/providers/PersistentStoreProvider"; 16 import { ··· 105 } = useAuth(); 106 const authed = !!agent?.did; 107 108 + // i dont remember why this is even here 109 + // useEffect(() => { 110 + // if (agent?.did) { 111 + // store.set(authedAtom, true); 112 + // } else { 113 + // store.set(authedAtom, false); 114 + // } 115 + // }, [status, agent, authed]); 116 + // useEffect(() => { 117 + // if (agent) { 118 + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment 119 + // // @ts-ignore is it just me or is the type really weird here it should be Agent not AtpAgent 120 + // store.set(agentAtom, agent); 121 + // } else { 122 + // store.set(agentAtom, null); 123 + // } 124 + // }, [status, agent, authed]); 125 126 //const { get, set } = usePersistentStore(); 127 // const [feed, setFeed] = React.useState<any[]>([]); ··· 161 162 // const savedFeeds = savedFeedsPref?.items || []; 163 164 + const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom); 165 + const isAuthRestoring = quickAuth ? status === "loading" : false; 166 + 167 + const identityresultmaybe = useQueryIdentity(!isAuthRestoring ? agent?.did : undefined); 168 const identity = identityresultmaybe?.data; 169 170 const prefsresultmaybe = useQueryPreferences({ 171 + agent: !isAuthRestoring ? (agent ?? undefined) : undefined, 172 + pdsUrl: !isAuthRestoring ? (identity?.pds) : undefined, 173 }); 174 const prefs = prefsresultmaybe?.data; 175 ··· 180 return savedFeedsPref?.items || []; 181 }, [prefs]); 182 183 + const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom); 184 + const [unauthedSelectedFeed, setUnauthedSelectedFeed] = useState(persistentSelectedFeed); 185 const selectedFeed = agent?.did 186 ? persistentSelectedFeed 187 : unauthedSelectedFeed; ··· 305 }, [scrollPositions]); 306 307 useLayoutEffect(() => { 308 + if (isAuthRestoring) return; 309 const savedPosition = scrollPositions[selectedFeed ?? "null"] ?? 0; 310 311 window.scrollTo({ top: savedPosition, behavior: "instant" }); 312 // eslint-disable-next-line react-hooks/exhaustive-deps 313 + }, [selectedFeed, isAuthRestoring]); 314 315 useLayoutEffect(() => { 316 + if (!selectedFeed || isAuthRestoring) return; 317 318 const handleScroll = () => { 319 scrollPositionsRef.current = { ··· 328 329 setScrollPositions(scrollPositionsRef.current); 330 }; 331 + }, [isAuthRestoring, selectedFeed, setScrollPositions]); 332 333 + const feedGengetrecordquery = useQueryArbitrary(!isAuthRestoring ? selectedFeed ?? undefined : undefined); 334 + const feedServiceDid = !isAuthRestoring ? (feedGengetrecordquery?.data?.value as any)?.did as string | undefined : undefined; 335 336 // const { 337 // data: feedData, ··· 347 348 // const feed = feedData?.feed || []; 349 350 + const isReadyForAuthedFeed = !isAuthRestoring && authed && agent && identity?.pds && feedServiceDid; 351 + const isReadyForUnauthedFeed = !isAuthRestoring && !authed && selectedFeed; 352 353 354 const [isAtTop] = useAtom(isAtTopAtom); ··· 357 <div 358 className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"}`} 359 > 360 + {!isAuthRestoring && savedFeeds.length > 0 ? ( 361 <div className={`flex items-center px-4 py-2 h-[52px] sticky top-0 bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] ${!isAtTop && "shadow-sm"} sm:shadow-none sm:bg-white sm:dark:bg-gray-950 z-10 border-0 sm:border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin`}> 362 {savedFeeds.map((item: any, idx: number) => { 363 const label = item.value.split("/").pop() || item.value; ··· 409 /> 410 ))} */} 411 412 + {isAuthRestoring || authed && (!identity?.pds || !feedServiceDid) && ( 413 <div className="p-4 text-center text-gray-500"> 414 Preparing your feed... 415 </div> 416 )} 417 418 + {!isAuthRestoring && (isReadyForAuthedFeed || isReadyForUnauthedFeed) ? ( 419 <InfiniteCustomFeed 420 key={selectedFeed!} 421 feedUri={selectedFeed!} ··· 424 /> 425 ) : ( 426 <div className="p-4 text-center text-gray-500"> 427 + Loading....... 428 </div> 429 )} 430 {/* {false && restoringScrollPosition && (
+7 -3
src/utils/atoms.ts
··· 1 - import type Agent from "@atproto/api"; 2 import { atom, createStore, useAtomValue } from "jotai"; 3 import { atomWithStorage } from "jotai/utils"; 4 import { useEffect } from "react"; 5 6 export const store = createStore(); 7 8 export const selectedFeedUriAtom = atomWithStorage<string | null>( 9 "selectedFeedUri", ··· 52 | { kind: "quote"; subject: string }; 53 export const composerAtom = atom<ComposerState>({ kind: "closed" }); 54 55 - export const agentAtom = atom<Agent | null>(null); 56 - export const authedAtom = atom<boolean>(false); 57 58 export function useAtomCssVar(atom: typeof hueAtom, cssVar: string) { 59 const value = useAtomValue(atom);
··· 1 import { atom, createStore, useAtomValue } from "jotai"; 2 import { atomWithStorage } from "jotai/utils"; 3 import { useEffect } from "react"; 4 5 export const store = createStore(); 6 + 7 + export const quickAuthAtom = atomWithStorage<string | null>( 8 + "quickAuth", 9 + null 10 + ); 11 12 export const selectedFeedUriAtom = atomWithStorage<string | null>( 13 "selectedFeedUri", ··· 56 | { kind: "quote"; subject: string }; 57 export const composerAtom = atom<ComposerState>({ kind: "closed" }); 58 59 + //export const agentAtom = atom<Agent | null>(null); 60 + //export const authedAtom = atom<boolean>(false); 61 62 export function useAtomCssVar(atom: typeof hueAtom, cssVar: string) { 63 const value = useAtomValue(atom);