From a65b2d2f9510ece72813924c4927e018b3cbb626 Mon Sep 17 00:00:00 2001 From: Jure <44338+hoornet@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:33:31 +0200 Subject: [PATCH] Resolve naddr references to clickable named links Parse naddr bech32 references as a dedicated segment type with kind, pubkey, and identifier. NaddrName component fetches the referenced event and displays its title/name/d-tag. Clicking navigates to article view (kind 30023) or author profile (other kinds). Supports emoji sets, app listings, and other addressable content. --- src/components/feed/TextSegments.tsx | 76 ++++++++++++++++++++++++++-- src/lib/parsing.ts | 17 +++++-- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/components/feed/TextSegments.tsx b/src/components/feed/TextSegments.tsx index 1044db9..f02190d 100644 --- a/src/components/feed/TextSegments.tsx +++ b/src/components/feed/TextSegments.tsx @@ -1,8 +1,9 @@ -import { ReactNode } from "react"; -import { nip19 } from "@nostr-dev-kit/ndk"; +import { ReactNode, useState, useEffect } from "react"; +import { nip19, NDKFilter, NDKKind } from "@nostr-dev-kit/ndk"; import { useUIStore } from "../../stores/ui"; import { useProfile } from "../../hooks/useProfile"; import { ContentSegment } from "../../lib/parsing"; +import { getNDK, fetchWithTimeout } from "../../lib/nostr"; // Returns true if we handled the URL internally (njump.me interception). export function tryHandleUrlInternally(url: string): boolean { @@ -31,17 +32,66 @@ export function tryOpenNostrEntity(raw: string): boolean { return true; } if (decoded.type === "naddr") { - const { kind } = decoded.data as { kind: number; pubkey: string; identifier: string }; + const { kind, pubkey } = decoded.data as { kind: number; pubkey: string; identifier: string }; if (kind === 30023) { openArticle(raw); return true; } + // For other addressable kinds (app listings, emoji sets, etc.) + // open the author's profile as the best available in-app destination + if (pubkey) { + openProfile(pubkey); + return true; + } } - // note / nevent / other naddr kinds — fall through to njump.me + // note / nevent — fall through to njump.me } catch { /* invalid entity */ } return false; } +/** Resolves an naddr reference to a human-readable name by fetching the event. */ +function NaddrName({ kind, pubkey, identifier, fallback }: { kind: number; pubkey: string; identifier: string; fallback: string }) { + const [name, setName] = useState(null); + + useEffect(() => { + let cancelled = false; + (async () => { + try { + const filter: NDKFilter = { kinds: [kind as NDKKind], authors: [pubkey], "#d": [identifier], limit: 1 }; + const events = await fetchWithTimeout(getNDK(), filter, 5000); + if (cancelled) return; + const event = Array.from(events)[0]; + if (!event) return; + + // Helper: return string if non-empty, else undefined + const tagVal = (key: string) => { + const v = event.tags.find((t) => t[0] === key)?.[1]; + return typeof v === "string" && v.trim() ? v.trim() : undefined; + }; + + // Try tags first: title, name, d + let resolved = tagVal("title") || tagVal("name") || tagVal("d"); + + // If no tag found, try parsing content as JSON (some kinds store metadata in content) + if (!resolved && event.content) { + try { + const json = JSON.parse(event.content); + const candidate = json.display_name || json.name || json.title; + if (typeof candidate === "string" && candidate.trim()) { + resolved = candidate.trim(); + } + } catch { /* not JSON, ignore */ } + } + + if (resolved) setName(resolved); + } catch { /* keep fallback */ } + })(); + return () => { cancelled = true; }; + }, [kind, pubkey, identifier]); + + return <>{name || fallback}; +} + export function MentionName({ pubkey, fallback }: { pubkey?: string; fallback: string }) { const profile = useProfile(pubkey ?? ""); if (!pubkey) return <>{fallback}; @@ -97,6 +147,24 @@ export function renderTextSegments( ); break; + case "naddr": + elements.push( + { e.stopPropagation(); tryOpenNostrEntity(seg.value); }} + title={`Kind ${seg.naddrKind} — click to open`} + > + 🔗 + + + ); + break; case "hashtag": elements.push(