From 7507cd9ba149971296aaad068126cddf58584632 Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 7 Dec 2023 18:09:00 +0700 Subject: [PATCH] wip: migrate to ark --- src/app.tsx | 6 +- src/app/auth/onboarding.tsx | 14 +- src/app/chats/components/mediaUploader.tsx | 10 +- src/app/error.tsx | 10 +- src/app/home/index.tsx | 6 +- src/app/new/components/mediaUploader.tsx | 10 +- src/app/new/components/mentionPopup.tsx | 8 +- src/app/notes/text.tsx | 7 +- src/app/nwc/components/form.tsx | 6 +- src/app/nwc/index.tsx | 8 +- src/app/relays/components/relayList.tsx | 7 +- src/app/relays/components/userRelayList.tsx | 21 +- src/app/settings/advanced.tsx | 6 +- src/app/settings/backup.tsx | 8 +- src/app/settings/components/postCard.tsx | 12 +- src/app/settings/components/profileCard.tsx | 14 +- src/app/settings/components/zapCard.tsx | 10 +- src/app/settings/general.tsx | 20 +- src/app/users/components/profile.tsx | 34 +-- src/libs/ark/ark.ts | 69 ++++- src/shared/avatarUploader.tsx | 8 +- src/shared/bannerUploader.tsx | 8 +- src/shared/notes/notify.tsx | 7 +- src/shared/notes/replies/list.tsx | 22 +- src/shared/notes/text.tsx | 7 +- src/shared/titleBar.tsx | 10 +- src/shared/widgets/notification.tsx | 6 +- src/shared/widgets/thread.tsx | 7 +- src/utils/hooks/useNostr.ts | 299 -------------------- 29 files changed, 206 insertions(+), 454 deletions(-) delete mode 100644 src/utils/hooks/useNostr.ts diff --git a/src/app.tsx b/src/app.tsx index 3f75e465..f5cbc78b 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -7,7 +7,7 @@ import { ChatsScreen } from '@app/chats'; import { ErrorScreen } from '@app/error'; import { ExploreScreen } from '@app/explore'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { LoaderIcon } from '@shared/icons'; import { AppLayout } from '@shared/layouts/app'; @@ -19,12 +19,12 @@ import { SettingsLayout } from '@shared/layouts/settings'; import './app.css'; export default function App() { - const { db } = useStorage(); + const { ark } = useArk(); const accountLoader = async () => { try { // redirect to welcome screen if none user exist - const totalAccount = await db.checkAccount(); + const totalAccount = await ark.checkAccount(); if (totalAccount === 0) return redirect('/auth/welcome'); return null; diff --git a/src/app/auth/onboarding.tsx b/src/app/auth/onboarding.tsx index fdf3f462..d4e065d4 100644 --- a/src/app/auth/onboarding.tsx +++ b/src/app/auth/onboarding.tsx @@ -3,12 +3,12 @@ import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notif import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { InfoIcon } from '@shared/icons'; export function OnboardingScreen() { - const { db } = useStorage(); + const { ark } = useArk(); const navigate = useNavigate(); const [settings, setSettings] = useState({ @@ -18,19 +18,19 @@ export function OnboardingScreen() { }); const next = () => { - if (!db.account.contacts.length) return navigate('/auth/follow'); + if (!ark.account.contacts.length) return navigate('/auth/follow'); return navigate('/auth/finish'); }; const toggleOutbox = async () => { - await db.createSetting('outbox', String(+!settings.outbox)); + await ark.createSetting('outbox', String(+!settings.outbox)); // update state setSettings((prev) => ({ ...prev, outbox: !settings.outbox })); }; const toggleAutoupdate = async () => { - await db.createSetting('autoupdate', String(+!settings.autoupdate)); - db.settings.autoupdate = !settings.autoupdate; + await ark.createSetting('autoupdate', String(+!settings.autoupdate)); + ark.settings.autoupdate = !settings.autoupdate; // update state setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate })); }; @@ -46,7 +46,7 @@ export function OnboardingScreen() { const permissionGranted = await isPermissionGranted(); setSettings((prev) => ({ ...prev, notification: permissionGranted })); - const data = await db.getAllSettings(); + const data = await ark.getAllSettings(); if (!data) return; data.forEach((item) => { diff --git a/src/app/chats/components/mediaUploader.tsx b/src/app/chats/components/mediaUploader.tsx index 165b9470..adec461f 100644 --- a/src/app/chats/components/mediaUploader.tsx +++ b/src/app/chats/components/mediaUploader.tsx @@ -1,22 +1,24 @@ import * as Tooltip from '@radix-ui/react-tooltip'; import { Dispatch, SetStateAction, useState } from 'react'; -import { LoaderIcon, MediaIcon } from '@shared/icons'; +import { useArk } from '@libs/ark'; -import { useNostr } from '@utils/hooks/useNostr'; +import { LoaderIcon, MediaIcon } from '@shared/icons'; export function MediaUploader({ setState, }: { setState: Dispatch>; }) { - const { upload } = useNostr(); + const { ark } = useArk(); const [loading, setLoading] = useState(false); const uploadMedia = async () => { setLoading(true); - const image = await upload(['mp4', 'mp3', 'webm', 'mkv', 'avi', 'mov']); + const image = await ark.upload({ + fileExts: ['mp4', 'mp3', 'webm', 'mkv', 'avi', 'mov'], + }); if (image) { setState((prev: string) => `${prev}\n${image}`); diff --git a/src/app/error.tsx b/src/app/error.tsx index 6028c818..0a8f6fc2 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -4,7 +4,7 @@ import { writeTextFile } from '@tauri-apps/plugin-fs'; import { relaunch } from '@tauri-apps/plugin-process'; import { useRouteError } from 'react-router-dom'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; interface RouteError { statusText: string; @@ -12,7 +12,7 @@ interface RouteError { } export function ErrorScreen() { - const { db } = useStorage(); + const { ark } = useArk(); const error = useRouteError() as RouteError; const restart = async () => { @@ -26,18 +26,18 @@ export function ErrorScreen() { const filePath = await save({ defaultPath: downloadPath + '/' + fileName, }); - const nsec = await db.secureLoad(db.account.pubkey); + const nsec = await ark.loadPrivkey(ark.account.pubkey); if (filePath) { if (nsec) { await writeTextFile( filePath, - `Nostr account, generated by Lume (lume.nu)\nPublic key: ${db.account.id}\nPrivate key: ${nsec}` + `Nostr account, generated by Lume (lume.nu)\nPublic key: ${ark.account.id}\nPrivate key: ${nsec}` ); } else { await writeTextFile( filePath, - `Nostr account, generated by Lume (lume.nu)\nPublic key: ${db.account.id}` + `Nostr account, generated by Lume (lume.nu)\nPublic key: ${ark.account.id}` ); } } // else { user cancel action } diff --git a/src/app/home/index.tsx b/src/app/home/index.tsx index fcca85ed..08072a9f 100644 --- a/src/app/home/index.tsx +++ b/src/app/home/index.tsx @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { useCallback, useRef, useState } from 'react'; import { VList, VListHandle } from 'virtua'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { LoaderIcon } from '@shared/icons'; import { @@ -28,11 +28,11 @@ export function HomeScreen() { const ref = useRef(null); const [selectedIndex, setSelectedIndex] = useState(-1); - const { db } = useStorage(); + const { ark } = useArk(); const { status, data } = useQuery({ queryKey: ['widgets'], queryFn: async () => { - const dbWidgets = await db.getWidgets(); + const dbWidgets = await ark.getWidgets(); const defaultWidgets = [ { id: '9999', diff --git a/src/app/new/components/mediaUploader.tsx b/src/app/new/components/mediaUploader.tsx index 89cdba11..6c0319c7 100644 --- a/src/app/new/components/mediaUploader.tsx +++ b/src/app/new/components/mediaUploader.tsx @@ -2,12 +2,12 @@ import { message } from '@tauri-apps/plugin-dialog'; import { Editor } from '@tiptap/react'; import { useState } from 'react'; +import { useArk } from '@libs/ark'; + import { MediaIcon } from '@shared/icons'; -import { useNostr } from '@utils/hooks/useNostr'; - export function MediaUploader({ editor }: { editor: Editor }) { - const { upload } = useNostr(); + const { ark } = useArk(); const [loading, setLoading] = useState(false); const uploadToNostrBuild = async () => { @@ -15,7 +15,9 @@ export function MediaUploader({ editor }: { editor: Editor }) { // start loading setLoading(true); - const image = await upload(['mp4', 'mp3', 'webm', 'mkv', 'avi', 'mov']); + const image = await ark.upload({ + fileExts: ['mp4', 'mp3', 'webm', 'mkv', 'avi', 'mov'], + }); if (image) { editor.commands.setImage({ src: image }); diff --git a/src/app/new/components/mentionPopup.tsx b/src/app/new/components/mentionPopup.tsx index 26c07eb0..c574cdb4 100644 --- a/src/app/new/components/mentionPopup.tsx +++ b/src/app/new/components/mentionPopup.tsx @@ -4,12 +4,12 @@ import { nip19 } from 'nostr-tools'; import { MentionPopupItem } from '@app/new/components'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { MentionIcon } from '@shared/icons'; export function MentionPopup({ editor }: { editor: Editor }) { - const { db } = useStorage(); + const { ark } = useArk(); const insertMention = (pubkey: string) => { editor.commands.insertContent(`nostr:${nip19.npubEncode(pubkey)}`); @@ -32,8 +32,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" >
- {db.account.contacts.length > 0 ? ( - db.account.contacts.map((item) => ( + {ark.account.contacts.length ? ( + ark.account.contacts.map((item) => ( diff --git a/src/app/notes/text.tsx b/src/app/notes/text.tsx index 9e723251..a1c7e744 100644 --- a/src/app/notes/text.tsx +++ b/src/app/notes/text.tsx @@ -6,6 +6,8 @@ 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, @@ -18,15 +20,14 @@ import { ReplyList } from '@shared/notes/replies/list'; import { User } from '@shared/user'; import { useEvent } from '@utils/hooks/useEvent'; -import { useNostr } from '@utils/hooks/useNostr'; export function TextNoteScreen() { const navigate = useNavigate(); const replyRef = useRef(null); const { id } = useParams(); + const { ark } = useArk(); const { status, data } = useEvent(id); - const { getEventThread } = useNostr(); const [isCopy, setIsCopy] = useState(false); @@ -50,7 +51,7 @@ export function TextNoteScreen() { }; const renderKind = (event: NDKEvent) => { - const thread = getEventThread(event.tags); + const thread = ark.getEventThread({ tags: event.tags }); switch (event.kind) { case NDKKind.Text: return ( diff --git a/src/app/nwc/components/form.tsx b/src/app/nwc/components/form.tsx index 41a38faa..807968be 100644 --- a/src/app/nwc/components/form.tsx +++ b/src/app/nwc/components/form.tsx @@ -1,12 +1,12 @@ import { useState } from 'react'; import { toast } from 'sonner'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { LoaderIcon } from '@shared/icons'; export function NWCForm({ setWalletConnectURL }) { - const { db } = useStorage(); + const { ark } = useArk(); const [uri, setUri] = useState(''); const [loading, setLoading] = useState(false); @@ -27,7 +27,7 @@ export function NWCForm({ setWalletConnectURL }) { const params = new URLSearchParams(uriObj.search); if (params.has('relay') && params.has('secret')) { - await db.secureSave(`${db.account.pubkey}-nwc`, uri); + await ark.createPrivkey(`${ark.account.pubkey}-nwc`, uri); setWalletConnectURL(uri); setLoading(false); } else { diff --git a/src/app/nwc/index.tsx b/src/app/nwc/index.tsx index a970338e..9340375d 100644 --- a/src/app/nwc/index.tsx +++ b/src/app/nwc/index.tsx @@ -2,22 +2,22 @@ import { useEffect, useState } from 'react'; import { NWCForm } from '@app/nwc/components/form'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { CheckCircleIcon } from '@shared/icons'; export function NWCScreen() { - const { db } = useStorage(); + const { ark } = useArk(); const [walletConnectURL, setWalletConnectURL] = useState(null); const remove = async () => { - await db.secureRemove(`${db.account.pubkey}-nwc`); + await ark.removePrivkey(`${ark.account.pubkey}-nwc`); setWalletConnectURL(null); }; useEffect(() => { async function getNWC() { - const nwc = await db.secureLoad(`${db.account.pubkey}-nwc`); + const nwc = await ark.loadPrivkey(`${ark.account.pubkey}-nwc`); if (nwc) setWalletConnectURL(nwc); } getNWC(); diff --git a/src/app/relays/components/relayList.tsx b/src/app/relays/components/relayList.tsx index 23861733..976f0e3c 100644 --- a/src/app/relays/components/relayList.tsx +++ b/src/app/relays/components/relayList.tsx @@ -2,19 +2,20 @@ import { useQuery } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { VList } from 'virtua'; +import { useArk } from '@libs/ark'; + import { LoaderIcon, PlusIcon, ShareIcon } from '@shared/icons'; import { User } from '@shared/user'; -import { useNostr } from '@utils/hooks/useNostr'; import { useRelay } from '@utils/hooks/useRelay'; export function RelayList() { - const { getAllRelaysByUsers } = useNostr(); + const { ark } = useArk(); const { connectRelay } = useRelay(); const { status, data } = useQuery({ queryKey: ['relays'], queryFn: async () => { - return await getAllRelaysByUsers(); + return await ark.getAllRelaysFromContacts(); }, refetchOnWindowFocus: false, refetchOnMount: false, diff --git a/src/app/relays/components/userRelayList.tsx b/src/app/relays/components/userRelayList.tsx index 3afa1235..68e0ca93 100644 --- a/src/app/relays/components/userRelayList.tsx +++ b/src/app/relays/components/userRelayList.tsx @@ -1,29 +1,26 @@ -import { NDKKind, NDKSubscriptionCacheUsage } from '@nostr-dev-kit/ndk'; +import { NDKKind } from '@nostr-dev-kit/ndk'; import { useQuery } from '@tanstack/react-query'; import { RelayForm } from '@app/relays/components/relayForm'; -import { useNDK } from '@libs/ndk/provider'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { CancelIcon, RefreshIcon } from '@shared/icons'; import { useRelay } from '@utils/hooks/useRelay'; export function UserRelayList() { - const { db } = useStorage(); - const { ndk } = useNDK(); + const { ark } = useArk(); const { removeRelay } = useRelay(); const { status, data, refetch } = useQuery({ - queryKey: ['relays', db.account.pubkey], + queryKey: ['relays', ark.account.pubkey], queryFn: async () => { - const event = await ndk.fetchEvent( - { + const event = await ark.getEventByFilter({ + filter: { kinds: [NDKKind.RelayList], - authors: [db.account.pubkey], + authors: [ark.account.pubkey], }, - { cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY } - ); + }); if (!event) return []; return event.tags; @@ -31,7 +28,7 @@ export function UserRelayList() { refetchOnWindowFocus: false, }); - const currentRelays = new Set([...ndk.pool.relays.values()].map((item) => item.url)); + const currentRelays = new Set([...ark.relays]); return (
diff --git a/src/app/settings/advanced.tsx b/src/app/settings/advanced.tsx index 456c7056..fd7100e2 100644 --- a/src/app/settings/advanced.tsx +++ b/src/app/settings/advanced.tsx @@ -1,10 +1,10 @@ -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; export function AdvancedSettingScreen() { - const { db } = useStorage(); + const { ark } = useArk(); const clearCache = async () => { - await db.clearCache(); + await ark.clearCache(); }; return ( diff --git a/src/app/settings/backup.tsx b/src/app/settings/backup.tsx index c8cd0cd1..a4b9f2c8 100644 --- a/src/app/settings/backup.tsx +++ b/src/app/settings/backup.tsx @@ -1,23 +1,23 @@ import { nip19 } from 'nostr-tools'; import { useEffect, useState } from 'react'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { EyeOffIcon } from '@shared/icons'; export function BackupSettingScreen() { - const { db } = useStorage(); + const { ark } = useArk(); const [privkey, setPrivkey] = useState(null); const [showPassword, setShowPassword] = useState(false); const removePrivkey = async () => { - await db.secureRemove(db.account.pubkey); + await ark.removePrivkey(ark.account.pubkey); }; useEffect(() => { async function loadPrivkey() { - const key = await db.secureLoad(db.account.pubkey); + const key = await ark.loadPrivkey(ark.account.pubkey); if (key) setPrivkey(key); } diff --git a/src/app/settings/components/postCard.tsx b/src/app/settings/components/postCard.tsx index 374fe14c..dd344214 100644 --- a/src/app/settings/components/postCard.tsx +++ b/src/app/settings/components/postCard.tsx @@ -2,19 +2,19 @@ import { useQuery } from '@tanstack/react-query'; import { fetch } from '@tauri-apps/plugin-http'; import { Link } from 'react-router-dom'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { LoaderIcon } from '@shared/icons'; import { compactNumber } from '@utils/number'; export function PostCard() { - const { db } = useStorage(); + const { ark } = useArk(); const { status, data } = useQuery({ - queryKey: ['user-stats', db.account.pubkey], + queryKey: ['user-stats', ark.account.pubkey], queryFn: async ({ signal }: { signal: AbortSignal }) => { const res = await fetch( - `https://api.nostr.band/v0/stats/profile/${db.account.pubkey}`, + `https://api.nostr.band/v0/stats/profile/${ark.account.pubkey}`, { signal, } @@ -41,14 +41,14 @@ export function PostCard() { ) : (

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

Posts

View diff --git a/src/app/settings/components/profileCard.tsx b/src/app/settings/components/profileCard.tsx index f8c8429c..bfec4ffd 100644 --- a/src/app/settings/components/profileCard.tsx +++ b/src/app/settings/components/profileCard.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 { EditIcon, LoaderIcon } from '@shared/icons'; @@ -10,12 +10,12 @@ import { useProfile } from '@utils/hooks/useProfile'; import { displayNpub } from '@utils/shortenKey'; export function ProfileCard() { - const { db } = useStorage(); - const { isLoading, user } = useProfile(db.account.pubkey); + const { ark } = useArk(); + const { isLoading, 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 (
@@ -38,7 +38,7 @@ export function ProfileCard() { {db.account.pubkey} @@ -57,7 +57,7 @@ export function ProfileCard() { {user?.display_name || user?.name}

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

diff --git a/src/app/settings/components/zapCard.tsx b/src/app/settings/components/zapCard.tsx index d1e35a84..e8f4a12b 100644 --- a/src/app/settings/components/zapCard.tsx +++ b/src/app/settings/components/zapCard.tsx @@ -1,19 +1,19 @@ import { useQuery } from '@tanstack/react-query'; import { fetch } from '@tauri-apps/plugin-http'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { LoaderIcon } from '@shared/icons'; import { compactNumber } from '@utils/number'; export function ZapCard() { - const { db } = useStorage(); + const { ark } = useArk(); const { status, data } = useQuery({ - queryKey: ['user-stats', db.account.pubkey], + queryKey: ['user-stats', ark.account.pubkey], queryFn: async ({ signal }: { signal: AbortSignal }) => { const res = await fetch( - `https://api.nostr.band/v0/stats/profile/${db.account.pubkey}`, + `https://api.nostr.band/v0/stats/profile/${ark.account.pubkey}`, { signal, } @@ -41,7 +41,7 @@ export function ZapCard() {

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

diff --git a/src/app/settings/general.tsx b/src/app/settings/general.tsx index a8f6437e..7ae91bde 100644 --- a/src/app/settings/general.tsx +++ b/src/app/settings/general.tsx @@ -6,12 +6,12 @@ import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notif import { useEffect, useState } from 'react'; import { twMerge } from 'tailwind-merge'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { DarkIcon, LightIcon, SystemModeIcon } from '@shared/icons'; export function GeneralSettingScreen() { - const { db } = useStorage(); + const { ark } = useArk(); const [settings, setSettings] = useState({ autoupdate: false, autolaunch: false, @@ -41,28 +41,28 @@ export function GeneralSettingScreen() { }; const toggleOutbox = async () => { - await db.createSetting('outbox', String(+!settings.outbox)); + await ark.createSetting('outbox', String(+!settings.outbox)); // update state setSettings((prev) => ({ ...prev, outbox: !settings.outbox })); }; const toggleMedia = async () => { - await db.createSetting('media', String(+!settings.media)); - db.settings.media = !settings.media; + await ark.createSetting('media', String(+!settings.media)); + ark.settings.media = !settings.media; // update state setSettings((prev) => ({ ...prev, media: !settings.media })); }; const toggleHashtag = async () => { - await db.createSetting('hashtag', String(+!settings.hashtag)); - db.settings.hashtag = !settings.hashtag; + await ark.createSetting('hashtag', String(+!settings.hashtag)); + ark.settings.hashtag = !settings.hashtag; // update state setSettings((prev) => ({ ...prev, hashtag: !settings.hashtag })); }; const toggleAutoupdate = async () => { - await db.createSetting('autoupdate', String(+!settings.autoupdate)); - db.settings.autoupdate = !settings.autoupdate; + await ark.createSetting('autoupdate', String(+!settings.autoupdate)); + ark.settings.autoupdate = !settings.autoupdate; // update state setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate })); }; @@ -86,7 +86,7 @@ export function GeneralSettingScreen() { const permissionGranted = await isPermissionGranted(); setSettings((prev) => ({ ...prev, notification: permissionGranted })); - const data = await db.getAllSettings(); + const data = await ark.getAllSettings(); if (!data) return; data.forEach((item) => { diff --git a/src/app/users/components/profile.tsx b/src/app/users/components/profile.tsx index 07629ed6..910c909a 100644 --- a/src/app/users/components/profile.tsx +++ b/src/app/users/components/profile.tsx @@ -1,4 +1,3 @@ -import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk'; import * as Avatar from '@radix-ui/react-avatar'; import { minidenticon } from 'minidenticons'; import { useEffect, useState } from 'react'; @@ -7,8 +6,7 @@ import { toast } from 'sonner'; import { UserStats } from '@app/users/components/stats'; -import { useNDK } from '@libs/ndk/provider'; -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { NIP05 } from '@shared/nip05'; @@ -16,8 +14,7 @@ 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); @@ -28,12 +25,10 @@ export function UserProfile({ pubkey }: { pubkey: string }) { const follow = async () => { try { - if (!ndk.signer) return navigate('/new/privkey'); + if (!ark.readyToSign) return navigate('/new/privkey'); setFollowed(true); - 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) { toast.success('You already follow this user'); @@ -47,32 +42,17 @@ export function UserProfile({ pubkey }: { pubkey: string }) { const unfollow = async () => { try { - if (!ndk.signer) return navigate('/new/privkey'); + if (!ark.readyToSign) return navigate('/new/privkey'); setFollowed(false); - const user = ndk.getUser({ pubkey: db.account.pubkey }); - const contacts = await user.follows(); - contacts.delete(new NDKUser({ pubkey: pubkey })); - - const list = [...contacts].map((item) => [ - 'p', - item.pubkey, - item.relayUrls?.[0] || '', - '', - ]); - const event = new NDKEvent(ndk); - event.content = ''; - event.kind = NDKKind.Contacts; - event.tags = list; - - await event.publish(); + await ark.deleteContact({ pubkey }); } catch (e) { toast.error(e); } }; useEffect(() => { - if (db.account.contacts.includes(pubkey)) { + if (ark.account.contacts.includes(pubkey)) { setFollowed(true); } }, []); diff --git a/src/libs/ark/ark.ts b/src/libs/ark/ark.ts index cad18714..33a7f6f6 100644 --- a/src/libs/ark/ark.ts +++ b/src/libs/ark/ark.ts @@ -241,10 +241,26 @@ export class Ark { /** * Save private key to OS secure storage - * @deprecated this method will be marked as private in the next update + * @deprecated this method will be remove in the next update */ public async createPrivkey(name: string, privkey: string) { - await this.#keyring_save(name, privkey); + 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) { @@ -458,7 +474,19 @@ export class Ark { public async deleteContact({ pubkey }: { pubkey: string }) { const user = this.#ndk.getUser({ pubkey: this.account.pubkey }); const contacts = await user.follows(); - return await user.follow(new NDKUser({ pubkey: pubkey }), contacts); + contacts.delete(new NDKUser({ pubkey: pubkey })); + + const event = new NDKEvent(this.#ndk); + event.content = ''; + event.kind = NDKKind.Contacts; + event.tags = [...contacts].map((item) => [ + 'p', + item.pubkey, + item.relayUrls?.[0] || '', + '', + ]); + + return await event.publish(); } public async getAllEvents({ filter }: { filter: NDKFilter }) { @@ -476,6 +504,15 @@ export class Ark { return event; } + public async getEventByFilter({ filter }: { filter: NDKFilter }) { + const event = await this.#ndk.fetchEvent(filter, { + cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST, + }); + + if (!event) return null; + return event; + } + public getEventThread({ tags }: { tags: NDKTag[] }) { let rootEventId: string = null; let replyEventId: string = null; @@ -551,6 +588,32 @@ export class Ark { return events; } + public async getAllRelaysFromContacts() { + const LIMIT = 1; + const relayMap = new Map(); + const relayEvents = this.#fetcher.fetchLatestEventsPerAuthor( + { + authors: this.account.contacts, + relayUrls: this.relays, + }, + { kinds: [NDKKind.RelayList] }, + LIMIT + ); + + for await (const { author, events } of relayEvents) { + if (events[0]) { + events[0].tags.forEach((tag) => { + const users = relayMap.get(tag[1]); + + if (!users) return relayMap.set(tag[1], [author]); + return users.push(author); + }); + } + } + + return relayMap; + } + public async getInfiniteEvents({ filter, limit, diff --git a/src/shared/avatarUploader.tsx b/src/shared/avatarUploader.tsx index 2f4e15d8..fd920e76 100644 --- a/src/shared/avatarUploader.tsx +++ b/src/shared/avatarUploader.tsx @@ -1,16 +1,16 @@ import { message } from '@tauri-apps/plugin-dialog'; import { Dispatch, SetStateAction, useState } from 'react'; -import { LoaderIcon } from '@shared/icons'; +import { useArk } from '@libs/ark'; -import { useNostr } from '@utils/hooks/useNostr'; +import { LoaderIcon } from '@shared/icons'; export function AvatarUploader({ setPicture, }: { setPicture: Dispatch>; }) { - const { upload } = useNostr(); + const { ark } = useArk(); const [loading, setLoading] = useState(false); const uploadAvatar = async () => { @@ -18,7 +18,7 @@ export function AvatarUploader({ // start loading setLoading(true); - const image = await upload(); + const image = await ark.upload({}); if (image) { setPicture(image); diff --git a/src/shared/bannerUploader.tsx b/src/shared/bannerUploader.tsx index b3e308be..17282caf 100644 --- a/src/shared/bannerUploader.tsx +++ b/src/shared/bannerUploader.tsx @@ -1,16 +1,16 @@ import { message } from '@tauri-apps/plugin-dialog'; import { Dispatch, SetStateAction, useState } from 'react'; -import { LoaderIcon, PlusIcon } from '@shared/icons'; +import { useArk } from '@libs/ark'; -import { useNostr } from '@utils/hooks/useNostr'; +import { LoaderIcon, PlusIcon } from '@shared/icons'; export function BannerUploader({ setBanner, }: { setBanner: Dispatch>; }) { - const { upload } = useNostr(); + const { ark } = useArk(); const [loading, setLoading] = useState(false); const uploadBanner = async () => { @@ -18,7 +18,7 @@ export function BannerUploader({ // start loading setLoading(true); - const image = await upload(); + const image = await ark.upload({}); if (image) { setBanner(image); diff --git a/src/shared/notes/notify.tsx b/src/shared/notes/notify.tsx index 606b2599..5fb9e403 100644 --- a/src/shared/notes/notify.tsx +++ b/src/shared/notes/notify.tsx @@ -1,20 +1,21 @@ 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/createdAt'; -import { useNostr } from '@utils/hooks/useNostr'; import { useWidget } from '@utils/hooks/useWidget'; export function NotifyNote({ event }: { event: NDKEvent }) { - const { getEventThread } = useNostr(); + const { ark } = useArk(); const { addWidget } = useWidget(); - const thread = getEventThread(event.tags); + const thread = ark.getEventThread({ tags: event.tags }); const createdAt = formatCreatedAt(event.created_at, false); if (event.kind === NDKKind.Reaction) { diff --git a/src/shared/notes/replies/list.tsx b/src/shared/notes/replies/list.tsx index ec16edd8..2b6dfbcd 100644 --- a/src/shared/notes/replies/list.tsx +++ b/src/shared/notes/replies/list.tsx @@ -1,38 +1,42 @@ +import { NDKSubscription } from '@nostr-dev-kit/ndk'; import { useEffect, useState } from 'react'; +import { useArk } from '@libs/ark'; + import { LoaderIcon } from '@shared/icons'; import { Reply } from '@shared/notes'; -import { useNostr } from '@utils/hooks/useNostr'; import { NDKEventWithReplies } from '@utils/types'; export function ReplyList({ eventId }: { eventId: string }) { - const { fetchAllReplies, sub } = useNostr(); + const { ark } = useArk(); const [data, setData] = useState(null); useEffect(() => { + let sub: NDKSubscription; let isCancelled = false; async function fetchRepliesAndSub() { - const events = await fetchAllReplies(eventId); + const events = await ark.getThreads({ id: eventId }); if (!isCancelled) { setData(events); } // subscribe for new replies - sub( - { + sub = ark.subscribe({ + filter: { '#e': [eventId], since: Math.floor(Date.now() / 1000), }, - (event: NDKEventWithReplies) => setData((prev) => [event, ...prev]), - false - ); + closeOnEose: false, + cb: (event: NDKEventWithReplies) => setData((prev) => [event, ...prev]), + }); } fetchRepliesAndSub(); return () => { isCancelled = true; + if (sub) sub.stop(); }; }, [eventId]); @@ -59,7 +63,7 @@ export function ReplyList({ eventId }: { eventId: string }) {
) : ( - data.map((event) => ) + data.map((event) => ) )}
); diff --git a/src/shared/notes/text.tsx b/src/shared/notes/text.tsx index ddc2e473..d9b318fd 100644 --- a/src/shared/notes/text.tsx +++ b/src/shared/notes/text.tsx @@ -2,20 +2,21 @@ import { NDKEvent } from '@nostr-dev-kit/ndk'; import { memo } from 'react'; import { twMerge } from 'tailwind-merge'; +import { useArk } from '@libs/ark'; + import { ChildNote, NoteActions } from '@shared/notes'; import { User } from '@shared/user'; import { WIDGET_KIND } from '@utils/constants'; -import { useNostr } from '@utils/hooks/useNostr'; import { useRichContent } from '@utils/hooks/useRichContent'; import { useWidget } from '@utils/hooks/useWidget'; export function TextNote({ event, className }: { event: NDKEvent; className?: string }) { const { parsedContent } = useRichContent(event.content); const { addWidget } = useWidget(); - const { getEventThread } = useNostr(); + const { ark } = useArk(); - const thread = getEventThread(event.tags); + const thread = ark.getEventThread({ tags: event.tags }); return (
diff --git a/src/shared/titleBar.tsx b/src/shared/titleBar.tsx index bc76f916..46dfa48b 100644 --- a/src/shared/titleBar.tsx +++ b/src/shared/titleBar.tsx @@ -1,4 +1,4 @@ -import { useStorage } from '@libs/storage/provider'; +import { useArk } from '@libs/ark'; import { CancelIcon } from '@shared/icons'; import { User } from '@shared/user'; @@ -14,7 +14,7 @@ export function TitleBar({ title?: string; isLive?: boolean; }) { - const { db } = useStorage(); + const { ark } = useArk(); const { removeWidget } = useWidget(); return ( @@ -33,13 +33,13 @@ export function TitleBar({
{id === '9999' ? (
- {db.account.contacts + {ark.account.contacts ?.slice(0, 8) .map((item) => )} - {db.account.contacts?.length > 8 ? ( + {ark.account.contacts?.length > 8 ? (
- +{db.account.contacts?.length - 8} + +{ark.account.contacts?.length - 8}
) : null} diff --git a/src/shared/widgets/notification.tsx b/src/shared/widgets/notification.tsx index e56934d9..14ef5d6e 100644 --- a/src/shared/widgets/notification.tsx +++ b/src/shared/widgets/notification.tsx @@ -3,6 +3,7 @@ import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; import { useCallback, useEffect, useMemo } from 'react'; import { VList } from 'virtua'; +import { useArk } from '@libs/ark'; import { useNDK } from '@libs/ndk/provider'; import { useStorage } from '@libs/storage/provider'; @@ -12,15 +13,12 @@ import { TitleBar } from '@shared/titleBar'; import { WidgetWrapper } from '@shared/widgets'; import { FETCH_LIMIT } from '@utils/constants'; -import { useNostr } from '@utils/hooks/useNostr'; import { sendNativeNotification } from '@utils/notification'; export function NotificationWidget() { const queryClient = useQueryClient(); - const { db } = useStorage(); - const { sub } = useNostr(); - const { ndk, relayUrls, fetcher } = useNDK(); + const { ark } = useArk(); const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({ queryKey: ['notification'], diff --git a/src/shared/widgets/thread.tsx b/src/shared/widgets/thread.tsx index dcb70fe2..3f846655 100644 --- a/src/shared/widgets/thread.tsx +++ b/src/shared/widgets/thread.tsx @@ -2,6 +2,8 @@ import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { useCallback } from 'react'; import { WVList } from 'virtua'; +import { useArk } from '@libs/ark'; + import { LoaderIcon } from '@shared/icons'; import { ChildNote, @@ -17,16 +19,15 @@ import { User } from '@shared/user'; import { WidgetWrapper } from '@shared/widgets'; import { useEvent } from '@utils/hooks/useEvent'; -import { useNostr } from '@utils/hooks/useNostr'; import { Widget } from '@utils/types'; export function ThreadWidget({ widget }: { widget: Widget }) { const { isFetching, isError, data } = useEvent(widget.content); - const { getEventThread } = useNostr(); + const { ark } = useArk(); const renderKind = useCallback( (event: NDKEvent) => { - const thread = getEventThread(event.tags); + const thread = ark.getEventThread({ tags: event.tags }); switch (event.kind) { case NDKKind.Text: return ( diff --git a/src/utils/hooks/useNostr.ts b/src/utils/hooks/useNostr.ts deleted file mode 100644 index 678f1692..00000000 --- a/src/utils/hooks/useNostr.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { - NDKEvent, - NDKFilter, - NDKKind, - NDKSubscription, - NDKTag, -} from '@nostr-dev-kit/ndk'; -import { open } from '@tauri-apps/plugin-dialog'; -import { readBinaryFile } from '@tauri-apps/plugin-fs'; -import { fetch } from '@tauri-apps/plugin-http'; -import { LRUCache } from 'lru-cache'; -import { NostrEventExt } from 'nostr-fetch'; -import { useMemo } from 'react'; - -import { useNDK } from '@libs/ndk/provider'; -import { useStorage } from '@libs/storage/provider'; - -import { nHoursAgo } from '@utils/date'; -import { getMultipleRandom } from '@utils/transform'; -import { NDKEventWithReplies } from '@utils/types'; - -export function useNostr() { - const { db } = useStorage(); - const { ndk, relayUrls, fetcher } = useNDK(); - - const subManager = useMemo( - () => - new LRUCache({ - max: 4, - dispose: (sub) => sub.stop(), - }), - [] - ); - - const sub = async ( - filter: NDKFilter, - callback: (event: NDKEvent) => void, - groupable?: boolean, - subKey?: string - ) => { - if (!ndk) throw new Error('NDK instance not found'); - - const key = subKey ?? JSON.stringify(filter); - if (!subManager.get(key)) { - const subEvent = ndk.subscribe(filter, { - closeOnEose: false, - groupable: groupable ?? true, - }); - - subEvent.addListener('event', (event: NDKEvent) => { - callback(event); - }); - - subManager.set(JSON.stringify(filter), subEvent); - console.log('sub: ', key); - } - }; - - const getEventThread = (tags: NDKTag[]) => { - let rootEventId: string = null; - let replyEventId: string = null; - - const events = tags.filter((el) => el[0] === 'e'); - - if (!events.length) return null; - - if (events.length === 1) - return { - rootEventId: events[0][1], - replyEventId: null, - }; - - if (events.length > 1) { - rootEventId = events.find((el) => el[3] === 'root')?.[1]; - replyEventId = events.find((el) => el[3] === 'reply')?.[1]; - - if (!rootEventId && !replyEventId) { - rootEventId = events[0][1]; - replyEventId = events[1][1]; - } - } - - return { - rootEventId, - replyEventId, - }; - }; - - const getAllActivities = async (limit?: number) => { - try { - const events = await ndk.fetchEvents({ - kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap], - '#p': [db.account.pubkey], - limit: limit ?? 50, - }); - - return [...events]; - } catch (e) { - console.error('Error fetching activities', e); - } - }; - - const fetchNIP04Messages = async (sender: string) => { - let senderMessages: NostrEventExt[] = []; - - if (sender !== db.account.pubkey) { - senderMessages = await fetcher.fetchAllEvents( - relayUrls, - { - kinds: [NDKKind.EncryptedDirectMessage], - authors: [sender], - '#p': [db.account.pubkey], - }, - { since: 0 } - ); - } - - const userMessages = await fetcher.fetchAllEvents( - relayUrls, - { - kinds: [NDKKind.EncryptedDirectMessage], - authors: [db.account.pubkey], - '#p': [sender], - }, - { since: 0 } - ); - - const all = [...senderMessages, ...userMessages].sort( - (a, b) => a.created_at - b.created_at - ); - - return all as unknown as NDKEvent[]; - }; - - const fetchAllReplies = async (id: string, data?: NDKEventWithReplies[]) => { - let events = data || null; - - if (!data) { - events = (await fetcher.fetchAllEvents( - relayUrls, - { - kinds: [NDKKind.Text], - '#e': [id], - }, - { since: 0 }, - { sort: true } - )) as unknown as NDKEventWithReplies[]; - } - - if (events.length > 0) { - const replies = new Set(); - events.forEach((event) => { - const tags = event.tags.filter((el) => el[0] === 'e' && el[1] !== id); - if (tags.length > 0) { - tags.forEach((tag) => { - const rootIndex = events.findIndex((el) => el.id === tag[1]); - if (rootIndex !== -1) { - const rootEvent = events[rootIndex]; - if (rootEvent && rootEvent.replies) { - rootEvent.replies.push(event); - } else { - rootEvent.replies = [event]; - } - replies.add(event.id); - } - }); - } - }); - const cleanEvents = events.filter((ev) => !replies.has(ev.id)); - return cleanEvents; - } - - return events; - }; - - const getAllNIP04Chats = async () => { - const events = await fetcher.fetchAllEvents( - relayUrls, - { - kinds: [NDKKind.EncryptedDirectMessage], - '#p': [db.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; - }; - - const getContactsByPubkey = async (pubkey: string) => { - const user = ndk.getUser({ pubkey: pubkey }); - const follows = [...(await user.follows())].map((user) => user.hexpubkey); - return getMultipleRandom([...follows], 10); - }; - - const getEventsByPubkey = async (pubkey: string) => { - const events = await fetcher.fetchAllEvents( - relayUrls, - { authors: [pubkey], kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Article] }, - { since: nHoursAgo(24) }, - { sort: true } - ); - return events as unknown as NDKEvent[]; - }; - - const getAllRelaysByUsers = async () => { - const relayMap = new Map(); - const relayEvents = fetcher.fetchLatestEventsPerAuthor( - { - authors: db.account.contacts, - relayUrls: relayUrls, - }, - { kinds: [NDKKind.RelayList] }, - 5 - ); - - for await (const { author, events } of relayEvents) { - if (events[0]) { - events[0].tags.forEach((tag) => { - const users = relayMap.get(tag[1]); - - if (!users) return relayMap.set(tag[1], [author]); - return users.push(author); - }); - } - } - - return relayMap; - }; - - const createZap = async (event: NDKEvent, amount: number, message?: string) => { - // @ts-expect-error, NostrEvent to NDKEvent - const ndkEvent = new NDKEvent(ndk, event); - const res = await ndkEvent.zap(amount, message ?? 'zap from lume'); - - return res; - }; - - const upload = async (ext: string[] = []) => { - const defaultExts = ['png', 'jpeg', 'jpg', 'gif'].concat(ext); - - const selected = await open({ - multiple: false, - filters: [ - { - name: 'Image', - extensions: defaultExts, - }, - ], - }); - - if (!selected) return null; - - const file = await readBinaryFile(selected.path); - const blob = new Blob([file]); - - const data = new FormData(); - data.append('fileToUpload', blob); - data.append('submit', 'Upload Image'); - - const res = await fetch('https://nostr.build/api/v2/upload/files', { - method: 'POST', - body: data, - }); - - if (!res.ok) return null; - - const json = await res.json(); - const content = json.data[0]; - - return content.url as string; - }; - - return { - sub, - getEventThread, - getAllNIP04Chats, - getContactsByPubkey, - getEventsByPubkey, - getAllRelaysByUsers, - getAllActivities, - fetchNIP04Messages, - fetchAllReplies, - createZap, - upload, - }; -}