From b5e1cffc914d58e376f88928bd71c9fb6d2bbfa6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Dec 2025 17:53:24 +0000 Subject: [PATCH] fix: add EOSE indicator, mute all icons, and fix relay URL normalization bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical fixes for ReqViewer relay state accuracy: 1. **URL Normalization Fix** (fixes mismatch with CONN): - Added normalizeRelayURL to normalize all relay URLs in finalRelays - RelayStateManager normalizes URLs (adds trailing slash, lowercase) but finalRelays did not, causing lookup failures in relayStates - Now normalizedRelays is used for all state lookups and passed to useReqTimelineEnhanced to ensure consistency - This fixes the bug where ReqViewer showed different connected relay counts than CONN viewer 2. **EOSE Indicator**: - Added back EOSE indicator to relay dropdown (was removed in UI redesign) - Shows subtle "EOSE" text when relay has sent End of Stored Events - Includes tooltip explaining "End of stored events received" 3. **Muted Icons** (per user request for subtlety): - Type indicators: blue-500/purple-500 → muted-foreground/60 - Strategy header icons: all → muted-foreground/60 - Section headers: green-500 → muted-foreground - Connection icons: green-500/yellow-500/red-500 → /70 opacity variants - Auth icons: same color reduction for consistency - Maintains semantic meaning while reducing visual noise All 639 tests passing. --- src/components/ReqViewer.tsx | 355 +++++++++++++++++++-------------- src/lib/relay-status-utils.tsx | 20 +- 2 files changed, 214 insertions(+), 161 deletions(-) diff --git a/src/components/ReqViewer.tsx b/src/components/ReqViewer.tsx index 91a6d79..6b4aa7a 100644 --- a/src/components/ReqViewer.tsx +++ b/src/components/ReqViewer.tsx @@ -72,12 +72,12 @@ import { useCopy } from "@/hooks/useCopy"; import { CodeCopyButton } from "@/components/CodeCopyButton"; import { SyntaxHighlight } from "@/components/SyntaxHighlight"; import { getConnectionIcon, getAuthIcon } from "@/lib/relay-status-utils"; +import { normalizeRelayURL } from "@/lib/relay-url"; import { getStatusText, getStatusTooltip, getStatusColor, shouldAnimate, - getRelayStateBadge, } from "@/lib/req-state-machine"; import { resolveFilterAliases, getTagValues } from "@/lib/nostr-utils"; import { useNostrEvent } from "@/hooks/useNostrEvent"; @@ -712,7 +712,6 @@ export default function ReqViewer({ const { relays: selectedRelays, reasoning, - isOptimized, phase: relaySelectionPhase, } = useOutboxRelays(resolvedFilter, outboxOptions); @@ -733,6 +732,20 @@ export default function ReqViewer({ return selectedRelays; }, [relays, relaySelectionPhase, selectedRelays]); + // Normalize relay URLs for consistent lookups in relayStates + // RelayStateManager normalizes all URLs (adds trailing slash, lowercase, etc.) + // so we must normalize here too to match the keys in relayStates + const normalizedRelays = useMemo(() => { + return finalRelays.map((url) => { + try { + return normalizeRelayURL(url); + } catch (err) { + console.warn("Failed to normalize relay URL:", url, err); + return url; // Fallback to original URL if normalization fails + } + }); + }, [finalRelays]); + // Streaming is the default behavior, closeOnEose inverts it const stream = !closeOnEose; @@ -746,7 +759,7 @@ export default function ReqViewer({ } = useReqTimelineEnhanced( `req-${JSON.stringify(filter)}-${closeOnEose}`, resolvedFilter, - finalRelays, + normalizedRelays, { limit: resolvedFilter.limit || 50, stream }, ); @@ -978,16 +991,22 @@ export default function ReqViewer({ - {/* Relay Status - shows ALL queried relays (outbox + fallback or explicit) */} -
-
- {!relays && isOptimized ? ( + {/* Header: Relay Selection Strategy */} +
+
+ {relays ? ( + // Explicit relays <> - Relay Selection{" "} - - ( + + Explicit Relays ({finalRelays.length}) + + ) : reasoning && reasoning.some((r) => !r.isFallback) ? ( + // NIP-65 Outbox + <> + + - ) + NIP-65 Outbox + {" "} + ({finalRelays.length} relays) ) : ( - "Relay Status" + // Fallback relays + <> + + Fallback Relays ({finalRelays.length}) + )}
+
- {/* Always show ALL relays from finalRelays (what's actually queried) */} -
- {finalRelays.map((url) => { - const globalState = relayStates[url]; - const reqState = reqRelayStates.get(url); - const connIcon = getConnectionIcon(globalState); - const authIcon = getAuthIcon(globalState); - const badge = reqState - ? getRelayStateBadge(reqState) - : null; + {(() => { + // Group relays by connection status + // Use normalizedRelays for lookups to match RelayStateManager's keys + const onlineRelays: string[] = []; + const disconnectedRelays: string[] = []; - // Find NIP-65 info for this relay (if using outbox) - const nip65Info = reasoning?.find((r) => r.relay === url); + normalizedRelays.forEach((url) => { + const globalState = relayStates[url]; + const isConnected = + globalState?.connectionState === "connected"; - // Determine relay type - const relayType = relays - ? "explicit" // Explicitly specified relays - : nip65Info && !nip65Info.isFallback - ? "outbox" // NIP-65 outbox relay - : "fallback"; // Fallback relay + if (isConnected) { + onlineRelays.push(url); + } else { + disconnectedRelays.push(url); + } + }); - // Type indicator icon - const typeIcon = { - explicit: ( - - - - - Explicit relay - - ), - outbox: ( - - - - - NIP-65 Outbox relay - - ), - fallback: ( - - - - - Fallback relay - - ), - }[relayType]; + const renderRelay = (url: string) => { + const globalState = relayStates[url]; + const reqState = reqRelayStates.get(url); + const connIcon = getConnectionIcon(globalState); + const authIcon = getAuthIcon(globalState); - return ( -
- -
- {/* Relay type indicator */} - {typeIcon} + // Find NIP-65 info for this relay (if using outbox) + const nip65Info = reasoning?.find((r) => r.relay === url); - {/* Event count */} - {reqState && reqState.eventCount > 0 && ( - - -
- - - {reqState.eventCount} - -
-
- - {reqState.eventCount} events received - -
- )} + // Determine relay type + const relayType = relays + ? "explicit" + : nip65Info && !nip65Info.isFallback + ? "outbox" + : "fallback"; - {/* Subscription state badge */} - {badge && ( - - {badge.text} - - )} + // Type indicator icon (smaller, on left) + const typeIcon = { + explicit: ( + + ), + outbox: ( + + ), + fallback: ( + + ), + }[relayType]; - {/* NIP-65 inbox/outbox indicators (if available) */} - {nip65Info && nip65Info.readers.length > 0 && ( - - -
- - - {nip65Info.readers.length} - -
-
- - Inbox relay for {nip65Info.readers.length}{" "} - author - {nip65Info.readers.length !== 1 ? "s" : ""} - -
- )} - {nip65Info && nip65Info.writers.length > 0 && ( - - -
- - - {nip65Info.writers.length} - -
-
- - Outbox relay for {nip65Info.writers.length}{" "} - author - {nip65Info.writers.length !== 1 ? "s" : ""} - -
- )} + return ( +
+ {/* Type icon on left */} + {typeIcon} - {/* Auth icon */} - {authIcon && ( - - -
- {authIcon.icon} -
-
- -

{authIcon.label}

-
-
- )} + {/* Relay URL */} + - {/* Connection icon */} + {/* Right side: stats and status */} +
+ {/* Event count */} + {reqState && reqState.eventCount > 0 && ( -
{connIcon.icon}
+
+ + + {reqState.eventCount} + +
-

{connIcon.label}

+ {reqState.eventCount} events received
-
+ )} + + {/* EOSE indicator */} + {reqState && reqState.subscriptionState === "eose" && ( + + +
+ + EOSE + +
+
+ + End of stored events received + +
+ )} + + {/* NIP-65 inbox/outbox indicators (if available) */} + {nip65Info && nip65Info.readers.length > 0 && ( + + +
+ + + {nip65Info.readers.length} + +
+
+ + Inbox for {nip65Info.readers.length} author + {nip65Info.readers.length !== 1 ? "s" : ""} + +
+ )} + {nip65Info && nip65Info.writers.length > 0 && ( + + +
+ + + {nip65Info.writers.length} + +
+
+ + Outbox for {nip65Info.writers.length} author + {nip65Info.writers.length !== 1 ? "s" : ""} + +
+ )} + + {/* Auth icon */} + {authIcon && ( + + +
{authIcon.icon}
+
+ +

{authIcon.label}

+
+
+ )} + + {/* Connection icon */} + + +
{connIcon.icon}
+
+ +

{connIcon.label}

+
+
- ); - })} -
-
+
+ ); + }; + + return ( + <> + {/* Online Section */} + {onlineRelays.length > 0 && ( +
+
+ Online ({onlineRelays.length}) +
+ {onlineRelays.map(renderRelay)} +
+ )} + + {/* Disconnected Section */} + {disconnectedRelays.length > 0 && ( +
+
+ Disconnected ({disconnectedRelays.length}) +
+ {disconnectedRelays.map(renderRelay)} +
+ )} + + ); + })()} diff --git a/src/lib/relay-status-utils.tsx b/src/lib/relay-status-utils.tsx index df4e2b7..506db29 100644 --- a/src/lib/relay-status-utils.tsx +++ b/src/lib/relay-status-utils.tsx @@ -24,19 +24,19 @@ export function getConnectionIcon(relay: RelayState | undefined) { const iconMap = { connected: { - icon: , + icon: , label: "Connected", }, connecting: { - icon: , + icon: , label: "Connecting", }, disconnected: { - icon: , + icon: , label: "Disconnected", }, error: { - icon: , + icon: , label: "Connection Error", }, }; @@ -54,27 +54,27 @@ export function getAuthIcon(relay: RelayState | undefined) { const iconMap = { authenticated: { - icon: , + icon: , label: "Authenticated", }, challenge_received: { - icon: , + icon: , label: "Challenge Received", }, authenticating: { - icon: , + icon: , label: "Authenticating", }, failed: { - icon: , + icon: , label: "Authentication Failed", }, rejected: { - icon: , + icon: , label: "Authentication Rejected", }, none: { - icon: , + icon: , label: "No Authentication", }, };