tangled
alpha
login
or
join now
whey.party
/
red-dwarf
82
fork
atom
an independent Bluesky client using Constellation, PDS Queries, and other services
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
client
app
82
fork
atom
overview
issues
25
pulls
pipelines
faster initial loading
rimar1337
4 months ago
69f8676a
78b41734
+72
-66
3 changed files
expand all
collapse all
unified
split
src
providers
UnifiedAuthProvider.tsx
routes
index.tsx
utils
atoms.ts
+26
-23
src/providers/UnifiedAuthProvider.tsx
···
1
1
-
// src/providers/UnifiedAuthProvider.tsx
2
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
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
18
-
import { oauthClient } from "../utils/oauthClient"; // Adjust path if needed
17
17
+
import { quickAuthAtom } from "~/utils/atoms";
18
18
+
19
19
+
import { oauthClient } from "../utils/oauthClient";
19
20
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
25
-
agent: Agent | null; // The agent is typed as the base class `Agent`
25
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
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
48
+
const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom);
49
49
50
50
-
// Unified Initialization Logic
51
50
const initialize = useCallback(async () => {
52
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
57
-
const apiAgent = new Agent(oauthResult.session); // Standard Agent
55
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
62
-
return; // Success
60
60
+
setQuickAuth(apiAgent?.did || null);
61
61
+
return;
63
62
}
64
63
} catch (e) {
65
64
console.error("OAuth init failed, checking password session.", e);
65
65
+
if (!quickAuth) {
66
66
+
// quickAuth restoration. if last used method is oauth we immediately call for oauth redo
67
67
+
// (and set a persistent atom somewhere to not retry again if it failed)
68
68
+
}
66
69
}
67
70
68
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
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
81
-
setAgent(apiAgent); // This works because AtpAgent is a subclass of Agent
82
82
+
setAgent(apiAgent);
82
83
setAuthMethod("password");
83
84
setStatus("signedIn");
84
84
-
return; // Success
85
85
+
setQuickAuth(apiAgent?.did || null);
86
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
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
97
-
}, []);
98
98
+
// do we want to null it here?
99
99
+
setQuickAuth(null);
100
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
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
116
-
}, [initialize]);
120
120
+
}, [initialize, setQuickAuth]);
117
121
118
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
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
140
-
setAgent(apiAgent); // Store the AtpAgent instance in our state
142
142
+
setAgent(apiAgent);
141
143
setAuthMethod("password");
142
144
setStatus("signedIn");
145
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
153
+
setQuickAuth(null);
150
154
throw e;
151
155
}
152
156
};
···
161
165
}
162
166
}, [status]);
163
167
164
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
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
189
+
setQuickAuth(null);
187
190
}
188
188
-
}, [status, authMethod, agent, oauthSession]);
191
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
4
-
import { useEffect, useLayoutEffect } from "react";
4
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
10
-
agentAtom,
11
11
-
authedAtom,
12
10
feedScrollPositionsAtom,
13
11
isAtTopAtom,
12
12
+
quickAuthAtom,
14
13
selectedFeedUriAtom,
15
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
110
-
useEffect(() => {
111
111
-
if (agent?.did) {
112
112
-
store.set(authedAtom, true);
113
113
-
} else {
114
114
-
store.set(authedAtom, false);
115
115
-
}
116
116
-
}, [status, agent, authed]);
117
117
-
useEffect(() => {
118
118
-
if (agent) {
119
119
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
120
120
-
// @ts-ignore is it just me or is the type really weird here it should be Agent not AtpAgent
121
121
-
store.set(agentAtom, agent);
122
122
-
} else {
123
123
-
store.set(agentAtom, null);
124
124
-
}
125
125
-
}, [status, agent, authed]);
108
108
+
// i dont remember why this is even here
109
109
+
// useEffect(() => {
110
110
+
// if (agent?.did) {
111
111
+
// store.set(authedAtom, true);
112
112
+
// } else {
113
113
+
// store.set(authedAtom, false);
114
114
+
// }
115
115
+
// }, [status, agent, authed]);
116
116
+
// useEffect(() => {
117
117
+
// if (agent) {
118
118
+
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
119
119
+
// // @ts-ignore is it just me or is the type really weird here it should be Agent not AtpAgent
120
120
+
// store.set(agentAtom, agent);
121
121
+
// } else {
122
122
+
// store.set(agentAtom, null);
123
123
+
// }
124
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
165
-
const identityresultmaybe = useQueryIdentity(agent?.did);
164
164
+
const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom);
165
165
+
const isAuthRestoring = quickAuth ? status === "loading" : false;
166
166
+
167
167
+
const identityresultmaybe = useQueryIdentity(!isAuthRestoring ? agent?.did : undefined);
166
168
const identity = identityresultmaybe?.data;
167
169
168
170
const prefsresultmaybe = useQueryPreferences({
169
169
-
agent: agent ?? undefined,
170
170
-
pdsUrl: identity?.pds,
171
171
+
agent: !isAuthRestoring ? (agent ?? undefined) : undefined,
172
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
181
-
const [persistentSelectedFeed, setPersistentSelectedFeed] =
182
182
-
useAtom(selectedFeedUriAtom); // React.useState<string | null>(null);
183
183
-
const [unauthedSelectedFeed, setUnauthedSelectedFeed] = React.useState(
184
184
-
persistentSelectedFeed
185
185
-
); // React.useState<string | null>(null);
183
183
+
const [persistentSelectedFeed, setPersistentSelectedFeed] = useAtom(selectedFeedUriAtom);
184
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
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
313
-
}, [selectedFeed]);
313
313
+
}, [selectedFeed, isAuthRestoring]);
314
314
315
315
useLayoutEffect(() => {
316
316
-
if (!selectedFeed) return;
316
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
331
-
}, [selectedFeed, setScrollPositions]);
331
331
+
}, [isAuthRestoring, selectedFeed, setScrollPositions]);
332
332
333
333
-
const feedGengetrecordquery = useQueryArbitrary(selectedFeed ?? undefined);
334
334
-
const feedServiceDid = (feedGengetrecordquery?.data?.value as any)?.did;
333
333
+
const feedGengetrecordquery = useQueryArbitrary(!isAuthRestoring ? selectedFeed ?? undefined : undefined);
334
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
350
-
const isReadyForAuthedFeed =
351
351
-
authed && agent && identity?.pds && feedServiceDid;
352
352
-
const isReadyForUnauthedFeed = !authed && selectedFeed;
350
350
+
const isReadyForAuthedFeed = !isAuthRestoring && authed && agent && identity?.pds && feedServiceDid;
351
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
361
-
{savedFeeds.length > 0 ? (
360
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
413
-
{authed && (!identity?.pds || !feedServiceDid) && (
412
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
419
-
{isReadyForAuthedFeed || isReadyForUnauthedFeed ? (
418
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
428
-
Select a feed to get started.
427
427
+
Loading.......
429
428
</div>
430
429
)}
431
430
{/* {false && restoringScrollPosition && (
+7
-3
src/utils/atoms.ts
···
1
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
6
+
7
7
+
export const quickAuthAtom = atomWithStorage<string | null>(
8
8
+
"quickAuth",
9
9
+
null
10
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
55
-
export const agentAtom = atom<Agent | null>(null);
56
56
-
export const authedAtom = atom<boolean>(false);
59
59
+
//export const agentAtom = atom<Agent | null>(null);
60
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);