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 { useState } from 'react'; import { twMerge } from 'tailwind-merge'; import { Button } from '@shared/button'; import { Suggestion } from '@shared/composer'; import { CancelIcon, LoaderIcon, PlusCircleIcon } from '@shared/icons'; import { MentionNote } from '@shared/notes'; import { useComposer } from '@stores/composer'; import { useNostr } from '@utils/hooks/useNostr'; import { useImageUploader } from '@utils/hooks/useUploader'; import { sendNativeNotification } from '@utils/notification'; export function Composer() { const { publish } = useNostr(); const [status, setStatus] = useState(null); const [reply, clearReply] = useComposer((state) => [state.reply, state.clearReply]); const upload = useImageUploader(); const editor = useEditor({ extensions: [ StarterKit.configure({ dropcursor: { color: '#fff', }, }), Placeholder.configure({ placeholder: 'Type something...' }), Mention.configure({ suggestion: Suggestion, renderLabel({ node }) { return `nostr:${nip19.npubEncode(node.attrs.id.pubkey)} `; }, }), Image.configure({ HTMLAttributes: { class: 'rounded-lg w-2/3 h-auto border border-white/10 outline outline-2 outline-offset-0 outline-white/20 ml-1', }, }), ], content: '', editorProps: { attributes: { class: twMerge( 'scrollbar-hide markdown break-all max-h-[500px] overflow-y-auto outline-none pr-2', `${reply.id ? '!min-h-42' : '!min-h-[120px]'}` ), }, }, }); const uploadImage = async (file?: string) => { const image = await upload(file, true); if (image.url) { editor.commands.setImage({ src: image.url }); editor.commands.createParagraphNear(); } }; const submit = async () => { setStatus('loading'); try { // get plaintext content const html = editor.getHTML(); const serializedContent = convert(html, { selectors: [ { selector: 'a', options: { linkBrackets: false } }, { selector: 'img', options: { linkBrackets: false } }, ], }); // define tags let tags: string[][] = []; // add reply to tags if present if (reply.id && reply.pubkey) { if (reply.root && reply.root.length > 1) { tags = [ ['e', reply.root, '', 'root'], ['e', reply.id, '', 'reply'], ['p', reply.pubkey], ]; } else { tags = [ ['e', reply.id, '', 'reply'], ['p', reply.pubkey], ]; } } // add hashtag to tags if present const hashtags = serializedContent .split(/\s/gm) .filter((s: string) => s.startsWith('#')); hashtags?.forEach((tag: string) => { tags.push(['t', tag.replace('#', '')]); }); // publish message await publish({ content: serializedContent, kind: 1, tags }); // send native notifiation await sendNativeNotification('Publish postr successfully'); // update state setStatus('done'); // reset editor editor.commands.clearContent(); if (reply.id) { clearReply(); } } catch { setStatus(null); console.log('failed to publish'); } }; return (
{reply.id && (
)}
); }