wip: relay manegament screen

This commit is contained in:
Ren Amamiya 2023-09-30 19:07:17 +07:00
parent a2e3247432
commit 11ed618a7f
10 changed files with 248 additions and 38 deletions

View File

@ -117,16 +117,14 @@ export default function App() {
const { RelaysScreen } = await import('@app/relays');
return { Component: RelaysScreen };
},
children: [
{
path: ':url',
loader: relayLoader,
async lazy() {
const { RelayScreen } = await import('@app/relays/relay');
return { Component: RelayScreen };
},
},
],
},
{
path: 'relays/:url',
loader: relayLoader,
async lazy() {
const { RelayScreen } = await import('@app/relays/relay');
return { Component: RelayScreen };
},
},
],
},

View File

@ -0,0 +1,29 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useNDK } from '@libs/ndk/provider';
export function RelayEventList({ relayUrl }: { relayUrl: string }) {
const { fetcher } = useNDK();
const { status, data } = useQuery(
['relay-event'],
async () => {
const events = await fetcher.fetchLatestEvents(
[relayUrl],
{
kinds: [NDKKind.Text, NDKKind.Repost, 1063, NDKKind.Article],
},
100
);
return events as unknown as NDKEvent[];
},
{ refetchOnWindowFocus: false }
);
return (
<div>
<p>TODO</p>
</div>
);
}

View File

