2026-01-07 09:32:53 +00:00
|
|
|
import React, { useState, lazy, Suspense } from 'react';
|
2025-12-03 21:56:50 +00:00
|
|
|
import { ChatMessage } from '../store/chatStore';
|
2025-12-26 08:11:41 +00:00
|
|
|
import { useAuthStore } from '@/features/auth/store/authStore';
|
2025-12-03 21:56:50 +00:00
|
|
|
import { cn } from '@/lib/utils';
|
2026-01-04 00:41:51 +00:00
|
|
|
import { Smile, MoreHorizontal } from 'lucide-react';
|
|
|
|
|
import { useChat } from '../hooks/useChat';
|
2026-01-07 09:32:53 +00:00
|
|
|
// PERF: Lazy load EmojiPicker (composant volumineux ~200KB)
|
|
|
|
|
const EmojiPicker = lazy(() => import('emoji-picker-react').then(module => ({ default: module.default })));
|
|
|
|
|
import { Theme } from 'emoji-picker-react';
|
|
|
|
|
import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
interface ChatMessageProps {
|
|
|
|
|
message: ChatMessage;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 02:34:34 +00:00
|
|
|
export const ChatMessageComponent: React.FC<ChatMessageProps> = ({
|
|
|
|
|
message,
|
|
|
|
|
}) => {
|
2025-12-03 21:56:50 +00:00
|
|
|
const { user } = useAuthStore();
|
2026-01-04 00:41:51 +00:00
|
|
|
const { addReaction } = useChat();
|
2025-12-25 13:27:28 +00:00
|
|
|
const isMe = user?.id === message.sender_id;
|
2026-01-04 00:41:51 +00:00
|
|
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
|
|
|
|
|
|
|
|
|
const handleEmojiClick = (emojiData: { emoji: string }) => {
|
|
|
|
|
addReaction(message.id, emojiData.emoji);
|
|
|
|
|
setShowEmojiPicker(false);
|
|
|
|
|
};
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
2026-01-04 00:41:51 +00:00
|
|
|
'group flex flex-col gap-1 p-1 max-w-[80%] my-1',
|
|
|
|
|
isMe ? 'ml-auto items-end' : 'mr-auto items-start',
|
2025-12-03 21:56:50 +00:00
|
|
|
)}
|
|
|
|
|
>
|
2026-01-04 00:41:51 +00:00
|
|
|
<div className="flex items-center gap-2 px-2">
|
|
|
|
|
<span className="font-semibold text-xs opacity-70">
|
|
|
|
|
{isMe
|
|
|
|
|
? 'Moi'
|
|
|
|
|
: message.sender_username || `Utilisateur ${message.sender_id.slice(0, 8)}`}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-[10px] opacity-50">
|
|
|
|
|
{new Date(message.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="relative flex items-center gap-2">
|
|
|
|
|
{isMe && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
|
|
|
|
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-gray-100 rounded-full transition-opacity"
|
|
|
|
|
>
|
|
|
|
|
<Smile size={16} className="text-gray-500" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
'px-3 py-2 rounded-2xl text-sm shadow-sm',
|
|
|
|
|
isMe
|
|
|
|
|
? 'bg-blue-600 text-white rounded-tr-none'
|
|
|
|
|
: 'bg-white border text-gray-800 rounded-tl-none',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{/* Attachments */}
|
|
|
|
|
{message.attachments && message.attachments.length > 0 && (
|
|
|
|
|
<div className="mb-2 flex flex-wrap gap-2">
|
|
|
|
|
{message.attachments.map((att, i) => (
|
|
|
|
|
<div key={i} className="max-w-full overflow-hidden rounded-lg">
|
|
|
|
|
{att.file_type.startsWith('image') ? (
|
|
|
|
|
<img
|
|
|
|
|
src={att.file_url}
|
|
|
|
|
alt={att.file_name}
|
|
|
|
|
className="max-h-60 object-contain cursor-pointer hover:opacity-90"
|
|
|
|
|
onClick={() => window.open(att.file_url, '_blank')}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<a
|
|
|
|
|
href={att.file_url}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="flex items-center gap-2 p-2 bg-gray-100 text-gray-800 rounded hover:bg-gray-200"
|
|
|
|
|
>
|
|
|
|
|
<MoreHorizontal size={16} />
|
|
|
|
|
<span className="truncate max-w-[150px]">{att.file_name}</span>
|
|
|
|
|
</a>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<p className="whitespace-pre-wrap break-words">{message.content}</p>
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
2026-01-04 00:41:51 +00:00
|
|
|
|
|
|
|
|
{!isMe && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
|
|
|
|
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-gray-100 rounded-full transition-opacity"
|
|
|
|
|
>
|
|
|
|
|
<Smile size={16} className="text-gray-500" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{showEmojiPicker && (
|
|
|
|
|
<div className="absolute z-50 bottom-full mb-2">
|
|
|
|
|
<div className="fixed inset-0" onClick={() => setShowEmojiPicker(false)} />
|
|
|
|
|
<div className="relative">
|
2026-01-07 09:32:53 +00:00
|
|
|
<Suspense fallback={<div className="w-[352px] h-[435px] bg-white rounded-lg flex items-center justify-center"><LoadingSpinner size="sm" /></div>}>
|
|
|
|
|
<EmojiPicker
|
|
|
|
|
onEmojiClick={handleEmojiClick}
|
|
|
|
|
theme={Theme.LIGHT}
|
|
|
|
|
lazyLoadEmojis={true}
|
|
|
|
|
/>
|
|
|
|
|
</Suspense>
|
2026-01-04 00:41:51 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
2026-01-04 00:41:51 +00:00
|
|
|
|
|
|
|
|
{/* Reactions Display */}
|
|
|
|
|
{message.reactions && Object.keys(message.reactions).length > 0 && (
|
|
|
|
|
<div className={cn(
|
|
|
|
|
"flex flex-wrap gap-1 px-1",
|
|
|
|
|
isMe ? "justify-end" : "justify-start"
|
|
|
|
|
)}>
|
|
|
|
|
{Object.entries(message.reactions).map(([emoji, users]) => (
|
|
|
|
|
<button
|
|
|
|
|
key={emoji}
|
|
|
|
|
onClick={() => addReaction(message.id, emoji)}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs border transition-all",
|
|
|
|
|
users.includes(user?.id || '')
|
|
|
|
|
? "bg-blue-50 border-blue-200 text-blue-700"
|
|
|
|
|
: "bg-gray-50 border-gray-100 text-gray-600 hover:border-gray-200"
|
|
|
|
|
)}
|
|
|
|
|
title={users.length > 1 ? `${users.length} personnes ont réagi` : "1 personne a réagi"}
|
|
|
|
|
>
|
|
|
|
|
<span>{emoji}</span>
|
|
|
|
|
{users.length > 1 && <span className="font-semibold">{users.length}</span>}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
2025-12-13 02:34:34 +00:00
|
|
|
};
|