import { MentionNote, User, useArk, useColumnContext } from "@lume/ark"; import { LoaderIcon, TrashIcon } from "@lume/icons"; import { useStorage } from "@lume/storage"; import { NDKCacheUserProfile } from "@lume/types"; import { COL_TYPES, cn, editorValueAtom } from "@lume/utils"; import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; import { useAtom } from "jotai"; import { useEffect, useRef, useState } from "react"; import { Descendant, Editor, Node, Range, Transforms, createEditor, } from "slate"; import { Editable, ReactEditor, Slate, useFocused, useSelected, useSlateStatic, withReact, } from "slate-react"; import { toast } from "sonner"; import { EditorAddMedia } from "./addMedia"; import { Portal, insertImage, insertMention, insertNostrEvent, isImageUrl, } from "./utils"; const withNostrEvent = (editor: ReactEditor) => { const { insertData, isVoid } = editor; editor.isVoid = (element) => { // @ts-expect-error, wtf return element.type === "event" ? true : isVoid(element); }; editor.insertData = (data) => { const text = data.getData("text/plain"); if (text.startsWith("nevent1") || text.startsWith("note1")) { insertNostrEvent(editor, text); } else { insertData(data); } }; return editor; }; const withMentions = (editor: ReactEditor) => { const { isInline, isVoid, markableVoid } = editor; editor.isInline = (element) => { // @ts-expect-error, wtf return element.type === "mention" ? true : isInline(element); }; editor.isVoid = (element) => { // @ts-expect-error, wtf return element.type === "mention" ? true : isVoid(element); }; editor.markableVoid = (element) => { // @ts-expect-error, wtf return element.type === "mention" || markableVoid(element); }; return editor; }; const withImages = (editor: ReactEditor) => { const { insertData, isVoid } = editor; editor.isVoid = (element) => { // @ts-expect-error, wtf return element.type === "image" ? true : isVoid(element); }; editor.insertData = (data) => { const text = data.getData("text/plain"); if (isImageUrl(text)) { insertImage(editor, text); } else { insertData(data); } }; return editor; }; const Image = ({ attributes, children, element }) => { const editor = useSlateStatic(); const path = ReactEditor.findPath(editor as ReactEditor, element); const selected = useSelected(); const focused = useFocused(); return (
{children}
{element.url}
); }; const Mention = ({ attributes, element }) => { const editor = useSlateStatic(); const path = ReactEditor.findPath(editor as ReactEditor, element); return ( Transforms.removeNodes(editor, { at: path })} className="inline-block text-blue-500 align-baseline hover:text-blue-600" >{`@${element.name}`} ); }; const Event = ({ attributes, element, children }) => { const editor = useSlateStatic(); const path = ReactEditor.findPath(editor as ReactEditor, element); return (
{children} {/* biome-ignore lint/a11y/useKeyWithClickEvents: */}
Transforms.removeNodes(editor, { at: path })} className="relative user-select-none" >
); }; const Element = (props) => { const { attributes, children, element } = props; switch (element.type) { case "image": return ; case "mention": return ; case "event": return ; default: return (

{children}

); } }; export function EditorForm() { const ark = useArk(); const storage = useStorage(); const ref = useRef(); const [editorValue, setEditorValue] = useAtom(editorValueAtom); const [contacts, setContacts] = useState([]); const [target, setTarget] = useState(); const [index, setIndex] = useState(0); const [search, setSearch] = useState(""); const [loading, setLoading] = useState(false); const [editor] = useState(() => withMentions(withNostrEvent(withImages(withReact(createEditor())))), ); const { addColumn } = useColumnContext(); const filters = contacts ?.filter((c) => c?.name?.toLowerCase().startsWith(search.toLowerCase())) ?.slice(0, 10); const reset = () => { // @ts-expect-error, backlog editor.children = [{ type: "paragraph", children: [{ text: "" }] }]; setEditorValue([{ type: "paragraph", children: [{ text: "" }] }]); }; const serialize = (nodes: Descendant[]) => { return nodes .map((n) => { // @ts-expect-error, backlog if (n.type === "image") return n.url; // @ts-expect-error, backlog if (n.type === "event") return n.eventId; // @ts-expect-error, backlog if (n.children.length) { // @ts-expect-error, backlog return n.children .map((n) => { if (n.type === "mention") return n.npub; return Node.string(n).trim(); }) .join(" "); } return Node.string(n); }) .join("\n"); }; const submit = async () => { try { setLoading(true); const event = new NDKEvent(ark.ndk); event.kind = NDKKind.Text; event.content = serialize(editor.children); const publish = await event.publish(); if (publish) { toast.success( `Event has been published successfully to ${publish.size} relays.`, ); // add current post as column thread addColumn({ kind: COL_TYPES.thread, content: event.id, title: "Thread", }); setLoading(false); return reset(); } } catch (e) { setLoading(false); toast.error(String(e)); } }; useEffect(() => { async function loadContacts() { const res = await storage.getAllCacheUsers(); if (res) setContacts(res); } loadContacts(); }, []); useEffect(() => { if (target && filters.length > 0) { const el = ref.current; const domRange = ReactEditor.toDOMRange(editor, target); const rect = domRange.getBoundingClientRect(); el.style.top = `${rect.top + window.pageYOffset + 24}px`; el.style.left = `${rect.left + window.pageXOffset}px`; } }, [filters.length, editor, index, search, target]); return (
{ const { selection } = editor; if (selection && Range.isCollapsed(selection)) { const [start] = Range.edges(selection); const wordBefore = Editor.before(editor, start, { unit: "word" }); const before = wordBefore && Editor.before(editor, wordBefore); const beforeRange = before && Editor.range(editor, before, start); const beforeText = beforeRange && Editor.string(editor, beforeRange); const beforeMatch = beforeText?.match(/^@(\w+)$/); const after = Editor.after(editor, start); const afterRange = Editor.range(editor, start, after); const afterText = Editor.string(editor, afterRange); const afterMatch = afterText.match(/^(\s|$)/); if (beforeMatch && afterMatch) { setTarget(beforeRange); setSearch(beforeMatch[1]); setIndex(0); return; } } setTarget(null); }} >

New Post

} placeholder="What are you up to?" className="focus:outline-none" /> {target && filters.length > 0 && (
{filters.map((contact, i) => ( // biome-ignore lint/a11y/useKeyWithClickEvents:
{ Transforms.select(editor, target); insertMention(editor, contact); setTarget(null); }} className="px-2 py-2 rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-900" >
))}
)}
); }