atproto explorer pdsls.dev
atproto tool
at v1.1.4 6.2 kB view raw
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 const alias = doc.alsoKnownAs?.find((alias) => alias.startsWith("at://")); 34 if (alias) { 35 setSessions(did, { 36 signedIn: storedSessions[did].signedIn, 37 handle: alias.replace("at://", ""), 38 }); 39 } 40 }); 41 sessionDids.forEach(async (did) => { 42 const avatar = await getAvatar(did); 43 if (avatar) setAvatars(did, avatar); 44 }); 45 } 46 }); 47 48 const resumeSession = async (did: Did) => { 49 try { 50 localStorage.setItem("lastSignedIn", did); 51 await retrieveSession(); 52 } catch { 53 const authUrl = await createAuthorizationUrl({ 54 scope: import.meta.env.VITE_OAUTH_SCOPE, 55 target: { type: "account", identifier: did }, 56 }); 57 58 await new Promise((resolve) => setTimeout(resolve, 250)); 59 60 location.assign(authUrl); 61 } 62 }; 63 64 const removeSession = async (did: Did) => { 65 const currentSession = agent()?.sub; 66 try { 67 const session = await getSession(did, { allowStale: true }); 68 const agent = new OAuthUserAgent(session); 69 await agent.signOut(); 70 } catch { 71 deleteStoredSession(did); 72 } 73 setSessions( 74 produce((accs) => { 75 delete accs[did]; 76 }), 77 ); 78 localStorage.setItem("sessions", JSON.stringify(sessions)); 79 if (currentSession === did) setAgent(undefined); 80 }; 81 82 const getAvatar = async (did: Did) => { 83 const rpc = new Client({ 84 handler: new CredentialManager({ service: "https://public.api.bsky.app" }), 85 }); 86 const res = await rpc.get("app.bsky.actor.getProfile", { params: { actor: did } }); 87 if (res.ok) { 88 return res.data.avatar; 89 } 90 return undefined; 91 }; 92 93 return ( 94 <> 95 <Modal open={openManager()} onClose={() => setOpenManager(false)}> 96 <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"> 97 <div class="mb-2 px-1 font-semibold"> 98 <span>Manage accounts</span> 99 </div> 100 <div class="mb-3 max-h-80 overflow-y-auto md:max-h-100"> 101 <For each={Object.keys(sessions)}> 102 {(did) => ( 103 <div class="flex w-full items-center justify-between"> 104 <A 105 href={`/at://${did}`} 106 onClick={() => setOpenManager(false)} 107 class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 108 > 109 <Show 110 when={avatars[did as Did]} 111 fallback={<span class="iconify lucide--user-round m-0.5 size-5"></span>} 112 > 113 <img 114 src={avatars[did as Did].replace("img/avatar/", "img/avatar_thumbnail/")} 115 class="size-6 rounded-full" 116 /> 117 </Show> 118 </A> 119 <button 120 class="flex grow items-center justify-between gap-1 truncate rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 121 onclick={() => resumeSession(did as Did)} 122 > 123 <span class="truncate"> 124 {sessions[did]?.handle ? sessions[did].handle : did} 125 </span> 126 <Show when={did === agent()?.sub && sessions[did].signedIn}> 127 <span class="iconify lucide--check shrink-0 text-green-500 dark:text-green-400"></span> 128 </Show> 129 <Show when={!sessions[did].signedIn}> 130 <span class="iconify lucide--circle-alert shrink-0 text-red-500 dark:text-red-400"></span> 131 </Show> 132 </button> 133 <button 134 onclick={() => removeSession(did as Did)} 135 class="flex items-center rounded-md p-2 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 136 > 137 <span class="iconify lucide--x"></span> 138 </button> 139 </div> 140 )} 141 </For> 142 </div> 143 <Login /> 144 </div> 145 </Modal> 146 <button 147 onclick={() => setOpenManager(true)} 148 class={`flex items-center rounded-lg ${agent() && avatars[agent()!.sub] ? "p-1.25" : "p-1.5"} hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600`} 149 > 150 {agent() && avatars[agent()!.sub] ? 151 <img 152 src={avatars[agent()!.sub].replace("img/avatar/", "img/avatar_thumbnail/")} 153 class="size-5 rounded-full" 154 /> 155 : <span class="iconify lucide--circle-user-round text-lg"></span>} 156 </button> 157 </> 158 ); 159};