fix: pass full EventPointer with relay hints through embed chain

The previous commit fixed q-tag parsing in chat adapters, but the
RichText/Mention chain was also discarding relay hints from nevent.

EventEmbed was only extracting `pointer.id` and passing it as a string
to QuotedEvent, losing the relay hints. Same issue in EmbeddedEvent.

Changes:
- Update QuotedEvent props: eventPointer instead of eventId string
- Update EmbeddedEvent props: eventPointer instead of eventId string
- Update EventEmbed to pass full pointer to QuotedEvent
- Update MarkdownContent to pass full pointers for note/nevent
- Update EventRefList to pass full pointer instead of just ID
- Update HighlightDetailRenderer to pass full eventPointer
- Update RepostRenderer to extract e-tag as EventPointer with hints

Now nevent mentions in content like nostr:nevent1... correctly use
the relay hints from the bech32 encoding when fetching the event.
This commit is contained in:
Claude
2026-01-22 12:38:24 +00:00
parent 2a9bc9458f
commit 5918fe59d5
7 changed files with 51 additions and 49 deletions

View File

@@ -1,16 +1,15 @@
import type { EventPointer, AddressPointer } from "nostr-tools/nip19";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { KindRenderer } from "./kinds";
import { EventCardSkeleton } from "@/components/ui/skeleton";
interface EmbeddedEventProps {
/** Event ID string for regular events */
eventId?: string;
/** AddressPointer for addressable/replaceable events */
addressPointer?: { kind: number; pubkey: string; identifier: string };
/** EventPointer with optional relay hints for regular events */
eventPointer?: EventPointer;
/** AddressPointer for addressable/replaceable events (includes relay hints) */
addressPointer?: AddressPointer;
/** Callback when user clicks to open the event in new window */
onOpen?: (
id: string | { kind: number; pubkey: string; identifier: string },
) => void;
onOpen?: (id: string | AddressPointer) => void;
/** Optional loading fallback */
loadingFallback?: React.ReactNode;
/** Optional className for container */
@@ -20,18 +19,19 @@ interface EmbeddedEventProps {
/**
* Reusable component for embedding Nostr events
* Handles loading state and displays the embedded event using KindRenderer
* Passes full pointer (including relay hints) for proper event resolution
*/
export function EmbeddedEvent({
eventId,
eventPointer,
addressPointer,
onOpen,
loadingFallback,
className = "my-4 border border-muted rounded overflow-hidden",
}: EmbeddedEventProps) {
// Determine pointer to use
const pointer = eventId || addressPointer;
// Determine pointer to use - full pointer preserves relay hints
const pointer = eventPointer || addressPointer;
// Load the event
// Load the event - passes full pointer with relay hints to useNostrEvent
const event = useNostrEvent(pointer);
// If event loaded, render it
@@ -50,19 +50,18 @@ export function EmbeddedEvent({
// Default loading state - show clickable link if onOpen provided
if (onOpen && pointer) {
const displayText =
typeof eventId === "string"
? `@${eventId.slice(0, 8)}...`
: addressPointer
? `@${addressPointer.identifier || addressPointer.kind}`
: "@event";
const displayText = eventPointer
? `@${eventPointer.id.slice(0, 8)}...`
: addressPointer
? `@${addressPointer.identifier || addressPointer.kind}`
: "@event";
return (
<a
href="#"
onClick={(e) => {
e.preventDefault();
onOpen(pointer);
onOpen(eventPointer?.id || addressPointer!);
}}
className="inline-flex items-center gap-1 text-accent underline decoration-dotted break-all"
>

View File

@@ -57,9 +57,10 @@ function NostrMention({ href }: { href: string }) {
</span>
);
case "note":
// note is just an event ID, wrap in EventPointer
return (
<EmbeddedEvent
eventId={parsed.data}
eventPointer={{ id: parsed.data }}
onOpen={(id) => {
addWindow(
"open",
@@ -70,9 +71,10 @@ function NostrMention({ href }: { href: string }) {
/>
);
case "nevent":
// nevent includes full EventPointer with relay hints
return (
<EmbeddedEvent
eventId={parsed.data.id}
eventPointer={parsed.data}
onOpen={(id) => {
addWindow(
"open",

View File

@@ -1,4 +1,5 @@
import { useState } from "react";
import type { EventPointer, AddressPointer } from "nostr-tools/nip19";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { KindRenderer } from "./kinds";
import { UserName } from "./UserName";
@@ -7,14 +8,12 @@ import { cn } from "@/lib/utils";
import { CompactQuoteSkeleton } from "@/components/ui/skeleton";
interface QuotedEventProps {
/** Event ID string for regular events */
eventId?: string;
/** AddressPointer for addressable/replaceable events */
addressPointer?: { kind: number; pubkey: string; identifier: string };
/** EventPointer with optional relay hints for regular events */
eventPointer?: EventPointer;
/** AddressPointer for addressable/replaceable events (includes relay hints) */
addressPointer?: AddressPointer;
/** Callback when user clicks to open the event in new window */
onOpen?: (
id: string | { kind: number; pubkey: string; identifier: string },
) => void;
onOpen?: (id: string | AddressPointer) => void;
/** Depth level for nesting (0 = root, 1 = first quote, 2+ = nested) */
depth?: number;
/** Optional className for container */
@@ -27,7 +26,7 @@ interface QuotedEventProps {
* - depth 2+: Show expandable preview only
*/
export function QuotedEvent({
eventId,
eventPointer,
addressPointer,
onOpen,
depth = 1,
@@ -35,21 +34,20 @@ export function QuotedEvent({
}: QuotedEventProps) {
const [isExpanded, setIsExpanded] = useState(depth < 2);
// Determine pointer to use
const pointer = eventId || addressPointer;
// Determine pointer to use - full pointer preserves relay hints
const pointer = eventPointer || addressPointer;
// Load the event
// Load the event - passes full pointer with relay hints to useNostrEvent
const event = useNostrEvent(pointer);
// Loading state
if (!event) {
if (onOpen && pointer) {
const displayText =
typeof eventId === "string"
? `@${eventId.slice(0, 8)}...`
: addressPointer
? `@${addressPointer.identifier || addressPointer.kind}`
: "@event";
const displayText = eventPointer
? `@${eventPointer.id.slice(0, 8)}...`
: addressPointer
? `@${addressPointer.identifier || addressPointer.kind}`
: "@event";
return (
<a
@@ -57,7 +55,7 @@ export function QuotedEvent({
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onOpen(pointer);
onOpen(eventPointer?.id || addressPointer!);
}}
className="inline-flex items-center gap-1 text-accent underline decoration-dotted break-all"
>

View File

@@ -1,4 +1,4 @@
import { EventPointer, AddressPointer } from "nostr-tools/nip19";
import type { EventPointer, AddressPointer } from "nostr-tools/nip19";
import { QuotedEvent } from "../QuotedEvent";
interface EventEmbedNodeProps {
@@ -11,16 +11,18 @@ interface EventEmbedNodeProps {
/**
* EventEmbed component for rendering quoted/embedded Nostr events
* Uses QuotedEvent with depth tracking for smart expand/collapse behavior
* Passes full pointer (including relay hints) for proper event resolution
*/
export function EventEmbed({ node, depth = 1 }: EventEmbedNodeProps) {
const { pointer } = node;
// Check if it's an EventPointer (has 'id') or AddressPointer (has 'kind' + 'pubkey')
const isEvent = "id" in pointer;
return (
<QuotedEvent
eventId={"id" in pointer ? pointer.id : undefined}
addressPointer={
"kind" in pointer && "pubkey" in pointer ? pointer : undefined
}
eventPointer={isEvent ? pointer : undefined}
addressPointer={!isEvent ? (pointer as AddressPointer) : undefined}
depth={depth}
/>
);

View File

@@ -122,7 +122,7 @@ export function Kind9802DetailRenderer({ event }: { event: NostrEvent }) {
Highlighted From
</div>
<EmbeddedEvent
eventId={eventPointer?.id}
eventPointer={eventPointer}
addressPointer={addressPointer}
onOpen={(pointer) => {
if (typeof pointer === "string") {

View File

@@ -1,4 +1,5 @@
import { Repeat2 } from "lucide-react";
import { getEventPointerFromETag } from "applesauce-core/helpers/pointers";
import { BaseEventContainer, type BaseEventProps } from "./BaseEventRenderer";
import { EmbeddedEvent } from "../EmbeddedEvent";
import { useGrimoire } from "@/core/state";
@@ -13,9 +14,9 @@ import { useGrimoire } from "@/core/state";
export function RepostRenderer({ event }: BaseEventProps) {
const { addWindow } = useGrimoire();
// Get the event being reposted (e tag)
// Get the event being reposted (e tag) with relay hints
const eTag = event.tags.find((tag) => tag[0] === "e");
const repostedEventId = eTag?.[1];
const repostedEventPointer = eTag ? getEventPointerFromETag(eTag) : null;
return (
<BaseEventContainer event={event}>
@@ -24,9 +25,9 @@ export function RepostRenderer({ event }: BaseEventProps) {
<Repeat2 className="size-4" />
<span>reposted</span>
</div>
{repostedEventId && (
{repostedEventPointer && (
<EmbeddedEvent
eventId={repostedEventId}
eventPointer={repostedEventPointer}
onOpen={(id) => {
addWindow(
"open",

View File

@@ -137,7 +137,7 @@ export function EventRefListFull({
{eventPointers.map((pointer) => (
<EmbeddedEvent
key={pointer.id}
eventId={pointer.id}
eventPointer={pointer}
className="border border-muted rounded overflow-hidden"
/>
))}