From f14799d5ae2278ec007064a0f3ac29f6918e4c1b Mon Sep 17 00:00:00 2001 From: Juliet Date: Sun, 14 Dec 2025 16:26:13 +0100 Subject: [PATCH] add did insertion --- src/components/create/file-upload.tsx | 109 ++++++++++++ src/components/create/handle-input.tsx | 87 ++++++++++ .../{create.tsx => create/index.tsx} | 155 ++++-------------- src/components/create/menu-item.tsx | 12 ++ src/components/create/state.ts | 4 + src/components/editor.tsx | 2 +- src/layout.tsx | 2 +- src/views/record.tsx | 2 +- 8 files changed, 249 insertions(+), 124 deletions(-) create mode 100644 src/components/create/file-upload.tsx create mode 100644 src/components/create/handle-input.tsx rename src/components/{create.tsx => create/index.tsx} (77%) create mode 100644 src/components/create/menu-item.tsx create mode 100644 src/components/create/state.ts diff --git a/src/components/create/file-upload.tsx b/src/components/create/file-upload.tsx new file mode 100644 index 0000000..bb1a0c9 --- /dev/null +++ b/src/components/create/file-upload.tsx @@ -0,0 +1,109 @@ +import { Client } from "@atcute/client"; +import { remove } from "@mary/exif-rm"; +import { createSignal, onCleanup, Show } from "solid-js"; +import { agent } from "../../auth/state"; +import { Button } from "../button.jsx"; +import { TextInput } from "../text-input.jsx"; +import { editorInstance } from "./state"; + +export const FileUpload = (props: { + file: File; + blobInput: HTMLInputElement; + onClose: () => void; +}) => { + const [uploading, setUploading] = createSignal(false); + const [error, setError] = createSignal(""); + + onCleanup(() => (props.blobInput.value = "")); + + const formatFileSize = (bytes: number) => { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]; + }; + + const uploadBlob = async () => { + let blob: Blob; + + const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value; + (document.getElementById("mimetype") as HTMLInputElement).value = ""; + if (mimetype) blob = new Blob([props.file], { type: mimetype }); + else blob = props.file; + + if ((document.getElementById("exif-rm") as HTMLInputElement).checked) { + const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer())); + if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type }); + } + + const rpc = new Client({ handler: agent()! }); + setUploading(true); + const res = await rpc.post("com.atproto.repo.uploadBlob", { + input: blob, + }); + setUploading(false); + if (!res.ok) { + setError(res.data.error); + return; + } + editorInstance.view.dispatch({ + changes: { + from: editorInstance.view.state.selection.main.head, + insert: JSON.stringify(res.data.blob, null, 2), + }, + }); + props.onClose(); + }; + + return ( +
+

Upload blob

+
+
+

+ {props.file.name} + + ({formatFileSize(props.file.size)}) + +

+
+
+ + +
+
+ + +
+

+ Metadata will be pasted after the cursor +

+ + Error: {error()} + +
+ + +
+ + Uploading +
+
+ + + +
+
+
+ ); +}; diff --git a/src/components/create/handle-input.tsx b/src/components/create/handle-input.tsx new file mode 100644 index 0000000..41de730 --- /dev/null +++ b/src/components/create/handle-input.tsx @@ -0,0 +1,87 @@ +import { Handle } from "@atcute/lexicons"; +import { createSignal, Show } from "solid-js"; +import { resolveHandle } from "../../utils/api"; +import { Button } from "../button.jsx"; +import { TextInput } from "../text-input.jsx"; +import { editorInstance } from "./state"; + +export const HandleInput = (props: { onClose: () => void }) => { + const [resolving, setResolving] = createSignal(false); + const [error, setError] = createSignal(""); + let handleFormRef!: HTMLFormElement; + + const resolveDid = async (e: SubmitEvent) => { + e.preventDefault(); + const formData = new FormData(handleFormRef); + const handleValue = formData.get("handle")?.toString().trim(); + + if (!handleValue) { + setError("Please enter a handle"); + return; + } + + setResolving(true); + setError(""); + try { + const did = await resolveHandle(handleValue as Handle); + editorInstance.view.dispatch({ + changes: { + from: editorInstance.view.state.selection.main.head, + insert: `"${did}"`, + }, + }); + props.onClose(); + handleFormRef.reset(); + } catch (err: any) { + setError(err.message || "Failed to resolve handle"); + } finally { + setResolving(false); + } + }; + + return ( +
+

Insert DID from handle

+
+
+ + +
+

+ DID will be pasted after the cursor +

+ + Error: {error()} + +
+ + +
+ + Resolving +
+
+ + + +
+
+
+ ); +}; diff --git a/src/components/create.tsx b/src/components/create/index.tsx similarity index 77% rename from src/components/create.tsx rename to src/components/create/index.tsx index c412d5e..0149d10 100644 --- a/src/components/create.tsx +++ b/src/components/create/index.tsx @@ -2,7 +2,6 @@ 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 { remove } from "@mary/exif-rm"; import { useNavigate, useParams } from "@solidjs/router"; import { createEffect, @@ -14,18 +13,21 @@ import { 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 { 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("../components/editor.jsx").then((m) => ({ default: m.Editor }))); +const Editor = lazy(() => import("../editor.jsx").then((m) => ({ default: m.Editor }))); -export const editorInstance = { view: null as any }; -export const [placeholder, setPlaceholder] = createSignal(); +export { editorInstance, placeholder, setPlaceholder }; export const RecordEditor = (props: { create: boolean; record?: any; refetch?: any }) => { const navigate = useNavigate(); @@ -34,6 +36,7 @@ export const RecordEditor = (props: { create: boolean; record?: any; refetch?: a 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); @@ -225,115 +228,9 @@ export const RecordEditor = (props: { create: boolean; record?: any; refetch?: a setOpenInsertMenu(false); }; - const MenuItem = (props: { icon: string; label: string; onClick: () => void }) => { - return ( - - ); - }; - - const FileUpload = (props: { file: File }) => { - const [uploading, setUploading] = createSignal(false); - const [error, setError] = createSignal(""); - - onCleanup(() => (blobInput.value = "")); - - const formatFileSize = (bytes: number) => { - if (bytes === 0) return "0 Bytes"; - const k = 1024; - const sizes = ["Bytes", "KB", "MB", "GB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]; - }; - - const uploadBlob = async () => { - let blob: Blob; - - const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value; - (document.getElementById("mimetype") as HTMLInputElement).value = ""; - if (mimetype) blob = new Blob([props.file], { type: mimetype }); - else blob = props.file; - - if ((document.getElementById("exif-rm") as HTMLInputElement).checked) { - const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer())); - if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type }); - } - - const rpc = new Client({ handler: agent()! }); - setUploading(true); - const res = await rpc.post("com.atproto.repo.uploadBlob", { - input: blob, - }); - setUploading(false); - if (!res.ok) { - setError(res.data.error); - return; - } - editorInstance.view.dispatch({ - changes: { - from: editorInstance.view.state.selection.main.head, - insert: JSON.stringify(res.data.blob, null, 2), - }, - }); - setOpenUpload(false); - }; - - return ( -
-

Upload blob

-
-
-

- {props.file.name} - - ({formatFileSize(props.file.size)}) - -

-
-
- - -
-
- - -
-

- Metadata will be pasted after the cursor -

- - Error: {error()} - -
- - -
- - Uploading -
-
- - - -
-
-
- ); + const insertDidFromHandle = () => { + setOpenInsertMenu(false); + setOpenHandleDialog(true); }; return ( @@ -471,6 +368,11 @@ export const RecordEditor = (props: { create: boolean; record?: any; refetch?: a
+ setOpenUpload(false)} closeOnClick={false} > - + setOpenUpload(false)} + /> + + setOpenHandleDialog(false)} + closeOnClick={false} + > + setOpenHandleDialog(false)} />
+ ); +}; diff --git a/src/components/create/state.ts b/src/components/create/state.ts new file mode 100644 index 0000000..584c74c --- /dev/null +++ b/src/components/create/state.ts @@ -0,0 +1,4 @@ +import { createSignal } from "solid-js"; + +export const editorInstance = { view: null as any }; +export const [placeholder, setPlaceholder] = createSignal(); diff --git a/src/components/editor.tsx b/src/components/editor.tsx index 1e0dab5..74d5685 100644 --- a/src/components/editor.tsx +++ b/src/components/editor.tsx @@ -7,7 +7,7 @@ import { basicDark } from "@fsegurai/codemirror-theme-basic-dark"; import { basicLight } from "@fsegurai/codemirror-theme-basic-light"; import { basicSetup, EditorView } from "codemirror"; import { onCleanup, onMount } from "solid-js"; -import { editorInstance } from "./create"; +import { editorInstance } from "./create/state"; const Editor = (props: { content: string }) => { let editorDiv!: HTMLDivElement; diff --git a/src/layout.tsx b/src/layout.tsx index dfd19c0..ae612d9 100644 --- a/src/layout.tsx +++ b/src/layout.tsx @@ -5,7 +5,7 @@ import { createEffect, ErrorBoundary, onCleanup, onMount, Show, Suspense } from import { AccountManager } from "./auth/account.jsx"; import { hasUserScope } from "./auth/scope-utils"; import { agent } from "./auth/state.js"; -import { RecordEditor } from "./components/create.jsx"; +import { RecordEditor } from "./components/create"; import { DropdownMenu, MenuProvider, MenuSeparator, NavMenu } from "./components/dropdown.jsx"; import { NavBar } from "./components/navbar.jsx"; import { NotificationContainer } from "./components/notification.jsx"; diff --git a/src/views/record.tsx b/src/views/record.tsx index 41c454a..44ec068 100644 --- a/src/views/record.tsx +++ b/src/views/record.tsx @@ -12,7 +12,7 @@ import { hasUserScope } from "../auth/scope-utils"; import { agent } from "../auth/state"; import { Backlinks } from "../components/backlinks.jsx"; import { Button } from "../components/button.jsx"; -import { RecordEditor, setPlaceholder } from "../components/create.jsx"; +import { RecordEditor, setPlaceholder } from "../components/create"; import { CopyMenu, DropdownMenu, -- 2.51.2