import { useMemo } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { Skeleton } from '@/components/ui/skeleton';
import { useEvent } from '@/hooks/useEvent';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
import { timeAgo } from '@/lib/timeAgo';
import { cn } from '@/lib/utils';
/** Bech32 charset used by NIP-19 identifiers. */
const B32 = '023456789acdefghjklmnpqrstuvwxyz';
/** Regex that matches nostr:npub1… and nostr:nprofile1… inside text. */
const MENTION_REGEX = new RegExp(`nostr:(npub1|nprofile1)[${B32}]+`, 'g');
/** A parsed segment of embedded-note text. */
type EmbedSegment =
| { type: 'text'; value: string }
| { type: 'mention'; pubkey: string; npub: string };
/** Split text into plain strings and mention segments. */
function parseEmbedSegments(text: string): EmbedSegment[] {
const segments: EmbedSegment[] = [];
let last = 0;
let m: RegExpExecArray | null;
MENTION_REGEX.lastIndex = 0;
while ((m = MENTION_REGEX.exec(text)) !== null) {
if (m.index > last) {
segments.push({ type: 'text', value: text.slice(last, m.index) });
}
try {
const bech32 = m[0].slice('nostr:'.length);
const decoded = nip19.decode(bech32);
const pubkey = decoded.type === 'npub'
? (decoded.data as string)
: (decoded.data as { pubkey: string }).pubkey;
const npub = nip19.npubEncode(pubkey);
segments.push({ type: 'mention', pubkey, npub });
} catch {
// If decode fails, keep as plain text
segments.push({ type: 'text', value: m[0] });
}
last = m.index + m[0].length;
}
if (last < text.length) {
segments.push({ type: 'text', value: text.slice(last) });
}
return segments;
}
interface EmbeddedNoteProps {
/** Hex event ID to fetch and display. */
eventId: string;
className?: string;
}
/** Maximum characters of note content to show in the embedded preview. */
const MAX_CONTENT_LENGTH = 280;
/** Inline embedded note card – similar to a link preview but for Nostr events. */
export function EmbeddedNote({ eventId, className }: EmbeddedNoteProps) {
const { data: event, isLoading, isError } = useEvent(eventId);
if (isLoading) {
return ;
}
if (isError || !event) {
return null;
}
return ;
}
/** The actual card once the event has been fetched. */
function EmbeddedNoteCard({
event,
className,
}: {
event: { id: string; pubkey: string; content: string; created_at: number; tags: string[][] };
className?: string;
}) {
const navigate = useNavigate();
const author = useAuthor(event.pubkey);
const metadata = author.data?.metadata;
const displayName = metadata?.name || genUserName(event.pubkey);
const npub = useMemo(() => nip19.npubEncode(event.pubkey), [event.pubkey]);
const neventId = useMemo(
() => nip19.neventEncode({ id: event.id, author: event.pubkey }),
[event.id, event.pubkey],
);
// Truncate long content, stripping media URLs and nested nostr event references
const truncatedContent = useMemo(() => {
const cleaned = event.content
// Strip media URLs
.replace(/https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg|mp4|webm|mov|mp3|ogg|wav|pdf)(\?[^\s]*)?/gi, '')
// Strip embedded event references (nevent / note) so they don't nest
.replace(/nostr:(nevent1|note1)[023456789acdefghjklmnpqrstuvwxyz]+/g, '')
// Collapse leftover whitespace
.replace(/\n{3,}/g, '\n\n')
.trim();
if (cleaned.length <= MAX_CONTENT_LENGTH) return cleaned;
return cleaned.slice(0, MAX_CONTENT_LENGTH).trimEnd() + '…';
}, [event.content]);
// Extract first image for a small thumbnail
const firstImage = useMemo(() => {
const match = event.content.match(
/https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg)(\?[^\s]*)?/i,
);
return match?.[0] ?? null;
}, [event.content]);
return (
{
e.stopPropagation();
navigate(`/${neventId}`);
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
navigate(`/${neventId}`);
}
}}
>
{/* Optional image thumbnail */}
{firstImage && (

{
(e.currentTarget.parentElement as HTMLElement).style.display = 'none';
}}
/>
)}
{/* Note content */}
{/* Author row */}
e.stopPropagation()}
>
{displayName[0]?.toUpperCase()}
e.stopPropagation()}
>
{displayName}
· {timeAgo(event.created_at)}
{/* Text preview with inline mentions */}
{truncatedContent && (
)}
);
}
/** Renders embedded-note text with @mentions resolved inline. */
function EmbedContentPreview({ text }: { text: string }) {
const segments = useMemo(() => parseEmbedSegments(text), [text]);
return (
{segments.map((seg, i) => {
if (seg.type === 'text') {
return {seg.value};
}
return ;
})}
);
}
/** Inline @mention inside an embedded note preview. */
function EmbedMention({ pubkey, npub }: { pubkey: string; npub: string }) {
const author = useAuthor(pubkey);
const hasRealName = !!author.data?.metadata?.name;
const displayName = author.data?.metadata?.name ?? genUserName(pubkey);
return (
e.stopPropagation()}
>
@{displayName}
);
}
function EmbeddedNoteSkeleton({ className }: { className?: string }) {
return (
);
}