From 9496af6273ca65087dd3ae4b66d25b70ba9076a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Sun, 14 Dec 2025 16:41:43 +0100 Subject: [PATCH] feat: better generic event detail relay list and JSON viewer --- src/components/CommandLauncher.tsx | 146 ++++++++--------- src/components/EventDetailViewer.tsx | 230 +++++++++++++++++++-------- src/components/TabBar.tsx | 4 +- src/components/nostr/user-menu.tsx | 6 +- src/core/state.ts | 6 +- src/lib/migrations.ts | 4 +- 6 files changed, 253 insertions(+), 143 deletions(-) diff --git a/src/components/CommandLauncher.tsx b/src/components/CommandLauncher.tsx index a82ea39..06c76af 100644 --- a/src/components/CommandLauncher.tsx +++ b/src/components/CommandLauncher.tsx @@ -109,84 +109,84 @@ export default function CommandLauncher({ Command Launcher - -
- + +
+ - - - {commandName - ? `No command found: ${commandName}` - : "Start typing..."} - + + + {commandName + ? `No command found: ${commandName}` + : "Start typing..."} + - {categories.map((category) => ( - - {filteredCommands - .filter(([_, cmd]) => cmd.category === category) - .map(([name, cmd]) => { - const isExactMatch = name === commandName; - return ( - handleSelect(name)} - className="command-item" - data-exact-match={isExactMatch} - > -
-
- {name} - {cmd.synopsis !== name && ( - - {cmd.synopsis.replace(name, "").trim()} - - )} - {isExactMatch && ( - - ✓ - - )} -
-
- {cmd.description.split(".")[0]} -
+ {categories.map((category) => ( + + {filteredCommands + .filter(([_, cmd]) => cmd.category === category) + .map(([name, cmd]) => { + const isExactMatch = name === commandName; + return ( + handleSelect(name)} + className="command-item" + data-exact-match={isExactMatch} + > +
+
+ {name} + {cmd.synopsis !== name && ( + + {cmd.synopsis.replace(name, "").trim()} + + )} + {isExactMatch && ( + + ✓ + + )}
- - ); - })} - - ))} - +
+ {cmd.description.split(".")[0]} +
+
+
+ ); + })} +
+ ))} + -
-
- ↑↓ navigate - execute - esc close -
- {recognizedCommand && ( -
Ready to execute
- )} +
+
+ ↑↓ navigate + execute + esc close
+ {recognizedCommand && ( +
Ready to execute
+ )}
- - - +
+ + + ); } diff --git a/src/components/EventDetailViewer.tsx b/src/components/EventDetailViewer.tsx index 9b652fe..7eda8fa 100644 --- a/src/components/EventDetailViewer.tsx +++ b/src/components/EventDetailViewer.tsx @@ -7,23 +7,103 @@ import { Kind3DetailView } from "./nostr/kinds/Kind3Renderer"; import { Kind30023DetailRenderer } from "./nostr/kinds/Kind30023DetailRenderer"; import { Kind9802DetailRenderer } from "./nostr/kinds/Kind9802DetailRenderer"; import { Kind10002DetailRenderer } from "./nostr/kinds/Kind10002DetailRenderer"; +import { JsonViewer } from "./JsonViewer"; +import { RelayLink } from "./nostr/RelayLink"; import { Copy, - Check, - ChevronDown, - ChevronRight, + CopyCheck, FileJson, Wifi, - Circle, + Loader2, + WifiOff, + XCircle, + ShieldCheck, + ShieldAlert, + ShieldX, + ShieldQuestion, + Shield, } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "./ui/dropdown-menu"; +import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; import { nip19, kinds } from "nostr-tools"; import { useCopy } from "../hooks/useCopy"; import { getSeenRelays } from "applesauce-core/helpers/relays"; +import { useRelayState } from "@/hooks/useRelayState"; +import type { RelayState } from "@/types/relay-state"; export interface EventDetailViewerProps { pointer: EventPointer | AddressPointer; } +// Helper functions for relay status icons (from ReqViewer) +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; +} + /** * EventDetailViewer - Detailed view for a single event * Shows compact metadata header and rendered content @@ -31,9 +111,8 @@ export interface EventDetailViewerProps { export function EventDetailViewer({ pointer }: EventDetailViewerProps) { const event = useNostrEvent(pointer); const [showJson, setShowJson] = useState(false); - const [showRelays, setShowRelays] = useState(false); const { copy: copyBech32, copied: copiedBech32 } = useCopy(); - const { copy: copyJson, copied: copiedJson } = useCopy(); + const { relays: relayStates } = useRelayState(); // Loading state if (!event) { @@ -72,6 +151,17 @@ export function EventDetailViewer({ pointer }: EventDetailViewerProps) { // minute: "2-digit", // }); + // Get relay state for each relay + const relayStatesForEvent = relays + ? relays.map((url) => ({ + url, + state: relayStates[url], + })) + : []; + const connectedCount = relayStatesForEvent.filter( + (r) => r.state?.connectionState === "connected", + ).length; + return (
{/* Compact Header - Single Line */} @@ -81,9 +171,10 @@ export function EventDetailViewer({ pointer }: EventDetailViewerProps) { onClick={() => copyBech32(bech32Id)} className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors truncate min-w-0" title={bech32Id} + aria-label="Copy event ID" > {copiedBech32 ? ( - + ) : ( )} @@ -94,72 +185,79 @@ export function EventDetailViewer({ pointer }: EventDetailViewerProps) { {/* Right: Relay Count and JSON Toggle */}
+ {/* Relay Dropdown */} {relays && relays.length > 0 && ( - + + + + + + {relayStatesForEvent.map(({ url, state }) => { + const connIcon = getConnectionIcon(state); + const authIcon = getAuthIcon(state); + + return ( + + +
e.stopPropagation()} + > + {authIcon && ( + + +
{authIcon.icon}
+
+ +

{authIcon.label}

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

{connIcon.label}

+
+
+
+
+ ); + })} +
+
)} + + {/* JSON Toggle */}
- {/* Expandable Relays */} - {showRelays && relays && relays.length > 0 && ( -
-
- {relays.map((relay) => ( -
- - - {relay} - -
- ))} -
-
- )} - - {/* Expandable JSON */} - {showJson && ( -
-
- -
-
-            {JSON.stringify(event, null, 2)}
-          
-
- )} - {/* Rendered Content - Focus Here */}
{event.kind === kinds.Metadata ? ( @@ -176,6 +274,14 @@ export function EventDetailViewer({ pointer }: EventDetailViewerProps) { )}
+ + {/* JSON Viewer Dialog */} +
); } diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx index c32cd3b..b897d37 100644 --- a/src/components/TabBar.tsx +++ b/src/components/TabBar.tsx @@ -30,7 +30,9 @@ export function TabBar() { : "text-muted-foreground hover:text-foreground hover:bg-muted", )} > - {ws.label && ws.label.trim() ? `${ws.number} ${ws.label}` : ws.number} + {ws.label && ws.label.trim() + ? `${ws.number} ${ws.label}` + : ws.number} ))}