From a1d145905ccf536b758992ef123ce8e01e94ecaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Thu, 11 Dec 2025 11:16:47 +0100 Subject: [PATCH] feat: render relay lists --- TODO.md | 3 + src/components/EventDetailViewer.tsx | 6 +- src/components/nostr/RelayLink.tsx | 93 +++++++++++++++---- .../nostr/kinds/Kind10002DetailRenderer.tsx | 67 +++++++++++++ .../nostr/kinds/Kind10002Renderer.tsx | 69 ++++++++++++++ src/components/nostr/kinds/index.tsx | 2 + src/components/nostr/user-menu.tsx | 4 +- 7 files changed, 221 insertions(+), 23 deletions(-) create mode 100644 src/components/nostr/kinds/Kind10002DetailRenderer.tsx create mode 100644 src/components/nostr/kinds/Kind10002Renderer.tsx diff --git a/TODO.md b/TODO.md index 5dcd568..cb2744f 100644 --- a/TODO.md +++ b/TODO.md @@ -31,3 +31,6 @@ look into reconnecting on errors ## TODO: improve text rendering avoid inserting `br`, look into noStrudel's eol metadata + +## TODO: window crashes on unsupported kind event +## TODO: app-wide error boundary. splash crash screen. diff --git a/src/components/EventDetailViewer.tsx b/src/components/EventDetailViewer.tsx index 2e3e6b0..acd34be 100644 --- a/src/components/EventDetailViewer.tsx +++ b/src/components/EventDetailViewer.tsx @@ -6,6 +6,7 @@ import { Kind0DetailRenderer } from "./nostr/kinds/Kind0DetailRenderer"; 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 { KindBadge } from "./KindBadge"; import { Copy, @@ -32,6 +33,7 @@ export function EventDetailViewer({ pointer }: EventDetailViewerProps) { const event = useNostrEvent(pointer); const [showJson, setShowJson] = useState(false); const [showRelays, setShowRelays] = useState(false); + const { copy, copied } = useCopy(); // Loading state if (!event) { @@ -42,8 +44,6 @@ export function EventDetailViewer({ pointer }: EventDetailViewerProps) { ); } - const { copy, copied } = useCopy(); - // Get relays this event was seen on using applesauce const seenRelaysSet = getSeenRelays(event); const relays = seenRelaysSet ? Array.from(seenRelaysSet) : undefined; @@ -181,6 +181,8 @@ export function EventDetailViewer({ pointer }: EventDetailViewerProps) { ) : event.kind === kinds.Highlights ? ( + ) : event.kind === kinds.RelayList ? ( + ) : ( )} diff --git a/src/components/nostr/RelayLink.tsx b/src/components/nostr/RelayLink.tsx index 6078017..3b62ca3 100644 --- a/src/components/nostr/RelayLink.tsx +++ b/src/components/nostr/RelayLink.tsx @@ -1,27 +1,37 @@ -import { Circle, Inbox, Send } from "lucide-react"; +import { Inbox, Send } from "lucide-react"; import { useGrimoire } from "@/core/state"; +import { useRelayInfo } from "@/hooks/useRelayInfo"; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/components/ui/hover-card"; +import { cn } from "@/lib/utils"; export interface RelayLinkProps { url: string; read?: boolean; write?: boolean; - connected?: boolean; className?: string; + urlClassname?: string; + iconClassname?: string; } /** * RelayLink - Clickable relay URL component - * Displays relay URL with connection status indicator and read/write badges + * Displays relay URL with read/write badges and tooltips * Opens relay detail window on click */ export function RelayLink({ url, + urlClassname, + iconClassname, read = false, write = false, - connected = true, className, }: RelayLinkProps) { const { addWindow } = useGrimoire(); + const relayInfo = useRelayInfo(url); const handleClick = () => { addWindow("relay", { url }, `Relay ${url}`); @@ -29,29 +39,72 @@ export function RelayLink({ return (
-
- - {url} +
+ {relayInfo?.icon && ( + + )} + {url}
{read && ( -
- -
+ + +
+ +
+
+ e.stopPropagation()} + > +
+
Read / Inbox
+

+ This relay is used to read events. Your client will fetch + events from this relay when loading your feed or searching for + content. +

+
+
+
)} {write && ( -
- -
+ + +
+ +
+
+ e.stopPropagation()} + > +
+
Write / Outbox
+

+ This relay is used to publish events. When you create a post + or update your profile, it will be sent to this relay for + others to discover. +

+
+
+
)}
diff --git a/src/components/nostr/kinds/Kind10002DetailRenderer.tsx b/src/components/nostr/kinds/Kind10002DetailRenderer.tsx new file mode 100644 index 0000000..e9ea3c7 --- /dev/null +++ b/src/components/nostr/kinds/Kind10002DetailRenderer.tsx @@ -0,0 +1,67 @@ +import { NostrEvent } from "@/types/nostr"; +import { getInboxes, getOutboxes } from "applesauce-core/helpers/mailboxes"; +import { RelayLink } from "../RelayLink"; + +interface RelayWithMode { + url: string; + read: boolean; + write: boolean; +} + +/** + * Kind 10002 Detail Renderer - NIP-65 Relay List Metadata (Detail View) + * Shows full relay list with read/write indicators + */ +export function Kind10002DetailRenderer({ event }: { event: NostrEvent }) { + const inboxRelays = getInboxes(event); + const outboxRelays = getOutboxes(event); + + // Combine into unified list with read/write flags + const relayMap = new Map(); + + inboxRelays.forEach((url) => { + const existing = relayMap.get(url); + if (existing) { + existing.read = true; + } else { + relayMap.set(url, { url, read: true, write: false }); + } + }); + + outboxRelays.forEach((url) => { + const existing = relayMap.get(url); + if (existing) { + existing.write = true; + } else { + relayMap.set(url, { url, read: false, write: true }); + } + }); + + const allRelays = Array.from(relayMap.values()); + + if (allRelays.length === 0) { + return ( +
+ No relays configured +
+ ); + } + + return ( +
+ {/* +

Relay List ({allRelays.length})

+ */} + {allRelays.map((relay) => ( + + ))} +
+ ); +} diff --git a/src/components/nostr/kinds/Kind10002Renderer.tsx b/src/components/nostr/kinds/Kind10002Renderer.tsx new file mode 100644 index 0000000..3bfe980 --- /dev/null +++ b/src/components/nostr/kinds/Kind10002Renderer.tsx @@ -0,0 +1,69 @@ +import { BaseEventProps, BaseEventContainer } from "./BaseEventRenderer"; +import { getInboxes, getOutboxes } from "applesauce-core/helpers/mailboxes"; +import { RelayLink } from "../RelayLink"; + +interface RelayWithMode { + url: string; + read: boolean; + write: boolean; +} + +/** + * Kind 10002 Renderer - NIP-65 Relay List Metadata (Feed View) + * Shows relay list with read/write indicators + */ +export function Kind10002Renderer({ event }: BaseEventProps) { + const inboxRelays = getInboxes(event); + const outboxRelays = getOutboxes(event); + + // Combine into unified list with read/write flags + const relayMap = new Map(); + + inboxRelays.forEach((url) => { + const existing = relayMap.get(url); + if (existing) { + existing.read = true; + } else { + relayMap.set(url, { url, read: true, write: false }); + } + }); + + outboxRelays.forEach((url) => { + const existing = relayMap.get(url); + if (existing) { + existing.write = true; + } else { + relayMap.set(url, { url, read: false, write: true }); + } + }); + + const allRelays = Array.from(relayMap.values()); + + if (allRelays.length === 0) { + return ( + +
+ No relays configured +
+
+ ); + } + + return ( + +
+ {allRelays.map((relay) => ( + + ))} +
+
+ ); +} diff --git a/src/components/nostr/kinds/index.tsx b/src/components/nostr/kinds/index.tsx index c0ddc0a..e1b2b04 100644 --- a/src/components/nostr/kinds/index.tsx +++ b/src/components/nostr/kinds/index.tsx @@ -9,6 +9,7 @@ import { Kind22Renderer } from "./Kind22Renderer"; import { Kind1063Renderer } from "./Kind1063Renderer"; import { Kind9735Renderer } from "./Kind9735Renderer"; import { Kind9802Renderer } from "./Kind9802Renderer"; +import { Kind10002Renderer } from "./Kind10002Renderer"; import { Kind30023Renderer } from "./Kind30023Renderer"; import { NostrEvent } from "@/types/nostr"; import { BaseEventContainer, type BaseEventProps } from "./BaseEventRenderer"; @@ -30,6 +31,7 @@ const kindRenderers: Record> = { 1111: Kind1Renderer, // Post 9735: Kind9735Renderer, // Zap Receipt 9802: Kind9802Renderer, // Highlight + 10002: Kind10002Renderer, // Relay List Metadata (NIP-65) 30023: Kind30023Renderer, // Long-form Article }; diff --git a/src/components/nostr/user-menu.tsx b/src/components/nostr/user-menu.tsx index de7a641..7c90917 100644 --- a/src/components/nostr/user-menu.tsx +++ b/src/components/nostr/user-menu.tsx @@ -112,7 +112,9 @@ export default function UserMenu() { {relays.all.map((relay) => (