Files
lume/src/shared/composer/composer.tsx
Ren Amamiya bac70b19ec polish
2023-08-19 15:27:10 +07:00

176 lines
5.2 KiB
TypeScript

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 | 'loading' | 'done'>(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 (
<div className="flex h-full flex-col px-4 pb-4">
<div className="flex h-full w-full gap-3">
<div className="flex w-8 shrink-0 items-center justify-center">
<div className="h-full w-[2px] bg-white/10" />
</div>
<div className="w-full">
<EditorContent
editor={editor}
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
/>
{reply.id && (
<div className="relative">
<MentionNote id={reply.id} />
<button
type="button"
onClick={() => clearReply()}
className="absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center rounded bg-white/10 px-2"
>
<CancelIcon className="h-4 w-4 text-white" />
</button>
</div>
)}
</div>
</div>
<div className="mt-4 flex items-center justify-between">
<button
type="button"
onClick={() => uploadImage()}
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-white/10"
>
<PlusCircleIcon className="h-5 w-5 text-white/50" />
</button>
<Button onClick={() => submit()} preset="publish">
{status === 'loading' ? (
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
) : (
'Postr'
)}
</Button>
</div>
</div>
);
}