mirror of
https://github.com/lumehq/lume.git
synced 2025-10-04 20:44:38 +02:00
feat: fix relay manager
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
import { useRelaylist } from "@lume/ark";
|
import { User, useRelaylist } from "@lume/ark";
|
||||||
import { PlusIcon, ShareIcon } from "@lume/icons";
|
import { PlusIcon, SearchIcon } from "@lume/icons";
|
||||||
import { normalizeRelayUrl } from "nostr-fetch";
|
import { normalizeRelayUrl } from "nostr-fetch";
|
||||||
import { Link } from "react-router-dom";
|
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 domain = new URL(url).hostname;
|
||||||
const { connectRelay } = useRelaylist();
|
const { connectRelay } = useRelaylist();
|
||||||
|
|
||||||
@@ -18,19 +18,35 @@ export function RelayItem({ url }: { url: string }) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex items-center gap-2">
|
<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
|
<Link
|
||||||
to={`/relays/${domain}/`}
|
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
|
Inspect
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => connectRelay.mutate(normalizeRelayUrl(url))}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { useArk, useRelay } from "@lume/ark";
|
import { useArk, useRelaylist } from "@lume/ark";
|
||||||
import { LoaderIcon, PlusIcon, ShareIcon } from "@lume/icons";
|
import { LoaderIcon, PlusIcon, ShareIcon } from "@lume/icons";
|
||||||
import { User } from "@lume/ui";
|
import { User } from "@lume/ui";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
@@ -7,7 +7,7 @@ import { VList } from "virtua";
|
|||||||
|
|
||||||
export function RelayList() {
|
export function RelayList() {
|
||||||
const ark = useArk();
|
const ark = useArk();
|
||||||
const { connectRelay } = useRelay();
|
const { connectRelay } = useRelaylist();
|
||||||
const { status, data } = useQuery({
|
const { status, data } = useQuery({
|
||||||
queryKey: ["relays"],
|
queryKey: ["relays"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
@@ -1,14 +1,15 @@
|
|||||||
import { useArk } from "@lume/ark";
|
import { useArk, useRelaylist } from "@lume/ark";
|
||||||
import { CancelIcon, RefreshIcon } from "@lume/icons";
|
import { CancelIcon, LoaderIcon, RefreshIcon } from "@lume/icons";
|
||||||
import { cn } from "@lume/utils";
|
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 { useQuery } from "@tanstack/react-query";
|
||||||
import { RelayForm } from "./relayForm";
|
import { RelayForm } from "./relayForm";
|
||||||
|
|
||||||
export function RelaySidebar({ className }: { className?: string }) {
|
export function RelaySidebar({ className }: { className?: string }) {
|
||||||
const ark = useArk();
|
const ark = useArk();
|
||||||
|
|
||||||
const { status, data, refetch } = useQuery({
|
const { removeRelay } = useRelaylist();
|
||||||
|
const { status, data, isRefetching, refetch } = useQuery({
|
||||||
queryKey: ["relay-personal"],
|
queryKey: ["relay-personal"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const event = await ark.getEventByFilter({
|
const event = await ark.getEventByFilter({
|
||||||
@@ -16,12 +17,15 @@ export function RelaySidebar({ className }: { className?: string }) {
|
|||||||
kinds: [NDKKind.RelayList],
|
kinds: [NDKKind.RelayList],
|
||||||
authors: [ark.account.pubkey],
|
authors: [ark.account.pubkey],
|
||||||
},
|
},
|
||||||
|
cache: NDKSubscriptionCacheUsage.ONLY_RELAY,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!event) return [];
|
if (!event) return [];
|
||||||
return event.tags.filter((tag) => tag[0] === "r");
|
return event.tags.filter((tag) => tag[0] === "r");
|
||||||
},
|
},
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
staleTime: Infinity,
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentRelays = new Set(
|
const currentRelays = new Set(
|
||||||
@@ -42,17 +46,19 @@ export function RelaySidebar({ className }: { className?: string }) {
|
|||||||
onClick={() => refetch()}
|
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 px-3 mt-3">
|
<div className="flex flex-col gap-2 px-3 mt-3">
|
||||||
{status === "pending" ? (
|
{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 ? (
|
) : !data.length ? (
|
||||||
<div className="flex items-center justify-center w-full h-20 rounded-xl bg-neutral-50 dark:bg-neutral-950">
|
<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">
|
<p className="text-sm font-medium">Empty.</p>
|
||||||
You not have personal relay list yet
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
data.map((item) => (
|
data.map((item) => (
|
||||||
@@ -73,7 +79,10 @@ export function RelaySidebar({ className }: { className?: string }) {
|
|||||||
</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[1].replace("wss://", "").replace("ws://", "")}
|
{item[1]
|
||||||
|
.replace("wss://", "")
|
||||||
|
.replace("ws://", "")
|
||||||
|
.replace("/", "")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex items-center gap-2">
|
<div className="inline-flex items-center gap-2">
|
||||||
@@ -84,9 +93,10 @@ export function RelaySidebar({ className }: { className?: string }) {
|
|||||||
) : null}
|
) : null}
|
||||||
<button
|
<button
|
||||||
type="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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,18 +1,25 @@
|
|||||||
import { useArk } from "@lume/ark";
|
import { useArk } from "@lume/ark";
|
||||||
import { LoaderIcon, PlusIcon, ShareIcon } from "@lume/icons";
|
import { LoaderIcon } from "@lume/icons";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { VList } from "virtua";
|
import { VList } from "virtua";
|
||||||
import { RelayItem } from "./components/relayItem";
|
import { RelayItem } from "./components/relayItem";
|
||||||
|
|
||||||
export function RelayFollowsScreen() {
|
export function RelayFollowsScreen() {
|
||||||
const ark = useArk();
|
const ark = useArk();
|
||||||
const { isLoading, data: relays } = useQuery({
|
const {
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
data: relays,
|
||||||
|
} = useQuery({
|
||||||
queryKey: ["relay-follows"],
|
queryKey: ["relay-follows"],
|
||||||
queryFn: async ({ signal }: { signal: AbortSignal }) => {
|
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,
|
refetchOnMount: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading) {
|
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 (
|
return (
|
||||||
<VList itemSize={49}>
|
<VList itemSize={49}>
|
||||||
{relays.map((item: string) => (
|
{[...relays].map(([key, value]) => (
|
||||||
<RelayItem key={item} url={item} />
|
<RelayItem key={key} url={key} users={value} />
|
||||||
))}
|
))}
|
||||||
</VList>
|
</VList>
|
||||||
);
|
);
|
||||||
|
@@ -14,6 +14,7 @@ export function RelayGlobalScreen() {
|
|||||||
},
|
},
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
@@ -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, {
|
const event = await this.ndk.fetchEvent(filter, {
|
||||||
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
|
cacheUsage: cache || NDKSubscriptionCacheUsage.CACHE_FIRST,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!event) return null;
|
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 fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk));
|
||||||
const connectedRelays = this.ndk.pool
|
const connectedRelays = this.ndk.pool
|
||||||
.connectedRelays()
|
.connectedRelays()
|
||||||
@@ -367,15 +370,21 @@ export class Ark {
|
|||||||
},
|
},
|
||||||
{ kinds: [NDKKind.RelayList] },
|
{ kinds: [NDKKind.RelayList] },
|
||||||
1,
|
1,
|
||||||
|
{ abortSignal: signal },
|
||||||
);
|
);
|
||||||
|
|
||||||
for await (const { author, events } of relayEvents) {
|
console.log(relayEvents);
|
||||||
if (events[0]) {
|
|
||||||
for (const tag of events[0].tags) {
|
|
||||||
const users = relayMap.get(tag[1]);
|
|
||||||
|
|
||||||
if (!users) relayMap.set(tag[1], [author]);
|
for await (const { author, events } of relayEvents) {
|
||||||
users.push(author);
|
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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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 { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { normalizeRelayUrl } from "nostr-fetch";
|
||||||
import { useArk } from "./useArk";
|
import { useArk } from "./useArk";
|
||||||
|
|
||||||
export function useRelaylist() {
|
export function useRelaylist() {
|
||||||
@@ -8,7 +9,7 @@ export function useRelaylist() {
|
|||||||
|
|
||||||
const connectRelay = useMutation({
|
const connectRelay = useMutation({
|
||||||
mutationFn: async (
|
mutationFn: async (
|
||||||
relay: NDKRelayUrl,
|
relay: WebSocket["url"],
|
||||||
purpose?: "read" | "write" | undefined,
|
purpose?: "read" | "write" | undefined,
|
||||||
) => {
|
) => {
|
||||||
// Cancel any outgoing refetches
|
// Cancel any outgoing refetches
|
||||||
@@ -16,11 +17,10 @@ export function useRelaylist() {
|
|||||||
queryKey: ["relay-personal"],
|
queryKey: ["relay-personal"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const relayUrl = normalizeRelayUrl(relay);
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const prevRelays: NDKTag[] = queryClient.getQueryData([
|
const prevRelays: NDKTag[] = queryClient.getQueryData(["relay-personal"]);
|
||||||
"relays",
|
|
||||||
ark.account.pubkey,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// create new relay list if not exist
|
// create new relay list if not exist
|
||||||
if (!prevRelays) {
|
if (!prevRelays) {
|
||||||
@@ -36,13 +36,13 @@ export function useRelaylist() {
|
|||||||
|
|
||||||
await ark.createEvent({
|
await ark.createEvent({
|
||||||
kind: NDKKind.RelayList,
|
kind: NDKKind.RelayList,
|
||||||
tags: [...prevRelays, ["r", relay, purpose ?? ""]],
|
tags: [...prevRelays, ["r", relayUrl, purpose ?? ""]],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Optimistically update to the new value
|
// Optimistically update to the new value
|
||||||
queryClient.setQueryData(["relay-personal"], (prev: NDKTag[]) => [
|
queryClient.setQueryData(["relay-personal"], (prev: NDKTag[]) => [
|
||||||
...prev,
|
...prev,
|
||||||
["r", relay, purpose ?? ""],
|
["r", relayUrl, purpose ?? ""],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Return a context object with the snapshotted value
|
// Return a context object with the snapshotted value
|
||||||
@@ -56,17 +56,14 @@ export function useRelaylist() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const removeRelay = useMutation({
|
const removeRelay = useMutation({
|
||||||
mutationFn: async (relay: NDKRelayUrl) => {
|
mutationFn: async (relay: WebSocket["url"]) => {
|
||||||
// Cancel any outgoing refetches
|
// Cancel any outgoing refetches
|
||||||
await queryClient.cancelQueries({
|
await queryClient.cancelQueries({
|
||||||
queryKey: ["relay-personal"],
|
queryKey: ["relay-personal"],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const prevRelays: NDKTag[] = queryClient.getQueryData([
|
const prevRelays: NDKTag[] = queryClient.getQueryData(["relay-personal"]);
|
||||||
"relays",
|
|
||||||
ark.account.pubkey,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!prevRelays) return;
|
if (!prevRelays) return;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user