@ -0,0 +1,66 @@
import { useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { useStorage } from '@libs/storage/provider';
import { PlusIcon } from '@shared/icons';
const domainRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/;
export function RelayForm() {
const { db } = useStorage();
const queryClient = useQueryClient();
const [url, setUrl] = useState('');
const [error, setError] = useState('');
const createRelay = async () => {
if (url.length < 1) return setError('Please enter relay url');
try {
const relay = new URL(url.replace(/\s/g, ''));
if (
domainRegex.test(relay.host) &&
(relay.protocol === 'wss:' || relay.protocol === 'ws:')
) {
const res = await db.createRelay(url);
if (!res) return setError("You're already using this relay");
queryClient.invalidateQueries(['user-relay']);
setError('');
setUrl('');
} else {
return setError(
'URL is invalid, a relay must use websocket protocol (start with wss:// or ws://). Please check again'
);
}
} catch {
return setError('Relay URL is not valid. Please check again');
}
};
return (
<div className="flex flex-col gap-1">
<div className="flex h-10 items-center justify-between rounded-lg bg-white/10 pr-1.5">
<input
className="h-full w-full bg-transparent pl-3 pr-1.5 placeholder:text-white/70 focus:outline-none"
type="url"
placeholder="wss://"
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<button
type="button"
onClick={() => createRelay()}
className="inline-flex h-6 w-6 items-center justify-center rounded bg-fuchsia-500 text-white hover:bg-fuchsia-600"
>
<PlusIcon className="h-4 w-4" />
</button>
</div>
<span className="text-sm text-red-400">{error}</span>
</div>
);
}

View File

@ -1,7 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { message } from '@tauri-apps/api/dialog';
import { normalizeRelayUrl } from 'nostr-fetch';
import { useNavigate } from 'react-router-dom';
import { VList } from 'virtua';
import { useStorage } from '@libs/storage/provider';
import { LoaderIcon, PlusIcon, ShareIcon } from '@shared/icons';
import { User } from '@shared/user';
@ -9,21 +13,31 @@ import { useNostr } from '@utils/hooks/useNostr';
export function RelayList() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { getAllRelaysByUsers } = useNostr();
const { db } = useStorage();
const { status, data } = useQuery(
['relays'],
async () => {
return await getAllRelaysByUsers();
},
{ refetchOnMount: false }
{ refetchOnWindowFocus: false }
);
const openRelay = (relayUrl: string) => {
const inspectRelay = (relayUrl: string) => {
const url = new URL(relayUrl);
navigate(`/relays/${url.hostname}`);
};
const connectRelay = async (relayUrl: string) => {
const url = normalizeRelayUrl(relayUrl);
const res = await db.createRelay(url);
if (!res) await message("You're aldready connected to this relay");
queryClient.invalidateQueries(['user-relay']);
};
return (
<div className="col-span-2 border-r border-white/5">
{status === 'loading' ? (
@ -49,7 +63,7 @@ export function RelayList() {
<div className="inline-flex items-center gap-2">
<button
type="button"
onClick={() => openRelay(key)}
onClick={() => inspectRelay(key)}
className="inline-flex h-6 items-center justify-center gap-1 rounded bg-white/10 px-1.5 text-sm font-medium hover:bg-white/20"
>
<ShareIcon className="h-3 w-3" />
@ -57,6 +71,7 @@ export function RelayList() {
</button>
<button
type="button"
onClick={() => connectRelay(key)}
className="inline-flex h-6 w-6 items-center justify-center rounded hover:bg-white/10"
>
<PlusIcon className="h-3 w-3" />

View File

@ -1,9 +1,15 @@
import { useQuery } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { RelayForm } from '@app/relays/components/relayForm';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
import { CancelIcon } from '@shared/icons';
export function UserRelay() {
const queryClient = useQueryClient();
const { relayUrls } = useNDK();
const { db } = useStorage();
const { status, data } = useQuery(
@ -14,6 +20,11 @@ export function UserRelay() {
{ refetchOnWindowFocus: false }
);
const removeRelay = async (relayUrl: string) => {
await db.removeRelay(relayUrl);
queryClient.invalidateQueries(['user-relay']);
};
return (
<div className="mt-3 px-3">
{status === 'loading' ? (
@ -23,22 +34,34 @@ export function UserRelay() {
{data.map((item) => (
<div
key={item}
className="inline-flex h-10 items-center gap-2.5 rounded-lg bg-white/10 px-3"
className="group flex h-10 items-center justify-between rounded-lg bg-white/10 pl-3 pr-1.5"
>
{relayUrls.includes(item) ? (
<span className="relative flex h-3 w-3">
<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-3 w-3 rounded-full bg-green-500"></span>
</span>
) : (
<span className="relative flex h-3 w-3">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75"></span>
<span className="relative inline-flex h-3 w-3 rounded-full bg-red-500"></span>
</span>
)}
<p className="text-sm font-medium">{item}</p>
<div className="inline-flex items-center gap-2.5">
{relayUrls.includes(item) ? (
<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="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span>
</span>
) : (
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500"></span>
</span>
)}
<p className="max-w-[20rem] truncate text-sm font-medium leading-none">
{item}
</p>
</div>
<button
type="button"
onClick={() => removeRelay(item)}
className="hidden h-6 w-6 items-center justify-center rounded hover:bg-white/10 group-hover:inline-flex"
>
<CancelIcon className="h-4 w-4 text-white" />
</button>
</div>
))}
<RelayForm />
</div>
)}
</div>

View File

@ -1,19 +1,61 @@
import { Suspense } from 'react';
import { Await, useLoaderData } from 'react-router-dom';
import { LoaderIcon } from '@shared/icons';
export function RelayScreen() {
const data: { relay?: { [key: string]: string } } = useLoaderData();
return (
<div>
<Suspense fallback={<p>Loading...</p>}>
<Await
resolve={data.relay}
errorElement={<div>Could not load relay information 😬</div>}
>
{(resolvedRelay) => <p>{JSON.stringify(resolvedRelay)}</p>}
</Await>
</Suspense>
<div className="grid h-full w-full grid-cols-3">
<div className="col-span-2 border-r border-white/5"></div>
<div className="col-span-1">
<div className="inline-flex h-16 w-full items-center border-b border-white/5 px-3">
<h3 className="font-semibold text-white">Information</h3>
</div>
<div className="mt-4 px-3">
<Suspense
fallback={
<div className="flex items-center gap-2 text-sm font-medium text-white">
<LoaderIcon className="h-4 w-4 animate-spin" />
Loading...
</div>
}
>
<Await
resolve={data.relay}
errorElement={
<div className="text-sm font-medium">
<p>Could not load relay information 😬</p>
</div>
}
>
{(resolvedRelay) => (
<div className="flex flex-col gap-2">
<p>
<span className="font-semibold">Name</span> : {resolvedRelay.name}
</p>
<p>
<span className="font-semibold">Description</span> :{' '}
{resolvedRelay.description}
</p>
<p>
<span className="font-semibold">Contact</span> :{' '}
{resolvedRelay.contact}
</p>
<p>
<span className="font-semibold">Software</span> : [open website]
</p>
<p>
<span className="font-semibold">Version</span> :{' '}
{resolvedRelay.version}
</p>
</div>
)}
</Await>
</Suspense>
</div>
</div>
</div>
);
}

View File

@ -310,6 +310,13 @@ export class LumeStorage {
}
public async createRelay(relay: string, purpose?: string) {
const existRelays: Relays[] = await this.db.select(
'SELECT * FROM relays WHERE relay = $1 AND account_id = $2 ORDER BY id DESC LIMIT 1;',
[relay, this.account.id]
);
if (existRelays.length > 0) return false;
return await this.db.execute(
'INSERT OR IGNORE INTO relays (account_id, relay, purpose) VALUES ($1, $2, $3);',
[this.account.id, relay, purpose || '']

View File

@ -67,3 +67,4 @@ export * from './nwc';
export * from './timeline';
export * from './dots';
export * from './handArrowDown';
export * from './relay';

View File

@ -0,0 +1,28 @@
import { SVGProps } from 'react';
export function RelayIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="square"
strokeLinejoin="round"
strokeWidth="1.5"
d="M21.25 12V5.75a1 1 0 00-1-1H3.75a1 1 0 00-1 1V12m18.5 0H2.75m18.5 0v6.25a1 1 0 01-1 1H3.75a1 1 0 01-1-1V12"
/>
<path
fill="currentColor"
stroke="currentColor"
strokeWidth="0.5"
d="M6.5 9.125a.75.75 0 100-1.5.75.75 0 000 1.5zm0 7.25a.75.75 0 100-1.5.75.75 0 000 1.5z"
/>
</svg>
);
}

View File

@ -15,6 +15,7 @@ import {
BellIcon,
NavArrowDownIcon,
NwcIcon,
RelayIcon,
SpaceIcon,
WorldIcon,
} from '@shared/icons';
@ -102,7 +103,7 @@ export function Navigation() {
}
>
<span className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded bg-white/10 backdrop-blur-xl">
<WorldIcon className="h-4 w-4 text-white" />
<RelayIcon className="h-4 w-4 text-white" />
</span>
Relays
</NavLink>