atmosphere explorer
0
fork

Configure Feed

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

PDS list virtual scrolling

Juliet bf079a55 b54f5474

+156 -69
+1
package.json
··· 54 54 "@solidjs/router": "^0.15.4", 55 55 "@takumi-rs/image-response": "^0.72.0", 56 56 "@takumi-rs/wasm": "^0.72.0", 57 + "@tanstack/solid-virtual": "^3.13.22", 57 58 "codemirror": "^6.0.2", 58 59 "solid-js": "^1.9.11" 59 60 },
+18
pnpm-lock.yaml
··· 98 98 '@takumi-rs/wasm': 99 99 specifier: ^0.72.0 100 100 version: 0.72.0 101 + '@tanstack/solid-virtual': 102 + specifier: ^3.13.22 103 + version: 3.13.22(solid-js@1.9.11) 101 104 codemirror: 102 105 specifier: ^6.0.2 103 106 version: 6.0.2 ··· 1016 1019 1017 1020 '@takumi-rs/wasm@0.72.0': 1018 1021 resolution: {integrity: sha512-5IoekEovpjzpoHd5LM4hRLRgRwW8RdFdF9TR2kmysB1rJF34z0ynIC/Fr0yhraxvFIP0fCzdinz14YzyTq6nuQ==} 1022 + 1023 + '@tanstack/solid-virtual@3.13.22': 1024 + resolution: {integrity: sha512-HoFRbuNfMz4IckIJAQ9RY8uJfT28AjL040cmwBZW8c1dD+VJVKRL0leZo90bU7BK4Mnhq6vt8801BZjyrY8Ffg==} 1025 + peerDependencies: 1026 + solid-js: ^1.3.0 1027 + 1028 + '@tanstack/virtual-core@3.13.22': 1029 + resolution: {integrity: sha512-isuUGKsc5TAPDoHSbWTbl1SCil54zOS2MiWz/9GCWHPUQOvNTQx8qJEWC7UWR0lShhbK0Lmkcf0SZYxvch7G3g==} 1019 1030 1020 1031 '@types/babel__core@7.20.5': 1021 1032 resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} ··· 2439 2450 '@takumi-rs/wasm': 0.72.0 2440 2451 2441 2452 '@takumi-rs/wasm@0.72.0': {} 2453 + 2454 + '@tanstack/solid-virtual@3.13.22(solid-js@1.9.11)': 2455 + dependencies: 2456 + '@tanstack/virtual-core': 3.13.22 2457 + solid-js: 1.9.11 2458 + 2459 + '@tanstack/virtual-core@3.13.22': {} 2442 2460 2443 2461 '@types/babel__core@7.20.5': 2444 2462 dependencies:
+137 -69
src/views/pds.tsx
··· 3 3 import { InferXRPCBodyOutput } from "@atcute/lexicons"; 4 4 import * as TID from "@atcute/tid"; 5 5 import { A, useLocation, useParams } from "@solidjs/router"; 6 + import { createWindowVirtualizer } from "@tanstack/solid-virtual"; 6 7 import { createResource, createSignal, For, Show } from "solid-js"; 7 8 import { Button } from "../components/button"; 8 9 import DidHoverCard from "../components/hover-card/did"; ··· 12 13 13 14 const LIMIT = 1000; 14 15 16 + const RepoCard = (props: { 17 + repo: ComAtprotoSyncListRepos.Repo; 18 + expanded: boolean; 19 + onToggle: () => void; 20 + }) => { 21 + const expanded = () => props.expanded; 22 + 23 + return ( 24 + <div 25 + classList={{ 26 + "group rounded-md border-[0.5px]": true, 27 + "dark:hover:bg-dark-200 border-transparent hover:bg-neutral-200/50": !expanded(), 28 + "dark:bg-dark-300 border-neutral-200 bg-neutral-50 shadow-sm transition-[background-color,border-color,box-shadow] duration-200 dark:border-neutral-700 dark:shadow-dark-700": 29 + expanded(), 30 + }} 31 + > 32 + <div class="flex min-w-0 flex-1 items-center"> 33 + <button 34 + type="button" 35 + onclick={() => props.onToggle()} 36 + class="flex min-w-0 flex-1 items-center gap-2 p-1.5" 37 + > 38 + <span 39 + classList={{ 40 + "mt-0.5 flex shrink-0 items-center text-neutral-400 transition-transform duration-200 dark:text-neutral-500": true, 41 + "rotate-90": expanded(), 42 + }} 43 + > 44 + <span class="iconify lucide--chevron-right"></span> 45 + </span> 46 + <div class="flex min-w-0 flex-1 items-center gap-x-2 text-sm"> 47 + <span class="min-w-0 truncate font-mono" onclick={(e) => e.stopPropagation()}> 48 + <DidHoverCard newTab did={props.repo.did} /> 49 + </span> 50 + <Show when={!props.repo.active}> 51 + <span class="flex shrink-0 items-center gap-1 text-red-500 dark:text-red-400"> 52 + <span 53 + class={`iconify ${ 54 + props.repo.status === "deactivated" ? "lucide--user-round-x" 55 + : props.repo.status === "takendown" ? "lucide--shield-ban" 56 + : "lucide--unplug" 57 + }`} 58 + ></span> 59 + {props.repo.status ?? "inactive"} 60 + </span> 61 + </Show> 62 + </div> 63 + </button> 64 + <Show when={expanded() || canHover}> 65 + <A 66 + href={`/at://${props.repo.did}`} 67 + classList={{ 68 + "flex shrink-0 items-center p-2 transition-colors duration-200": true, 69 + "invisible group-hover:visible not-hover:text-neutral-500 not-hover:dark:text-neutral-400": 70 + !expanded(), 71 + }} 72 + > 73 + <span class="iconify lucide--arrow-right"></span> 74 + </A> 75 + </Show> 76 + </div> 77 + <div 78 + classList={{ 79 + "grid transition-[grid-template-rows] duration-200 ease-out": true, 80 + "grid-rows-[1fr]": expanded(), 81 + "grid-rows-[0fr]": !expanded(), 82 + }} 83 + > 84 + <div class="overflow-hidden"> 85 + <div class="ml-7.5 flex flex-col gap-1.5 pb-1.5 font-mono text-xs text-neutral-500 dark:text-neutral-400"> 86 + <Show when={props.repo.head}> 87 + <span class="truncate">{props.repo.head}</span> 88 + </Show> 89 + <Show when={TID.validate(props.repo.rev)}> 90 + <div class="flex gap-1 text-neutral-700 dark:text-neutral-300"> 91 + <span>{props.repo.rev}</span> 92 + <span>•</span> 93 + <span>{localDateFromTimestamp(TID.parse(props.repo.rev).timestamp / 1000)}</span> 94 + </div> 95 + </Show> 96 + </div> 97 + </div> 98 + </div> 99 + </div> 100 + ); 101 + }; 102 + 15 103 export const PdsView = () => { 16 104 const params = useParams(); 17 105 const location = useLocation(); ··· 35 123 if (!res.ok) console.error(res.data.error); 36 124 else setServerInfos(res.data); 37 125 }; 126 + 127 + getVersion(); 128 + describeServer(); 38 129 39 130 const fetchRepos = async () => { 40 - getVersion(); 41 - describeServer(); 42 131 const res = await rpc.get("com.atproto.sync.listRepos", { 43 132 params: { limit: LIMIT, cursor: cursor() }, 44 133 }); ··· 51 140 const [response, { refetch }] = createResource(fetchRepos); 52 141 const [repos, setRepos] = createSignal<ComAtprotoSyncListRepos.Repo[]>(); 53 142 54 - const RepoCard = (repo: ComAtprotoSyncListRepos.Repo) => { 55 - const [expanded, setExpanded] = createSignal(false); 56 - const [hovering, setHovering] = createSignal(false); 143 + const [expandedIndex, setExpandedIndex] = createSignal<number | null>(null); 57 144 58 - return ( 59 - <div class="flex flex-col gap-1"> 60 - <div 61 - class="dark:hover:bg-dark-200 flex min-w-0 flex-1 items-center rounded hover:bg-neutral-200/70" 62 - onMouseEnter={() => canHover && setHovering(true)} 63 - onMouseLeave={() => canHover && setHovering(false)} 64 - > 65 - <button 66 - type="button" 67 - onclick={() => setExpanded(!expanded())} 68 - class="flex min-w-0 flex-1 items-center gap-2 p-1.5" 69 - > 70 - <span class="mt-0.5 flex shrink-0 items-center text-neutral-400 dark:text-neutral-500"> 71 - {expanded() ? 72 - <span class="iconify lucide--chevron-down"></span> 73 - : <span class="iconify lucide--chevron-right"></span>} 74 - </span> 75 - <div class="flex min-w-0 flex-1 items-center gap-x-2 text-sm"> 76 - <span class="min-w-0 truncate font-mono" onclick={(e) => e.stopPropagation()}> 77 - <DidHoverCard newTab did={repo.did} /> 78 - </span> 79 - <Show when={!repo.active}> 80 - <span class="flex shrink-0 items-center gap-1 text-red-500 dark:text-red-400"> 81 - <span 82 - class={`iconify ${ 83 - repo.status === "deactivated" ? "lucide--user-round-x" 84 - : repo.status === "takendown" ? "lucide--shield-ban" 85 - : "lucide--unplug" 86 - }`} 87 - ></span> 88 - {repo.status ?? "inactive"} 89 - </span> 90 - </Show> 91 - </div> 92 - </button> 93 - <Show when={expanded() || hovering()}> 94 - <A 95 - href={`/at://${repo.did}`} 96 - class="flex shrink-0 items-center p-2 transition-colors not-hover:text-neutral-500 not-hover:dark:text-neutral-400" 97 - > 98 - <span class="iconify lucide--arrow-right"></span> 99 - </A> 100 - </Show> 101 - </div> 102 - <Show when={expanded()}> 103 - <div class="mb-2 ml-7.5 flex flex-col gap-1 font-mono text-xs text-neutral-500 dark:text-neutral-400"> 104 - <Show when={repo.head}> 105 - <span class="truncate">{repo.head}</span> 106 - </Show> 107 - <Show when={TID.validate(repo.rev)}> 108 - <div class="flex gap-1 text-neutral-700 dark:text-neutral-300"> 109 - <span>{repo.rev}</span> 110 - <span>•</span> 111 - <span>{localDateFromTimestamp(TID.parse(repo.rev).timestamp / 1000)}</span> 112 - </div> 113 - </Show> 114 - </div> 115 - </Show> 116 - </div> 117 - ); 118 - }; 145 + let containerRef: HTMLDivElement | undefined; 146 + const virtualizer = createWindowVirtualizer({ 147 + get count() { 148 + return repos()?.length ?? 0; 149 + }, 150 + estimateSize: () => 32, 151 + overscan: 10, 152 + get scrollMargin() { 153 + return containerRef?.offsetTop ?? 0; 154 + }, 155 + }); 119 156 120 157 const Tab = (props: { tab: "repos" | "info" | "firehose"; label: string }) => ( 121 158 <A ··· 147 184 <Tab tab="firehose" label="Firehose" /> 148 185 </div> 149 186 <Show when={!location.hash || location.hash === "#repos"}> 150 - <div class="-mx-2 flex flex-col pb-12"> 151 - <For each={repos()}>{(repo) => <RepoCard {...repo} />}</For> 187 + <div 188 + class="-mx-2 mb-9" 189 + ref={containerRef} 190 + style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative" }} 191 + > 192 + <For each={virtualizer.getVirtualItems()}> 193 + {(virtualItem) => ( 194 + <div 195 + data-index={virtualItem.index} 196 + ref={(el) => virtualizer.measureElement(el)} 197 + classList={{ "z-10": expandedIndex() === virtualItem.index }} 198 + style={{ 199 + position: "absolute", 200 + top: `${virtualItem.start - virtualizer.options.scrollMargin}px`, 201 + left: 0, 202 + width: "100%", 203 + }} 204 + > 205 + <RepoCard 206 + repo={repos()![virtualItem.index]} 207 + expanded={expandedIndex() === virtualItem.index} 208 + onToggle={() => 209 + setExpandedIndex( 210 + expandedIndex() === virtualItem.index ? null : virtualItem.index, 211 + ) 212 + } 213 + /> 214 + </div> 215 + )} 216 + </For> 152 217 </div> 153 218 </Show> 154 219 <div class="flex flex-col gap-3"> ··· 256 321 </p> 257 322 <Show when={cursor()}> 258 323 <Button 259 - onClick={() => refetch()} 324 + onClick={() => { 325 + setExpandedIndex(null); 326 + refetch(); 327 + }} 260 328 disabled={response.loading} 261 329 classList={{ "w-20 h-7.5 justify-center": true }} 262 330 >