atproto explorer
at main 121 lines 4.9 kB view raw
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 };