From 9e9b1db012d2eb3b9c968010723469f2f687a0b3 Mon Sep 17 00:00:00 2001 From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:56:36 +0100 Subject: [PATCH] feat: manage user online status and typing indicator in socket gateway - Added tracking of online users with real-time status updates (online/offline). - Implemented `handleTyping` to broadcast user typing events to recipients. - Added `check_status` handler to query user online status. - Enhanced CORS configuration to support multi-domain deployments with credentials. --- backend/src/realtime/events.gateway.ts | 52 +++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/backend/src/realtime/events.gateway.ts b/backend/src/realtime/events.gateway.ts index 77c3c7d..a2157db 100644 --- a/backend/src/realtime/events.gateway.ts +++ b/backend/src/realtime/events.gateway.ts @@ -17,8 +17,16 @@ import { JwtService } from "../crypto/services/jwt.service"; @WebSocketGateway({ cors: { - origin: "*", + origin: ( + _origin: string, + callback: (err: Error | null, allow?: boolean) => void, + ) => { + // En production, on pourrait restreindre ici + // Pour l'instant on autorise tout en mode credentials pour faciliter le déploiement multi-domaines + callback(null, true); + }, credentials: true, + methods: ["GET", "POST"], }, }) export class EventsGateway @@ -28,6 +36,7 @@ export class EventsGateway server!: Server; private readonly logger = new Logger(EventsGateway.name); + private readonly onlineUsers = new Map>(); // userId -> Set of socketIds constructor( private readonly jwtService: JwtService, @@ -69,6 +78,13 @@ export class EventsGateway // Rejoindre une room personnelle pour les notifications client.join(`user:${payload.sub}`); + // Gérer le statut en ligne + if (!this.onlineUsers.has(payload.sub)) { + this.onlineUsers.set(payload.sub, new Set()); + this.server.emit("user_status", { userId: payload.sub, status: "online" }); + } + this.onlineUsers.get(payload.sub)?.add(client.id); + this.logger.log(`Client connected: ${client.id} (User: ${payload.sub})`); } catch (error) { this.logger.error(`Connection error for client ${client.id}: ${error}`); @@ -77,6 +93,15 @@ export class EventsGateway } handleDisconnect(client: Socket) { + const userId = client.data.user?.sub; + if (userId && this.onlineUsers.has(userId)) { + const sockets = this.onlineUsers.get(userId); + sockets?.delete(client.id); + if (sockets?.size === 0) { + this.onlineUsers.delete(userId); + this.server.emit("user_status", { userId, status: "offline" }); + } + } this.logger.log(`Client disconnected: ${client.id}`); } @@ -98,6 +123,31 @@ export class EventsGateway this.logger.log(`Client ${client.id} left content room: ${contentId}`); } + @SubscribeMessage("typing") + handleTyping( + @ConnectedSocket() client: Socket, + @MessageBody() data: { recipientId: string; isTyping: boolean }, + ) { + const userId = client.data.user?.sub; + if (!userId) return; + + this.server.to(`user:${data.recipientId}`).emit("user_typing", { + userId, + isTyping: data.isTyping, + }); + } + + @SubscribeMessage("check_status") + handleCheckStatus( + @ConnectedSocket() _client: Socket, + @MessageBody() userId: string, + ) { + return { + userId, + status: this.onlineUsers.has(userId) ? "online" : "offline", + }; + } + // Méthode utilitaire pour envoyer des messages à un utilisateur spécifique sendToUser(userId: string, event: string, data: any) { this.server.to(`user:${userId}`).emit(event, data);