Files
lume/apps/desktop/src/routes/new/post.tsx
2023-12-25 14:28:39 +07:00

174 lines
5.3 KiB
TypeScript

import { MentionNote, useArk, useSuggestion, useWidget } from "@lume/ark";
import { CancelIcon, LoaderIcon } from "@lume/icons";
import { WIDGET_KIND } from "@lume/utils";
import { NDKKind } from "@nostr-dev-kit/ndk";
import CharacterCount from "@tiptap/extension-character-count";
import Image from "@tiptap/extension-image";
import Mention from "@tiptap/extension-mention";
import Placeholder from "@tiptap/extension-placeholder";
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { convert } from "html-to-text";
import { nip19 } from "nostr-tools";
import { useLayoutEffect, useRef, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { toast } from "sonner";
import { MediaUploader, MentionPopup } from "./components";
export function NewPostScreen() {
const ark = useArk();
const { addWidget } = useWidget();
const { suggestion } = useSuggestion();
const [loading, setLoading] = useState(false);
const [height, setHeight] = useState(0);
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const containerRef = useRef(null);
const editor = useEditor({
extensions: [
StarterKit.configure(),
Placeholder.configure({ placeholder: "Sharing some thoughts..." }),
Image.configure({
HTMLAttributes: {
class:
"rounded-lg w-full object-cover h-auto max-h-[400px] border border-neutral-200 dark:border-neutral-800 outline outline-1 outline-offset-0 outline-neutral-300 dark:outline-neutral-700",
},
}),
CharacterCount.configure(),
Mention.configure({
suggestion,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
renderLabel({ options, node }) {
const npub = nip19.npubEncode(node.attrs.id);
return `nostr:${npub}`;
},
}),
],
content: JSON.parse(localStorage.getItem("editor-post") || "{}"),
editorProps: {
attributes: {
class:
"outline-none prose prose-lg prose-neutral max-w-none select-text whitespace-pre-line break-words dark:prose-invert hover:prose-a:text-blue-500",
},
},
onUpdate: ({ editor }) => {
const jsonContent = JSON.stringify(editor.getJSON());
localStorage.setItem("editor-post", jsonContent);
},
});
const submit = async () => {
try {
if (!ark.ndk.signer) return navigate("/new/privkey");
setLoading(true);
// get plaintext content
const html = editor.getHTML();
const serializedContent = convert(html, {
selectors: [
{ selector: "a", options: { linkBrackets: false } },
{ selector: "img", options: { linkBrackets: false } },
],
});
// add reply to tags if present
const replyTo = searchParams.get("replyTo");
const rootReplyTo = searchParams.get("rootReplyTo");
// publish event
const publish = await ark.createEvent({
kind: NDKKind.Text,
tags: [],
content: serializedContent,
replyTo,
rootReplyTo,
});
if (publish) {
toast.success(
`Broadcasted to ${publish.seens.length} relays successfully.`,
);
// update state
setLoading(false);
setSearchParams({});
// open new widget with this event id
if (!replyTo) {
addWidget.mutate({
title: "Thread",
content: publish.id,
kind: WIDGET_KIND.thread,
});
}
// reset editor
editor.commands.clearContent();
localStorage.setItem("editor-post", "{}");
}
} catch (e) {
setLoading(false);
toast.error(e);
}
};
useLayoutEffect(() => {
setHeight(containerRef.current.clientHeight);
}, []);
return (
<div className="flex h-[500px] flex-1 flex-col gap-4">
<div className="flex-1 overflow-y-auto">
<div ref={containerRef} style={{ height: `${height}px` }}>
<EditorContent
editor={editor}
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
/>
{searchParams.get("replyTo") && (
<div className="relative max-w-lg">
<MentionNote eventId={searchParams.get("replyTo")} />
<button
type="button"
onClick={() => setSearchParams({})}
className="absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center rounded bg-neutral-200 px-2 dark:bg-neutral-800"
>
<CancelIcon className="h-5 w-5" />
</button>
</div>
)}
</div>
</div>
<div className="inline-flex h-16 w-full items-center justify-between border-t border-neutral-100 bg-neutral-50 dark:border-neutral-900 dark:bg-neutral-950">
<span className="text-sm font-medium tabular-nums text-neutral-600 dark:text-neutral-400">
{editor?.storage?.characterCount.characters()} characters
</span>
<div className="flex items-center">
<div className="inline-flex items-center gap-2">
<MediaUploader editor={editor} />
<MentionPopup editor={editor} />
</div>
<div className="mx-3 h-6 w-px bg-neutral-200 dark:bg-neutral-800" />
<button
type="button"
onClick={() => submit()}
disabled={editor?.isEmpty}
className="inline-flex h-9 w-20 items-center justify-center rounded-lg bg-blue-500 px-2 font-medium text-white hover:bg-blue-600 disabled:opacity-50"
>
{loading === true ? (
<LoaderIcon className="h-5 w-5 animate-spin" />
) : (
"Post"
)}
</button>
</div>
</div>
</div>
);
}