veza/apps/web/src/features/chat/components/ChatMessage.tsx

148 lines
5.5 KiB
TypeScript
Raw Normal View History

import React, { useState, lazy, Suspense } from 'react';
import { ChatMessage } from '../store/chatStore';
import { useAuthStore } from '@/features/auth/store/authStore';
import { cn } from '@/lib/utils';
2026-01-04 00:41:51 +00:00
import { Smile, MoreHorizontal } from 'lucide-react';
import { useChat } from '../hooks/useChat';
// 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';
interface ChatMessageProps {
message: ChatMessage;
}
2025-12-13 02:34:34 +00:00
export const ChatMessageComponent: React.FC<ChatMessageProps> = ({
message,
}) => {
const { user } = useAuthStore();
2026-01-04 00:41:51 +00:00
const { addReaction } = useChat();
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);
};
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',
)}
>
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>
</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">
<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>
)}
</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>
)}
</div>
);
2025-12-13 02:34:34 +00:00
};