improve relay form

This commit is contained in:
2023-12-02 17:53:45 +07:00
parent a528b646e3
commit 255dcb43fe
8 changed files with 178 additions and 102 deletions

View File

@@ -1,7 +1,7 @@
import { NDKEvent, NDKKind, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { downloadDir } from '@tauri-apps/api/path'; import { downloadDir } from '@tauri-apps/api/path';
import { writeText } from '@tauri-apps/plugin-clipboard-manager'; import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { message, save } from '@tauri-apps/plugin-dialog'; import { save } from '@tauri-apps/plugin-dialog';
import { writeTextFile } from '@tauri-apps/plugin-fs'; import { writeTextFile } from '@tauri-apps/plugin-fs';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { minidenticon } from 'minidenticons'; import { minidenticon } from 'minidenticons';
@@ -67,7 +67,6 @@ export function CreateAccountScreen() {
const event = new NDKEvent(ndk); const event = new NDKEvent(ndk);
event.content = JSON.stringify(profile); event.content = JSON.stringify(profile);
event.kind = NDKKind.Metadata; event.kind = NDKKind.Metadata;
event.created_at = Math.floor(Date.now() / 1000);
event.pubkey = userPubkey; event.pubkey = userPubkey;
event.tags = []; event.tags = [];
@@ -76,6 +75,16 @@ export function CreateAccountScreen() {
if (publish) { if (publish) {
await db.createAccount(userNpub, userPubkey); await db.createAccount(userNpub, userPubkey);
await db.secureSave(userPubkey, userPrivkey); await db.secureSave(userPubkey, userPrivkey);
const relayListEvent = new NDKEvent(ndk);
relayListEvent.kind = NDKKind.RelayList;
relayListEvent.tags = [...ndk.pool.relays.values()].map((item) => [
'r',
item.url,
]);
await relayListEvent.publish();
setKeys({ setKeys({
npub: userNpub, npub: userNpub,
nsec: userNsec, nsec: userNsec,
@@ -88,7 +97,7 @@ export function CreateAccountScreen() {
setLoading(false); setLoading(false);
} }
} catch (e) { } catch (e) {
return toast(e); return toast.error(e);
} }
}; };
@@ -113,7 +122,7 @@ export function CreateAccountScreen() {
setDownloaded(true); setDownloaded(true);
} // else { user cancel action } } // else { user cancel action }
} catch (e) { } catch (e) {
await message(e, { title: 'Cannot download account keys', type: 'error' }); return toast.error(e);
} }
}; };

View File

