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,270 @@
import React, {
createContext,
useContext,
useState,
useEffect,
useRef,
useCallback,
} from 'react';
import { useSession } from 'next-auth/react';
import logger from '@/utils/logger';
import { WS_CHAT_URL } from '@/utils/Url';
const ChatConnectionContext = createContext();
export const ChatConnectionProvider = ({ children }) => {
const { data: session, status } = useSession(); // Ajouter le hook useSession
const [isConnected, setIsConnected] = useState(false);
const [connectionStatus, setConnectionStatus] = useState('disconnected'); // 'disconnected', 'connecting', 'connected', 'error'
const [userPresences, setUserPresences] = useState({}); // Nouvel état pour les présences
const websocketRef = useRef(null);
const reconnectTimeoutRef = useRef(null);
const [reconnectAttempts, setReconnectAttempts] = useState(0);
const [currentUserId, setCurrentUserId] = useState(null);
const maxReconnectAttempts = 5;
// Système de callbacks pour les messages
const messageCallbacksRef = useRef(new Set());
// Fonctions pour gérer les callbacks de messages
const addMessageCallback = useCallback((callback) => {
messageCallbacksRef.current.add(callback);
return () => {
messageCallbacksRef.current.delete(callback);
};
}, []);
const notifyMessageCallbacks = useCallback((data) => {
messageCallbacksRef.current.forEach((callback) => {
try {
callback(data);
} catch (error) {
logger.error('ChatConnection: Error in message callback', error);
}
});
}, []);
// Gestion des présences utilisateur
const handlePresenceUpdate = useCallback((data) => {
const { user_id, status } = data;
setUserPresences((prev) => ({
...prev,
[user_id]: { status },
}));
}, []);
// Configuration WebSocket
const getWebSocketUrl = (userId) => {
if (!userId) {
logger.warn('ChatConnection: No user ID provided for WebSocket URL');
return null;
}
// Récupérer le token d'authentification depuis NextAuth session
const token = session?.user?.token;
if (!token) {
logger.warn(
'ChatConnection: No access token found for WebSocket connection'
);
return null;
}
// Construire l'URL WebSocket avec le token
const baseUrl = WS_CHAT_URL(userId);
const wsUrl = `${baseUrl}?token=${encodeURIComponent(token)}`;
return wsUrl;
};
// Connexion WebSocket
const connectToChat = (userId = null) => {
const userIdToUse = userId || currentUserId;
// Vérifier que la session est chargée
if (status === 'loading') {
setConnectionStatus('connecting');
return;
}
if (status === 'unauthenticated' || !session) {
logger.warn('ChatConnection: User not authenticated');
setConnectionStatus('error');
return;
}
if (!userIdToUse) {
logger.warn('ChatConnection: Cannot connect without user ID');
setConnectionStatus('error');
return;
}
if (websocketRef.current?.readyState === WebSocket.OPEN) {
return;
}
setCurrentUserId(userIdToUse);
setConnectionStatus('connecting');
try {
const wsUrl = getWebSocketUrl(userIdToUse);
if (!wsUrl) {
throw new Error(
'Cannot generate WebSocket URL - missing token or user ID'
);
}
websocketRef.current = new WebSocket(wsUrl);
websocketRef.current.onopen = () => {
logger.info(
'ChatConnection: Connected successfully for user:',
userIdToUse
);
setIsConnected(true);
setConnectionStatus('connected');
setReconnectAttempts(0);
};
websocketRef.current.onclose = (event) => {
setIsConnected(false);
setConnectionStatus('disconnected');
// Tentative de reconnexion automatique
if (reconnectAttempts < maxReconnectAttempts && !event.wasClean) {
const timeout = Math.min(
1000 * Math.pow(2, reconnectAttempts),
30000
);
reconnectTimeoutRef.current = setTimeout(() => {
setReconnectAttempts((prev) => prev + 1);
connectToChat();
}, timeout);
}
};
websocketRef.current.onerror = (error) => {
logger.error('ChatConnection: WebSocket error', error);
setConnectionStatus('error');
setIsConnected(false);
};
websocketRef.current.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// Gérer les messages de présence
if (data.type === 'presence_update') {
handlePresenceUpdate(data);
}
// Notifier tous les callbacks enregistrés
notifyMessageCallbacks(data);
} catch (error) {
logger.error('ChatConnection: Error parsing message', error);
}
};
} catch (error) {
logger.error('ChatConnection: Error creating WebSocket', error);
setConnectionStatus('error');
setIsConnected(false);
}
};
// Déconnexion WebSocket
const disconnectFromChat = () => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
if (websocketRef.current) {
websocketRef.current.close(1000, 'User disconnected');
websocketRef.current = null;
}
setIsConnected(false);
setConnectionStatus('disconnected');
setReconnectAttempts(0);
logger.info('ChatConnection: Disconnected by user');
};
// Envoi de message
const sendMessage = (message) => {
if (websocketRef.current?.readyState === WebSocket.OPEN) {
const messageStr = JSON.stringify(message);
websocketRef.current.send(messageStr);
return true;
} else {
logger.warn('ChatConnection: Cannot send message - not connected');
return false;
}
};
// Obtenir la référence WebSocket pour les composants qui en ont besoin
const getWebSocket = () => websocketRef.current;
// Effet pour la gestion de la session et connexion automatique
useEffect(() => {
// Si la session change vers authenticated et qu'on a un user_id, essayer de se connecter
if (status === 'authenticated' && session?.user?.user_id && !isConnected) {
connectToChat(session.user.user_id);
}
// Si la session devient unauthenticated, déconnecter
if (status === 'unauthenticated' && isConnected) {
disconnectFromChat();
}
}, [
status,
session?.user?.user_id,
isConnected,
connectToChat,
disconnectFromChat,
]);
// Nettoyage à la destruction du composant
useEffect(() => {
return () => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
if (websocketRef.current) {
websocketRef.current.close();
}
};
}, []);
const value = {
isConnected,
connectionStatus,
userPresences, // Ajouter les présences utilisateur
connectToChat,
disconnectFromChat,
sendMessage,
getWebSocket,
reconnectAttempts,
maxReconnectAttempts,
addMessageCallback, // Ajouter cette fonction
};
return (
<ChatConnectionContext.Provider value={value}>
{children}
</ChatConnectionContext.Provider>
);
};
export const useChatConnection = () => {
const context = useContext(ChatConnectionContext);
if (!context) {
throw new Error(
'useChatConnection must be used within a ChatConnectionProvider'
);
}
return context;
};
export default ChatConnectionContext;