![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/lavocat.quebec/private_html/src/components/ui/ |
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useSession } from 'next-auth/react';
import EmojiPicker, { saveRecentEmoji } from './EmojiPicker';
interface Reaction {
id: string;
emoji: string;
userId: string;
user: {
id: string;
name: string;
};
createdAt: string;
}
interface MessageReactionsProps {
messageId: string;
reactions: Reaction[];
onAddReaction: (messageId: string, emoji: string) => void;
onRemoveReaction: (messageId: string, emoji: string) => void;
className?: string;
}
const MessageReactions: React.FC<MessageReactionsProps> = ({
messageId,
reactions,
onAddReaction,
onRemoveReaction,
className = ''
}) => {
const { data: session } = useSession();
const [hoveredReaction, setHoveredReaction] = useState<string | null>(null);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
// Group reactions by emoji
const groupedReactions = reactions.reduce((acc, reaction) => {
if (!acc[reaction.emoji]) {
acc[reaction.emoji] = [];
}
acc[reaction.emoji].push(reaction);
return acc;
}, {} as Record<string, Reaction[]>);
const handleReactionClick = (emoji: string) => {
if (!session?.user?.id) return;
const userReaction = groupedReactions[emoji]?.find(
r => r.userId === session.user.id
);
if (userReaction) {
onRemoveReaction(messageId, emoji);
} else {
onAddReaction(messageId, emoji);
}
};
const getReactionTooltip = (emoji: string, reactions: Reaction[]) => {
const names = reactions.map(r => r.user.name);
if (names.length === 1) {
return `${names[0]} reacted with ${emoji}
} else if (names.length === 2) {
return `${names[0]} and ${names[1]} reacted with ${emoji}
} else if (names.length <= 5) {
return `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]} reacted with ${emoji}
} else {
return `${names.slice(0, 3).join(', ')} and ${names.length - 3} others reacted with ${emoji}
}
};
const handleEmojiSelect = (emoji: string) => {
onAddReaction(messageId, emoji);
saveRecentEmoji(emoji);
setShowEmojiPicker(false);
};
return (
<div className={`flex flex-wrap items-center gap-1 mt-1 ${className}
<AnimatePresence>
{Object.entries(groupedReactions).map(([emoji, reactionList]) => {
const hasUserReaction = session?.user?.id &&
reactionList.some(r => r.userId === session.user.id);
return (
<motion.button
key={emoji}
initial={{ scale: 0 }}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
onClick={() => handleReactionClick(emoji)}
onMouseEnter={() => setHoveredReaction(emoji)}
onMouseLeave={() => setHoveredReaction(null)}
className={
relative inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium
transition-all duration-200 border
${hasUserReaction
? 'bg-blue-100 dark:bg-blue-900/30 border-blue-300 dark:border-blue-600 text-blue-700 dark:text-blue-300'
: 'bg-gray-100 dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}
title={getReactionTooltip(emoji, reactionList)}
>
<span className="text-sm">{emoji}</span>
<span className="text-xs font-medium">{reactionList.length}</span>
{/* Tooltip */}
<AnimatePresence>
{hoveredReaction === emoji && (
<motion.div
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 5 }}
transition={{ duration: 0.15 }}
className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 z-10"
>
<div className="bg-gray-900 dark:bg-gray-700 text-white text-xs rounded-lg px-3 py-2 whitespace-nowrap max-w-xs">
{getReactionTooltip(emoji, reactionList)}
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"></div>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.button>
);
})}
</AnimatePresence>
{/* Add reaction button */}
<div className="relative">
<motion.button
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors opacity-60 hover:opacity-100"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
title="Add reaction"
>
<span className="text-xs">+</span>
</motion.button>
<EmojiPicker
isOpen={showEmojiPicker}
onClose={() => setShowEmojiPicker(false)}
onEmojiSelect={handleEmojiSelect}
position="top"
/>
</div>
</div>
);
};
export default MessageReactions;