import { useState, useCallback, useMemo } from 'react'; import { MoreHorizontal } from 'lucide-react'; import { useQueryClient } from '@tanstack/react-query'; import { EmojiPicker } from '@/components/EmojiPicker'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useEmojiUsage } from '@/hooks/useEmojiUsage'; import { cn } from '@/lib/utils'; import type { EventStats } from '@/hooks/useTrending'; import type { ResolvedEmoji } from '@/components/CustomEmoji'; interface QuickReactMenuProps { /** The event ID being reacted to. */ eventId: string; /** The pubkey of the event author. */ eventPubkey: string; /** The kind number of the event being reacted to. */ eventKind: number; /** Called after an emoji is selected so the parent can close the popover. */ onClose?: () => void; /** Called when the full picker is opened/closed so the parent can lock the popover open. */ onExpandChange?: (expanded: boolean) => void; /** Optional extra class names. */ className?: string; } export function QuickReactMenu({ eventId, eventPubkey, eventKind, onClose, onExpandChange, className, }: QuickReactMenuProps) { const { user } = useCurrentUser(); const { mutate: publishEvent } = useNostrPublish(); const queryClient = useQueryClient(); const { trackEmojiUsage, getTopEmojis } = useEmojiUsage(); const [showFullPicker, setShowFullPicker] = useState(false); const [selectedEmoji, setSelectedEmoji] = useState(null); // Get user's most-used emojis (or defaults) const quickEmojis = useMemo(() => getTopEmojis(6), [getTopEmojis]); const handleEmojiSelect = useCallback((emoji: string) => { if (!user) return; // Close the entire popover (don't reset showFullPicker first — that // causes the quick-select to flash before the popover unmounts). onClose?.(); // Set selected emoji for optimistic update setSelectedEmoji(emoji); // Track emoji usage trackEmojiUsage(emoji); // Optimistically update stats cache immediately const displayEmoji = (emoji === '+' || emoji === '') ? '👍' : emoji; const resolvedEmoji: ResolvedEmoji = { content: displayEmoji }; const prevStats = queryClient.getQueryData(['event-stats', eventId]); if (prevStats) { queryClient.setQueryData(['event-stats', eventId], { ...prevStats, reactions: prevStats.reactions + 1, reactionEmojis: prevStats.reactionEmojis.some((e) => e.content === displayEmoji) ? prevStats.reactionEmojis : [...prevStats.reactionEmojis, resolvedEmoji], }); } // Store user's own reaction for this event queryClient.setQueryData(['user-reaction', eventId], resolvedEmoji); // Publish kind 7 reaction publishEvent( { kind: 7, content: emoji, created_at: Math.floor(Date.now() / 1000), tags: [ ['e', eventId], ['p', eventPubkey], ['k', String(eventKind)], ], }, { onSuccess: () => { // Delay invalidation so the relay has time to index the new event. // Without this, the refetch returns stale counts and overwrites // the optimistic update. setTimeout(() => { queryClient.invalidateQueries({ queryKey: ['event-stats', eventId] }); queryClient.invalidateQueries({ queryKey: ['event-interactions', eventId] }); }, 3000); }, onError: () => { // Revert optimistic update on failure setSelectedEmoji(null); // Revert stats if (prevStats) { queryClient.setQueryData(['event-stats', eventId], prevStats); } // Remove user reaction queryClient.removeQueries({ queryKey: ['user-reaction', eventId] }); }, }, ); }, [user, eventId, eventPubkey, eventKind, publishEvent, queryClient, trackEmojiUsage, onClose]); if (!user) return null; if (showFullPicker) { return (
e.stopPropagation()} >
); } return (
e.stopPropagation()} > {/* Quick emoji buttons */} {quickEmojis.map((emoji) => ( ))} {/* More button to show full picker */}
); }