diff --git a/components/KIND20Card.tsx b/components/KIND20Card.tsx index a1eeb13..23cc09c 100644 --- a/components/KIND20Card.tsx +++ b/components/KIND20Card.tsx @@ -13,7 +13,7 @@ import type { Event as NostrEvent } from "nostr-tools" import ZapButton from "./ZapButton" import Image from "next/image" import CardOptionsDropdown from "./CardOptionsDropdown" -import { renderTextWithLinkedTags } from "@/utils/textUtils" +import { renderTextWithLinkedTags, renderTextWithLinks } from "@/utils/textUtils" // Function to extract all images from a kind 20 event's imeta tags const extractImagesFromEvent = (tags: string[][]): string[] => { @@ -169,7 +169,7 @@ const KIND20Card: React.FC = ({ )}
-
{renderTextWithLinkedTags(text, tags)}
+
{renderTextWithLinks(text, tags, { [pubkey]: userData || {} })}

diff --git a/components/NoteCard.tsx b/components/NoteCard.tsx index 56cf85e..62a77b3 100644 --- a/components/NoteCard.tsx +++ b/components/NoteCard.tsx @@ -30,7 +30,7 @@ import Link from 'next/link'; import { Event as NostrEvent } from "nostr-tools"; import ZapButton from './ZapButton'; import CardOptionsDropdown from './CardOptionsDropdown'; -import { renderTextWithLinkedTags } from '@/utils/textUtils'; +import { renderTextWithLinkedTags, renderTextWithLinks } from '@/utils/textUtils'; interface NoteCardProps { pubkey: string; @@ -143,7 +143,7 @@ const NoteCard: React.FC = ({ pubkey, text, eventId, tags, event, }
- {renderTextWithLinkedTags(textWithoutImage, tags)} + {renderTextWithLinks(textWithoutImage, tags, { [pubkey]: userData || {} })}

diff --git a/utils/textUtils.tsx b/utils/textUtils.tsx index 99b45fe..7191141 100644 --- a/utils/textUtils.tsx +++ b/utils/textUtils.tsx @@ -1,5 +1,6 @@ import React, { ReactNode } from 'react'; import Link from 'next/link'; +import { nip19 } from 'nostr-tools'; /** * Renders text content with hyperlinked hashtags @@ -54,3 +55,119 @@ export function renderTextWithLinkedTags(content: string, eventTags: string[][]) return result; } + +/** + * Replace nostr:npub references with @username in content + * @param content The text content that may contain nostr:npub references + * @param eventTags The tags array from a Nostr event + * @param userData Optional object containing profile data for referenced users + * @returns Text with replaced nostr:npub references + */ +export function replaceNostrReferences( + content: string, + eventTags: string[][], + userData?: Record +): ReactNode[] { + if (!content) return []; + + // Extract all pubkey references from the event tags (p tags contain referenced pubkeys) + const pubkeyRefs = eventTags + .filter((tag) => tag[0] === "p") + .map((tag) => ({ pubkey: tag[1], relay: tag.length > 2 ? tag[2] : undefined, petname: tag.length > 3 ? tag[3] : undefined })); + + // Find nostr:npub and nostr:nprofile references in the content + const nostrRegex = /nostr:(npub1[a-z0-9]+|nprofile1[a-z0-9]+)/g; + let lastIndex = 0; + const result: ReactNode[] = []; + let match; + + while ((match = nostrRegex.exec(content)) !== null) { + const fullRef = match[0]; // nostr:npub1... + const matchIndex = match.index; + + // Add text before the reference + if (matchIndex > lastIndex) { + result.push(content.substring(lastIndex, matchIndex)); + } + + try { + // Extract the identifier (remove "nostr:" prefix) + const nostrId = fullRef.substring(6); + let pubkey: string; + let profileRoute = nostrId; + + // Decode npub or nprofile to get the pubkey + if (nostrId.startsWith('npub1')) { + const { data } = nip19.decode(nostrId); + pubkey = data as string; + } else if (nostrId.startsWith('nprofile1')) { + const { data } = nip19.decode(nostrId); + pubkey = (data as { pubkey: string }).pubkey; + // Still use the nprofile as the route to preserve relay information + profileRoute = nostrId; + } else { + throw new Error('Unsupported nostr ID type'); + } + + // Find if we have any profile data for this pubkey + const pubkeyData = pubkeyRefs.find(ref => ref.pubkey === pubkey); + const displayName = pubkeyData?.petname || + (userData && userData[pubkey] && + (userData[pubkey].username || + userData[pubkey].display_name || + userData[pubkey].name)) || + nostrId.substring(0, 8) + '...'; + + // Create a link for the user reference + result.push( + + @{displayName} + + ); + } catch (error) { + // If there's an error parsing the npub, just include it as-is + result.push(fullRef); + } + + lastIndex = matchIndex + fullRef.length; + } + + // Add any remaining text + if (lastIndex < content.length) { + result.push(content.substring(lastIndex)); + } + + return result; +} + +/** + * Combines hashtag and nostr reference handling in one function + * @param content The text content to process + * @param eventTags The tags array from a Nostr event + * @param userData Optional object containing profile data for referenced users + * @returns Processed content with links + */ +export function renderTextWithLinks( + content: string, + eventTags: string[][], + userData?: Record +): ReactNode[] { + if (!content) return []; + + // First replace hashtags + const withHashtags = renderTextWithLinkedTags(content, eventTags); + + // Then handle nostr references for each text segment + const result: ReactNode[] = []; + + for (const item of withHashtags) { + if (typeof item === 'string' && (item.includes('nostr:npub') || item.includes('nostr:nprofile'))) { + const withReferences = replaceNostrReferences(item, eventTags, userData); + result.push(...withReferences); + } else { + result.push(item); + } + } + + return result; +}