update composer

This commit is contained in:
Ren Amamiya 2023-05-12 17:03:49 +07:00
parent 5fc3f2f929
commit 50a376ae6b
4 changed files with 130 additions and 59 deletions

View File

@ -3,8 +3,9 @@ import PlusCircleIcon from '@shared/icons/plusCircle';
import { createBlobFromFile } from '@utils/createBlobFromFile'; import { createBlobFromFile } from '@utils/createBlobFromFile';
import { open } from '@tauri-apps/api/dialog'; import { open } from '@tauri-apps/api/dialog';
import { listen } from '@tauri-apps/api/event';
import { Body, fetch } from '@tauri-apps/api/http'; import { Body, fetch } from '@tauri-apps/api/http';
import { useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { Transforms } from 'slate'; import { Transforms } from 'slate';
import { useSlateStatic } from 'slate-react'; import { useSlateStatic } from 'slate-react';
@ -13,30 +14,14 @@ export function ImageUploader() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const insertImage = (editor, url) => { const insertImage = (editor, url) => {
const text = { text: url }; const image = { type: 'image', url, children: [{ text: url }] };
const image = { type: 'image', url, children: [text] };
Transforms.insertNodes(editor, image); Transforms.insertNodes(editor, image);
}; };
const openFileDialog = async () => { const uploadToVoidCat = useCallback(
const selected: any = await open({ async (filepath) => {
multiple: false, const filename = filepath.split('/').pop();
filters: [ const file = await createBlobFromFile(filepath);
{
name: 'Image',
extensions: ['png', 'jpeg', 'jpg', 'gif'],
},
],
});
if (Array.isArray(selected)) {
// user selected multiple files
} else if (selected === null) {
// user cancelled the selection
} else {
setLoading(true);
const filename = selected.split('/').pop();
const file = await createBlobFromFile(selected);
const buf = await file.arrayBuffer(); const buf = await file.arrayBuffer();
try { try {
@ -68,12 +53,52 @@ export function ImageUploader() {
console.log('There was an error', error); console.log('There was an error', error);
} }
} }
},
[editor]
);
const openFileDialog = async () => {
const selected: any = await open({
multiple: false,
filters: [
{
name: 'Image',
extensions: ['png', 'jpeg', 'jpg', 'gif'],
},
],
});
if (Array.isArray(selected)) {
// user selected multiple files
} else if (selected === null) {
// user cancelled the selection
} else {
setLoading(true);
// upload file
uploadToVoidCat(selected);
} }
}; };
useEffect(() => {
async function initFileDrop() {
const unlisten = await listen('tauri://file-drop', (event) => {
// set loading state
setLoading(true);
// upload file
uploadToVoidCat(event.payload[0]);
});
return () => {
unlisten();
};
}
initFileDrop();
}, [uploadToVoidCat]);
return ( return (
<button <button
type="button" type="button"
autoFocus={false}
onClick={() => openFileDialog()} onClick={() => openFileDialog()}
className="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded hover:bg-zinc-800" className="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded hover:bg-zinc-800"
> >

View File

@ -32,8 +32,9 @@ export function ComposerModal() {
<> <>
<button <button
type="button" type="button"
autoFocus={false}
onClick={() => openModal()} onClick={() => openModal()}
className="inline-flex h-7 w-max items-center justify-center gap-1 rounded-md bg-fuchsia-500 px-2.5 text-xs font-medium text-zinc-200 shadow-button hover:bg-fuchsia-600" className="inline-flex h-7 w-max items-center justify-center gap-1 rounded-md bg-fuchsia-500 px-2.5 text-xs font-medium text-zinc-200 shadow-button hover:bg-fuchsia-600 focus:outline-none"
> >
<ComposeIcon width={14} height={14} /> <ComposeIcon width={14} height={14} />
Compose Compose
@ -68,17 +69,14 @@ export function ComposerModal() {
<span> <span>
<ChevronRightIcon width={14} height={14} className="text-zinc-500" /> <ChevronRightIcon width={14} height={14} className="text-zinc-500" />
</span> </span>
<button <div className="inline-flex h-6 w-max items-center justify-center gap-0.5 rounded bg-zinc-800 pl-3 pr-1.5 text-xs font-medium text-zinc-400 shadow-mini-button">
autoFocus={false} New Post
className="inline-flex h-6 w-max items-center justify-center gap-0.5 rounded bg-zinc-800 pl-3 pr-1.5 text-xs font-medium text-zinc-400 shadow-mini-button"
>
Post
<ChevronDownIcon width={14} height={14} /> <ChevronDownIcon width={14} height={14} />
</button> </div>
</div> </div>
<div <div
onClick={closeModal} onClick={closeModal}
className="inline-flex h-5 w-5 cursor-pointer items-center justify-center hover:bg-zinc-800" className="inline-flex h-5 w-5 cursor-pointer items-center justify-center rounded hover:bg-zinc-800"
> >
<CancelIcon width={16} height={16} className="text-zinc-500" /> <CancelIcon width={16} height={16} className="text-zinc-500" />
</div> </div>

View File

@ -1,4 +1,5 @@
import { ImageUploader } from '@shared/composer/imageUploader'; import { ImageUploader } from '@shared/composer/imageUploader';
import TrashIcon from '@shared/icons/trash';
import { RelayContext } from '@shared/relayProvider'; import { RelayContext } from '@shared/relayProvider';
import { WRITEONLY_RELAYS } from '@stores/constants'; import { WRITEONLY_RELAYS } from '@stores/constants';
@ -7,29 +8,65 @@ import { dateToUnix } from '@utils/date';
import { getEventHash, signEvent } from 'nostr-tools'; import { getEventHash, signEvent } from 'nostr-tools';
import { useCallback, useContext, useMemo, useState } from 'react'; import { useCallback, useContext, useMemo, useState } from 'react';
import { Node, createEditor } from 'slate'; import { Node, Transforms, createEditor } from 'slate';
import { withHistory } from 'slate-history'; import { withHistory } from 'slate-history';
import { Editable, Slate, withReact } from 'slate-react'; import { Editable, ReactEditor, Slate, useSlateStatic, withReact } from 'slate-react';
const initialValue = [ const withImages = (editor) => {
{ const { isVoid } = editor;
type: 'paragraph',
children: [{ text: '' }], editor.isVoid = (element) => {
}, return element.type === 'image' ? true : isVoid(element);
]; };
return editor;
};
const ImagePreview = ({ attributes, children, element }: { attributes: any; children: any; element: any }) => {
const editor: any = useSlateStatic();
const path = ReactEditor.findPath(editor, element);
return (
<figure {...attributes} className="m-0 mt-3">
{children}
<div contentEditable={false} className="relative">
<img src={element.url} className="m-0 h-auto w-full rounded-md" />
<button
onClick={() => Transforms.removeNodes(editor, { at: path })}
className="absolute right-2 top-2 inline-flex h-7 w-7 items-center justify-center gap-0.5 rounded bg-zinc-800 text-xs font-medium text-zinc-400 shadow-mini-button hover:bg-zinc-700"
>
<TrashIcon width={14} height={14} className="text-zinc-100" />
</button>
</div>
</figure>
);
};
export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) { export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) {
const pool: any = useContext(RelayContext); const pool: any = useContext(RelayContext);
const editor = useMemo(() => withHistory(withReact(createEditor())), []);
const [content, setContent] = useState(null); const editor = useMemo(() => withReact(withImages(withHistory(createEditor()))), []);
const [content, setContent] = useState<Node[]>([
{
children: [
{
text: '',
},
],
},
]);
const serialize = useCallback((nodes: Node[]) => { const serialize = useCallback((nodes: Node[]) => {
return nodes.map((n) => Node.string(n)).join('\n'); return nodes.map((n) => Node.string(n)).join('\n');
}, []); }, []);
const submit = () => { const submit = () => {
// serialize content
const serializedContent = serialize(content);
console.log(serializedContent);
const event: any = { const event: any = {
content: content, content: serializedContent,
created_at: dateToUnix(), created_at: dateToUnix(),
kind: 1, kind: 1,
pubkey: pubkey, pubkey: pubkey,
@ -40,35 +77,33 @@ export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) {
// publish note // publish note
pool.publish(event, WRITEONLY_RELAYS); pool.publish(event, WRITEONLY_RELAYS);
// reset form
setContent('');
}; };
return ( const renderElement = useCallback((props: any) => {
<Slate switch (props.element.type) {
editor={editor} case 'image':
value={initialValue} if (props.element.url) {
onChange={(value) => { return <ImagePreview {...props} />;
const isAstChange = editor.operations.some((op) => 'set_selection' !== op.type);
if (isAstChange) {
const content = serialize(value);
setContent(content);
} }
}} default:
> return <p {...props.attributes}>{props.children}</p>;
}
}, []);
return (
<Slate editor={editor} value={content} onChange={setContent}>
<div className="flex h-full flex-col px-4 pb-4"> <div className="flex h-full flex-col px-4 pb-4">
<div className="flex h-full w-full gap-2"> <div className="flex h-full w-full gap-2">
<div className="flex w-8 shrink-0 items-center justify-center"> <div className="flex w-8 shrink-0 items-center justify-center">
<div className="h-full w-[2px] bg-zinc-800"></div> <div className="h-full w-[2px] bg-zinc-800"></div>
</div> </div>
<div className="prose prose-zinc relative w-full max-w-none select-text break-words dark:prose-invert prose-p:text-[15px] prose-p:leading-tight prose-a:text-[15px] prose-a:font-normal prose-a:leading-tight prose-a:text-fuchsia-500 prose-a:no-underline hover:prose-a:text-fuchsia-600 hover:prose-a:underline prose-ol:mb-1 prose-ul:mb-1 prose-li:text-[15px] prose-li:leading-tight"> <div className="prose prose-zinc relative h-max w-full max-w-none select-text break-words pb-3 dark:prose-invert prose-p:mb-0.5 prose-p:mt-0 prose-p:text-[15px] prose-p:leading-tight prose-a:text-[15px] prose-a:font-normal prose-a:leading-tight prose-a:text-fuchsia-500 prose-a:no-underline hover:prose-a:text-fuchsia-600 hover:prose-a:underline prose-ol:mb-1 prose-ul:mb-1 prose-li:text-[15px] prose-li:leading-tight">
<Editable <Editable
autoFocus
placeholder="What's on your mind?" placeholder="What's on your mind?"
autoCapitalize="false"
autoCorrect="false"
spellCheck="false" spellCheck="false"
autoFocus={true} className="!min-h-[86px]"
className="min-h-20 mb-3 h-20" renderElement={renderElement}
/> />
</div> </div>
</div> </div>
@ -76,6 +111,7 @@ export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) {
<ImageUploader /> <ImageUploader />
<button <button
type="button" type="button"
autoFocus={false}
onClick={submit} onClick={submit}
className="inline-flex h-7 w-max items-center justify-center gap-1 rounded-md bg-fuchsia-500 px-3.5 text-xs font-medium text-zinc-200 shadow-button hover:bg-fuchsia-600" className="inline-flex h-7 w-max items-center justify-center gap-1 rounded-md bg-fuchsia-500 px-3.5 text-xs font-medium text-zinc-200 shadow-button hover:bg-fuchsia-600"
> >

View File

@ -0,0 +1,12 @@
import { SVGProps } from 'react';
export default function TrashIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg width={24} height={24} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M5.75 21.25L5.00156 21.2983C5.02702 21.6929 5.35453 22 5.75 22V21.25ZM18.25 21.25V22C18.6455 22 18.973 21.6929 18.9984 21.2983L18.25 21.25ZM2.75 5C2.33579 5 2 5.33579 2 5.75C2 6.16421 2.33579 6.5 2.75 6.5V5ZM21.25 6.5C21.6642 6.5 22 6.16421 22 5.75C22 5.33579 21.6642 5 21.25 5V6.5ZM10.5 10.75C10.5 10.3358 10.1642 10 9.75 10C9.33579 10 9 10.3358 9 10.75H10.5ZM9 16.25C9 16.6642 9.33579 17 9.75 17C10.1642 17 10.5 16.6642 10.5 16.25H9ZM15 10.75C15 10.3358 14.6642 10 14.25 10C13.8358 10 13.5 10.3358 13.5 10.75H15ZM13.5 16.25C13.5 16.6642 13.8358 17 14.25 17C14.6642 17 15 16.6642 15 16.25H13.5ZM15.1477 5.93694C15.2509 6.33808 15.6598 6.57957 16.0609 6.47633C16.4621 6.37308 16.7036 5.9642 16.6003 5.56306L15.1477 5.93694ZM4.00156 5.79829L5.00156 21.2983L6.49844 21.2017L5.49844 5.70171L4.00156 5.79829ZM5.75 22H18.25V20.5H5.75V22ZM18.9984 21.2983L19.9984 5.79829L18.5016 5.70171L17.5016 21.2017L18.9984 21.2983ZM19.25 5H4.75V6.5H19.25V5ZM2.75 6.5H4.75V5H2.75V6.5ZM19.25 6.5H21.25V5H19.25V6.5ZM9 10.75V16.25H10.5V10.75H9ZM13.5 10.75V16.25H15V10.75H13.5ZM12 3.5C13.5134 3.5 14.7868 4.53504 15.1477 5.93694L16.6003 5.56306C16.0731 3.51451 14.2144 2 12 2V3.5ZM8.85237 5.93694C9.21319 4.53504 10.4867 3.5 12 3.5V2C9.78568 2 7.92697 3.51451 7.39971 5.56306L8.85237 5.93694Z"
fill="currentColor"
/>
</svg>
);
}