update composer
This commit is contained in:
@@ -11,7 +11,7 @@ import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function SplashScreen() {
|
||||
const { db } = useStorage();
|
||||
const { ndk, relayUrls } = useNDK();
|
||||
const { ndk } = useNDK();
|
||||
const { fetchUserData, prefetchEvents } = useNostr();
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
@@ -68,9 +68,7 @@ export function SplashScreen() {
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col gap-1 text-center">
|
||||
<h3 className="text-lg font-semibold leading-none text-white">
|
||||
{!ndk
|
||||
? 'Connecting to relay...'
|
||||
: `Connected to ${relayUrls.length} relays`}
|
||||
{!ndk ? 'Connecting to relay...' : 'Fetching events from the last login.'}
|
||||
</h3>
|
||||
<p className="text-sm text-white/50">
|
||||
This may take a few seconds, please don't close app.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { message } from '@tauri-apps/plugin-dialog';
|
||||
import Image from '@tiptap/extension-image';
|
||||
import Mention from '@tiptap/extension-mention';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
@@ -9,24 +10,21 @@ import { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { Suggestion } from '@shared/composer';
|
||||
import { CancelIcon, LoaderIcon, PlusCircleIcon } from '@shared/icons';
|
||||
import { CancelIcon, LoaderIcon, MediaIcon, MentionIcon } 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 [loading, setLoading] = useState<boolean>(false);
|
||||
const [reply, clearReply] = useComposer((state) => [state.reply, state.clearReply]);
|
||||
|
||||
const expand = useComposer((state) => state.expand)
|
||||
const upload = useImageUploader();
|
||||
const { publish, upload } = useNostr();
|
||||
|
||||
const expand = useComposer((state) => state.expand);
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
@@ -65,9 +63,9 @@ export function Composer() {
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
setStatus('loading');
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// get plaintext content
|
||||
const html = editor.getHTML();
|
||||
const serializedContent = convert(html, {
|
||||
@@ -108,18 +106,19 @@ export function Composer() {
|
||||
await publish({ content: serializedContent, kind: 1, tags });
|
||||
|
||||
// send native notifiation
|
||||
await sendNativeNotification('Publish post successfully');
|
||||
await sendNativeNotification('Post has been published successfully.');
|
||||
|
||||
// update state
|
||||
setStatus('done');
|
||||
setLoading(false);
|
||||
// reset editor
|
||||
editor.commands.clearContent();
|
||||
// reset reply
|
||||
if (reply.id) {
|
||||
clearReply();
|
||||
}
|
||||
} catch {
|
||||
setStatus(null);
|
||||
console.log('failed to publish');
|
||||
setLoading(false);
|
||||
await message('Publishing post failed.', { title: 'Lume', type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -136,7 +135,10 @@ export function Composer() {
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
className={twMerge('scrollbar-hide markdown break-all max-h-[500px] overflow-y-auto outline-none pr-2', expand ? 'min-h-[500px]' : 'min-h-[120px]')}
|
||||
className={twMerge(
|
||||
'scrollbar-hide markdown max-h-[500px] overflow-y-auto break-all pr-2 outline-none',
|
||||
expand ? 'min-h-[500px]' : 'min-h-[120px]'
|
||||
)}
|
||||
/>
|
||||
{reply.id && (
|
||||
<div className="relative">
|
||||
@@ -152,17 +154,31 @@ export function Composer() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between bg-white/5 rounded-b-xl p-2 border-t border-white/10">
|
||||
<div className="flex items-center justify-between rounded-b-xl border-t border-white/10 bg-white/5 p-2">
|
||||
<div className="inline-flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => uploadImage()}
|
||||
className="ml-2 inline-flex h-10 w-max items-center justify-center gap-1.5 rounded-lg px-2 text-sm font-medium text-white/80 hover:bg-white/10 hover:backdrop-blur-xl"
|
||||
>
|
||||
<MediaIcon className="h-5 w-5 text-white/80" />
|
||||
Add media
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => uploadImage()}
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-lg hover:bg-white/10 hover:backdrop-blur-xl"
|
||||
>
|
||||
<MentionIcon className="h-5 w-5 text-white/80" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => uploadImage()}
|
||||
className="ml-2 inline-flex h-10 w-10 items-center justify-center rounded-lg backdrop-blur-xl hover:bg-white/10"
|
||||
onClick={() => submit()}
|
||||
disabled={editor && editor.isEmpty}
|
||||
className="inline-flex h-10 w-20 items-center justify-center rounded-lg bg-fuchsia-500 px-2 font-semibold hover:bg-fuchsia-600 disabled:opacity-50"
|
||||
>
|
||||
<PlusCircleIcon className="h-5 w-5 text-white" />
|
||||
</button>
|
||||
<button onClick={() => submit()} className="inline-flex items-center justify-center w-max px-8 rounded-lg font-bold h-10 bg-fuchsia-500 hover:bg-fuchsia-600">
|
||||
{status === 'loading' ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
||||
{loading === true ? (
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||
) : (
|
||||
'Post'
|
||||
)}
|
||||
|
||||
7
src/shared/composer/mention/popup.tsx
Normal file
7
src/shared/composer/mention/popup.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export function MentionPopup() {
|
||||
return (
|
||||
<div>
|
||||
<p>TODO</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -57,3 +57,4 @@ export * from './focus';
|
||||
export * from './chevronUp';
|
||||
export * from './secure';
|
||||
export * from './verified';
|
||||
export * from './mention';
|
||||
|
||||
22
src/shared/icons/mention.tsx
Normal file
22
src/shared/icons/mention.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { SVGProps } from 'react';
|
||||
|
||||
export function MentionIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M11.85 13.251c-3.719.065-6.427 2.567-7.18 5.915-.13.575.338 1.084.927 1.084h6.901m-.647-6.999l.147-.001c.353 0 .696.022 1.03.064m-1.177-.063a7.889 7.889 0 00-1.852.249m3.028-.186c.334.042.658.104.972.186m-.972-.186a7.475 7.475 0 011.972.524m3.25 1.412v3m0 0v3m0-3h-3m3 0h3m-5.5-11.75a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export function LocalNetworkWidget() {
|
||||
useInfiniteQuery({
|
||||
queryKey: ['local-network-widget'],
|
||||
queryFn: async ({ pageParam = 0 }) => {
|
||||
return await db.getAllEvents(30, pageParam);
|
||||
return await db.getAllEvents(20, pageParam);
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { magnetDecode } from '@ctrl/magnet-link';
|
||||
import {
|
||||
NDKEvent,
|
||||
NDKFilter,
|
||||
@@ -7,6 +8,8 @@ import {
|
||||
NDKUser,
|
||||
} from '@nostr-dev-kit/ndk';
|
||||
import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
|
||||
import { message, open } from '@tauri-apps/plugin-dialog';
|
||||
import { VoidApi } from '@void-cat/api';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import { NostrFetcher } from 'nostr-fetch';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
@@ -17,6 +20,7 @@ import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
import { createBlobFromFile } from '@utils/createBlobFromFile';
|
||||
import { nHoursAgo } from '@utils/date';
|
||||
import { NDKEventWithReplies } from '@utils/types';
|
||||
|
||||
@@ -324,6 +328,91 @@ export function useNostr() {
|
||||
return res;
|
||||
};
|
||||
|
||||
const upload = async (file: null | string, nip94?: boolean) => {
|
||||
try {
|
||||
const voidcat = new VoidApi('https://void.cat');
|
||||
|
||||
let filepath = file;
|
||||
if (!file) {
|
||||
const selected = await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
name: 'Media',
|
||||
extensions: [
|
||||
'png',
|
||||
'jpeg',
|
||||
'jpg',
|
||||
'gif',
|
||||
'mp4',
|
||||
'mp3',
|
||||
'webm',
|
||||
'mkv',
|
||||
'avi',
|
||||
'mov',
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (Array.isArray(selected)) {
|
||||
// user selected multiple files
|
||||
} else if (selected === null) {
|
||||
return {
|
||||
url: null,
|
||||
error: 'Cancelled',
|
||||
};
|
||||
} else {
|
||||
filepath = selected.path;
|
||||
}
|
||||
}
|
||||
|
||||
const filename = filepath.split('/').pop();
|
||||
const filetype = filename.split('.').pop();
|
||||
|
||||
const blob = await createBlobFromFile(filepath);
|
||||
const uploader = voidcat.getUploader(blob);
|
||||
|
||||
// upload file
|
||||
const res = await uploader.upload();
|
||||
|
||||
if (res.ok) {
|
||||
const url =
|
||||
res.file?.metadata?.url ?? `https://void.cat/d/${res.file?.id}.${filetype}`;
|
||||
|
||||
if (nip94) {
|
||||
const tags = [
|
||||
['url', url],
|
||||
['x', res.file?.metadata?.digest ?? ''],
|
||||
['m', res.file?.metadata?.mimeType ?? 'application/octet-stream'],
|
||||
['size', res.file?.metadata?.size.toString() ?? '0'],
|
||||
];
|
||||
|
||||
if (res.file?.metadata?.magnetLink) {
|
||||
tags.push(['magnet', res.file.metadata.magnetLink]);
|
||||
const parsedMagnet = magnetDecode(res.file.metadata.magnetLink);
|
||||
if (parsedMagnet?.infoHash) {
|
||||
tags.push(['i', parsedMagnet?.infoHash]);
|
||||
}
|
||||
}
|
||||
|
||||
await publish({ content: '', kind: 1063, tags: tags });
|
||||
}
|
||||
|
||||
return {
|
||||
url: url,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
url: null,
|
||||
error: 'Upload failed',
|
||||
};
|
||||
} catch (e) {
|
||||
await message(e, { title: 'Lume', type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
sub,
|
||||
fetchUserData,
|
||||
@@ -336,5 +425,6 @@ export function useNostr() {
|
||||
fetchAllReplies,
|
||||
publish,
|
||||
createZap,
|
||||
upload,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,7 +18,18 @@ export function useImageUploader() {
|
||||
filters: [
|
||||
{
|
||||
name: 'Image',
|
||||
extensions: ['png', 'jpeg', 'jpg', 'gif'],
|
||||
extensions: [
|
||||
'png',
|
||||
'jpeg',
|
||||
'jpg',
|
||||
'gif',
|
||||
'mp4',
|
||||
'mp3',
|
||||
'webm',
|
||||
'mkv',
|
||||
'avi',
|
||||
'mov',
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user