From 7c5fe75572e4db1aa349c4b4bc4cd558cc8469c8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 1 Jun 2025 09:28:20 -0500 Subject: [PATCH] Generate a deterministic name from pubkey if no display name is set --- src/components/NoteContent.test.tsx | 32 ++++++++++++++++++++++ src/components/NoteContent.tsx | 42 ++++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/components/NoteContent.test.tsx b/src/components/NoteContent.test.tsx index 9feaaf3..b5e8257 100644 --- a/src/components/NoteContent.test.tsx +++ b/src/components/NoteContent.test.tsx @@ -101,4 +101,36 @@ describe('NoteContent', () => { expect(nostrHashtag).toHaveAttribute('href', '/t/nostr'); expect(bitcoinHashtag).toHaveAttribute('href', '/t/bitcoin'); }); + + it('generates deterministic names for users without metadata and styles them differently', () => { + // Use a valid npub for testing + const event: NostrEvent = { + id: 'test-id', + pubkey: 'test-pubkey', + created_at: Math.floor(Date.now() / 1000), + kind: 1, + tags: [], + content: `Mentioning nostr:npub1zg69v7ys40x77y352eufp27daufrg4ncjz4ummcjx3t83y9tehhsqepuh0`, + sig: 'test-sig', + }; + + render( + + + + ); + + // The mention should be rendered with a deterministic name + const mention = screen.getByRole('link'); + expect(mention).toBeInTheDocument(); + + // Should have muted styling for generated names (gray instead of blue) + expect(mention).toHaveClass('text-gray-500'); + expect(mention).not.toHaveClass('text-blue-500'); + + // The text should start with @ and contain a generated name (not a truncated npub) + const linkText = mention.textContent; + expect(linkText).not.toMatch(/^@npub1/); // Should not be a truncated npub + expect(linkText).toEqual("@SwiftFalcon"); + }); }); \ No newline at end of file diff --git a/src/components/NoteContent.tsx b/src/components/NoteContent.tsx index 2a19343..2572463 100644 --- a/src/components/NoteContent.tsx +++ b/src/components/NoteContent.tsx @@ -20,7 +20,7 @@ export function NoteContent({ const text = event.content; // Regex to find URLs, Nostr references, and hashtags - const regex = /(https?:\/\/[^\s]+)|nostr:(npub1|note1|nprofile1|nevent1)([a-z0-9]+)|(#\w+)/g; + const regex = /(https?:\/\/[^\s]+)|nostr:(npub1|note1|nprofile1|nevent1)([023456789acdefghjklmnpqrstuvwxyz]+)|(#\w+)/g; const parts: React.ReactNode[] = []; let lastIndex = 0; @@ -117,14 +117,50 @@ export function NoteContent({ function NostrMention({ pubkey }: { pubkey: string }) { const author = useAuthor(pubkey); const npub = nip19.npubEncode(pubkey); - const displayName = author.data?.metadata?.name ?? npub.slice(0, 8); + const hasRealName = !!author.data?.metadata?.name; + const displayName = author.data?.metadata?.name ?? generateDeterministicName(pubkey); return ( @{displayName} ); +} + +// Generate a deterministic name based on pubkey +function generateDeterministicName(pubkey: string): string { + // Use a simple hash of the pubkey to generate consistent adjective + noun combinations + const adjectives = [ + 'Swift', 'Bright', 'Calm', 'Bold', 'Wise', 'Kind', 'Quick', 'Brave', + 'Cool', 'Sharp', 'Clear', 'Strong', 'Smart', 'Fast', 'Keen', 'Pure', + 'Noble', 'Gentle', 'Fierce', 'Steady', 'Clever', 'Proud', 'Silent', 'Wild' + ]; + + const nouns = [ + 'Fox', 'Eagle', 'Wolf', 'Bear', 'Lion', 'Tiger', 'Hawk', 'Owl', + 'Deer', 'Raven', 'Falcon', 'Lynx', 'Otter', 'Whale', 'Shark', 'Dolphin', + 'Phoenix', 'Dragon', 'Panther', 'Jaguar', 'Cheetah', 'Leopard', 'Puma', 'Cobra' + ]; + + // Create a simple hash from the pubkey + let hash = 0; + for (let i = 0; i < pubkey.length; i++) { + const char = pubkey.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32-bit integer + } + + // Use absolute value to ensure positive index + const adjIndex = Math.abs(hash) % adjectives.length; + const nounIndex = Math.abs(hash >> 8) % nouns.length; + + return `${adjectives[adjIndex]}${nouns[nounIndex]}`; } \ No newline at end of file