Files
eranos/src/components/ReactionButton.tsx
T
Chad Curtis 35422ec6bb Fix optimistic updates, show user reaction emoji, compact embeds, and unify repost menu
- Add true optimistic updates for reactions and reposts via setQueryData
  instead of only invalidating queries after publish
- Track user's own reaction per event and display their emoji in place of
  the Heart icon across feed, post details, and notifications
- Strip whitespace around block embeds and skipped media URLs to eliminate
  blank lines caused by whitespace-pre-wrap
- Only render link preview cards for standalone URLs; inline URLs in
  sentences render as plain clickable links
- Remove border-b separators and use fixed-height object-cover on preview
  images to eliminate gaps
- Tighten padding on EmbeddedNote and EmbeddedNaddr cards
- Use fixed-height object-cover for ImageGallery grid images
- Unify RepostMenu to use Popover on all screen sizes instead of Drawer
  on mobile
2026-02-19 16:19:19 -06:00

102 lines
3.1 KiB
TypeScript

import { useState, useRef, useCallback } from 'react';
import { Heart } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { QuickReactMenu } from '@/components/QuickReactMenu';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useUserReaction } from '@/hooks/useUserReaction';
import { cn } from '@/lib/utils';
interface ReactionButtonProps {
/** 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;
/** Current reaction count from stats. */
reactionCount?: number;
/** Optional extra class names. */
className?: string;
}
export function ReactionButton({
eventId,
eventPubkey,
eventKind,
reactionCount = 0,
className,
}: ReactionButtonProps) {
const { user } = useCurrentUser();
const [menuOpen, setMenuOpen] = useState(false);
const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const userReaction = useUserReaction(eventId);
const hasReacted = !!userReaction;
const handleMouseEnter = useCallback(() => {
if (!user) return;
// Clear any pending close timeout
if (closeTimeoutRef.current) {
clearTimeout(closeTimeoutRef.current);
closeTimeoutRef.current = null;
}
setMenuOpen(true);
}, [user]);
const handleMouseLeave = useCallback(() => {
// Delay closing to allow user to move to the menu
closeTimeoutRef.current = setTimeout(() => {
setMenuOpen(false);
}, 150);
}, []);
return (
<Popover open={menuOpen} onOpenChange={setMenuOpen}>
<PopoverTrigger asChild>
<button
className={cn(
'flex items-center gap-1.5 p-2 rounded-full transition-colors',
hasReacted
? 'text-pink-500'
: 'text-muted-foreground hover:text-pink-500 hover:bg-pink-500/10',
className,
)}
title="React"
onClick={(e) => {
e.stopPropagation();
if (!user) return;
setMenuOpen((prev) => !prev);
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{hasReacted ? (
<span className="size-5 flex items-center justify-center text-base leading-none">{userReaction}</span>
) : (
<Heart className="size-5" />
)}
{reactionCount > 0 && (
<span className={cn('text-sm tabular-nums', hasReacted && 'text-pink-500')}>{reactionCount}</span>
)}
</button>
</PopoverTrigger>
<PopoverContent
className="w-auto p-0 border-0 bg-transparent shadow-none"
side="top"
align="start"
onClick={(e) => e.stopPropagation()}
onOpenAutoFocus={(e) => e.preventDefault()}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<QuickReactMenu
eventId={eventId}
eventPubkey={eventPubkey}
eventKind={eventKind}
/>
</PopoverContent>
</Popover>
);
}