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,249 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useSession } from 'next-auth/react';
import { WS_CHAT_URL } from '@/utils/Url';
import logger from '@/utils/logger';
const useWebSocket = (userId, onMessage, onConnectionChange) => {
const [isConnected, setIsConnected] = useState(false);
const [connectionStatus, setConnectionStatus] = useState('disconnected');
const wsRef = useRef(null);
const reconnectTimeoutRef = useRef(null);
const reconnectAttemptsRef = useRef(0);
const isConnectingRef = useRef(false); // Empêcher les connexions multiples
const maxReconnectAttempts = 5;
// Récupération du token JWT
const { data: session } = useSession();
const authToken = session?.user?.token;
// Références stables pour les callbacks
const onMessageRef = useRef(onMessage);
const onConnectionChangeRef = useRef(onConnectionChange);
useEffect(() => {
onMessageRef.current = onMessage;
}, [onMessage]);
useEffect(() => {
onConnectionChangeRef.current = onConnectionChange;
}, [onConnectionChange]);
const connect = useCallback(() => {
if (!userId || !authToken) {
logger.warn('WebSocket: userId ou token manquant');
return;
}
// Empêcher les connexions multiples simultanées
if (
isConnectingRef.current ||
(wsRef.current && wsRef.current.readyState === WebSocket.CONNECTING)
) {
logger.debug('WebSocket: connexion déjà en cours');
return;
}
// Fermer la connexion existante si elle existe
if (wsRef.current) {
wsRef.current.close();
wsRef.current = null;
}
isConnectingRef.current = true;
try {
// Ajouter le token à l'URL du WebSocket
const wsUrl = new URL(WS_CHAT_URL(userId));
wsUrl.searchParams.append('token', authToken);
wsRef.current = new WebSocket(wsUrl.toString());
wsRef.current.onopen = () => {
logger.debug('WebSocket connecté');
isConnectingRef.current = false;
setIsConnected(true);
setConnectionStatus('connected');
reconnectAttemptsRef.current = 0;
onConnectionChangeRef.current?.(true);
};
wsRef.current.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
onMessageRef.current?.(data);
} catch (error) {
logger.error('Erreur lors du parsing du message WebSocket:', error);
}
};
wsRef.current.onclose = (event) => {
logger.debug('WebSocket fermé:', event.code, event.reason);
isConnectingRef.current = false;
setIsConnected(false);
setConnectionStatus('disconnected');
onConnectionChangeRef.current?.(false);
// Tentative de reconnexion automatique seulement si la fermeture n'est pas intentionnelle
if (
event.code !== 1000 &&
reconnectAttemptsRef.current < maxReconnectAttempts
) {
reconnectAttemptsRef.current++;
setConnectionStatus('reconnecting');
const delay = Math.min(
1000 * Math.pow(2, reconnectAttemptsRef.current),
30000
);
reconnectTimeoutRef.current = setTimeout(() => {
logger.debug(
`Tentative de reconnexion ${reconnectAttemptsRef.current}/${maxReconnectAttempts}`
);
connect();
}, delay);
} else {
setConnectionStatus('failed');
}
};
wsRef.current.onerror = (error) => {
logger.error('Erreur WebSocket:', error);
isConnectingRef.current = false;
setConnectionStatus('error');
};
} catch (error) {
logger.error('Erreur lors de la création du WebSocket:', error);
isConnectingRef.current = false;
setConnectionStatus('error');
}
}, [userId, authToken]);
const disconnect = useCallback(() => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
if (wsRef.current) {
wsRef.current.close();
wsRef.current = null;
}
setIsConnected(false);
setConnectionStatus('disconnected');
}, []);
const sendMessage = useCallback(
(message) => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
// Ajouter le token à chaque message
const messageWithAuth = {
...message,
token: authToken,
};
wsRef.current.send(JSON.stringify(messageWithAuth));
return true;
} else {
logger.warn("WebSocket non connecté, impossible d'envoyer le message");
return false;
}
},
[authToken]
);
const sendTypingStart = useCallback(
(conversationId) => {
sendMessage({
type: 'typing_start',
conversation_id: conversationId,
});
},
[sendMessage]
);
const sendTypingStop = useCallback(
(conversationId) => {
sendMessage({
type: 'typing_stop',
conversation_id: conversationId,
});
},
[sendMessage]
);
const markAsRead = useCallback(
(conversationId) => {
sendMessage({
type: 'mark_as_read',
conversation_id: conversationId,
});
},
[sendMessage]
);
const joinConversation = useCallback(
(conversationId) => {
sendMessage({
type: 'join_conversation',
conversation_id: conversationId,
});
},
[sendMessage]
);
const leaveConversation = useCallback(
(conversationId) => {
sendMessage({
type: 'leave_conversation',
conversation_id: conversationId,
});
},
[sendMessage]
);
const sendChatMessage = useCallback(
(conversationId, content, attachment = null) => {
const messageData = {
type: 'send_message',
conversation_id: conversationId,
content: content,
message_type: attachment ? 'file' : 'text',
};
// Ajouter les informations du fichier si présent
if (attachment) {
messageData.attachment = attachment;
}
return sendMessage(messageData);
},
[sendMessage]
);
useEffect(() => {
// Se connecter seulement si on a un userId et un token
if (userId && authToken) {
connect();
}
return () => {
disconnect();
};
}, [userId, authToken]); // Retirer connect et disconnect des dépendances
return {
isConnected,
connectionStatus,
sendMessage,
sendTypingStart,
sendTypingStop,
markAsRead,
joinConversation,
leaveConversation,
sendChatMessage,
reconnect: connect,
disconnect,
};
};
export default useWebSocket;