feat: fix relay manager

This commit is contained in:
reya 2024-01-18 09:41:53 +07:00
parent c3482cddd8
commit 240fe8bc7c
7 changed files with 98 additions and 50 deletions

View File

@ -1,9 +1,9 @@
import { useRelaylist } from "@lume/ark";
import { PlusIcon, ShareIcon } from "@lume/icons";
import { User, useRelaylist } from "@lume/ark";
import { PlusIcon, SearchIcon } from "@lume/icons";
import { normalizeRelayUrl } from "nostr-fetch";
import { Link } from "react-router-dom";
export function RelayItem({ url }: { url: string }) {
export function RelayItem({ url, users }: { url: string; users?: string[] }) {
const domain = new URL(url).hostname;
const { connectRelay } = useRelaylist();
@ -18,19 +18,35 @@ export function RelayItem({ url }: { url: string }) {
</span>
</div>
<div className="inline-flex items-center gap-2">
{users ? (
<div className="isolate flex -space-x-2 mr-4">
{users.slice(0, 4).map((item) => (
<User.Provider pubkey={item}>
<User.Root>
<User.Avatar className="size-8 inline-block rounded-full ring-1 ring-neutral-100 dark:ring-neutral-900" />
</User.Root>
</User.Provider>
))}
{users.length > 4 ? (
<div className="inline-flex size-8 items-center justify-center rounded-full bg-neutral-100 text-neutral-900 ring-1 ring-neutral-200 dark:bg-neutral-900 dark:text-neutral-100 dark:ring-neutral-800">
<span className="text-xs font-medium">+{users.length - 4}</span>
</div>
) : null}
</div>
) : null}
<Link
to={`/relays/${domain}/`}
className="inline-flex h-6 items-center justify-center gap-1 rounded bg-neutral-100 px-1.5 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
className="inline-flex h-8 items-center justify-center gap-2 rounded-lg bg-neutral-100 px-2 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
<ShareIcon className="h-3 w-3" />
<SearchIcon className="size-4" />
Inspect
</Link>
<button
type="button"
onClick={() => connectRelay.mutate(normalizeRelayUrl(url))}
className="inline-flex h-6 w-6 items-center justify-center rounded bg-blue-100 text-blue-500 hover:bg-blue-200 dark:bg-blue-900 hover:dark:bg-blue-800"
className="inline-flex size-8 items-center justify-center rounded-lg bg-blue-100 text-blue-500 hover:bg-blue-200 dark:bg-blue-900 hover:dark:bg-blue-800"
>
<PlusIcon className="size-4" />
<PlusIcon className="size-5" />
</button>
</div>
</div>

View File

@ -1,4 +1,4 @@
import { useArk, useRelay } from "@lume/ark";
import { useArk, useRelaylist } from "@lume/ark";
import { LoaderIcon, PlusIcon, ShareIcon } from "@lume/icons";
import { User } from "@lume/ui";
import { useQuery } from "@tanstack/react-query";
@ -7,7 +7,7 @@ import { VList } from "virtua";
export function RelayList() {
const ark = useArk();
const { connectRelay } = useRelay();
const { connectRelay } = useRelaylist();
const { status, data } = useQuery({
queryKey: ["relays"],
queryFn: async () => {

View File

@ -1,14 +1,15 @@
import { useArk } from "@lume/ark";
import { CancelIcon, RefreshIcon } from "@lume/icons";
import { useArk, useRelaylist } from "@lume/ark";
import { CancelIcon, LoaderIcon, RefreshIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { NDKKind } from "@nostr-dev-kit/ndk";
import { NDKKind, NDKSubscriptionCacheUsage } from "@nostr-dev-kit/ndk";
import { useQuery } from "@tanstack/react-query";
import { RelayForm } from "./relayForm";
export function RelaySidebar({ className }: { className?: string }) {
const ark = useArk();
const { status, data, refetch } = useQuery({
const { removeRelay } = useRelaylist();
const { status, data, isRefetching, refetch } = useQuery({
queryKey: ["relay-personal"],
queryFn: async () => {
const event = await ark.getEventByFilter({
@ -16,12 +17,15 @@ export function RelaySidebar({ className }: { className?: string }) {
kinds: [NDKKind.RelayList],
authors: [ark.account.pubkey],
},
cache: NDKSubscriptionCacheUsage.ONLY_RELAY,
});
if (!event) return [];
return event.tags.filter((tag) => tag[0] === "r");
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: Infinity,
});
const currentRelays = new Set(
@ -42,17 +46,19 @@ export function RelaySidebar({ className }: { className?: string }) {
onClick={() => refetch()}
className="inline-flex items-center justify-center w-6 h-6 rounded-md shrink-0 hover:bg-neutral-100 dark:hover:bg-neutral-900"
>
<RefreshIcon className="w-4 h-4" />
<RefreshIcon
className={cn("size-4", isRefetching ? "animate-spin" : "")}
/>
</button>
</div>
<div className="flex flex-col gap-2 px-3 mt-3">
{status === "pending" ? (
<p>Loading...</p>
<div className="flex items-center justify-center w-full h-20 rounded-lg bg-black/10 dark:bg-white/10">
<LoaderIcon className="size-5 animate-spin" />
</div>
) : !data.length ? (
<div className="flex items-center justify-center w-full h-20 rounded-xl bg-neutral-50 dark:bg-neutral-950">
<p className="text-sm font-medium">
You not have personal relay list yet
</p>
<div className="flex items-center justify-center w-full h-20 rounded-lg bg-black/10 dark:bg-white/10">
<p className="text-sm font-medium">Empty.</p>
</div>
) : (
data.map((item) => (
@ -73,7 +79,10 @@ export function RelaySidebar({ className }: { className?: string }) {
</span>
)}
<p className="max-w-[20rem] truncate text-sm font-medium text-neutral-900 dark:text-neutral-100">
{item[1].replace("wss://", "").replace("ws://", "")}
{item[1]
.replace("wss://", "")
.replace("ws://", "")
.replace("/", "")}
</p>
</div>
<div className="inline-flex items-center gap-2">
@ -84,9 +93,10 @@ export function RelaySidebar({ className }: { className?: string }) {
) : null}
<button
type="button"
className="items-center justify-center hidden w-6 h-6 rounded group-hover:inline-flex hover:bg-neutral-300 dark:hover:bg-neutral-700"
onClick={() => removeRelay.mutate(item[1])}
className="items-center justify-center hidden size-6 rounded group-hover:inline-flex hover:bg-neutral-300 dark:hover:bg-neutral-700"
>
<CancelIcon className="w-4 h-4 text-neutral-900 dark:text-neutral-100" />
<CancelIcon className="size-4 text-neutral-900 dark:text-neutral-100" />
</button>
</div>
</div>

View File

@ -1,18 +1,25 @@
import { useArk } from "@lume/ark";
import { LoaderIcon, PlusIcon, ShareIcon } from "@lume/icons";
import { LoaderIcon } from "@lume/icons";
import { useQuery } from "@tanstack/react-query";
import { VList } from "virtua";
import { RelayItem } from "./components/relayItem";
export function RelayFollowsScreen() {
const ark = useArk();
const { isLoading, data: relays } = useQuery({
const {
isLoading,
isError,
data: relays,
} = useQuery({
queryKey: ["relay-follows"],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
return await ark.getAllRelaysFromContacts();
const data = await ark.getAllRelaysFromContacts({ signal });
if (!data) throw new Error("Failed to get relay list from contacts");
return data;
},
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
});
if (isLoading) {
@ -23,10 +30,18 @@ export function RelayFollowsScreen() {
);
}
if (isError || !relays) {
return (
<div className="flex h-full w-full items-center justify-center">
<p>Error</p>
</div>
);
}
return (
<VList itemSize={49}>
{relays.map((item: string) => (
<RelayItem key={item} url={item} />
{[...relays].map(([key, value]) => (
<RelayItem key={key} url={key} users={value} />
))}
</VList>
);

View File

@ -14,6 +14,7 @@ export function RelayGlobalScreen() {
},
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
});
if (isLoading) {

View File

@ -254,9 +254,12 @@ export class Ark {
}
}
public async getEventByFilter({ filter }: { filter: NDKFilter }) {
public async getEventByFilter({
filter,
cache,
}: { filter: NDKFilter; cache?: NDKSubscriptionCacheUsage }) {
const event = await this.ndk.fetchEvent(filter, {
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
cacheUsage: cache || NDKSubscriptionCacheUsage.CACHE_FIRST,
});
if (!event) return null;
@ -352,7 +355,7 @@ export class Ark {
}
}
public async getAllRelaysFromContacts() {
public async getAllRelaysFromContacts({ signal }: { signal: AbortSignal }) {
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk));
const connectedRelays = this.ndk.pool
.connectedRelays()
@ -367,15 +370,21 @@ export class Ark {
},
{ kinds: [NDKKind.RelayList] },
1,
{ abortSignal: signal },
);
for await (const { author, events } of relayEvents) {
if (events[0]) {
for (const tag of events[0].tags) {
const users = relayMap.get(tag[1]);
console.log(relayEvents);
if (!users) relayMap.set(tag[1], [author]);
users.push(author);
for await (const { author, events } of relayEvents) {
if (events.length) {
const relayTags = events[0].tags.filter((item) => item[0] === "r");
for (const tag of relayTags) {
const item = relayMap.get(tag[1]);
if (item?.length) {
item.push(author);
} else {
relayMap.set(tag[1], [author]);
}
}
}
}

View File

@ -1,5 +1,6 @@
import { NDKKind, NDKRelayUrl, NDKTag } from "@nostr-dev-kit/ndk";
import { NDKKind, NDKTag } from "@nostr-dev-kit/ndk";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { normalizeRelayUrl } from "nostr-fetch";
import { useArk } from "./useArk";
export function useRelaylist() {
@ -8,7 +9,7 @@ export function useRelaylist() {
const connectRelay = useMutation({
mutationFn: async (
relay: NDKRelayUrl,
relay: WebSocket["url"],
purpose?: "read" | "write" | undefined,
) => {
// Cancel any outgoing refetches
@ -16,11 +17,10 @@ export function useRelaylist() {
queryKey: ["relay-personal"],
});
const relayUrl = normalizeRelayUrl(relay);
// Snapshot the previous value
const prevRelays: NDKTag[] = queryClient.getQueryData([
"relays",
ark.account.pubkey,
]);
const prevRelays: NDKTag[] = queryClient.getQueryData(["relay-personal"]);
// create new relay list if not exist
if (!prevRelays) {
@ -36,13 +36,13 @@ export function useRelaylist() {
await ark.createEvent({
kind: NDKKind.RelayList,
tags: [...prevRelays, ["r", relay, purpose ?? ""]],
tags: [...prevRelays, ["r", relayUrl, purpose ?? ""]],
});
// Optimistically update to the new value
queryClient.setQueryData(["relay-personal"], (prev: NDKTag[]) => [
...prev,
["r", relay, purpose ?? ""],
["r", relayUrl, purpose ?? ""],
]);
// Return a context object with the snapshotted value
@ -56,17 +56,14 @@ export function useRelaylist() {
});
const removeRelay = useMutation({
mutationFn: async (relay: NDKRelayUrl) => {
mutationFn: async (relay: WebSocket["url"]) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({
queryKey: ["relay-personal"],
});
// Snapshot the previous value
const prevRelays: NDKTag[] = queryClient.getQueryData([
"relays",
ark.account.pubkey,
]);
const prevRelays: NDKTag[] = queryClient.getQueryData(["relay-personal"]);
if (!prevRelays) return;