mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-19 19:46:34 +00:00
update composer
This commit is contained in:
parent
2804d25f06
commit
7bd38f3c74
@ -32,6 +32,7 @@
|
|||||||
"react-virtuoso": "^4.3.5",
|
"react-virtuoso": "^4.3.5",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"slate": "^0.94.1",
|
"slate": "^0.94.1",
|
||||||
|
"slate-history": "^0.93.0",
|
||||||
"slate-react": "^0.94.0",
|
"slate-react": "^0.94.0",
|
||||||
"swr": "^2.1.5",
|
"swr": "^2.1.5",
|
||||||
"tailwind-merge": "^1.12.0",
|
"tailwind-merge": "^1.12.0",
|
||||||
|
@ -55,6 +55,9 @@ dependencies:
|
|||||||
slate:
|
slate:
|
||||||
specifier: ^0.94.1
|
specifier: ^0.94.1
|
||||||
version: 0.94.1
|
version: 0.94.1
|
||||||
|
slate-history:
|
||||||
|
specifier: ^0.93.0
|
||||||
|
version: 0.93.0(slate@0.94.1)
|
||||||
slate-react:
|
slate-react:
|
||||||
specifier: ^0.94.0
|
specifier: ^0.94.0
|
||||||
version: 0.94.0(react-dom@18.2.0)(react@18.2.0)(slate@0.94.1)
|
version: 0.94.0(react-dom@18.2.0)(react@18.2.0)(slate@0.94.1)
|
||||||
@ -4275,6 +4278,16 @@ packages:
|
|||||||
engines: { node: '>=8' }
|
engines: { node: '>=8' }
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/slate-history@0.93.0(slate@0.94.1):
|
||||||
|
resolution:
|
||||||
|
{ integrity: sha512-Gr1GMGPipRuxIz41jD2/rbvzPj8eyar56TVMyJBvBeIpQSSjNISssvGNDYfJlSWM8eaRqf6DAcxMKzsLCYeX6g== }
|
||||||
|
peerDependencies:
|
||||||
|
slate: '>=0.65.3'
|
||||||
|
dependencies:
|
||||||
|
is-plain-object: 5.0.0
|
||||||
|
slate: 0.94.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/slate-react@0.94.0(react-dom@18.2.0)(react@18.2.0)(slate@0.94.1):
|
/slate-react@0.94.0(react-dom@18.2.0)(react@18.2.0)(slate@0.94.1):
|
||||||
resolution:
|
resolution:
|
||||||
{ integrity: sha512-8LW3HdC3Rya+3E1Gm5voyXdEfQrJGvm0QYl0bgtW2C06Dh82imxhGIPqRv2sj8j2Zf869oJGNp2bGUoCuNBJ6g== }
|
{ integrity: sha512-8LW3HdC3Rya+3E1Gm5voyXdEfQrJGvm0QYl0bgtW2C06Dh82imxhGIPqRv2sj8j2Zf869oJGNp2bGUoCuNBJ6g== }
|
||||||
|
@ -24,6 +24,10 @@
|
|||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span[data-slate-placeholder] {
|
||||||
|
@apply top-0;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes loop {
|
@keyframes loop {
|
||||||
0% {
|
0% {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
|
99
src/shared/composer/imageUploader.tsx
Normal file
99
src/shared/composer/imageUploader.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import PlusCircleIcon from '@shared/icons/plusCircle';
|
||||||
|
|
||||||
|
import { createBlobFromFile } from '@utils/createBlobFromFile';
|
||||||
|
|
||||||
|
import { open } from '@tauri-apps/api/dialog';
|
||||||
|
import { Body, fetch } from '@tauri-apps/api/http';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Transforms } from 'slate';
|
||||||
|
import { useSlateStatic } from 'slate-react';
|
||||||
|
|
||||||
|
export function ImageUploader() {
|
||||||
|
const editor = useSlateStatic();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const insertImage = (editor, url) => {
|
||||||
|
const text = { text: url };
|
||||||
|
const image = { type: 'image', url, children: [text] };
|
||||||
|
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 buf = await file.arrayBuffer();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: { data: { file: { id: string } } } = await fetch('https://void.cat/upload?cli=false', {
|
||||||
|
method: 'POST',
|
||||||
|
timeout: 5,
|
||||||
|
headers: {
|
||||||
|
accept: '*/*',
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'V-Filename': filename,
|
||||||
|
'V-Description': 'Uploaded from https://lume.nu',
|
||||||
|
'V-Strip-Metadata': 'true',
|
||||||
|
},
|
||||||
|
body: Body.bytes(buf),
|
||||||
|
});
|
||||||
|
const image = 'https://void.cat/d/' + res.data.file.id + '.webp';
|
||||||
|
// update parent state
|
||||||
|
insertImage(editor, image);
|
||||||
|
// reset loading state
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
// reset loading state
|
||||||
|
setLoading(false);
|
||||||
|
// handle error
|
||||||
|
if (error instanceof SyntaxError) {
|
||||||
|
// Unexpected token < in JSON
|
||||||
|
console.log('There was a SyntaxError', error);
|
||||||
|
} else {
|
||||||
|
console.log('There was an error', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => openFileDialog()}
|
||||||
|
className="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded hover:bg-zinc-800"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<svg
|
||||||
|
className="h-4 w-4 animate-spin text-black dark:text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<PlusCircleIcon width={20} height={20} className="text-zinc-500" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
@ -14,8 +14,6 @@ import { Dialog, Transition } from '@headlessui/react';
|
|||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
|
|
||||||
import PlusCircleIcon from '../icons/plusCircle';
|
|
||||||
|
|
||||||
export function ComposerModal() {
|
export function ComposerModal() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [composer] = useAtom(composerAtom);
|
const [composer] = useAtom(composerAtom);
|
||||||
@ -78,29 +76,14 @@ export function ComposerModal() {
|
|||||||
<ChevronDownIcon width={14} height={14} />
|
<ChevronDownIcon width={14} height={14} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div
|
||||||
|
onClick={closeModal}
|
||||||
|
className="inline-flex h-5 w-5 cursor-pointer items-center justify-center hover:bg-zinc-800"
|
||||||
|
>
|
||||||
<CancelIcon width={16} height={16} className="text-zinc-500" />
|
<CancelIcon width={16} height={16} className="text-zinc-500" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full flex-col px-4 pb-4">
|
{composer.type === 'post' && account && <Post pubkey={account.pubkey} privkey={account.privkey} />}
|
||||||
<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>
|
|
||||||
{composer.type === 'post' && <Post />}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<button className="inline-flex h-8 w-8 items-center justify-center rounded">
|
|
||||||
<PlusCircleIcon width={20} height={20} className="text-zinc-500" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
Post
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,3 +1,88 @@
|
|||||||
export function Post() {
|
import { ImageUploader } from '@shared/composer/imageUploader';
|
||||||
return <div className="relative w-full"></div>;
|
import { RelayContext } from '@shared/relayProvider';
|
||||||
|
|
||||||
|
import { WRITEONLY_RELAYS } from '@stores/constants';
|
||||||
|
|
||||||
|
import { dateToUnix } from '@utils/date';
|
||||||
|
|
||||||
|
import { getEventHash, signEvent } from 'nostr-tools';
|
||||||
|
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||||
|
import { Node, createEditor } from 'slate';
|
||||||
|
import { withHistory } from 'slate-history';
|
||||||
|
import { Editable, Slate, withReact } from 'slate-react';
|
||||||
|
|
||||||
|
const initialValue = [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
children: [{ text: '' }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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 serialize = useCallback((nodes: Node[]) => {
|
||||||
|
return nodes.map((n) => Node.string(n)).join('\n');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
const event: any = {
|
||||||
|
content: content,
|
||||||
|
created_at: dateToUnix(),
|
||||||
|
kind: 1,
|
||||||
|
pubkey: pubkey,
|
||||||
|
tags: [],
|
||||||
|
};
|
||||||
|
event.id = getEventHash(event);
|
||||||
|
event.sig = signEvent(event, privkey);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
<Editable
|
||||||
|
placeholder="What's on your mind?"
|
||||||
|
autoCapitalize="false"
|
||||||
|
autoCorrect="false"
|
||||||
|
spellCheck="false"
|
||||||
|
autoFocus={true}
|
||||||
|
className="min-h-20 mb-3 h-20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<ImageUploader />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
Post
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Slate>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
import { atomWithReset } from 'jotai/utils';
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
export const composerAtom = atomWithReset({ type: 'post', content: '' });
|
export const composerAtom = atom({ type: 'post' });
|
||||||
|
Loading…
Reference in New Issue
Block a user