import { isCid, isDid, isNsid, isResourceUri, Nsid } from "@atcute/lexicons/syntax"; import { A, useNavigate, useParams } from "@solidjs/router"; import { createContext, createEffect, createResource, createSignal, ErrorBoundary, For, on, Show, useContext, } from "solid-js"; import { resolveLexiconAuthority } from "../utils/api"; import { formatFileSize } from "../utils/format"; import { hideMedia } from "../views/settings"; import DidHoverCard from "./hover-card/did"; import RecordHoverCard from "./hover-card/record"; import { pds } from "./navbar"; import { addNotification, removeNotification } from "./notification"; import VideoPlayer from "./video-player"; interface JSONContext { repo: string; truncate?: boolean; parentIsBlob?: boolean; newTab?: boolean; hideBlobs?: boolean; } const JSONCtx = createContext(); const useJSONCtx = () => useContext(JSONCtx)!; interface AtBlob { $type: string; ref: { $link: string }; mimeType: string; size: number; } const isURL = URL.canParse ?? ((url, base) => { try { new URL(url, base); return true; } catch { return false; } }); const JSONString = (props: { data: string; isType?: boolean; isLink?: boolean }) => { const ctx = useJSONCtx(); const navigate = useNavigate(); const params = useParams(); const handleClick = async (lex: string) => { try { const [nsid, anchor] = lex.split("#"); const authority = await resolveLexiconAuthority(nsid as Nsid); const hash = anchor ? `#schema:${anchor}` : "#schema"; if (ctx.newTab) window.open(`/at://${authority}/com.atproto.lexicon.schema/${nsid}${hash}`, "_blank"); else navigate(`/at://${authority}/com.atproto.lexicon.schema/${nsid}${hash}`); } catch (err) { console.error("Failed to resolve lexicon authority:", err); const id = addNotification({ message: "Could not resolve schema", type: "error", }); setTimeout(() => removeNotification(id), 5000); } }; const MAX_LENGTH = 200; const isTruncated = () => ctx.truncate && props.data.length > MAX_LENGTH; const displayData = () => (isTruncated() ? props.data.slice(0, MAX_LENGTH) : props.data); const remainingChars = () => props.data.length - MAX_LENGTH; return ( " {(part) => ( <> {isResourceUri(part) ? : isDid(part) ? : isNsid(part.split("#")[0]) && props.isType ? : isCid(part) && props.isLink && ctx.parentIsBlob && params.repo ? {part} : ( isURL(part) && ["http:", "https:", "web+at:"].includes(new URL(part).protocol) && part.split("\n").length === 1 ) ? {part} : part} )} " (+{remainingChars().toLocaleString()}) ); }; const JSONNumber = ({ data, isSize }: { data: number; isSize?: boolean }) => { return ( {data} ({formatFileSize(data)}) ); }; const JSONBoolean = ({ data }: { data: boolean }) => { return {data ? "true" : "false"}; }; const JSONNull = () => { return null; }; const CollapsibleItem = (props: { label: string | number; value: JSONType; maxWidth?: string; isType?: boolean; isLink?: boolean; isSize?: boolean; parentIsBlob?: boolean; }) => { const ctx = useJSONCtx(); const [show, setShow] = createSignal(true); const isBlobContext = props.parentIsBlob ?? ctx.parentIsBlob; return ( ); }; const JSONObject = (props: { data: { [x: string]: JSONType } }) => { const ctx = useJSONCtx(); const params = useParams(); const [hide, setHide] = createSignal( localStorage.hideMedia === "true" || params.rkey === undefined, ); const [mediaLoaded, setMediaLoaded] = createSignal(false); createEffect(() => { if (hideMedia()) setHide(hideMedia()); }); createEffect( on( hide, (value) => { if (value === false) setMediaLoaded(false); }, { defer: true }, ), ); const isBlob = props.data.$type === "blob"; const isBlobContext = isBlob || ctx.parentIsBlob; const rawObj = ( {([key, value]) => ( )} ); const blob: AtBlob = props.data as any; const canShowMedia = () => pds() && !ctx.hideBlobs && (blob.mimeType.startsWith("image/") || blob.mimeType === "video/mp4"); const MediaDisplay = () => { const [imageUrl] = createResource( () => (blob.mimeType.startsWith("image/") ? blob.ref.$link : null), async (cid) => { const url = `https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${ctx.repo}&cid=${cid}`; await new Promise((resolve) => { const img = new Image(); img.src = url; img.onload = () => resolve(); img.onerror = () => resolve(); }); return url; }, ); return (
} > setMediaLoaded(true)} /> Failed to load video}> setMediaLoaded(true)} /> ); }; if (blob.$type === "blob") { return ( <> {rawObj} ); } return rawObj; }; const JSONArray = (props: { data: JSONType[] }) => { return ( {(value, index) => } ); }; const JSONValueInner = (props: { data: JSONType; isType?: boolean; isLink?: boolean; isSize?: boolean; }) => { const data = props.data; if (typeof data === "string") return ; if (typeof data === "number") return ; if (typeof data === "boolean") return ; if (data === null) return ; if (Array.isArray(data)) return ; return ; }; export const JSONValue = (props: { data: JSONType; repo: string; truncate?: boolean; newTab?: boolean; hideBlobs?: boolean; }) => { return ( ); }; export type JSONType = string | number | boolean | null | { [x: string]: JSONType } | JSONType[];