atproto explorer pdsls.dev
atproto tool
1import { Client, CredentialManager } from "@atcute/client"; 2import { Did } from "@atcute/lexicons"; 3import { 4 createAuthorizationUrl, 5 deleteStoredSession, 6 getSession, 7 OAuthUserAgent, 8} from "@atcute/oauth-browser-client"; 9import { A } from "@solidjs/router"; 10import { createSignal, For, onMount, Show } from "solid-js"; 11import { createStore, produce } from "solid-js/store"; 12import { resolveDidDoc } from "../utils/api.js"; 13import { agent, Login, retrieveSession, Sessions, setAgent } from "./login.jsx"; 14import { Modal } from "./modal.jsx"; 15 16export const [sessions, setSessions] = createStore<Sessions>(); 17 18export const AccountManager = () => { 19 const [openManager, setOpenManager] = createSignal(false); 20 const [avatars, setAvatars] = createStore<Record<Did, string>>(); 21 22 onMount(async () => { 23 try { 24 await retrieveSession(); 25 } catch {} 26 27 const localSessions = localStorage.getItem("sessions"); 28 if (localSessions) { 29 const storedSessions: Sessions = JSON.parse(localSessions); 30 const sessionDids = Object.keys(storedSessions) as Did[]; 31 sessionDids.forEach(async (did) => { 32 const doc = await resolveDidDoc(did); 33 doc.alsoKnownAs?.forEach((alias) => { 34 if (alias.startsWith("at://")) { 35 setSessions(did, { 36 signedIn: storedSessions[did].signedIn, 37 handle: alias.replace("at://", ""), 38 }); 39 return; 40 } 41 }); 42 }); 43 sessionDids.forEach(async (did) => { 44 const avatar = await getAvatar(did); 45 if (avatar) setAvatars(did, avatar); 46 }); 47 } 48 }); 49 50 const resumeSession = async (did: Did) => { 51 try { 52 localStorage.setItem("lastSignedIn", did); 53 await retrieveSession(); 54 } catch { 55 const authUrl = await createAuthorizationUrl({ 56 scope: import.meta.env.VITE_OAUTH_SCOPE, 57 target: { type: "account", identifier: did }, 58 }); 59 60 await new Promise((resolve) => setTimeout(resolve, 250)); 61 62 location.assign(authUrl); 63 } 64 }; 65 66 const removeSession = async (did: Did) => { 67 const currentSession = agent()?.sub; 68 try { 69 const session = await getSession(did, { allowStale: true }); 70 const agent = new OAuthUserAgent(session); 71 await agent.signOut(); 72 } catch { 73 deleteStoredSession(did); 74 } 75 setSessions( 76 produce((accs) => { 77 delete accs[did]; 78 }), 79 ); 80 localStorage.setItem("sessions", JSON.stringify(sessions)); 81 if (currentSession === did) setAgent(undefined); 82 }; 83 84 const getAvatar = async (did: Did) => { 85 const rpc = new Client({ 86 handler: new CredentialManager({ service: "https://public.api.bsky.app" }), 87 }); 88 const res = await rpc.get("app.bsky.actor.getProfile", { params: { actor: did } }); 89 if (res.ok) { 90 return res.data.avatar; 91 } 92 return undefined; 93 }; 94 95 return ( 96 <> 97 <Modal open={openManager()} onClose={() => setOpenManager(false)}> 98 <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-18 left-[50%] w-88 -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"> 99 <div class="mb-2 px-1 font-semibold"> 100 <span>Manage accounts</span> 101 </div> 102 <div class="mb-3 max-h-80 overflow-y-auto md:max-h-100"> 103 <For each={Object.keys(sessions)}> 104 {(did) => ( 105 <div class="flex items-center"> 106 <button 107 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" 108 onclick={() => resumeSession(did as Did)} 109 > 110 <span class="flex items-center gap-2 truncate"> 111 <Show when={avatars[did as Did]}> 112 <img 113 src={avatars[did as Did].replace("img/avatar/", "img/avatar_thumbnail/")} 114 class="size-6 rounded-full" 115 /> 116 </Show> 117 <span class="truncate"> 118 {sessions[did]?.handle ? sessions[did].handle : did} 119 </span> 120 </span> 121 <Show when={did === agent()?.sub && sessions[did].signedIn}> 122 <span class="iconify lucide--check shrink-0 text-green-500 dark:text-green-400"></span> 123 </Show> 124 <Show when={!sessions[did].signedIn}> 125 <span class="iconify lucide--circle-alert shrink-0 text-red-500 dark:text-red-400"></span> 126 </Show> 127 </button> 128 <A 129 href={`/at://${did}`} 130 onClick={() => setOpenManager(false)} 131 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" 132 > 133 <span class="iconify lucide--user-round"></span> 134 </A> 135 <button 136 onclick={() => removeSession(did as Did)} 137 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" 138 > 139 <span class="iconify lucide--x"></span> 140 </button> 141 </div> 142 )} 143 </For> 144 </div> 145 <Login /> 146 </div> 147 </Modal> 148 <button 149 onclick={() => setOpenManager(true)} 150 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" 151 > 152 {agent() && avatars[agent()!.sub] ? 153 <img 154 src={avatars[agent()!.sub].replace("img/avatar/", "img/avatar_thumbnail/")} 155 class="size-5 rounded-full" 156 /> 157 : <span class="iconify lucide--circle-user-round text-xl"></span>} 158 </button> 159 </> 160 ); 161};