import { isCid, isDid, isNsid, isResourceUri, Nsid } from "@atcute/lexicons/syntax"; import { A, useLocation, useNavigate, useParams } from "@solidjs/router"; import { createContext, createEffect, createResource, createSignal, ErrorBoundary, For, onCleanup, Show, useContext, } from "solid-js"; import { Portal } from "solid-js/web"; import { resolveLexiconAuthority } from "../lib/api"; import { formatFileSize } from "../utils/format"; import { hideMedia } from "../views/settings"; import DidHoverCard from "./hover-card/did"; import RecordHoverCard from "./hover-card/record"; import { addNotification, removeNotification } from "./notification"; import VideoPlayer from "./video-player"; interface JSONContext { repo: string; pds?: string; truncate?: boolean; parentIsBlob?: boolean; newTab?: boolean; hideBlobs?: boolean; keyLinks?: boolean; path?: string; } 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 CollapsibleItem = (props: { label: string | number; value: JSONType; maxWidth?: string; isType?: boolean; isLink?: boolean; isSize?: boolean; isIndex?: boolean; parentIsBlob?: boolean; }) => { const ctx = useJSONCtx(); const location = useLocation(); const [show, setShow] = createSignal(true); const isBlobContext = props.parentIsBlob ?? ctx.parentIsBlob; const labelStr = () => { const l = String(props.label); return l.startsWith("#") ? l.slice(1) : l; }; const fullPath = () => (ctx.path ? `${ctx.path}.${labelStr()}` : labelStr()); const isHighlighted = () => location.hash === `#record:${fullPath()}`; createEffect(() => { if (isHighlighted()) { requestAnimationFrame(() => { document .getElementById(`key-${fullPath()}`) ?.scrollIntoView({ behavior: "instant", block: "center" }); }); } }); const isObject = () => props.value === Object(props.value); const isEmpty = () => Array.isArray(props.value) ? (props.value as JSONType[]).length === 0 : Object.keys(props.value as object).length === 0; const summary = () => { if (Array.isArray(props.value)) { const len = (props.value as JSONType[]).length; return `[ ${len} ${len === 1 ? "item" : "items"} ]`; } const len = Object.keys(props.value as object).length; return `{ ${len} ${len === 1 ? "key" : "keys"} }`; }; return ( {props.label} : } > {props.label} : setShow(!show())} > ); }; const JSONObject = (props: { data: { [x: string]: JSONType } }) => { const ctx = useJSONCtx(); const params = useParams(); const [hide, setHide] = createSignal( localStorage.hideMedia === "true" || params.rkey === undefined, ); createEffect(() => { if (hideMedia()) setHide(hideMedia()); }); const isBlob = props.data.$type === "blob"; const isBlobContext = isBlob || ctx.parentIsBlob; const rawObj = ( {([key, value]) => ( )} ); const blob: AtBlob = props.data as any; const canShowMedia = () => ctx.pds && !ctx.hideBlobs && (blob.mimeType.startsWith("image/") || blob.mimeType === "video/mp4" || blob.mimeType.startsWith("audio/")); const MediaDisplay = () => { const [expanded, setExpanded] = createSignal(false); const [closing, setClosing] = createSignal(false); const closeExpanded = () => { setClosing(true); setTimeout(() => { setExpanded(false); setClosing(false); }, 200); }; createEffect(() => { if (!expanded()) return; const handler = (e: KeyboardEvent) => { if (e.key === "Escape") closeExpanded(); }; window.addEventListener("keydown", handler); onCleanup(() => window.removeEventListener("keydown", handler)); }); const [imageUrl] = createResource( () => (blob.mimeType.startsWith("image/") ? blob.ref.$link : null), async (cid) => { const url = `${ctx.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 (
} > setExpanded(true)} />
Failed to load video}> ); }; if (Object.keys(props.data).length === 0) return {"{ }"}; if (blob.$type === "blob") { return ( <> {rawObj} ); } return rawObj; }; const JSONArray = (props: { data: JSONType[] }) => { if (props.data.length === 0) return [ ]; 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 {String(data)}; if (data === null) return null; if (Array.isArray(data)) return ; return ; }; export const JSONValue = (props: { data: JSONType; repo: string; pds?: string; truncate?: boolean; newTab?: boolean; hideBlobs?: boolean; keyLinks?: boolean; }) => { return ( ); }; export type JSONType = string | number | boolean | null | { [x: string]: JSONType } | JSONType[];