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) */} - setShowRelays(!showRelays)} - className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors" - > - {showRelays ? ( - - ) : ( - - )} - - {defaultRelays.length} - + {/* Relay Count (Dropdown) */} + + + + + + {connectedCount}/{defaultRelays.length} + + + + + {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) */} - {/* Expandable Relays */} - {showRelays && ( - - - {defaultRelays.map((relay) => ( - - - - {relay} - - - ))} - - - )} - {/* Expandable Query */} {showQuery && (
{authIcon.label}
{connIcon.label}