mkstack/src/components/NoteContent.tsx

166 lines
4.9 KiB
TypeScript
Raw Normal View History

import { useMemo } from 'react';
2025-05-17 00:37:39 -05:00
import { type NostrEvent } from '@nostrify/nostrify';
import { Link } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
import { useAuthor } from '@/hooks/useAuthor';
import { cn } from '@/lib/utils';
interface NoteContentProps {
event: NostrEvent;
className?: string;
}
/** Parses content of text note events so that URLs and hashtags are linkified. */
export function NoteContent({
2025-05-17 00:37:39 -05:00
event,
className,
}: NoteContentProps) {
2025-05-17 00:37:39 -05:00
// Process the content to render mentions, links, etc.
const content = useMemo(() => {
const text = event.content;
// Regex to find URLs, Nostr references, and hashtags
const regex = /(https?:\/\/[^\s]+)|nostr:(npub1|note1|nprofile1|nevent1)([023456789acdefghjklmnpqrstuvwxyz]+)|(#\w+)/g;
const parts: React.ReactNode[] = [];
let lastIndex = 0;
let match: RegExpExecArray | null;
let keyCounter = 0;
while ((match = regex.exec(text)) !== null) {
const [fullMatch, url, nostrPrefix, nostrData, hashtag] = match;
const index = match.index;
2025-05-17 00:37:39 -05:00
// Add text before this match
if (index > lastIndex) {
parts.push(text.substring(lastIndex, index));
}
2025-05-17 00:37:39 -05:00
if (url) {
// Handle URLs
parts.push(
<a
key={`url-${keyCounter++}`}
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
{url}
</a>
);
} else if (nostrPrefix && nostrData) {
// Handle Nostr references
try {
const nostrId = `${nostrPrefix}${nostrData}`;
const decoded = nip19.decode(nostrId);
2025-05-17 00:37:39 -05:00
if (decoded.type === 'npub') {
const pubkey = decoded.data;
parts.push(
<NostrMention key={`mention-${keyCounter++}`} pubkey={pubkey} />
);
} else {
// For other types, just show as a link
parts.push(
<Link
key={`nostr-${keyCounter++}`}
to={`/${nostrId}`}
className="text-blue-500 hover:underline"
>
{fullMatch}
</Link>
);
2025-05-17 00:37:39 -05:00
}
} catch (e) {
// If decoding fails, just render as text
parts.push(fullMatch);
}
} else if (hashtag) {
// Handle hashtags
const tag = hashtag.slice(1); // Remove the #
parts.push(
<Link
key={`hashtag-${keyCounter++}`}
to={`/t/${tag}`}
className="text-blue-500 hover:underline"
>
{hashtag}
</Link>
);
2025-05-17 00:37:39 -05:00
}
lastIndex = index + fullMatch.length;
}
// Add any remaining text
if (lastIndex < text.length) {
parts.push(text.substring(lastIndex));
}
// If no special content was found, just use the plain text
if (parts.length === 0) {
parts.push(text);
}
2025-05-17 00:37:39 -05:00
return parts;
2025-05-17 00:37:39 -05:00
}, [event]);
2025-05-17 00:37:39 -05:00
return (
<div className={cn("whitespace-pre-wrap break-words", className)}>
{content.length > 0 ? content : event.content}
</div>
);
}
// Helper component to display user mentions
function NostrMention({ pubkey }: { pubkey: string }) {
const author = useAuthor(pubkey);
const npub = nip19.npubEncode(pubkey);
const hasRealName = !!author.data?.metadata?.name;
const displayName = author.data?.metadata?.name ?? generateDeterministicName(pubkey);
2025-05-17 00:37:39 -05:00
return (
<Link
to={`/${npub}`}
className={cn(
"font-medium hover:underline",
hasRealName
? "text-blue-500"
: "text-gray-500 hover:text-gray-700"
)}
2025-05-17 00:37:39 -05:00
>
@{displayName}
</Link>
);
}
// 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]}`;
2025-05-17 00:37:39 -05:00
}