174 lines
6.4 KiB
TypeScript
174 lines
6.4 KiB
TypeScript
|
|
import { useEffect, useRef } from 'react';
|
||
|
|
import { useChatStore } from '@/stores/chat';
|
||
|
|
import { useAuthStore } from '@/stores/auth';
|
||
|
|
import { Card, CardContent } from '@/components/ui/card';
|
||
|
|
import { Button } from '@/components/ui/button';
|
||
|
|
import { sanitizeChatMessage } from '@/utils/sanitize';
|
||
|
|
import {
|
||
|
|
MoreVertical,
|
||
|
|
Reply,
|
||
|
|
Smile,
|
||
|
|
ThumbsUp,
|
||
|
|
ThumbsDown,
|
||
|
|
MessageSquare,
|
||
|
|
} from 'lucide-react';
|
||
|
|
|
||
|
|
export function ChatMessages() {
|
||
|
|
const { currentConversation, messages, typingUsers } = useChatStore();
|
||
|
|
const { user } = useAuthStore();
|
||
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||
|
|
|
||
|
|
const conversationMessages = currentConversation
|
||
|
|
? messages[currentConversation.id] || []
|
||
|
|
: [];
|
||
|
|
|
||
|
|
const typingUserIds = currentConversation
|
||
|
|
? typingUsers[currentConversation.id] || []
|
||
|
|
: [];
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||
|
|
}, [conversationMessages]);
|
||
|
|
|
||
|
|
if (!currentConversation) {
|
||
|
|
return (
|
||
|
|
<div className='flex-1 flex items-center justify-center bg-muted/50'>
|
||
|
|
<div className='text-center'>
|
||
|
|
<MessageSquare className='h-12 w-12 mx-auto mb-4 text-muted-foreground' />
|
||
|
|
<h3 className='text-lg font-medium text-muted-foreground'>
|
||
|
|
Sélectionnez une conversation
|
||
|
|
</h3>
|
||
|
|
<p className='text-sm text-muted-foreground'>
|
||
|
|
Choisissez une conversation pour commencer à discuter
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className='flex-1 flex flex-col'>
|
||
|
|
{/* En-tête de la conversation */}
|
||
|
|
<div className='p-4 border-b bg-background'>
|
||
|
|
<div className='flex items-center justify-between'>
|
||
|
|
<div>
|
||
|
|
<h3 className='font-semibold'>
|
||
|
|
{currentConversation.name ||
|
||
|
|
`Conversation ${currentConversation.id.slice(0, 8)}`}
|
||
|
|
</h3>
|
||
|
|
<p className='text-sm text-muted-foreground'>
|
||
|
|
{currentConversation.participants.length} participant
|
||
|
|
{currentConversation.participants.length > 1 ? 's' : ''}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<Button variant='ghost' size='icon'>
|
||
|
|
<MoreVertical className='h-4 w-4' />
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Messages */}
|
||
|
|
<div className='flex-1 overflow-y-auto p-4 space-y-4'>
|
||
|
|
{conversationMessages.length === 0 ? (
|
||
|
|
<div className='text-center text-muted-foreground'>
|
||
|
|
<p>Aucun message dans cette conversation</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
conversationMessages.map(message => {
|
||
|
|
const isOwn = message.sender_id === user?.id;
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
key={message.id}
|
||
|
|
className={`flex ${isOwn ? 'justify-end' : 'justify-start'}`}
|
||
|
|
>
|
||
|
|
<div className={`max-w-[70%] ${isOwn ? 'order-2' : 'order-1'}`}>
|
||
|
|
<div className='flex items-center space-x-2 mb-1'>
|
||
|
|
<span className='text-xs text-muted-foreground'>
|
||
|
|
{isOwn ? 'Vous' : `Utilisateur ${message.sender_id}`}
|
||
|
|
</span>
|
||
|
|
<span className='text-xs text-muted-foreground'>
|
||
|
|
{new Date(message.created_at).toLocaleTimeString([], {
|
||
|
|
hour: '2-digit',
|
||
|
|
minute: '2-digit',
|
||
|
|
})}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<Card
|
||
|
|
className={`${isOwn ? 'bg-primary text-primary-foreground' : ''}`}
|
||
|
|
>
|
||
|
|
<CardContent className='p-3'>
|
||
|
|
<p
|
||
|
|
className='text-sm'
|
||
|
|
dangerouslySetInnerHTML={{
|
||
|
|
__html: sanitizeChatMessage(message.content)
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Réactions */}
|
||
|
|
{message.reactions && message.reactions.length > 0 && (
|
||
|
|
<div className='flex flex-wrap gap-1 mt-2'>
|
||
|
|
{message.reactions.map((reaction, index) => (
|
||
|
|
<Button
|
||
|
|
key={index}
|
||
|
|
variant='ghost'
|
||
|
|
size='sm'
|
||
|
|
className='h-6 px-2 text-xs'
|
||
|
|
>
|
||
|
|
{reaction.emoji} {reaction.user_id}
|
||
|
|
</Button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Actions sur le message */}
|
||
|
|
<div className='flex items-center space-x-1 mt-1 opacity-0 group-hover:opacity-100 transition-opacity'>
|
||
|
|
<Button variant='ghost' size='sm' className='h-6 px-2'>
|
||
|
|
<ThumbsUp className='h-3 w-3' />
|
||
|
|
</Button>
|
||
|
|
<Button variant='ghost' size='sm' className='h-6 px-2'>
|
||
|
|
<ThumbsDown className='h-3 w-3' />
|
||
|
|
</Button>
|
||
|
|
<Button variant='ghost' size='sm' className='h-6 px-2'>
|
||
|
|
<Reply className='h-3 w-3' />
|
||
|
|
</Button>
|
||
|
|
<Button variant='ghost' size='sm' className='h-6 px-2'>
|
||
|
|
<Smile className='h-3 w-3' />
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
})
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Indicateur de frappe */}
|
||
|
|
{typingUserIds.length > 0 && (
|
||
|
|
<div className='flex items-center space-x-2 text-sm text-muted-foreground'>
|
||
|
|
<div className='flex space-x-1'>
|
||
|
|
<div className='w-2 h-2 bg-muted-foreground rounded-full animate-bounce'></div>
|
||
|
|
<div
|
||
|
|
className='w-2 h-2 bg-muted-foreground rounded-full animate-bounce'
|
||
|
|
style={{ animationDelay: '0.1s' }}
|
||
|
|
></div>
|
||
|
|
<div
|
||
|
|
className='w-2 h-2 bg-muted-foreground rounded-full animate-bounce'
|
||
|
|
style={{ animationDelay: '0.2s' }}
|
||
|
|
></div>
|
||
|
|
</div>
|
||
|
|
<span>
|
||
|
|
{typingUserIds.length === 1
|
||
|
|
? "Quelqu'un"
|
||
|
|
: `${typingUserIds.length} personnes`}{' '}
|
||
|
|
est en train d'écrire...
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div ref={messagesEndRef} />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|