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