diff --git a/src/app/chats/components/chatForm.tsx b/src/app/chats/components/chatForm.tsx index 27b62455..62389df7 100644 --- a/src/app/chats/components/chatForm.tsx +++ b/src/app/chats/components/chatForm.tsx @@ -1,29 +1,19 @@ -import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk'; import { useState } from 'react'; import { toast } from 'sonner'; import { MediaUploader } from '@app/chats/components/mediaUploader'; -import { useNDK } from '@libs/ndk/provider'; +import { useArk } from '@libs/ark'; import { EnterIcon } from '@shared/icons'; export function ChatForm({ receiverPubkey }: { receiverPubkey: string }) { - const { ndk } = useNDK(); + const { ark } = useArk(); const [value, setValue] = useState(''); const submit = async () => { try { - const recipient = new NDKUser({ pubkey: receiverPubkey }); - const message = await ndk.signer.encrypt(recipient, value); - - const event = new NDKEvent(ndk); - event.content = message; - event.kind = NDKKind.EncryptedDirectMessage; - event.tag(recipient); - - const publish = await event.publish(); - + const publish = await ark.nip04Encrypt({ content: value, pubkey: receiverPubkey }); if (publish) setValue(''); } catch (e) { toast.error(e); diff --git a/src/app/chats/hooks/useDecryptMessage.tsx b/src/app/chats/hooks/useDecryptMessage.tsx index 88e3d00e..b1320130 100644 --- a/src/app/chats/hooks/useDecryptMessage.tsx +++ b/src/app/chats/hooks/useDecryptMessage.tsx @@ -1,26 +1,17 @@ -import { NDKEvent, NDKUser } from '@nostr-dev-kit/ndk'; +import { NDKEvent } from '@nostr-dev-kit/ndk'; import { useEffect, useState } from 'react'; -import { useNDK } from '@libs/ndk/provider'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; -export function useDecryptMessage(message: NDKEvent) { - const { db } = useStorage(); - const { ndk } = useNDK(); - - const [content, setContent] = useState(message.content); +export function useDecryptMessage(event: NDKEvent) { + const { ark } = useArk(); + const [content, setContent] = useState(event.content); useEffect(() => { async function decryptContent() { try { - const sender = new NDKUser({ - pubkey: - db.account.pubkey === message.pubkey - ? message.tags.find((el) => el[0] === 'p')[1] - : message.pubkey, - }); - const result = await ndk.signer.decrypt(sender, message.content); - setContent(result); + const message = await ark.nip04Decrypt({ event }); + setContent(message); } catch (e) { console.error(e); } diff --git a/src/app/relays/components/relayEventList.tsx b/src/app/relays/components/relayEventList.tsx index e9249a04..ce60f9f3 100644 --- a/src/app/relays/components/relayEventList.tsx +++ b/src/app/relays/components/relayEventList.tsx @@ -1,33 +1,58 @@ import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; -import { useQuery } from '@tanstack/react-query'; -import { normalizeRelayUrl } from 'nostr-fetch'; -import { useCallback } from 'react'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import { useCallback, useMemo } from 'react'; import { VList } from 'virtua'; -import { useNDK } from '@libs/ndk/provider'; +import { useArk } from '@libs/ark'; -import { LoaderIcon } from '@shared/icons'; -import { MemoizedRepost, MemoizedTextNote, UnknownNote } from '@shared/notes'; +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 }) { - const { fetcher } = useNDK(); - const { status, data } = useQuery({ - queryKey: ['relay-events', relayUrl], - queryFn: async () => { - const url = 'wss://' + relayUrl; - const events = await fetcher.fetchLatestEvents( - [normalizeRelayUrl(url)], - { - kinds: [NDKKind.Text, NDKKind.Repost], - }, - 20 - ); - return events as unknown as NDKEvent[]; - }, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - refetchOnMount: false, - }); + const { ark } = useArk(); + const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = + useInfiniteQuery({ + queryKey: ['relay-events', relayUrl], + initialPageParam: 0, + queryFn: async ({ + signal, + pageParam, + }: { + signal: AbortSignal; + pageParam: number; + }) => { + const url = 'wss://' + relayUrl; + const events = await ark.getRelayEvents({ + relayUrl: url, + filter: { + kinds: [NDKKind.Text, NDKKind.Repost], + }, + limit: FETCH_LIMIT, + pageParam, + signal, + }); + + return events; + }, + getNextPageParam: (lastPage) => { + const lastEvent = lastPage.at(-1); + if (!lastEvent) return; + return lastEvent.created_at - 1; + }, + refetchOnWindowFocus: false, + }); + + const allEvents = useMemo( + () => (data ? data.pages.flatMap((page) => page) : []), + [data] + ); const renderItem = useCallback( (event: NDKEvent) => { @@ -46,16 +71,33 @@ export function RelayEventList({ relayUrl }: { relayUrl: string }) { return ( {status === 'pending' ? ( -
-
- -

