forked from
pds.ls/pdsls
this repo has no description
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-800 dark:shadow-dark-800 absolute top-12 left-[50%] w-[22rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-200 p-4 text-neutral-900 shadow-md transition-opacity duration-300 dark:border-neutral-700 dark:text-neutral-200 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 w-full items-center justify-between gap-x-2 rounded-lg hover:bg-neutral-100 active:bg-neutral-100 dark:hover:bg-neutral-600 dark:active:bg-neutral-600">
80 <button
81 class="flex basis-full items-center justify-between gap-1 truncate p-1"
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 <div class="flex items-center gap-1">
90 <A
91 href={`/at://${did}`}
92 onClick={() => setOpenManager(false)}
93 class="flex items-center p-1"
94 >
95 <span class="iconify lucide--book-user"></span>
96 </A>
97 <button
98 onclick={() => removeSession(did as Did)}
99 class="flex items-center p-1 hover:text-red-500 hover:dark:text-red-400"
100 >
101 <span class="iconify lucide--user-round-x"></span>
102 </button>
103 </div>
104 </div>
105 )}
106 </For>
107 </div>
108 <Login />
109 </div>
110 </Modal>
111 <button
112 onclick={() => setOpenManager(true)}
113 class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-700"
114 >
115 {agent() && avatar() ?
116 <img src={avatar()} class="dark:shadow-dark-800 size-5 rounded-full shadow-sm" />
117 : <span class="iconify lucide--circle-user-round text-xl"></span>}
118 </button>
119 </>
120 );
121};
122
123export { AccountManager };