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}
);
};
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"
>
))}
)}
);
}