atmosphere explorer
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at atcute 209 lines 7.4 kB view raw
1import { Did } from "@atcute/lexicons"; 2import { deleteStoredSession, getSession, OAuthUserAgent } from "@atcute/oauth-browser-client"; 3import { A } from "@solidjs/router"; 4import { createEffect, For, onMount, Show } from "solid-js"; 5import { produce } from "solid-js/store"; 6import { ActionMenu, DropdownMenu, MenuProvider, NavMenu } from "../components/dropdown.jsx"; 7import { Modal } from "../components/modal.jsx"; 8import { Login } from "./login.jsx"; 9import { useOAuthScopeFlow } from "./scope-flow.js"; 10import { ScopeSelector } from "./scope-selector.jsx"; 11import { parseScopeString } from "./scope-utils.js"; 12import { 13 getAvatar, 14 loadHandleForSession, 15 loadSessionsFromStorage, 16 resumeSession, 17 retrieveSession, 18 saveSessionToStorage, 19} from "./session-manager.js"; 20import { 21 agent, 22 avatars, 23 openManager, 24 pendingPermissionEdit, 25 sessions, 26 setAgent, 27 setAvatars, 28 setOpenManager, 29 setPendingPermissionEdit, 30 setSessions, 31 setShowAddAccount, 32 showAddAccount, 33} from "./state.js"; 34 35const AccountDropdown = (props: { did: Did; onEditPermissions: (did: Did) => void }) => { 36 const removeSession = async (did: Did) => { 37 const currentSession = agent()?.sub; 38 try { 39 const session = await getSession(did, { allowStale: true }); 40 const agent = new OAuthUserAgent(session); 41 await agent.signOut(); 42 } catch { 43 deleteStoredSession(did); 44 } 45 setSessions( 46 produce((accs) => { 47 delete accs[did]; 48 }), 49 ); 50 saveSessionToStorage(sessions); 51 if (currentSession === did) setAgent(undefined); 52 }; 53 54 return ( 55 <MenuProvider> 56 <DropdownMenu icon="lucide--ellipsis" buttonClass="rounded-md p-2"> 57 <NavMenu 58 href={`/at://${props.did}`} 59 label="Go to repo" 60 icon="lucide--user-round" 61 shortcut={agent()?.sub === props.did ? "G" : undefined} 62 /> 63 <ActionMenu 64 icon="lucide--settings" 65 label="Edit permissions" 66 onClick={() => props.onEditPermissions(props.did)} 67 /> 68 <ActionMenu 69 icon="lucide--x" 70 label="Remove account" 71 onClick={() => removeSession(props.did)} 72 /> 73 </DropdownMenu> 74 </MenuProvider> 75 ); 76}; 77 78export const AccountManager = () => { 79 const getThumbnailUrl = (avatarUrl: string) => { 80 return avatarUrl.replace("img/avatar/", "img/avatar_thumbnail/"); 81 }; 82 83 const scopeFlow = useOAuthScopeFlow({ 84 beforeRedirect: (account) => resumeSession(account as Did), 85 }); 86 87 createEffect(() => { 88 const pending = pendingPermissionEdit(); 89 if (pending) { 90 scopeFlow.initiateWithRedirect(pending); 91 setPendingPermissionEdit(null); 92 } 93 }); 94 95 const handleAccountClick = async (did: Did) => { 96 try { 97 await resumeSession(did); 98 } catch { 99 scopeFlow.initiate(did); 100 } 101 }; 102 103 onMount(async () => { 104 try { 105 await retrieveSession(); 106 } catch {} 107 108 const storedSessions = loadSessionsFromStorage(); 109 if (storedSessions) { 110 const sessionDids = Object.keys(storedSessions) as Did[]; 111 sessionDids.forEach(async (did) => { 112 await loadHandleForSession(did, storedSessions); 113 }); 114 sessionDids.forEach(async (did) => { 115 const avatar = await getAvatar(did); 116 if (avatar) setAvatars(did, avatar); 117 }); 118 } 119 }); 120 121 return ( 122 <> 123 <Modal 124 open={openManager()} 125 onClose={() => { 126 setOpenManager(false); 127 setShowAddAccount(false); 128 scopeFlow.cancel(); 129 }} 130 alignTop 131 contentClass="dark:bg-dark-300 dark:shadow-dark-700 pointer-events-auto w-full max-w-sm rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 mx-3 shadow-md dark:border-neutral-700" 132 > 133 <Show when={!scopeFlow.showScopeSelector() && !showAddAccount()}> 134 <div class="mb-2 px-1 font-semibold"> 135 <span>Switch account</span> 136 </div> 137 <div class="mb-3 max-h-80 overflow-y-auto md:max-h-100"> 138 <For each={Object.keys(sessions)}> 139 {(did) => ( 140 <div class="flex w-full items-center justify-between"> 141 <A 142 href={`/at://${did}`} 143 onClick={() => setOpenManager(false)} 144 class="flex shrink-0 items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 145 > 146 <Show 147 when={avatars[did as Did]} 148 fallback={<span class="iconify lucide--user-round m-0.5 size-5"></span>} 149 > 150 <img src={getThumbnailUrl(avatars[did as Did])} class="size-6 rounded-full" /> 151 </Show> 152 </A> 153 <button 154 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" 155 onclick={() => handleAccountClick(did as Did)} 156 > 157 <span class="truncate">{sessions[did]?.handle || did}</span> 158 <Show when={did === agent()?.sub && sessions[did].signedIn}> 159 <span class="iconify lucide--circle-check shrink-0 text-blue-500 dark:text-blue-400"></span> 160 </Show> 161 <Show when={!sessions[did].signedIn}> 162 <span class="iconify lucide--circle-alert shrink-0 text-red-500 dark:text-red-400"></span> 163 </Show> 164 </button> 165 <AccountDropdown 166 did={did as Did} 167 onEditPermissions={(accountDid) => scopeFlow.initiateWithRedirect(accountDid)} 168 /> 169 </div> 170 )} 171 </For> 172 </div> 173 <button 174 onclick={() => setShowAddAccount(true)} 175 class="dark:hover:bg-dark-200 dark:active:bg-dark-100 flex w-full items-center justify-center gap-2 rounded-lg border border-neutral-200 px-3 py-2 hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700" 176 > 177 <span class="iconify lucide--plus"></span> 178 <span>Add account</span> 179 </button> 180 </Show> 181 182 <Show when={showAddAccount() && !scopeFlow.showScopeSelector()}> 183 <Login onCancel={() => setShowAddAccount(false)} /> 184 </Show> 185 186 <Show when={scopeFlow.showScopeSelector()}> 187 <ScopeSelector 188 initialScopes={parseScopeString( 189 sessions[scopeFlow.pendingAccount()]?.grantedScopes || "", 190 )} 191 onConfirm={scopeFlow.complete} 192 onCancel={() => { 193 scopeFlow.cancel(); 194 setShowAddAccount(false); 195 }} 196 /> 197 </Show> 198 </Modal> 199 <button 200 onclick={() => setOpenManager(true)} 201 class={`flex items-center rounded-md ${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`} 202 > 203 {agent() && avatars[agent()!.sub] ? 204 <img src={getThumbnailUrl(avatars[agent()!.sub])} class="size-5 rounded-full" /> 205 : <span class="iconify lucide--circle-user-round text-lg"></span>} 206 </button> 207 </> 208 ); 209};