You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
74 lines
2.9 KiB
74 lines
2.9 KiB
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<string, TEmoji>() |
|
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] |
|
) |
|
}
|
|
|