From 6b09212fe9c878fba05b125a8a879a0551214d47 Mon Sep 17 00:00:00 2001 From: Gigi Date: Mon, 20 Oct 2025 14:55:00 +0200 Subject: [PATCH] feat: resolve user profiles for npub mentions in highlight comments - Create NostrMentionLink component to fetch and display user names - Replace truncated pubkey display with resolved profile names - Fetch profiles in background non-blocking way using useEventModel - Falls back to truncated pubkey if profile not available --- src/components/HighlightItem.tsx | 104 +++--------------------- src/components/NostrMentionLink.tsx | 118 ++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 95 deletions(-) create mode 100644 src/components/NostrMentionLink.tsx diff --git a/src/components/HighlightItem.tsx b/src/components/HighlightItem.tsx index e7b972b2..d85a6c9e 100644 --- a/src/components/HighlightItem.tsx +++ b/src/components/HighlightItem.tsx @@ -17,6 +17,7 @@ import { getNostrUrl } from '../config/nostrGateways' import CompactButton from './CompactButton' import { HighlightCitation } from './HighlightCitation' import { useNavigate } from 'react-router-dom' +import NostrMentionLink from './NostrMentionLink' // Helper to detect if a URL is an image const isImageUrl = (url: string): boolean => { @@ -29,99 +30,6 @@ const isImageUrl = (url: string): boolean => { } } -// Helper to render a nostr identifier -const renderNostrId = (nostrUri: string, index: number): React.ReactElement => { - try { - // Remove nostr: prefix - const identifier = nostrUri.replace(/^nostr:/, '') - const decoded = nip19.decode(identifier) - - switch (decoded.type) { - case 'npub': { - const pubkey = decoded.data - return ( - e.stopPropagation()} - > - @{pubkey.slice(0, 8)}... - - ) - } - case 'nprofile': { - const { pubkey } = decoded.data - const npub = nip19.npubEncode(pubkey) - return ( - e.stopPropagation()} - > - @{pubkey.slice(0, 8)}... - - ) - } - case 'naddr': { - const { kind, pubkey, identifier } = decoded.data - // Check if it's a blog post (kind:30023) - if (kind === 30023) { - const naddr = nip19.naddrEncode({ kind, pubkey, identifier }) - return ( - e.stopPropagation()} - > - {identifier || 'Article'} - - ) - } - // For other kinds, show shortened identifier - return ( - - nostr:{identifier.slice(0, 12)}... - - ) - } - case 'note': { - const eventId = decoded.data - return ( - - note:{eventId.slice(0, 12)}... - - ) - } - case 'nevent': { - const { id } = decoded.data - return ( - - event:{id.slice(0, 12)}... - - ) - } - default: - // Fallback for unrecognized types - return ( - - {identifier.slice(0, 20)}... - - ) - } - } catch (error) { - // If decoding fails, show shortened identifier - const identifier = nostrUri.replace(/^nostr:/, '') - return ( - - {identifier.slice(0, 20)}... - - ) - } -} - // Component to render comment with links, inline images, and nostr identifiers const CommentContent: React.FC<{ text: string }> = ({ text }) => { // Pattern to match both http(s) URLs and nostr: URIs @@ -131,9 +39,15 @@ const CommentContent: React.FC<{ text: string }> = ({ text }) => { return ( <> {parts.map((part, index) => { - // Handle nostr: URIs + // Handle nostr: URIs - now with profile resolution if (part.startsWith('nostr:')) { - return renderNostrId(part, index) + return ( + e.stopPropagation()} + /> + ) } // Handle http(s) URLs diff --git a/src/components/NostrMentionLink.tsx b/src/components/NostrMentionLink.tsx new file mode 100644 index 00000000..6b507028 --- /dev/null +++ b/src/components/NostrMentionLink.tsx @@ -0,0 +1,118 @@ +import React from 'react' +import { nip19 } from 'nostr-tools' +import { useEventModel } from 'applesauce-react/hooks' +import { Models } from 'applesauce-core' + +interface NostrMentionLinkProps { + nostrUri: string + onClick?: (e: React.MouseEvent) => void + className?: string +} + +/** + * Component to render nostr mentions with resolved profile names + * Handles npub, nprofile, note, nevent, and naddr URIs + */ +const NostrMentionLink: React.FC = ({ + nostrUri, + onClick, + className = 'highlight-comment-link' +}) => { + try { + // Remove nostr: prefix + const identifier = nostrUri.replace(/^nostr:/, '') + const decoded = nip19.decode(identifier) + + switch (decoded.type) { + case 'npub': { + const pubkey = decoded.data + // Fetch profile in the background + const profile = useEventModel(Models.ProfileModel, [pubkey]) + const displayName = profile?.name || profile?.display_name || profile?.nip05 || `${pubkey.slice(0, 8)}...` + + return ( + + @{displayName} + + ) + } + case 'nprofile': { + const { pubkey } = decoded.data + // Fetch profile in the background + const profile = useEventModel(Models.ProfileModel, [pubkey]) + const displayName = profile?.name || profile?.display_name || profile?.nip05 || `${pubkey.slice(0, 8)}...` + const npub = nip19.npubEncode(pubkey) + + return ( + + @{displayName} + + ) + } + case 'naddr': { + const { kind, pubkey, identifier: addrIdentifier } = decoded.data + // Check if it's a blog post (kind:30023) + if (kind === 30023) { + const naddr = nip19.naddrEncode({ kind, pubkey, identifier: addrIdentifier }) + return ( + + {addrIdentifier || 'Article'} + + ) + } + // For other kinds, show shortened identifier + return ( + + nostr:{addrIdentifier.slice(0, 12)}... + + ) + } + case 'note': { + const eventId = decoded.data + return ( + + note:{eventId.slice(0, 12)}... + + ) + } + case 'nevent': { + const { id } = decoded.data + return ( + + event:{id.slice(0, 12)}... + + ) + } + default: + // Fallback for unrecognized types + return ( + + {identifier.slice(0, 20)}... + + ) + } + } catch (error) { + // If decoding fails, show shortened identifier + const identifier = nostrUri.replace(/^nostr:/, '') + return ( + + {identifier.slice(0, 20)}... + + ) + } +} + +export default NostrMentionLink +