-
- {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}
))}