@@ -66,7 +66,7 @@ export function FollowScreen() {
const submit = async () => { const submit = async () => {
try { try {
setLoading(true); setLoading(true);
if (!follows.length) navigate('/'); if (!follows.length) return navigate('/auth/finish');
const event = new NDKEvent(ndk); const event = new NDKEvent(ndk);
event.kind = NDKKind.Contacts; event.kind = NDKKind.Contacts;
@@ -81,7 +81,8 @@ export function FollowScreen() {
if (item.startsWith('npub')) return nip19.decode(item).data as string; if (item.startsWith('npub')) return nip19.decode(item).data as string;
return item; return item;
}); });
navigate('/auth/finish');
return navigate('/auth/finish');
} }
} catch (e) { } catch (e) {
setLoading(false); setLoading(false);

View File

@@ -1,69 +1,62 @@
import { useQueryClient } from '@tanstack/react-query'; import { NDKRelayUrl } from '@nostr-dev-kit/ndk';
import { normalizeRelayUrl } from 'nostr-fetch';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'sonner';
import { useStorage } from '@libs/storage/provider';
import { PlusIcon } from '@shared/icons'; import { PlusIcon } from '@shared/icons';
import { useRelay } from '@utils/hooks/useRelay';
const domainRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/; const domainRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/;
export function RelayForm() { export function RelayForm() {
const { db } = useStorage(); const { connectRelay } = useRelay();
const queryClient = useQueryClient(); const [relay, setRelay] = useState<{
url: NDKRelayUrl;
purpose: 'read' | 'write' | undefined;
}>({ url: '', purpose: undefined });
const [url, setUrl] = useState(''); const create = () => {
const [error, setError] = useState(''); if (relay.url.length < 1) return toast.info('Please enter relay url');
const createRelay = async () => {
if (url.length < 1) return setError('Please enter relay url');
try { try {
const relay = new URL(url.replace(/\s/g, '')); const relayUrl = new URL(relay.url.replace(/\s/g, ''));
if ( if (
domainRegex.test(relay.host) && domainRegex.test(relayUrl.host) &&
(relay.protocol === 'wss:' || relay.protocol === 'ws:') (relayUrl.protocol === 'wss:' || relayUrl.protocol === 'ws:')
) { ) {
const res = await db.createRelay(url); connectRelay.mutate(normalizeRelayUrl(relay.url));
if (!res) return setError("You're already using this relay"); setRelay({ url: '', purpose: undefined });
queryClient.invalidateQueries({
queryKey: ['user-relay'],
});
setError('');
setUrl('');
} else { } else {
return setError( return toast.error(
'URL is invalid, a relay must use websocket protocol (start with wss:// or ws://). Please check again' 'URL is invalid, a relay must use websocket protocol (start with wss:// or ws://). Please check again'
); );
} }
} catch { } catch {
return setError('Relay URL is not valid. Please check again'); return toast.error('Relay URL is not valid. Please check again');
} }
}; };
return ( return (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="flex h-10 items-center justify-between rounded-lg bg-neutral-200 pr-1.5 dark:bg-neutral-800"> <div className="flex gap-2">
<input <input
className="h-full w-full bg-transparent pl-3 pr-1.5 text-neutral-900 placeholder:text-neutral-600 focus:outline-none dark:text-neutral-100 dark:placeholder:text-neutral-400" className="h-11 flex-1 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
type="url"
placeholder="wss://" placeholder="wss://"
spellCheck={false} spellCheck={false}
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
value={url} value={relay.url}
onChange={(e) => setUrl(e.target.value)} onChange={(e) => setRelay((prev) => ({ ...prev, url: e.target.value }))}
/> />
<button <button
type="button" type="button"
onClick={() => createRelay()} onClick={() => create()}
className="inline-flex h-6 w-6 items-center justify-center rounded bg-blue-500 text-white hover:bg-blue-600" className="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-blue-500 text-white hover:bg-blue-600"
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-5 w-5" />
</button> </button>
</div> </div>
<span className="text-sm text-red-400">{error}</span>
</div> </div>
); );
} }

View File

@@ -1,22 +1,18 @@
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { normalizeRelayUrl } from 'nostr-fetch';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { VList } from 'virtua'; import { VList } from 'virtua';
import { useStorage } from '@libs/storage/provider';
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 { useNostr } from '@utils/hooks/useNostr';
import { useRelay } from '@utils/hooks/useRelay';
export function RelayList() { export function RelayList() {
const navigate = useNavigate(); const navigate = useNavigate();
const queryClient = useQueryClient();
const { getAllRelaysByUsers } = useNostr(); const { getAllRelaysByUsers } = useNostr();
const { db } = useStorage(); const { connectRelay } = useRelay();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['relays'], queryKey: ['relays'],
queryFn: async () => { queryFn: async () => {
@@ -33,20 +29,6 @@ export function RelayList() {
navigate(`/relays/${url.hostname}`); navigate(`/relays/${url.hostname}`);
}; };
const connectRelay = async (relayUrl: string) => {
const url = normalizeRelayUrl(relayUrl);
const res = await db.createRelay(url);
if (res) {
toast.info('Connected. You need to restart app to take effect');
queryClient.invalidateQueries({
queryKey: ['user-relay'],
});
} else {
toast.warning("You're aldready connected to this relay");
}
};
return ( return (
<div className="col-span-2 border-r border-neutral-100 dark:border-neutral-900"> <div className="col-span-2 border-r border-neutral-100 dark:border-neutral-900">
{status === 'pending' ? ( {status === 'pending' ? (
@@ -59,9 +41,7 @@ export function RelayList() {
) : ( ) : (
<VList className="h-full"> <VList className="h-full">
<div className="inline-flex h-16 w-full items-center border-b border-neutral-100 px-3 dark:border-neutral-900"> <div className="inline-flex h-16 w-full items-center border-b border-neutral-100 px-3 dark:border-neutral-900">
<h3 className="font-semibold text-neutral-950 dark:text-neutral-50"> <h3 className="font-semibold">Relay discovery</h3>
All relays
</h3>
</div> </div>
{[...data].map(([key, value]) => ( {[...data].map(([key, value]) => (
<div <div
@@ -80,7 +60,7 @@ export function RelayList() {
</button> </button>
<button <button
type="button" type="button"
onClick={() => connectRelay(key)} onClick={() => connectRelay.mutate(key)}
className="inline-flex h-6 w-6 items-center justify-center rounded text-neutral-900 hover:bg-neutral-200 dark:text-neutral-100 dark:hover:bg-neutral-800" className="inline-flex h-6 w-6 items-center justify-center rounded text-neutral-900 hover:bg-neutral-200 dark:text-neutral-100 dark:hover:bg-neutral-800"
> >
<PlusIcon className="h-3 w-3" /> <PlusIcon className="h-3 w-3" />

View File

@@ -1,4 +1,5 @@
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { NDKKind, NDKSubscriptionCacheUsage } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { RelayForm } from '@app/relays/components/relayForm'; import { RelayForm } from '@app/relays/components/relayForm';
@@ -7,39 +8,49 @@ import { useStorage } from '@libs/storage/provider';
import { CancelIcon } from '@shared/icons'; import { CancelIcon } from '@shared/icons';
export function UserRelay() { import { useRelay } from '@utils/hooks/useRelay';
const queryClient = useQueryClient();
const { relayUrls } = useNDK(); export function UserRelayList() {
const { db } = useStorage(); const { db } = useStorage();
const { ndk, relayUrls } = useNDK();
const { removeRelay } = useRelay();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['user-relay'], queryKey: ['relays', db.account.pubkey],
queryFn: async () => { queryFn: async () => {
return await db.getExplicitRelayUrls(); const event = await ndk.fetchEvent(
{
kinds: [NDKKind.RelayList],
authors: [db.account.pubkey],
},
{ cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY }
);
if (!event) throw new Error('relay set not found');
return event.tags;
}, },
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
const removeRelay = async (relayUrl: string) => {
await db.removeRelay(relayUrl);
queryClient.invalidateQueries({
queryKey: ['user-relay'],
});
};
return ( return (
<div className="mt-3 px-3"> <div className="col-span-1">
{status === 'pending' ? ( <div className="inline-flex h-16 w-full items-center border-b border-neutral-100 px-3 dark:border-neutral-900">
<p>Loading...</p> <h3 className="font-semibold">Connected relays</h3>
) : ( </div>
<div className="flex flex-col gap-2"> <div className="mt-3 flex flex-col gap-2 px-3">
{data.map((item) => ( {status === 'pending' ? (
<p>Loading...</p>
) : !data ? (
<div className="flex h-20 w-full items-center justify-center rounded-xl bg-neutral-100 dark:bg-neutral-900">
<p className="text-sm font-medium">You not have personal relay set yet</p>
</div>
) : (
data.map((item) => (
<div <div
key={item} key={item[1]}
className="group flex h-10 items-center justify-between rounded-lg bg-neutral-100 pl-3 pr-1.5 dark:bg-neutral-900" className="group flex h-11 items-center justify-between rounded-lg bg-neutral-100 pl-3 pr-1.5 dark:bg-neutral-900"
> >
<div className="inline-flex items-center gap-2.5"> <div className="inline-flex items-center gap-2.5">
{relayUrls.includes(item) ? ( {relayUrls.includes(item[1]) ? (
<span className="relative flex h-2 w-2"> <span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span> <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-teal-500"></span> <span className="relative inline-flex h-2 w-2 rounded-full bg-teal-500"></span>
@@ -51,21 +62,21 @@ export function UserRelay() {
</span> </span>
)} )}
<p className="max-w-[20rem] truncate text-sm font-medium text-neutral-900 dark:text-neutral-100"> <p className="max-w-[20rem] truncate text-sm font-medium text-neutral-900 dark:text-neutral-100">
{item} {item[1]}
</p> </p>
</div> </div>
<button <button
type="button" type="button"
onClick={() => removeRelay(item)} onClick={() => removeRelay.mutate(item[1])}
className="hidden h-6 w-6 items-center justify-center rounded group-hover:inline-flex hover:bg-neutral-300 dark:hover:bg-neutral-700" className="hidden h-6 w-6 items-center justify-center rounded group-hover:inline-flex hover:bg-neutral-300 dark:hover:bg-neutral-700"
> >
<CancelIcon className="h-4 w-4 text-neutral-900 dark:text-neutral-100" /> <CancelIcon className="h-4 w-4 text-neutral-900 dark:text-neutral-100" />
</button> </button>
</div> </div>
))} ))
<RelayForm /> )}
</div> <RelayForm />
)} </div>
</div> </div>
); );
} }

View File

@@ -1,18 +1,11 @@
import { RelayList } from '@app/relays/components/relayList'; import { RelayList } from '@app/relays/components/relayList';
import { UserRelay } from '@app/relays/components/userRelay'; import { UserRelayList } from '@app/relays/components/userRelayList';
export function RelaysScreen() { export function RelaysScreen() {
return ( return (
<div className="grid h-full w-full grid-cols-3"> <div className="grid h-full w-full grid-cols-3">
<RelayList /> <RelayList />
<div className="col-span-1"> <UserRelayList />
<div className="inline-flex h-16 w-full items-center border-b border-neutral-100 px-3 dark:border-neutral-900">
<h3 className="font-semibold text-neutral-900 dark:text-neutral-100">
Connected relays
</h3>
</div>
<UserRelay />
</div>
</div> </div>
); );
} }

View File

@@ -72,7 +72,7 @@ export const NDKInstance = () => {
enableOutboxModel: outbox, enableOutboxModel: outbox,
autoConnectUserRelays: true, autoConnectUserRelays: true,
autoFetchUserMutelist: true, autoFetchUserMutelist: true,
clientName: 'Lume', // clientName: 'Lume',
// clientNip89: '', // clientNip89: '',
}); });

View File

@@ -0,0 +1,89 @@
import { NDKEvent, NDKKind, NDKRelayUrl, NDKTag } from '@nostr-dev-kit/ndk';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
export function useRelay() {
const { db } = useStorage();
const { ndk } = useNDK();
const queryClient = useQueryClient();
const connectRelay = useMutation({
mutationFn: async (relay: NDKRelayUrl, purpose?: 'read' | 'write' | undefined) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['relays', db.account.pubkey] });
// Snapshot the previous value
const prevRelays: NDKTag[] = queryClient.getQueryData([
'relays',
db.account.pubkey,
]);
// create new relay list if not exist
if (!prevRelays) {
const newListEvent = new NDKEvent(ndk);
newListEvent.kind = NDKKind.RelayList;
newListEvent.tags = [['r', relay, purpose ?? '']];
await newListEvent.publish();
}
// add relay to exist list
const index = prevRelays.findIndex((el) => el[1] === relay);
if (index > -1) return;
const event = new NDKEvent(ndk);
event.kind = NDKKind.RelayList;
event.tags = [...prevRelays, ['r', relay, purpose ?? '']];
await event.publish();
// Optimistically update to the new value
queryClient.setQueryData(['relays', db.account.pubkey], (prev: NDKTag[]) => [
...prev,
['r', relay, purpose ?? ''],
]);
// Return a context object with the snapshotted value
return { prevRelays };
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['relays', db.account.pubkey] });
},
});
const removeRelay = useMutation({
mutationFn: async (relay: NDKRelayUrl) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['relays', db.account.pubkey] });
// Snapshot the previous value
const prevRelays: NDKTag[] = queryClient.getQueryData([
'relays',
db.account.pubkey,
]);
if (!prevRelays) return;
const index = prevRelays.findIndex((el) => el[1] === relay);
if (index > -1) prevRelays.splice(index, 1);
const event = new NDKEvent(ndk);
event.kind = NDKKind.RelayList;
event.tags = prevRelays;
await event.publish();
// Optimistically update to the new value
queryClient.setQueryData(['relays', db.account.pubkey], prevRelays);
// Return a context object with the snapshotted value
return { prevRelays };
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['relays', db.account.pubkey] });
},
});
return { connectRelay, removeRelay };
}