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.
 
 
 
 

99 lines
3.4 KiB

import Emoji from '@/components/Emoji'
import { ExtendedKind } from '@/constants'
import {
EMPTY_AUTHOR_NIP30_EMOJIS,
fetchAuthorNip30EmojiInfos,
fetchAuthorNip30EmojiInfosFromIndexedDb,
getAuthorNip30EmojiCache,
subscribeAuthorNip30EmojiCache
} from '@/lib/nip30-author-emojis'
import { resolveAuthorEmojiForReactionShortcode, resolveReactionEmojiSync } from '@/lib/reaction-display'
import { cn } from '@/lib/utils'
import { TEmoji } from '@/types'
import { Event, kinds } from 'nostr-tools'
import { useEffect, useMemo, useSyncExternalStore } from 'react'
/**
* Renders a reaction glyph (Unicode, standard :shortcode:, or NIP-30 custom image from reactor profile).
*/
export default function ReactionEmojiDisplay({
event,
className,
maxRawLength = 64,
variant = 'default'
}: {
event: Event
className?: string
/** Truncate long reaction text beyond this length */
maxRawLength?: number
/** `compact`: content previews; `thread`: reply-list reaction rows (large glyph). */
variant?: 'default' | 'compact' | 'thread'
}) {
const sync = useMemo(
() => resolveReactionEmojiSync(event, maxRawLength),
[event, maxRawLength]
)
const reactorPubkey = event.pubkey?.trim().toLowerCase() ?? ''
const needsAuthorLookup = sync.mode === 'profile'
const authorEmojis = useSyncExternalStore(
(onStoreChange) =>
needsAuthorLookup && /^[0-9a-f]{64}$/.test(reactorPubkey)
? subscribeAuthorNip30EmojiCache(reactorPubkey, onStoreChange)
: () => {},
() =>
needsAuthorLookup && /^[0-9a-f]{64}$/.test(reactorPubkey)
? getAuthorNip30EmojiCache(reactorPubkey)
: EMPTY_AUTHOR_NIP30_EMOJIS,
() => EMPTY_AUTHOR_NIP30_EMOJIS
)
useEffect(() => {
if (!needsAuthorLookup || !/^[0-9a-f]{64}$/.test(reactorPubkey)) return
void fetchAuthorNip30EmojiInfosFromIndexedDb(reactorPubkey)
void fetchAuthorNip30EmojiInfos(reactorPubkey)
}, [needsAuthorLookup, reactorPubkey])
const value: TEmoji | string = useMemo(() => {
if (sync.mode === 'display') return sync.value
const hit = resolveAuthorEmojiForReactionShortcode(authorEmojis, sync.shortcode)
return hit ?? sync.placeholder
}, [sync, authorEmojis])
if (
(event.kind !== kinds.Reaction && event.kind !== ExtendedKind.EXTERNAL_REACTION) ||
(sync.mode === 'display' && sync.value === '')
) {
return null
}
/** Unicode / shortcode strings must not get `img` max-height classes — {@link Emoji} merges both onto one span and clips glyphs. */
const emojiClassNames =
variant === 'thread'
? typeof value === 'object'
? {
img: 'size-[calc(1.85rem*4/3)] max-h-[2.25rem] w-auto rounded-md opacity-95 inline-block align-middle'
}
: { text: 'text-2xl sm:text-3xl leading-normal tracking-tight' }
: {
img:
variant === 'compact'
? 'size-[calc(1rem*4/3)] max-h-[1em] w-auto rounded-sm'
: 'size-[calc(1.75rem*4/3)] max-h-[1.5em] w-auto rounded-sm',
text: variant === 'compact' ? 'text-base leading-none' : 'text-2xl leading-none'
}
return (
<span
className={cn(
'inline-flex shrink-0 items-center justify-center select-none',
variant === 'thread' ? 'overflow-visible leading-normal py-0.5' : 'leading-none',
className
)}
aria-hidden
>
<Emoji emoji={value} classNames={emojiClassNames} />
</span>
)
}