From 33614be77dc3d95fb36509aae108cdde91f7c3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Sun, 14 Dec 2025 00:33:25 +0100 Subject: [PATCH] ui: improve relays view in req viewer --- src/components/ReqViewer.tsx | 179 +++++++++++++++++++++++++++++------ 1 file changed, 148 insertions(+), 31 deletions(-) diff --git a/src/components/ReqViewer.tsx b/src/components/ReqViewer.tsx index 2b46e3e..37aef7b 100644 --- a/src/components/ReqViewer.tsx +++ b/src/components/ReqViewer.tsx @@ -5,16 +5,33 @@ import { Radio, FileText, Wifi, + WifiOff, + Loader2, + XCircle, + ShieldCheck, + ShieldAlert, + ShieldX, + ShieldQuestion, + Shield, Filter as FilterIcon, - Circle, } from "lucide-react"; import { Virtuoso } from "react-virtuoso"; import { useReqTimeline } from "@/hooks/useReqTimeline"; import { useGrimoire } from "@/core/state"; import { useProfile } from "@/hooks/useProfile"; +import { useRelayState } from "@/hooks/useRelayState"; import { FeedEvent } from "./nostr/Feed"; import { KindBadge } from "./KindBadge"; +import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "./ui/dropdown-menu"; +import { RelayLink } from "./nostr/RelayLink"; import type { NostrFilter } from "@/types/nostr"; +import type { RelayState } from "@/types/relay-state"; import { formatEventIds, formatDTags, @@ -30,6 +47,70 @@ const MemoizedFeedEvent = memo( (prev, next) => prev.event.id === next.event.id, ); +// Helper functions for relay status icons +function getConnectionIcon(relay: RelayState | undefined) { + if (!relay) { + return { + icon: , + label: "Unknown", + }; + } + + const iconMap = { + connected: { + icon: , + label: "Connected", + }, + connecting: { + icon: , + label: "Connecting", + }, + disconnected: { + icon: , + label: "Disconnected", + }, + error: { + icon: , + label: "Connection Error", + }, + }; + return iconMap[relay.connectionState]; +} + +function getAuthIcon(relay: RelayState | undefined) { + if (!relay || relay.authStatus === "none") { + return null; + } + + const iconMap = { + authenticated: { + icon: , + label: "Authenticated", + }, + challenge_received: { + icon: , + label: "Challenge Received", + }, + authenticating: { + icon: , + label: "Authenticating", + }, + failed: { + icon: , + label: "Authentication Failed", + }, + rejected: { + icon: , + label: "Authentication Rejected", + }, + none: { + icon: , + label: "No Authentication", + }, + }; + return iconMap[relay.authStatus] || iconMap.none; +} + interface ReqViewerProps { filter: NostrFilter; relays?: string[]; @@ -220,6 +301,7 @@ export default function ReqViewer({ nip05PTags, }: ReqViewerProps) { const { state } = useGrimoire(); + const { relays: relayStates } = useRelayState(); // NIP-05 resolution already happened in argParser before window creation // The filter prop already contains resolved pubkeys @@ -232,6 +314,15 @@ export default function ReqViewer({ ? state.activeAccount.relays.inbox.map((r) => r.url) : ["wss://theforest.nostr1.com"]); + // Get relay state for each relay and calculate connected count + const relayStatesForReq = defaultRelays.map((url) => ({ + url, + state: relayStates[url], + })); + const connectedCount = relayStatesForReq.filter( + (r) => r.state?.connectionState === "connected", + ).length; + // Streaming is the default behavior, closeOnEose inverts it const stream = !closeOnEose; @@ -242,7 +333,6 @@ export default function ReqViewer({ { limit: filter.limit || 50, stream }, ); - const [showRelays, setShowRelays] = useState(false); const [showQuery, setShowQuery] = useState(false); return ( @@ -291,19 +381,62 @@ export default function ReqViewer({ {events.length} - {/* Relay Count (Clickable) */} - + {/* Relay Count (Dropdown) */} + + + + + + {relayStatesForReq.map(({ url, state }) => { + const connIcon = getConnectionIcon(state); + const authIcon = getAuthIcon(state); + + return ( + + +
e.stopPropagation()} + > + {authIcon && ( + + +
{authIcon.icon}
+
+ +

{authIcon.label}

+
+
+ )} + + + +
{connIcon.icon}
+
+ +

{connIcon.label}

+
+
+
+
+ ); + })} +
+
{/* Query (Clickable) */}