From 05a05a1940a333fb9271281292751f3443230f13 Mon Sep 17 00:00:00 2001 From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:50:53 +0100 Subject: [PATCH] feat: add share dialog and typing indicator in messages - Implemented `ShareDialog` component for sharing content directly with other users. - Added typing indicator when a user is composing a message in an active conversation. - Updated `SocketProvider` to handle improved connection management and user status updates. - Enhanced the messages UI with real-time online status and typing indicators for better feedback. --- .../src/app/(dashboard)/messages/page.tsx | 70 ++++++- frontend/src/components/content-card.tsx | 28 ++- frontend/src/components/share-dialog.tsx | 185 ++++++++++++++++++ frontend/src/providers/socket-provider.tsx | 12 +- 4 files changed, 287 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/share-dialog.tsx diff --git a/frontend/src/app/(dashboard)/messages/page.tsx b/frontend/src/app/(dashboard)/messages/page.tsx index ab34b31..0a8bb0e 100644 --- a/frontend/src/app/(dashboard)/messages/page.tsx +++ b/frontend/src/app/(dashboard)/messages/page.tsx @@ -32,8 +32,29 @@ export default function MessagesPage() { const [activeConv, setActiveConv] = React.useState(null); const [messages, setMessages] = React.useState([]); const [newMessage, setNewMessage] = React.useState(""); + const typingTimeoutRef = React.useRef(null); + + const handleTyping = () => { + if (!socket || !activeConv) return; + + socket.emit("typing", { + recipientId: activeConv.recipient.uuid, + isTyping: true, + }); + + if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current); + + typingTimeoutRef.current = setTimeout(() => { + socket.emit("typing", { + recipientId: activeConv.recipient.uuid, + isTyping: false, + }); + }, 3000); + }; const [isLoadingConvs, setIsLoadingConvs] = React.useState(true); const [isLoadingMsgs, setIsLoadingMsgs] = React.useState(false); + const [isOtherTyping, setIsOtherTyping] = React.useState(false); + const [onlineUsers, setOnlineUsers] = React.useState>(new Set()); const [searchQuery, setSearchQuery] = React.useState(""); const [searchResults, setSearchResults] = React.useState([]); @@ -120,6 +141,7 @@ export default function MessagesPage() { (data: { conversationId: string; message: Message }) => { if (activeConv?.id === data.conversationId) { setMessages((prev) => [...prev, data.message]); + setIsOtherTyping(false); // S'il a envoyé un message, il ne tape plus } // Mettre à jour la liste des conversations setConversations((prev) => { @@ -144,8 +166,28 @@ export default function MessagesPage() { }, ); + socket.on("user_status", (data: { userId: string; status: string }) => { + setOnlineUsers((prev) => { + const next = new Set(prev); + if (data.status === "online") { + next.add(data.userId); + } else { + next.delete(data.userId); + } + return next; + }); + }); + + socket.on("user_typing", (data: { userId: string; isTyping: boolean }) => { + if (activeConv?.recipient.uuid === data.userId) { + setIsOtherTyping(data.isTyping); + } + }); + return () => { socket.off("new_message"); + socket.off("user_status"); + socket.off("user_typing"); }; } }, [socket, activeConv]); @@ -355,7 +397,17 @@ export default function MessagesPage() {

{activeConv.recipient.displayName || activeConv.recipient.username}

- En ligne + + {onlineUsers.has(activeConv.recipient.uuid) + ? "En ligne" + : "Hors ligne"} + @@ -406,6 +458,17 @@ export default function MessagesPage() { )) )} + {isOtherTyping && ( +
+
+
+ + + +
+
+
+ )} @@ -415,7 +478,10 @@ export default function MessagesPage() { setNewMessage(e.target.value)} + onChange={(e) => { + setNewMessage(e.target.value); + handleTyping(); + }} className="rounded-full px-4" /> + )} + + + +
+ {isLoading && results.length === 0 ? ( +
+ Chargement... +
+ ) : results.length === 0 ? ( +
+ Aucun membre trouvé. +
+ ) : ( + results.map((user) => ( +
+
+ + + {user.username[0].toUpperCase()} + +
+ + {user.displayName || user.username} + + + @{user.username} + +
+
+ +
+ )) + )} +
+
+
+ +
+ + + ); +} diff --git a/frontend/src/providers/socket-provider.tsx b/frontend/src/providers/socket-provider.tsx index a9bfe9a..6461064 100644 --- a/frontend/src/providers/socket-provider.tsx +++ b/frontend/src/providers/socket-provider.tsx @@ -24,15 +24,25 @@ export function SocketProvider({ children }: { children: React.ReactNode }) { React.useEffect(() => { if (isAuthenticated) { const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + + // Initialisation du socket avec configuration optimisée pour la production const socketInstance = io(apiUrl, { withCredentials: true, - transports: ["websocket"], + transports: ["websocket"], // Recommandé pour éviter les problèmes de sticky sessions + reconnectionAttempts: 5, + reconnectionDelay: 1000, }); socketInstance.on("connect", () => { + console.log("WebSocket connected to:", apiUrl); setIsConnected(true); }); + socketInstance.on("connect_error", (error) => { + console.error("WebSocket connection error:", error); + // Si le websocket pur échoue, on peut tenter le polling en fallback (optionnel) + }); + socketInstance.on("disconnect", () => { setIsConnected(false); });