feat: enhance MarkdownContent to support Nostr mentions and improve URI handling

This commit is contained in:
2025-10-12 22:21:31 +02:00
parent 22256c49d9
commit 1c38a22ae6

View File

@@ -1,8 +1,11 @@
import { useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Link } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
import { cn } from '@/lib/utils';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
interface MarkdownContentProps {
content: string;
@@ -14,6 +17,36 @@ interface MarkdownContentProps {
* Used for rendering NIP-23 long-form blog posts
*/
export function MarkdownContent({ content, className }: MarkdownContentProps) {
// Preprocess content to convert plain nostr: URIs into markdown links
const processedContent = useMemo(() => {
// Regex to find nostr: URIs that are NOT already in markdown link format
// This matches nostr:npub1..., nostr:note1..., etc. that aren't part of [text](nostr:...)
const nostrUriRegex = /(?<!\]\()nostr:(npub1|note1|nprofile1|nevent1|naddr1)([023456789acdefghjklmnpqrstuvwxyz]+)/g;
return content.replace(nostrUriRegex, (match, prefix, data) => {
const nostrId = `${prefix}${data}`;
try {
// Validate it's a proper NIP-19 identifier
const decoded = nip19.decode(nostrId);
// For npub/nprofile, we'll show @username in the link text
// For other types, show the nostr: URI
if (decoded.type === 'npub' || decoded.type === 'nprofile') {
const pubkey = decoded.type === 'npub' ? decoded.data : decoded.data.pubkey;
// We'll use a special format that we can detect in the link component
return `[nostr-mention:${pubkey}](/${nostrId})`;
} else {
// For note, nevent, naddr - show the nostr: URI as link text
return `[${match}](/${nostrId})`;
}
} catch {
// If decoding fails, leave it as-is
return match;
}
});
}, [content]);
return (
<div className={cn('prose prose-slate dark:prose-invert prose-headings:font-bold prose-h1:text-4xl prose-h2:text-3xl prose-h3:text-2xl prose-h4:text-xl prose-h5:text-lg prose-h6:text-base max-w-none break-words overflow-wrap-anywhere', className)}>
<ReactMarkdown
@@ -52,7 +85,13 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
),
// Custom link renderer to handle nostr: URIs
a: ({ node, href, children, ...props }) => {
// Handle nostr: URIs
// Handle nostr-mention links (generated by preprocessor)
if (typeof children === 'string' && children.startsWith('nostr-mention:')) {
const pubkey = children.substring(14); // Remove "nostr-mention:" prefix
return <NostrMention pubkey={pubkey} href={href} />;
}
// Handle nostr: URIs in markdown links [text](nostr:npub1...)
if (href?.startsWith('nostr:')) {
const nostrId = href.substring(6); // Remove "nostr:" prefix
@@ -74,7 +113,19 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
}
}
// Regular links open in new tab
// Handle internal links (starting with /)
if (href?.startsWith('/')) {
return (
<Link
to={href}
className="text-blue-500 hover:underline break-all"
>
{children}
</Link>
);
}
// Regular external links open in new tab
return (
<a
href={href}
@@ -123,8 +174,29 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
),
}}
>
{content}
{processedContent}
</ReactMarkdown>
</div>
);
}
// Helper component to display user mentions
function NostrMention({ pubkey, href }: { pubkey: string; href?: string }) {
const author = useAuthor(pubkey);
const hasRealName = !!author.data?.metadata?.name;
const displayName = author.data?.metadata?.name ?? genUserName(pubkey);
return (
<Link
to={href || `/${nip19.npubEncode(pubkey)}`}
className={cn(
"font-medium hover:underline",
hasRealName
? "text-blue-500"
: "text-gray-500 hover:text-gray-700"
)}
>
@{displayName}
</Link>
);
}