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