atproto explorer
0
fork

Configure Feed

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

merge jetstream and firehose views

Juliet 7e9125a4 57aeb184

+267 -342
+3 -4
src/index.tsx
··· 12 12 import { BlobView } from "./views/blob.tsx"; 13 13 import { CollectionView } from "./views/collection.tsx"; 14 14 import { LabelView } from "./views/labels.tsx"; 15 - import { JetstreamView } from "./views/jetstream.tsx"; 16 - import { SubscribeReposView } from "./views/subscribeRepos.tsx"; 15 + import { StreamView } from "./views/stream.tsx"; 17 16 import { lazy } from "solid-js"; 18 17 19 18 const RecordView = lazy(() => import("./views/record.tsx")); ··· 22 21 () => ( 23 22 <Router root={Layout}> 24 23 <Route path="/" component={Home} /> 25 - <Route path="/jetstream" component={JetstreamView} /> 26 - <Route path="/firehose" component={SubscribeReposView} /> 24 + <Route path="/jetstream" component={StreamView} /> 25 + <Route path="/firehose" component={StreamView} /> 27 26 <Route path="/:pds" component={PdsView} /> 28 27 <Route path="/:pds/:repo" component={RepoView} /> 29 28 <Route path="/:pds/:repo/blobs" component={BlobView} />
src/lib/firehose.ts src/utils/firehose.ts
-191
src/views/jetstream.tsx
··· 1 - import { createSignal, For, Show, onCleanup, onMount } from "solid-js"; 2 - import { JSONValue } from "../components/json"; 3 - import { action, useAction, useSearchParams } from "@solidjs/router"; 4 - 5 - const LIMIT = 25; 6 - type Parameter = { name: string; param: string | string[] | undefined }; 7 - 8 - const JetstreamView = () => { 9 - const [searchParams, setSearchParams] = useSearchParams(); 10 - const [parameters, setParameters] = createSignal<Parameter[]>([]); 11 - 12 - const [records, setRecords] = createSignal<Array<any>>([]); 13 - const [connected, setConnected] = createSignal(false); 14 - const [allEvents, setAllEvents] = createSignal(false); 15 - let socket: WebSocket; 16 - 17 - const connectSocket = action(async (formData: FormData) => { 18 - if (connected()) { 19 - socket?.close(); 20 - setConnected(false); 21 - return; 22 - } 23 - setRecords([]); 24 - 25 - let url = 26 - formData.get("instance")?.toString() ?? 27 - "wss://jetstream1.us-east.bsky.network/subscribe"; 28 - url = url.concat("?"); 29 - 30 - const collections = formData.get("collections")?.toString().split(","); 31 - collections?.forEach((collection) => { 32 - if (collection.length) 33 - url = url.concat(`wantedCollections=${collection}&`); 34 - }); 35 - 36 - const dids = formData.get("dids")?.toString().split(","); 37 - dids?.forEach((did) => { 38 - if (did.length) url = url.concat(`wantedDids=${did}&`); 39 - }); 40 - 41 - const cursor = formData.get("cursor")?.toString(); 42 - if (cursor?.length) url = url.concat(`cursor=${cursor}`); 43 - if (url.endsWith("&")) url = url.slice(0, -1); 44 - 45 - if (searchParams.allEvents === "on") setAllEvents(true); 46 - 47 - setSearchParams({ 48 - instance: formData.get("instance")?.toString(), 49 - collections: formData.get("collections")?.toString(), 50 - dids: formData.get("dids")?.toString(), 51 - cursor: formData.get("cursor")?.toString(), 52 - allEvents: formData.get("allEvents")?.toString(), 53 - }); 54 - 55 - setParameters([ 56 - { name: "Instance", param: formData.get("instance")?.toString() }, 57 - { name: "Collections", param: formData.get("collections")?.toString() }, 58 - { name: "DIDs", param: formData.get("dids")?.toString() }, 59 - { name: "Cursor", param: formData.get("cursor")?.toString() }, 60 - { name: "All Events", param: formData.get("allEvents")?.toString() }, 61 - ]); 62 - 63 - socket = new WebSocket(url); 64 - setConnected(true); 65 - socket.addEventListener("message", (event) => { 66 - const rec = JSON.parse(event.data); 67 - if (allEvents() || (rec.kind !== "account" && rec.kind !== "identity")) 68 - setRecords(records().concat(rec).slice(-LIMIT)); 69 - }); 70 - }); 71 - 72 - const connect = useAction(connectSocket); 73 - 74 - onMount(async () => { 75 - const formData = new FormData(); 76 - if (searchParams.instance) 77 - formData.append("instance", searchParams.instance.toString()); 78 - if (searchParams.collections) 79 - formData.append("collections", searchParams.collections.toString()); 80 - if (searchParams.dids) 81 - formData.append("dids", searchParams.dids.toString()); 82 - if (searchParams.cursor) 83 - formData.append("cursor", searchParams.cursor.toString()); 84 - if (searchParams.allEvents) 85 - formData.append("allEvents", searchParams.allEvents.toString()); 86 - if (searchParams.instance) connect(formData); 87 - }); 88 - 89 - onCleanup(() => socket?.close()); 90 - 91 - return ( 92 - <div class="mt-4 flex flex-col items-center gap-y-3"> 93 - <h1 class="text-lg font-bold">Jetstream Viewer</h1> 94 - <form method="post" action={connectSocket} class="flex flex-col gap-y-3"> 95 - <Show when={!connected()}> 96 - <label class="flex items-center justify-end gap-x-2"> 97 - <span class="">Instance</span> 98 - <input 99 - type="text" 100 - name="instance" 101 - spellcheck={false} 102 - value={ 103 - searchParams.instance ?? 104 - "wss://jetstream1.us-east.bsky.network/subscribe" 105 - } 106 - class="w-16rem dark:bg-dark-100 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300" 107 - /> 108 - </label> 109 - <label class="flex items-center justify-end gap-x-2"> 110 - <span class="">Collections</span> 111 - <textarea 112 - name="collections" 113 - spellcheck={false} 114 - placeholder="Comma-separated list of collections" 115 - value={searchParams.collections ?? ""} 116 - class="w-16rem dark:bg-dark-100 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300" 117 - /> 118 - </label> 119 - <label class="flex items-center justify-end gap-x-2"> 120 - <span class="">DIDs</span> 121 - <textarea 122 - name="dids" 123 - spellcheck={false} 124 - placeholder="Comma-separated list of DIDs" 125 - value={searchParams.dids ?? ""} 126 - class="w-16rem dark:bg-dark-100 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300" 127 - /> 128 - </label> 129 - <label class="flex items-center justify-end gap-x-2"> 130 - <span class="">Cursor</span> 131 - <input 132 - type="text" 133 - name="cursor" 134 - spellcheck={false} 135 - placeholder="Leave empty for live-tail" 136 - value={searchParams.cursor ?? ""} 137 - class="w-16rem dark:bg-dark-100 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300" 138 - /> 139 - </label> 140 - <div class="flex items-center justify-end gap-x-1"> 141 - <input 142 - type="checkbox" 143 - name="allEvents" 144 - id="allEvents" 145 - checked={searchParams.allEvents === "on" ? true : false} 146 - onChange={(e) => setAllEvents(e.currentTarget.checked)} 147 - /> 148 - <label for="allEvents" class="select-none"> 149 - Show account and identity events 150 - </label> 151 - </div> 152 - </Show> 153 - <Show when={connected()}> 154 - <div class="break-anywhere flex flex-col gap-1"> 155 - <For each={parameters()}> 156 - {(param) => ( 157 - <Show when={param.param}> 158 - <div class="flex"> 159 - <div class="min-w-6rem font-semibold text-stone-600 dark:text-stone-400"> 160 - {param.name} 161 - </div> 162 - {param.param} 163 - </div> 164 - </Show> 165 - )} 166 - </For> 167 - </div> 168 - </Show> 169 - <div class="flex justify-end"> 170 - <button 171 - type="submit" 172 - class="dark:bg-dark-700 dark:hover:bg-dark-800 w-fit rounded-lg border border-slate-400 bg-white px-2.5 py-1.5 text-sm font-bold hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-700 dark:focus:ring-slate-300" 173 - > 174 - {connected() ? "Disconnect" : "Connect"} 175 - </button> 176 - </div> 177 - </form> 178 - <div class="break-anywhere md:w-screen-md flex h-screen w-full flex-col gap-2 divide-y divide-neutral-500 overflow-auto whitespace-pre-wrap font-mono text-sm"> 179 - <For each={records().toReversed()}> 180 - {(rec) => ( 181 - <div class="pt-2"> 182 - <JSONValue data={rec} repo={rec.did} /> 183 - </div> 184 - )} 185 - </For> 186 - </div> 187 - </div> 188 - ); 189 - }; 190 - 191 - export { JetstreamView };
+264
src/views/stream.tsx
··· 1 + import { createSignal, For, Show, onCleanup, onMount } from "solid-js"; 2 + import { JSONValue } from "../components/json"; 3 + import { A, useLocation, useSearchParams } from "@solidjs/router"; 4 + import { Firehose } from "../utils/firehose"; 5 + 6 + const LIMIT = 25; 7 + type Parameter = { name: string; param: string | string[] | undefined }; 8 + enum StreamType { 9 + JETSTREAM, 10 + FIREHOSE, 11 + } 12 + 13 + const StreamView = () => { 14 + const [searchParams, setSearchParams] = useSearchParams(); 15 + const [parameters, setParameters] = createSignal<Parameter[]>([]); 16 + const streamType = 17 + useLocation().pathname === "/firehose" ? 18 + StreamType.FIREHOSE 19 + : StreamType.JETSTREAM; 20 + 21 + const [records, setRecords] = createSignal<Array<any>>([]); 22 + const [connected, setConnected] = createSignal(false); 23 + const [allEvents, setAllEvents] = createSignal(false); 24 + let socket: WebSocket; 25 + let firehose: Firehose; 26 + let formRef!: HTMLFormElement; 27 + 28 + const connectSocket = async (formData: FormData) => { 29 + if (connected()) { 30 + if (streamType === StreamType.JETSTREAM) socket?.close(); 31 + else firehose?.close(); 32 + setConnected(false); 33 + return; 34 + } 35 + setRecords([]); 36 + 37 + let url = ""; 38 + if (streamType === StreamType.JETSTREAM) { 39 + url = 40 + formData.get("instance")?.toString() ?? 41 + "wss://jetstream1.us-east.bsky.network/subscribe"; 42 + url = url.concat("?"); 43 + } else { 44 + url = formData.get("instance")?.toString() ?? "wss://bsky.network"; 45 + } 46 + 47 + const collections = formData.get("collections")?.toString().split(","); 48 + collections?.forEach((collection) => { 49 + if (collection.length) 50 + url = url.concat(`wantedCollections=${collection}&`); 51 + }); 52 + 53 + const dids = formData.get("dids")?.toString().split(","); 54 + dids?.forEach((did) => { 55 + if (did.length) url = url.concat(`wantedDids=${did}&`); 56 + }); 57 + 58 + const cursor = formData.get("cursor")?.toString(); 59 + if (streamType === StreamType.JETSTREAM) { 60 + if (cursor?.length) url = url.concat(`cursor=${cursor}`); 61 + if (url.endsWith("&")) url = url.slice(0, -1); 62 + } 63 + 64 + if (searchParams.allEvents === "on") setAllEvents(true); 65 + 66 + setSearchParams({ 67 + instance: formData.get("instance")?.toString(), 68 + collections: formData.get("collections")?.toString(), 69 + dids: formData.get("dids")?.toString(), 70 + cursor: formData.get("cursor")?.toString(), 71 + allEvents: formData.get("allEvents")?.toString(), 72 + }); 73 + 74 + setParameters([ 75 + { name: "Instance", param: formData.get("instance")?.toString() }, 76 + { name: "Collections", param: formData.get("collections")?.toString() }, 77 + { name: "DIDs", param: formData.get("dids")?.toString() }, 78 + { name: "Cursor", param: formData.get("cursor")?.toString() }, 79 + { name: "All Events", param: formData.get("allEvents")?.toString() }, 80 + ]); 81 + 82 + setConnected(true); 83 + if (streamType === StreamType.JETSTREAM) { 84 + socket = new WebSocket(url); 85 + socket.addEventListener("message", (event) => { 86 + const rec = JSON.parse(event.data); 87 + if (allEvents() || (rec.kind !== "account" && rec.kind !== "identity")) 88 + setRecords(records().concat(rec).slice(-LIMIT)); 89 + }); 90 + } else { 91 + firehose = new Firehose({ 92 + relay: url, 93 + cursor: cursor, 94 + }); 95 + firehose.on("error", (err) => { 96 + console.error(err); 97 + }); 98 + firehose.on("commit", (commit) => { 99 + for (const op of commit.ops) { 100 + const record = { 101 + $type: commit.$type, 102 + repo: commit.repo, 103 + seq: commit.seq, 104 + time: commit.time, 105 + rev: commit.rev, 106 + since: commit.since, 107 + tooBig: commit.tooBig, 108 + op: op, 109 + }; 110 + setRecords(records().concat(record).slice(-LIMIT)); 111 + } 112 + }); 113 + firehose.on("identity", (identity) => { 114 + setRecords(records().concat(identity).slice(-LIMIT)); 115 + }); 116 + firehose.on("account", (account) => { 117 + setRecords(records().concat(account).slice(-LIMIT)); 118 + }); 119 + firehose.start(); 120 + } 121 + }; 122 + 123 + onMount(async () => { 124 + const formData = new FormData(); 125 + if (searchParams.instance) 126 + formData.append("instance", searchParams.instance.toString()); 127 + if (searchParams.collections) 128 + formData.append("collections", searchParams.collections.toString()); 129 + if (searchParams.dids) 130 + formData.append("dids", searchParams.dids.toString()); 131 + if (searchParams.cursor) 132 + formData.append("cursor", searchParams.cursor.toString()); 133 + if (searchParams.allEvents) 134 + formData.append("allEvents", searchParams.allEvents.toString()); 135 + if (searchParams.instance) connectSocket(formData); 136 + }); 137 + 138 + onCleanup(() => socket?.close()); 139 + 140 + return ( 141 + <div class="mt-4 flex flex-col items-center gap-y-3"> 142 + <div class="flex divide-x-2 text-lg font-bold"> 143 + <A 144 + class="pr-2" 145 + inactiveClass="text-lightblue-500 hover:underline" 146 + href="/jetstream" 147 + > 148 + Jetstream 149 + </A> 150 + <A 151 + class="pl-2" 152 + inactiveClass="text-lightblue-500 hover:underline" 153 + href="/firehose" 154 + > 155 + Firehose 156 + </A> 157 + </div> 158 + <form ref={formRef} class="flex flex-col gap-y-3"> 159 + <Show when={!connected()}> 160 + <label class="flex items-center justify-end gap-x-2"> 161 + <span class="">Instance</span> 162 + <input 163 + type="text" 164 + name="instance" 165 + spellcheck={false} 166 + value={ 167 + searchParams.instance ?? 168 + (streamType === StreamType.JETSTREAM ? 169 + "wss://jetstream1.us-east.bsky.network/subscribe" 170 + : "wss://bsky.network") 171 + } 172 + class="w-16rem dark:bg-dark-100 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300" 173 + /> 174 + </label> 175 + <Show when={streamType === StreamType.JETSTREAM}> 176 + <label class="flex items-center justify-end gap-x-2"> 177 + <span class="">Collections</span> 178 + <textarea 179 + name="collections" 180 + spellcheck={false} 181 + placeholder="Comma-separated list of collections" 182 + value={searchParams.collections ?? ""} 183 + class="w-16rem dark:bg-dark-100 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300" 184 + /> 185 + </label> 186 + </Show> 187 + <Show when={streamType === StreamType.JETSTREAM}> 188 + <label class="flex items-center justify-end gap-x-2"> 189 + <span class="">DIDs</span> 190 + <textarea 191 + name="dids" 192 + spellcheck={false} 193 + placeholder="Comma-separated list of DIDs" 194 + value={searchParams.dids ?? ""} 195 + class="w-16rem dark:bg-dark-100 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300" 196 + /> 197 + </label> 198 + </Show> 199 + <label class="flex items-center justify-end gap-x-2"> 200 + <span class="">Cursor</span> 201 + <input 202 + type="text" 203 + name="cursor" 204 + spellcheck={false} 205 + placeholder="Leave empty for live-tail" 206 + value={searchParams.cursor ?? ""} 207 + class="w-16rem dark:bg-dark-100 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300" 208 + /> 209 + </label> 210 + <Show when={streamType === StreamType.JETSTREAM}> 211 + <div class="flex items-center justify-end gap-x-1"> 212 + <input 213 + type="checkbox" 214 + name="allEvents" 215 + id="allEvents" 216 + checked={searchParams.allEvents === "on" ? true : false} 217 + onChange={(e) => setAllEvents(e.currentTarget.checked)} 218 + /> 219 + <label for="allEvents" class="select-none"> 220 + Show account and identity events 221 + </label> 222 + </div> 223 + </Show> 224 + </Show> 225 + <Show when={connected()}> 226 + <div class="break-anywhere flex flex-col gap-1"> 227 + <For each={parameters()}> 228 + {(param) => ( 229 + <Show when={param.param}> 230 + <div class="flex"> 231 + <div class="min-w-6rem font-semibold text-stone-600 dark:text-stone-400"> 232 + {param.name} 233 + </div> 234 + {param.param} 235 + </div> 236 + </Show> 237 + )} 238 + </For> 239 + </div> 240 + </Show> 241 + <div class="flex justify-end"> 242 + <button 243 + type="button" 244 + onclick={() => connectSocket(new FormData(formRef))} 245 + class="dark:bg-dark-700 dark:hover:bg-dark-800 w-fit rounded-lg border border-slate-400 bg-white px-2.5 py-1.5 text-sm font-bold hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-700 dark:focus:ring-slate-300" 246 + > 247 + {connected() ? "Disconnect" : "Connect"} 248 + </button> 249 + </div> 250 + </form> 251 + <div class="break-anywhere md:w-screen-md flex h-screen w-full flex-col gap-2 divide-y divide-neutral-500 overflow-auto whitespace-pre-wrap font-mono text-sm"> 252 + <For each={records().toReversed()}> 253 + {(rec) => ( 254 + <div class="pt-2"> 255 + <JSONValue data={rec} repo={rec.did ?? rec.repo} /> 256 + </div> 257 + )} 258 + </For> 259 + </div> 260 + </div> 261 + ); 262 + }; 263 + 264 + export { StreamView };
-147
src/views/subscribeRepos.tsx
··· 1 - import { createSignal, For, Show, onCleanup, onMount } from "solid-js"; 2 - import { JSONValue } from "../components/json"; 3 - import { action, useAction, useSearchParams } from "@solidjs/router"; 4 - import { Firehose } from "../lib/firehose"; 5 - 6 - const LIMIT = 25; 7 - type Parameter = { name: string; param: string | string[] | undefined }; 8 - 9 - const SubscribeReposView = () => { 10 - const [searchParams, setSearchParams] = useSearchParams(); 11 - const [parameters, setParameters] = createSignal<Parameter[]>([]); 12 - const [records, setRecords] = createSignal<Array<any>>([]); 13 - const [connected, setConnected] = createSignal(false); 14 - let firehose: Firehose; 15 - 16 - const connectSocket = action(async (formData: FormData) => { 17 - if (connected()) { 18 - firehose?.close(); 19 - setConnected(false); 20 - return; 21 - } 22 - setRecords([]); 23 - 24 - const url = formData.get("instance")?.toString() ?? "wss://bsky.network"; 25 - const cursor = formData.get("cursor")?.toString(); 26 - 27 - setSearchParams({ 28 - instance: formData.get("instance")?.toString(), 29 - cursor: formData.get("cursor")?.toString(), 30 - }); 31 - 32 - setParameters([ 33 - { name: "Instance", param: formData.get("instance")?.toString() }, 34 - { name: "Cursor", param: formData.get("cursor")?.toString() }, 35 - ]); 36 - 37 - setConnected(true); 38 - firehose = new Firehose({ 39 - relay: url, 40 - cursor: cursor, 41 - }); 42 - firehose.on("error", (err) => { 43 - console.error(err); 44 - }); 45 - firehose.on("commit", (commit) => { 46 - for (const op of commit.ops) { 47 - const record = { 48 - $type: commit.$type, 49 - repo: commit.repo, 50 - seq: commit.seq, 51 - time: commit.time, 52 - rev: commit.rev, 53 - since: commit.since, 54 - tooBig: commit.tooBig, 55 - op: op, 56 - }; 57 - setRecords(records().concat(record).slice(-LIMIT)); 58 - } 59 - }); 60 - firehose.on("identity", (identity) => { 61 - setRecords(records().concat(identity).slice(-LIMIT)); 62 - }); 63 - firehose.on("account", (account) => { 64 - setRecords(records().concat(account).slice(-LIMIT)); 65 - }); 66 - firehose.start(); 67 - }); 68 - 69 - const connect = useAction(connectSocket); 70 - 71 - onMount(async () => { 72 - const formData = new FormData(); 73 - if (searchParams.instance) 74 - formData.append("instance", searchParams.instance.toString()); 75 - if (searchParams.cursor) 76 - formData.append("cursor", searchParams.cursor.toString()); 77 - if (searchParams.instance) connect(formData); 78 - }); 79 - 80 - onCleanup(() => firehose?.close()); 81 - 82 - return ( 83 - <div class="mt-4 flex flex-col items-center gap-y-3"> 84 - <h1 class="text-lg font-bold">com.atproto.sync.subscribeRepos</h1> 85 - <form method="post" action={connectSocket} class="flex flex-col gap-y-3"> 86 - <Show when={!connected()}> 87 - <label class="flex items-center justify-end gap-x-2"> 88 - <span class="">Instance</span> 89 - <input 90 - type="text" 91 - name="instance" 92 - spellcheck={false} 93 - value={searchParams.instance ?? "wss://bsky.network"} 94 - class="w-16rem dark:bg-dark-100 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300" 95 - /> 96 - </label> 97 - <label class="flex items-center justify-end gap-x-2"> 98 - <span class="">Cursor</span> 99 - <input 100 - type="text" 101 - name="cursor" 102 - spellcheck={false} 103 - placeholder="Leave empty for live-tail" 104 - value={searchParams.cursor ?? ""} 105 - class="w-16rem dark:bg-dark-100 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300" 106 - /> 107 - </label> 108 - </Show> 109 - <Show when={connected()}> 110 - <div class="break-anywhere flex flex-col gap-1"> 111 - <For each={parameters()}> 112 - {(param) => ( 113 - <Show when={param.param}> 114 - <div class="flex"> 115 - <div class="min-w-6rem font-semibold text-stone-600 dark:text-stone-400"> 116 - {param.name} 117 - </div> 118 - {param.param} 119 - </div> 120 - </Show> 121 - )} 122 - </For> 123 - </div> 124 - </Show> 125 - <div class="flex justify-end"> 126 - <button 127 - type="submit" 128 - class="dark:bg-dark-700 dark:hover:bg-dark-800 w-fit rounded-lg border border-slate-400 bg-white px-2.5 py-1.5 text-sm font-bold hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-700 dark:focus:ring-slate-300" 129 - > 130 - {connected() ? "Disconnect" : "Connect"} 131 - </button> 132 - </div> 133 - </form> 134 - <div class="break-anywhere md:w-screen-md flex h-screen w-full flex-col gap-2 divide-y divide-neutral-500 overflow-auto whitespace-pre-wrap font-mono text-sm"> 135 - <For each={records().toReversed()}> 136 - {(rec) => ( 137 - <div class="pt-2"> 138 - <JSONValue data={rec} repo={rec.did ?? rec.repo} /> 139 - </div> 140 - )} 141 - </For> 142 - </div> 143 - </div> 144 - ); 145 - }; 146 - 147 - export { SubscribeReposView };