From a885c574e6d05a02359a989d09c6873a164cde09 Mon Sep 17 00:00:00 2001 From: Hussein Saad Date: Sun, 20 Apr 2025 18:07:32 +0200 Subject: [PATCH 1/3] feat: add forward reference to NotificationsModule in ChatModule --- src/chat/chat.module.ts | 2 ++ src/notifications/notifications.module.ts | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/chat/chat.module.ts b/src/chat/chat.module.ts index 4ab1e26..812fed1 100644 --- a/src/chat/chat.module.ts +++ b/src/chat/chat.module.ts @@ -15,6 +15,7 @@ import { Service, ServiceSchema } from '../services/schemas/service.schema'; import { User, UserSchema } from '../users/schemas/user.schema'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; +import { NotificationsModule } from '../notifications/notifications.module'; @Module({ imports: [ @@ -27,6 +28,7 @@ import { ConfigService } from '@nestjs/config'; forwardRef(() => UserModule), forwardRef(() => ServiceBookingsModule), forwardRef(() => ServicesModule), + forwardRef(() => NotificationsModule), ], providers: [ChatGateway, ChatService, JwtService, ConfigService], exports: [ChatService, ChatGateway], diff --git a/src/notifications/notifications.module.ts b/src/notifications/notifications.module.ts index 70ef09f..7c0ee8c 100644 --- a/src/notifications/notifications.module.ts +++ b/src/notifications/notifications.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { NotificationService } from './notifications.service'; import { NotificationsController } from './notifications.controller'; @@ -6,12 +6,14 @@ import { Notification, NotificationSchema, } from './schemas/notification.schema'; +import { ChatModule } from '../chat/chat.module'; @Module({ imports: [ MongooseModule.forFeature([ { name: Notification.name, schema: NotificationSchema }, ]), + forwardRef(() => ChatModule), ], controllers: [NotificationsController], providers: [NotificationService], From 9134d1c2a8ec211cfe6b725b2742fc0514ea7fa1 Mon Sep 17 00:00:00 2001 From: Hussein Saad Date: Sun, 20 Apr 2025 18:07:44 +0200 Subject: [PATCH 2/3] feat: enhance ChatGateway to manage user notifications and subscriptions --- src/chat/chat.gateway.ts | 83 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 4 deletions(-) diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts index 77af337..989e653 100644 --- a/src/chat/chat.gateway.ts +++ b/src/chat/chat.gateway.ts @@ -10,13 +10,19 @@ import { } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { ChatService } from './chat.service'; -import { Logger, UseGuards, UsePipes, ValidationPipe } from '@nestjs/common'; +import { + Logger, + UseGuards, + UsePipes, + ValidationPipe, + Inject, + forwardRef, +} from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { UsersService } from '../users/users.service'; -import { CreateMessageDto } from './dto/create-message.dto'; -import { JoinRoomDto } from './dto/join-room.dto'; import { WsJwtGuard } from './guards/ws-jwt.guard'; +import { NotificationService } from '../notifications/notifications.service'; @UsePipes(new ValidationPipe()) @WebSocketGateway({ @@ -30,12 +36,15 @@ export class ChatGateway @WebSocketServer() server: Server; private logger: Logger = new Logger('ChatGateway'); private wsJwtGuard: WsJwtGuard; + private userSocketMap = new Map(); constructor( private readonly chatService: ChatService, private readonly jwtService: JwtService, private readonly configService: ConfigService, private readonly usersService: UsersService, + @Inject(forwardRef(() => NotificationService)) + private readonly notificationService: NotificationService, ) { this.wsJwtGuard = new WsJwtGuard(jwtService, configService, usersService); } @@ -53,9 +62,35 @@ export class ChatGateway if (!canActivate) { this.logger.warn(`Disconnecting unauthenticated client: ${client.id}`); } else { + const userId = client.data.user?.userId; this.logger.log( - `Client authenticated: ${client.id}, User ID: ${client.data.user?.userId}`, + `Client authenticated: ${client.id}, User ID: ${userId}`, ); + if (userId) { + const userSockets = this.userSocketMap.get(userId) || []; + userSockets.push(client.id); + this.userSocketMap.set(userId, userSockets); + + const notificationRoom = `notifications_${userId}`; + client.join(notificationRoom); + this.logger.log( + `User ${userId} joined notification room: ${notificationRoom}`, + ); + try { + const unreadNotifications = + await this.notificationService.getUserUnreadNotifications(userId); + if (unreadNotifications.length > 0) { + this.logger.log( + `Sending ${unreadNotifications.length} unread notifications to user ${userId}`, + ); + client.emit('unreadNotifications', unreadNotifications); + } + } catch (error) { + this.logger.error( + `Error fetching unread notifications for user ${userId}: ${error.message}`, + ); + } + } } } catch (e) { this.logger.error(`Authentication error for ${client.id}: ${e.message}`); @@ -65,6 +100,46 @@ export class ChatGateway handleDisconnect(client: Socket) { this.logger.log(`Client disconnected: ${client.id}`); + const userId = client.data?.user?.userId; + if (userId) { + const userSockets = this.userSocketMap.get(userId) || []; + const updatedSockets = userSockets.filter( + (socketId) => socketId !== client.id, + ); + + if (updatedSockets.length > 0) { + this.userSocketMap.set(userId, updatedSockets); + } else { + this.userSocketMap.delete(userId); + } + } + } + + sendNotificationToUser(userId: string, notification: any): void { + const notificationRoom = `notifications_${userId}`; + this.server.to(notificationRoom).emit('notification', notification); + this.logger.log(`Notification sent to user ${userId}`); + } + + isUserOnline(userId: string): boolean { + return ( + this.userSocketMap.has(userId) && + this.userSocketMap.get(userId).length > 0 + ); + } + + @UseGuards(WsJwtGuard) + @SubscribeMessage('subscribeToNotifications') + handleSubscribeToNotifications(@ConnectedSocket() client: Socket): void { + const userId = client.data.user.userId; + const notificationRoom = `notifications_${userId}`; + + client.join(notificationRoom); + this.logger.log(`User ${userId} subscribed to notifications`); + client.emit('notificationSubscription', { + success: true, + message: 'Subscribed to notifications', + }); } @SubscribeMessage('test') From fef879c080a0440cc7f4895d6f8ebe78dc6c3398 Mon Sep 17 00:00:00 2001 From: Hussein Saad Date: Sun, 20 Apr 2025 18:07:51 +0200 Subject: [PATCH 3/3] feat: integrate real-time notification delivery in NotificationService --- src/notifications/notifications.service.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/notifications/notifications.service.ts b/src/notifications/notifications.service.ts index dc3a9e8..35dc236 100644 --- a/src/notifications/notifications.service.ts +++ b/src/notifications/notifications.service.ts @@ -1,7 +1,8 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger, forwardRef, Inject } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model, Types } from 'mongoose'; import { Notification } from './schemas/notification.schema'; +import { ChatGateway } from '../chat/chat.gateway'; @Injectable() export class NotificationService { @@ -10,6 +11,8 @@ export class NotificationService { constructor( @InjectModel(Notification.name) private notificationModel: Model, + @Inject(forwardRef(() => ChatGateway)) + private readonly chatGateway: ChatGateway, ) {} async createNotification(notificationData: { @@ -52,6 +55,23 @@ export class NotificationService { this.logger.log( `Created notification ${savedNotification._id} for user ${recipientId}`, ); + + const recipientIdString = recipientId.toString(); + if (this.chatGateway.isUserOnline(recipientIdString)) { + const notificationToSend = savedNotification.toObject(); + this.chatGateway.sendNotificationToUser( + recipientIdString, + notificationToSend, + ); + this.logger.log( + `Real-time notification sent to user ${recipientIdString}`, + ); + } else { + this.logger.log( + `User ${recipientIdString} is offline, notification will be delivered when they connect`, + ); + } + return savedNotification; } catch (error) { this.logger.error(