From b1a44f2cbfc33f05ea4f70aea2467872efc1b268 Mon Sep 17 00:00:00 2001 From: reya Date: Sun, 22 Oct 2023 15:48:06 +0700 Subject: [PATCH] wip: new composer --- package.json | 2 + pnpm-lock.yaml | 114 ++++++++++++++ src/app/notes/components/editor/article.tsx | 124 +++++++++++++++ src/app/notes/components/editor/file.tsx | 162 ++++++++++++++++++++ src/app/notes/components/editor/post.tsx | 100 ++++++++++-- src/app/notes/components/index.ts | 2 + src/app/notes/new.tsx | 129 +++++++++------- src/libs/storage/instance.ts | 4 +- src/main.jsx | 2 +- src/shared/composer/mediaUploader.tsx | 77 ++++++---- src/shared/composer/mention/popup.tsx | 25 ++- src/shared/layouts/auth.tsx | 4 +- src/shared/layouts/note.tsx | 5 +- src/shared/widgets/local/network.tsx | 40 ++++- src/utils/rawEvent.ts | 1 - 15 files changed, 675 insertions(+), 116 deletions(-) create mode 100644 src/app/notes/components/editor/article.tsx create mode 100644 src/app/notes/components/editor/file.tsx diff --git a/package.json b/package.json index d6aa0445..1bfbab73 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-toolbar": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", "@tanstack/react-query": "^4.36.1", "@tauri-apps/api": "2.0.0-alpha.8", @@ -48,6 +49,7 @@ "@tauri-apps/plugin-updater": "2.0.0-alpha.1", "@tauri-apps/plugin-upload": "2.0.0-alpha.1", "@tauri-apps/plugin-window": "2.0.0-alpha.1", + "@tiptap/extension-character-count": "^2.1.12", "@tiptap/extension-image": "^2.1.12", "@tiptap/extension-mention": "^2.1.12", "@tiptap/extension-placeholder": "^2.1.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13e6c655..052cff96 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ dependencies: '@radix-ui/react-popover': specifier: ^1.0.7 version: 1.0.7(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-toolbar': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-tooltip': specifier: ^1.0.7 version: 1.0.7(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0) @@ -95,6 +98,9 @@ dependencies: '@tauri-apps/plugin-window': specifier: 2.0.0-alpha.1 version: 2.0.0-alpha.1 + '@tiptap/extension-character-count': + specifier: ^2.1.12 + version: 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) '@tiptap/extension-image': specifier: ^2.1.12 version: 2.1.12(@tiptap/core@2.1.12) @@ -1551,6 +1557,27 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.29 + '@types/react-dom': 18.2.14 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-slot@1.0.2(@types/react@18.2.29)(react@18.2.0): resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -1566,6 +1593,83 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-toggle-group@1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-context': 1.0.1(@types/react@18.2.29)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.29)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-toggle': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.29)(react@18.2.0) + '@types/react': 18.2.29 + '@types/react-dom': 18.2.14 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@radix-ui/react-toggle@1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.29)(react@18.2.0) + '@types/react': 18.2.29 + '@types/react-dom': 18.2.14 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@radix-ui/react-toolbar@1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-tBgmM/O7a07xbaEkYJWYTXkIdU/1pW4/KZORR43toC/4XWyBCURK0ei9kMUdp+gTPPKBgYLxXmRSH1EVcIDp8Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-context': 1.0.1(@types/react@18.2.29)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.29)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-separator': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-toggle-group': 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.29 + '@types/react-dom': 18.2.14 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-tooltip@1.0.7(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==} peerDependencies: @@ -2279,6 +2383,16 @@ packages: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) dev: false + /@tiptap/extension-character-count@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12): + resolution: {integrity: sha512-+GFbBG13nvF8mFIeisSERG/Q3CuRsTNwVZIRbJTLgGdbHXFqPhJh4Xfm7cv7OaOYevUlVyO+z5pGD7wIl1bLqQ==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + dependencies: + '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) + '@tiptap/pm': 2.1.12 + dev: false + /@tiptap/extension-code-block@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12): resolution: {integrity: sha512-RXtSYCVsnk8D+K80uNZShClfZjvv1EgO42JlXLVGWQdIgaNyuOv/6I/Jdf+ZzhnpsBnHufW+6TJjwP5vJPSPHA==} peerDependencies: diff --git a/src/app/notes/components/editor/article.tsx b/src/app/notes/components/editor/article.tsx new file mode 100644 index 00000000..dc566182 --- /dev/null +++ b/src/app/notes/components/editor/article.tsx @@ -0,0 +1,124 @@ +import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; +import CharacterCount from '@tiptap/extension-character-count'; +import Image from '@tiptap/extension-image'; +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 { useState } from 'react'; +import { toast } from 'sonner'; + +import { useNDK } from '@libs/ndk/provider'; + +import { MediaUploader, MentionPopup } from '@shared/composer'; +import { LoaderIcon } from '@shared/icons'; + +export function ArticleEditor() { + const { ndk } = useNDK(); + const [loading, setLoading] = useState(false); + + const editor = useEditor({ + extensions: [ + StarterKit.configure(), + Placeholder.configure({ placeholder: 'Type something...' }), + Image.configure({ + HTMLAttributes: { + class: + 'rounded-lg w-full object-cover h-auto max-h-[400px] border border-neutral-200 dark:border-neutral-800 outline outline-1 outline-offset-0 outline-neutral-300 dark:outline-neutral-700', + }, + }), + CharacterCount.configure(), + ], + content: JSON.parse(localStorage.getItem('editor-post') || '{}'), + editorProps: { + attributes: { + class: + 'outline-none prose prose-lg prose-neutral max-w-none select-text whitespace-pre-line break-words dark:prose-invert hover:prose-a:text-blue-500', + }, + }, + onUpdate: ({ editor }) => { + const jsonContent = JSON.stringify(editor.getJSON()); + localStorage.setItem('editor-post', jsonContent); + }, + }); + + const submit = async () => { + try { + setLoading(true); + + // get plaintext content + const html = editor.getHTML(); + const serializedContent = convert(html, { + selectors: [ + { selector: 'a', options: { linkBrackets: false } }, + { selector: 'img', options: { linkBrackets: false } }, + ], + }); + + // define tags + const tags: string[][] = []; + + // 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 + const event = new NDKEvent(ndk); + event.content = serializedContent; + event.kind = NDKKind.Article; + event.tags = tags; + + const publishedRelays = await event.publish(); + if (publishedRelays) { + toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`); + // update state + setLoading(false); + // reset editor + editor.commands.clearContent(); + localStorage.setItem('editor-post', '{}'); + } + } catch (e) { + setLoading(false); + toast.error(e); + } + }; + + return ( +
+ +
+ + {editor?.storage?.characterCount.characters()} + +
+
+ + +
+
+ +
+
+
+ ); +} diff --git a/src/app/notes/components/editor/file.tsx b/src/app/notes/components/editor/file.tsx new file mode 100644 index 00000000..817d7d13 --- /dev/null +++ b/src/app/notes/components/editor/file.tsx @@ -0,0 +1,162 @@ +import { NDKEvent } from '@nostr-dev-kit/ndk'; +import { message, open } from '@tauri-apps/plugin-dialog'; +import { readBinaryFile } from '@tauri-apps/plugin-fs'; +import { useState } from 'react'; +import { toast } from 'sonner'; + +import { useNDK } from '@libs/ndk/provider'; + +import { LoaderIcon } from '@shared/icons'; + +export function FileEditor() { + const { ndk } = useNDK(); + + const [loading, setLoading] = useState(false); + const [isPublish, setIsPublish] = useState(false); + const [metadata, setMetadata] = useState(null); + const [caption, setCaption] = useState(''); + + const uploadFile = async () => { + try { + setLoading(true); + + const selected = await open({ + multiple: false, + filters: [ + { + name: 'Media', + extensions: [ + 'png', + 'jpeg', + 'jpg', + 'gif', + 'mp4', + 'mp3', + 'webm', + 'mkv', + 'avi', + 'mov', + ], + }, + ], + }); + + if (!selected) { + setLoading(false); + return; + } + + const file = await readBinaryFile(selected.path); + const blob = new Blob([file]); + + const data = new FormData(); + data.append('fileToUpload', blob); + data.append('submit', 'Upload Image'); + + const res = await fetch('https://nostr.build/api/v2/upload/files', { + method: 'POST', + body: data, + }); + + if (res.ok) { + const json = await res.json(); + const data = json.data[0]; + + setMetadata([ + ['url', data.url], + ['m', data.mime ?? 'application/octet-stream'], + ['x', data.sha256 ?? ''], + ['size', data.size.toString() ?? '0'], + ['dim', `${data.dimensions.width}x${data.dimensions.height}` ?? '0'], + ['blurhash', data.blurhash ?? ''], + ['thumb', data.thumbnail ?? ''], + ]); + + // stop loading + setLoading(false); + } + } catch (e) { + // stop loading + setLoading(false); + await message(`Upload failed, error: ${e}`, { title: 'Lume', type: 'error' }); + } + }; + + const submit = async () => { + try { + setIsPublish(true); + + const event = new NDKEvent(ndk); + event.content = caption; + event.kind = 1063; + event.tags = metadata; + + const publishedRelays = await event.publish(); + if (publishedRelays) { + setMetadata(null); + setIsPublish(false); + toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`); + } + } catch (e) { + setIsPublish(false); + toast.error(e); + } + }; + + return ( +
+
+ +
+
+ setCaption(e.target.value)} + spellCheck={false} + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + placeholder="Caption (Optional)..." + className="h-11 flex-1 rounded-lg bg-neutral-100 px-3 placeholder:text-neutral-500 dark:bg-neutral-900 dark:placeholder:text-neutral-400" + /> + +
+
+
+
+ ); +} diff --git a/src/app/notes/components/editor/post.tsx b/src/app/notes/components/editor/post.tsx index de650e81..60a90a88 100644 --- a/src/app/notes/components/editor/post.tsx +++ b/src/app/notes/components/editor/post.tsx @@ -1,25 +1,39 @@ +import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; +import CharacterCount from '@tiptap/extension-character-count'; import Image from '@tiptap/extension-image'; 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 { useState } from 'react'; +import { toast } from 'sonner'; + +import { useNDK } from '@libs/ndk/provider'; + +import { MediaUploader, MentionPopup } from '@shared/composer'; +import { LoaderIcon } from '@shared/icons'; export function PostEditor() { + const { ndk } = useNDK(); + const [loading, setLoading] = useState(false); + const editor = useEditor({ extensions: [ StarterKit.configure(), - Placeholder.configure({ placeholder: 'Type something...' }), + Placeholder.configure({ placeholder: 'Sharing some thoughts...' }), Image.configure({ HTMLAttributes: { class: - 'rounded-lg w-full h-auto border border-white/10 outline outline-2 outline-offset-0 outline-white/20 ml-1', + 'rounded-lg w-full object-cover h-auto max-h-[400px] border border-neutral-200 dark:border-neutral-800 outline outline-1 outline-offset-0 outline-neutral-300 dark:outline-neutral-700', }, }), + CharacterCount.configure(), ], content: JSON.parse(localStorage.getItem('editor-post') || '{}'), editorProps: { attributes: { class: - 'h-full outline-none prose prose-lg prose-neutral max-w-none select-text whitespace-pre-line break-words dark:prose-invert hover:prose-a:text-blue-500', + 'outline-none prose prose-lg prose-neutral max-w-none select-text whitespace-pre-line break-words dark:prose-invert hover:prose-a:text-blue-500', }, }, onUpdate: ({ editor }) => { @@ -28,13 +42,79 @@ export function PostEditor() { }, }); + const submit = async () => { + try { + setLoading(true); + + // get plaintext content + const html = editor.getHTML(); + const serializedContent = convert(html, { + selectors: [ + { selector: 'a', options: { linkBrackets: false } }, + { selector: 'img', options: { linkBrackets: false } }, + ], + }); + + // define tags + const tags: string[][] = []; + + // 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 + const event = new NDKEvent(ndk); + event.content = serializedContent; + event.kind = NDKKind.Text; + event.tags = tags; + + const publishedRelays = await event.publish(); + if (publishedRelays) { + toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`); + // update state + setLoading(false); + // reset editor + editor.commands.clearContent(); + localStorage.setItem('editor-post', '{}'); + } + } catch (e) { + setLoading(false); + toast.error(e); + } + }; + return ( - +
+ +
+ + {editor?.storage?.characterCount.characters()} + +
+
+ + +
+
+ +
+
+
); } diff --git a/src/app/notes/components/index.ts b/src/app/notes/components/index.ts index bfd163e0..098a1ce3 100644 --- a/src/app/notes/components/index.ts +++ b/src/app/notes/components/index.ts @@ -1 +1,3 @@ export * from './editor/post'; +export * from './editor/article'; +export * from './editor/file'; diff --git a/src/app/notes/new.tsx b/src/app/notes/new.tsx index b7c95886..ea5034d1 100644 --- a/src/app/notes/new.tsx +++ b/src/app/notes/new.tsx @@ -2,73 +2,84 @@ import { useState } from 'react'; import { Link } from 'react-router-dom'; import { twMerge } from 'tailwind-merge'; -import { PostEditor } from '@app/notes/components'; +import { ArticleEditor, FileEditor, PostEditor } from '@app/notes/components'; import { ArrowLeftIcon } from '@shared/icons'; export function NewNoteScreen() { const [type, setType] = useState<'post' | 'article' | 'file' | 'raw'>('post'); + const renderEditor = () => { + switch (type) { + case 'post': + return ; + case 'article': + return ; + case 'file': + return ; + default: + return null; + } + }; + return ( -
-
-
- - - -
-
-
-
- - - - -
-
- {type === 'post' ? : null} -
-
+
+
+ + +
+
+
+
+ + + + +
+
+
{renderEditor()}
+
+
); } diff --git a/src/libs/storage/instance.ts b/src/libs/storage/instance.ts index 5ac8499d..a876b033 100644 --- a/src/libs/storage/instance.ts +++ b/src/libs/storage/instance.ts @@ -52,10 +52,10 @@ export class LumeStorage { const account = results[0]; if (typeof account.follows === 'string') - account.follows = JSON.parse(account.follows); + account.follows = JSON.parse(account.follows) ?? []; if (typeof account.circles === 'string') - account.circles = JSON.parse(account.circles); + account.circles = JSON.parse(account.circles) ?? []; if (typeof account.last_login_at === 'string') account.last_login_at = parseInt(account.last_login_at); diff --git a/src/main.jsx b/src/main.jsx index 6048b434..1e152d8e 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -16,7 +16,7 @@ root.render( - + diff --git a/src/shared/composer/mediaUploader.tsx b/src/shared/composer/mediaUploader.tsx index 4c23a859..947b64ee 100644 --- a/src/shared/composer/mediaUploader.tsx +++ b/src/shared/composer/mediaUploader.tsx @@ -1,24 +1,61 @@ -import { UnlistenFn, listen } from '@tauri-apps/api/event'; -import { message } from '@tauri-apps/plugin-dialog'; +import { message, open } from '@tauri-apps/plugin-dialog'; +import { readBinaryFile } from '@tauri-apps/plugin-fs'; import { Editor } from '@tiptap/react'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { MediaIcon } from '@shared/icons'; -import { useNostr } from '@utils/hooks/useNostr'; - export function MediaUploader({ editor }: { editor: Editor }) { - const { upload } = useNostr(); const [loading, setLoading] = useState(false); - const uploadToNostrBuild = async (file?: string) => { + const uploadToNostrBuild = async () => { try { // start loading setLoading(true); - const image = await upload(file, true); - if (image.url) { - editor.commands.setImage({ src: image.url }); + const selected = await open({ + multiple: false, + filters: [ + { + name: 'Media', + extensions: [ + 'png', + 'jpeg', + 'jpg', + 'gif', + 'mp4', + 'mp3', + 'webm', + 'mkv', + 'avi', + 'mov', + ], + }, + ], + }); + + if (!selected) { + setLoading(false); + return; + } + + const file = await readBinaryFile(selected.path); + const blob = new Blob([file]); + + const data = new FormData(); + data.append('fileToUpload', blob); + data.append('submit', 'Upload Image'); + + const res = await fetch('https://nostr.build/api/v2/upload/files', { + method: 'POST', + body: data, + }); + + if (res.ok) { + const json = await res.json(); + const content = json.data[0]; + + editor.commands.setImage({ src: content.url }); editor.commands.createParagraphNear(); // stop loading @@ -31,29 +68,11 @@ export function MediaUploader({ editor }: { editor: Editor }) { } }; - useEffect(() => { - let unlisten: UnlistenFn; - - async function listenDnD() { - unlisten = await listen('tauri://file-drop', (event) => { - uploadToNostrBuild(event.payload[0]); - }); - } - - // start listen drag and drop event - listenDnD(); - - // clean up - return () => { - unlisten(); - }; - }, []); - return ( - +
- {db.account.follows.map((item) => ( - - ))} + {db.account.follows.length > 0 ? ( + db.account.follows.map((item) => ( + + )) + ) : ( +
+ Contact list is empty +
+ )}
diff --git a/src/shared/layouts/auth.tsx b/src/shared/layouts/auth.tsx index a4683905..988d8ed0 100644 --- a/src/shared/layouts/auth.tsx +++ b/src/shared/layouts/auth.tsx @@ -7,13 +7,13 @@ export function AuthLayout() { const { db } = useStorage(); return ( -
+
{db.platform !== 'macos' ? ( ) : (
)} -
+
diff --git a/src/shared/layouts/note.tsx b/src/shared/layouts/note.tsx index 0c4f87b4..b8cbff8d 100644 --- a/src/shared/layouts/note.tsx +++ b/src/shared/layouts/note.tsx @@ -7,13 +7,14 @@ export function NoteLayout() { const { db } = useStorage(); return ( -
+
{db.platform !== 'macos' ? ( ) : (
)} -
+
+
diff --git a/src/shared/widgets/local/network.tsx b/src/shared/widgets/local/network.tsx index ceac6abb..79339cd7 100644 --- a/src/shared/widgets/local/network.tsx +++ b/src/shared/widgets/local/network.tsx @@ -5,7 +5,7 @@ import { VList } from 'virtua'; import { useStorage } from '@libs/storage/provider'; -import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; +import { ArrowRightCircleIcon, ArrowRightIcon, LoaderIcon } from '@shared/icons'; import { MemoizedArticleNote, MemoizedFileNote, @@ -18,7 +18,7 @@ import { NoteSkeleton } from '@shared/notes/skeleton'; import { TitleBar } from '@shared/titleBar'; import { EventLoader, WidgetWrapper } from '@shared/widgets'; -import { useWidgets } from '@stores/widgets'; +import { WidgetKinds, useWidgets } from '@stores/widgets'; import { useNostr } from '@utils/hooks/useNostr'; import { toRawEvent } from '@utils/rawEvent'; @@ -36,6 +36,7 @@ export function LocalNetworkWidget() { getNextPageParam: (lastPage) => lastPage.nextCursor, }); + const setWidget = useWidgets((state) => state.setWidget); const isFetched = useWidgets((state) => state.isFetched); const dbEvents = useMemo( () => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []), @@ -83,10 +84,18 @@ export function LocalNetworkWidget() { [dbEvents] ); + const openTrendingWidgets = async () => { + setWidget(db, { + kind: WidgetKinds.nostrBand.trendingAccounts, + title: 'Trending Accounts', + content: '', + }); + }; + // subscribe for new event // sub will be managed by lru-cache useEffect(() => { - if (db.account && db.account.circles && dbEvents.length > 0) { + if (db.account && db.account.circles.length > 0 && dbEvents.length > 0) { const filter: NDKFilter = { kinds: [NDKKind.Text, NDKKind.Repost], authors: db.account.circles, @@ -100,6 +109,31 @@ export function LocalNetworkWidget() { } }, [data]); + if (db.account.circles.length < 1) { + return ( + +
+
+

👋

+

You have not follow anyone yet

+
+ If you are new to Nostr, you can click button below to open trending users + and start follow some of theme +
+ +
+
+
+ ); + } + return ( diff --git a/src/utils/rawEvent.ts b/src/utils/rawEvent.ts index 0532cb23..acb885d2 100644 --- a/src/utils/rawEvent.ts +++ b/src/utils/rawEvent.ts @@ -8,6 +8,5 @@ export function toRawEvent(event: NDKEvent) { delete event.isParamReplaceable; delete event.isReplaceable; delete event.repost; - delete event.relay; return event; }