this repo has no description
0
fork

Configure Feed

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

at main 209 lines 8.7 kB view raw
1import { CredentialManager, Client } from "@atcute/client"; 2 3import { useNavigate, useParams } from "@solidjs/router"; 4import { createSignal, onMount, Show } from "solid-js"; 5 6import { Backlinks } from "../components/backlinks.jsx"; 7import { JSONValue } from "../components/json.jsx"; 8import { agent } from "../components/login.jsx"; 9import { pds, setCID, setValidRecord, setValidSchema, validRecord } from "../components/navbar.jsx"; 10 11import { didDocCache, getAllBacklinks, LinkData, resolvePDS } from "../utils/api.js"; 12import { AtUri, uriTemplates } from "../utils/templates.js"; 13import { verifyRecord } from "../utils/verify.js"; 14import { ActorIdentifier, InferXRPCBodyOutput, is } from "@atcute/lexicons"; 15import { lexiconDoc } from "@atcute/lexicon-doc"; 16import { ComAtprotoRepoGetRecord } from "@atcute/atproto"; 17import { lexicons } from "../utils/types/lexicons.js"; 18import { RecordEditor } from "../components/create.jsx"; 19import { addToClipboard } from "../utils/copy.js"; 20import Tooltip from "../components/tooltip.jsx"; 21import { Modal } from "../components/modal.jsx"; 22 23export const RecordView = () => { 24 const navigate = useNavigate(); 25 const params = useParams(); 26 const [record, setRecord] = 27 createSignal<InferXRPCBodyOutput<ComAtprotoRepoGetRecord.mainSchema["output"]>>(); 28 const [backlinks, setBacklinks] = createSignal<{ links: LinkData; target: string }>(); 29 const [openDelete, setOpenDelete] = createSignal(false); 30 const [notice, setNotice] = createSignal(""); 31 const [showBacklinks, setShowBacklinks] = createSignal(false); 32 const [externalLink, setExternalLink] = createSignal< 33 { label: string; link: string; icon?: string } | undefined 34 >(); 35 const did = params.repo; 36 let rpc: Client; 37 38 onMount(async () => { 39 setCID(undefined); 40 setValidRecord(undefined); 41 setValidSchema(undefined); 42 const pds = await resolvePDS(did); 43 rpc = new Client({ handler: new CredentialManager({ service: pds }) }); 44 const res = await rpc.get("com.atproto.repo.getRecord", { 45 params: { 46 repo: did as ActorIdentifier, 47 collection: params.collection as `${string}.${string}.${string}`, 48 rkey: params.rkey, 49 }, 50 }); 51 if (!res.ok) { 52 setValidRecord(false); 53 setNotice(res.data.error); 54 throw new Error(res.data.error); 55 } 56 setRecord(res.data); 57 setCID(res.data.cid); 58 setExternalLink(checkUri(res.data.uri)); 59 60 try { 61 if (params.collection in lexicons) { 62 if (is(lexicons[params.collection], res.data.value)) setValidSchema(true); 63 else setValidSchema(false); 64 } else if (params.collection === "com.atproto.lexicon.schema") { 65 try { 66 lexiconDoc.parse(res.data.value, { mode: "passthrough" }); 67 setValidSchema(true); 68 } catch (e) { 69 console.error(e); 70 setValidSchema(false); 71 } 72 } 73 const { errors } = await verifyRecord({ 74 rpc: rpc, 75 uri: res.data.uri, 76 cid: res.data.cid!, 77 record: res.data.value, 78 didDoc: didDocCache[res.data.uri.split("/")[2]], 79 }); 80 81 if (errors.length > 0) { 82 console.warn(errors); 83 setNotice(`Invalid record: ${errors.map((e) => e.message).join("\n")}`); 84 } 85 setValidRecord(errors.length === 0); 86 } catch (err) { 87 console.error(err); 88 setValidRecord(false); 89 } 90 if (localStorage.backlinks === "true") { 91 try { 92 const backlinkTarget = `at://${did}/${params.collection}/${params.rkey}`; 93 const backlinks = await getAllBacklinks(backlinkTarget); 94 setBacklinks({ links: backlinks.links, target: backlinkTarget }); 95 } catch (e) { 96 console.error(e); 97 } 98 } 99 }); 100 101 const deleteRecord = async () => { 102 rpc = new Client({ handler: agent()! }); 103 await rpc.post("com.atproto.repo.deleteRecord", { 104 input: { 105 repo: params.repo as ActorIdentifier, 106 collection: params.collection as `${string}.${string}.${string}`, 107 rkey: params.rkey, 108 }, 109 }); 110 navigate(`/at://${params.repo}/${params.collection}`); 111 }; 112 113 const checkUri = (uri: string) => { 114 const uriParts = uri.split("/"); // expected: ["at:", "", "repo", "collection", "rkey"] 115 if (uriParts.length != 5) return undefined; 116 if (uriParts[0] !== "at:" || uriParts[1] !== "") return undefined; 117 const parsedUri: AtUri = { repo: uriParts[2], collection: uriParts[3], rkey: uriParts[4] }; 118 const template = uriTemplates[parsedUri.collection]; 119 if (!template) return undefined; 120 return template(parsedUri); 121 }; 122 123 return ( 124 <div class="flex w-full flex-col items-center"> 125 <Show when={record() === undefined && validRecord() !== false}> 126 <div class="i-lucide-loader-circle mt-3 animate-spin text-xl" /> 127 </Show> 128 <Show when={validRecord() === false}> 129 <div class="mt-3 break-words text-red-500 dark:text-red-400">{notice()}</div> 130 </Show> 131 <Show when={record()}> 132 <div class="dark:shadow-dark-900/80 dark:bg-dark-300 my-3 flex gap-3 rounded-full bg-white px-2.5 py-2 shadow-sm"> 133 <Tooltip text="Copy record"> 134 <button onclick={() => addToClipboard(JSON.stringify(record()?.value, null, 2))}> 135 <div class="i-lucide-copy text-xl" /> 136 </button> 137 </Tooltip> 138 <Show when={agent() && agent()?.sub === record()?.uri.split("/")[2]}> 139 <RecordEditor create={false} record={record()?.value} /> 140 <div class="relative flex"> 141 <Tooltip text="Delete"> 142 <button onclick={() => setOpenDelete(true)}> 143 <div class="i-lucide-trash-2 text-xl" /> 144 </button> 145 </Tooltip> 146 <Modal open={openDelete()} onClose={() => setOpenDelete(false)}> 147 <div class="starting:opacity-0 dark:bg-dark-800/70 border-0.5 dark:shadow-dark-900/80 backdrop-blur-xs left-50% top-70 absolute -translate-x-1/2 rounded-md border-neutral-300 bg-zinc-200/70 p-4 text-slate-900 shadow-md transition-opacity duration-300 dark:border-neutral-700 dark:text-slate-100"> 148 <h2 class="mb-2 font-bold">Delete this record?</h2> 149 <div class="flex justify-end gap-2"> 150 <button 151 type="button" 152 onclick={() => setOpenDelete(false)} 153 class="dark:hover:bg-dark-100 dark:bg-dark-300 dark:shadow-dark-900/80 rounded-lg bg-white px-2 py-1.5 text-sm font-bold shadow-sm hover:bg-zinc-100" 154 > 155 Cancel 156 </button> 157 <button 158 type="button" 159 onclick={deleteRecord} 160 class="dark:shadow-dark-900/80 rounded-lg bg-red-500 px-2 py-1.5 text-sm font-bold text-slate-100 shadow-sm hover:bg-red-400" 161 > 162 Delete 163 </button> 164 </div> 165 </div> 166 </Modal> 167 </div> 168 </Show> 169 <Show when={externalLink()}> 170 {(externalLink) => ( 171 <Tooltip text={`Open on ${externalLink().label}`}> 172 <a target="_blank" href={externalLink()?.link}> 173 <div class={`${externalLink().icon ?? "i-lucide-app-window"} text-xl`} /> 174 </a> 175 </Tooltip> 176 )} 177 </Show> 178 <Tooltip text="Record on PDS"> 179 <a 180 href={`https://${pds()}/xrpc/com.atproto.repo.getRecord?repo=${params.repo}&collection=${params.collection}&rkey=${params.rkey}`} 181 target="_blank" 182 > 183 <div class="i-lucide-external-link text-xl" /> 184 </a> 185 </Tooltip> 186 <Show when={backlinks()}> 187 <Tooltip text={showBacklinks() ? "Show record" : "Show backlinks"}> 188 <button onclick={() => setShowBacklinks(!showBacklinks())}> 189 <div 190 class={`${showBacklinks() ? "i-lucide-file-json" : "i-lucide-send-to-back"} text-xl`} 191 /> 192 </button> 193 </Tooltip> 194 </Show> 195 </div> 196 <Show when={!showBacklinks()}> 197 <div class="break-anywhere w-full whitespace-pre-wrap font-mono text-xs sm:text-sm"> 198 <JSONValue data={record()?.value as any} repo={record()!.uri.split("/")[2]} /> 199 </div> 200 </Show> 201 <Show when={showBacklinks()}> 202 <Show when={backlinks()}> 203 {(backlinks) => <Backlinks links={backlinks().links} target={backlinks().target} />} 204 </Show> 205 </Show> 206 </Show> 207 </div> 208 ); 209};