atmosphere explorer pds.ls
tool typescript atproto
437
fork

Configure Feed

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

at main 120 lines 3.5 kB view raw
1import { Client, simpleFetchHandler } from "@atcute/client"; 2import { ActorIdentifier } from "@atcute/lexicons"; 3import { createSignal, Show } from "solid-js"; 4import { getPDS } from "../../lib/api"; 5import { JSONValue } from "../json"; 6import HoverCard from "./base"; 7 8interface RecordHoverCardProps { 9 uri: string; 10 newTab?: boolean; 11 class?: string; 12 labelClass?: string; 13 trigger?: any; 14 hoverDelay?: number; 15} 16 17const recordCache = new Map<string, { value: unknown; loading: boolean; error?: string }>(); 18 19const parseAtUri = (uri: string) => { 20 const match = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/); 21 if (!match) return null; 22 return { repo: match[1], collection: match[2], rkey: match[3] }; 23}; 24 25const prefetchRecord = async (uri: string) => { 26 if (recordCache.has(uri)) return; 27 28 const parsed = parseAtUri(uri); 29 if (!parsed) return; 30 31 recordCache.set(uri, { value: null, loading: true }); 32 33 try { 34 const pds = await getPDS(parsed.repo); 35 const rpc = new Client({ handler: simpleFetchHandler({ service: pds }) }); 36 const res = await rpc.get("com.atproto.repo.getRecord", { 37 params: { 38 repo: parsed.repo as ActorIdentifier, 39 collection: parsed.collection as `${string}.${string}.${string}`, 40 rkey: parsed.rkey, 41 }, 42 }); 43 44 if (!res.ok) { 45 recordCache.set(uri, { value: null, loading: false, error: res.data.error }); 46 return; 47 } 48 49 recordCache.set(uri, { value: res.data.value, loading: false }); 50 } catch (err: any) { 51 recordCache.set(uri, { value: null, loading: false, error: err.message || "Failed to fetch" }); 52 } 53}; 54 55const RecordHoverCard = (props: RecordHoverCardProps) => { 56 const [record, setRecord] = createSignal<{ 57 value: unknown; 58 loading: boolean; 59 error?: string; 60 } | null>(null); 61 62 const parsed = () => parseAtUri(props.uri); 63 64 const handlePrefetch = () => { 65 prefetchRecord(props.uri); 66 67 // Start polling for cache updates 68 const cached = recordCache.get(props.uri); 69 setRecord(cached || { value: null, loading: true }); 70 71 if (!cached || cached.loading) { 72 const pollInterval = setInterval(() => { 73 const updated = recordCache.get(props.uri); 74 if (updated && !updated.loading) { 75 setRecord(updated); 76 clearInterval(pollInterval); 77 } 78 }, 100); 79 80 setTimeout(() => clearInterval(pollInterval), 10000); 81 } 82 }; 83 84 return ( 85 <HoverCard 86 href={`/${props.uri}`} 87 label={props.uri} 88 newTab={props.newTab} 89 onHover={handlePrefetch} 90 hoverDelay={props.hoverDelay ?? 300} 91 trigger={props.trigger} 92 class={props.class} 93 labelClass={props.labelClass} 94 > 95 <Show when={record()?.loading}> 96 <div class="flex items-center gap-2 font-sans text-sm text-neutral-500 dark:text-neutral-400"> 97 <span class="iconify lucide--loader-circle animate-spin" /> 98 Loading... 99 </div> 100 </Show> 101 <Show when={record()?.error}> 102 <div class="font-sans text-sm text-red-500 dark:text-red-400">{record()?.error}</div> 103 </Show> 104 <Show when={record()?.value && !record()?.loading}> 105 <div class="font-mono text-xs wrap-break-word"> 106 <JSONValue 107 data={record()?.value as any} 108 repo={parsed()?.repo || ""} 109 truncate 110 newTab 111 hideBlobs 112 preview 113 /> 114 </div> 115 </Show> 116 </HoverCard> 117 ); 118}; 119 120export default RecordHoverCard;