an independent Bluesky client using Constellation, PDS Queries, and other services reddwarf.app
frontend spa bluesky reddwarf microcosm client app
99
fork

Configure Feed

Select the types of activity you want to include in your feed.

faster initial loading

rimar1337 69f8676a 78b41734

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