Loading newsfeed...

+
+
+
) : ( - data.map((item) => renderItem(item)) + allEvents.map((item) => renderItem(item)) )} -
+
+ {hasNextPage ? ( + + ) : null} +
); } diff --git a/src/app/users/index.tsx b/src/app/users/index.tsx index f34c57cf..491edd76 100644 --- a/src/app/users/index.tsx +++ b/src/app/users/index.tsx @@ -1,30 +1,60 @@ import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; -import { useQuery } from '@tanstack/react-query'; -import { useCallback } from 'react'; +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 { useNDK } from '@libs/ndk/provider'; +import { useArk } from '@libs/ark'; -import { MemoizedRepost, MemoizedTextNote, UnknownNote } from '@shared/notes'; +import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; +import { + MemoizedRepost, + MemoizedTextNote, + NoteSkeleton, + UnknownNote, +} from '@shared/notes'; + +import { FETCH_LIMIT } from '@utils/constants'; export function UserScreen() { const { pubkey } = useParams(); - const { ndk } = useNDK(); - const { status, data } = useQuery({ - queryKey: ['user-feed', pubkey], - queryFn: async () => { - const events = await ndk.fetchEvents({ - kinds: [NDKKind.Text, NDKKind.Repost], - authors: [pubkey], - limit: 20, - }); - const sorted = [...events].sort((a, b) => b.created_at - a.created_at); - return sorted; - }, - refetchOnWindowFocus: false, - }); + const { ark } = useArk(); + const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = + useInfiniteQuery({ + queryKey: ['user-posts', pubkey], + initialPageParam: 0, + queryFn: async ({ + signal, + pageParam, + }: { + signal: AbortSignal; + pageParam: number; + }) => { + const events = await ark.getInfiniteEvents({ + filter: { + kinds: [NDKKind.Text, NDKKind.Repost], + authors: [pubkey], + }, + limit: FETCH_LIMIT, + pageParam, + signal, + }); + + return events; + }, + getNextPageParam: (lastPage) => { + const lastEvent = lastPage.at(-1); + if (!lastEvent) return; + return lastEvent.created_at - 1; + }, + refetchOnWindowFocus: false, + }); + + const allEvents = useMemo( + () => (data ? data.pages.flatMap((page) => page) : []), + [data] + ); // render event match event kind const renderItem = useCallback( @@ -50,20 +80,33 @@ export function UserScreen() {
{status === 'pending' ? ( -
Loading...
- ) : data.length === 0 ? (
-
-
-

- User doesn't have any posts in the last 48 hours. -

-
+
+
) : ( - data.map((item) => renderItem(item)) + allEvents.map((item) => renderItem(item)) )} +
+ {hasNextPage ? ( + + ) : null} +
diff --git a/src/libs/ark/ark.ts b/src/libs/ark/ark.ts index 33a7f6f6..49290dd7 100644 --- a/src/libs/ark/ark.ts +++ b/src/libs/ark/ark.ts @@ -13,9 +13,15 @@ import { ndkAdapter } from '@nostr-fetch/adapter-ndk'; 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 Database from '@tauri-apps/plugin-sql'; -import { NostrEventExt, NostrFetcher, normalizeRelayUrlSet } from 'nostr-fetch'; +import { + NostrEventExt, + NostrFetcher, + normalizeRelayUrl, + normalizeRelayUrlSet, +} from 'nostr-fetch'; import { toast } from 'sonner'; import { NDKCacheAdapterTauri } from '@libs/ark'; @@ -77,7 +83,7 @@ export class Ark { // NIP-46 Signer if (nsecbunker) { const localSignerPrivkey = await this.#keyring_load( - `${this.account.pubkey}-nsecbunker` + `${this.account.id}-nsecbunker` ); if (!localSignerPrivkey) { @@ -89,9 +95,9 @@ export class Ark { const bunker = new NDK({ explicitRelayUrls: ['wss://relay.nsecbunker.com', 'wss://nostr.vulpem.com'], }); - bunker.connect(); + await bunker.connect(); - const remoteSigner = new NDKNip46Signer(bunker, this.account.id, localSigner); + const remoteSigner = new NDKNip46Signer(bunker, this.account.pubkey, localSigner); await remoteSigner.blockUntilReady(); this.readyToSign = true; @@ -619,11 +625,13 @@ export class Ark { limit, pageParam = 0, signal = undefined, + dedup = true, }: { filter: NDKFilter; limit: number; pageParam?: number; signal?: AbortSignal; + dedup?: boolean; }) { const rootIds = new Set(); const dedupQueue = new Set(); @@ -637,18 +645,53 @@ export class Ark { return new NDKEvent(this.#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); + if (dedup) { + 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); + } + + return ndkEvents.sort((a, b) => b.created_at - a.created_at); + } + + public async getRelayEvents({ + relayUrl, + filter, + limit, + pageParam = 0, + signal = undefined, + }: { + relayUrl: string; + filter: NDKFilter; + limit: number; + pageParam?: number; + signal?: AbortSignal; + dedup?: boolean; + }) { + const events = await this.#fetcher.fetchLatestEvents( + [normalizeRelayUrl(relayUrl)], + filter, + limit, + { + asOf: pageParam === 0 ? undefined : pageParam, + abortSignal: signal, } + ); + + const ndkEvents = events.map((event) => { + return new NDKEvent(this.#ndk, event); }); - return ndkEvents - .filter((event) => !dedupQueue.has(event.id)) - .sort((a, b) => b.created_at - a.created_at); + return ndkEvents.sort((a, b) => b.created_at - a.created_at); } /** @@ -714,11 +757,60 @@ export class Ark { if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`); const data: NIP05 = await res.json(); - if (data.names) { - if (data.names[localPath.toLowerCase()] !== pubkey) return false; - if (data.names[localPath] !== pubkey) return false; - return true; - } + + if (!data.names) return false; + + if (data.names[localPath.toLowerCase()] === pubkey) return true; + if (data.names[localPath] === pubkey) return true; + return false; } + + 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) { + console.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 publish; + } catch (e) { + console.error(e); + } + } + + public async replyTo({ content, event }: { content: string; event: NDKEvent }) { + try { + const replyEvent = new NDKEvent(this.#ndk); + event.content = content; + event.kind = NDKKind.Text; + event.tag(event, 'reply'); + + return await replyEvent.publish(); + } catch (e) { + console.error(e); + } + } } diff --git a/src/shared/accounts/active.tsx b/src/shared/accounts/active.tsx index 1d3c6512..23e95665 100644 --- a/src/shared/accounts/active.tsx +++ b/src/shared/accounts/active.tsx @@ -2,7 +2,7 @@ import * as Avatar from '@radix-ui/react-avatar'; import { minidenticon } from 'minidenticons'; import { Link } from 'react-router-dom'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { AccountMoreActions } from '@shared/accounts/more'; import { NetworkStatusIndicator } from '@shared/networkStatusIndicator'; @@ -10,12 +10,12 @@ import { NetworkStatusIndicator } from '@shared/networkStatusIndicator'; import { useProfile } from '@utils/hooks/useProfile'; export function ActiveAccount() { - const { db } = useStorage(); - const { user } = useProfile(db.account.pubkey); + const { ark } = useArk(); + const { user } = useProfile(ark.account.pubkey); const svgURI = 'data:image/svg+xml;utf8,' + - encodeURIComponent(minidenticon(db.account.pubkey, 90, 50)); + encodeURIComponent(minidenticon(ark.account.pubkey, 90, 50)); return (
@@ -23,7 +23,7 @@ export function ActiveAccount() { {db.account.pubkey} diff --git a/src/shared/layouts/app.tsx b/src/shared/layouts/app.tsx index 659196e6..b98d09a4 100644 --- a/src/shared/layouts/app.tsx +++ b/src/shared/layouts/app.tsx @@ -2,21 +2,21 @@ import { Outlet, ScrollRestoration } from 'react-router-dom'; import { twMerge } from 'tailwind-merge'; import { WindowTitlebar } from 'tauri-controls'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { Navigation } from '@shared/navigation'; export function AppLayout() { - const { db } = useStorage(); + const { ark } = useArk(); return (
- {db.platform !== 'macos' ? ( + {ark.platform !== 'macos' ? ( ) : (
@@ -26,7 +26,7 @@ export function AppLayout() { data-tauri-drag-region className={twMerge( 'h-full w-[64px] shrink-0', - db.platform !== 'macos' ? 'pt-2' : 'pt-0' + ark.platform !== 'macos' ? 'pt-2' : 'pt-0' )} > diff --git a/src/shared/layouts/auth.tsx b/src/shared/layouts/auth.tsx index 4296eca1..52629947 100644 --- a/src/shared/layouts/auth.tsx +++ b/src/shared/layouts/auth.tsx @@ -1,14 +1,14 @@ import { Outlet, ScrollRestoration } from 'react-router-dom'; import { WindowTitlebar } from 'tauri-controls'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; export function AuthLayout() { - const { db } = useStorage(); + const { ark } = useArk(); return (
- {db.platform !== 'macos' ? ( + {ark.platform !== 'macos' ? ( ) : (
diff --git a/src/shared/layouts/new.tsx b/src/shared/layouts/new.tsx index c92a2799..877da035 100644 --- a/src/shared/layouts/new.tsx +++ b/src/shared/layouts/new.tsx @@ -2,17 +2,17 @@ import { Link, NavLink, Outlet, useLocation } from 'react-router-dom'; import { twMerge } from 'tailwind-merge'; import { WindowTitlebar } from 'tauri-controls'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { ArrowLeftIcon } from '@shared/icons'; export function NewLayout() { - const { db } = useStorage(); + const { ark } = useArk(); const location = useLocation(); return (
- {db.platform !== 'macos' ? ( + {ark.platform !== 'macos' ? ( ) : (
diff --git a/src/shared/layouts/note.tsx b/src/shared/layouts/note.tsx index 4b3106a1..c9eaea3e 100644 --- a/src/shared/layouts/note.tsx +++ b/src/shared/layouts/note.tsx @@ -1,14 +1,14 @@ import { Outlet, ScrollRestoration } from 'react-router-dom'; import { WindowTitlebar } from 'tauri-controls'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; export function NoteLayout() { - const { db } = useStorage(); + const { ark } = useArk(); return (
- {db.platform !== 'macos' ? ( + {ark.platform !== 'macos' ? ( ) : (
diff --git a/src/shared/layouts/settings.tsx b/src/shared/layouts/settings.tsx index 5ccb689f..cbcbbdea 100644 --- a/src/shared/layouts/settings.tsx +++ b/src/shared/layouts/settings.tsx @@ -2,7 +2,7 @@ import { NavLink, Outlet, ScrollRestoration, useNavigate } from 'react-router-do import { twMerge } from 'tailwind-merge'; import { WindowTitlebar } from 'tauri-controls'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { AdvancedSettingsIcon, @@ -14,12 +14,12 @@ import { } from '@shared/icons'; export function SettingsLayout() { - const { db } = useStorage(); + const { ark } = useArk(); const navigate = useNavigate(); return (
- {db.platform !== 'macos' ? ( + {ark.platform !== 'macos' ? ( ) : (
diff --git a/src/shared/notes/actions/reaction.tsx b/src/shared/notes/actions/reaction.tsx index 30143541..e7395c78 100644 --- a/src/shared/notes/actions/reaction.tsx +++ b/src/shared/notes/actions/reaction.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; -import { useNDK } from '@libs/ndk/provider'; +import { useArk } from '@libs/ark'; import { ReactionIcon } from '@shared/icons'; @@ -35,7 +35,7 @@ export function NoteReaction({ event }: { event: NDKEvent }) { const [open, setOpen] = useState(false); const [reaction, setReaction] = useState(null); - const { ndk } = useNDK(); + const { ark } = useArk(); const navigate = useNavigate(); const getReactionImage = (content: string) => { @@ -45,7 +45,7 @@ export function NoteReaction({ event }: { event: NDKEvent }) { const react = async (content: string) => { try { - if (!ndk.signer) return navigate('/new/privkey'); + if (!ark.readyToSign) return navigate('/new/privkey'); setReaction(content); diff --git a/src/shared/notes/actions/repost.tsx b/src/shared/notes/actions/repost.tsx index 4a3d4a66..d6a98324 100644 --- a/src/shared/notes/actions/repost.tsx +++ b/src/shared/notes/actions/repost.tsx @@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; import { twMerge } from 'tailwind-merge'; -import { useNDK } from '@libs/ndk/provider'; +import { useArk } from '@libs/ark'; import { LoaderIcon, RepostIcon } from '@shared/icons'; @@ -15,12 +15,12 @@ export function NoteRepost({ event }: { event: NDKEvent }) { const [isLoading, setIsLoading] = useState(false); const [isRepost, setIsRepost] = useState(false); - const { ndk } = useNDK(); + const { ark } = useArk(); const navigate = useNavigate(); const submit = async () => { try { - if (!ndk.signer) return navigate('/new/privkey'); + if (!ark.readyToSign) return navigate('/new/privkey'); setIsLoading(true); diff --git a/src/shared/notes/actions/zap.tsx b/src/shared/notes/actions/zap.tsx index 6c45abc3..04d3a5bb 100644 --- a/src/shared/notes/actions/zap.tsx +++ b/src/shared/notes/actions/zap.tsx @@ -9,8 +9,7 @@ import { useEffect, useRef, useState } from 'react'; import CurrencyInput from 'react-currency-input-field'; import { useNavigate } from 'react-router-dom'; -import { useNDK } from '@libs/ndk/provider'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { CancelIcon, ZapIcon } from '@shared/icons'; @@ -20,11 +19,7 @@ import { compactNumber } from '@utils/number'; import { displayNpub } from '@utils/shortenKey'; export function NoteZap({ event }: { event: NDKEvent }) { - const nwc = useRef(null); - const navigate = useNavigate(); - - const { db } = useStorage(); - const { ndk } = useNDK(); + const { ark } = useArk(); const { user } = useProfile(event.pubkey); const [walletConnectURL, setWalletConnectURL] = useState(null); @@ -35,9 +30,12 @@ export function NoteZap({ event }: { event: NDKEvent }) { const [isCompleted, setIsCompleted] = useState(false); const [isLoading, setIsLoading] = useState(false); + const nwc = useRef(null); + const navigate = useNavigate(); + const createZapRequest = async () => { try { - if (!ndk.signer) return navigate('/new/privkey'); + if (!ark.readyToSign) return navigate('/new/privkey'); const zapAmount = parseInt(amount) * 1000; const res = await event.zap(zapAmount, zapMessage); @@ -88,7 +86,7 @@ export function NoteZap({ event }: { event: NDKEvent }) { useEffect(() => { async function getWalletConnectURL() { const uri: string = await invoke('secure_load', { - key: `${db.account.pubkey}-nwc`, + key: `${ark.account.pubkey}-nwc`, }); if (uri) setWalletConnectURL(uri); } diff --git a/src/shared/notes/replies/form.tsx b/src/shared/notes/replies/form.tsx index f84dfcf4..6b2fa9e8 100644 --- a/src/shared/notes/replies/form.tsx +++ b/src/shared/notes/replies/form.tsx @@ -1,15 +1,15 @@ -import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; +import { NDKEvent } from '@nostr-dev-kit/ndk'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; -import { useNDK } from '@libs/ndk/provider'; +import { useArk } from '@libs/ark'; import { LoaderIcon } from '@shared/icons'; import { ReplyMediaUploader } from '@shared/notes'; export function NoteReplyForm({ rootEvent }: { rootEvent: NDKEvent }) { - const { ndk } = useNDK(); + const { ark } = useArk(); const navigate = useNavigate(); const [value, setValue] = useState(''); @@ -17,22 +17,14 @@ export function NoteReplyForm({ rootEvent }: { rootEvent: NDKEvent }) { const submit = async () => { try { - if (!ndk.signer) return navigate('/new/privkey'); - + if (!ark.readyToSign) return navigate('/new/privkey'); setLoading(true); - const event = new NDKEvent(ndk); - event.content = value; - event.kind = NDKKind.Text; - - // tag root event - event.tag(rootEvent, 'reply'); - // publish event - const publishedRelays = await event.publish(); + const publish = await ark.replyTo({ content: value, event: rootEvent }); - if (publishedRelays) { - toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`); + if (publish) { + toast.success(`Broadcasted to ${publish.size} relays successfully.`); // reset state setValue(''); diff --git a/src/shared/userProfile.tsx b/src/shared/userProfile.tsx index 8e0f84f7..c66799ac 100644 --- a/src/shared/userProfile.tsx +++ b/src/shared/userProfile.tsx @@ -1,10 +1,8 @@ -import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk'; import { useEffect, useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { toast } from 'sonner'; -import { useNDK } from '@libs/ndk/provider'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { NIP05 } from '@shared/nip05'; @@ -12,18 +10,14 @@ import { useProfile } from '@utils/hooks/useProfile'; import { displayNpub } from '@utils/shortenKey'; export function UserProfile({ pubkey }: { pubkey: string }) { - const { db } = useStorage(); - const { ndk } = useNDK(); + const { ark } = useArk(); const { user } = useProfile(pubkey); const [followed, setFollowed] = useState(false); - const navigate = useNavigate(); const follow = async (pubkey: string) => { try { - const user = ndk.getUser({ pubkey: db.account.pubkey }); - const contacts = await user.follows(); - const add = await user.follow(new NDKUser({ pubkey: pubkey }), contacts); + const add = await ark.createContact({ pubkey }); if (add) { setFollowed(true); @@ -37,22 +31,9 @@ export function UserProfile({ pubkey }: { pubkey: string }) { const unfollow = async (pubkey: string) => { try { - if (!ndk.signer) return navigate('/new/privkey'); + const remove = await ark.deleteContact({ pubkey }); - const user = ndk.getUser({ pubkey: db.account.pubkey }); - const contacts = await user.follows(); - contacts.delete(new NDKUser({ pubkey: pubkey })); - - let list: string[][]; - contacts.forEach((el) => list.push(['p', el.pubkey, el.relayUrls?.[0] || '', ''])); - - const event = new NDKEvent(ndk); - event.content = ''; - event.kind = NDKKind.Contacts; - event.tags = list; - - const publishedRelays = await event.publish(); - if (publishedRelays) { + if (remove) { setFollowed(false); } } catch (error) { @@ -61,7 +42,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) { }; useEffect(() => { - if (db.account.contacts.includes(pubkey)) { + if (ark.account.contacts.includes(pubkey)) { setFollowed(true); } }, []); diff --git a/src/shared/widgets/article.tsx b/src/shared/widgets/article.tsx index 7a29799b..7c90092b 100644 --- a/src/shared/widgets/article.tsx +++ b/src/shared/widgets/article.tsx @@ -1,11 +1,10 @@ -import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; +import { NDKKind } from '@nostr-dev-kit/ndk'; import { useInfiniteQuery } from '@tanstack/react-query'; import { FetchFilter } from 'nostr-fetch'; import { useMemo } from 'react'; import { VList } from 'virtua'; -import { useNDK } from '@libs/ndk/provider'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { MemoizedArticleNote } from '@shared/notes'; @@ -16,8 +15,7 @@ import { FETCH_LIMIT } from '@utils/constants'; import { Widget } from '@utils/types'; export function ArticleWidget({ widget }: { widget: Widget }) { - const { db } = useStorage(); - const { ndk, relayUrls, fetcher } = useNDK(); + const { ark } = useArk(); const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({ queryKey: ['article', widget.id], @@ -39,20 +37,19 @@ export function ArticleWidget({ widget }: { widget: Widget }) { } else { filter = { kinds: [NDKKind.Article], - authors: db.account.contacts, + authors: ark.account.contacts, }; } - const events = await fetcher.fetchLatestEvents(relayUrls, filter, FETCH_LIMIT, { - asOf: pageParam === 0 ? undefined : pageParam, - abortSignal: signal, + const events = await ark.getInfiniteEvents({ + filter, + limit: FETCH_LIMIT, + pageParam, + signal, + dedup: false, }); - const ndkEvents = events.map((event) => { - return new NDKEvent(ndk, event); - }); - - return ndkEvents.sort((a, b) => b.created_at - a.created_at); + return events; }, getNextPageParam: (lastPage) => { const lastEvent = lastPage.at(-1); diff --git a/src/shared/widgets/file.tsx b/src/shared/widgets/file.tsx index 2fe07523..d1f95604 100644 --- a/src/shared/widgets/file.tsx +++ b/src/shared/widgets/file.tsx @@ -1,11 +1,9 @@ -import { NDKEvent } from '@nostr-dev-kit/ndk'; import { useInfiniteQuery } from '@tanstack/react-query'; import { FetchFilter } from 'nostr-fetch'; import { useMemo } from 'react'; import { VList } from 'virtua'; -import { useNDK } from '@libs/ndk/provider'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { MemoizedFileNote } from '@shared/notes'; @@ -16,8 +14,7 @@ import { FETCH_LIMIT } from '@utils/constants'; import { Widget } from '@utils/types'; export function FileWidget({ widget }: { widget: Widget }) { - const { db } = useStorage(); - const { ndk, relayUrls, fetcher } = useNDK(); + const { ark } = useArk(); const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({ queryKey: ['media', widget.id], @@ -39,20 +36,19 @@ export function FileWidget({ widget }: { widget: Widget }) { } else { filter = { kinds: [1063], - authors: db.account.contacts, + authors: ark.account.contacts, }; } - const events = await fetcher.fetchLatestEvents(relayUrls, filter, FETCH_LIMIT, { - asOf: pageParam === 0 ? undefined : pageParam, - abortSignal: signal, + const events = await ark.getInfiniteEvents({ + filter, + limit: FETCH_LIMIT, + pageParam, + signal, + dedup: false, }); - const ndkEvents = events.map((event) => { - return new NDKEvent(ndk, event); - }); - - return ndkEvents.sort((a, b) => b.created_at - a.created_at); + return events; }, getNextPageParam: (lastPage) => { const lastEvent = lastPage.at(-1); diff --git a/src/shared/widgets/group.tsx b/src/shared/widgets/group.tsx index 41be9b0c..dd9aeaec 100644 --- a/src/shared/widgets/group.tsx +++ b/src/shared/widgets/group.tsx @@ -3,7 +3,7 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import { useCallback, useMemo } from 'react'; import { VList } from 'virtua'; -import { useNDK } from '@libs/ndk/provider'; +import { useArk } from '@libs/ark'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { @@ -19,7 +19,7 @@ import { FETCH_LIMIT } from '@utils/constants'; import { Widget } from '@utils/types'; export function GroupWidget({ widget }: { widget: Widget }) { - const { relayUrls, ndk, fetcher } = useNDK(); + const { ark } = useArk(); const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({ queryKey: ['groupfeeds', widget.id], @@ -32,21 +32,18 @@ export function GroupWidget({ widget }: { widget: Widget }) { pageParam: number; }) => { const authors = JSON.parse(widget.content); - const events = await fetcher.fetchLatestEvents( - relayUrls, - { + const events = await ark.getInfiniteEvents({ + filter: { kinds: [NDKKind.Text, NDKKind.Repost], authors: authors, }, - FETCH_LIMIT, - { asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal } - ); - - const ndkEvents = events.map((event) => { - return new NDKEvent(ndk, event); + limit: FETCH_LIMIT, + pageParam, + signal, + dedup: false, }); - return ndkEvents.sort((a, b) => b.created_at - a.created_at); + return events; }, getNextPageParam: (lastPage) => { const lastEvent = lastPage.at(-1); diff --git a/src/shared/widgets/hashtag.tsx b/src/shared/widgets/hashtag.tsx index 178de71f..567f39f5 100644 --- a/src/shared/widgets/hashtag.tsx +++ b/src/shared/widgets/hashtag.tsx @@ -3,7 +3,7 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import { useCallback, useMemo } from 'react'; import { VList } from 'virtua'; -import { useNDK } from '@libs/ndk/provider'; +import { useArk } from '@libs/ark'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { MemoizedRepost, MemoizedTextNote, UnknownNote } from '@shared/notes'; @@ -14,7 +14,7 @@ import { FETCH_LIMIT } from '@utils/constants'; import { Widget } from '@utils/types'; export function HashtagWidget({ widget }: { widget: Widget }) { - const { ndk, relayUrls, fetcher } = useNDK(); + const { ark } = useArk(); const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({ queryKey: ['hashtag', widget.id], @@ -26,21 +26,18 @@ export function HashtagWidget({ widget }: { widget: Widget }) { signal: AbortSignal; pageParam: number; }) => { - const events = await fetcher.fetchLatestEvents( - relayUrls, - { + const events = await ark.getInfiniteEvents({ + filter: { kinds: [NDKKind.Text, NDKKind.Repost], '#t': [widget.content], }, - FETCH_LIMIT, - { asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal } - ); - - const ndkEvents = events.map((event) => { - return new NDKEvent(ndk, event); + limit: FETCH_LIMIT, + pageParam, + signal, + dedup: false, }); - return ndkEvents.sort((a, b) => b.created_at - a.created_at); + return events; }, getNextPageParam: (lastPage) => { const lastEvent = lastPage.at(-1); diff --git a/src/shared/widgets/newsfeed.tsx b/src/shared/widgets/newsfeed.tsx index b2e8bf16..5ed78a79 100644 --- a/src/shared/widgets/newsfeed.tsx +++ b/src/shared/widgets/newsfeed.tsx @@ -3,8 +3,7 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import { useCallback, useMemo, useRef } from 'react'; import { VList, VListHandle } from 'virtua'; -import { useNDK } from '@libs/ndk/provider'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons'; import { @@ -19,8 +18,7 @@ import { LiveUpdater, WidgetWrapper } from '@shared/widgets'; import { FETCH_LIMIT } from '@utils/constants'; export function NewsfeedWidget() { - const { db } = useStorage(); - const { relayUrls, ndk, fetcher } = useNDK(); + const { ark } = useArk(); const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({ queryKey: ['newsfeed'], @@ -32,35 +30,17 @@ export function NewsfeedWidget() { signal: AbortSignal; pageParam: number; }) => { - const rootIds = new Set(); - const dedupQueue = new Set(); - - const events = await fetcher.fetchLatestEvents( - relayUrls, - { + const events = await ark.getInfiniteEvents({ + filter: { kinds: [NDKKind.Text, NDKKind.Repost], - authors: db.account.contacts, + authors: ark.account.contacts, }, - FETCH_LIMIT, - { asOf: pageParam === 0 ? undefined : pageParam, abortSignal: signal } - ); - - const ndkEvents = events.map((event) => { - return new NDKEvent(ndk, event); + limit: FETCH_LIMIT, + pageParam, + signal, }); - 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); + return events; }, getNextPageParam: (lastPage) => { const lastEvent = lastPage.at(-1); diff --git a/src/shared/widgets/other/addGroupFeeds.tsx b/src/shared/widgets/other/addGroupFeeds.tsx index 4d2f9b9c..6713e441 100644 --- a/src/shared/widgets/other/addGroupFeeds.tsx +++ b/src/shared/widgets/other/addGroupFeeds.tsx @@ -1,7 +1,7 @@ import * as Dialog from '@radix-ui/react-dialog'; import { useState } from 'react'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { ArrowRightCircleIcon, @@ -16,7 +16,7 @@ import { WIDGET_KIND } from '@utils/constants'; import { useWidget } from '@utils/hooks/useWidget'; export function AddGroupFeeds({ currentWidgetId }: { currentWidgetId: string }) { - const { db } = useStorage(); + const { ark } = useArk(); const { replaceWidget } = useWidget(); const [title, setTitle] = useState(''); @@ -95,7 +95,7 @@ export function AddGroupFeeds({ currentWidgetId }: { currentWidgetId: string }) Users
- {db.account.contacts.map((item: string) => ( + {ark.account?.contacts?.map((item: string) => (
- ) : data.length === 0 ? ( -
-
-
-

- No new post from 24 hours ago -

-
-
-
) : ( - data.map((item) => renderItem(item)) + allEvents.map((item) => renderItem(item)) )} +
+ {hasNextPage ? ( + + ) : null} +
diff --git a/src/utils/hooks/useSuggestion.ts b/src/utils/hooks/useSuggestion.ts index e8317d80..5307d878 100644 --- a/src/utils/hooks/useSuggestion.ts +++ b/src/utils/hooks/useSuggestion.ts @@ -4,14 +4,14 @@ import tippy from 'tippy.js'; import { MentionList } from '@app/new/components'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; export function useSuggestion() { - const { db } = useStorage(); + const { ark } = useArk(); const suggestion: MentionOptions['suggestion'] = { items: async ({ query }) => { - const users = await db.getAllCacheUsers(); + const users = await ark.getAllCacheUsers(); return users .filter((item) => { if (item.name) return item.name.toLowerCase().startsWith(query.toLowerCase()); diff --git a/src/utils/hooks/useWidget.ts b/src/utils/hooks/useWidget.ts index e8c902a8..c64d5519 100644 --- a/src/utils/hooks/useWidget.ts +++ b/src/utils/hooks/useWidget.ts @@ -1,16 +1,16 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { Widget } from '@utils/types'; export function useWidget() { - const { db } = useStorage(); + const { ark } = useArk(); const queryClient = useQueryClient(); const addWidget = useMutation({ mutationFn: async (widget: Widget) => { - return await db.createWidget(widget.kind, widget.title, widget.content); + return await ark.createWidget(widget.kind, widget.title, widget.content); }, onSuccess: (data) => { queryClient.setQueryData(['widgets'], (old: Widget[]) => [...old, data]); @@ -26,8 +26,8 @@ export function useWidget() { const prevWidgets = queryClient.getQueryData(['widgets']); // create new widget - await db.removeWidget(currentId); - const newWidget = await db.createWidget(widget.kind, widget.title, widget.content); + await ark.removeWidget(currentId); + const newWidget = await ark.createWidget(widget.kind, widget.title, widget.content); // Optimistically update to the new value queryClient.setQueryData(['widgets'], (prev: Widget[]) => [ @@ -57,7 +57,7 @@ export function useWidget() { ); // Update in database - await db.removeWidget(id); + await ark.removeWidget(id); // Return a context object with the snapshotted value return { prevWidgets };