diff --git a/src/components/ProfileViewer.tsx b/src/components/ProfileViewer.tsx
index e8e564f..4d5fb72 100644
--- a/src/components/ProfileViewer.tsx
+++ b/src/components/ProfileViewer.tsx
@@ -1,27 +1,106 @@
-import { useState } from "react";
import { useProfile } from "@/hooks/useProfile";
import { UserName } from "./nostr/UserName";
import Nip05 from "./nostr/nip05";
import {
Copy,
- Check,
- ChevronDown,
- ChevronRight,
+ CopyCheck,
User as UserIcon,
- Circle,
Inbox,
Send,
+ Wifi,
+ Loader2,
+ WifiOff,
+ XCircle,
+ ShieldCheck,
+ ShieldAlert,
+ ShieldX,
+ ShieldQuestion,
+ Shield,
} from "lucide-react";
import { kinds, nip19 } from "nostr-tools";
import { useEventStore, useObservableMemo } from "applesauce-react/hooks";
import { getInboxes, getOutboxes } from "applesauce-core/helpers/mailboxes";
import { useCopy } from "../hooks/useCopy";
import { RichText } from "./nostr/RichText";
+import { RelayLink } from "./nostr/RelayLink";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "./ui/dropdown-menu";
+import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
+import { useRelayState } from "@/hooks/useRelayState";
+import type { RelayState } from "@/types/relay-state";
export interface ProfileViewerProps {
pubkey: string;
}
+// Helper functions for relay status icons (from EventDetailViewer)
+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;
+}
+
/**
* ProfileViewer - Detailed view for a user profile
* Shows profile metadata, inbox/outbox relays, and raw JSON
@@ -29,8 +108,8 @@ export interface ProfileViewerProps {
export function ProfileViewer({ pubkey }: ProfileViewerProps) {
const profile = useProfile(pubkey);
const eventStore = useEventStore();
- const [showInboxes, setShowInboxes] = useState(false);
- const [showOutboxes, setShowOutboxes] = useState(false);
+ const { copy, copied } = useCopy();
+ const { relays: relayStates } = useRelayState();
// Get mailbox relays (kind 10002)
const mailboxEvent = useObservableMemo(
@@ -48,11 +127,14 @@ export function ProfileViewer({ pubkey }: ProfileViewerProps) {
[eventStore, pubkey],
);
- const { copy, copied } = useCopy();
-
// Combine all relays (inbox + outbox) for nprofile
const allRelays = [...new Set([...inboxRelays, ...outboxRelays])];
+ // Calculate connection count for relay dropdown
+ const connectedCount = allRelays.filter(
+ (url) => relayStates[url]?.connectionState === "connected",
+ ).length;
+
// Generate npub or nprofile depending on relay availability
const identifier =
allRelays.length > 0
@@ -71,9 +153,10 @@ export function ProfileViewer({ pubkey }: ProfileViewerProps) {
onClick={() => copy(identifier)}
className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors truncate min-w-0"
title={identifier}
+ aria-label="Copy profile ID"
>
{copied ? (
-
+
) : (
)}
@@ -82,7 +165,7 @@ export function ProfileViewer({ pubkey }: ProfileViewerProps) {
- {/* Right: Profile icon and Relay counts */}
+ {/* Right: Profile icon and Relay dropdown */}
@@ -90,81 +173,93 @@ export function ProfileViewer({ pubkey }: ProfileViewerProps) {
{allRelays.length > 0 && (
- <>
- {inboxRelays.length > 0 && (
+
+
- )}
+
+
+ {allRelays.map((url) => {
+ const state = relayStates[url];
+ const connIcon = getConnectionIcon(state);
+ const authIcon = getAuthIcon(state);
+ const isInbox = inboxRelays.includes(url);
+ const isOutbox = outboxRelays.includes(url);
- {outboxRelays.length > 0 && (
-
- )}
- >
+ return (
+
+
+ {isInbox && (
+
+
+
+
+
+ Inbox
+
+
+ )}
+ {isOutbox && (
+
+
+
+
+
+ Outbox
+
+
+ )}
+
+
+ e.stopPropagation()}
+ >
+ {authIcon && (
+
+
+ {authIcon.icon}
+
+
+ {authIcon.label}
+
+
+ )}
+
+
+
+ {connIcon.icon}
+
+
+ {connIcon.label}
+
+
+
+
+ );
+ })}
+
+
)}
- {/* Expandable Inbox Relays */}
- {showInboxes && inboxRelays.length > 0 && (
-
-
- Inbox Relays
-
-
- {inboxRelays.map((relay) => (
-
-
-
- {relay}
-
-
- ))}
-
-
- )}
-
- {/* Expandable Outbox Relays */}
- {showOutboxes && outboxRelays.length > 0 && (
-
-
- Outbox Relays
-
-
- {outboxRelays.map((relay) => (
-
-
-
- {relay}
-
-
- ))}
-
-
- )}
-
{/* Profile Content */}
{!profile && !profileEvent && (
diff --git a/src/components/RelayViewer.tsx b/src/components/RelayViewer.tsx
index 2f5507c..6e50325 100644
--- a/src/components/RelayViewer.tsx
+++ b/src/components/RelayViewer.tsx
@@ -1,4 +1,4 @@
-import { Copy, Check } from "lucide-react";
+import { Copy, CopyCheck } from "lucide-react";
import { useRelayInfo } from "../hooks/useRelayInfo";
import { useCopy } from "../hooks/useCopy";
import { Button } from "./ui/button";
@@ -24,13 +24,13 @@ export function RelayViewer({ url }: RelayViewerProps) {
{url}