import { MentionNote, useStorage } from "@lume/ark"; import { TrashIcon } from "@lume/icons"; import { NDKCacheUserProfile } from "@lume/types"; import { cn, editorValueAtom } from "@lume/utils"; import { useAtomValue } from "jotai"; import { useEffect, useRef, useState } from "react"; import { Editor, Range, Transforms, createEditor } from "slate"; import { Editable, ReactEditor, Slate, useFocused, useSelected, useSlateStatic, withReact, } from "slate-react"; import { User } from "../user"; 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 storage = useStorage(); const ref = useRef(); const initialValue = useAtomValue(editorValueAtom); const [contacts, setContacts] = useState([]); const [target, setTarget] = useState(); const [index, setIndex] = useState(0); const [search, setSearch] = useState(""); const [editor] = useState(() => withMentions(withNostrEvent(withImages(withReact(createEditor())))), ); const filters = contacts ?.filter((c) => c?.name?.toLowerCase().startsWith(search.toLowerCase())) ?.slice(0, 10); 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); }} >
} 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" >
))}
)}
); }