wip: migrate to ark

This commit is contained in:
2023-12-07 18:09:00 +07:00
parent 95124e5ded
commit 7507cd9ba1
29 changed files with 206 additions and 454 deletions

View File

@@ -7,7 +7,7 @@ import { ChatsScreen } from '@app/chats';
import { ErrorScreen } from '@app/error'; import { ErrorScreen } from '@app/error';
import { ExploreScreen } from '@app/explore'; import { ExploreScreen } from '@app/explore';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { AppLayout } from '@shared/layouts/app'; import { AppLayout } from '@shared/layouts/app';
@@ -19,12 +19,12 @@ import { SettingsLayout } from '@shared/layouts/settings';
import './app.css'; import './app.css';
export default function App() { export default function App() {
const { db } = useStorage(); const { ark } = useArk();
const accountLoader = async () => { const accountLoader = async () => {
try { try {
// redirect to welcome screen if none user exist // 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'); if (totalAccount === 0) return redirect('/auth/welcome');
return null; return null;

View File

@@ -3,12 +3,12 @@ import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notif
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { InfoIcon } from '@shared/icons'; import { InfoIcon } from '@shared/icons';
export function OnboardingScreen() { export function OnboardingScreen() {
const { db } = useStorage(); const { ark } = useArk();
const navigate = useNavigate(); const navigate = useNavigate();
const [settings, setSettings] = useState({ const [settings, setSettings] = useState({
@@ -18,19 +18,19 @@ export function OnboardingScreen() {
}); });
const next = () => { const next = () => {
if (!db.account.contacts.length) return navigate('/auth/follow'); if (!ark.account.contacts.length) return navigate('/auth/follow');
return navigate('/auth/finish'); return navigate('/auth/finish');
}; };
const toggleOutbox = async () => { const toggleOutbox = async () => {
await db.createSetting('outbox', String(+!settings.outbox)); await ark.createSetting('outbox', String(+!settings.outbox));
// update state // update state
setSettings((prev) => ({ ...prev, outbox: !settings.outbox })); setSettings((prev) => ({ ...prev, outbox: !settings.outbox }));
}; };
const toggleAutoupdate = async () => { const toggleAutoupdate = async () => {
await db.createSetting('autoupdate', String(+!settings.autoupdate)); await ark.createSetting('autoupdate', String(+!settings.autoupdate));
db.settings.autoupdate = !settings.autoupdate; ark.settings.autoupdate = !settings.autoupdate;
// update state // update state
setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate })); setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate }));
}; };
@@ -46,7 +46,7 @@ export function OnboardingScreen() {
const permissionGranted = await isPermissionGranted(); const permissionGranted = await isPermissionGranted();
setSettings((prev) => ({ ...prev, notification: permissionGranted })); setSettings((prev) => ({ ...prev, notification: permissionGranted }));
const data = await db.getAllSettings(); const data = await ark.getAllSettings();
if (!data) return; if (!data) return;
data.forEach((item) => { data.forEach((item) => {

View File

@@ -1,22 +1,24 @@
import * as Tooltip from '@radix-ui/react-tooltip'; import * as Tooltip from '@radix-ui/react-tooltip';
import { Dispatch, SetStateAction, useState } from 'react'; 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({ export function MediaUploader({
setState, setState,
}: { }: {
setState: Dispatch<SetStateAction<string>>; setState: Dispatch<SetStateAction<string>>;
}) { }) {
const { upload } = useNostr(); const { ark } = useArk();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const uploadMedia = async () => { const uploadMedia = async () => {
setLoading(true); 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) { if (image) {
setState((prev: string) => `${prev}\n${image}`); setState((prev: string) => `${prev}\n${image}`);

View File

@@ -4,7 +4,7 @@ import { writeTextFile } from '@tauri-apps/plugin-fs';
import { relaunch } from '@tauri-apps/plugin-process'; import { relaunch } from '@tauri-apps/plugin-process';
import { useRouteError } from 'react-router-dom'; import { useRouteError } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
interface RouteError { interface RouteError {
statusText: string; statusText: string;
@@ -12,7 +12,7 @@ interface RouteError {
} }
export function ErrorScreen() { export function ErrorScreen() {
const { db } = useStorage(); const { ark } = useArk();
const error = useRouteError() as RouteError; const error = useRouteError() as RouteError;
const restart = async () => { const restart = async () => {
@@ -26,18 +26,18 @@ export function ErrorScreen() {
const filePath = await save({ const filePath = await save({
defaultPath: downloadPath + '/' + fileName, defaultPath: downloadPath + '/' + fileName,
}); });
const nsec = await db.secureLoad(db.account.pubkey); const nsec = await ark.loadPrivkey(ark.account.pubkey);
if (filePath) { if (filePath) {
if (nsec) { if (nsec) {
await writeTextFile( await writeTextFile(
filePath, 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 { } else {
await writeTextFile( await writeTextFile(
filePath, 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 } } // else { user cancel action }

View File

@@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query';
import { useCallback, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import { VList, VListHandle } from 'virtua'; import { VList, VListHandle } from 'virtua';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { import {
@@ -28,11 +28,11 @@ export function HomeScreen() {
const ref = useRef<VListHandle>(null); const ref = useRef<VListHandle>(null);
const [selectedIndex, setSelectedIndex] = useState(-1); const [selectedIndex, setSelectedIndex] = useState(-1);
const { db } = useStorage(); const { ark } = useArk();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['widgets'], queryKey: ['widgets'],
queryFn: async () => { queryFn: async () => {
const dbWidgets = await db.getWidgets(); const dbWidgets = await ark.getWidgets();
const defaultWidgets = [ const defaultWidgets = [
{ {
id: '9999', id: '9999',

View File

@@ -2,12 +2,12 @@ import { message } from '@tauri-apps/plugin-dialog';
import { Editor } from '@tiptap/react'; import { Editor } from '@tiptap/react';
import { useState } from 'react'; import { useState } from 'react';
import { useArk } from '@libs/ark';
import { MediaIcon } from '@shared/icons'; import { MediaIcon } from '@shared/icons';
import { useNostr } from '@utils/hooks/useNostr';
export function MediaUploader({ editor }: { editor: Editor }) { export function MediaUploader({ editor }: { editor: Editor }) {
const { upload } = useNostr(); const { ark } = useArk();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const uploadToNostrBuild = async () => { const uploadToNostrBuild = async () => {
@@ -15,7 +15,9 @@ export function MediaUploader({ editor }: { editor: Editor }) {
// start loading // start loading
setLoading(true); 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) { if (image) {
editor.commands.setImage({ src: image }); editor.commands.setImage({ src: image });

View File

@@ -4,12 +4,12 @@ import { nip19 } from 'nostr-tools';
import { MentionPopupItem } from '@app/new/components'; import { MentionPopupItem } from '@app/new/components';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { MentionIcon } from '@shared/icons'; import { MentionIcon } from '@shared/icons';
export function MentionPopup({ editor }: { editor: Editor }) { export function MentionPopup({ editor }: { editor: Editor }) {
const { db } = useStorage(); const { ark } = useArk();
const insertMention = (pubkey: string) => { const insertMention = (pubkey: string) => {
editor.commands.insertContent(`nostr:${nip19.npubEncode(pubkey)}`); 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" 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"
> >
<div className="flex flex-col gap-1 py-1"> <div className="flex flex-col gap-1 py-1">
{db.account.contacts.length > 0 ? ( {ark.account.contacts.length ? (
db.account.contacts.map((item) => ( ark.account.contacts.map((item) => (
<button key={item} type="button" onClick={() => insertMention(item)}> <button key={item} type="button" onClick={() => insertMention(item)}>
<MentionPopupItem pubkey={item} /> <MentionPopupItem pubkey={item} />
</button> </button>

View File

@@ -6,6 +6,8 @@ import { useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useArk } from '@libs/ark';
import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons'; import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons';
import { import {
ChildNote, ChildNote,
@@ -18,15 +20,14 @@ import { ReplyList } from '@shared/notes/replies/list';
import { User } from '@shared/user'; import { User } from '@shared/user';
import { useEvent } from '@utils/hooks/useEvent'; import { useEvent } from '@utils/hooks/useEvent';
import { useNostr } from '@utils/hooks/useNostr';
export function TextNoteScreen() { export function TextNoteScreen() {
const navigate = useNavigate(); const navigate = useNavigate();
const replyRef = useRef(null); const replyRef = useRef(null);
const { id } = useParams(); const { id } = useParams();
const { ark } = useArk();
const { status, data } = useEvent(id); const { status, data } = useEvent(id);
const { getEventThread } = useNostr();
const [isCopy, setIsCopy] = useState(false); const [isCopy, setIsCopy] = useState(false);
@@ -50,7 +51,7 @@ export function TextNoteScreen() {
}; };
const renderKind = (event: NDKEvent) => { const renderKind = (event: NDKEvent) => {
const thread = getEventThread(event.tags); const thread = ark.getEventThread({ tags: event.tags });
switch (event.kind) { switch (event.kind) {
case NDKKind.Text: case NDKKind.Text:
return ( return (

View File

@@ -1,12 +1,12 @@
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
export function NWCForm({ setWalletConnectURL }) { export function NWCForm({ setWalletConnectURL }) {
const { db } = useStorage(); const { ark } = useArk();
const [uri, setUri] = useState(''); const [uri, setUri] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -27,7 +27,7 @@ export function NWCForm({ setWalletConnectURL }) {
const params = new URLSearchParams(uriObj.search); const params = new URLSearchParams(uriObj.search);
if (params.has('relay') && params.has('secret')) { 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); setWalletConnectURL(uri);
setLoading(false); setLoading(false);
} else { } else {

View File

@@ -2,22 +2,22 @@ import { useEffect, useState } from 'react';
import { NWCForm } from '@app/nwc/components/form'; import { NWCForm } from '@app/nwc/components/form';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { CheckCircleIcon } from '@shared/icons'; import { CheckCircleIcon } from '@shared/icons';
export function NWCScreen() { export function NWCScreen() {
const { db } = useStorage(); const { ark } = useArk();
const [walletConnectURL, setWalletConnectURL] = useState<null | string>(null); const [walletConnectURL, setWalletConnectURL] = useState<null | string>(null);
const remove = async () => { const remove = async () => {
await db.secureRemove(`${db.account.pubkey}-nwc`); await ark.removePrivkey(`${ark.account.pubkey}-nwc`);
setWalletConnectURL(null); setWalletConnectURL(null);
}; };
useEffect(() => { useEffect(() => {
async function getNWC() { 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); if (nwc) setWalletConnectURL(nwc);
} }
getNWC(); getNWC();

View File

@@ -2,19 +2,20 @@ import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { useArk } from '@libs/ark';
import { LoaderIcon, PlusIcon, ShareIcon } from '@shared/icons'; import { LoaderIcon, PlusIcon, ShareIcon } from '@shared/icons';
import { User } from '@shared/user'; import { User } from '@shared/user';
import { useNostr } from '@utils/hooks/useNostr';
import { useRelay } from '@utils/hooks/useRelay'; import { useRelay } from '@utils/hooks/useRelay';
export function RelayList() { export function RelayList() {
const { getAllRelaysByUsers } = useNostr(); const { ark } = useArk();
const { connectRelay } = useRelay(); const { connectRelay } = useRelay();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['relays'], queryKey: ['relays'],
queryFn: async () => { queryFn: async () => {
return await getAllRelaysByUsers(); return await ark.getAllRelaysFromContacts();
}, },
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnMount: false, refetchOnMount: false,

View File

@@ -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 { useQuery } from '@tanstack/react-query';
import { RelayForm } from '@app/relays/components/relayForm'; import { RelayForm } from '@app/relays/components/relayForm';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { CancelIcon, RefreshIcon } from '@shared/icons'; import { CancelIcon, RefreshIcon } from '@shared/icons';
import { useRelay } from '@utils/hooks/useRelay'; import { useRelay } from '@utils/hooks/useRelay';
export function UserRelayList() { export function UserRelayList() {
const { db } = useStorage(); const { ark } = useArk();
const { ndk } = useNDK();
const { removeRelay } = useRelay(); const { removeRelay } = useRelay();
const { status, data, refetch } = useQuery({ const { status, data, refetch } = useQuery({
queryKey: ['relays', db.account.pubkey], queryKey: ['relays', ark.account.pubkey],
queryFn: async () => { queryFn: async () => {
const event = await ndk.fetchEvent( const event = await ark.getEventByFilter({
{ filter: {
kinds: [NDKKind.RelayList], kinds: [NDKKind.RelayList],
authors: [db.account.pubkey], authors: [ark.account.pubkey],
}, },
{ cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY } });
);
if (!event) return []; if (!event) return [];
return event.tags; return event.tags;
@@ -31,7 +28,7 @@ export function UserRelayList() {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
const currentRelays = new Set([...ndk.pool.relays.values()].map((item) => item.url)); const currentRelays = new Set([...ark.relays]);
return ( return (
<div className="col-span-1"> <div className="col-span-1">

View File

@@ -1,10 +1,10 @@
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
export function AdvancedSettingScreen() { export function AdvancedSettingScreen() {
const { db } = useStorage(); const { ark } = useArk();
const clearCache = async () => { const clearCache = async () => {
await db.clearCache(); await ark.clearCache();
}; };
return ( return (

View File

@@ -1,23 +1,23 @@
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { EyeOffIcon } from '@shared/icons'; import { EyeOffIcon } from '@shared/icons';
export function BackupSettingScreen() { export function BackupSettingScreen() {
const { db } = useStorage(); const { ark } = useArk();
const [privkey, setPrivkey] = useState(null); const [privkey, setPrivkey] = useState(null);
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const removePrivkey = async () => { const removePrivkey = async () => {
await db.secureRemove(db.account.pubkey); await ark.removePrivkey(ark.account.pubkey);
}; };
useEffect(() => { useEffect(() => {
async function loadPrivkey() { async function loadPrivkey() {
const key = await db.secureLoad(db.account.pubkey); const key = await ark.loadPrivkey(ark.account.pubkey);
if (key) setPrivkey(key); if (key) setPrivkey(key);
} }

View File

@@ -2,19 +2,19 @@ import { useQuery } from '@tanstack/react-query';
import { fetch } from '@tauri-apps/plugin-http'; import { fetch } from '@tauri-apps/plugin-http';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { compactNumber } from '@utils/number'; import { compactNumber } from '@utils/number';
export function PostCard() { export function PostCard() {
const { db } = useStorage(); const { ark } = useArk();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['user-stats', db.account.pubkey], queryKey: ['user-stats', ark.account.pubkey],
queryFn: async ({ signal }: { signal: AbortSignal }) => { queryFn: async ({ signal }: { signal: AbortSignal }) => {
const res = await fetch( 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, signal,
} }
@@ -41,14 +41,14 @@ export function PostCard() {
) : ( ) : (
<div className="flex h-full w-full flex-col justify-between p-4"> <div className="flex h-full w-full flex-col justify-between p-4">
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100"> <h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
{compactNumber.format(data.stats[db.account.pubkey].pub_note_count)} {compactNumber.format(data.stats[ark.account.pubkey].pub_note_count)}
</h3> </h3>
<div className="mt-auto flex h-6 w-full items-center justify-between"> <div className="mt-auto flex h-6 w-full items-center justify-between">
<p className="text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400"> <p className="text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">
Posts Posts
</p> </p>
<Link <Link
to={`/users/${db.account.pubkey}`} to={`/users/${ark.account.pubkey}`}
className="inline-flex h-6 w-max items-center gap-1 rounded-full bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700" className="inline-flex h-6 w-max items-center gap-1 rounded-full bg-neutral-200 px-2.5 text-sm font-medium hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
> >
View View

View File

@@ -2,7 +2,7 @@ import * as Avatar from '@radix-ui/react-avatar';
import { minidenticon } from 'minidenticons'; import { minidenticon } from 'minidenticons';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { EditIcon, LoaderIcon } from '@shared/icons'; import { EditIcon, LoaderIcon } from '@shared/icons';
@@ -10,12 +10,12 @@ import { useProfile } from '@utils/hooks/useProfile';
import { displayNpub } from '@utils/shortenKey'; import { displayNpub } from '@utils/shortenKey';
export function ProfileCard() { export function ProfileCard() {
const { db } = useStorage(); const { ark } = useArk();
const { isLoading, user } = useProfile(db.account.pubkey); const { isLoading, user } = useProfile(ark.account.pubkey);
const svgURI = const svgURI =
'data:image/svg+xml;utf8,' + 'data:image/svg+xml;utf8,' +
encodeURIComponent(minidenticon(db.account.pubkey, 90, 50)); encodeURIComponent(minidenticon(ark.account.pubkey, 90, 50));
return ( return (
<div className="mb-4 h-56 w-full rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900"> <div className="mb-4 h-56 w-full rounded-2xl bg-neutral-100 transition-all duration-150 ease-smooth hover:scale-105 dark:bg-neutral-900">
@@ -38,7 +38,7 @@ export function ProfileCard() {
<Avatar.Root className="shrink-0"> <Avatar.Root className="shrink-0">
<Avatar.Image <Avatar.Image
src={user?.picture || user?.image} src={user?.picture || user?.image}
alt={db.account.pubkey} alt={ark.account.pubkey}
loading="lazy" loading="lazy"
decoding="async" decoding="async"
style={{ contentVisibility: 'auto' }} style={{ contentVisibility: 'auto' }}
@@ -47,7 +47,7 @@ export function ProfileCard() {
<Avatar.Fallback delayMs={300}> <Avatar.Fallback delayMs={300}>
<img <img
src={svgURI} src={svgURI}
alt={db.account.pubkey} alt={ark.account.pubkey}
className="h-16 w-16 rounded-xl bg-black dark:bg-white" className="h-16 w-16 rounded-xl bg-black dark:bg-white"
/> />
</Avatar.Fallback> </Avatar.Fallback>
@@ -57,7 +57,7 @@ export function ProfileCard() {
{user?.display_name || user?.name} {user?.display_name || user?.name}
</h3> </h3>
<p className="text-lg text-neutral-700 dark:text-neutral-300"> <p className="text-lg text-neutral-700 dark:text-neutral-300">
{user?.nip05 || displayNpub(db.account.pubkey, 16)} {user?.nip05 || displayNpub(ark.account.pubkey, 16)}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -1,19 +1,19 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { fetch } from '@tauri-apps/plugin-http'; import { fetch } from '@tauri-apps/plugin-http';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { compactNumber } from '@utils/number'; import { compactNumber } from '@utils/number';
export function ZapCard() { export function ZapCard() {
const { db } = useStorage(); const { ark } = useArk();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['user-stats', db.account.pubkey], queryKey: ['user-stats', ark.account.pubkey],
queryFn: async ({ signal }: { signal: AbortSignal }) => { queryFn: async ({ signal }: { signal: AbortSignal }) => {
const res = await fetch( 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, signal,
} }
@@ -41,7 +41,7 @@ export function ZapCard() {
<div className="flex h-full w-full flex-col justify-between p-4"> <div className="flex h-full w-full flex-col justify-between p-4">
<h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100"> <h3 className="pt-1 text-5xl font-semibold tabular-nums text-neutral-900 dark:text-neutral-100">
{compactNumber.format( {compactNumber.format(
data?.stats[db.account.pubkey]?.zaps_received?.msats / 1000 || 0 data?.stats[ark.account.pubkey]?.zaps_received?.msats / 1000 || 0
)} )}
</h3> </h3>
<div className="mt-auto flex h-6 items-center text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400"> <div className="mt-auto flex h-6 items-center text-xl font-medium leading-none text-neutral-600 dark:text-neutral-400">

View File

@@ -6,12 +6,12 @@ import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notif
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { DarkIcon, LightIcon, SystemModeIcon } from '@shared/icons'; import { DarkIcon, LightIcon, SystemModeIcon } from '@shared/icons';
export function GeneralSettingScreen() { export function GeneralSettingScreen() {
const { db } = useStorage(); const { ark } = useArk();
const [settings, setSettings] = useState({ const [settings, setSettings] = useState({
autoupdate: false, autoupdate: false,
autolaunch: false, autolaunch: false,
@@ -41,28 +41,28 @@ export function GeneralSettingScreen() {
}; };
const toggleOutbox = async () => { const toggleOutbox = async () => {
await db.createSetting('outbox', String(+!settings.outbox)); await ark.createSetting('outbox', String(+!settings.outbox));
// update state // update state
setSettings((prev) => ({ ...prev, outbox: !settings.outbox })); setSettings((prev) => ({ ...prev, outbox: !settings.outbox }));
}; };
const toggleMedia = async () => { const toggleMedia = async () => {
await db.createSetting('media', String(+!settings.media)); await ark.createSetting('media', String(+!settings.media));
db.settings.media = !settings.media; ark.settings.media = !settings.media;
// update state // update state
setSettings((prev) => ({ ...prev, media: !settings.media })); setSettings((prev) => ({ ...prev, media: !settings.media }));
}; };
const toggleHashtag = async () => { const toggleHashtag = async () => {
await db.createSetting('hashtag', String(+!settings.hashtag)); await ark.createSetting('hashtag', String(+!settings.hashtag));
db.settings.hashtag = !settings.hashtag; ark.settings.hashtag = !settings.hashtag;
// update state // update state
setSettings((prev) => ({ ...prev, hashtag: !settings.hashtag })); setSettings((prev) => ({ ...prev, hashtag: !settings.hashtag }));
}; };
const toggleAutoupdate = async () => { const toggleAutoupdate = async () => {
await db.createSetting('autoupdate', String(+!settings.autoupdate)); await ark.createSetting('autoupdate', String(+!settings.autoupdate));
db.settings.autoupdate = !settings.autoupdate; ark.settings.autoupdate = !settings.autoupdate;
// update state // update state
setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate })); setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate }));
}; };
@@ -86,7 +86,7 @@ export function GeneralSettingScreen() {
const permissionGranted = await isPermissionGranted(); const permissionGranted = await isPermissionGranted();
setSettings((prev) => ({ ...prev, notification: permissionGranted })); setSettings((prev) => ({ ...prev, notification: permissionGranted }));
const data = await db.getAllSettings(); const data = await ark.getAllSettings();
if (!data) return; if (!data) return;
data.forEach((item) => { data.forEach((item) => {

View File

@@ -1,4 +1,3 @@
import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk';
import * as Avatar from '@radix-ui/react-avatar'; import * as Avatar from '@radix-ui/react-avatar';
import { minidenticon } from 'minidenticons'; import { minidenticon } from 'minidenticons';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -7,8 +6,7 @@ import { toast } from 'sonner';
import { UserStats } from '@app/users/components/stats'; import { UserStats } from '@app/users/components/stats';
import { useNDK } from '@libs/ndk/provider'; import { useArk } from '@libs/ark';
import { useStorage } from '@libs/storage/provider';
import { NIP05 } from '@shared/nip05'; import { NIP05 } from '@shared/nip05';
@@ -16,8 +14,7 @@ import { useProfile } from '@utils/hooks/useProfile';
import { displayNpub } from '@utils/shortenKey'; import { displayNpub } from '@utils/shortenKey';
export function UserProfile({ pubkey }: { pubkey: string }) { export function UserProfile({ pubkey }: { pubkey: string }) {
const { db } = useStorage(); const { ark } = useArk();
const { ndk } = useNDK();
const { user } = useProfile(pubkey); const { user } = useProfile(pubkey);
const [followed, setFollowed] = useState(false); const [followed, setFollowed] = useState(false);
@@ -28,12 +25,10 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
const follow = async () => { const follow = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey'); if (!ark.readyToSign) return navigate('/new/privkey');
setFollowed(true); setFollowed(true);
const user = ndk.getUser({ pubkey: db.account.pubkey }); const add = await ark.createContact({ pubkey });
const contacts = await user.follows();
const add = await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
if (!add) { if (!add) {
toast.success('You already follow this user'); toast.success('You already follow this user');
@@ -47,32 +42,17 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
const unfollow = async () => { const unfollow = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey'); if (!ark.readyToSign) return navigate('/new/privkey');
setFollowed(false); setFollowed(false);
const user = ndk.getUser({ pubkey: db.account.pubkey }); await ark.deleteContact({ 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();
} catch (e) { } catch (e) {
toast.error(e); toast.error(e);
} }
}; };
useEffect(() => { useEffect(() => {
if (db.account.contacts.includes(pubkey)) { if (ark.account.contacts.includes(pubkey)) {
setFollowed(true); setFollowed(true);
} }
}, []); }, []);

View File

@@ -241,10 +241,26 @@ export class Ark {
/** /**
* Save private key to OS secure storage * 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) { 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) { public async updateAccount(column: string, value: string) {
@@ -458,7 +474,19 @@ export class Ark {
public async deleteContact({ pubkey }: { pubkey: string }) { public async deleteContact({ pubkey }: { pubkey: string }) {
const user = this.#ndk.getUser({ pubkey: this.account.pubkey }); const user = this.#ndk.getUser({ pubkey: this.account.pubkey });
const contacts = await user.follows(); 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 }) { public async getAllEvents({ filter }: { filter: NDKFilter }) {
@@ -476,6 +504,15 @@ export class Ark {
return event; 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[] }) { public getEventThread({ tags }: { tags: NDKTag[] }) {
let rootEventId: string = null; let rootEventId: string = null;
let replyEventId: string = null; let replyEventId: string = null;
@@ -551,6 +588,32 @@ export class Ark {
return events; return events;
} }
public async getAllRelaysFromContacts() {
const LIMIT = 1;
const relayMap = new Map<string, string[]>();
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({ public async getInfiniteEvents({
filter, filter,
limit, limit,

View File

@@ -1,16 +1,16 @@
import { message } from '@tauri-apps/plugin-dialog'; import { message } from '@tauri-apps/plugin-dialog';
import { Dispatch, SetStateAction, useState } from 'react'; 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({ export function AvatarUploader({
setPicture, setPicture,
}: { }: {
setPicture: Dispatch<SetStateAction<string>>; setPicture: Dispatch<SetStateAction<string>>;
}) { }) {
const { upload } = useNostr(); const { ark } = useArk();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const uploadAvatar = async () => { const uploadAvatar = async () => {
@@ -18,7 +18,7 @@ export function AvatarUploader({
// start loading // start loading
setLoading(true); setLoading(true);
const image = await upload(); const image = await ark.upload({});
if (image) { if (image) {
setPicture(image); setPicture(image);

View File

@@ -1,16 +1,16 @@
import { message } from '@tauri-apps/plugin-dialog'; import { message } from '@tauri-apps/plugin-dialog';
import { Dispatch, SetStateAction, useState } from 'react'; 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({ export function BannerUploader({
setBanner, setBanner,
}: { }: {
setBanner: Dispatch<SetStateAction<string>>; setBanner: Dispatch<SetStateAction<string>>;
}) { }) {
const { upload } = useNostr(); const { ark } = useArk();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const uploadBanner = async () => { const uploadBanner = async () => {
@@ -18,7 +18,7 @@ export function BannerUploader({
// start loading // start loading
setLoading(true); setLoading(true);
const image = await upload(); const image = await ark.upload({});
if (image) { if (image) {
setBanner(image); setBanner(image);

View File

@@ -1,20 +1,21 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { memo } from 'react'; import { memo } from 'react';
import { useArk } from '@libs/ark';
import { ReplyIcon, RepostIcon } from '@shared/icons'; import { ReplyIcon, RepostIcon } from '@shared/icons';
import { ChildNote, TextKind } from '@shared/notes'; import { ChildNote, TextKind } from '@shared/notes';
import { User } from '@shared/user'; import { User } from '@shared/user';
import { WIDGET_KIND } from '@utils/constants'; import { WIDGET_KIND } from '@utils/constants';
import { formatCreatedAt } from '@utils/createdAt'; import { formatCreatedAt } from '@utils/createdAt';
import { useNostr } from '@utils/hooks/useNostr';
import { useWidget } from '@utils/hooks/useWidget'; import { useWidget } from '@utils/hooks/useWidget';
export function NotifyNote({ event }: { event: NDKEvent }) { export function NotifyNote({ event }: { event: NDKEvent }) {
const { getEventThread } = useNostr(); const { ark } = useArk();
const { addWidget } = useWidget(); const { addWidget } = useWidget();
const thread = getEventThread(event.tags); const thread = ark.getEventThread({ tags: event.tags });
const createdAt = formatCreatedAt(event.created_at, false); const createdAt = formatCreatedAt(event.created_at, false);
if (event.kind === NDKKind.Reaction) { if (event.kind === NDKKind.Reaction) {

View File

@@ -1,38 +1,42 @@
import { NDKSubscription } from '@nostr-dev-kit/ndk';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { Reply } from '@shared/notes'; import { Reply } from '@shared/notes';
import { useNostr } from '@utils/hooks/useNostr';
import { NDKEventWithReplies } from '@utils/types'; import { NDKEventWithReplies } from '@utils/types';
export function ReplyList({ eventId }: { eventId: string }) { export function ReplyList({ eventId }: { eventId: string }) {
const { fetchAllReplies, sub } = useNostr(); const { ark } = useArk();
const [data, setData] = useState<null | NDKEventWithReplies[]>(null); const [data, setData] = useState<null | NDKEventWithReplies[]>(null);
useEffect(() => { useEffect(() => {
let sub: NDKSubscription;
let isCancelled = false; let isCancelled = false;
async function fetchRepliesAndSub() { async function fetchRepliesAndSub() {
const events = await fetchAllReplies(eventId); const events = await ark.getThreads({ id: eventId });
if (!isCancelled) { if (!isCancelled) {
setData(events); setData(events);
} }
// subscribe for new replies // subscribe for new replies
sub( sub = ark.subscribe({
{ filter: {
'#e': [eventId], '#e': [eventId],
since: Math.floor(Date.now() / 1000), since: Math.floor(Date.now() / 1000),
}, },
(event: NDKEventWithReplies) => setData((prev) => [event, ...prev]), closeOnEose: false,
false cb: (event: NDKEventWithReplies) => setData((prev) => [event, ...prev]),
); });
} }
fetchRepliesAndSub(); fetchRepliesAndSub();
return () => { return () => {
isCancelled = true; isCancelled = true;
if (sub) sub.stop();
}; };
}, [eventId]); }, [eventId]);
@@ -59,7 +63,7 @@ export function ReplyList({ eventId }: { eventId: string }) {
</div> </div>
</div> </div>
) : ( ) : (
data.map((event) => <Reply key={event.id} event={event} root={eventId} />) data.map((event) => <Reply key={event.id} event={event} />)
)} )}
</div> </div>
); );

View File

@@ -2,20 +2,21 @@ import { NDKEvent } from '@nostr-dev-kit/ndk';
import { memo } from 'react'; import { memo } from 'react';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { useArk } from '@libs/ark';
import { ChildNote, NoteActions } from '@shared/notes'; import { ChildNote, NoteActions } from '@shared/notes';
import { User } from '@shared/user'; import { User } from '@shared/user';
import { WIDGET_KIND } from '@utils/constants'; import { WIDGET_KIND } from '@utils/constants';
import { useNostr } from '@utils/hooks/useNostr';
import { useRichContent } from '@utils/hooks/useRichContent'; import { useRichContent } from '@utils/hooks/useRichContent';
import { useWidget } from '@utils/hooks/useWidget'; import { useWidget } from '@utils/hooks/useWidget';
export function TextNote({ event, className }: { event: NDKEvent; className?: string }) { export function TextNote({ event, className }: { event: NDKEvent; className?: string }) {
const { parsedContent } = useRichContent(event.content); const { parsedContent } = useRichContent(event.content);
const { addWidget } = useWidget(); const { addWidget } = useWidget();
const { getEventThread } = useNostr(); const { ark } = useArk();
const thread = getEventThread(event.tags); const thread = ark.getEventThread({ tags: event.tags });
return ( return (
<div className={twMerge('mb-3 h-min w-full px-3', className)}> <div className={twMerge('mb-3 h-min w-full px-3', className)}>

View File

@@ -1,4 +1,4 @@
import { useStorage } from '@libs/storage/provider'; import { useArk } from '@libs/ark';
import { CancelIcon } from '@shared/icons'; import { CancelIcon } from '@shared/icons';
import { User } from '@shared/user'; import { User } from '@shared/user';
@@ -14,7 +14,7 @@ export function TitleBar({
title?: string; title?: string;
isLive?: boolean; isLive?: boolean;
}) { }) {
const { db } = useStorage(); const { ark } = useArk();
const { removeWidget } = useWidget(); const { removeWidget } = useWidget();
return ( return (
@@ -33,13 +33,13 @@ export function TitleBar({
<div className="col-span-1 flex justify-center"> <div className="col-span-1 flex justify-center">
{id === '9999' ? ( {id === '9999' ? (
<div className="isolate flex -space-x-2"> <div className="isolate flex -space-x-2">
{db.account.contacts {ark.account.contacts
?.slice(0, 8) ?.slice(0, 8)
.map((item) => <User key={item} pubkey={item} variant="ministacked" />)} .map((item) => <User key={item} pubkey={item} variant="ministacked" />)}
{db.account.contacts?.length > 8 ? ( {ark.account.contacts?.length > 8 ? (
<div className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-neutral-300 text-neutral-900 ring-1 ring-white dark:bg-neutral-700 dark:text-neutral-100 dark:ring-black"> <div className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-neutral-300 text-neutral-900 ring-1 ring-white dark:bg-neutral-700 dark:text-neutral-100 dark:ring-black">
<span className="text-[8px] font-medium"> <span className="text-[8px] font-medium">
+{db.account.contacts?.length - 8} +{ark.account.contacts?.length - 8}
</span> </span>
</div> </div>
) : null} ) : null}

View File

@@ -3,6 +3,7 @@ import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { useArk } from '@libs/ark';
import { useNDK } from '@libs/ndk/provider'; import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider'; import { useStorage } from '@libs/storage/provider';
@@ -12,15 +13,12 @@ import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets'; import { WidgetWrapper } from '@shared/widgets';
import { FETCH_LIMIT } from '@utils/constants'; import { FETCH_LIMIT } from '@utils/constants';
import { useNostr } from '@utils/hooks/useNostr';
import { sendNativeNotification } from '@utils/notification'; import { sendNativeNotification } from '@utils/notification';
export function NotificationWidget() { export function NotificationWidget() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { db } = useStorage(); const { ark } = useArk();
const { sub } = useNostr();
const { ndk, relayUrls, fetcher } = useNDK();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({ useInfiniteQuery({
queryKey: ['notification'], queryKey: ['notification'],

View File

@@ -2,6 +2,8 @@ import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { WVList } from 'virtua'; import { WVList } from 'virtua';
import { useArk } from '@libs/ark';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { import {
ChildNote, ChildNote,
@@ -17,16 +19,15 @@ import { User } from '@shared/user';
import { WidgetWrapper } from '@shared/widgets'; import { WidgetWrapper } from '@shared/widgets';
import { useEvent } from '@utils/hooks/useEvent'; import { useEvent } from '@utils/hooks/useEvent';
import { useNostr } from '@utils/hooks/useNostr';
import { Widget } from '@utils/types'; import { Widget } from '@utils/types';
export function ThreadWidget({ widget }: { widget: Widget }) { export function ThreadWidget({ widget }: { widget: Widget }) {
const { isFetching, isError, data } = useEvent(widget.content); const { isFetching, isError, data } = useEvent(widget.content);
const { getEventThread } = useNostr(); const { ark } = useArk();
const renderKind = useCallback( const renderKind = useCallback(
(event: NDKEvent) => { (event: NDKEvent) => {
const thread = getEventThread(event.tags); const thread = ark.getEventThread({ tags: event.tags });
switch (event.kind) { switch (event.kind) {
case NDKKind.Text: case NDKKind.Text:
return ( return (

View File

@@ -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<string, NDKSubscription, void>({
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<false>[] = [];
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<string, string[]>();
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,
};
}