mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-18 11:13:30 +00:00
wip: relay manegament screen
This commit is contained in:
parent
a2e3247432
commit
11ed618a7f
18
src/app.tsx
18
src/app.tsx
@ -117,16 +117,14 @@ export default function App() {
|
|||||||
const { RelaysScreen } = await import('@app/relays');
|
const { RelaysScreen } = await import('@app/relays');
|
||||||
return { Component: RelaysScreen };
|
return { Component: RelaysScreen };
|
||||||
},
|
},
|
||||||
children: [
|
},
|
||||||
{
|
{
|
||||||
path: ':url',
|
path: 'relays/:url',
|
||||||
loader: relayLoader,
|
loader: relayLoader,
|
||||||
async lazy() {
|
async lazy() {
|
||||||
const { RelayScreen } = await import('@app/relays/relay');
|
const { RelayScreen } = await import('@app/relays/relay');
|
||||||
return { Component: RelayScreen };
|
return { Component: RelayScreen };
|
||||||
},
|
},
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
29
src/app/relays/components/relayEventList.tsx
Normal file
29
src/app/relays/components/relayEventList.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
66
src/app/relays/components/relayForm.tsx
Normal file
66
src/app/relays/components/relayForm.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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 { useNavigate } from 'react-router-dom';
|
||||||
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';
|
||||||
|
|
||||||
@ -9,21 +13,31 @@ import { useNostr } from '@utils/hooks/useNostr';
|
|||||||
|
|
||||||
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 { status, data } = useQuery(
|
const { status, data } = useQuery(
|
||||||
['relays'],
|
['relays'],
|
||||||
async () => {
|
async () => {
|
||||||
return await getAllRelaysByUsers();
|
return await getAllRelaysByUsers();
|
||||||
},
|
},
|
||||||
{ refetchOnMount: false }
|
{ refetchOnWindowFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const openRelay = (relayUrl: string) => {
|
const inspectRelay = (relayUrl: string) => {
|
||||||
const url = new URL(relayUrl);
|
const url = new URL(relayUrl);
|
||||||
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) await message("You're aldready connected to this relay");
|
||||||
|
queryClient.invalidateQueries(['user-relay']);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="col-span-2 border-r border-white/5">
|
<div className="col-span-2 border-r border-white/5">
|
||||||
{status === 'loading' ? (
|
{status === 'loading' ? (
|
||||||
@ -49,7 +63,7 @@ export function RelayList() {
|
|||||||
<div className="inline-flex items-center gap-2">
|
<div className="inline-flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="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"
|
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" />
|
<ShareIcon className="h-3 w-3" />
|
||||||
@ -57,6 +71,7 @@ export function RelayList() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
onClick={() => connectRelay(key)}
|
||||||
className="inline-flex h-6 w-6 items-center justify-center rounded hover:bg-white/10"
|
className="inline-flex h-6 w-6 items-center justify-center rounded hover:bg-white/10"
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-3 w-3" />
|
<PlusIcon className="h-3 w-3" />
|
||||||
|
@ -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 { useNDK } from '@libs/ndk/provider';
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
|
import { CancelIcon } from '@shared/icons';
|
||||||
|
|
||||||
export function UserRelay() {
|
export function UserRelay() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { relayUrls } = useNDK();
|
const { relayUrls } = useNDK();
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
const { status, data } = useQuery(
|
const { status, data } = useQuery(
|
||||||
@ -14,6 +20,11 @@ export function UserRelay() {
|
|||||||
{ refetchOnWindowFocus: false }
|
{ refetchOnWindowFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const removeRelay = async (relayUrl: string) => {
|
||||||
|
await db.removeRelay(relayUrl);
|
||||||
|
queryClient.invalidateQueries(['user-relay']);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 px-3">
|
<div className="mt-3 px-3">
|
||||||
{status === 'loading' ? (
|
{status === 'loading' ? (
|
||||||
@ -23,22 +34,34 @@ export function UserRelay() {
|
|||||||
{data.map((item) => (
|
{data.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item}
|
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) ? (
|
<div className="inline-flex items-center gap-2.5">
|
||||||
<span className="relative flex h-3 w-3">
|
{relayUrls.includes(item) ? (
|
||||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
|
<span className="relative flex h-2 w-2">
|
||||||
<span className="relative inline-flex h-3 w-3 rounded-full bg-green-500"></span>
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
|
||||||
</span>
|
<span className="relative inline-flex h-2 w-2 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 flex h-2 w-2">
|
||||||
<span className="relative inline-flex h-3 w-3 rounded-full bg-red-500"></span>
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75"></span>
|
||||||
</span>
|
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500"></span>
|
||||||
)}
|
</span>
|
||||||
<p className="text-sm font-medium">{item}</p>
|
)}
|
||||||
|
<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>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
<RelayForm />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,19 +1,61 @@
|
|||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { Await, useLoaderData } from 'react-router-dom';
|
import { Await, useLoaderData } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { LoaderIcon } from '@shared/icons';
|
||||||
|
|
||||||
export function RelayScreen() {
|
export function RelayScreen() {
|
||||||
const data: { relay?: { [key: string]: string } } = useLoaderData();
|
const data: { relay?: { [key: string]: string } } = useLoaderData();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="grid h-full w-full grid-cols-3">
|
||||||
<Suspense fallback={<p>Loading...</p>}>
|
<div className="col-span-2 border-r border-white/5"></div>
|
||||||
<Await
|
<div className="col-span-1">
|
||||||
resolve={data.relay}
|
<div className="inline-flex h-16 w-full items-center border-b border-white/5 px-3">
|
||||||
errorElement={<div>Could not load relay information 😬</div>}
|
<h3 className="font-semibold text-white">Information</h3>
|
||||||
>
|
</div>
|
||||||
{(resolvedRelay) => <p>{JSON.stringify(resolvedRelay)}</p>}
|
<div className="mt-4 px-3">
|
||||||
</Await>
|
<Suspense
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -310,6 +310,13 @@ export class LumeStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async createRelay(relay: string, purpose?: string) {
|
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(
|
return await this.db.execute(
|
||||||
'INSERT OR IGNORE INTO relays (account_id, relay, purpose) VALUES ($1, $2, $3);',
|
'INSERT OR IGNORE INTO relays (account_id, relay, purpose) VALUES ($1, $2, $3);',
|
||||||
[this.account.id, relay, purpose || '']
|
[this.account.id, relay, purpose || '']
|
||||||
|
@ -67,3 +67,4 @@ export * from './nwc';
|
|||||||
export * from './timeline';
|
export * from './timeline';
|
||||||
export * from './dots';
|
export * from './dots';
|
||||||
export * from './handArrowDown';
|
export * from './handArrowDown';
|
||||||
|
export * from './relay';
|
||||||
|
28
src/shared/icons/relay.tsx
Normal file
28
src/shared/icons/relay.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -15,6 +15,7 @@ import {
|
|||||||
BellIcon,
|
BellIcon,
|
||||||
NavArrowDownIcon,
|
NavArrowDownIcon,
|
||||||
NwcIcon,
|
NwcIcon,
|
||||||
|
RelayIcon,
|
||||||
SpaceIcon,
|
SpaceIcon,
|
||||||
WorldIcon,
|
WorldIcon,
|
||||||
} from '@shared/icons';
|
} 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">
|
<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>
|
</span>
|
||||||
Relays
|
Relays
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
Loading…
Reference in New Issue
Block a user