From a6da07cd3f17698a298eaf7d0dce0c363383ac27 Mon Sep 17 00:00:00 2001 From: reya Date: Sun, 24 Dec 2023 19:14:46 +0700 Subject: [PATCH] refactor: everything --- package.json | 1 + pnpm-lock.yaml | 21 + src/app.tsx | 12 +- src/app/auth/follow.tsx | 13 +- src/app/auth/import.tsx | 15 +- src/app/auth/onboarding.tsx | 13 +- src/app/auth/tutorials/note.tsx | 4 +- src/app/chats/chat.tsx | 119 ---- src/app/chats/components/chatForm.tsx | 57 -- src/app/chats/components/chatListItem.tsx | 75 --- src/app/chats/components/mediaUploader.tsx | 55 -- src/app/chats/components/message.tsx | 24 - src/app/chats/hooks/useDecryptMessage.tsx | 23 - src/app/chats/index.tsx | 66 --- src/app/depot/components/contact.tsx | 12 +- src/app/depot/components/profile.tsx | 8 +- src/app/depot/components/relays.tsx | 7 +- src/app/depot/index.tsx | 7 +- src/app/depot/onboarding.tsx | 9 +- src/app/error.tsx | 10 +- src/app/home/components/index.ts | 2 + .../home/components}/newsfeed.tsx | 32 +- .../home/components}/notification.tsx | 31 +- src/app/home/index.tsx | 47 +- src/app/new/article.tsx | 2 +- src/app/new/components/mentionPopup.tsx | 8 +- src/app/new/file.tsx | 2 +- src/app/new/post.tsx | 8 +- src/app/new/privkey.tsx | 7 +- src/app/notes/article.tsx | 124 ---- src/app/notes/text.tsx | 133 ----- src/app/nwc/components/form.tsx | 6 +- src/app/nwc/index.tsx | 8 +- src/app/relays/components/relayEventList.tsx | 20 +- src/app/relays/components/userRelayList.tsx | 10 +- src/app/settings/backup.tsx | 8 +- src/app/settings/components/postCard.tsx | 12 +- src/app/settings/components/profileCard.tsx | 16 +- src/app/settings/components/relayCard.tsx | 6 +- src/app/settings/components/zapCard.tsx | 10 +- src/app/settings/editProfile.tsx | 18 +- src/app/settings/general.tsx | 21 +- src/app/users/components/profile.tsx | 10 +- src/app/users/index.tsx | 20 +- src/libs/ark/ark.ts | 559 ++---------------- src/libs/ark/components/note/builds/reply.tsx | 67 +++ .../ark/components/note/builds}/repost.tsx | 61 +- .../ark/components/note/builds/skeleton.tsx | 24 + src/libs/ark/components/note/builds/text.tsx | 25 + .../ark/components/note/{ => buttons}/pin.tsx | 14 +- .../note/{ => buttons}/reaction.tsx | 0 .../ark/components/note/buttons/reply.tsx | 43 ++ .../components/note/{ => buttons}/repost.tsx | 0 .../ark/components/note/{ => buttons}/zap.tsx | 7 +- src/libs/ark/components/note/child.tsx | 23 +- src/libs/ark/components/note/childUser.tsx | 26 +- src/libs/ark/components/note/index.ts | 33 +- .../ark/components/note}/kinds/article.tsx | 13 +- .../ark/components/note/kinds/media.tsx} | 52 +- .../note/{kind.tsx => kinds/text.tsx} | 6 +- .../ark/components/note}/mentions/hashtag.tsx | 2 +- .../ark/components/note}/mentions/invoice.tsx | 0 .../ark/components/note/mentions/note.tsx | 63 ++ .../ark/components/note}/mentions/user.tsx | 3 +- .../ark/components/note}/preview/image.tsx | 0 .../ark/components/note}/preview/link.tsx | 0 .../ark/components/note}/preview/video.tsx | 0 src/libs/ark/components/note/reply.tsx | 100 ++-- src/libs/ark/components/note/root.tsx | 11 +- src/libs/ark/components/note/thread.tsx | 38 ++ src/libs/ark/components/note/user.tsx | 75 ++- src/libs/ark/components/widget/header.tsx | 2 +- src/libs/ark/components/widget/index.ts | 2 + src/libs/ark/components/widget/live.tsx | 42 ++ src/{utils => libs/ark}/hooks/useEvent.ts | 16 +- src/libs/ark/hooks/useProfile.ts | 27 + .../ark}/hooks/useRichContent.tsx | 14 +- src/{utils => libs/ark}/hooks/useWidget.ts | 34 +- src/libs/ark/index.ts | 4 + src/libs/ark/provider.tsx | 191 +++++- src/libs/cache/index.ts | 142 +---- src/libs/storage/index.ts | 396 +++++++++++++ src/main.jsx | 6 +- src/shared/{accounts => account}/active.tsx | 16 +- src/shared/{accounts => account}/logout.tsx | 0 src/shared/{accounts => account}/more.tsx | 2 +- src/shared/navigation.tsx | 2 +- src/shared/notes/actions.tsx | 87 --- src/shared/notes/actions/more.tsx | 63 -- src/shared/notes/actions/reaction.tsx | 128 ---- src/shared/notes/actions/repost.tsx | 100 ---- src/shared/notes/actions/zap.tsx | 252 -------- src/shared/notes/article.tsx | 73 --- src/shared/notes/child.tsx | 38 -- src/shared/notes/file.tsx | 85 --- src/shared/notes/index.ts | 28 - src/shared/notes/kinds/text.tsx | 24 - src/shared/notes/mentions/note.tsx | 78 --- src/shared/notes/notify.tsx | 155 ----- src/shared/notes/replies/form.tsx | 58 -- src/shared/notes/replies/item.tsx | 50 -- src/shared/notes/replies/list.tsx | 67 --- .../notes/replies/replyMediaUploader.tsx | 78 --- src/shared/notes/replies/sub.tsx | 15 - src/shared/notes/skeleton.tsx | 20 - src/shared/notes/text.tsx | 54 -- src/shared/notes/unknown.tsx | 27 - src/shared/user.tsx | 4 - src/shared/widgets/article.tsx | 113 ---- src/shared/widgets/file.tsx | 112 ---- src/shared/widgets/group.tsx | 105 ---- src/shared/widgets/hashtag.tsx | 112 ---- src/shared/widgets/index.ts | 18 - .../widgets/nostrBand/trendingAccounts.tsx | 67 --- .../widgets/nostrBand/trendingNotes.tsx | 65 -- src/shared/widgets/other/addGroupFeeds.tsx | 128 ---- src/shared/widgets/other/addHashtagFeeds.tsx | 133 ----- src/shared/widgets/other/liveUpdater.tsx | 60 -- .../widgets/other/nostrBandUserProfile.tsx | 88 --- src/shared/widgets/other/toggleWidgetList.tsx | 26 - src/shared/widgets/other/userProfile.tsx | 106 ---- src/shared/widgets/other/widgetList.tsx | 124 ---- src/shared/widgets/thread.tsx | 80 --- src/shared/widgets/topic.tsx | 105 ---- src/shared/widgets/user.tsx | 117 ---- src/utils/hooks/useRelay.ts | 19 +- src/utils/hooks/useSuggestion.ts | 6 +- 127 files changed, 1447 insertions(+), 4874 deletions(-) delete mode 100644 src/app/chats/chat.tsx delete mode 100644 src/app/chats/components/chatForm.tsx delete mode 100644 src/app/chats/components/chatListItem.tsx delete mode 100644 src/app/chats/components/mediaUploader.tsx delete mode 100644 src/app/chats/components/message.tsx delete mode 100644 src/app/chats/hooks/useDecryptMessage.tsx delete mode 100644 src/app/chats/index.tsx create mode 100644 src/app/home/components/index.ts rename src/{shared/widgets => app/home/components}/newsfeed.tsx (77%) rename src/{shared/widgets => app/home/components}/notification.tsx (83%) delete mode 100644 src/app/notes/article.tsx delete mode 100644 src/app/notes/text.tsx create mode 100644 src/libs/ark/components/note/builds/reply.tsx rename src/{shared/notes => libs/ark/components/note/builds}/repost.tsx (51%) create mode 100644 src/libs/ark/components/note/builds/skeleton.tsx create mode 100644 src/libs/ark/components/note/builds/text.tsx rename src/libs/ark/components/note/{ => buttons}/pin.tsx (76%) rename src/libs/ark/components/note/{ => buttons}/reaction.tsx (100%) create mode 100644 src/libs/ark/components/note/buttons/reply.tsx rename src/libs/ark/components/note/{ => buttons}/repost.tsx (100%) rename src/libs/ark/components/note/{ => buttons}/zap.tsx (98%) rename src/{shared/notes => libs/ark/components/note}/kinds/article.tsx (89%) rename src/{shared/notes/kinds/file.tsx => libs/ark/components/note/kinds/media.tsx} (66%) rename src/libs/ark/components/note/{kind.tsx => kinds/text.tsx} (64%) rename src/{shared/notes => libs/ark/components/note}/mentions/hashtag.tsx (89%) rename src/{shared/notes => libs/ark/components/note}/mentions/invoice.tsx (100%) create mode 100644 src/libs/ark/components/note/mentions/note.tsx rename src/{shared/notes => libs/ark/components/note}/mentions/user.tsx (86%) rename src/{shared/notes => libs/ark/components/note}/preview/image.tsx (100%) rename src/{shared/notes => libs/ark/components/note}/preview/link.tsx (100%) rename src/{shared/notes => libs/ark/components/note}/preview/video.tsx (100%) create mode 100644 src/libs/ark/components/note/thread.tsx create mode 100644 src/libs/ark/components/widget/live.tsx rename src/{utils => libs/ark}/hooks/useEvent.ts (50%) create mode 100644 src/libs/ark/hooks/useProfile.ts rename src/{utils => libs/ark}/hooks/useRichContent.tsx (93%) rename src/{utils => libs/ark}/hooks/useWidget.ts (64%) create mode 100644 src/libs/storage/index.ts rename src/shared/{accounts => account}/active.tsx (81%) rename src/shared/{accounts => account}/logout.tsx (100%) rename src/shared/{accounts => account}/more.tsx (95%) delete mode 100644 src/shared/notes/actions.tsx delete mode 100644 src/shared/notes/actions/more.tsx delete mode 100644 src/shared/notes/actions/reaction.tsx delete mode 100644 src/shared/notes/actions/repost.tsx delete mode 100644 src/shared/notes/actions/zap.tsx delete mode 100644 src/shared/notes/article.tsx delete mode 100644 src/shared/notes/child.tsx delete mode 100644 src/shared/notes/file.tsx delete mode 100644 src/shared/notes/index.ts delete mode 100644 src/shared/notes/kinds/text.tsx delete mode 100644 src/shared/notes/mentions/note.tsx delete mode 100644 src/shared/notes/notify.tsx delete mode 100644 src/shared/notes/replies/form.tsx delete mode 100644 src/shared/notes/replies/item.tsx delete mode 100644 src/shared/notes/replies/list.tsx delete mode 100644 src/shared/notes/replies/replyMediaUploader.tsx delete mode 100644 src/shared/notes/replies/sub.tsx delete mode 100644 src/shared/notes/skeleton.tsx delete mode 100644 src/shared/notes/text.tsx delete mode 100644 src/shared/notes/unknown.tsx delete mode 100644 src/shared/widgets/article.tsx delete mode 100644 src/shared/widgets/file.tsx delete mode 100644 src/shared/widgets/group.tsx delete mode 100644 src/shared/widgets/hashtag.tsx delete mode 100644 src/shared/widgets/index.ts delete mode 100644 src/shared/widgets/nostrBand/trendingAccounts.tsx delete mode 100644 src/shared/widgets/nostrBand/trendingNotes.tsx delete mode 100644 src/shared/widgets/other/addGroupFeeds.tsx delete mode 100644 src/shared/widgets/other/addHashtagFeeds.tsx delete mode 100644 src/shared/widgets/other/liveUpdater.tsx delete mode 100644 src/shared/widgets/other/nostrBandUserProfile.tsx delete mode 100644 src/shared/widgets/other/toggleWidgetList.tsx delete mode 100644 src/shared/widgets/other/userProfile.tsx delete mode 100644 src/shared/widgets/other/widgetList.tsx delete mode 100644 src/shared/widgets/thread.tsx delete mode 100644 src/shared/widgets/topic.tsx delete mode 100644 src/shared/widgets/user.tsx diff --git a/package.json b/package.json index 2fd95f6f..a754faed 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "sonner": "^1.2.4", "tippy.js": "^6.3.7", "tiptap-markdown": "^0.8.8", + "use-context-selector": "^1.4.1", "use-react-workers": "^0.3.0", "virtua": "^0.18.0", "zustand": "^4.4.7" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e56da65c..10859104 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -203,6 +203,9 @@ dependencies: tiptap-markdown: specifier: ^0.8.8 version: 0.8.8(@tiptap/core@2.1.13) + use-context-selector: + specifier: ^1.4.1 + version: 1.4.1(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0) use-react-workers: specifier: ^0.3.0 version: 0.3.0(react@18.2.0) @@ -5855,6 +5858,24 @@ packages: tslib: 2.6.2 dev: false + /use-context-selector@1.4.1(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0): + resolution: {integrity: sha512-Io2ArvcRO+6MWIhkdfMFt+WKQX+Vb++W8DS2l03z/Vw/rz3BclKpM0ynr4LYGyU85Eke+Yx5oIhTY++QR0ZDoA==} + peerDependencies: + react: '>=16.8.0' + react-dom: '*' + react-native: '*' + scheduler: '>=0.19.0' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + scheduler: 0.23.0 + dev: false + /use-react-workers@0.3.0(react@18.2.0): resolution: {integrity: sha512-CQv/b5lnccR5G1HzrCFbkyeCcKD+TEYFm20veNd+huNSRBM0OXxdvcxAU7vUp3rj8/bHx7WE/rYvCHRyTfJOpQ==} peerDependencies: diff --git a/src/app.tsx b/src/app.tsx index e61a98df..b1ed5919 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,7 +1,7 @@ import { fetch } from '@tauri-apps/plugin-http'; import { RouterProvider, createBrowserRouter, defer, redirect } from 'react-router-dom'; import { ErrorScreen } from '@app/error'; -import { useArk } from '@libs/ark'; +import { useStorage } from '@libs/ark'; import { LoaderIcon } from '@shared/icons'; import { AppLayout } from '@shared/layouts/app'; import { AuthLayout } from '@shared/layouts/auth'; @@ -10,18 +10,18 @@ import { HomeLayout } from '@shared/layouts/home'; import { SettingsLayout } from '@shared/layouts/settings'; export default function App() { - const ark = useArk(); + const storage = useStorage(); const router = createBrowserRouter([ { - element: , + element: , children: [ { path: '/', element: , errorElement: , loader: async () => { - if (!ark.account) return redirect('auth/welcome'); + if (!storage.account) return redirect('auth/welcome'); return null; }, children: [ @@ -168,7 +168,7 @@ export default function App() { { index: true, loader: () => { - const depot = ark.checkDepot(); + const depot = storage.checkDepot(); if (!depot) return redirect('/depot/onboarding/'); return null; }, @@ -190,7 +190,7 @@ export default function App() { }, { path: 'auth', - element: , + element: , errorElement: , children: [ { diff --git a/src/app/auth/follow.tsx b/src/app/auth/follow.tsx index f9450f10..de9ee016 100644 --- a/src/app/auth/follow.tsx +++ b/src/app/auth/follow.tsx @@ -1,4 +1,3 @@ -import { NDKKind } from '@nostr-dev-kit/ndk'; import * as Accordion from '@radix-ui/react-accordion'; import { useQuery } from '@tanstack/react-query'; import { nip19 } from 'nostr-tools'; @@ -35,6 +34,8 @@ const LUME_USERS = ['npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6x export function FollowScreen() { const ark = useArk(); + const navigate = useNavigate(); + const { status, data } = useQuery({ queryKey: ['trending-profiles-widget'], queryFn: async () => { @@ -49,8 +50,6 @@ export function FollowScreen() { const [loading, setLoading] = useState(false); const [follows, setFollows] = useState([]); - const navigate = useNavigate(); - // toggle follow state const toggleFollow = (pubkey: string) => { const arr = follows.includes(pubkey) @@ -64,8 +63,7 @@ export function FollowScreen() { setLoading(true); if (!follows.length) return navigate('/auth/finish'); - const publish = await ark.createEvent({ - kind: NDKKind.Contacts, + const publish = await ark.newContactList({ tags: follows.map((item) => { if (item.startsWith('npub1')) return ['p', nip19.decode(item).data as string]; return ['p', item]; @@ -73,11 +71,6 @@ export function FollowScreen() { }); if (publish) { - ark.account.contacts = follows.map((item) => { - if (item.startsWith('npub1')) return nip19.decode(item).data as string; - return item; - }); - setLoading(false); return navigate('/auth/finish'); } diff --git a/src/app/auth/import.tsx b/src/app/auth/import.tsx index 92c32030..01242375 100644 --- a/src/app/auth/import.tsx +++ b/src/app/auth/import.tsx @@ -7,7 +7,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; import { twMerge } from 'tailwind-merge'; -import { useArk } from '@libs/ark'; +import { useArk, useStorage } from '@libs/ark'; import { ArrowLeftIcon, LoaderIcon } from '@shared/icons'; import { User } from '@shared/user'; @@ -20,6 +20,7 @@ export function ImportAccountScreen() { const [savedPrivkey, setSavedPrivkey] = useState(false); const ark = useArk(); + const storage = useStorage(); const navigate = useNavigate(); const submitNpub = async () => { @@ -42,8 +43,8 @@ export function ImportAccountScreen() { const pubkey = nip19.decode(npub.split('#')[0]).data as string; const localSigner = NDKPrivateKeySigner.generate(); - await ark.createSetting('nsecbunker', '1'); - await ark.createPrivkey(`${npub}-nsecbunker`, localSigner.privateKey); + await storage.createSetting('nsecbunker', '1'); + await storage.createPrivkey(`${npub}-nsecbunker`, localSigner.privateKey); // open nsecbunker web app in default browser await open('https://app.nsecbunker.com/keys'); @@ -74,7 +75,7 @@ export function ImportAccountScreen() { setLoading(true); // add account to db - await ark.createAccount({ id: npub, pubkey }); + await storage.createAccount({ id: npub, pubkey }); // get account contacts await ark.getUserContacts({ pubkey }); @@ -99,7 +100,7 @@ export function ImportAccountScreen() { if (nsec.length > 50 && nsec.startsWith('nsec1')) { try { const privkey = nip19.decode(nsec).data as string; - await ark.createPrivkey(pubkey, privkey); + await storage.createPrivkey(pubkey, privkey); ark.updateNostrSigner({ signer: new NDKPrivateKeySigner(privkey) }); setSavedPrivkey(true); @@ -279,9 +280,9 @@ export function ImportAccountScreen() {

Lume will put your private key to{' '} - {ark.platform === 'macos' + {storage.platform === 'macos' ? 'Apple Keychain (macOS)' - : ark.platform === 'windows' + : storage.platform === 'windows' ? 'Credential Manager (Windows)' : 'Secret Service (Linux)'} diff --git a/src/app/auth/onboarding.tsx b/src/app/auth/onboarding.tsx index a4515bf8..f7c5af22 100644 --- a/src/app/auth/onboarding.tsx +++ b/src/app/auth/onboarding.tsx @@ -2,11 +2,11 @@ import * as Switch from '@radix-ui/react-switch'; import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification'; import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useArk } from '@libs/ark'; +import { useStorage } from '@libs/ark'; import { InfoIcon } from '@shared/icons'; export function OnboardingScreen() { - const ark = useArk(); + const storage = useStorage(); const navigate = useNavigate(); const [settings, setSettings] = useState({ @@ -16,19 +16,18 @@ export function OnboardingScreen() { }); const next = () => { - if (!ark.account.contacts.length) return navigate('/auth/follow'); + if (!storage.account.contacts.length) return navigate('/auth/follow'); return navigate('/auth/finish'); }; const toggleOutbox = async () => { - await ark.createSetting('outbox', String(+!settings.outbox)); + await storage.createSetting('outbox', String(+!settings.outbox)); // update state setSettings((prev) => ({ ...prev, outbox: !settings.outbox })); }; const toggleAutoupdate = async () => { - await ark.createSetting('autoupdate', String(+!settings.autoupdate)); - ark.settings.autoupdate = !settings.autoupdate; + await storage.createSetting('autoupdate', String(+!settings.autoupdate)); // update state setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate })); }; @@ -44,7 +43,7 @@ export function OnboardingScreen() { const permissionGranted = await isPermissionGranted(); setSettings((prev) => ({ ...prev, notification: permissionGranted })); - const data = await ark.getAllSettings(); + const data = await storage.getAllSettings(); if (!data) return; data.forEach((item) => { diff --git a/src/app/auth/tutorials/note.tsx b/src/app/auth/tutorials/note.tsx index 2df21386..97b43bb5 100644 --- a/src/app/auth/tutorials/note.tsx +++ b/src/app/auth/tutorials/note.tsx @@ -1,7 +1,7 @@ import { NDKEvent } from '@nostr-dev-kit/ndk'; import { Link } from 'react-router-dom'; +import { TextNote } from '@libs/ark'; import { EditIcon, ReactionIcon, ReplyIcon, RepostIcon, ZapIcon } from '@shared/icons'; -import { TextNote } from '@shared/notes'; export function TutorialNoteScreen() { const exampleEvent = new NDKEvent(undefined, { @@ -32,7 +32,7 @@ export function TutorialNoteScreen() { updated in real-time.

Here is one example:

- +

Here are how you can interact with a note:

diff --git a/src/app/chats/chat.tsx b/src/app/chats/chat.tsx deleted file mode 100644 index 395472c6..00000000 --- a/src/app/chats/chat.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { NDKEvent } from '@nostr-dev-kit/ndk'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { useCallback, useEffect, useRef } from 'react'; -import { useParams } from 'react-router-dom'; -import { VList, VListHandle } from 'virtua'; -import { ChatForm } from '@app/chats/components/chatForm'; -import { ChatMessage } from '@app/chats/components/message'; -import { useArk } from '@libs/ark'; -import { LoaderIcon } from '@shared/icons'; -import { User } from '@shared/user'; - -export function ChatScreen() { - const ark = useArk(); - const { pubkey } = useParams(); - const { status, data } = useQuery({ - queryKey: ['nip04-dm', pubkey], - queryFn: async () => { - return await ark.getAllMessagesByPubkey({ pubkey }); - }, - refetchOnWindowFocus: false, - }); - - const queryClient = useQueryClient(); - const listRef = useRef(null); - - const newMessage = useMutation({ - mutationFn: async (event: NDKEvent) => { - // Cancel any outgoing refetches - await queryClient.cancelQueries({ queryKey: ['nip04-dm', pubkey] }); - - // Snapshot the previous value - const prevMessages = queryClient.getQueryData(['nip04-dm', pubkey]); - - // Optimistically update to the new value - queryClient.setQueryData(['nip04-dm', pubkey], (prev: NDKEvent[]) => [ - ...prev, - event, - ]); - - // Return a context object with the snapshotted value - return { prevMessages }; - }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ['nip04-dm', pubkey] }); - }, - }); - - const renderItem = useCallback( - (message: NDKEvent) => { - return ( - - ); - }, - [data] - ); - - useEffect(() => { - if (data && data.length > 0) listRef.current?.scrollToIndex(data.length); - }, [data]); - - useEffect(() => { - const sub = ark.subscribe({ - filter: { - kinds: [4], - authors: [ark.account.pubkey], - '#p': [pubkey], - since: Math.floor(Date.now() / 1000), - }, - closeOnEose: false, - cb: (event) => newMessage.mutate(event), - }); - - return () => { - sub.stop(); - }; - }, [pubkey]); - - return ( -
-
-
-
- -
-
- {status === 'pending' ? ( -
-
- -

- Loading messages -

-
-
- ) : data.length === 0 ? ( -
-

🙌

-

- You two didn't talk yet, let's send first message -

-
- ) : ( - - {data.map((message) => renderItem(message))} - - )} -
-
- -
-
-
-
- ); -} diff --git a/src/app/chats/components/chatForm.tsx b/src/app/chats/components/chatForm.tsx deleted file mode 100644 index 038aea9a..00000000 --- a/src/app/chats/components/chatForm.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useState } from 'react'; -import { toast } from 'sonner'; -import { MediaUploader } from '@app/chats/components/mediaUploader'; -import { useArk } from '@libs/ark'; -import { EnterIcon } from '@shared/icons'; - -export function ChatForm({ receiverPubkey }: { receiverPubkey: string }) { - const ark = useArk(); - const [value, setValue] = useState(''); - - const submit = async () => { - try { - const publish = await ark.nip04Encrypt({ content: value, pubkey: receiverPubkey }); - if (publish) setValue(''); - } catch (e) { - toast.error(e); - } - }; - - const handleEnterPress = (e: { - key: string; - shiftKey: KeyboardEvent['shiftKey']; - preventDefault: () => void; - }) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - submit(); - } - }; - - return ( -
- -
- setValue(e.target.value)} - onKeyDown={handleEnterPress} - spellCheck={false} - autoComplete="off" - autoCorrect="off" - autoCapitalize="off" - placeholder="Message..." - className="h-10 flex-1 resize-none border-none bg-transparent px-3 text-neutral-900 placeholder:text-neutral-600 focus:border-none focus:shadow-none focus:outline-none focus:ring-0 dark:text-neutral-100 dark:placeholder:text-neutral-300" - /> - -
-
- ); -} diff --git a/src/app/chats/components/chatListItem.tsx b/src/app/chats/components/chatListItem.tsx deleted file mode 100644 index 13c41bbb..00000000 --- a/src/app/chats/components/chatListItem.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { NDKEvent } from '@nostr-dev-kit/ndk'; -import * as Avatar from '@radix-ui/react-avatar'; -import { minidenticon } from 'minidenticons'; -import { memo } from 'react'; -import { NavLink } from 'react-router-dom'; -import { twMerge } from 'tailwind-merge'; -import { useDecryptMessage } from '@app/chats/hooks/useDecryptMessage'; -import { displayNpub, formatCreatedAt } from '@utils/formater'; -import { useProfile } from '@utils/hooks/useProfile'; - -export const ChatListItem = memo(function ChatListItem({ event }: { event: NDKEvent }) { - const { isLoading, user } = useProfile(event.pubkey); - const decryptedContent = useDecryptMessage(event); - - const createdAt = formatCreatedAt(event.created_at, true); - const svgURI = - 'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(event.pubkey, 90, 50)); - - if (isLoading) { - return ( -
-
-
-
-
-
-
- ); - } - - return ( - - twMerge( - 'flex items-center gap-2.5 px-3 py-1.5 hover:bg-neutral-200 dark:hover:bg-neutral-800', - isActive - ? 'bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100' - : 'text-neutral-500 dark:text-neutral-300' - ) - } - > - - - - {event.pubkey} - - -
-
- {user?.name || - user?.display_name || - user?.displayName || - displayNpub(event.pubkey, 16)} -
-
-
{decryptedContent}
-
{createdAt}
-
-
-
- ); -}); diff --git a/src/app/chats/components/mediaUploader.tsx b/src/app/chats/components/mediaUploader.tsx deleted file mode 100644 index aa1a4284..00000000 --- a/src/app/chats/components/mediaUploader.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import * as Tooltip from '@radix-ui/react-tooltip'; -import { Dispatch, SetStateAction, useState } from 'react'; -import { useArk } from '@libs/ark'; -import { LoaderIcon, MediaIcon } from '@shared/icons'; - -export function MediaUploader({ - setState, -}: { - setState: Dispatch>; -}) { - const ark = useArk(); - const [loading, setLoading] = useState(false); - - const uploadMedia = async () => { - setLoading(true); - - const image = await ark.upload({ - fileExts: ['mp4', 'mp3', 'webm', 'mkv', 'avi', 'mov'], - }); - - if (image) { - setState((prev: string) => `${prev}\n${image}`); - setLoading(false); - } - }; - - return ( - - - - - - - - Upload media - - - - - - ); -} diff --git a/src/app/chats/components/message.tsx b/src/app/chats/components/message.tsx deleted file mode 100644 index da528739..00000000 --- a/src/app/chats/components/message.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { NDKEvent } from '@nostr-dev-kit/ndk'; -import { twMerge } from 'tailwind-merge'; -import { useDecryptMessage } from '@app/chats/hooks/useDecryptMessage'; - -export function ChatMessage({ message, isSelf }: { message: NDKEvent; isSelf: boolean }) { - const decryptedContent = useDecryptMessage(message); - - return ( -
- {!decryptedContent ? ( -

Decrypting...

- ) : ( -

{decryptedContent}

- )} -
- ); -} diff --git a/src/app/chats/hooks/useDecryptMessage.tsx b/src/app/chats/hooks/useDecryptMessage.tsx deleted file mode 100644 index 35fd3da8..00000000 --- a/src/app/chats/hooks/useDecryptMessage.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { NDKEvent } from '@nostr-dev-kit/ndk'; -import { useEffect, useState } from 'react'; -import { useArk } from '@libs/ark'; - -export function useDecryptMessage(event: NDKEvent) { - const ark = useArk(); - const [content, setContent] = useState(event.content); - - useEffect(() => { - async function decryptContent() { - try { - const message = await ark.nip04Decrypt({ event }); - setContent(message); - } catch (e) { - console.error(e); - } - } - - decryptContent(); - }, []); - - return content; -} diff --git a/src/app/chats/index.tsx b/src/app/chats/index.tsx deleted file mode 100644 index 867459ae..00000000 --- a/src/app/chats/index.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { NDKEvent } from '@nostr-dev-kit/ndk'; -import { useQuery } from '@tanstack/react-query'; -import { useCallback } from 'react'; -import { Outlet } from 'react-router-dom'; -import { ChatListItem } from '@app/chats/components/chatListItem'; -import { useArk } from '@libs/ark'; -import { LoaderIcon } from '@shared/icons'; - -export function ChatsScreen() { - const ark = useArk(); - const { status, data } = useQuery({ - queryKey: ['nip04-chats'], - queryFn: async () => { - return await ark.getAllChats(); - }, - refetchOnWindowFocus: false, - refetchOnMount: false, - refetchOnReconnect: false, - staleTime: Infinity, - }); - - const renderItem = useCallback( - (event: NDKEvent) => { - return ; - }, - [data] - ); - - return ( -
-
-
-

- All chats -

-
-
- {status === 'pending' ? ( -
-
- -
- Loading messages... -
-
-
- ) : data.length < 1 ? ( -
-
-
No message
-
-
- ) : ( - data.map((item) => renderItem(item)) - )} -
-
-
- -
-
- ); -} diff --git a/src/app/depot/components/contact.tsx b/src/app/depot/components/contact.tsx index 3e470c4f..c4ca8618 100644 --- a/src/app/depot/components/contact.tsx +++ b/src/app/depot/components/contact.tsx @@ -1,12 +1,14 @@ import { NDKKind } from '@nostr-dev-kit/ndk'; import { useState } from 'react'; import { toast } from 'sonner'; -import { useArk } from '@libs/ark'; +import { useArk, useStorage } from '@libs/ark'; import { LoaderIcon, RunIcon } from '@shared/icons'; import { User } from '@shared/user'; export function DepotContactCard() { const ark = useArk(); + const storage = useStorage(); + const [status, setStatus] = useState(false); const backupContact = async () => { @@ -14,7 +16,7 @@ export function DepotContactCard() { setStatus(true); const event = await ark.getEventByFilter({ - filter: { authors: [ark.account.pubkey], kinds: [NDKKind.Contacts] }, + filter: { authors: [storage.account.pubkey], kinds: [NDKKind.Contacts] }, }); // broadcast to depot @@ -34,13 +36,13 @@ export function DepotContactCard() {
- {ark.account.contacts + {storage.account.contacts ?.slice(0, 8) .map((item) => )} - {ark.account.contacts?.length > 8 ? ( + {storage.account.contacts?.length > 8 ? (
- +{ark.account.contacts?.length - 8} + +{storage.account.contacts?.length - 8}
) : null} diff --git a/src/app/depot/components/profile.tsx b/src/app/depot/components/profile.tsx index ea1687c1..9e1d4b9d 100644 --- a/src/app/depot/components/profile.tsx +++ b/src/app/depot/components/profile.tsx @@ -1,12 +1,14 @@ import { NDKKind } from '@nostr-dev-kit/ndk'; import { useState } from 'react'; import { toast } from 'sonner'; -import { useArk } from '@libs/ark'; +import { useArk, useStorage } from '@libs/ark'; import { LoaderIcon, RunIcon } from '@shared/icons'; import { User } from '@shared/user'; export function DepotProfileCard() { const ark = useArk(); + const storage = useStorage(); + const [status, setStatus] = useState(false); const backupProfile = async () => { @@ -14,7 +16,7 @@ export function DepotProfileCard() { setStatus(true); const event = await ark.getEventByFilter({ - filter: { authors: [ark.account.pubkey], kinds: [NDKKind.Metadata] }, + filter: { authors: [storage.account.pubkey], kinds: [NDKKind.Metadata] }, }); // broadcast to depot @@ -33,7 +35,7 @@ export function DepotProfileCard() { return (
- +
Profile
diff --git a/src/app/depot/components/relays.tsx b/src/app/depot/components/relays.tsx index d10edde3..c919cc73 100644 --- a/src/app/depot/components/relays.tsx +++ b/src/app/depot/components/relays.tsx @@ -1,11 +1,12 @@ import { NDKKind } from '@nostr-dev-kit/ndk'; import { useEffect, useState } from 'react'; import { toast } from 'sonner'; -import { useArk } from '@libs/ark'; +import { useArk, useStorage } from '@libs/ark'; import { LoaderIcon, RunIcon } from '@shared/icons'; export function DepotRelaysCard() { const ark = useArk(); + const storage = useStorage(); const [status, setStatus] = useState(false); const [relaySize, setRelaySize] = useState(0); @@ -15,7 +16,7 @@ export function DepotRelaysCard() { setStatus(true); const event = await ark.getEventByFilter({ - filter: { authors: [ark.account.pubkey], kinds: [NDKKind.RelayList] }, + filter: { authors: [storage.account.pubkey], kinds: [NDKKind.RelayList] }, }); // broadcast to depot @@ -34,7 +35,7 @@ export function DepotRelaysCard() { useEffect(() => { async function loadRelays() { const event = await ark.getEventByFilter({ - filter: { authors: [ark.account.pubkey], kinds: [NDKKind.RelayList] }, + filter: { authors: [storage.account.pubkey], kinds: [NDKKind.RelayList] }, }); if (event) setRelaySize(event.tags.length); } diff --git a/src/app/depot/index.tsx b/src/app/depot/index.tsx index abc4be00..dc41e1d3 100644 --- a/src/app/depot/index.tsx +++ b/src/app/depot/index.tsx @@ -8,11 +8,12 @@ import { DepotContactCard } from '@app/depot/components/contact'; import { DepotMembers } from '@app/depot/components/members'; import { DepotProfileCard } from '@app/depot/components/profile'; import { DepotRelaysCard } from '@app/depot/components/relays'; -import { useArk } from '@libs/ark'; +import { useArk, useStorage } from '@libs/ark'; import { ChevronDownIcon, DepotIcon, GossipIcon } from '@shared/icons'; export function DepotScreen() { const ark = useArk(); + const storage = useStorage(); const [dataPath, setDataPath] = useState(''); const [tunnelUrl, setTunnelUrl] = useState(''); @@ -33,7 +34,7 @@ export function DepotScreen() { if (!/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/.test(relayUrl.host)) return; const relayEvent = await ark.getEventByFilter({ - filter: { authors: [ark.account.pubkey], kinds: [NDKKind.RelayList] }, + filter: { authors: [storage.account.pubkey], kinds: [NDKKind.RelayList] }, }); let publish: { id: string; seens: string[] }; @@ -54,7 +55,7 @@ export function DepotScreen() { }); if (publish) { - await ark.createSetting('tunnel_url', tunnelUrl); + await storage.createSetting('tunnel_url', tunnelUrl); toast.success('Update relay list successfully.'); setTunnelUrl(''); diff --git a/src/app/depot/onboarding.tsx b/src/app/depot/onboarding.tsx index 8156a127..a35bfb13 100644 --- a/src/app/depot/onboarding.tsx +++ b/src/app/depot/onboarding.tsx @@ -4,12 +4,13 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { parse, stringify } from 'smol-toml'; import { toast } from 'sonner'; -import { useArk } from '@libs/ark'; +import { useArk, useStorage } from '@libs/ark'; import { LoaderIcon } from '@shared/icons'; import { delay } from '@utils/delay'; export function DepotOnboardingScreen() { const ark = useArk(); + const storage = useStorage(); const navigate = useNavigate(); const [loading, setLoading] = useState(false); @@ -24,15 +25,15 @@ export function DepotOnboardingScreen() { const parsedConfig = parse(config); // add current user to whitelist - parsedConfig.authorization['pubkey_whitelist'].push(ark.account.pubkey); + parsedConfig.authorization['pubkey_whitelist'].push(storage.account.pubkey); // update new config const newConfig = stringify(parsedConfig); await writeTextFile(defaultConfig, newConfig); // launch depot - await ark.launchDepot(); - await ark.createSetting('depot', '1'); + await storage.launchDepot(); + await storage.createSetting('depot', '1'); await delay(2000); // delay 2s to make sure depot is running // default depot url: ws://localhost:6090 diff --git a/src/app/error.tsx b/src/app/error.tsx index bb6d88c6..5c0bf408 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -3,7 +3,7 @@ import { message, save } from '@tauri-apps/plugin-dialog'; import { writeTextFile } from '@tauri-apps/plugin-fs'; import { relaunch } from '@tauri-apps/plugin-process'; import { useRouteError } from 'react-router-dom'; -import { useArk } from '@libs/ark'; +import { useStorage } from '@libs/ark'; interface RouteError { statusText: string; @@ -11,7 +11,7 @@ interface RouteError { } export function ErrorScreen() { - const ark = useArk(); + const storage = useStorage(); const error = useRouteError() as RouteError; const restart = async () => { @@ -25,18 +25,18 @@ export function ErrorScreen() { const filePath = await save({ defaultPath: downloadPath + '/' + fileName, }); - const nsec = await ark.loadPrivkey(ark.account.pubkey); + const nsec = await storage.loadPrivkey(storage.account.pubkey); if (filePath) { if (nsec) { await writeTextFile( filePath, - `Nostr account, generated by Lume (lume.nu)\nPublic key: ${ark.account.id}\nPrivate key: ${nsec}` + `Nostr account, generated by Lume (lume.nu)\nPublic key: ${storage.account.id}\nPrivate key: ${nsec}` ); } else { await writeTextFile( filePath, - `Nostr account, generated by Lume (lume.nu)\nPublic key: ${ark.account.id}` + `Nostr account, generated by Lume (lume.nu)\nPublic key: ${storage.account.id}` ); } } // else { user cancel action } diff --git a/src/app/home/components/index.ts b/src/app/home/components/index.ts new file mode 100644 index 00000000..0505d67f --- /dev/null +++ b/src/app/home/components/index.ts @@ -0,0 +1,2 @@ +export * from './newsfeed'; +export * from './notification'; diff --git a/src/shared/widgets/newsfeed.tsx b/src/app/home/components/newsfeed.tsx similarity index 77% rename from src/shared/widgets/newsfeed.tsx rename to src/app/home/components/newsfeed.tsx index d0afcc15..44457067 100644 --- a/src/shared/widgets/newsfeed.tsx +++ b/src/app/home/components/newsfeed.tsx @@ -2,21 +2,16 @@ import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { useInfiniteQuery } from '@tanstack/react-query'; import { useMemo, useRef } from 'react'; import { VList, VListHandle } from 'virtua'; -import { Widget, useArk } from '@libs/ark'; +import { RepostNote, TextNote, Widget, useArk, useStorage } from '@libs/ark'; import { ArrowRightCircleIcon, LoaderIcon, TimelineIcon } from '@shared/icons'; -import { - MemoizedRepost, - MemoizedTextNote, - NoteSkeleton, - UnknownNote, -} from '@shared/notes'; import { FETCH_LIMIT } from '@utils/constants'; export function NewsfeedWidget() { const ark = useArk(); + const storage = useStorage(); const ref = useRef(); - const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = + const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({ queryKey: ['newsfeed'], initialPageParam: 0, @@ -30,9 +25,9 @@ export function NewsfeedWidget() { const events = await ark.getInfiniteEvents({ filter: { kinds: [NDKKind.Text, NDKKind.Repost], - authors: !ark.account.contacts.length - ? [ark.account.pubkey] - : ark.account.contacts, + authors: !storage.account.contacts.length + ? [storage.account.pubkey] + : storage.account.contacts, }, limit: FETCH_LIMIT, pageParam, @@ -57,11 +52,11 @@ export function NewsfeedWidget() { const renderItem = (event: NDKEvent) => { switch (event.kind) { case NDKKind.Text: - return ; + return ; case NDKKind.Repost: - return ; + return ; default: - return ; + return ; } }; @@ -75,11 +70,10 @@ export function NewsfeedWidget() { /> - {status === 'pending' ? ( -
-
- -
+ {isLoading ? ( +
+ + Loading
) : ( allEvents.map((item) => renderItem(item)) diff --git a/src/shared/widgets/notification.tsx b/src/app/home/components/notification.tsx similarity index 83% rename from src/shared/widgets/notification.tsx rename to src/app/home/components/notification.tsx index ec6bdd0b..a3b48a51 100644 --- a/src/shared/widgets/notification.tsx +++ b/src/app/home/components/notification.tsx @@ -2,14 +2,14 @@ import { NDKEvent, NDKKind, NDKSubscription } from '@nostr-dev-kit/ndk'; import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect, useMemo } from 'react'; import { VList } from 'virtua'; -import { Widget, useArk } from '@libs/ark'; +import { NoteSkeleton, TextNote, Widget, useArk, useStorage } from '@libs/ark'; import { AnnouncementIcon, ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; -import { MemoizedNotifyNote, NoteSkeleton } from '@shared/notes'; import { FETCH_LIMIT } from '@utils/constants'; import { sendNativeNotification } from '@utils/notification'; export function NotificationWidget() { const ark = useArk(); + const storage = useStorage(); const queryClient = useQueryClient(); const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = @@ -26,7 +26,7 @@ export function NotificationWidget() { const events = await ark.getInfiniteEvents({ filter: { kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap], - '#p': [ark.account.pubkey], + '#p': [storage.account.pubkey], }, limit: FETCH_LIMIT, pageParam, @@ -52,17 +52,17 @@ export function NotificationWidget() { ); const renderEvent = (event: NDKEvent) => { - if (event.pubkey === ark.account.pubkey) return null; - return ; + if (event.pubkey === storage.account.pubkey) return null; + return ; }; useEffect(() => { let sub: NDKSubscription = undefined; - if (status === 'success' && ark.account) { + if (status === 'success' && storage.account) { const filter = { kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap], - '#p': [ark.account.pubkey], + '#p': [storage.account.pubkey], since: Math.floor(Date.now() / 1000), }; @@ -85,17 +85,6 @@ export function NotificationWidget() { return await sendNativeNotification( `${profile.displayName || profile.name} has replied to your note` ); - case NDKKind.EncryptedDirectMessage: { - if (location.pathname !== '/chats') { - return await sendNativeNotification( - `${ - profile.displayName || profile.name - } has send you a encrypted message` - ); - } else { - break; - } - } case NDKKind.Repost: return await sendNativeNotification( `${profile.displayName || profile.name} has reposted to your note` @@ -133,11 +122,7 @@ export function NotificationWidget() { {status === 'pending' ? ( -
-
- -
-
+ ) : allEvents.length < 1 ? (
🎉
diff --git a/src/app/home/index.tsx b/src/app/home/index.tsx index 42706c90..8b3f6414 100644 --- a/src/app/home/index.tsx +++ b/src/app/home/index.tsx @@ -1,40 +1,21 @@ import { useQuery } from '@tanstack/react-query'; import { useRef, useState } from 'react'; import { VList, VListHandle } from 'virtua'; -import { useArk } from '@libs/ark'; +import { NewsfeedWidget, NotificationWidget } from '@app/home/components'; +import { useStorage } from '@libs/ark'; import { LoaderIcon } from '@shared/icons'; -import { - ArticleWidget, - FileWidget, - GroupWidget, - HashtagWidget, - NewsfeedWidget, - NotificationWidget, - ThreadWidget, - TopicWidget, - TrendingAccountsWidget, - TrendingNotesWidget, - UserWidget, - WidgetList, -} from '@shared/widgets'; import { WIDGET_KIND } from '@utils/constants'; import { WidgetProps } from '@utils/types'; export function HomeScreen() { - const ark = useArk(); + const storage = useStorage(); const ref = useRef(null); const { isLoading, data } = useQuery({ queryKey: ['widgets'], queryFn: async () => { - const dbWidgets = await ark.getWidgets(); + const dbWidgets = await storage.getWidgets(); const defaultWidgets = [ - { - id: '9998', - title: 'Notification', - content: '', - kind: WIDGET_KIND.notification, - }, { id: '9999', title: 'Newsfeed', @@ -59,26 +40,6 @@ export function HomeScreen() { return ; case WIDGET_KIND.newsfeed: return ; - case WIDGET_KIND.topic: - return ; - case WIDGET_KIND.user: - return ; - case WIDGET_KIND.thread: - return ; - case WIDGET_KIND.article: - return ; - case WIDGET_KIND.file: - return ; - case WIDGET_KIND.hashtag: - return ; - case WIDGET_KIND.group: - return ; - case WIDGET_KIND.trendingNotes: - return ; - case WIDGET_KIND.trendingAccounts: - return ; - case WIDGET_KIND.list: - return ; default: return ; } diff --git a/src/app/new/article.tsx b/src/app/new/article.tsx index 271a754b..3ad35e5a 100644 --- a/src/app/new/article.tsx +++ b/src/app/new/article.tsx @@ -66,7 +66,7 @@ export function NewArticleScreen() { const submit = async () => { try { - if (!ark.readyToSign) return navigate('/new/privkey'); + if (!ark.ndk.signer) return navigate('/new/privkey'); setLoading(true); diff --git a/src/app/new/components/mentionPopup.tsx b/src/app/new/components/mentionPopup.tsx index 4398f59c..2bce874e 100644 --- a/src/app/new/components/mentionPopup.tsx +++ b/src/app/new/components/mentionPopup.tsx @@ -2,11 +2,11 @@ import * as Popover from '@radix-ui/react-popover'; import { Editor } from '@tiptap/react'; import { nip19 } from 'nostr-tools'; import { MentionPopupItem } from '@app/new/components'; -import { useArk } from '@libs/ark'; +import { useStorage } from '@libs/ark'; import { MentionIcon } from '@shared/icons'; export function MentionPopup({ editor }: { editor: Editor }) { - const ark = useArk(); + const storage = useStorage(); const insertMention = (pubkey: string) => { editor.commands.insertContent(`nostr:${nip19.npubEncode(pubkey)}`); @@ -29,8 +29,8 @@ export function MentionPopup({ editor }: { editor: Editor }) { className="h-full max-h-[200px] w-[250px] overflow-hidden overflow-y-auto rounded-lg border border-neutral-200 bg-neutral-100 focus:outline-none dark:border-neutral-800 dark:bg-neutral-900" >
- {ark.account.contacts.length ? ( - ark.account.contacts.map((item) => ( + {storage.account.contacts.length ? ( + storage.account.contacts.map((item) => ( diff --git a/src/app/new/file.tsx b/src/app/new/file.tsx index 935667c2..78194f35 100644 --- a/src/app/new/file.tsx +++ b/src/app/new/file.tsx @@ -83,7 +83,7 @@ export function NewFileScreen() { const submit = async () => { try { - if (!ark.readyToSign) return navigate('/new/privkey'); + if (!ark.ndk.signer) return navigate('/new/privkey'); setIsPublish(true); diff --git a/src/app/new/post.tsx b/src/app/new/post.tsx index 52e2a9bc..9a1df8ea 100644 --- a/src/app/new/post.tsx +++ b/src/app/new/post.tsx @@ -11,12 +11,10 @@ import { useLayoutEffect, useRef, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { toast } from 'sonner'; import { MediaUploader, MentionPopup } from '@app/new/components'; -import { useArk } from '@libs/ark'; +import { MentionNote, useArk, useWidget } from '@libs/ark'; import { CancelIcon, LoaderIcon } from '@shared/icons'; -import { MentionNote } from '@shared/notes'; import { WIDGET_KIND } from '@utils/constants'; import { useSuggestion } from '@utils/hooks/useSuggestion'; -import { useWidget } from '@utils/hooks/useWidget'; export function NewPostScreen() { const ark = useArk(); @@ -64,7 +62,7 @@ export function NewPostScreen() { const submit = async () => { try { - if (!ark.readyToSign) return navigate('/new/privkey'); + if (!ark.ndk.signer) return navigate('/new/privkey'); setLoading(true); @@ -133,7 +131,7 @@ export function NewPostScreen() { /> {searchParams.get('replyTo') && (
- + - -
-
- {status === 'pending' ? ( -
Loading...
- ) : ( -
-
- {metadata.image && ( - {metadata.title} - )} -
-

{metadata.title}

- - Published: {metadata.publishedAt.toString()} - -
-
- - {data.content} - -
- )} -
-
-
- -
- -
-
- ); -} diff --git a/src/app/notes/text.tsx b/src/app/notes/text.tsx deleted file mode 100644 index a1dc7a0c..00000000 --- a/src/app/notes/text.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; -import { writeText } from '@tauri-apps/plugin-clipboard-manager'; -import { nip19 } from 'nostr-tools'; -import { EventPointer } from 'nostr-tools/lib/types/nip19'; -import { useRef, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { toast } from 'sonner'; -import { useArk } from '@libs/ark'; -import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons'; -import { - ChildNote, - MemoizedTextKind, - NoteActions, - NoteReplyForm, - UnknownNote, -} from '@shared/notes'; -import { ReplyList } from '@shared/notes/replies/list'; -import { User } from '@shared/user'; -import { useEvent } from '@utils/hooks/useEvent'; - -export function TextNoteScreen() { - const navigate = useNavigate(); - const replyRef = useRef(null); - - const { id } = useParams(); - const ark = useArk(); - const { status, data } = useEvent(id); - - const [isCopy, setIsCopy] = useState(false); - - const share = async () => { - try { - await writeText( - 'https://njump.me/' + - nip19.neventEncode({ id: data?.id, author: data?.pubkey } as EventPointer) - ); - // update state - setIsCopy(true); - // reset state after 2 sec - setTimeout(() => setIsCopy(false), 2000); - } catch (e) { - toast.error(e); - } - }; - - const scrollToReply = () => { - replyRef.current.scrollIntoView(); - }; - - const renderKind = (event: NDKEvent) => { - const thread = ark.getEventThread({ tags: event.tags }); - switch (event.kind) { - case NDKKind.Text: - return ( - <> - {thread ? ( -
-
- {thread.rootEventId ? ( - - ) : null} - {thread.replyEventId ? : null} -
-
- ) : null} - - - ); - default: - return ; - } - }; - - return ( -
-
-
- -
- - -
-
-
-
-
- {status === 'pending' ? ( -
Loading...
- ) : ( -
-
- -
{renderKind(data)}
-
- -
-
-
- )} -
-
- -
- -
-
-
-
-
- ); -} diff --git a/src/app/nwc/components/form.tsx b/src/app/nwc/components/form.tsx index 3f5083ce..051239fc 100644 --- a/src/app/nwc/components/form.tsx +++ b/src/app/nwc/components/form.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; import { toast } from 'sonner'; -import { useArk } from '@libs/ark'; +import { useStorage } from '@libs/ark'; import { LoaderIcon } from '@shared/icons'; export function NWCForm({ setWalletConnectURL }) { - const ark = useArk(); + const storage = useStorage(); const [uri, setUri] = useState(''); const [loading, setLoading] = useState(false); @@ -25,7 +25,7 @@ export function NWCForm({ setWalletConnectURL }) { const params = new URLSearchParams(uriObj.search); if (params.has('relay') && params.has('secret')) { - await ark.createPrivkey(`${ark.account.pubkey}-nwc`, uri); + await storage.createPrivkey(`${storage.account.pubkey}-nwc`, uri); setWalletConnectURL(uri); setLoading(false); } else { diff --git a/src/app/nwc/index.tsx b/src/app/nwc/index.tsx index 450aba51..826d8630 100644 --- a/src/app/nwc/index.tsx +++ b/src/app/nwc/index.tsx @@ -1,20 +1,20 @@ import { useEffect, useState } from 'react'; import { NWCForm } from '@app/nwc/components/form'; -import { useArk } from '@libs/ark'; +import { useStorage } from '@libs/ark'; import { CheckCircleIcon } from '@shared/icons'; export function NWCScreen() { - const ark = useArk(); + const storage = useStorage(); const [walletConnectURL, setWalletConnectURL] = useState(null); const remove = async () => { - await ark.removePrivkey(`${ark.account.pubkey}-nwc`); + await storage.removePrivkey(`${storage.account.pubkey}-nwc`); setWalletConnectURL(null); }; useEffect(() => { async function getNWC() { - const nwc = await ark.loadPrivkey(`${ark.account.pubkey}-nwc`); + const nwc = await storage.loadPrivkey(`${storage.account.pubkey}-nwc`); if (nwc) setWalletConnectURL(nwc); } getNWC(); diff --git a/src/app/relays/components/relayEventList.tsx b/src/app/relays/components/relayEventList.tsx index cbd13201..d832fcb4 100644 --- a/src/app/relays/components/relayEventList.tsx +++ b/src/app/relays/components/relayEventList.tsx @@ -2,14 +2,8 @@ import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { useInfiniteQuery } from '@tanstack/react-query'; import { useCallback, useMemo } from 'react'; import { VList } from 'virtua'; -import { useArk } from '@libs/ark'; +import { NoteSkeleton, RepostNote, TextNote, useArk } from '@libs/ark'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; -import { - MemoizedRepost, - MemoizedTextNote, - NoteSkeleton, - UnknownNote, -} from '@shared/notes'; import { FETCH_LIMIT } from '@utils/constants'; export function RelayEventList({ relayUrl }: { relayUrl: string }) { @@ -55,11 +49,11 @@ export function RelayEventList({ relayUrl }: { relayUrl: string }) { (event: NDKEvent) => { switch (event.kind) { case NDKKind.Text: - return ; + return ; case NDKKind.Repost: - return ; + return ; default: - return ; + return ; } }, [data] @@ -68,11 +62,7 @@ export function RelayEventList({ relayUrl }: { relayUrl: string }) { return ( {status === 'pending' ? ( -
-
- -
-
+ ) : ( allEvents.map((item) => renderItem(item)) )} diff --git a/src/app/relays/components/userRelayList.tsx b/src/app/relays/components/userRelayList.tsx index b88f0c6c..5b046fb8 100644 --- a/src/app/relays/components/userRelayList.tsx +++ b/src/app/relays/components/userRelayList.tsx @@ -1,20 +1,22 @@ import { NDKKind } from '@nostr-dev-kit/ndk'; import { useQuery } from '@tanstack/react-query'; import { RelayForm } from '@app/relays/components/relayForm'; -import { useArk } from '@libs/ark'; +import { useArk, useStorage } from '@libs/ark'; import { CancelIcon, RefreshIcon } from '@shared/icons'; import { useRelay } from '@utils/hooks/useRelay'; export function UserRelayList() { const ark = useArk(); + const storage = useStorage(); + const { removeRelay } = useRelay(); const { status, data, refetch } = useQuery({ - queryKey: ['relays', ark.account.pubkey], + queryKey: ['relays', storage.account.pubkey], queryFn: async () => { const event = await ark.getEventByFilter({ filter: { kinds: [NDKKind.RelayList], - authors: [ark.account.pubkey], + authors: [storage.account.pubkey], }, }); @@ -24,7 +26,7 @@ export function UserRelayList() { refetchOnWindowFocus: false, }); - const currentRelays = new Set([...ark.relays]); + const currentRelays = new Set(ark.ndk.pool.connectedRelays().map((item) => item.url)); return (
diff --git a/src/app/settings/backup.tsx b/src/app/settings/backup.tsx index 566b35bc..9ce96e89 100644 --- a/src/app/settings/backup.tsx +++ b/src/app/settings/backup.tsx @@ -1,21 +1,21 @@ import { nip19 } from 'nostr-tools'; import { useEffect, useState } from 'react'; -import { useArk } from '@libs/ark'; +import { useStorage } from '@libs/ark'; import { EyeOffIcon } from '@shared/icons'; export function BackupSettingScreen() { - const ark = useArk(); + const storage = useStorage(); const [privkey, setPrivkey] = useState(null); const [showPassword, setShowPassword] = useState(false); const removePrivkey = async () => { - await ark.removePrivkey(ark.account.pubkey); + await storage.removePrivkey(storage.account.pubkey); }; useEffect(() => { async function loadPrivkey() { - const key = await ark.loadPrivkey(ark.account.pubkey); + const key = await storage.loadPrivkey(storage.account.pubkey); if (key) setPrivkey(key); } diff --git a/src/app/settings/components/postCard.tsx b/src/app/settings/components/postCard.tsx index fabfe210..b04f6a86 100644 --- a/src/app/settings/components/postCard.tsx +++ b/src/app/settings/components/postCard.tsx @@ -1,17 +1,17 @@ import { useQuery } from '@tanstack/react-query'; import { fetch } from '@tauri-apps/plugin-http'; import { Link } from 'react-router-dom'; -import { useArk } from '@libs/ark'; +import { useStorage } from '@libs/ark'; import { LoaderIcon } from '@shared/icons'; import { compactNumber } from '@utils/formater'; export function PostCard() { - const ark = useArk(); + const storage = useStorage(); const { status, data } = useQuery({ - queryKey: ['user-stats', ark.account.pubkey], + queryKey: ['user-stats', storage.account.pubkey], queryFn: async ({ signal }: { signal: AbortSignal }) => { const res = await fetch( - `https://api.nostr.band/v0/stats/profile/${ark.account.pubkey}`, + `https://api.nostr.band/v0/stats/profile/${storage.account.pubkey}`, { signal, } @@ -38,14 +38,14 @@ export function PostCard() { ) : (

- {compactNumber.format(data.stats[ark.account.pubkey].pub_note_count)} + {compactNumber.format(data.stats[storage.account.pubkey].pub_note_count)}

Posts

View diff --git a/src/app/settings/components/profileCard.tsx b/src/app/settings/components/profileCard.tsx index 5f2280e4..43c80fbf 100644 --- a/src/app/settings/components/profileCard.tsx +++ b/src/app/settings/components/profileCard.tsx @@ -3,21 +3,21 @@ import { writeText } from '@tauri-apps/plugin-clipboard-manager'; import { minidenticon } from 'minidenticons'; import { nip19 } from 'nostr-tools'; import { Link } from 'react-router-dom'; -import { useArk } from '@libs/ark'; +import { useStorage } from '@libs/ark'; import { EditIcon, LoaderIcon } from '@shared/icons'; import { displayNpub } from '@utils/formater'; import { useProfile } from '@utils/hooks/useProfile'; export function ProfileCard() { - const ark = useArk(); + const storage = useStorage(); const svgURI = 'data:image/svg+xml;utf8,' + - encodeURIComponent(minidenticon(ark.account.pubkey, 90, 50)); + encodeURIComponent(minidenticon(storage.account.pubkey, 90, 50)); - const { isLoading, user } = useProfile(ark.account.pubkey); + const { isLoading, user } = useProfile(storage.account.pubkey); const copyNpub = async () => { - return await writeText(nip19.npubEncode(ark.account.pubkey)); + return await writeText(nip19.npubEncode(storage.account.pubkey)); }; return ( @@ -48,7 +48,7 @@ export function ProfileCard() { {ark.account.pubkey} @@ -67,7 +67,7 @@ export function ProfileCard() { {user?.display_name || user?.name}

- {user?.nip05 || displayNpub(ark.account.pubkey, 16)} + {user?.nip05 || displayNpub(storage.account.pubkey, 16)}

diff --git a/src/app/settings/components/relayCard.tsx b/src/app/settings/components/relayCard.tsx index 019d7a8d..547399bd 100644 --- a/src/app/settings/components/relayCard.tsx +++ b/src/app/settings/components/relayCard.tsx @@ -1,13 +1,15 @@ import { useQuery } from '@tanstack/react-query'; import { Link } from 'react-router-dom'; -import { useArk } from '@libs/ark'; +import { useArk, useStorage } from '@libs/ark'; import { EditIcon, LoaderIcon } from '@shared/icons'; import { compactNumber } from '@utils/formater'; export function RelayCard() { const ark = useArk(); + const storage = useStorage(); + const { status, data } = useQuery({ - queryKey: ['relays', ark.account.pubkey], + queryKey: ['relays', storage.account.pubkey], queryFn: async () => { const relays = await ark.getUserRelays({}); return relays; diff --git a/src/app/settings/components/zapCard.tsx b/src/app/settings/components/zapCard.tsx index deec4985..480b9973 100644 --- a/src/app/settings/components/zapCard.tsx +++ b/src/app/settings/components/zapCard.tsx @@ -1,16 +1,16 @@ import { useQuery } from '@tanstack/react-query'; import { fetch } from '@tauri-apps/plugin-http'; -import { useArk } from '@libs/ark'; +import { useStorage } from '@libs/ark'; import { LoaderIcon } from '@shared/icons'; import { compactNumber } from '@utils/formater'; export function ZapCard() { - const ark = useArk(); + const storage = useStorage(); const { status, data } = useQuery({ - queryKey: ['user-stats', ark.account.pubkey], + queryKey: ['user-stats', storage.account.pubkey], queryFn: async ({ signal }: { signal: AbortSignal }) => { const res = await fetch( - `https://api.nostr.band/v0/stats/profile/${ark.account.pubkey}`, + `https://api.nostr.band/v0/stats/profile/${storage.account.pubkey}`, { signal, } @@ -38,7 +38,7 @@ export function ZapCard() {

{compactNumber.format( - data?.stats[ark.account.pubkey]?.zaps_received?.msats / 1000 || 0 + data?.stats[storage.account.pubkey]?.zaps_received?.msats / 1000 || 0 )}

diff --git a/src/app/settings/editProfile.tsx b/src/app/settings/editProfile.tsx index c0151a8c..e8a4e55c 100644 --- a/src/app/settings/editProfile.tsx +++ b/src/app/settings/editProfile.tsx @@ -4,7 +4,7 @@ import { message } from '@tauri-apps/plugin-dialog'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; -import { useArk } from '@libs/ark'; +import { useArk, useStorage } from '@libs/ark'; import { CheckCircleIcon, LoaderIcon, PlusIcon, UnverifiedIcon } from '@shared/icons'; export function EditProfileScreen() { @@ -14,6 +14,8 @@ export function EditProfileScreen() { const [nip05, setNIP05] = useState({ verified: true, text: '' }); const ark = useArk(); + const storage = useStorage(); + const { register, handleSubmit, @@ -22,7 +24,10 @@ export function EditProfileScreen() { formState: { isValid, errors }, } = useForm({ defaultValues: async () => { - const res: NDKUserProfile = queryClient.getQueryData(['user', ark.account.pubkey]); + const res: NDKUserProfile = queryClient.getQueryData([ + 'user', + storage.account.pubkey, + ]); if (res.image) { setPicture(res.image); } @@ -41,7 +46,7 @@ export function EditProfileScreen() { const uploadAvatar = async () => { try { - if (!ark.readyToSign) return navigate('/new/privkey'); + if (!ark.ndk.signer) return navigate('/new/privkey'); setLoading(true); @@ -85,7 +90,10 @@ export function EditProfileScreen() { }; if (data.nip05) { - const verify = ark.validateNIP05({ pubkey: ark.account.pubkey, nip05: data.nip05 }); + const verify = ark.validateNIP05({ + pubkey: storage.account.pubkey, + nip05: data.nip05, + }); if (verify) { content = { ...content, nip05: data.nip05 }; } else { @@ -106,7 +114,7 @@ export function EditProfileScreen() { if (publish) { // invalid cache queryClient.invalidateQueries({ - queryKey: ['user', ark.account.pubkey], + queryKey: ['user', storage.account.pubkey], }); // reset form reset(); diff --git a/src/app/settings/general.tsx b/src/app/settings/general.tsx index 7174026c..ac71b85a 100644 --- a/src/app/settings/general.tsx +++ b/src/app/settings/general.tsx @@ -5,11 +5,12 @@ import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'; import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification'; import { useEffect, useState } from 'react'; import { twMerge } from 'tailwind-merge'; -import { useArk } from '@libs/ark'; +import { useStorage } from '@libs/ark'; import { DarkIcon, LightIcon, SystemModeIcon } from '@shared/icons'; export function GeneralSettingScreen() { - const ark = useArk(); + const storage = useStorage(); + const [settings, setSettings] = useState({ autoupdate: false, autolaunch: false, @@ -39,28 +40,28 @@ export function GeneralSettingScreen() { }; const toggleOutbox = async () => { - await ark.createSetting('outbox', String(+!settings.outbox)); + await storage.createSetting('outbox', String(+!settings.outbox)); // update state setSettings((prev) => ({ ...prev, outbox: !settings.outbox })); }; const toggleMedia = async () => { - await ark.createSetting('media', String(+!settings.media)); - ark.settings.media = !settings.media; + await storage.createSetting('media', String(+!settings.media)); + storage.settings.media = !settings.media; // update state setSettings((prev) => ({ ...prev, media: !settings.media })); }; const toggleHashtag = async () => { - await ark.createSetting('hashtag', String(+!settings.hashtag)); - ark.settings.hashtag = !settings.hashtag; + await storage.createSetting('hashtag', String(+!settings.hashtag)); + storage.settings.hashtag = !settings.hashtag; // update state setSettings((prev) => ({ ...prev, hashtag: !settings.hashtag })); }; const toggleAutoupdate = async () => { - await ark.createSetting('autoupdate', String(+!settings.autoupdate)); - ark.settings.autoupdate = !settings.autoupdate; + await storage.createSetting('autoupdate', String(+!settings.autoupdate)); + storage.settings.autoupdate = !settings.autoupdate; // update state setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate })); }; @@ -84,7 +85,7 @@ export function GeneralSettingScreen() { const permissionGranted = await isPermissionGranted(); setSettings((prev) => ({ ...prev, notification: permissionGranted })); - const data = await ark.getAllSettings(); + const data = await storage.getAllSettings(); if (!data) return; data.forEach((item) => { diff --git a/src/app/users/components/profile.tsx b/src/app/users/components/profile.tsx index 0c9c0fd8..5bce595f 100644 --- a/src/app/users/components/profile.tsx +++ b/src/app/users/components/profile.tsx @@ -4,13 +4,15 @@ import { useEffect, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; import { UserStats } from '@app/users/components/stats'; -import { useArk } from '@libs/ark'; +import { useArk, useStorage } from '@libs/ark'; import { NIP05 } from '@shared/nip05'; import { displayNpub } from '@utils/formater'; import { useProfile } from '@utils/hooks/useProfile'; export function UserProfile({ pubkey }: { pubkey: string }) { const ark = useArk(); + const storage = useStorage(); + const { user } = useProfile(pubkey); const [followed, setFollowed] = useState(false); @@ -21,7 +23,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) { const follow = async () => { try { - if (!ark.readyToSign) return navigate('/new/privkey'); + if (!ark.ndk.signer) return navigate('/new/privkey'); setFollowed(true); const add = await ark.createContact({ pubkey }); @@ -38,7 +40,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) { const unfollow = async () => { try { - if (!ark.readyToSign) return navigate('/new/privkey'); + if (!ark.ndk.signer) return navigate('/new/privkey'); setFollowed(false); await ark.deleteContact({ pubkey }); @@ -48,7 +50,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) { }; useEffect(() => { - if (ark.account.contacts.includes(pubkey)) { + if (storage.account.contacts.includes(pubkey)) { setFollowed(true); } }, []); diff --git a/src/app/users/index.tsx b/src/app/users/index.tsx index dec44863..b2a92fd2 100644 --- a/src/app/users/index.tsx +++ b/src/app/users/index.tsx @@ -3,14 +3,8 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import { useCallback, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { UserProfile } from '@app/users/components/profile'; -import { useArk } from '@libs/ark'; +import { NoteSkeleton, RepostNote, TextNote, useArk } from '@libs/ark'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; -import { - MemoizedRepost, - MemoizedTextNote, - NoteSkeleton, - UnknownNote, -} from '@shared/notes'; import { FETCH_LIMIT } from '@utils/constants'; export function UserScreen() { @@ -57,11 +51,11 @@ export function UserScreen() { (event: NDKEvent) => { switch (event.kind) { case NDKKind.Text: - return ; + return ; case NDKKind.Repost: - return ; + return ; default: - return ; + return ; } }, [data] @@ -76,11 +70,7 @@ export function UserScreen() {
{status === 'pending' ? ( -
-
- -
-
+ ) : ( allEvents.map((item) => renderItem(item)) )} diff --git a/src/libs/ark/ark.ts b/src/libs/ark/ark.ts index e22322be..6dd99b45 100644 --- a/src/libs/ark/ark.ts +++ b/src/libs/ark/ark.ts @@ -10,76 +10,37 @@ import NDK, { NDKUser, NostrEvent, } from '@nostr-dev-kit/ndk'; -import { ndkAdapter } from '@nostr-fetch/adapter-ndk'; -import { appConfigDir, resolveResource } from '@tauri-apps/api/path'; -import { invoke } from '@tauri-apps/api/primitives'; import { open } from '@tauri-apps/plugin-dialog'; import { readBinaryFile } from '@tauri-apps/plugin-fs'; import { fetch } from '@tauri-apps/plugin-http'; -import { Platform } from '@tauri-apps/plugin-os'; -import { Child, Command } from '@tauri-apps/plugin-shell'; -import Database from '@tauri-apps/plugin-sql'; -import { - NostrEventExt, - NostrFetcher, - normalizeRelayUrl, - normalizeRelayUrlSet, -} from 'nostr-fetch'; +import { NostrFetcher, normalizeRelayUrl } from 'nostr-fetch'; import { nip19 } from 'nostr-tools'; -import { NDKCacheAdapterTauri } from '@libs/cache'; -import { delay } from '@utils/delay'; -import { - type Account, - type NDKCacheUser, - type NDKCacheUserProfile, - type NDKEventWithReplies, - type NIP05, - type WidgetProps, -} from '@utils/types'; +import { LumeStorage } from '@libs/storage'; +import { Account, type NDKEventWithReplies, type NIP05 } from '@utils/types'; export class Ark { - #storage: Database; - #depot: Child; + #storage: LumeStorage; + #fetcher: NostrFetcher; public ndk: NDK; - public fetcher: NostrFetcher; - public account: Account | null; - public relays: string[] | null; - public readyToSign: boolean; - readonly platform: Platform | null; - readonly settings: { - autoupdate: boolean; - bunker: boolean; - outbox: boolean; - media: boolean; - hashtag: boolean; - depot: boolean; - tunnelUrl: string; - }; + public account: Account; - constructor({ storage, platform }: { storage: Database; platform: Platform }) { + constructor({ + ndk, + storage, + + fetcher, + }: { + ndk: NDK; + storage: LumeStorage; + + fetcher: NostrFetcher; + }) { + this.ndk = ndk; this.#storage = storage; - this.platform = platform; - this.settings = { - autoupdate: false, - bunker: false, - outbox: false, - media: true, - hashtag: true, - depot: false, - tunnelUrl: '', - }; - } - - public async launchDepot() { - const configPath = await resolveResource('resources/config.toml'); - const dataPath = await appConfigDir(); - - const command = Command.sidecar('bin/depot', ['-c', configPath, '-d', dataPath]); - this.#depot = await command.spawn(); + this.#fetcher = fetcher; } public async connectDepot() { - if (!this.#depot) return; return this.ndk.addExplicitRelay( new NDKRelay(normalizeRelayUrl('ws://localhost:6090')), undefined, @@ -87,349 +48,11 @@ export class Ark { ); } - public checkDepot() { - if (this.#depot) return true; - return false; - } - - async #keyring_save(key: string, value: string) { - return await invoke('secure_save', { key, value }); - } - - async #keyring_load(key: string) { - try { - const value: string = await invoke('secure_load', { key }); - if (!value) return null; - return value; - } catch { - return null; - } - } - - async #keyring_remove(key: string) { - return await invoke('secure_remove', { key }); - } - - async #initNostrSigner({ nsecbunker }: { nsecbunker?: boolean }) { - const account = await this.getActiveAccount(); - if (!account) return null; - - // update active account - this.account = account; - - try { - // NIP-46 Signer - if (nsecbunker) { - const localSignerPrivkey = await this.#keyring_load( - `${this.account.id}-nsecbunker` - ); - - if (!localSignerPrivkey) { - this.readyToSign = false; - return null; - } - - const localSigner = new NDKPrivateKeySigner(localSignerPrivkey); - const bunker = new NDK({ - explicitRelayUrls: normalizeRelayUrlSet([ - 'wss://relay.nsecbunker.com/', - 'wss://nostr.vulpem.com/', - ]), - }); - await bunker.connect(3000); - - const remoteSigner = new NDKNip46Signer(bunker, this.account.pubkey, localSigner); - await remoteSigner.blockUntilReady(); - - this.readyToSign = true; - return remoteSigner; - } - - // Privkey Signer - const userPrivkey = await this.#keyring_load(this.account.pubkey); - - if (!userPrivkey) { - this.readyToSign = false; - return null; - } - - this.readyToSign = true; - return new NDKPrivateKeySigner(userPrivkey); - } catch (e) { - console.log(e); - return null; - } - } - - public async init() { - const settings = await this.getAllSettings(); - - for (const item of settings) { - if (item.key === 'nsecbunker') this.settings.bunker = !!parseInt(item.value); - if (item.key === 'outbox') this.settings.outbox = !!parseInt(item.value); - if (item.key === 'media') this.settings.media = !!parseInt(item.value); - if (item.key === 'hashtag') this.settings.hashtag = !!parseInt(item.value); - if (item.key === 'autoupdate') this.settings.autoupdate = !!parseInt(item.value); - if (item.key === 'depot') this.settings.depot = !!parseInt(item.value); - if (item.key === 'tunnel_url') this.settings.tunnelUrl = item.value; - } - - const explicitRelayUrls = normalizeRelayUrlSet([ - 'wss://relay.damus.io', - 'wss://relay.nostr.band/all', - 'wss://nostr.mutinywallet.com', - ]); - - if (this.settings.depot) { - await this.launchDepot(); - await delay(2000); - - explicitRelayUrls.push(normalizeRelayUrl('ws://localhost:6090')); - } - - // #TODO: user should config outbox relays - const outboxRelayUrls = normalizeRelayUrlSet(['wss://purplepag.es']); - - // #TODO: user should config blacklist relays - // No need to connect depot tunnel url - const blacklistRelayUrls = this.settings.tunnelUrl.length - ? [this.settings.tunnelUrl, this.settings.tunnelUrl + '/'] - : []; - - const cacheAdapter = new NDKCacheAdapterTauri(this.#storage); - const ndk = new NDK({ - cacheAdapter, - explicitRelayUrls, - outboxRelayUrls, - blacklistRelayUrls, - enableOutboxModel: this.settings.outbox, - autoConnectUserRelays: true, - autoFetchUserMutelist: true, - // clientName: 'Lume', - // clientNip89: '', - }); - - // add signer if exist - const signer = await this.#initNostrSigner({ nsecbunker: this.settings.bunker }); - if (signer) ndk.signer = signer; - - // connect - await ndk.connect(3000); - const fetcher = NostrFetcher.withCustomPool(ndkAdapter(ndk)); - - // update account's metadata - if (this.account) { - const user = ndk.getUser({ pubkey: this.account.pubkey }); - ndk.activeUser = user; - - const contacts = await user.follows(); - this.account.contacts = [...contacts].map((user) => user.pubkey); - } - - this.relays = [...ndk.pool.relays.values()].map((relay) => relay.url); - this.ndk = ndk; - this.fetcher = fetcher; - } - public updateNostrSigner({ signer }: { signer: NDKNip46Signer | NDKPrivateKeySigner }) { this.ndk.signer = signer; - this.readyToSign = true; return this.ndk.signer; } - public async getAllCacheUsers() { - const results: Array = await this.#storage.select( - 'SELECT * FROM ndk_users ORDER BY createdAt DESC;' - ); - - if (!results.length) return []; - - const users: NDKCacheUserProfile[] = results.map((item) => ({ - pubkey: item.pubkey, - ...JSON.parse(item.profile as string), - })); - return users; - } - - public async checkAccount() { - const result: Array<{ total: string }> = await this.#storage.select( - 'SELECT COUNT(*) AS "total" FROM accounts WHERE is_active = "1" ORDER BY id DESC LIMIT 1;' - ); - return parseInt(result[0].total); - } - - public async getActiveAccount() { - const results: Array = await this.#storage.select( - 'SELECT * FROM accounts WHERE is_active = "1" ORDER BY id DESC LIMIT 1;' - ); - - if (results.length) { - return results[0]; - } else { - return null; - } - } - - public async createAccount({ - id, - pubkey, - privkey, - }: { - id: string; - pubkey: string; - privkey?: string; - }) { - const existAccounts: Array = await this.#storage.select( - 'SELECT * FROM accounts WHERE pubkey = $1 ORDER BY id DESC LIMIT 1;', - [pubkey] - ); - - if (existAccounts.length) { - await this.#storage.execute( - "UPDATE accounts SET is_active = '1' WHERE pubkey = $1;", - [pubkey] - ); - } else { - await this.#storage.execute( - 'INSERT OR IGNORE INTO accounts (id, pubkey, is_active) VALUES ($1, $2, $3);', - [id, pubkey, 1] - ); - - if (privkey) await this.#keyring_save(pubkey, privkey); - } - - const account = await this.getActiveAccount(); - this.account = account; - this.account.contacts = []; - - return account; - } - - /** - * Save private key to OS secure storage - * @deprecated this method will be remove in the next update - */ - public async createPrivkey(name: string, privkey: string) { - return await this.#keyring_save(name, privkey); - } - - /** - * Load private key from OS secure storage - * @deprecated this method will be remove in the next update - */ - public async loadPrivkey(name: string) { - return await this.#keyring_load(name); - } - - /** - * Remove private key from OS secure storage - * @deprecated this method will be remove in the next update - */ - public async removePrivkey(name: string) { - return await this.#keyring_remove(name); - } - - public async updateAccount(column: string, value: string) { - const insert = await this.#storage.execute( - `UPDATE accounts SET ${column} = $1 WHERE id = $2;`, - [value, this.account.id] - ); - - if (insert) { - const account = await this.getActiveAccount(); - return account; - } - } - - public async getWidgets() { - const widgets: Array = await this.#storage.select( - 'SELECT * FROM widgets WHERE account_id = $1 ORDER BY created_at DESC;', - [this.account.id] - ); - return widgets; - } - - public async createWidget(kind: number, title: string, content: string | string[]) { - const insert = await this.#storage.execute( - 'INSERT INTO widgets (account_id, kind, title, content) VALUES ($1, $2, $3, $4);', - [this.account.id, kind, title, content] - ); - - if (insert) { - const widgets: Array = await this.#storage.select( - 'SELECT * FROM widgets ORDER BY id DESC LIMIT 1;' - ); - if (widgets.length < 1) console.error('get created widget failed'); - return widgets[0]; - } else { - console.error('create widget failed'); - } - } - - public async removeWidget(id: string) { - const res = await this.#storage.execute('DELETE FROM widgets WHERE id = $1;', [id]); - if (res) return id; - } - - public async createSetting(key: string, value: string | undefined) { - const currentSetting = await this.checkSettingValue(key); - - if (!currentSetting) { - return await this.#storage.execute( - 'INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);', - [key, value] - ); - } - - return await this.#storage.execute('UPDATE settings SET value = $1 WHERE key = $2;', [ - value, - key, - ]); - } - - public async getAllSettings() { - const results: { key: string; value: string }[] = await this.#storage.select( - 'SELECT * FROM settings ORDER BY id DESC;' - ); - if (results.length < 1) return []; - return results; - } - - public async checkSettingValue(key: string) { - const results: { key: string; value: string }[] = await this.#storage.select( - 'SELECT * FROM settings WHERE key = $1 ORDER BY id DESC LIMIT 1;', - [key] - ); - if (!results.length) return false; - return results[0].value; - } - - public async getSettingValue(key: string) { - const results: { key: string; value: string }[] = await this.#storage.select( - 'SELECT * FROM settings WHERE key = $1 ORDER BY id DESC LIMIT 1;', - [key] - ); - if (!results.length) return '0'; - return results[0].value; - } - - public async clearCache() { - await this.#storage.execute('DELETE FROM ndk_events;'); - await this.#storage.execute('DELETE FROM ndk_eventtags;'); - await this.#storage.execute('DELETE FROM ndk_users;'); - } - - public async logout() { - await this.#keyring_remove(this.account.pubkey); - await this.#keyring_remove(`${this.account.pubkey}-nsecbunker`); - await this.#storage.execute("UPDATE accounts SET is_active = '0' WHERE id = $1;", [ - this.account.id, - ]); - - this.account = null; - this.ndk.signer = null; - } - public subscribe({ filter, closeOnEose = false, @@ -520,37 +143,52 @@ export class Ark { outbox?: boolean; }) { try { - const user = this.ndk.getUser({ pubkey: pubkey ? pubkey : this.account.pubkey }); + const user = this.ndk.getUser({ + pubkey: pubkey ? pubkey : this.#storage.account.pubkey, + }); const contacts = [...(await user.follows(undefined, outbox))].map( (user) => user.pubkey ); - if (pubkey === this.account.pubkey) this.account.contacts = contacts; + if (pubkey === this.#storage.account.pubkey) + this.#storage.account.contacts = contacts; return contacts; } catch (e) { throw new Error(e); - return []; } } public async getUserRelays({ pubkey }: { pubkey?: string }) { try { - const user = this.ndk.getUser({ pubkey: pubkey ? pubkey : this.account.pubkey }); + const user = this.ndk.getUser({ + pubkey: pubkey ? pubkey : this.#storage.account.pubkey, + }); return await user.relayList(); } catch (e) { throw new Error(e); - return null; + } + } + + public async newContactList({ tags }: { tags: NDKTag[] }) { + const publish = await this.createEvent({ + kind: NDKKind.Contacts, + tags: tags, + }); + + if (publish) { + this.#storage.account.contacts = tags.map((item) => item[1]); + return publish; } } public async createContact({ pubkey }: { pubkey: string }) { - const user = this.ndk.getUser({ pubkey: this.account.pubkey }); + const user = this.ndk.getUser({ pubkey: this.#storage.account.pubkey }); const contacts = await user.follows(); return await user.follow(new NDKUser({ pubkey: pubkey }), contacts); } public async deleteContact({ pubkey }: { pubkey: string }) { - const user = this.ndk.getUser({ pubkey: this.account.pubkey }); + const user = this.ndk.getUser({ pubkey: this.#storage.account.pubkey }); const contacts = await user.follows(); contacts.delete(new NDKUser({ pubkey: pubkey })); @@ -644,7 +282,7 @@ export class Ark { if (!data) { const relayUrls = [...this.ndk.pool.relays.values()].map((item) => item.url); - const rawEvents = (await this.fetcher.fetchAllEvents( + const rawEvents = (await this.#fetcher.fetchAllEvents( relayUrls, { kinds: [NDKKind.Text], @@ -686,11 +324,12 @@ export class Ark { public async getAllRelaysFromContacts() { const LIMIT = 1; + const connectedRelays = this.ndk.pool.connectedRelays().map((item) => item.url); const relayMap = new Map(); - const relayEvents = this.fetcher.fetchLatestEventsPerAuthor( + const relayEvents = this.#fetcher.fetchLatestEventsPerAuthor( { - authors: this.account.contacts, - relayUrls: this.relays, + authors: this.#storage.account.contacts, + relayUrls: connectedRelays, }, { kinds: [NDKKind.RelayList] }, LIMIT @@ -725,8 +364,9 @@ export class Ark { }) { const rootIds = new Set(); const dedupQueue = new Set(); + const connectedRelays = this.ndk.pool.connectedRelays().map((item) => item.url); - const events = await this.fetcher.fetchLatestEvents(this.relays, filter, limit, { + const events = await this.#fetcher.fetchLatestEvents(connectedRelays, filter, limit, { asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal, }); @@ -767,7 +407,7 @@ export class Ark { signal?: AbortSignal; dedup?: boolean; }) { - const events = await this.fetcher.fetchLatestEvents( + const events = await this.#fetcher.fetchLatestEvents( [normalizeRelayUrl(relayUrl)], filter, limit, @@ -856,107 +496,6 @@ export class Ark { return false; } - /** - * Return all NIP-04 messages - * @deprecated NIP-04 will be replace by NIP-44 in the next update - */ - public async getAllChats() { - const events = await this.fetcher.fetchAllEvents( - this.relays, - { - kinds: [NDKKind.EncryptedDirectMessage], - '#p': [this.account.pubkey], - }, - { since: 0 } - ); - - const dedup: NDKEvent[] = Object.values( - events.reduce((ev, { id, content, pubkey, created_at, tags }) => { - if (ev[pubkey]) { - if (ev[pubkey].created_at < created_at) { - ev[pubkey] = { id, content, pubkey, created_at, tags }; - } - } else { - ev[pubkey] = { id, content, pubkey, created_at, tags }; - } - return ev; - }, {}) - ); - - return dedup; - } - - /** - * Return all NIP-04 messages by pubkey - * @deprecated NIP-04 will be replace by NIP-44 in the next update - */ - public async getAllMessagesByPubkey({ pubkey }: { pubkey: string }) { - let senderMessages: NostrEventExt[] = []; - - if (pubkey !== this.account.pubkey) { - senderMessages = await this.fetcher.fetchAllEvents( - this.relays, - { - kinds: [NDKKind.EncryptedDirectMessage], - authors: [pubkey], - '#p': [this.account.pubkey], - }, - { since: 0 } - ); - } - - const userMessages = await this.fetcher.fetchAllEvents( - this.relays, - { - kinds: [NDKKind.EncryptedDirectMessage], - authors: [this.account.pubkey], - '#p': [pubkey], - }, - { since: 0 } - ); - - const all = [...senderMessages, ...userMessages].sort( - (a, b) => a.created_at - b.created_at - ); - - return all as unknown as NDKEvent[]; - } - - public async nip04Decrypt({ event }: { event: NDKEvent }) { - try { - const sender = new NDKUser({ - pubkey: - this.account.pubkey === event.pubkey - ? event.tags.find((el) => el[0] === 'p')[1] - : event.pubkey, - }); - const content = await this.ndk.signer.decrypt(sender, event.content); - - return content; - } catch (e) { - throw new Error(e); - } - } - - public async nip04Encrypt({ content, pubkey }: { content: string; pubkey: string }) { - try { - const recipient = new NDKUser({ pubkey }); - const message = await this.ndk.signer.encrypt(recipient, content); - - const event = new NDKEvent(this.ndk); - event.content = message; - event.kind = NDKKind.EncryptedDirectMessage; - event.tag(recipient); - - const publish = await event.publish(); - - if (!publish) throw new Error('Failed to send NIP-04 encrypted message'); - return { id: event.id, seens: [...publish.values()].map((item) => item.url) }; - } catch (e) { - throw new Error(e); - } - } - public async replyTo({ content, event }: { content: string; event: NDKEvent }) { try { const replyEvent = new NDKEvent(this.ndk); diff --git a/src/libs/ark/components/note/builds/reply.tsx b/src/libs/ark/components/note/builds/reply.tsx new file mode 100644 index 00000000..f4a853ba --- /dev/null +++ b/src/libs/ark/components/note/builds/reply.tsx @@ -0,0 +1,67 @@ +import * as Collapsible from '@radix-ui/react-collapsible'; +import { useState } from 'react'; +import { twMerge } from 'tailwind-merge'; +import { NavArrowDownIcon } from '@shared/icons'; +import { User } from '@shared/user'; +import { NDKEventWithReplies } from '@utils/types'; +import { Note } from '..'; + +export function Reply({ + event, + rootEvent, +}: { + event: NDKEventWithReplies; + rootEvent: string; +}) { + const [open, setOpen] = useState(false); + + return ( + + + + +
+ {event.replies?.length > 0 ? ( + +
+ + {event.replies?.length + + ' ' + + (event.replies?.length === 1 ? 'reply' : 'replies')} +
+
+ ) : null} +
+ + + + +
+
+
+ {event.replies?.length > 0 ? ( + + {event.replies?.map((childEvent) => ( + + + +
+ +
+ + + + +
+
+
+ ))} +
+ ) : null} +
+
+
+ ); +} diff --git a/src/shared/notes/repost.tsx b/src/libs/ark/components/note/builds/repost.tsx similarity index 51% rename from src/shared/notes/repost.tsx rename to src/libs/ark/components/note/builds/repost.tsx index 4a0a7a2d..2100f968 100644 --- a/src/shared/notes/repost.tsx +++ b/src/libs/ark/components/note/builds/repost.tsx @@ -1,17 +1,9 @@ import { NDKEvent, NDKKind, NostrEvent } from '@nostr-dev-kit/ndk'; import { useQuery } from '@tanstack/react-query'; -import { memo } from 'react'; -import { useArk } from '@libs/ark'; -import { - MemoizedArticleKind, - MemoizedFileKind, - MemoizedTextKind, - NoteActions, - NoteSkeleton, -} from '@shared/notes'; -import { User } from '@shared/user'; +import { useArk } from '@libs/ark/provider'; +import { Note } from '..'; -export function Repost({ event }: { event: NDKEvent }) { +export function RepostNote({ event }: { event: NDKEvent }) { const ark = useArk(); const { isLoading, @@ -25,7 +17,6 @@ export function Repost({ event }: { event: NDKEvent }) { const embed = JSON.parse(event.content) as NostrEvent; return new NDKEvent(ark.ndk, embed); } - const id = event.tags.find((el) => el[0] === 'e')[1]; return await ark.getEventById({ id }); } catch { @@ -39,29 +30,22 @@ export function Repost({ event }: { event: NDKEvent }) { if (!repostEvent) return null; switch (repostEvent.kind) { case NDKKind.Text: - return ; + return ; case 1063: - return ; - case NDKKind.Article: - return ; + return ; default: return null; } }; if (isLoading) { - return ( -
- -
- ); + return
; } if (isError) { return (
-

Failed to load event

@@ -73,21 +57,26 @@ export function Repost({ event }: { event: NDKEvent }) { } return ( -
-
- -
- - {renderContentByKind()} - + + +
+ + {renderContentByKind()} +
+ +
+ + + + +
-
+ ); } - -export const MemoizedRepost = memo(Repost); diff --git a/src/libs/ark/components/note/builds/skeleton.tsx b/src/libs/ark/components/note/builds/skeleton.tsx new file mode 100644 index 00000000..6b52ca9b --- /dev/null +++ b/src/libs/ark/components/note/builds/skeleton.tsx @@ -0,0 +1,24 @@ +import { Note } from '..'; + +export function NoteSkeleton() { + return ( + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + ); +} diff --git a/src/libs/ark/components/note/builds/text.tsx b/src/libs/ark/components/note/builds/text.tsx new file mode 100644 index 00000000..483a5b6b --- /dev/null +++ b/src/libs/ark/components/note/builds/text.tsx @@ -0,0 +1,25 @@ +import { NDKEvent } from '@nostr-dev-kit/ndk'; +import { useArk } from '@libs/ark/provider'; +import { Note } from '..'; + +export function TextNote({ event }: { event: NDKEvent }) { + const ark = useArk(); + const thread = ark.getEventThread({ tags: event.tags }); + + return ( + + + + +
+ +
+ + + + +
+
+
+ ); +} diff --git a/src/libs/ark/components/note/pin.tsx b/src/libs/ark/components/note/buttons/pin.tsx similarity index 76% rename from src/libs/ark/components/note/pin.tsx rename to src/libs/ark/components/note/buttons/pin.tsx index 836e4337..09b906dc 100644 --- a/src/libs/ark/components/note/pin.tsx +++ b/src/libs/ark/components/note/buttons/pin.tsx @@ -1,14 +1,24 @@ import * as Tooltip from '@radix-ui/react-tooltip'; +import { useWidget } from '@libs/ark'; import { PinIcon } from '@shared/icons'; +import { WIDGET_KIND } from '@utils/constants'; + +export function NotePin({ eventId }: { eventId: string }) { + const { addWidget } = useWidget(); -export function NotePin({ action }: { action: () => void }) { return ( + + + + Quick reply + + + + + + ); +} diff --git a/src/libs/ark/components/note/repost.tsx b/src/libs/ark/components/note/buttons/repost.tsx similarity index 100% rename from src/libs/ark/components/note/repost.tsx rename to src/libs/ark/components/note/buttons/repost.tsx diff --git a/src/libs/ark/components/note/zap.tsx b/src/libs/ark/components/note/buttons/zap.tsx similarity index 98% rename from src/libs/ark/components/note/zap.tsx rename to src/libs/ark/components/note/buttons/zap.tsx index fcab79b3..cf0b2a4e 100644 --- a/src/libs/ark/components/note/zap.tsx +++ b/src/libs/ark/components/note/buttons/zap.tsx @@ -8,7 +8,7 @@ import { QRCodeSVG } from 'qrcode.react'; import { useEffect, useRef, useState } from 'react'; import CurrencyInput from 'react-currency-input-field'; import { useNavigate } from 'react-router-dom'; -import { useArk } from '@libs/ark'; +import { useArk, useStorage } from '@libs/ark'; import { CancelIcon, ZapIcon } from '@shared/icons'; import { compactNumber, displayNpub } from '@utils/formater'; import { useProfile } from '@utils/hooks/useProfile'; @@ -26,12 +26,13 @@ export function NoteZap({ event }: { event: NDKEvent }) { const { user } = useProfile(event.pubkey); const ark = useArk(); + const storage = useStorage(); const nwc = useRef(null); const navigate = useNavigate(); const createZapRequest = async () => { try { - if (!ark.readyToSign) return navigate('/new/privkey'); + if (!ark.ndk.signer) return navigate('/new/privkey'); const zapAmount = parseInt(amount) * 1000; const res = await event.zap(zapAmount, zapMessage); @@ -82,7 +83,7 @@ export function NoteZap({ event }: { event: NDKEvent }) { useEffect(() => { async function getWalletConnectURL() { const uri: string = await invoke('secure_load', { - key: `${ark.account.pubkey}-nwc`, + key: `${storage.account.pubkey}-nwc`, }); if (uri) setWalletConnectURL(uri); } diff --git a/src/libs/ark/components/note/child.tsx b/src/libs/ark/components/note/child.tsx index a5e81388..0b2ed817 100644 --- a/src/libs/ark/components/note/child.tsx +++ b/src/libs/ark/components/note/child.tsx @@ -1,27 +1,8 @@ -import { useQuery } from '@tanstack/react-query'; -import { useArk } from '@libs/ark'; +import { useEvent } from '@libs/ark'; import { NoteChildUser } from './childUser'; export function NoteChild({ eventId, isRoot }: { eventId: string; isRoot?: boolean }) { - const ark = useArk(); - const { isLoading, isError, data } = useQuery({ - queryKey: ['event', eventId], - queryFn: async () => { - // get event from relay - const event = await ark.getEventById({ id: eventId }); - - if (!event) - throw new Error( - `Cannot get event with ${eventId}, will be retry after 10 seconds` - ); - - return event; - }, - refetchOnWindowFocus: false, - refetchOnMount: false, - refetchOnReconnect: false, - retry: 2, - }); + const { isLoading, isError, data } = useEvent(eventId); if (isLoading) { return ( diff --git a/src/libs/ark/components/note/childUser.tsx b/src/libs/ark/components/note/childUser.tsx index c260d31d..b58ac75f 100644 --- a/src/libs/ark/components/note/childUser.tsx +++ b/src/libs/ark/components/note/childUser.tsx @@ -1,39 +1,17 @@ import * as Avatar from '@radix-ui/react-avatar'; -import { useQuery } from '@tanstack/react-query'; import { minidenticon } from 'minidenticons'; import { useMemo } from 'react'; -import { useArk } from '@libs/ark'; +import { useProfile } from '@libs/ark'; import { displayNpub } from '@utils/formater'; export function NoteChildUser({ pubkey, subtext }: { pubkey: string; subtext: string }) { - const ark = useArk(); const fallbackName = useMemo(() => displayNpub(pubkey, 16), [pubkey]); const fallbackAvatar = useMemo( () => 'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(pubkey, 90, 50)), [pubkey] ); - const { isLoading, data: user } = useQuery({ - queryKey: ['user', pubkey], - queryFn: async () => { - try { - const profile = await ark.getUserProfile({ pubkey }); - - if (!profile) - throw new Error( - `Cannot get metadata for ${pubkey}, will be retry after 10 seconds` - ); - - return profile; - } catch (e) { - throw new Error(e); - } - }, - refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: 2, - }); + const { isLoading, user } = useProfile(pubkey); if (isLoading) { return ( diff --git a/src/libs/ark/components/note/index.ts b/src/libs/ark/components/note/index.ts index 14609f4e..c9ab8f90 100644 --- a/src/libs/ark/components/note/index.ts +++ b/src/libs/ark/components/note/index.ts @@ -1,23 +1,42 @@ +import { NotePin } from './buttons/pin'; +import { NoteReaction } from './buttons/reaction'; +import { NoteReply } from './buttons/reply'; +import { NoteRepost } from './buttons/repost'; +import { NoteZap } from './buttons/zap'; import { NoteChild } from './child'; -import { NoteKind } from './kind'; +import { NoteArticleContent } from './kinds/article'; +import { NoteMediaContent } from './kinds/media'; +import { NoteTextContent } from './kinds/text'; import { NoteMenu } from './menu'; -import { NotePin } from './pin'; -import { NoteReaction } from './reaction'; -import { NoteReply } from './reply'; -import { NoteRepost } from './repost'; +import { NoteReplies } from './reply'; import { NoteRoot } from './root'; +import { NoteThread } from './thread'; import { NoteUser } from './user'; -import { NoteZap } from './zap'; export const Note = { Root: NoteRoot, User: NoteUser, Menu: NoteMenu, - Kind: NoteKind, Reply: NoteReply, Repost: NoteRepost, Reaction: NoteReaction, Zap: NoteZap, Pin: NotePin, Child: NoteChild, + Thread: NoteThread, + TextContent: NoteTextContent, + MediaContent: NoteMediaContent, + ArticleContent: NoteArticleContent, + Replies: NoteReplies, }; + +export * from './builds/text'; +export * from './builds/repost'; +export * from './builds/skeleton'; +export * from './preview/image'; +export * from './preview/link'; +export * from './preview/video'; +export * from './mentions/note'; +export * from './mentions/user'; +export * from './mentions/hashtag'; +export * from './mentions/invoice'; diff --git a/src/shared/notes/kinds/article.tsx b/src/libs/ark/components/note/kinds/article.tsx similarity index 89% rename from src/shared/notes/kinds/article.tsx rename to src/libs/ark/components/note/kinds/article.tsx index 50e34a84..2235797f 100644 --- a/src/shared/notes/kinds/article.tsx +++ b/src/libs/ark/components/note/kinds/article.tsx @@ -1,8 +1,13 @@ import { NDKTag } from '@nostr-dev-kit/ndk'; -import { memo } from 'react'; import { Link } from 'react-router-dom'; -export function ArticleKind({ id, tags }: { id: string; tags: NDKTag[] }) { +export function NoteArticleContent({ + eventId, + tags, +}: { + eventId: string; + tags: NDKTag[]; +}) { const getMetadata = () => { const title = tags.find((tag) => tag[0] === 'title')?.[1]; const image = tags.find((tag) => tag[0] === 'image')?.[1]; @@ -26,7 +31,7 @@ export function ArticleKind({ id, tags }: { id: string; tags: NDKTag[] }) { return ( @@ -56,5 +61,3 @@ export function ArticleKind({ id, tags }: { id: string; tags: NDKTag[] }) { ); } - -export const MemoizedArticleKind = memo(ArticleKind); diff --git a/src/shared/notes/kinds/file.tsx b/src/libs/ark/components/note/kinds/media.tsx similarity index 66% rename from src/shared/notes/kinds/file.tsx rename to src/libs/ark/components/note/kinds/media.tsx index e49fb9e0..2b58e891 100644 --- a/src/shared/notes/kinds/file.tsx +++ b/src/libs/ark/components/note/kinds/media.tsx @@ -6,12 +6,18 @@ import { DefaultVideoLayout, defaultLayoutIcons, } from '@vidstack/react/player/layouts/default'; -import { memo } from 'react'; import { Link } from 'react-router-dom'; +import { twMerge } from 'tailwind-merge'; import { DownloadIcon } from '@shared/icons'; import { fileType } from '@utils/nip94'; -export function FileKind({ tags }: { tags: NDKTag[] }) { +export function NoteMediaContent({ + tags, + className, +}: { + tags: NDKTag[]; + className?: string; +}) { const url = tags.find((el) => el[0] === 'url')[1]; const type = fileType(url); @@ -23,7 +29,7 @@ export function FileKind({ tags }: { tags: NDKTag[] }) { if (type === 'image') { return ( -
+
{url} - - - +
+ + + + +
); } return ( - - {url} - +
+ + {url} + +
); } - -export const MemoizedFileKind = memo(FileKind); diff --git a/src/libs/ark/components/note/kind.tsx b/src/libs/ark/components/note/kinds/text.tsx similarity index 64% rename from src/libs/ark/components/note/kind.tsx rename to src/libs/ark/components/note/kinds/text.tsx index 0ee9612d..e7024953 100644 --- a/src/libs/ark/components/note/kind.tsx +++ b/src/libs/ark/components/note/kinds/text.tsx @@ -1,7 +1,7 @@ import { twMerge } from 'tailwind-merge'; -import { useRichContent } from '@utils/hooks/useRichContent'; +import { useRichContent } from '@libs/ark'; -export function NoteKind({ +export function NoteTextContent({ content, className, }: { @@ -13,7 +13,7 @@ export function NoteKind({ return (
diff --git a/src/shared/notes/mentions/hashtag.tsx b/src/libs/ark/components/note/mentions/hashtag.tsx similarity index 89% rename from src/shared/notes/mentions/hashtag.tsx rename to src/libs/ark/components/note/mentions/hashtag.tsx index 74a0d265..50905f01 100644 --- a/src/shared/notes/mentions/hashtag.tsx +++ b/src/libs/ark/components/note/mentions/hashtag.tsx @@ -1,5 +1,5 @@ +import { useWidget } from '@libs/ark/hooks/useWidget'; import { WIDGET_KIND } from '@utils/constants'; -import { useWidget } from '@utils/hooks/useWidget'; export function Hashtag({ tag }: { tag: string }) { const { addWidget } = useWidget(); diff --git a/src/shared/notes/mentions/invoice.tsx b/src/libs/ark/components/note/mentions/invoice.tsx similarity index 100% rename from src/shared/notes/mentions/invoice.tsx rename to src/libs/ark/components/note/mentions/invoice.tsx diff --git a/src/libs/ark/components/note/mentions/note.tsx b/src/libs/ark/components/note/mentions/note.tsx new file mode 100644 index 00000000..e5a803e5 --- /dev/null +++ b/src/libs/ark/components/note/mentions/note.tsx @@ -0,0 +1,63 @@ +import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; +import { memo } from 'react'; +import { useEvent, useWidget } from '@libs/ark'; +import { WIDGET_KIND } from '@utils/constants'; +import { Note } from '..'; + +export const MentionNote = memo(function MentionNote({ eventId }: { eventId: string }) { + const { isLoading, isError, data } = useEvent(eventId); + const { addWidget } = useWidget(); + + const renderKind = (event: NDKEvent) => { + switch (event.kind) { + case NDKKind.Text: + return ; + case NDKKind.Article: + return ; + case 1063: + return ; + default: + return ; + } + }; + + if (isLoading) { + return ( +
+ Loading +
+ ); + } + + if (isError) { + return ( +
+ Failed to fetch event +
+ ); + } + + return ( + +
+ +
+
+ {renderKind(data)} + +
+
+ ); +}); diff --git a/src/shared/notes/mentions/user.tsx b/src/libs/ark/components/note/mentions/user.tsx similarity index 86% rename from src/shared/notes/mentions/user.tsx rename to src/libs/ark/components/note/mentions/user.tsx index 6aee06fb..c1dae88c 100644 --- a/src/shared/notes/mentions/user.tsx +++ b/src/libs/ark/components/note/mentions/user.tsx @@ -1,7 +1,6 @@ import { memo } from 'react'; +import { useProfile, useWidget } from '@libs/ark'; import { WIDGET_KIND } from '@utils/constants'; -import { useProfile } from '@utils/hooks/useProfile'; -import { useWidget } from '@utils/hooks/useWidget'; export const MentionUser = memo(function MentionUser({ pubkey }: { pubkey: string }) { const { user } = useProfile(pubkey); diff --git a/src/shared/notes/preview/image.tsx b/src/libs/ark/components/note/preview/image.tsx similarity index 100% rename from src/shared/notes/preview/image.tsx rename to src/libs/ark/components/note/preview/image.tsx diff --git a/src/shared/notes/preview/link.tsx b/src/libs/ark/components/note/preview/link.tsx similarity index 100% rename from src/shared/notes/preview/link.tsx rename to src/libs/ark/components/note/preview/link.tsx diff --git a/src/shared/notes/preview/video.tsx b/src/libs/ark/components/note/preview/video.tsx similarity index 100% rename from src/shared/notes/preview/video.tsx rename to src/libs/ark/components/note/preview/video.tsx diff --git a/src/libs/ark/components/note/reply.tsx b/src/libs/ark/components/note/reply.tsx index cc7dbd51..1b955c60 100644 --- a/src/libs/ark/components/note/reply.tsx +++ b/src/libs/ark/components/note/reply.tsx @@ -1,43 +1,67 @@ -import * as Tooltip from '@radix-ui/react-tooltip'; -import { createSearchParams, useNavigate } from 'react-router-dom'; -import { ReplyIcon } from '@shared/icons'; +import { NDKSubscription } from '@nostr-dev-kit/ndk'; +import { useEffect, useState } from 'react'; +import { useArk } from '@libs/ark'; +import { LoaderIcon } from '@shared/icons'; +import { NDKEventWithReplies } from '@utils/types'; +import { Reply } from './builds/reply'; -export function NoteReply({ - eventId, - rootEventId, -}: { - eventId: string; - rootEventId?: string; -}) { - const navigate = useNavigate(); +export function NoteReplies({ eventId }: { eventId: string }) { + const ark = useArk(); + const [data, setData] = useState(null); + + useEffect(() => { + let sub: NDKSubscription; + let isCancelled = false; + + async function fetchRepliesAndSub() { + const events = await ark.getThreads({ id: eventId }); + if (!isCancelled) { + setData(events); + } + // subscribe for new replies + sub = ark.subscribe({ + filter: { + '#e': [eventId], + since: Math.floor(Date.now() / 1000), + }, + closeOnEose: false, + cb: (event: NDKEventWithReplies) => setData((prev) => [event, ...prev]), + }); + } + + fetchRepliesAndSub(); + + return () => { + isCancelled = true; + if (sub) sub.stop(); + }; + }, [eventId]); + + if (!data) { + return ( +
+
+ +
+
+ ); + } return ( - - - - - - - - Quick reply - - - - - +
+

Replies

+ {data?.length === 0 ? ( +
+
+

👋

+

+ Be the first to Reply! +

+
+
+ ) : ( + data.map((event) => ) + )} +
); } diff --git a/src/libs/ark/components/note/root.tsx b/src/libs/ark/components/note/root.tsx index 6db52ac2..b14a81da 100644 --- a/src/libs/ark/components/note/root.tsx +++ b/src/libs/ark/components/note/root.tsx @@ -9,10 +9,13 @@ export function NoteRoot({ className?: string; }) { return ( -
-
- {children} -
+
+ {children}
); } diff --git a/src/libs/ark/components/note/thread.tsx b/src/libs/ark/components/note/thread.tsx new file mode 100644 index 00000000..897aa9f8 --- /dev/null +++ b/src/libs/ark/components/note/thread.tsx @@ -0,0 +1,38 @@ +import { twMerge } from 'tailwind-merge'; +import { useWidget } from '@libs/ark'; +import { WIDGET_KIND } from '@utils/constants'; +import { Note } from '.'; + +export function NoteThread({ + thread, + className, +}: { + thread: { rootEventId: string; replyEventId: string }; + className?: string; +}) { + const { addWidget } = useWidget(); + + if (!thread) return null; + + return ( +
+
+ {thread.rootEventId ? : null} + {thread.replyEventId ? : null} + +
+
+ ); +} diff --git a/src/libs/ark/components/note/user.tsx b/src/libs/ark/components/note/user.tsx index 12fae21d..434c7955 100644 --- a/src/libs/ark/components/note/user.tsx +++ b/src/libs/ark/components/note/user.tsx @@ -1,9 +1,8 @@ import * as Avatar from '@radix-ui/react-avatar'; -import { useQuery } from '@tanstack/react-query'; import { minidenticon } from 'minidenticons'; import { useMemo } from 'react'; import { twMerge } from 'tailwind-merge'; -import { useArk } from '@libs/ark'; +import { useProfile } from '@libs/ark'; import { RepostIcon } from '@shared/icons'; import { displayNpub, formatCreatedAt } from '@utils/formater'; @@ -15,10 +14,9 @@ export function NoteUser({ }: { pubkey: string; time: number; - variant?: 'text' | 'repost'; + variant?: 'text' | 'repost' | 'mention'; className?: string; }) { - const ark = useArk(); const createdAt = useMemo(() => formatCreatedAt(time), [time]); const fallbackName = useMemo(() => displayNpub(pubkey, 16), [pubkey]); const fallbackAvatar = useMemo( @@ -26,27 +24,58 @@ export function NoteUser({ [pubkey] ); - const { isLoading, data: user } = useQuery({ - queryKey: ['user', pubkey], - queryFn: async () => { - try { - const profile = await ark.getUserProfile({ pubkey }); + const { isLoading, user } = useProfile(pubkey); - if (!profile) - throw new Error( - `Cannot get metadata for ${pubkey}, will be retry after 10 seconds` - ); + if (variant === 'mention') { + if (isLoading) { + return ( +
+ + + +
+
+ {fallbackName} +
+ · + {createdAt} +
+
+ ); + } - return profile; - } catch (e) { - throw new Error(e); - } - }, - refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: 2, - }); + return ( +
+ + + + {pubkey} + + +
+
+ {user?.name || user?.display_name || user?.displayName || fallbackName} +
+ · + {createdAt} +
+
+ ); + } if (variant === 'repost') { if (isLoading) { diff --git a/src/libs/ark/components/widget/header.tsx b/src/libs/ark/components/widget/header.tsx index 80c755da..906e4b30 100644 --- a/src/libs/ark/components/widget/header.tsx +++ b/src/libs/ark/components/widget/header.tsx @@ -1,6 +1,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import { useQueryClient } from '@tanstack/react-query'; import { ReactNode } from 'react'; +import { useWidget } from '@libs/ark'; import { ArrowLeftIcon, ArrowRightIcon, @@ -9,7 +10,6 @@ import { ThreadIcon, TrashIcon, } from '@shared/icons'; -import { useWidget } from '@utils/hooks/useWidget'; export function WidgetHeader({ id, diff --git a/src/libs/ark/components/widget/index.ts b/src/libs/ark/components/widget/index.ts index 11397f42..7a60fa32 100644 --- a/src/libs/ark/components/widget/index.ts +++ b/src/libs/ark/components/widget/index.ts @@ -1,9 +1,11 @@ import { WidgetContent } from './content'; import { WidgetHeader } from './header'; +import { WidgetLive } from './live'; import { WidgetRoot } from './root'; export const Widget = { Root: WidgetRoot, + Live: WidgetLive, Header: WidgetHeader, Content: WidgetContent, }; diff --git a/src/libs/ark/components/widget/live.tsx b/src/libs/ark/components/widget/live.tsx new file mode 100644 index 00000000..46d53b11 --- /dev/null +++ b/src/libs/ark/components/widget/live.tsx @@ -0,0 +1,42 @@ +import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk'; +import { useEffect, useState } from 'react'; +import { useArk } from '@libs/ark/provider'; +import { ChevronUpIcon } from '@shared/icons'; + +export function WidgetLive({ + filter, + onClick, +}: { + filter: NDKFilter; + onClick: () => void; +}) { + const ark = useArk(); + const [events, setEvents] = useState([]); + + useEffect(() => { + const sub = ark.subscribe({ + filter, + closeOnEose: false, + cb: (event: NDKEvent) => setEvents((prev) => [...prev, event]), + }); + + return () => { + if (sub) sub.stop(); + }; + }, []); + + if (!events.length) return null; + + return ( +
+ +
+ ); +} diff --git a/src/utils/hooks/useEvent.ts b/src/libs/ark/hooks/useEvent.ts similarity index 50% rename from src/utils/hooks/useEvent.ts rename to src/libs/ark/hooks/useEvent.ts index 5e09c9ac..115161d4 100644 --- a/src/utils/hooks/useEvent.ts +++ b/src/libs/ark/hooks/useEvent.ts @@ -1,24 +1,14 @@ -import { NDKEvent, NostrEvent } from '@nostr-dev-kit/ndk'; import { useQuery } from '@tanstack/react-query'; import { useArk } from '@libs/ark'; -export function useEvent(id: undefined | string, embed?: undefined | string) { +export function useEvent(id: string) { const ark = useArk(); - const { status, isFetching, isError, data } = useQuery({ + const { status, isLoading, isError, data } = useQuery({ queryKey: ['event', id], queryFn: async () => { - // return embed event (nostr.band api) - if (embed) { - const embedEvent: NostrEvent = JSON.parse(embed); - return new NDKEvent(ark.ndk, embedEvent); - } - - // get event from relay const event = await ark.getEventById({ id }); - if (!event) throw new Error(`Cannot get event with ${id}, will be retry after 10 seconds`); - return event; }, refetchOnWindowFocus: false, @@ -27,5 +17,5 @@ export function useEvent(id: undefined | string, embed?: undefined | string) { retry: 2, }); - return { status, isFetching, isError, data }; + return { status, isLoading, isError, data }; } diff --git a/src/libs/ark/hooks/useProfile.ts b/src/libs/ark/hooks/useProfile.ts new file mode 100644 index 00000000..b01bb355 --- /dev/null +++ b/src/libs/ark/hooks/useProfile.ts @@ -0,0 +1,27 @@ +import { useQuery } from '@tanstack/react-query'; +import { useArk } from '@libs/ark'; + +export function useProfile(pubkey: string) { + const ark = useArk(); + const { + isLoading, + isError, + data: user, + } = useQuery({ + queryKey: ['user', pubkey], + queryFn: async () => { + const profile = await ark.getUserProfile({ pubkey }); + if (!profile) + throw new Error( + `Cannot get metadata for ${pubkey}, will be retry after 10 seconds` + ); + return profile; + }, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + retry: 2, + }); + + return { isLoading, isError, user }; +} diff --git a/src/utils/hooks/useRichContent.tsx b/src/libs/ark/hooks/useRichContent.tsx similarity index 93% rename from src/utils/hooks/useRichContent.tsx rename to src/libs/ark/hooks/useRichContent.tsx index 715280f0..b9409bd9 100644 --- a/src/utils/hooks/useRichContent.tsx +++ b/src/libs/ark/hooks/useRichContent.tsx @@ -3,7 +3,6 @@ import { nip19 } from 'nostr-tools'; import { ReactNode } from 'react'; import { Link } from 'react-router-dom'; import reactStringReplace from 'react-string-replace'; -import { useArk } from '@libs/ark'; import { Hashtag, ImagePreview, @@ -11,7 +10,8 @@ import { MentionNote, MentionUser, VideoPreview, -} from '@shared/notes'; + useStorage, +} from '@libs/ark'; const NOSTR_MENTIONS = [ '@npub1', @@ -54,7 +54,7 @@ const VIDEOS = [ ]; export function useRichContent(content: string, textmode: boolean = false) { - const ark = useArk(); + const storage = useStorage(); let parsedContent: string | ReactNode[] = content.replace(/\n+/g, '\n'); let linkPreview: string; @@ -66,7 +66,7 @@ export function useRichContent(content: string, textmode: boolean = false) { const words = text.split(/( |\n)/); if (!textmode) { - if (ark.settings.media) { + if (storage.settings.media) { images = words.filter((word) => IMAGES.some((el) => word.endsWith(el))); videos = words.filter((word) => VIDEOS.some((el) => word.endsWith(el))); } @@ -98,7 +98,7 @@ export function useRichContent(content: string, textmode: boolean = false) { if (hashtags.length) { hashtags.forEach((hashtag) => { parsedContent = reactStringReplace(parsedContent, hashtag, (match, i) => { - if (ark.settings.hashtag) return ; + if (storage.settings.hashtag) return ; return null; }); }); @@ -111,13 +111,13 @@ export function useRichContent(content: string, textmode: boolean = false) { if (decoded.type === 'note') { parsedContent = reactStringReplace(parsedContent, event, (match, i) => ( - + )); } if (decoded.type === 'nevent') { parsedContent = reactStringReplace(parsedContent, event, (match, i) => ( - + )); } }); diff --git a/src/utils/hooks/useWidget.ts b/src/libs/ark/hooks/useWidget.ts similarity index 64% rename from src/utils/hooks/useWidget.ts rename to src/libs/ark/hooks/useWidget.ts index c17b185a..8a66eeb6 100644 --- a/src/utils/hooks/useWidget.ts +++ b/src/libs/ark/hooks/useWidget.ts @@ -1,22 +1,28 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { useArk } from '@libs/ark'; -import { Widget } from '@utils/types'; +import { useStorage } from '@libs/ark'; +import { WidgetProps } from '@utils/types'; export function useWidget() { - const ark = useArk(); + const storage = useStorage(); const queryClient = useQueryClient(); const addWidget = useMutation({ - mutationFn: async (widget: Widget) => { - return await ark.createWidget(widget.kind, widget.title, widget.content); + mutationFn: async (widget: WidgetProps) => { + return await storage.createWidget(widget.kind, widget.title, widget.content); }, onSuccess: (data) => { - queryClient.setQueryData(['widgets'], (old: Widget[]) => [...old, data]); + queryClient.setQueryData(['widgets'], (old: WidgetProps[]) => [...old, data]); }, }); const replaceWidget = useMutation({ - mutationFn: async ({ currentId, widget }: { currentId: string; widget: Widget }) => { + mutationFn: async ({ + currentId, + widget, + }: { + currentId: string; + widget: WidgetProps; + }) => { // Cancel any outgoing refetches await queryClient.cancelQueries({ queryKey: ['widgets'] }); @@ -24,11 +30,15 @@ export function useWidget() { const prevWidgets = queryClient.getQueryData(['widgets']); // create new widget - await ark.removeWidget(currentId); - const newWidget = await ark.createWidget(widget.kind, widget.title, widget.content); + await storage.removeWidget(currentId); + const newWidget = await storage.createWidget( + widget.kind, + widget.title, + widget.content + ); // Optimistically update to the new value - queryClient.setQueryData(['widgets'], (prev: Widget[]) => [ + queryClient.setQueryData(['widgets'], (prev: WidgetProps[]) => [ ...prev.filter((t) => t.id !== currentId), newWidget, ]); @@ -50,12 +60,12 @@ export function useWidget() { const prevWidgets = queryClient.getQueryData(['widgets']); // Optimistically update to the new value - queryClient.setQueryData(['widgets'], (prev: Widget[]) => + queryClient.setQueryData(['widgets'], (prev: WidgetProps[]) => prev.filter((t) => t.id !== id) ); // Update in database - await ark.removeWidget(id); + await storage.removeWidget(id); // Return a context object with the snapshotted value return { prevWidgets }; diff --git a/src/libs/ark/index.ts b/src/libs/ark/index.ts index 9a1d1c23..8fec2a01 100644 --- a/src/libs/ark/index.ts +++ b/src/libs/ark/index.ts @@ -2,3 +2,7 @@ export * from './ark'; export * from './provider'; export * from './components/widget'; export * from './components/note'; +export * from './hooks/useWidget'; +export * from './hooks/useRichContent'; +export * from './hooks/useEvent'; +export * from './hooks/useProfile'; diff --git a/src/libs/ark/provider.tsx b/src/libs/ark/provider.tsx index 8df21b71..19246430 100644 --- a/src/libs/ark/provider.tsx +++ b/src/libs/ark/provider.tsx @@ -1,57 +1,174 @@ -import { ask } from '@tauri-apps/plugin-dialog'; +import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; +import { ndkAdapter } from '@nostr-fetch/adapter-ndk'; import { platform } from '@tauri-apps/plugin-os'; import { relaunch } from '@tauri-apps/plugin-process'; import Database from '@tauri-apps/plugin-sql'; import { check } from '@tauri-apps/plugin-updater'; import Markdown from 'markdown-to-jsx'; -import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'; +import { NostrFetcher, normalizeRelayUrl, normalizeRelayUrlSet } from 'nostr-fetch'; +import { PropsWithChildren, useEffect, useState } from 'react'; +import { createContext, useContextSelector } from 'use-context-selector'; import { Ark } from '@libs/ark'; +import { NDKCacheAdapterTauri } from '@libs/cache'; +import { LumeStorage } from '@libs/storage'; import { LoaderIcon } from '@shared/icons'; import { QUOTES } from '@utils/constants'; +import { delay } from '@utils/delay'; -const ArkContext = createContext(undefined); +type Context = { + storage: LumeStorage; + ark: Ark; +}; -const ArkProvider = ({ children }: PropsWithChildren) => { - const [ark, setArk] = useState(undefined); +const LumeContext = createContext({ + storage: undefined, + ark: undefined, +}); + +const LumeProvider = ({ children }: PropsWithChildren) => { + const [context, setContext] = useState(undefined); const [isNewVersion, setIsNewVersion] = useState(false); - async function initArk() { + async function initNostrSigner({ + storage, + nsecbunker, + }: { + storage: LumeStorage; + nsecbunker?: boolean; + }) { try { - const sqlite = await Database.load('sqlite:lume_v2.db'); - const platformName = await platform(); + if (!storage.account) return null; - const _ark = new Ark({ storage: sqlite, platform: platformName }); - await _ark.init(); + // NIP-46 Signer + if (nsecbunker) { + const localSignerPrivkey = await storage.loadPrivkey( + `${storage.account.id}-nsecbunker` + ); - // check update - if (_ark.settings.autoupdate) { - const update = await check(); - // install new version - if (update) { - setIsNewVersion(true); + if (!localSignerPrivkey) return null; - await update.downloadAndInstall(); - await relaunch(); - } + const localSigner = new NDKPrivateKeySigner(localSignerPrivkey); + const bunker = new NDK({ + explicitRelayUrls: normalizeRelayUrlSet([ + 'wss://relay.nsecbunker.com/', + 'wss://nostr.vulpem.com/', + ]), + }); + await bunker.connect(3000); + + const remoteSigner = new NDKNip46Signer( + bunker, + storage.account.pubkey, + localSigner + ); + await remoteSigner.blockUntilReady(); + + return remoteSigner; } - setArk(_ark); + // Privkey Signer + const userPrivkey = await storage.loadPrivkey(storage.account.pubkey); + + if (!userPrivkey) { + return null; + } + + return new NDKPrivateKeySigner(userPrivkey); } catch (e) { console.error(e); - const yes = await ask(`${e}. Click "Yes" to relaunch app`, { - title: 'Lume', - type: 'error', - okLabel: 'Yes', - }); - if (yes) relaunch(); + return null; } } + async function init() { + const platformName = await platform(); + const sqliteAdapter = await Database.load('sqlite:lume_v2.db'); + + const storage = new LumeStorage(sqliteAdapter, platformName); + storage.init(); + + // check for new update + if (storage.settings.autoupdate) { + const update = await check(); + // install new version + if (update) { + setIsNewVersion(true); + + await update.downloadAndInstall(); + await relaunch(); + } + } + + const explicitRelayUrls = normalizeRelayUrlSet([ + 'wss://relay.damus.io', + 'wss://relay.nostr.band/all', + 'wss://nostr.mutinywallet.com', + ]); + + if (storage.settings.depot) { + await storage.launchDepot(); + await delay(2000); + + explicitRelayUrls.push(normalizeRelayUrl('ws://localhost:6090')); + } + + // #TODO: user should config outbox relays + const outboxRelayUrls = normalizeRelayUrlSet(['wss://purplepag.es']); + + // #TODO: user should config blacklist relays + // No need to connect depot tunnel url + const blacklistRelayUrls = storage.settings.tunnelUrl.length + ? [storage.settings.tunnelUrl, storage.settings.tunnelUrl + '/'] + : []; + + const cacheAdapter = new NDKCacheAdapterTauri(storage); + const ndk = new NDK({ + cacheAdapter, + explicitRelayUrls, + outboxRelayUrls, + blacklistRelayUrls, + enableOutboxModel: storage.settings.lowPowerMode ? false : storage.settings.outbox, + autoConnectUserRelays: storage.settings.lowPowerMode ? false : true, + autoFetchUserMutelist: storage.settings.lowPowerMode ? false : true, + // clientName: 'Lume', + // clientNip89: '', + }); + + // add signer + const signer = await initNostrSigner({ + storage, + nsecbunker: storage.settings.bunker, + }); + + if (signer) ndk.signer = signer; + + // connect + await ndk.connect(3000); + + // update account's metadata + if (storage.account) { + const user = ndk.getUser({ pubkey: storage.account.pubkey }); + ndk.activeUser = user; + + const contacts = await user.follows(); + storage.account.contacts = [...contacts].map((user) => user.pubkey); + } + + // init nostr fetcher + const fetcher = NostrFetcher.withCustomPool(ndkAdapter(ndk)); + + // ark utils + const ark = new Ark({ storage, ndk, fetcher }); + + // update context + setContext({ ark, storage }); + } + useEffect(() => { - if (!ark && !isNewVersion) initArk(); + if (!context && !isNewVersion) init(); }, []); - if (!ark) { + if (!context) { return (
) => { ); } - return {children}; + return ( + + {children} + + ); }; const useArk = () => { - const context = useContext(ArkContext); + const context = useContextSelector(LumeContext, (state) => state.ark); if (context === undefined) { throw new Error('Please import Ark Provider to use useArk() hook'); } return context; }; -export { ArkProvider, useArk }; +const useStorage = () => { + const context = useContextSelector(LumeContext, (state) => state.storage); + if (context === undefined) { + throw new Error('Please import Ark Provider to use useStorage() hook'); + } + return context; +}; + +export { LumeProvider, useArk, useStorage }; diff --git a/src/libs/cache/index.ts b/src/libs/cache/index.ts index 0ef1b574..43dcf06c 100644 --- a/src/libs/cache/index.ts +++ b/src/libs/cache/index.ts @@ -10,20 +10,19 @@ import { NDKUserProfile, profileFromEvent, } from '@nostr-dev-kit/ndk'; -import Database from '@tauri-apps/plugin-sql'; import { LRUCache } from 'lru-cache'; import { NostrEvent } from 'nostr-fetch'; import { matchFilter } from 'nostr-tools'; -import { NDKCacheEvent, NDKCacheEventTag, NDKCacheUser } from '@utils/types'; +import { LumeStorage } from '@libs/storage'; export class NDKCacheAdapterTauri implements NDKCacheAdapter { - #db: Database; + #storage: LumeStorage; private dirtyProfiles: Set = new Set(); public profiles?: LRUCache; readonly locking: boolean; - constructor(db: Database) { - this.#db = db; + constructor(storage: LumeStorage) { + this.#storage = storage; this.locking = true; this.profiles = new LRUCache({ @@ -35,115 +34,6 @@ export class NDKCacheAdapterTauri implements NDKCacheAdapter { }, 1000 * 10); } - async #getCacheUser(pubkey: string) { - const results: Array = await this.#db.select( - 'SELECT * FROM ndk_users WHERE pubkey = $1 ORDER BY pubkey DESC LIMIT 1;', - [pubkey] - ); - - if (!results.length) return null; - - if (typeof results[0].profile === 'string') - results[0].profile = JSON.parse(results[0].profile); - - return results[0]; - } - - async #getCacheEvent(id: string) { - const results: Array = await this.#db.select( - 'SELECT * FROM ndk_events WHERE id = $1 ORDER BY id DESC LIMIT 1;', - [id] - ); - - if (!results.length) return null; - return results[0]; - } - - async #getCacheEvents(ids: string[]) { - const idsArr = `'${ids.join("','")}'`; - - const results: Array = await this.#db.select( - `SELECT * FROM ndk_events WHERE id IN (${idsArr}) ORDER BY id;` - ); - - if (!results.length) return []; - return results; - } - - async #getCacheEventsByPubkey(pubkey: string) { - const results: Array = await this.#db.select( - 'SELECT * FROM ndk_events WHERE pubkey = $1 ORDER BY id;', - [pubkey] - ); - - if (!results.length) return []; - return results; - } - - async #getCacheEventsByKind(kind: number) { - const results: Array = await this.#db.select( - 'SELECT * FROM ndk_events WHERE kind = $1 ORDER BY id;', - [kind] - ); - - if (!results.length) return []; - return results; - } - - async #getCacheEventsByKindAndAuthor(kind: number, pubkey: string) { - const results: Array = await this.#db.select( - 'SELECT * FROM ndk_events WHERE kind = $1 AND pubkey = $2 ORDER BY id;', - [kind, pubkey] - ); - - if (!results.length) return []; - return results; - } - - async #getCacheEventTagsByTagValue(tagValue: string) { - const results: Array = await this.#db.select( - 'SELECT * FROM ndk_eventtags WHERE tagValue = $1 ORDER BY id;', - [tagValue] - ); - - if (!results.length) return []; - return results; - } - - async #setCacheEvent({ - id, - pubkey, - content, - kind, - createdAt, - relay, - event, - }: NDKCacheEvent) { - return await this.#db.execute( - 'INSERT OR IGNORE INTO ndk_events (id, pubkey, content, kind, createdAt, relay, event) VALUES ($1, $2, $3, $4, $5, $6, $7);', - [id, pubkey, content, kind, createdAt, relay, event] - ); - } - - async #setCacheEventTag({ id, eventId, tag, value, tagValue }: NDKCacheEventTag) { - return await this.#db.execute( - 'INSERT OR IGNORE INTO ndk_eventtags (id, eventId, tag, value, tagValue) VALUES ($1, $2, $3, $4, $5);', - [id, eventId, tag, value, tagValue] - ); - } - - async #setCacheProfiles(profiles: Array) { - return await Promise.all( - profiles.map( - async (profile) => - await this.#db.execute( - 'INSERT OR IGNORE INTO ndk_users (pubkey, profile, createdAt) VALUES ($1, $2, $3);', - [profile.pubkey, profile.profile, profile.createdAt] - ) - ) - ); - } - public async query(subscription: NDKSubscription): Promise { Promise.allSettled( subscription.filters.map((filter) => this.processFilter(filter, subscription)) @@ -156,7 +46,7 @@ export class NDKCacheAdapterTauri implements NDKCacheAdapter { let profile = this.profiles.get(pubkey); if (!profile) { - const user = await this.#getCacheUser(pubkey); + const user = await this.#storage.getCacheUser(pubkey); if (user) { profile = user.profile as NDKUserProfile; this.profiles.set(pubkey, profile); @@ -211,7 +101,7 @@ export class NDKCacheAdapterTauri implements NDKCacheAdapter { if (event.isParamReplaceable()) { const replaceableId = `${event.kind}:${event.pubkey}:${event.tagId()}`; - const existingEvent = await this.#getCacheEvent(replaceableId); + const existingEvent = await this.#storage.getCacheEvent(replaceableId); if ( existingEvent && event.created_at && @@ -222,7 +112,7 @@ export class NDKCacheAdapterTauri implements NDKCacheAdapter { } if (addEvent) { - this.#setCacheEvent({ + this.#storage.setCacheEvent({ id: event.tagId(), pubkey: event.pubkey, content: event.content, @@ -238,7 +128,7 @@ export class NDKCacheAdapterTauri implements NDKCacheAdapter { event.tags.forEach((tag) => { if (tag[0].length !== 1) return; - this.#setCacheEventTag({ + this.#storage.setCacheEventTag({ id: `${event.id}:${tag[0]}:${tag[1]}`, eventId: event.id, tag: tag[0], @@ -267,7 +157,7 @@ export class NDKCacheAdapterTauri implements NDKCacheAdapter { if (hasAllKeys && filter.authors) { for (const pubkey of filter.authors) { - const events = await this.#getCacheEventsByPubkey(pubkey); + const events = await this.#storage.getCacheEventsByPubkey(pubkey); for (const event of events) { let rawEvent: NostrEvent; try { @@ -303,7 +193,7 @@ export class NDKCacheAdapterTauri implements NDKCacheAdapter { if (hasAllKeys && filter.kinds) { for (const kind of filter.kinds) { - const events = await this.#getCacheEventsByKind(kind); + const events = await this.#storage.getCacheEventsByKind(kind); for (const event of events) { let rawEvent: NostrEvent; try { @@ -337,7 +227,7 @@ export class NDKCacheAdapterTauri implements NDKCacheAdapter { if (hasAllKeys && filter.ids) { for (const id of filter.ids) { - const event = await this.#getCacheEvent(id); + const event = await this.#storage.getCacheEvent(id); if (!event) continue; let rawEvent: NostrEvent; @@ -380,7 +270,7 @@ export class NDKCacheAdapterTauri implements NDKCacheAdapter { for (const author of filter.authors) { for (const dTag of filter['#d']) { const replaceableId = `${kind}:${author}:${dTag}`; - const event = await this.#getCacheEvent(replaceableId); + const event = await this.#storage.getCacheEvent(replaceableId); if (!event) continue; let rawEvent: NostrEvent; @@ -420,7 +310,7 @@ export class NDKCacheAdapterTauri implements NDKCacheAdapter { if (filter.kinds && filter.authors) { for (const kind of filter.kinds) { for (const author of filter.authors) { - const events = await this.#getCacheEventsByKindAndAuthor(kind, author); + const events = await this.#storage.getCacheEventsByKindAndAuthor(kind, author); for (const event of events) { let rawEvent: NostrEvent; @@ -485,12 +375,12 @@ export class NDKCacheAdapterTauri implements NDKCacheAdapter { } for (const value of values) { - const eventTags = await this.#getCacheEventTagsByTagValue(tag + value); + const eventTags = await this.#storage.getCacheEventTagsByTagValue(tag + value); if (!eventTags.length) continue; const eventIds = eventTags.map((t) => t.eventId); - const events = await this.#getCacheEvents(eventIds); + const events = await this.#storage.getCacheEvents(eventIds); for (const event of events) { let rawEvent; try { @@ -532,7 +422,7 @@ export class NDKCacheAdapterTauri implements NDKCacheAdapter { } if (profiles.length) { - await this.#setCacheProfiles(profiles); + await this.#storage.setCacheProfiles(profiles); } this.dirtyProfiles.clear(); diff --git a/src/libs/storage/index.ts b/src/libs/storage/index.ts new file mode 100644 index 00000000..7042b828 --- /dev/null +++ b/src/libs/storage/index.ts @@ -0,0 +1,396 @@ +import { appConfigDir, resolveResource } from '@tauri-apps/api/path'; +import { invoke } from '@tauri-apps/api/primitives'; +import { Platform } from '@tauri-apps/plugin-os'; +import { Child, Command } from '@tauri-apps/plugin-shell'; +import Database from '@tauri-apps/plugin-sql'; +import { + Account, + NDKCacheEvent, + NDKCacheEventTag, + NDKCacheUser, + NDKCacheUserProfile, + WidgetProps, +} from '@utils/types'; + +export class LumeStorage { + #db: Database; + #depot: Child; + readonly platform: Platform; + public account: Account; + public settings: { + autoupdate: boolean; + bunker: boolean; + outbox: boolean; + media: boolean; + hashtag: boolean; + depot: boolean; + tunnelUrl: string; + lowPowerMode: boolean; + }; + + constructor(db: Database, platform: Platform) { + this.#db = db; + this.#depot = undefined; + this.platform = platform; + this.settings = { + autoupdate: false, + bunker: false, + outbox: false, + media: true, + hashtag: true, + depot: false, + tunnelUrl: '', + lowPowerMode: false, + }; + } + + public async init() { + const settings = await this.getAllSettings(); + + for (const item of settings) { + if (item.key === 'nsecbunker') this.settings.bunker = !!parseInt(item.value); + if (item.key === 'outbox') this.settings.outbox = !!parseInt(item.value); + if (item.key === 'media') this.settings.media = !!parseInt(item.value); + if (item.key === 'hashtag') this.settings.hashtag = !!parseInt(item.value); + if (item.key === 'autoupdate') this.settings.autoupdate = !!parseInt(item.value); + if (item.key === 'depot') this.settings.depot = !!parseInt(item.value); + if (item.key === 'tunnel_url') this.settings.tunnelUrl = item.value; + } + + const account = await this.getActiveAccount(); + if (account) this.account = account; + } + + async #keyring_save(key: string, value: string) { + return await invoke('secure_save', { key, value }); + } + + async #keyring_load(key: string) { + try { + const value: string = await invoke('secure_load', { key }); + if (!value) return null; + return value; + } catch { + return null; + } + } + + async #keyring_remove(key: string) { + return await invoke('secure_remove', { key }); + } + + public async launchDepot() { + const configPath = await resolveResource('resources/config.toml'); + const dataPath = await appConfigDir(); + + const command = Command.sidecar('bin/depot', ['-c', configPath, '-d', dataPath]); + this.#depot = await command.spawn(); + } + + public checkDepot() { + if (this.#depot) return true; + return false; + } + + public async stopDepot() { + if (this.#depot) return this.#depot.kill(); + } + + public async getCacheUser(pubkey: string) { + const results: Array = await this.#db.select( + 'SELECT * FROM ndk_users WHERE pubkey = $1 ORDER BY pubkey DESC LIMIT 1;', + [pubkey] + ); + + if (!results.length) return null; + + if (typeof results[0].profile === 'string') + results[0].profile = JSON.parse(results[0].profile); + + return results[0]; + } + + public async getCacheEvent(id: string) { + const results: Array = await this.#db.select( + 'SELECT * FROM ndk_events WHERE id = $1 ORDER BY id DESC LIMIT 1;', + [id] + ); + + if (!results.length) return null; + return results[0]; + } + + public async getCacheEvents(ids: string[]) { + const idsArr = `'${ids.join("','")}'`; + + const results: Array = await this.#db.select( + `SELECT * FROM ndk_events WHERE id IN (${idsArr}) ORDER BY id;` + ); + + if (!results.length) return []; + return results; + } + + public async getCacheEventsByPubkey(pubkey: string) { + const results: Array = await this.#db.select( + 'SELECT * FROM ndk_events WHERE pubkey = $1 ORDER BY id;', + [pubkey] + ); + + if (!results.length) return []; + return results; + } + + public async getCacheEventsByKind(kind: number) { + const results: Array = await this.#db.select( + 'SELECT * FROM ndk_events WHERE kind = $1 ORDER BY id;', + [kind] + ); + + if (!results.length) return []; + return results; + } + + public async getCacheEventsByKindAndAuthor(kind: number, pubkey: string) { + const results: Array = await this.#db.select( + 'SELECT * FROM ndk_events WHERE kind = $1 AND pubkey = $2 ORDER BY id;', + [kind, pubkey] + ); + + if (!results.length) return []; + return results; + } + + public async getCacheEventTagsByTagValue(tagValue: string) { + const results: Array = await this.#db.select( + 'SELECT * FROM ndk_eventtags WHERE tagValue = $1 ORDER BY id;', + [tagValue] + ); + + if (!results.length) return []; + return results; + } + + public async setCacheEvent({ + id, + pubkey, + content, + kind, + createdAt, + relay, + event, + }: NDKCacheEvent) { + return await this.#db.execute( + 'INSERT OR IGNORE INTO ndk_events (id, pubkey, content, kind, createdAt, relay, event) VALUES ($1, $2, $3, $4, $5, $6, $7);', + [id, pubkey, content, kind, createdAt, relay, event] + ); + } + + public async setCacheEventTag({ id, eventId, tag, value, tagValue }: NDKCacheEventTag) { + return await this.#db.execute( + 'INSERT OR IGNORE INTO ndk_eventtags (id, eventId, tag, value, tagValue) VALUES ($1, $2, $3, $4, $5);', + [id, eventId, tag, value, tagValue] + ); + } + + public async setCacheProfiles(profiles: Array) { + return await Promise.all( + profiles.map( + async (profile) => + await this.#db.execute( + 'INSERT OR IGNORE INTO ndk_users (pubkey, profile, createdAt) VALUES ($1, $2, $3);', + [profile.pubkey, profile.profile, profile.createdAt] + ) + ) + ); + } + + public async getAllCacheUsers() { + const results: Array = await this.#db.select( + 'SELECT * FROM ndk_users ORDER BY createdAt DESC;' + ); + + if (!results.length) return []; + + const users: NDKCacheUserProfile[] = results.map((item) => ({ + pubkey: item.pubkey, + ...JSON.parse(item.profile as string), + })); + return users; + } + + public async checkAccount() { + const result: Array<{ total: string }> = await this.#db.select( + 'SELECT COUNT(*) AS "total" FROM accounts WHERE is_active = "1" ORDER BY id DESC LIMIT 1;' + ); + return parseInt(result[0].total); + } + + public async getActiveAccount() { + const results: Array = await this.#db.select( + 'SELECT * FROM accounts WHERE is_active = "1" ORDER BY id DESC LIMIT 1;' + ); + + if (results.length) { + this.account = results[0]; + return results[0]; + } else { + return null; + } + } + + public async createAccount({ + id, + pubkey, + privkey, + }: { + id: string; + pubkey: string; + privkey?: string; + }) { + const existAccounts: Array = await this.#db.select( + 'SELECT * FROM accounts WHERE pubkey = $1 ORDER BY id DESC LIMIT 1;', + [pubkey] + ); + + if (existAccounts.length) { + await this.#db.execute("UPDATE accounts SET is_active = '1' WHERE pubkey = $1;", [ + pubkey, + ]); + } else { + await this.#db.execute( + 'INSERT OR IGNORE INTO accounts (id, pubkey, is_active) VALUES ($1, $2, $3);', + [id, pubkey, 1] + ); + + if (privkey) await this.#keyring_save(pubkey, privkey); + } + + const account = await this.getActiveAccount(); + this.account = account; + this.account.contacts = []; + + return account; + } + + /** + * Save private key to OS secure storage + * @deprecated this method will be remove in the next update + */ + public async createPrivkey(name: string, privkey: string) { + return await this.#keyring_save(name, privkey); + } + + /** + * Load private key from OS secure storage + * @deprecated this method will be remove in the next update + */ + public async loadPrivkey(name: string) { + return await this.#keyring_load(name); + } + + /** + * Remove private key from OS secure storage + * @deprecated this method will be remove in the next update + */ + public async removePrivkey(name: string) { + return await this.#keyring_remove(name); + } + + public async updateAccount(column: string, value: string) { + const insert = await this.#db.execute( + `UPDATE accounts SET ${column} = $1 WHERE id = $2;`, + [value, this.account.id] + ); + + if (insert) { + const account = await this.getActiveAccount(); + return account; + } + } + + public async getWidgets() { + const widgets: Array = await this.#db.select( + 'SELECT * FROM widgets WHERE account_id = $1 ORDER BY created_at DESC;', + [this.account.id] + ); + return widgets; + } + + public async createWidget(kind: number, title: string, content: string | string[]) { + const insert = await this.#db.execute( + 'INSERT INTO widgets (account_id, kind, title, content) VALUES ($1, $2, $3, $4);', + [this.account.id, kind, title, content] + ); + + if (insert) { + const widgets: Array = await this.#db.select( + 'SELECT * FROM widgets ORDER BY id DESC LIMIT 1;' + ); + if (widgets.length < 1) console.error('get created widget failed'); + return widgets[0]; + } else { + console.error('create widget failed'); + } + } + + public async removeWidget(id: string) { + const res = await this.#db.execute('DELETE FROM widgets WHERE id = $1;', [id]); + if (res) return id; + } + + public async createSetting(key: string, value: string | undefined) { + const currentSetting = await this.checkSettingValue(key); + + if (!currentSetting) { + return await this.#db.execute( + 'INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);', + [key, value] + ); + } + + return await this.#db.execute('UPDATE settings SET value = $1 WHERE key = $2;', [ + value, + key, + ]); + } + + public async getAllSettings() { + const results: { key: string; value: string }[] = await this.#db.select( + 'SELECT * FROM settings ORDER BY id DESC;' + ); + if (results.length < 1) return []; + return results; + } + + public async checkSettingValue(key: string) { + const results: { key: string; value: string }[] = await this.#db.select( + 'SELECT * FROM settings WHERE key = $1 ORDER BY id DESC LIMIT 1;', + [key] + ); + if (!results.length) return false; + return results[0].value; + } + + public async getSettingValue(key: string) { + const results: { key: string; value: string }[] = await this.#db.select( + 'SELECT * FROM settings WHERE key = $1 ORDER BY id DESC LIMIT 1;', + [key] + ); + if (!results.length) return '0'; + return results[0].value; + } + + public async clearCache() { + await this.#db.execute('DELETE FROM ndk_events;'); + await this.#db.execute('DELETE FROM ndk_eventtags;'); + await this.#db.execute('DELETE FROM ndk_users;'); + } + + public async logout() { + this.account = null; + return await this.#db.execute("UPDATE accounts SET is_active = '0' WHERE id = $1;", [ + this.account.id, + ]); + } +} diff --git a/src/main.jsx b/src/main.jsx index 65fec4ec..b6f2473d 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,7 +1,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { createRoot } from 'react-dom/client'; import { Toaster } from 'sonner'; -import { ArkProvider } from '@libs/ark/provider'; +import { LumeProvider } from '@libs/ark'; import App from './app'; import './app.css'; @@ -19,8 +19,8 @@ const root = createRoot(container); root.render( - + - + ); diff --git a/src/shared/accounts/active.tsx b/src/shared/account/active.tsx similarity index 81% rename from src/shared/accounts/active.tsx rename to src/shared/account/active.tsx index 7bc6e44f..af9c2cb0 100644 --- a/src/shared/accounts/active.tsx +++ b/src/shared/account/active.tsx @@ -2,20 +2,20 @@ import * as Avatar from '@radix-ui/react-avatar'; import { minidenticon } from 'minidenticons'; import { Link } from 'react-router-dom'; import { twMerge } from 'tailwind-merge'; -import { useArk } from '@libs/ark'; -import { AccountMoreActions } from '@shared/accounts/more'; +import { useStorage } from '@libs/ark'; +import { AccountMoreActions } from '@shared/account/more'; import { useNetworkStatus } from '@utils/hooks/useNetworkStatus'; import { useProfile } from '@utils/hooks/useProfile'; export function ActiveAccount() { - const ark = useArk(); - const { user } = useProfile(ark.account.pubkey); - + const storage = useStorage(); const isOnline = useNetworkStatus(); + const { user } = useProfile(storage.account.pubkey); + const svgURI = 'data:image/svg+xml;utf8,' + - encodeURIComponent(minidenticon(ark.account.pubkey, 90, 50)); + encodeURIComponent(minidenticon(storage.account.pubkey, 90, 50)); return (
@@ -23,7 +23,7 @@ export function ActiveAccount() { {ark.account.pubkey} diff --git a/src/shared/accounts/logout.tsx b/src/shared/account/logout.tsx similarity index 100% rename from src/shared/accounts/logout.tsx rename to src/shared/account/logout.tsx diff --git a/src/shared/accounts/more.tsx b/src/shared/account/more.tsx similarity index 95% rename from src/shared/accounts/more.tsx rename to src/shared/account/more.tsx index 08177369..65d0320f 100644 --- a/src/shared/accounts/more.tsx +++ b/src/shared/account/more.tsx @@ -1,6 +1,6 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import { Link } from 'react-router-dom'; -import { Logout } from '@shared/accounts/logout'; +import { Logout } from '@shared/account/logout'; import { HorizontalDotsIcon } from '@shared/icons'; export function AccountMoreActions() { diff --git a/src/shared/navigation.tsx b/src/shared/navigation.tsx index aadbd378..6c666fe6 100644 --- a/src/shared/navigation.tsx +++ b/src/shared/navigation.tsx @@ -1,6 +1,6 @@ import { Link, NavLink } from 'react-router-dom'; import { twMerge } from 'tailwind-merge'; -import { ActiveAccount } from '@shared/accounts/active'; +import { ActiveAccount } from '@shared/account/active'; import { DepotIcon, HomeIcon, diff --git a/src/shared/notes/actions.tsx b/src/shared/notes/actions.tsx deleted file mode 100644 index a50bdd95..00000000 --- a/src/shared/notes/actions.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { NDKEvent } from '@nostr-dev-kit/ndk'; -import * as Tooltip from '@radix-ui/react-tooltip'; -import { createSearchParams, useNavigate } from 'react-router-dom'; -import { PinIcon, ReplyIcon } from '@shared/icons'; -import { NoteReaction } from '@shared/notes/actions/reaction'; -import { NoteRepost } from '@shared/notes/actions/repost'; -import { NoteZap } from '@shared/notes/actions/zap'; -import { WIDGET_KIND } from '@utils/constants'; -import { useWidget } from '@utils/hooks/useWidget'; - -export function NoteActions({ - event, - rootEventId, - canOpenEvent = true, -}: { - event: NDKEvent; - rootEventId?: string; - canOpenEvent?: boolean; -}) { - const { addWidget } = useWidget(); - const navigate = useNavigate(); - - return ( - -
- {canOpenEvent && ( -
- - - - - - - Pin note - - - - -
- )} -
- - - - - - - Quick reply - - - - - - - -
-
-
- ); -} diff --git a/src/shared/notes/actions/more.tsx b/src/shared/notes/actions/more.tsx deleted file mode 100644 index c3e7fa23..00000000 --- a/src/shared/notes/actions/more.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; -import { writeText } from '@tauri-apps/plugin-clipboard-manager'; -import { nip19 } from 'nostr-tools'; -import { EventPointer } from 'nostr-tools/lib/types/nip19'; -import { useState } from 'react'; -import { Link } from 'react-router-dom'; -import { HorizontalDotsIcon } from '@shared/icons'; - -export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) { - const [open, setOpen] = useState(false); - - const copyID = async () => { - await writeText(nip19.neventEncode({ id: id, author: pubkey } as EventPointer)); - setOpen(false); - }; - - const copyLink = async () => { - await writeText( - 'https://njump.me/' + nip19.neventEncode({ id: id, author: pubkey } as EventPointer) - ); - setOpen(false); - }; - - return ( - - - - - - - - - - - - - - - View profile - - - - - - ); -} diff --git a/src/shared/notes/actions/reaction.tsx b/src/shared/notes/actions/reaction.tsx deleted file mode 100644 index ed6f2e1b..00000000 --- a/src/shared/notes/actions/reaction.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { NDKEvent } from '@nostr-dev-kit/ndk'; -import * as Popover from '@radix-ui/react-popover'; -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { toast } from 'sonner'; -import { useArk } from '@libs/ark'; -import { ReactionIcon } from '@shared/icons'; - -const REACTIONS = [ - { - content: '👏', - img: '/clapping_hands.png', - }, - { - content: '🤪', - img: '/face_with_tongue.png', - }, - { - content: '😮', - img: '/face_with_open_mouth.png', - }, - { - content: '😢', - img: '/crying_face.png', - }, - { - content: '🤡', - img: '/clown_face.png', - }, -]; - -export function NoteReaction({ event }: { event: NDKEvent }) { - const [open, setOpen] = useState(false); - const [reaction, setReaction] = useState(null); - - const ark = useArk(); - const navigate = useNavigate(); - - const getReactionImage = (content: string) => { - const reaction: { img: string } = REACTIONS.find((el) => el.content === content); - return reaction.img; - }; - - const react = async (content: string) => { - try { - if (!ark.readyToSign) return navigate('/new/privkey'); - - setReaction(content); - - // react - await event.react(content); - setOpen(false); - } catch (e) { - toast.error(e); - } - }; - - return ( - - - - - - -
- - - - - -
- -
-
-
- ); -} diff --git a/src/shared/notes/actions/repost.tsx b/src/shared/notes/actions/repost.tsx deleted file mode 100644 index 9494a405..00000000 --- a/src/shared/notes/actions/repost.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { NDKEvent } from '@nostr-dev-kit/ndk'; -import * as AlertDialog from '@radix-ui/react-alert-dialog'; -import * as Tooltip from '@radix-ui/react-tooltip'; -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { toast } from 'sonner'; -import { twMerge } from 'tailwind-merge'; -import { useArk } from '@libs/ark'; -import { LoaderIcon, RepostIcon } from '@shared/icons'; - -export function NoteRepost({ event }: { event: NDKEvent }) { - const [open, setOpen] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [isRepost, setIsRepost] = useState(false); - - const ark = useArk(); - const navigate = useNavigate(); - - const submit = async () => { - try { - if (!ark.readyToSign) return navigate('/new/privkey'); - - setIsLoading(true); - - // repsot - await event.repost(true); - - // reset state - setOpen(false); - setIsRepost(true); - - toast.success("You've reposted this post successfully"); - } catch (e) { - setIsLoading(false); - toast.error('Repost failed, try again later'); - } - }; - - return ( - - - - - - - - - - Repost - - - - - - - -
-
- - Confirm repost this post? - - - Reposted post will be visible to your followers, and you cannot undo this - action. - -
-
- - - - -
-
-
-
-
- ); -} diff --git a/src/shared/notes/actions/zap.tsx b/src/shared/notes/actions/zap.tsx deleted file mode 100644 index db241f48..00000000 --- a/src/shared/notes/actions/zap.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import { webln } from '@getalby/sdk'; -import { SendPaymentResponse } from '@getalby/sdk/dist/types'; -import { NDKEvent } from '@nostr-dev-kit/ndk'; -import * as Dialog from '@radix-ui/react-dialog'; -import { invoke } from '@tauri-apps/api/primitives'; -import { message } from '@tauri-apps/plugin-dialog'; -import { QRCodeSVG } from 'qrcode.react'; -import { useEffect, useRef, useState } from 'react'; -import CurrencyInput from 'react-currency-input-field'; -import { useNavigate } from 'react-router-dom'; -import { useArk } from '@libs/ark'; -import { CancelIcon, ZapIcon } from '@shared/icons'; -import { compactNumber, displayNpub } from '@utils/formater'; -import { useProfile } from '@utils/hooks/useProfile'; -import { sendNativeNotification } from '@utils/notification'; - -export function NoteZap({ event }: { event: NDKEvent }) { - const ark = useArk(); - const { user } = useProfile(event.pubkey); - - const [walletConnectURL, setWalletConnectURL] = useState(null); - const [amount, setAmount] = useState('21'); - const [zapMessage, setZapMessage] = useState(''); - const [invoice, setInvoice] = useState(null); - const [isOpen, setIsOpen] = useState(false); - const [isCompleted, setIsCompleted] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - const nwc = useRef(null); - const navigate = useNavigate(); - - const createZapRequest = async () => { - try { - if (!ark.readyToSign) return navigate('/new/privkey'); - - const zapAmount = parseInt(amount) * 1000; - const res = await event.zap(zapAmount, zapMessage); - - if (!res) - return await message('Cannot create zap request', { - title: 'Zap', - type: 'error', - }); - - // user don't connect nwc, create QR Code for invoice - if (!walletConnectURL) return setInvoice(res); - - // user connect nwc - nwc.current = new webln.NostrWebLNProvider({ - nostrWalletConnectUrl: walletConnectURL, - }); - await nwc.current.enable(); - - // start loading - setIsLoading(true); - // send payment via nwc - const send: SendPaymentResponse = await nwc.current.sendPayment(res); - - if (send) { - await sendNativeNotification( - `You've tipped ${compactNumber.format(send.amount)} sats to ${ - user?.name || user?.display_name || user?.displayName - }` - ); - - // eose - nwc.current.close(); - setIsCompleted(true); - setIsLoading(false); - - // reset after 3 secs - const timeout = setTimeout(() => setIsCompleted(false), 3000); - clearTimeout(timeout); - } - } catch (e) { - nwc.current.close(); - setIsLoading(false); - await message(JSON.stringify(e), { title: 'Zap', type: 'error' }); - } - }; - - useEffect(() => { - async function getWalletConnectURL() { - const uri: string = await invoke('secure_load', { - key: `${ark.account.pubkey}-nwc`, - }); - if (uri) setWalletConnectURL(uri); - } - - if (isOpen) getWalletConnectURL(); - - return () => { - setAmount('21'); - setZapMessage(''); - setIsCompleted(false); - setIsLoading(false); - }; - }, [isOpen]); - - return ( - - - - - - - -
-
-
- - Send tip to{' '} - {user?.name || user?.displayName || displayNpub(event.pubkey, 16)} - - - - -
-
- {!invoice ? ( - <> -
-
- setAmount(value)} - className="w-full flex-1 border-none bg-transparent text-right text-4xl font-semibold placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400" - /> - - sats - -
-
- - - - - -
-
-
- setZapMessage(e.target.value)} - spellCheck={false} - autoComplete="off" - autoCorrect="off" - autoCapitalize="off" - placeholder="Enter message (optional)" - className="w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-400" - /> -
- {walletConnectURL ? ( - - ) : ( - - )} -
-
- - ) : ( -
-
- -
-
-

Scan to zap

- - You must use Bitcoin wallet which support Lightning -
- such as: Blue Wallet, Bitkit, Phoenix,... -
-
-
- )} -
-
- - - - ); -} diff --git a/src/shared/notes/article.tsx b/src/shared/notes/article.tsx deleted file mode 100644 index c3a97572..00000000 --- a/src/shared/notes/article.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { NDKEvent } from '@nostr-dev-kit/ndk'; -import { memo } from 'react'; -import { Link } from 'react-router-dom'; -import { User } from '@shared/user'; -import { NoteActions } from './actions'; - -export function ArticleNote({ event }: { event: NDKEvent }) { - const getMetadata = () => { - const title = event.tags.find((tag) => tag[0] === 'title')?.[1]; - const image = event.tags.find((tag) => tag[0] === 'image')?.[1]; - const summary = event.tags.find((tag) => tag[0] === 'summary')?.[1]; - - let publishedAt: Date | string | number = event.tags.find( - (tag) => tag[0] === 'published_at' - )?.[1]; - if (publishedAt) { - publishedAt = new Date(parseInt(publishedAt) * 1000).toLocaleDateString('en-US'); - } else { - publishedAt = new Date(event.created_at * 1000).toLocaleDateString('en-US'); - } - - return { - title, - image, - publishedAt, - summary, - }; - }; - - const metadata = getMetadata(); - - return ( -
-
- -
- - {metadata.image && ( - {metadata.title} - )} -
-
- {metadata.title} -
- {metadata.summary ? ( -

- {metadata.summary} -

- ) : null} - - {metadata.publishedAt.toString()} - -
- -
- -
-
- ); -} - -export const MemoizedArticleNote = memo(ArticleNote); diff --git a/src/shared/notes/child.tsx b/src/shared/notes/child.tsx deleted file mode 100644 index d9266712..00000000 --- a/src/shared/notes/child.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { NoteSkeleton } from '@shared/notes'; -import { User } from '@shared/user'; -import { useEvent } from '@utils/hooks/useEvent'; - -export function ChildNote({ id, isRoot }: { id: string; isRoot?: boolean }) { - const { isFetching, isError, data } = useEvent(id); - - if (isFetching) { - return ; - } - - if (isError) { - return ( -
-
- Failed to fetch event -
-
- ); - } - - return ( -
-
-
-
- {data?.content} -
-
- -
- ); -} diff --git a/src/shared/notes/file.tsx b/src/shared/notes/file.tsx deleted file mode 100644 index 11452d07..00000000 --- a/src/shared/notes/file.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { NDKEvent } from '@nostr-dev-kit/ndk'; -import { downloadDir } from '@tauri-apps/api/path'; -import { download } from '@tauri-apps/plugin-upload'; -import { MediaPlayer, MediaProvider } from '@vidstack/react'; -import { - DefaultVideoLayout, - defaultLayoutIcons, -} from '@vidstack/react/player/layouts/default'; -import { memo } from 'react'; -import { Link } from 'react-router-dom'; -import { DownloadIcon } from '@shared/icons'; -import { NoteActions } from '@shared/notes'; -import { User } from '@shared/user'; -import { fileType } from '@utils/nip94'; - -export function FileNote({ event }: { event: NDKEvent }) { - const downloadImage = async (url: string) => { - const downloadDirPath = await downloadDir(); - const filename = url.substring(url.lastIndexOf('/') + 1); - return await download(url, downloadDirPath + `/${filename}`); - }; - - const renderFileType = () => { - const url = event.tags.find((el) => el[0] === 'url')[1]; - const type = fileType(url); - - switch (type) { - case 'image': - return ( -
- {url} - -
- ); - case 'video': - return ( - - - - - ); - default: - return ( - - {url} - - ); - } - }; - - return ( -
-
- -
{renderFileType()}
- -
-
- ); -} - -export const MemoizedFileNote = memo(FileNote); diff --git a/src/shared/notes/index.ts b/src/shared/notes/index.ts deleted file mode 100644 index b8dd7c07..00000000 --- a/src/shared/notes/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -export * from './text'; -export * from './repost'; -export * from './file'; -export * from './article'; -export * from './child'; -export * from './notify'; -export * from './unknown'; -export * from './skeleton'; -export * from './actions'; -export * from './actions/reaction'; -export * from './actions/repost'; -export * from './actions/zap'; -export * from './actions/more'; -export * from './preview/image'; -export * from './preview/link'; -export * from './preview/video'; -export * from './replies/form'; -export * from './replies/item'; -export * from './replies/list'; -export * from './replies/sub'; -export * from './replies/replyMediaUploader'; -export * from './mentions/note'; -export * from './mentions/user'; -export * from './mentions/hashtag'; -export * from './mentions/invoice'; -export * from './kinds/text'; -export * from './kinds/article'; -export * from './kinds/file'; diff --git a/src/shared/notes/kinds/text.tsx b/src/shared/notes/kinds/text.tsx deleted file mode 100644 index e2448c18..00000000 --- a/src/shared/notes/kinds/text.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { memo } from 'react'; -import { useRichContent } from '@utils/hooks/useRichContent'; - -export function TextKind({ content, textmode }: { content: string; textmode?: boolean }) { - const { parsedContent } = useRichContent(content, textmode); - - if (textmode) { - return ( -
- {parsedContent} -
- ); - } - - return ( -
-
- {parsedContent} -
-
- ); -} - -export const MemoizedTextKind = memo(TextKind); diff --git a/src/shared/notes/mentions/note.tsx b/src/shared/notes/mentions/note.tsx deleted file mode 100644 index 5ee3febe..00000000 --- a/src/shared/notes/mentions/note.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; -import { memo } from 'react'; -import { - MemoizedArticleKind, - MemoizedFileKind, - MemoizedTextKind, - NoteSkeleton, -} from '@shared/notes'; -import { User } from '@shared/user'; -import { WIDGET_KIND } from '@utils/constants'; -import { useEvent } from '@utils/hooks/useEvent'; -import { useWidget } from '@utils/hooks/useWidget'; - -export const MentionNote = memo(function MentionNote({ - id, - editing, -}: { - id: string; - editing?: boolean; -}) { - const { isFetching, isError, data } = useEvent(id); - const { addWidget } = useWidget(); - - const renderKind = (event: NDKEvent) => { - switch (event.kind) { - case NDKKind.Text: - return ; - case NDKKind.Article: - return ; - case 1063: - return ; - default: - return null; - } - }; - - if (isFetching) { - return ( -
- -
- ); - } - - if (isError) { - return ( -
- Failed to fetch event -
- ); - } - - return ( -
-
- -
-
- {renderKind(data)} - {!editing ? ( - - ) : null} -
-
- ); -}); diff --git a/src/shared/notes/notify.tsx b/src/shared/notes/notify.tsx deleted file mode 100644 index 26b169f6..00000000 --- a/src/shared/notes/notify.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; -import { memo } from 'react'; -import { useArk } from '@libs/ark'; -import { ReplyIcon, RepostIcon } from '@shared/icons'; -import { ChildNote, TextKind } from '@shared/notes'; -import { User } from '@shared/user'; -import { WIDGET_KIND } from '@utils/constants'; -import { formatCreatedAt } from '@utils/formater'; -import { useWidget } from '@utils/hooks/useWidget'; - -export function NotifyNote({ event }: { event: NDKEvent }) { - const ark = useArk(); - const { addWidget } = useWidget(); - - const thread = ark.getEventThread({ tags: event.tags }); - const createdAt = formatCreatedAt(event.created_at, false); - - if (event.kind === NDKKind.Reaction) { - return ( -
-
-
-
-
- {event.content === '+' ? '👍' : event.content} -
-
-
- -

reacted

-
-
{createdAt}
-
-
-
-
-
-
- {thread.rootEventId ? : null} -
-
- -
-
-
- ); - } - - if (event.kind === NDKKind.Repost) { - return ( -
-
-
-
-
- -
-
-
- -

reposted

-
-
{createdAt}
-
-
-
-
-
-
- {thread.rootEventId ? : null} -
-
- -
-
-
- ); - } - - if (event.kind === NDKKind.Text) { - return ( -
-
-
-
-
- -
-
-
- -

replied

-
-
{createdAt}
-
-
-
-
-
-
- {thread?.replyEventId ? ( - - ) : thread?.rootEventId ? ( - - ) : null} - -
-
- -
-
-
- ); - } -} - -export const MemoizedNotifyNote = memo(NotifyNote); diff --git a/src/shared/notes/replies/form.tsx b/src/shared/notes/replies/form.tsx deleted file mode 100644 index 7c4ef8a7..00000000 --- a/src/shared/notes/replies/form.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { NDKEvent } from '@nostr-dev-kit/ndk'; -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { toast } from 'sonner'; -import { useArk } from '@libs/ark'; -import { LoaderIcon } from '@shared/icons'; -import { ReplyMediaUploader } from '@shared/notes'; - -export function NoteReplyForm({ rootEvent }: { rootEvent: NDKEvent }) { - const ark = useArk(); - const navigate = useNavigate(); - - const [value, setValue] = useState(''); - const [loading, setLoading] = useState(false); - - const submit = async () => { - try { - if (!ark.readyToSign) return navigate('/new/privkey'); - setLoading(true); - - // publish event - const publish = await ark.replyTo({ content: value, event: rootEvent }); - - if (publish) { - toast.success(`Broadcasted to ${publish.size} relays successfully.`); - - // reset state - setValue(''); - setLoading(false); - } - } catch (e) { - setLoading(false); - toast.error(e); - } - }; - - return ( -
-