feat: mise en place de la messagerie [#17]

This commit is contained in:
Luc SORIGNET
2025-05-26 13:24:42 +02:00
parent e2df29d851
commit d37145b73e
64 changed files with 13113 additions and 853 deletions

View File

@ -0,0 +1,196 @@
import React from 'react';
import { User, Trash2 } from 'lucide-react';
import { getGravatarUrl } from '@/utils/gravatar';
const ConversationItem = ({
conversation,
isSelected,
onClick,
onDelete, // Nouvelle prop pour la suppression
unreadCount = 0,
lastMessage,
isTyping = false,
userPresences = {}, // Nouveau prop pour les statuts de présence
}) => {
const formatTime = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
const now = new Date();
const diffInHours = (now - date) / (1000 * 60 * 60);
if (diffInHours < 24) {
return date.toLocaleTimeString('fr-FR', {
hour: '2-digit',
minute: '2-digit',
});
} else {
return date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
});
}
};
const getInterlocutorName = () => {
if (conversation.interlocuteur) {
// Si nous avons le nom et prénom, les utiliser
if (
conversation.interlocuteur.first_name &&
conversation.interlocuteur.last_name
) {
return `${conversation.interlocuteur.first_name} ${conversation.interlocuteur.last_name}`;
}
// Sinon, utiliser l'email comme fallback
if (conversation.interlocuteur.email) {
return conversation.interlocuteur.email;
}
}
return conversation.name || 'Utilisateur inconnu';
};
const getLastMessageText = () => {
if (isTyping) {
return (
<span className="text-emerald-500 italic">Tape un message...</span>
);
}
if (lastMessage) {
return lastMessage.content || lastMessage.corpus || 'Message...';
}
if (conversation.last_message) {
return (
conversation.last_message.content ||
conversation.last_message.corpus ||
'Message...'
);
}
return 'Aucun message';
};
const getLastMessageTime = () => {
if (lastMessage) {
return formatTime(lastMessage.created_at || lastMessage.date_envoi);
}
if (conversation.last_message) {
return formatTime(
conversation.last_message.created_at ||
conversation.last_message.date_envoi
);
}
return '';
};
const getUserPresenceStatus = () => {
if (conversation.interlocuteur?.id) {
const presence = userPresences[conversation.interlocuteur.id];
return presence?.status || 'offline';
}
return 'offline';
};
const getPresenceColor = (status) => {
switch (status) {
case 'online':
return 'bg-emerald-400';
case 'away':
return 'bg-yellow-400';
case 'busy':
return 'bg-red-400';
case 'offline':
default:
return 'bg-gray-400';
}
};
const getPresenceLabel = (status) => {
switch (status) {
case 'online':
return 'En ligne';
case 'away':
return 'Absent';
case 'busy':
return 'Occupé';
case 'offline':
default:
return 'Hors ligne';
}
};
const presenceStatus = getUserPresenceStatus();
return (
<div
className={`group flex items-center p-3 cursor-pointer rounded-lg transition-all duration-200 hover:bg-gray-50 ${
isSelected
? 'bg-emerald-50 border-l-4 border-emerald-500'
: 'hover:bg-gray-50'
}`}
onClick={onClick}
>
{/* Avatar */}
<div className="relative">
<img
src={getGravatarUrl(
conversation.interlocuteur?.email || 'default',
48
)}
alt={`Avatar de ${getInterlocutorName()}`}
className="w-12 h-12 rounded-full object-cover shadow-md"
/>
{/* Indicateur de statut en ligne */}
<div
className={`absolute -bottom-0.5 -right-0.5 w-4 h-4 ${getPresenceColor(presenceStatus)} border-2 border-white rounded-full`}
title={getPresenceLabel(presenceStatus)}
></div>
</div>
{/* Contenu de la conversation */}
<div className="flex-1 ml-3 overflow-hidden">
<div className="flex items-center justify-between">
<h3
className={`font-semibold truncate ${
isSelected ? 'text-emerald-700' : 'text-gray-900'
}`}
>
{getInterlocutorName()}
</h3>
<div className="flex items-center space-x-2">
{unreadCount > 0 && (
<span className="bg-red-500 text-white text-xs rounded-full w-4 h-4 text-center"></span>
)}
<span className="text-xs text-gray-500">
{getLastMessageTime()}
</span>
{/* Bouton de suppression */}
{onDelete && (
<button
onClick={(e) => {
e.stopPropagation(); // Empêcher la sélection de la conversation
onDelete();
}}
className="opacity-0 group-hover:opacity-100 hover:bg-red-100 p-1 rounded transition-all duration-200"
title="Supprimer la conversation"
>
<Trash2 className="w-4 h-4 text-red-500 hover:text-red-700" />
</button>
)}
</div>
</div>
<p
className={`text-sm truncate mt-1 ${isTyping ? '' : 'text-gray-600'}`}
>
{getLastMessageText()}
</p>
</div>
</div>
);
};
export default ConversationItem;