import { EMOJI_SHORT_CODE_REGEX } from '@/lib/content-patterns' import { EMPTY_AUTHOR_NIP30_EMOJIS, fetchAuthorNip30EmojiInfos, fetchAuthorNip30EmojiInfosFromIndexedDb, getAuthorNip30EmojiCache, subscribeAuthorNip30EmojiCache } from '@/lib/nip30-author-emojis' import { getEmojiInfosFromEmojiTags } from '@/lib/tag' import { TEmoji } from '@/types' import { emojis, shortcodeToEmoji } from '@tiptap/extension-emoji' import { type Event } from 'nostr-tools' import { useEffect, useMemo, useSyncExternalStore } from 'react' /** Event `emoji` tags override the same shortcode from the author's kind 0. */ export function mergeEmojiInfosEventOverridesAuthor( fromAuthor: readonly TEmoji[], fromEvent: readonly TEmoji[] ): TEmoji[] { const m = new Map() for (const e of fromAuthor) m.set(e.shortcode, e) for (const e of fromEvent) m.set(e.shortcode, e) return [...m.values()] } /** * True when `content` contains a `:shortcode:` that is neither defined on the event nor a known * standard (Unicode) shortcode — likely a custom emoji from the author's profile. */ export function contentNeedsAuthorEmojiLookup(content: string | undefined, eventTagInfos: TEmoji[]): boolean { if (!content) return false const eventCodes = new Set(eventTagInfos.map((e) => e.shortcode)) const re = new RegExp(EMOJI_SHORT_CODE_REGEX.source, 'g') let m: RegExpExecArray | null while ((m = re.exec(content)) !== null) { const code = m[1].trim() if (eventCodes.has(code)) continue const native = shortcodeToEmoji(code, emojis) ?? shortcodeToEmoji(code.replace(/\s+/g, '_'), emojis) if (!native?.emoji) return true } return false } /** * NIP-30 emoji tags on the event plus, when needed, the author’s published custom emoji * (kind 0, 10030, and 30030 — same inventory approach as the emoji picker). */ export function useEmojiInfosForEvent(event: Event | undefined | null): TEmoji[] { const fromEvent = useMemo(() => getEmojiInfosFromEmojiTags(event?.tags ?? []), [event?.tags]) const needsLookup = useMemo( () => (event ? contentNeedsAuthorEmojiLookup(event.content, fromEvent) : false), [event?.id, event?.content, fromEvent] ) const pubkey = event?.pubkey?.trim().toLowerCase() ?? '' const validPk = /^[0-9a-f]{64}$/.test(pubkey) const fromAuthor = useSyncExternalStore( (onStoreChange) => validPk && needsLookup ? subscribeAuthorNip30EmojiCache(pubkey, onStoreChange) : () => {}, () => (validPk && needsLookup ? getAuthorNip30EmojiCache(pubkey) : EMPTY_AUTHOR_NIP30_EMOJIS), () => EMPTY_AUTHOR_NIP30_EMOJIS ) useEffect(() => { if (!needsLookup || !validPk) return void fetchAuthorNip30EmojiInfosFromIndexedDb(pubkey) void fetchAuthorNip30EmojiInfos(pubkey) }, [needsLookup, validPk, pubkey]) return useMemo( () => mergeEmojiInfosEventOverridesAuthor(fromAuthor, fromEvent), [fromAuthor, fromEvent] ) }