atproto explorer
0
fork

Configure Feed

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

add label query

Juliet 0770e3e2 b07288ab

+204 -9
+19
src/components/navbar.tsx
··· 4 4 5 5 export const [pds, setPDS] = createSignal<string>(); 6 6 export const [cid, setCID] = createSignal<string>(); 7 + export const [isLabeler, setIsLabeler] = createSignal(false); 7 8 export const [validRecord, setValidRecord] = createSignal<boolean | undefined>( 8 9 undefined, 9 10 ); ··· 114 115 inactiveClass="text-lightblue-500 w-full hover:underline" 115 116 > 116 117 blobs 118 + </A> 119 + </div> 120 + </Show> 121 + <Show 122 + when={ 123 + isLabeler() && !props.params.collection && !props.params.rkey 124 + } 125 + > 126 + <div class="mt-1 flex items-center"> 127 + <Tooltip text="Labels"> 128 + <div class="i-mdi-tag-outline mr-1 text-sm" /> 129 + </Tooltip> 130 + <A 131 + end 132 + href={`at/${props.params.repo}/labels`} 133 + inactiveClass="text-lightblue-500 w-full hover:underline" 134 + > 135 + labels 117 136 </A> 118 137 </div> 119 138 </Show>
+2
src/index.tsx
··· 12 12 import { BlobView } from "./views/blob.tsx"; 13 13 import { CollectionView } from "./views/collection.tsx"; 14 14 import { RecordView } from "./views/record.tsx"; 15 + import { LabelView } from "./views/labels.tsx"; 15 16 16 17 render( 17 18 () => ( ··· 20 21 <Route path="/:pds" component={PdsView} /> 21 22 <Route path="/:pds/:repo" component={RepoView} /> 22 23 <Route path="/:pds/:repo/blobs" component={BlobView} /> 24 + <Route path="/:pds/:repo/labels" component={LabelView} /> 23 25 <Route path="/:pds/:repo/:collection" component={CollectionView} /> 24 26 <Route path="/:pds/:repo/:collection/:rkey" component={RecordView} /> 25 27 </Router>
+12
src/styles/icons.css
··· 291 291 width: 1.2em; 292 292 height: 1.2em; 293 293 } 294 + 295 + .i-mdi-tag-outline { 296 + --un-icon: url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' width='1.2em' height='1.2em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m21.41 11.58l-9-9A2 2 0 0 0 11 2H4a2 2 0 0 0-2 2v7a2 2 0 0 0 .59 1.42l9 9A2 2 0 0 0 13 22a2 2 0 0 0 1.41-.59l7-7A2 2 0 0 0 22 13a2 2 0 0 0-.59-1.42M13 20l-9-9V4h7l9 9M6.5 5A1.5 1.5 0 1 1 5 6.5A1.5 1.5 0 0 1 6.5 5'/%3E%3C/svg%3E"); 297 + -webkit-mask: var(--un-icon) no-repeat; 298 + mask: var(--un-icon) no-repeat; 299 + -webkit-mask-size: 100% 100%; 300 + mask-size: 100% 100%; 301 + background-color: currentColor; 302 + color: inherit; 303 + width: 1.2em; 304 + height: 1.2em; 305 + }
+8 -3
src/utils/api.ts
··· 1 1 import { CredentialManager, XRPC } from "@atcute/client"; 2 2 import { query } from "@solidjs/router"; 3 - import { setPDS } from "../components/navbar"; 3 + import { setIsLabeler, setPDS } from "../components/navbar"; 4 4 import { DidDocument } from "@atcute/client/utils/did"; 5 5 6 6 const didPDSCache: Record<string, string> = {}; 7 + const labelerCache: Record<string, string> = {}; 7 8 const didDocCache: Record<string, DidDocument> = {}; 8 9 const getPDS = query(async (did: string) => { 9 10 if (did in didPDSCache) return didPDSCache[did]; ··· 19 20 if (service.id === "#atproto_pds") { 20 21 didPDSCache[did] = service.serviceEndpoint.toString(); 21 22 didDocCache[did] = doc; 22 - return service.serviceEndpoint.toString(); 23 + } 24 + if (service.id === "#atproto_labeler") { 25 + labelerCache[did] = service.serviceEndpoint.toString(); 26 + setIsLabeler(true); 23 27 } 24 28 } 29 + return didPDSCache[did]; 25 30 }); 26 31 }, "getPDS"); 27 32 ··· 43 48 return pds; 44 49 }; 45 50 46 - export { getPDS, didDocCache, resolveHandle, resolvePDS }; 51 + export { getPDS, labelerCache, didDocCache, resolveHandle, resolvePDS };
+7
src/utils/date.ts
··· 1 + const getDateFromTimestamp = (timestamp: number) => 2 + new Date(timestamp - new Date().getTimezoneOffset() * 60 * 1000) 3 + .toISOString() 4 + .split(".")[0] 5 + .replace("T", " "); 6 + 7 + export { getDateFromTimestamp };
+1 -6
src/views/collection.tsx
··· 21 21 import { agent, loginState } from "../components/login.jsx"; 22 22 import { createStore } from "solid-js/store"; 23 23 import Tooltip from "../components/tooltip.jsx"; 24 + import { getDateFromTimestamp } from "../utils/date.js"; 24 25 25 26 interface AtprotoRecord { 26 27 rkey: string; ··· 40 41 41 42 const isOverflowing = (elem: HTMLElement, previewHeight: number) => 42 43 elem.offsetTop - window.scrollY + previewHeight + 32 > window.innerHeight; 43 - 44 - const getDateFromTimestamp = (timestamp: number) => 45 - new Date(timestamp - new Date().getTimezoneOffset() * 60 * 1000) 46 - .toISOString() 47 - .split(".")[0] 48 - .replace("T", " "); 49 44 50 45 return ( 51 46 <span
+155
src/views/labels.tsx
··· 1 + import { createResource, createSignal, For, onMount, Show } from "solid-js"; 2 + import { CredentialManager, XRPC } from "@atcute/client"; 3 + import { useParams } from "@solidjs/router"; 4 + import { labelerCache, resolvePDS } from "../utils/api.js"; 5 + import { ComAtprotoLabelDefs } from "@atcute/client/lexicons"; 6 + import { getDateFromTimestamp } from "../utils/date.js"; 7 + 8 + const LabelView = () => { 9 + const params = useParams(); 10 + const [cursor, setCursor] = createSignal<string>(); 11 + const [labels, setLabels] = createSignal<ComAtprotoLabelDefs.Label[]>([]); 12 + const [uriPatterns, setUriPatterns] = createSignal<string>(); 13 + const did = params.repo; 14 + let rpc: XRPC; 15 + 16 + onMount(async () => { 17 + await resolvePDS(did); 18 + rpc = new XRPC({ 19 + handler: new CredentialManager({ service: labelerCache[did] }), 20 + }); 21 + }); 22 + 23 + const fetchLabels = async () => { 24 + const res = await rpc.get("com.atproto.label.queryLabels", { 25 + params: { 26 + uriPatterns: uriPatterns()!.split(","), 27 + sources: [did as `did:${string}`], 28 + cursor: cursor(), 29 + }, 30 + }); 31 + setCursor(res.data.labels.length < 50 ? undefined : res.data.cursor); 32 + setLabels(labels().concat(res.data.labels) ?? res.data.labels); 33 + return res.data.labels; 34 + }; 35 + 36 + const [response, { refetch }] = createResource(uriPatterns, fetchLabels); 37 + 38 + return ( 39 + <> 40 + <div class="z-5 dark:bg-dark-700 sticky top-0 flex w-full flex-col items-center justify-center gap-2 bg-slate-100 py-4"> 41 + <form 42 + class="flex flex-col items-center gap-y-1" 43 + onsubmit={(e) => e.preventDefault()} 44 + > 45 + <div class="w-full"> 46 + <label for="patterns" class="ml-0.5 text-sm"> 47 + URI Patterns (comma-separated) 48 + </label> 49 + </div> 50 + <div class="flex items-center gap-x-2"> 51 + <textarea 52 + id="patterns" 53 + name="patterns" 54 + spellcheck={false} 55 + rows={3} 56 + cols={25} 57 + class="dark:bg-dark-100 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300" 58 + /> 59 + <button 60 + onclick={() => 61 + setUriPatterns( 62 + (document.getElementById("patterns") as HTMLInputElement) 63 + .value, 64 + ) 65 + } 66 + type="submit" 67 + class="dark:bg-dark-700 dark:hover:bg-dark-800 rounded-lg border border-gray-400 bg-white px-2.5 py-1.5 text-sm font-bold hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-gray-300" 68 + > 69 + Get 70 + </button> 71 + </div> 72 + </form> 73 + <div class="flex items-center gap-x-2"> 74 + <Show when={labels().length}> 75 + <div> 76 + <span> 77 + {labels().length} label{labels().length > 1 ? "s" : ""} 78 + </span> 79 + </div> 80 + </Show> 81 + <Show when={cursor()}> 82 + <div class="flex h-[2rem] w-[5.5rem] items-center justify-center text-nowrap"> 83 + <Show when={!response.loading}> 84 + <button 85 + type="button" 86 + onclick={() => refetch()} 87 + class="dark:bg-dark-700 dark:hover:bg-dark-800 rounded-lg border border-gray-400 bg-white px-2 py-1.5 text-sm font-bold hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-gray-300" 88 + > 89 + Load More 90 + </button> 91 + </Show> 92 + <Show when={response.loading}> 93 + <div class="i-line-md-loading-twotone-loop text-xl"></div> 94 + </Show> 95 + </div> 96 + </Show> 97 + </div> 98 + </div> 99 + <div class="break-anywhere flex flex-col gap-2 divide-y divide-neutral-500 whitespace-pre-wrap font-mono"> 100 + <For each={labels()}> 101 + {(label) => ( 102 + <div class="flex flex-col gap-x-2 pt-2"> 103 + <div class="flex gap-x-2"> 104 + <div class="min-w-[5rem] font-semibold text-stone-600 dark:text-stone-400"> 105 + URI 106 + </div> 107 + {label.uri} 108 + </div> 109 + <Show when={label.cid}> 110 + <div class="flex gap-x-2"> 111 + <div class="min-w-[5rem] font-semibold text-stone-600 dark:text-stone-400"> 112 + CID 113 + </div> 114 + {label.cid} 115 + </div> 116 + </Show> 117 + <div class="flex gap-x-2"> 118 + <div class="min-w-[5rem] font-semibold text-stone-600 dark:text-stone-400"> 119 + Label 120 + </div> 121 + {label.val} 122 + </div> 123 + <Show when={label.neg}> 124 + <div class="flex gap-x-2"> 125 + <div class="min-w-[5rem] font-semibold text-stone-600 dark:text-stone-400"> 126 + Negated 127 + </div> 128 + {label.neg} 129 + </div> 130 + </Show> 131 + <div class="flex gap-x-2"> 132 + <div class="min-w-[5rem] font-semibold text-stone-600 dark:text-stone-400"> 133 + Created 134 + </div> 135 + {getDateFromTimestamp(new Date(label.cts).getTime())} 136 + </div> 137 + <Show when={label.exp}> 138 + {(exp) => ( 139 + <div class="flex gap-x-2"> 140 + <div class="min-w-[5rem] font-semibold text-stone-600 dark:text-stone-400"> 141 + Expires 142 + </div> 143 + {getDateFromTimestamp(new Date(exp()).getTime())} 144 + </div> 145 + )} 146 + </Show> 147 + </div> 148 + )} 149 + </For> 150 + </div> 151 + </> 152 + ); 153 + }; 154 + 155 + export { LabelView };