forked from
pds.ls/pdsls
atproto explorer
1import { Client, CredentialManager } from "@atcute/client";
2import { Did } from "@atcute/lexicons";
3import { deleteStoredSession, getSession, OAuthUserAgent } from "@atcute/oauth-browser-client";
4import { A } from "@solidjs/router";
5import { createSignal, For, onMount, Show } from "solid-js";
6import { createStore } from "solid-js/store";
7import { resolveDidDoc } from "../utils/api.js";
8import { agent, Login, retrieveSession, setAgent } from "./login.jsx";
9import { Modal } from "./modal.jsx";
10
11const AccountManager = () => {
12 const [openManager, setOpenManager] = createSignal(false);
13 const [sessions, setSessions] = createStore<Record<string, string | undefined>>();
14 const [avatar, setAvatar] = createSignal<string>();
15
16 onMount(async () => {
17 await retrieveSession();
18
19 const storedSessions = localStorage.getItem("atcute-oauth:sessions");
20 if (storedSessions) {
21 const sessionDids = Object.keys(JSON.parse(storedSessions)) as Did[];
22 sessionDids.forEach((did) => setSessions(did, ""));
23 sessionDids.forEach(async (did) => {
24 const doc = await resolveDidDoc(did);
25 doc.alsoKnownAs?.forEach((alias) => {
26 if (alias.startsWith("at://")) {
27 setSessions(did, alias.replace("at://", ""));
28 return;
29 }
30 });
31 });
32 }
33
34 const repo = localStorage.getItem("lastSignedIn");
35 if (repo) setAvatar(await getAvatar(repo as Did));
36 });
37
38 const resumeSession = async (did: Did) => {
39 localStorage.setItem("lastSignedIn", did);
40 retrieveSession();
41 setAvatar(await getAvatar(did));
42 };
43
44 const removeSession = async (did: Did) => {
45 const currentSession = agent()?.sub;
46 try {
47 const session = await getSession(did, { allowStale: true });
48 const agent = new OAuthUserAgent(session);
49 await agent.signOut();
50 } catch {
51 deleteStoredSession(did);
52 }
53 setSessions(did, undefined);
54 if (currentSession === did) setAgent(undefined);
55 };
56
57 const getAvatar = async (did: Did) => {
58 const rpc = new Client({
59 handler: new CredentialManager({ service: "https://public.api.bsky.app" }),
60 });
61 const res = await rpc.get("app.bsky.actor.getProfile", { params: { actor: did } });
62 if (res.ok) {
63 return res.data.avatar;
64 }
65 return undefined;
66 };
67
68 return (
69 <>
70 <Modal open={openManager()} onClose={() => setOpenManager(false)}>
71 <div class="dark:bg-dark-300 dark:shadow-dark-800 absolute top-16 left-[50%] w-[22rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0">
72 <div class="mb-2 flex items-center gap-1 font-semibold">
73 <span class="iconify lucide--user-round"></span>
74 <span>Manage accounts</span>
75 </div>
76 <div class="mb-3 max-h-[20rem] overflow-y-auto md:max-h-[25rem]">
77 <For each={Object.keys(sessions)}>
78 {(did) => (
79 <div class="flex items-center">
80 <button
81 class="flex w-full items-center justify-between gap-1 truncate rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
82 onclick={() => resumeSession(did as Did)}
83 >
84 <span class="truncate">{sessions[did]?.length ? sessions[did] : did}</span>
85 <Show when={did === agent()?.sub}>
86 <span class="iconify lucide--check shrink-0"></span>
87 </Show>
88 </button>
89 <A
90 href={`/at://${did}`}
91 onClick={() => setOpenManager(false)}
92 class="flex items-center rounded-lg p-2 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
93 >
94 <span class="iconify lucide--book-user"></span>
95 </A>
96 <button
97 onclick={() => removeSession(did as Did)}
98 class="flex items-center rounded-lg p-2 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
99 >
100 <span class="iconify lucide--user-round-x"></span>
101 </button>
102 </div>
103 )}
104 </For>
105 </div>
106 <Login />
107 </div>
108 </Modal>
109 <button
110 onclick={() => setOpenManager(true)}
111 class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
112 >
113 {agent() && avatar() ?
114 <img src={avatar()} class="size-5 rounded-full" />
115 : <span class="iconify lucide--circle-user-round text-xl"></span>}
116 </button>
117 </>
118 );
119};
120
121export { AccountManager };