From 779bb5c112cc4eacbaa684d70f168ca4a5c69d89 Mon Sep 17 00:00:00 2001 From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:20:04 +0100 Subject: [PATCH] feat: integrate user preferences for online status in WebSocket gateway - Added `UsersService` to manage user preferences in `EventsGateway`. - Enhanced online/offline broadcasting to respect user `showOnlineStatus` preference. - Updated `handleTyping` and `check_status` to verify user preferences before emitting events. - Abstracted status broadcasting logic into `broadcastStatus`. --- backend/src/realtime/events.gateway.spec.ts | 6 +++ backend/src/realtime/events.gateway.ts | 48 ++++++++++++++++++--- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/backend/src/realtime/events.gateway.spec.ts b/backend/src/realtime/events.gateway.spec.ts index 7571633..bfeee18 100644 --- a/backend/src/realtime/events.gateway.spec.ts +++ b/backend/src/realtime/events.gateway.spec.ts @@ -1,6 +1,7 @@ import { ConfigService } from "@nestjs/config"; import { Test, TestingModule } from "@nestjs/testing"; import { JwtService } from "../crypto/services/jwt.service"; +import { UsersService } from "../users/users.service"; import { EventsGateway } from "./events.gateway"; describe("EventsGateway", () => { @@ -15,12 +16,17 @@ describe("EventsGateway", () => { get: jest.fn().mockReturnValue("secret-password-32-chars-long-!!!"), }; + const mockUsersService = { + findOne: jest.fn(), + }; + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ EventsGateway, { provide: JwtService, useValue: mockJwtService }, { provide: ConfigService, useValue: mockConfigService }, + { provide: UsersService, useValue: mockUsersService }, ], }).compile(); diff --git a/backend/src/realtime/events.gateway.ts b/backend/src/realtime/events.gateway.ts index ecb3167..a559ee2 100644 --- a/backend/src/realtime/events.gateway.ts +++ b/backend/src/realtime/events.gateway.ts @@ -1,4 +1,4 @@ -import { Logger } from "@nestjs/common"; +import { forwardRef, Inject, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { ConnectedSocket, @@ -14,6 +14,7 @@ import { getIronSession } from "iron-session"; import { Server, Socket } from "socket.io"; import { getSessionOptions, SessionData } from "../auth/session.config"; import { JwtService } from "../crypto/services/jwt.service"; +import { UsersService } from "../users/users.service"; @WebSocketGateway({ transports: ["websocket"], @@ -63,6 +64,8 @@ export class EventsGateway constructor( private readonly jwtService: JwtService, private readonly configService: ConfigService, + @Inject(forwardRef(() => UsersService)) + private readonly usersService: UsersService, ) {} afterInit(_server: Server) { @@ -106,9 +109,15 @@ export class EventsGateway // Gérer le statut en ligne const userId = payload.sub as string; + if (!this.onlineUsers.has(userId)) { this.onlineUsers.set(userId, new Set()); - this.server.emit("user_status", { userId, status: "online" }); + + // Vérifier les préférences de l'utilisateur + const user = await this.usersService.findOne(userId); + if (user?.showOnlineStatus) { + this.broadcastStatus(userId, "online"); + } } this.onlineUsers.get(userId)?.add(client.id); @@ -119,19 +128,31 @@ export class EventsGateway } } - handleDisconnect(client: Socket) { + async 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" }); + + const user = await this.usersService.findOne(userId); + if (user?.showOnlineStatus) { + this.broadcastStatus(userId, "offline"); + } } } this.logger.log(`Client disconnected: ${client.id}`); } + broadcastStatus(userId: string, status: "online" | "offline") { + this.server.emit("user_status", { userId, status }); + } + + isUserOnline(userId: string): boolean { + return this.onlineUsers.has(userId); + } + @SubscribeMessage("join_content") handleJoinContent( @ConnectedSocket() client: Socket, @@ -151,13 +172,20 @@ export class EventsGateway } @SubscribeMessage("typing") - handleTyping( + async handleTyping( @ConnectedSocket() client: Socket, @MessageBody() data: { recipientId: string; isTyping: boolean }, ) { const userId = client.data.user?.sub; if (!userId) return; + // Optionnel: vérifier si l'utilisateur autorise le statut en ligne avant d'émettre "typing" + // ou si on considère que typing est une interaction directe qui outrepasse le statut. + // Instagram affiche "Typing..." même si le statut en ligne est désactivé si on est dans le chat. + // Mais par souci de cohérence avec "showOnlineStatus", on peut le vérifier. + const user = await this.usersService.findOne(userId); + if (!user?.showOnlineStatus) return; + this.server.to(`user:${data.recipientId}`).emit("user_typing", { userId, isTyping: data.isTyping, @@ -165,13 +193,19 @@ export class EventsGateway } @SubscribeMessage("check_status") - handleCheckStatus( + async handleCheckStatus( @ConnectedSocket() _client: Socket, @MessageBody() userId: string, ) { + const isOnline = this.onlineUsers.has(userId); + if (!isOnline) return { userId, status: "offline" }; + + const user = await this.usersService.findOne(userId); + if (!user?.showOnlineStatus) return { userId, status: "offline" }; + return { userId, - status: this.onlineUsers.has(userId) ? "online" : "offline", + status: "online", }; }