diff --git a/package.json b/package.json index d958daf3..0c18fe2f 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@radix-ui/react-toolbar": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", "@tanstack/react-query": "^5.8.7", + "@tanstack/react-query-devtools": "^5.10.0", "@tauri-apps/api": "2.0.0-alpha.11", "@tauri-apps/cli": "2.0.0-alpha.17", "@tauri-apps/plugin-autostart": "2.0.0-alpha.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae41afd1..9a77b92f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ dependencies: '@tanstack/react-query': specifier: ^5.8.7 version: 5.8.7(react-dom@18.2.0)(react@18.2.0) + '@tanstack/react-query-devtools': + specifier: ^5.10.0 + version: 5.10.0(@tanstack/react-query@5.8.7)(react@18.2.0) '@tauri-apps/api': specifier: 2.0.0-alpha.11 version: 2.0.0-alpha.11 @@ -2058,6 +2061,21 @@ packages: resolution: {integrity: sha512-58xOSkxxZK4SGQ/uzX8MDZHLGZCkxlgkPxnfhxUOL2uchnNHyay2UVcR3mQNMgaMwH1e2l+0n+zfS7+UJ/MAJw==} dev: false + /@tanstack/query-devtools@5.10.0: + resolution: {integrity: sha512-ZN17ZHiPFc8B1AZplueTLI6qxqNPuelV/8Q6gx2coNac2++AD1gq8NvCQxfq9HoaqFJM8xqS3oPYxa3HZZTo8w==} + dev: false + + /@tanstack/react-query-devtools@5.10.0(@tanstack/react-query@5.8.7)(react@18.2.0): + resolution: {integrity: sha512-amQN/6BdSMiYtILISk5j3IrZZiWz+HOHjJdcJNZHaTpzS7aIM1Z82bghCSoPdBtS7P1KbdoW35aD4glm9EkIuQ==} + peerDependencies: + '@tanstack/react-query': ^5.10.0 + react: ^18.0.0 + dependencies: + '@tanstack/query-devtools': 5.10.0 + '@tanstack/react-query': 5.8.7(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + dev: false + /@tanstack/react-query@5.8.7(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-RYSSMmkhbJ7tPkf8w+MSRIXQLoUCm7DRnTLDcdf+uampupnriEsob3fVWTt9oaEj+AJWEKeCErDBdZeNcAzURQ==} peerDependencies: diff --git a/src/libs/ndk/instance.ts b/src/libs/ndk/instance.ts index 6c11b339..c23e93a2 100644 --- a/src/libs/ndk/instance.ts +++ b/src/libs/ndk/instance.ts @@ -1,66 +1,28 @@ -import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; +import NDK, { + NDKEvent, + NDKKind, + NDKNip46Signer, + NDKPrivateKeySigner, +} from '@nostr-dev-kit/ndk'; import { ndkAdapter } from '@nostr-fetch/adapter-ndk'; +import { useQueryClient } from '@tanstack/react-query'; import { ask } from '@tauri-apps/plugin-dialog'; -import { fetch } from '@tauri-apps/plugin-http'; import { relaunch } from '@tauri-apps/plugin-process'; import { NostrFetcher } from 'nostr-fetch'; -import { useEffect, useMemo, useState } from 'react'; -import { toast } from 'sonner'; +import { useEffect, useState } from 'react'; import NDKCacheAdapterTauri from '@libs/ndk/cache'; import { useStorage } from '@libs/storage/provider'; +import { FETCH_LIMIT } from '@stores/constants'; + export const NDKInstance = () => { - const [ndk, setNDK] = useState(undefined); - const [relayUrls, setRelayUrls] = useState([]); - const { db } = useStorage(); - const fetcher = useMemo( - () => (ndk ? NostrFetcher.withCustomPool(ndkAdapter(ndk)) : null), - [ndk] - ); + const queryClient = useQueryClient(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async function getExplicitRelays() { - try { - // get relays - const relays = await db.getExplicitRelayUrls(); - const onlineRelays = new Set(relays); - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 8000); - - for (const relay of relays) { - try { - const url = new URL(relay); - const res = await fetch(`https://${url.hostname}`, { - method: 'GET', - headers: { - Accept: 'application/nostr+json', - }, - signal: controller.signal, - }); - - if (!res.ok) { - toast.warning(`${relay} is not working, skipping...`); - onlineRelays.delete(relay); - } - - toast.success(`Connected to ${relay}`); - } catch { - toast.warning(`${relay} is not working, skipping...`); - onlineRelays.delete(relay); - } finally { - clearTimeout(timeoutId); - } - } - - // return all online relays - return [...onlineRelays]; - } catch (e) { - console.error(e); - } - } + const [ndk, setNDK] = useState(undefined); + const [fetcher, setFetcher] = useState(undefined); + const [relayUrls, setRelayUrls] = useState([]); async function getSigner(nsecbunker?: boolean) { if (!db.account) return; @@ -104,17 +66,88 @@ export const NDKInstance = () => { // connect await instance.connect(); + const tmpFetcher = NostrFetcher.withCustomPool(ndkAdapter(instance)); // update account's metadata if (db.account) { const user = instance.getUser({ pubkey: db.account.pubkey }); - if (user) { - db.account.contacts = [...(await user.follows())].map((user) => user.pubkey); - db.account.relayList = await user.relayList(); - } + db.account.contacts = [...(await user.follows())].map((user) => user.pubkey); + db.account.relayList = await user.relayList(); + + // prefetch data + await queryClient.prefetchInfiniteQuery({ + queryKey: ['newsfeed'], + initialPageParam: 0, + queryFn: async ({ + signal, + pageParam, + }: { + signal: AbortSignal; + pageParam: number; + }) => { + const rootIds = new Set(); + const dedupQueue = new Set(); + + const events = await tmpFetcher.fetchLatestEvents( + explicitRelayUrls, + { + kinds: [NDKKind.Text, NDKKind.Repost], + authors: db.account.contacts, + }, + FETCH_LIMIT, + { asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal } + ); + + const ndkEvents = events.map((event) => { + return new NDKEvent(ndk, event); + }); + + ndkEvents.forEach((event) => { + const tags = event.tags.filter((el) => el[0] === 'e'); + if (tags && tags.length > 0) { + const rootId = tags.filter((el) => el[3] === 'root')[1] ?? tags[0][1]; + if (rootIds.has(rootId)) return dedupQueue.add(event.id); + rootIds.add(rootId); + } + }); + + return ndkEvents + .filter((event) => !dedupQueue.has(event.id)) + .sort((a, b) => b.created_at - a.created_at); + }, + }); + + await queryClient.prefetchInfiniteQuery({ + queryKey: ['notification'], + initialPageParam: 0, + queryFn: async ({ + signal, + pageParam, + }: { + signal: AbortSignal; + pageParam: number; + }) => { + const events = await tmpFetcher.fetchLatestEvents( + explicitRelayUrls, + { + kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap], + '#p': [db.account.pubkey], + }, + FETCH_LIMIT, + { asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal } + ); + + const ndkEvents = events.map((event) => { + return new NDKEvent(ndk, event); + }); + + return ndkEvents.sort((a, b) => b.created_at - a.created_at); + }, + }); } setNDK(instance); + setFetcher(tmpFetcher); setRelayUrls(explicitRelayUrls); } catch (e) { const yes = await ask( @@ -135,7 +168,7 @@ export const NDKInstance = () => { return { ndk, - relayUrls, fetcher, + relayUrls, }; }; diff --git a/src/main.jsx b/src/main.jsx index ad848641..326f277f 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,4 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { createRoot } from 'react-dom/client'; import { Toaster } from 'sonner'; @@ -20,6 +21,7 @@ const root = createRoot(container); root.render( +