import { Client } from "@atcute/client"; import { Did } from "@atcute/lexicons"; import { isNsid, isRecordKey } from "@atcute/lexicons/syntax"; import { getSession, OAuthUserAgent } from "@atcute/oauth-browser-client"; import { useNavigate, useParams } from "@solidjs/router"; import { createEffect, createSignal, For, lazy, onCleanup, onMount, Show, Suspense, } from "solid-js"; import { hasUserScope } from "../../auth/scope-utils"; import { agent, sessions } from "../../auth/state"; import { Button } from "../button.jsx"; import { Modal } from "../modal.jsx"; import { addNotification, removeNotification } from "../notification.jsx"; import { TextInput } from "../text-input.jsx"; import Tooltip from "../tooltip.jsx"; import { FileUpload } from "./file-upload"; import { HandleInput } from "./handle-input"; import { MenuItem } from "./menu-item"; import { editorInstance, placeholder, setPlaceholder } from "./state"; const Editor = lazy(() => import("../editor.jsx").then((m) => ({ default: m.Editor }))); export { editorInstance, placeholder, setPlaceholder }; export const RecordEditor = (props: { create: boolean; record?: any; refetch?: any }) => { const navigate = useNavigate(); const params = useParams(); const [openDialog, setOpenDialog] = createSignal(false); const [notice, setNotice] = createSignal(""); const [openUpload, setOpenUpload] = createSignal(false); const [openInsertMenu, setOpenInsertMenu] = createSignal(false); const [openHandleDialog, setOpenHandleDialog] = createSignal(false); const [validate, setValidate] = createSignal(undefined); const [isMaximized, setIsMaximized] = createSignal(false); const [isMinimized, setIsMinimized] = createSignal(false); const [collectionError, setCollectionError] = createSignal(""); const [rkeyError, setRkeyError] = createSignal(""); let blobInput!: HTMLInputElement; let formRef!: HTMLFormElement; let insertMenuRef!: HTMLDivElement; createEffect(() => { if (openInsertMenu()) { const handleClickOutside = (e: MouseEvent) => { if (insertMenuRef && !insertMenuRef.contains(e.target as Node)) { setOpenInsertMenu(false); } }; document.addEventListener("mousedown", handleClickOutside); onCleanup(() => document.removeEventListener("mousedown", handleClickOutside)); } }); onMount(() => { const keyEvent = (ev: KeyboardEvent) => { if (ev.target instanceof HTMLInputElement || ev.target instanceof HTMLTextAreaElement) return; if ((ev.target as HTMLElement).closest("[data-modal]")) return; const key = props.create ? "n" : "e"; if (ev.key === key) { ev.preventDefault(); if (openDialog() && isMinimized()) { setIsMinimized(false); } else if (!openDialog() && !document.querySelector("[data-modal]")) { setOpenDialog(true); } } }; window.addEventListener("keydown", keyEvent); onCleanup(() => window.removeEventListener("keydown", keyEvent)); }); const defaultPlaceholder = () => { return { $type: "app.bsky.feed.post", text: "This post was sent from PDSls", embed: { $type: "app.bsky.embed.external", external: { uri: "https://pdsls.dev", title: "PDSls", description: "Browse the public data on atproto", }, }, langs: ["en"], createdAt: new Date().toISOString(), }; }; const getValidateIcon = () => { return ( validate() === true ? "lucide--circle-check" : validate() === false ? "lucide--circle-x" : "lucide--circle" ); }; const getValidateLabel = () => { return ( validate() === true ? "True" : validate() === false ? "False" : "Unset" ); }; createEffect(() => { if (openDialog()) { setValidate(undefined); setCollectionError(""); setRkeyError(""); } }); const createRecord = async (formData: FormData) => { const repo = formData.get("repo")?.toString(); if (!repo) return; const rpc = new Client({ handler: new OAuthUserAgent(await getSession(repo as Did)) }); const collection = formData.get("collection"); const rkey = formData.get("rkey"); let record: any; try { record = JSON.parse(editorInstance.view.state.doc.toString()); } catch (e: any) { setNotice(e.message); return; } const res = await rpc.post("com.atproto.repo.createRecord", { input: { repo: repo as Did, collection: collection ? collection.toString() : record.$type, rkey: rkey?.toString().length ? rkey?.toString() : undefined, record: record, validate: validate(), }, }); if (!res.ok) { setNotice(`${res.data.error}: ${res.data.message}`); return; } setOpenDialog(false); const id = addNotification({ message: "Record created", type: "success", }); setTimeout(() => removeNotification(id), 3000); navigate(`/${res.data.uri}`); }; const editRecord = async (recreate?: boolean) => { const record = editorInstance.view.state.doc.toString(); if (!record) return; const rpc = new Client({ handler: agent()! }); try { const editedRecord = JSON.parse(record); if (recreate) { const res = await rpc.post("com.atproto.repo.applyWrites", { input: { repo: agent()!.sub, validate: validate(), writes: [ { collection: params.collection as `${string}.${string}.${string}`, rkey: params.rkey!, $type: "com.atproto.repo.applyWrites#delete", }, { collection: params.collection as `${string}.${string}.${string}`, rkey: params.rkey, $type: "com.atproto.repo.applyWrites#create", value: editedRecord, }, ], }, }); if (!res.ok) { setNotice(`${res.data.error}: ${res.data.message}`); return; } } else { const res = await rpc.post("com.atproto.repo.applyWrites", { input: { repo: agent()!.sub, validate: validate(), writes: [ { collection: params.collection as `${string}.${string}.${string}`, rkey: params.rkey!, $type: "com.atproto.repo.applyWrites#update", value: editedRecord, }, ], }, }); if (!res.ok) { setNotice(`${res.data.error}: ${res.data.message}`); return; } } setOpenDialog(false); const id = addNotification({ message: "Record edited", type: "success", }); setTimeout(() => removeNotification(id), 3000); props.refetch(); } catch (err: any) { setNotice(err.message); } }; const insertTimestamp = () => { const timestamp = new Date().toISOString(); editorInstance.view.dispatch({ changes: { from: editorInstance.view.state.selection.main.head, insert: `"${timestamp}"`, }, }); setOpenInsertMenu(false); }; const insertDidFromHandle = () => { setOpenInsertMenu(false); setOpenHandleDialog(true); }; return ( <> setOpenDialog(false)} closeOnClick={false} nonBlocking={isMinimized()} >
{props.create ? "Creating" : "Editing"} record
at:// / { const value = e.currentTarget.value; if (!value || isNsid(value)) setCollectionError(""); else setCollectionError( "Invalid collection: use reverse domain format (e.g. app.bsky.feed.post)", ); }} /> / { const value = e.currentTarget.value; if (!value || isRecordKey(value)) setRkeyError(""); else setRkeyError("Invalid record key: 1-512 chars, use a-z A-Z 0-9 . _ ~ : -"); }} />
{collectionError()}
{rkeyError()}
} >
{notice()}
{ setOpenInsertMenu(false); blobInput.click(); }} />
{ if (e.target.files !== null) setOpenUpload(true); }} />
setOpenUpload(false)} closeOnClick={false} > setOpenUpload(false)} /> setOpenHandleDialog(false)} closeOnClick={false} > setOpenHandleDialog(false)} />
); };