From a81b76eceac0919fc1873cdf8362e5fe55b97adb Mon Sep 17 00:00:00 2001 From: Luc SORIGNET Date: Sun, 5 Apr 2026 15:02:51 +0200 Subject: [PATCH] fix: messagerie --- Back-End/GestionMessagerie/consumers.py | 4 +- Front-End/src/app/[locale]/admin/layout.js | 12 ++- Front-End/src/app/[locale]/parents/layout.js | 16 ++-- .../src/app/[locale]/users/login/page.js | 7 +- Front-End/src/components/Footer.js | 2 +- Front-End/src/components/Sidebar.js | 12 ++- .../src/context/ChatConnectionContext.js | 94 ++++++++++++++----- Front-End/src/pages/api/download.js | 2 - 8 files changed, 103 insertions(+), 46 deletions(-) diff --git a/Back-End/GestionMessagerie/consumers.py b/Back-End/GestionMessagerie/consumers.py index ba32b84..f3ab9cf 100644 --- a/Back-End/GestionMessagerie/consumers.py +++ b/Back-End/GestionMessagerie/consumers.py @@ -72,11 +72,11 @@ class ChatConsumer(AsyncWebsocketConsumer): if presence: await self.broadcast_presence_update(self.user_id, 'online') + await self.accept() + # Envoyer les statuts de présence existants des autres utilisateurs connectés await self.send_existing_user_presences() - await self.accept() - logger.info(f"User {self.user_id} connected to chat") async def send_existing_user_presences(self): diff --git a/Front-End/src/app/[locale]/admin/layout.js b/Front-End/src/app/[locale]/admin/layout.js index fcd6925..ea2922f 100644 --- a/Front-End/src/app/[locale]/admin/layout.js +++ b/Front-End/src/app/[locale]/admin/layout.js @@ -34,12 +34,13 @@ import Footer from '@/components/Footer'; import MobileTopbar from '@/components/MobileTopbar'; import { RIGHTS } from '@/utils/rights'; import { useEstablishment } from '@/context/EstablishmentContext'; +import { useChatConnection } from '@/context/ChatConnectionContext'; export default function Layout({ children }) { const t = useTranslations('sidebar'); const [isSidebarOpen, setIsSidebarOpen] = useState(false); - const { profileRole, establishments, clearContext } = - useEstablishment(); + const { profileRole, establishments, clearContext } = useEstablishment(); + const { totalUnreadCount, resetUnreadCount } = useChatConnection(); const sidebarItems = { admin: { @@ -83,6 +84,7 @@ export default function Layout({ children }) { name: t('messagerie'), url: FE_ADMIN_MESSAGERIE_URL, icon: MessageSquare, + badge: totalUnreadCount, }, feedback: { id: 'feedback', @@ -119,7 +121,11 @@ export default function Layout({ children }) { useEffect(() => { // Fermer la sidebar quand on change de page sur mobile setIsSidebarOpen(false); - }, [pathname]); + // Réinitialiser le compteur non lu quand on ouvre la messagerie + if (pathname?.includes('/messagerie')) { + resetUnreadCount(); + } + }, [pathname, resetUnreadCount]); // Filtrage dynamique des items de la sidebar selon le rôle let sidebarItemsToDisplay = Object.values(sidebarItems); diff --git a/Front-End/src/app/[locale]/parents/layout.js b/Front-End/src/app/[locale]/parents/layout.js index 92e7e05..1c5b4d0 100644 --- a/Front-End/src/app/[locale]/parents/layout.js +++ b/Front-End/src/app/[locale]/parents/layout.js @@ -4,10 +4,7 @@ import React, { useState, useEffect } from 'react'; import Sidebar from '@/components/Sidebar'; import { useRouter, usePathname } from 'next/navigation'; import { MessageSquare, Settings, Home } from 'lucide-react'; -import { - FE_PARENTS_HOME_URL, - FE_PARENTS_MESSAGERIE_URL -} from '@/utils/Url'; +import { FE_PARENTS_HOME_URL, FE_PARENTS_MESSAGERIE_URL } from '@/utils/Url'; import ProtectedRoute from '@/components/ProtectedRoute'; import { disconnect } from '@/app/actions/authAction'; import Popup from '@/components/Popup'; @@ -15,6 +12,7 @@ import MobileTopbar from '@/components/MobileTopbar'; import { RIGHTS } from '@/utils/rights'; import { useEstablishment } from '@/context/EstablishmentContext'; import Footer from '@/components/Footer'; +import { useChatConnection } from '@/context/ChatConnectionContext'; export default function Layout({ children }) { const router = useRouter(); @@ -22,6 +20,7 @@ export default function Layout({ children }) { const [isPopupVisible, setIsPopupVisible] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(false); const { clearContext } = useEstablishment(); + const { totalUnreadCount, resetUnreadCount } = useChatConnection(); const softwareName = 'N3WT School'; const softwareVersion = `${process.env.NEXT_PUBLIC_APP_VERSION}`; @@ -41,7 +40,8 @@ export default function Layout({ children }) { name: 'Messagerie', url: FE_PARENTS_MESSAGERIE_URL, icon: MessageSquare, - } + badge: totalUnreadCount, + }, ]; // Déterminer la page actuelle pour la sidebar @@ -70,7 +70,11 @@ export default function Layout({ children }) { useEffect(() => { // Fermer la sidebar quand on change de page sur mobile setIsSidebarOpen(false); - }, [pathname]); + // Réinitialiser le compteur non lu quand on ouvre la messagerie + if (pathname?.includes('/messagerie')) { + resetUnreadCount(); + } + }, [pathname, resetUnreadCount]); return ( diff --git a/Front-End/src/app/[locale]/users/login/page.js b/Front-End/src/app/[locale]/users/login/page.js index 80fe348..5e76246 100644 --- a/Front-End/src/app/[locale]/users/login/page.js +++ b/Front-End/src/app/[locale]/users/login/page.js @@ -80,12 +80,7 @@ export default function Page() { return ; // Affichez le composant Loader } else { return ( -
+
diff --git a/Front-End/src/components/Footer.js b/Front-End/src/components/Footer.js index 588f4ea..14a26a7 100644 --- a/Front-End/src/components/Footer.js +++ b/Front-End/src/components/Footer.js @@ -10,7 +10,7 @@ export default function Footer({ softwareName, softwareVersion }) {
- {softwareName} - {softwareVersion} +  {softwareName} - {softwareVersion}
diff --git a/Front-End/src/components/Sidebar.js b/Front-End/src/components/Sidebar.js index 83d4c79..c1a629e 100644 --- a/Front-End/src/components/Sidebar.js +++ b/Front-End/src/components/Sidebar.js @@ -3,14 +3,21 @@ import React, { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import ProfileSelector from '@/components/ProfileSelector'; -const SidebarItem = ({ icon: Icon, text, active, url, onClick }) => ( +const SidebarItem = ({ icon: Icon, text, active, url, onClick, badge }) => (
- +
+ + {badge > 0 && ( + + {badge > 99 ? '99+' : badge} + + )} +
{text}
); @@ -45,6 +52,7 @@ function Sidebar({ currentPage, items, onCloseMobile }) { text={item.name} active={item.id === selectedItem} url={item.url} + badge={item.badge} onClick={() => handleItemClick(item.url)} /> ))} diff --git a/Front-End/src/context/ChatConnectionContext.js b/Front-End/src/context/ChatConnectionContext.js index ccb0c71..49dde73 100644 --- a/Front-End/src/context/ChatConnectionContext.js +++ b/Front-End/src/context/ChatConnectionContext.js @@ -6,9 +6,12 @@ import React, { useRef, useCallback, } from 'react'; -import { useSession, getSession } from 'next-auth/react'; +import { useSession } from 'next-auth/react'; import logger from '@/utils/logger'; -import { WS_CHAT_URL } from '@/utils/Url'; +import { + WS_CHAT_URL, + BE_GESTIONMESSAGERIE_CONVERSATIONS_URL, +} from '@/utils/Url'; const ChatConnectionContext = createContext(); @@ -22,6 +25,8 @@ export const ChatConnectionProvider = ({ children }) => { const [reconnectAttempts, setReconnectAttempts] = useState(0); const [currentUserId, setCurrentUserId] = useState(null); const maxReconnectAttempts = 5; + const isConnectedRef = useRef(false); + const [totalUnreadCount, setTotalUnreadCount] = useState(0); // Système de callbacks pour les messages const messageCallbacksRef = useRef(new Set()); @@ -54,15 +59,13 @@ export const ChatConnectionProvider = ({ children }) => { }, []); // Configuration WebSocket - const getWebSocketUrl = async (userId) => { + const getWebSocketUrl = (userId) => { if (!userId) { logger.warn('ChatConnection: No user ID provided for WebSocket URL'); return null; } - // Forcer un refresh de session pour obtenir un token JWT valide - const freshSession = await getSession(); - const token = freshSession?.user?.token; + const token = session?.user?.token; if (!token) { logger.warn( @@ -71,11 +74,8 @@ export const ChatConnectionProvider = ({ children }) => { return null; } - // Construire l'URL WebSocket avec le token const baseUrl = WS_CHAT_URL(userId); - const wsUrl = `${baseUrl}?token=${encodeURIComponent(token)}`; - - return wsUrl; + return `${baseUrl}?token=${encodeURIComponent(token)}`; }; // Connexion WebSocket @@ -108,7 +108,7 @@ export const ChatConnectionProvider = ({ children }) => { setConnectionStatus('connecting'); try { - const wsUrl = await getWebSocketUrl(userIdToUse); + const wsUrl = getWebSocketUrl(userIdToUse); if (!wsUrl) { throw new Error( @@ -123,12 +123,15 @@ export const ChatConnectionProvider = ({ children }) => { 'ChatConnection: Connected successfully for user:', userIdToUse ); + isConnectedRef.current = true; setIsConnected(true); setConnectionStatus('connected'); setReconnectAttempts(0); + fetchInitialUnreadCount(); }; websocketRef.current.onclose = (event) => { + isConnectedRef.current = false; setIsConnected(false); setConnectionStatus('disconnected'); @@ -149,6 +152,7 @@ export const ChatConnectionProvider = ({ children }) => { websocketRef.current.onerror = (error) => { logger.error('ChatConnection: WebSocket error', error); setConnectionStatus('error'); + isConnectedRef.current = false; setIsConnected(false); }; @@ -161,6 +165,17 @@ export const ChatConnectionProvider = ({ children }) => { handlePresenceUpdate(data); } + // Incrémenter le compteur de messages non lus si c'est un message entrant + if (data.type === 'new_message' && data.message) { + const senderId = String( + data.message.sender?.id ?? data.message.sender_id ?? '' + ); + const currentId = String(session?.user?.user_id ?? ''); + if (senderId && currentId && senderId !== currentId) { + setTotalUnreadCount((prev) => prev + 1); + } + } + // Notifier tous les callbacks enregistrés notifyMessageCallbacks(data); } catch (error) { @@ -170,6 +185,7 @@ export const ChatConnectionProvider = ({ children }) => { } catch (error) { logger.error('ChatConnection: Error creating WebSocket', error); setConnectionStatus('error'); + isConnectedRef.current = false; setIsConnected(false); } }; @@ -186,6 +202,7 @@ export const ChatConnectionProvider = ({ children }) => { websocketRef.current = null; } + isConnectedRef.current = false; setIsConnected(false); setConnectionStatus('disconnected'); setReconnectAttempts(0); @@ -207,24 +224,51 @@ export const ChatConnectionProvider = ({ children }) => { // Obtenir la référence WebSocket pour les composants qui en ont besoin const getWebSocket = () => websocketRef.current; + // Réinitialiser le compteur de messages non lus (ex: quand on ouvre la messagerie) + const resetUnreadCount = useCallback(() => { + setTotalUnreadCount(0); + }, []); + + // Récupérer le total initial des messages non lus depuis l'API + const fetchInitialUnreadCount = useCallback(async () => { + const token = session?.user?.token; + if (!token) return; + try { + const resp = await fetch(BE_GESTIONMESSAGERIE_CONVERSATIONS_URL, { + credentials: 'include', + headers: { Authorization: `Bearer ${token}` }, + }); + if (!resp.ok) return; + const data = await resp.json(); + const conversations = Array.isArray(data) ? data : (data.results ?? []); + const total = conversations.reduce( + (sum, conv) => sum + (conv.unread_count || 0), + 0 + ); + setTotalUnreadCount(total); + } catch (e) { + logger.warn( + 'ChatConnection: impossible de récupérer le compteur non lu initial', + e + ); + } + }, [session?.user?.token]); + // 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) { + if ( + status === 'authenticated' && + session?.user?.user_id && + !isConnectedRef.current + ) { connectToChat(session.user.user_id); } - // Si la session devient unauthenticated, déconnecter - if (status === 'unauthenticated' && isConnected) { + if (status === 'unauthenticated' && isConnectedRef.current) { disconnectFromChat(); } - }, [ - status, - session?.user?.user_id, - isConnected, - connectToChat, - disconnectFromChat, - ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [status, session?.user?.user_id]); // Nettoyage à la destruction du composant useEffect(() => { @@ -241,14 +285,16 @@ export const ChatConnectionProvider = ({ children }) => { const value = { isConnected, connectionStatus, - userPresences, // Ajouter les présences utilisateur + userPresences, connectToChat, disconnectFromChat, sendMessage, getWebSocket, reconnectAttempts, maxReconnectAttempts, - addMessageCallback, // Ajouter cette fonction + addMessageCallback, + totalUnreadCount, + resetUnreadCount, }; return ( diff --git a/Front-End/src/pages/api/download.js b/Front-End/src/pages/api/download.js index 876096b..2615289 100644 --- a/Front-End/src/pages/api/download.js +++ b/Front-End/src/pages/api/download.js @@ -1,4 +1,3 @@ -import { logger } from '@/utils/logger'; import { getToken } from 'next-auth/jwt'; const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL; @@ -49,7 +48,6 @@ export default async function handler(req, res) { const buffer = Buffer.from(await backendRes.arrayBuffer()); return res.send(buffer); } catch (error) { - logger.error('Download proxy error:', error); return res.status(500).json({ error: 'Erreur lors du téléchargement' }); } }