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,233 @@
import React, { useState, useRef, useEffect } from 'react';
import { Send, Paperclip } from 'lucide-react';
import FileUpload from './FileUpload';
import { uploadFile } from '@/app/actions/messagerieAction';
import logger from '@/utils/logger';
const MessageInput = ({
onSendMessage,
onTypingStart,
onTypingStop,
disabled = false,
placeholder = 'Tapez votre message...',
conversationId = null,
senderId = null,
}) => {
const [message, setMessage] = useState('');
const [isTyping, setIsTyping] = useState(false);
const [selectedFile, setSelectedFile] = useState(null);
const [showFileUpload, setShowFileUpload] = useState(false);
const textareaRef = useRef(null);
const typingTimeoutRef = useRef(null);
// Ajuster la hauteur du textarea automatiquement
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 120)}px`;
}
}, [message]);
const handleInputChange = (e) => {
const value = e.target.value;
setMessage(value);
// Gestion du statut de frappe
if (value.trim() && !isTyping) {
setIsTyping(true);
onTypingStart?.();
}
// Réinitialiser le timeout de frappe
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
typingTimeoutRef.current = setTimeout(() => {
if (isTyping) {
setIsTyping(false);
onTypingStop?.();
}
}, 1000);
};
const handleKeyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
const handleSend = () => {
const trimmedMessage = message.trim();
logger.debug('📝 MessageInput: handleSend appelé:', {
message,
trimmedMessage,
disabled,
});
if (!trimmedMessage || disabled) {
logger.debug('❌ MessageInput: Message vide ou désactivé');
return;
}
logger.debug(
'📤 MessageInput: Appel de onSendMessage avec:',
trimmedMessage
);
onSendMessage(trimmedMessage);
setMessage('');
// Arrêter le statut de frappe
if (isTyping) {
setIsTyping(false);
onTypingStop?.();
}
// Effacer le timeout
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
};
const handleFileSelect = (e) => {
const file = e.target.files[0];
if (file) {
setSelectedFile(file);
setShowFileUpload(true);
}
// Réinitialiser l'input file
e.target.value = '';
};
const handleFileUpload = async (
file,
conversationId,
senderId,
onProgress
) => {
try {
const result = await uploadFile(
file,
conversationId,
senderId,
onProgress
);
// Envoyer un message avec le fichier
onSendMessage('', {
type: 'file',
fileName: file.name,
fileSize: file.size,
fileType: file.type,
fileUrl: result.fileUrl,
});
// Réinitialiser l'état
setSelectedFile(null);
setShowFileUpload(false);
return result;
} catch (error) {
throw error;
}
};
const handleCancelFileUpload = () => {
setSelectedFile(null);
setShowFileUpload(false);
};
// Nettoyage du timeout lors du démontage du composant
useEffect(() => {
return () => {
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
};
}, []);
return (
<div className="border-t border-gray-200 bg-white">
{/* Aperçu d'upload de fichier */}
{showFileUpload && selectedFile && (
<div className="p-4 border-b border-gray-200">
<FileUpload
file={selectedFile}
onUpload={handleFileUpload}
onCancel={handleCancelFileUpload}
conversationId={conversationId}
senderId={senderId}
/>
</div>
)}
{/* Zone de saisie */}
<div className="p-4">
<div className="flex items-end space-x-3">
{/* Zone de saisie */}
<div className="flex-1 relative">
<textarea
ref={textareaRef}
value={message}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={disabled}
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl resize-none focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
style={{ minHeight: '44px', maxHeight: '120px' }}
/>
</div>
{/* Boutons à droite (trombone au-dessus, envoi en dessous) */}
<div className="flex flex-col space-y-2 flex-shrink-0">
{/* Bouton d'ajout de fichier */}
<div className="relative">
<input
type="file"
id="file-upload"
className="hidden"
onChange={handleFileSelect}
accept="image/*,application/pdf,.doc,.docx,.xls,.xlsx,.txt"
/>
<label
htmlFor="file-upload"
className="flex items-center justify-center w-10 h-10 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full cursor-pointer transition-colors"
>
<Paperclip className="w-5 h-5" />
</label>
</div>
{/* Bouton d'envoi */}
<button
onClick={handleSend}
disabled={!message.trim() || disabled}
className={`flex items-center justify-center w-10 h-10 rounded-full transition-all ${
message.trim() && !disabled
? 'bg-blue-500 hover:bg-blue-600 text-white shadow-lg hover:shadow-xl'
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
}`}
>
<Send className="w-5 h-5" />
</button>
</div>
</div>
{/* Indicateur de limite de caractères (optionnel) */}
{message.length > 900 && (
<div className="mt-2 text-right">
<span
className={`text-xs ${
message.length > 1000 ? 'text-red-500' : 'text-yellow-500'
}`}
>
{message.length}/1000
</span>
</div>
)}
</div>
</div>
);
};
export default MessageInput;