import type { TEmoji } from '@/types' /** * Single source for the quick-like emoji row used by the EmojiPicker / LikeButton. * EmojiPicker re-exports this list as EMOJI_PICKER_REACTIONS for LikeButton. */ /** NIP-25 default positive reaction is the character `+`, not a Unicode heart. */ export const DEFAULT_LIKE_REACTION_CONTENT = '+' as const /** * Visual glyph for {@link DEFAULT_LIKE_REACTION_CONTENT} in UI (heart suit, emoji presentation). * Published reaction content stays `+`. */ export const DEFAULT_LIKE_REACTION_DISPLAY_EMOJI = '\u2665\uFE0F' export const DEFAULT_SUGGESTED_EMOJIS = [ DEFAULT_LIKE_REACTION_CONTENT, '๐Ÿ‘', '๐Ÿ”ฅ', '๐Ÿ˜‚', '๐Ÿ˜ข', '๐Ÿซ‚', '๐Ÿš€' ] as const /** Kind-7 bodies many clients publish instead of NIP-25 `+`. */ const COMMON_HEART_LIKE_GLYPHS = new Set([ 'โค', 'โค๏ธ', 'โ™ฅ', 'โ™ฅ๏ธ', '๐Ÿฉท', '๐Ÿงก', '๐Ÿ’›', '๐Ÿ’š', '๐Ÿ’™', '๐Ÿฉต', '๐Ÿ’œ', '๐ŸคŽ', '๐Ÿ–ค', '๐Ÿฉถ', '๐Ÿค' ]) const THUMBS_UP_GLYPHS = new Set(['๐Ÿ‘', '+1']) const THUMBS_DOWN_GLYPHS = new Set(['๐Ÿ‘Ž', '-1']) const ARROW_UP_GLYPHS = new Set(['โฌ†', 'โฌ†๏ธ', 'โ†‘', '๐Ÿ”ผ']) const ARROW_DOWN_GLYPHS = new Set(['โฌ‡', 'โฌ‡๏ธ', 'โ†“', '๐Ÿ”ฝ']) const HEART_LIKE_SHORTCODES = new Set(['', '+', 'heart', 'love', 'plus']) const THUMBS_UP_SHORTCODES = new Set(['thumbsup', 'thumbs_up', '+1', 'like', 'thumbup']) const THUMBS_DOWN_SHORTCODES = new Set(['thumbsdown', 'thumbs_down', '-1', 'thumbdown']) const ARROW_UP_SHORTCODES = new Set(['arrow_up', 'arrowup', 'up', 'upvote', 'up_arrow']) const ARROW_DOWN_SHORTCODES = new Set(['arrow_down', 'arrowdown', 'down', 'downvote', 'down_arrow']) const DISLIKE_SHORTCODES = new Set(['dislike']) function normalizedReactionString(emoji: TEmoji | string): string | undefined { if (typeof emoji === 'object' && emoji !== null && 'shortcode' in emoji) { return emoji.shortcode.trim().toLowerCase() } if (typeof emoji === 'string') return emoji.trim() return undefined } function normalizedGlyph(s: string): string { return s.normalize('NFC').replace(/\ufe0f/gi, '').trim() } function matchesGlyphSet(raw: string, glyphs: Set): boolean { const c = raw.trim() if (glyphs.has(c)) return true const n = normalizedGlyph(c) for (const g of glyphs) { if (normalizedGlyph(g) === n) return true } return false } function matchesShortcodeSet( emoji: TEmoji | string, normalized: string, codes: Set ): boolean { if (typeof emoji === 'object' && emoji !== null && 'shortcode' in emoji) { return codes.has(normalized) } return false } function isHeartOrPlusLike(emoji: TEmoji | string): boolean { if (typeof emoji === 'object' && emoji !== null && 'shortcode' in emoji) { return HEART_LIKE_SHORTCODES.has(emoji.shortcode.trim().toLowerCase()) } if (typeof emoji !== 'string') return false const c = emoji.trim() return c === '' || c === DEFAULT_LIKE_REACTION_CONTENT || COMMON_HEART_LIKE_GLYPHS.has(c) } function isThumbsUp(emoji: TEmoji | string): boolean { const normalized = normalizedReactionString(emoji) if (normalized === undefined) return false if (matchesShortcodeSet(emoji, normalized, THUMBS_UP_SHORTCODES)) return true if (typeof emoji === 'string') return matchesGlyphSet(normalized, THUMBS_UP_GLYPHS) return false } function isThumbsDown(emoji: TEmoji | string): boolean { const normalized = normalizedReactionString(emoji) if (normalized === undefined) return false if (matchesShortcodeSet(emoji, normalized, THUMBS_DOWN_SHORTCODES)) return true if (typeof emoji === 'string') return matchesGlyphSet(normalized, THUMBS_DOWN_GLYPHS) return false } function isArrowUp(emoji: TEmoji | string): boolean { const normalized = normalizedReactionString(emoji) if (normalized === undefined) return false if (matchesShortcodeSet(emoji, normalized, ARROW_UP_SHORTCODES)) return true if (typeof emoji === 'string') return matchesGlyphSet(normalized, ARROW_UP_GLYPHS) return false } function isArrowDown(emoji: TEmoji | string): boolean { const normalized = normalizedReactionString(emoji) if (normalized === undefined) return false if (matchesShortcodeSet(emoji, normalized, ARROW_DOWN_SHORTCODES)) return true if (typeof emoji === 'string') return matchesGlyphSet(normalized, ARROW_DOWN_GLYPHS) return false } function isDislike(emoji: TEmoji | string): boolean { const normalized = normalizedReactionString(emoji) if (normalized === undefined) return false if (matchesShortcodeSet(emoji, normalized, DISLIKE_SHORTCODES)) return true if (typeof emoji === 'string' && normalized.toLowerCase() === 'dislike') return true return false } /** * Generic positive/negative reactions (hearts, +, thumbs, arrows, explicit dislike) โ€” counted in * note stats only; not rendered as separate thread rows. */ export function isGenericStatsReactionEmoji(emoji: TEmoji | string): boolean { return ( isHeartOrPlusLike(emoji) || isThumbsUp(emoji) || isThumbsDown(emoji) || isArrowUp(emoji) || isArrowDown(emoji) || isDislike(emoji) ) } export function isGenericStatsReactionContent(content: string): boolean { return isGenericStatsReactionEmoji(content) } /** @deprecated Use {@link isGenericStatsReactionContent}. */ export const isLowEffortCollapsedReactionContent = isGenericStatsReactionContent