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 { open } from '@tauri-apps/api/dialog';
import { listen } from '@tauri-apps/api/event';
import { Body, fetch } from '@tauri-apps/api/http';
import { useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { Transforms } from 'slate';
import { useSlateStatic } from 'slate-react';
@ -13,30 +14,14 @@ export function ImageUploader() {
const [loading, setLoading] = useState(false);
const insertImage = (editor, url) => {
const text = { text: url };
const image = { type: 'image', url, children: [text] };
const image = { type: 'image', url, children: [{ text: url }] };
Transforms.insertNodes(editor, image);
};
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);
const filename = selected.split('/').pop();
const file = await createBlobFromFile(selected);
const uploadToVoidCat = useCallback(
async (filepath) => {
const filename = filepath.split('/').pop();
const file = await createBlobFromFile(filepath);
const buf = await file.arrayBuffer();
try {
@ -68,12 +53,52 @@ export function ImageUploader() {
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 (
<button
type="button"
autoFocus={false}
onClick={() => openFileDialog()}
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
type="button"
autoFocus={false}
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} />
Compose
@ -68,17 +69,14 @@ export function ComposerModal() {
<span>
<ChevronRightIcon width={14} height={14} className="text-zinc-500" />
</span>
<button
autoFocus={false}
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
<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">
New Post
<ChevronDownIcon width={14} height={14} />
</button>
</div>
</div>
<div
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" />
</div>

View File

@ -1,4 +1,5 @@
import { ImageUploader } from '@shared/composer/imageUploader';
import TrashIcon from '@shared/icons/trash';
import { RelayContext } from '@shared/relayProvider';
import { WRITEONLY_RELAYS } from '@stores/constants';
@ -7,29 +8,65 @@ import { dateToUnix } from '@utils/date';
import { getEventHash, signEvent } from 'nostr-tools';
import { useCallback, useContext, useMemo, useState } from 'react';
import { Node, createEditor } from 'slate';
import { Node, Transforms, createEditor } from 'slate';
import { withHistory } from 'slate-history';
import { Editable, Slate, withReact } from 'slate-react';
import { Editable, ReactEditor, Slate, useSlateStatic, withReact } from 'slate-react';
const initialValue = [
{
type: 'paragraph',
children: [{ text: '' }],
},
];
const withImages = (editor) => {
const { isVoid } = editor;
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 }) {
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[]) => {
return nodes.map((n) => Node.string(n)).join('\n');
}, []);
const submit = () => {
// serialize content
const serializedContent = serialize(content);
console.log(serializedContent);
const event: any = {
content: content,
content: serializedContent,
created_at: dateToUnix(),
kind: 1,
pubkey: pubkey,
@ -40,35 +77,33 @@ export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) {
// publish note
pool.publish(event, WRITEONLY_RELAYS);
// reset form
setContent('');
};
return (
<Slate
editor={editor}
value={initialValue}
onChange={(value) => {
const isAstChange = editor.operations.some((op) => 'set_selection' !== op.type);
if (isAstChange) {
const content = serialize(value);
setContent(content);
const renderElement = useCallback((props: any) => {
switch (props.element.type) {
case 'image':
if (props.element.url) {
return <ImagePreview {...props} />;
}
}}
>
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 w-full gap-2">
<div className="flex w-8 shrink-0 items-center justify-center">
<div className="h-full w-[2px] bg-zinc-800"></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
autoFocus
placeholder="What's on your mind?"
autoCapitalize="false"
autoCorrect="false"
spellCheck="false"
autoFocus={true}
className="min-h-20 mb-3 h-20"
className="!min-h-[86px]"
renderElement={renderElement}
/>
</div>
</div>
@ -76,6 +111,7 @@ export function Post({ pubkey, privkey }: { pubkey: string; privkey: string }) {
<ImageUploader />
<button
type="button"
autoFocus={false}
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"
>

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