import { type ReactNode } from 'react'; import { cn } from '@/lib/utils'; interface CustomEmojiImgProps { /** Shortcode without surrounding colons. */ name: string; /** The emoji image URL from a NIP-30 `emoji` tag. */ url: string; className?: string; } /** * Inline NIP-30 custom emoji image. */ export function CustomEmojiImg({ name, url, className }: CustomEmojiImgProps) { return ( {`:${name}:`} ); } interface EmojifiedTextProps { /** Event tags containing NIP-30 `emoji` entries used to resolve shortcodes. */ tags: string[][]; children: string; className?: string; } /** * Render a string, replacing `:shortcode:` patterns with inline * custom emoji images sourced from the provided `emoji` tags. * Used inside usernames, reaction displays, etc. */ export function EmojifiedText({ tags, children, className }: EmojifiedTextProps) { const emojiMap = new Map(); for (const tag of tags) { if (tag[0] === 'emoji' && tag[1] && tag[2]) { emojiMap.set(tag[1], tag[2]); } } if (emojiMap.size === 0) { return {children}; } const parts: ReactNode[] = []; const regex = /:([a-zA-Z0-9_-]+):/g; let lastIndex = 0; let match: RegExpExecArray | null; while ((match = regex.exec(children)) !== null) { const [full, shortcode] = match; const url = emojiMap.get(shortcode); if (!url) continue; if (match.index > lastIndex) { parts.push(children.substring(lastIndex, match.index)); } parts.push( , ); lastIndex = match.index + full.length; } if (lastIndex < children.length) { parts.push(children.substring(lastIndex)); } return ( {parts.length > 0 ? parts : children} ); }