From ebd937f7e72067da6326f4093b06ec9390bb8b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9COmarNabil005=E2=80=9D?= Date: Wed, 3 Dec 2025 11:13:51 +0200 Subject: [PATCH 1/2] feat: added unseen messages count for old endpoints and added a new endpoint --- src/conversations/conversations.controller.ts | 39 ++++++++++ src/conversations/conversations.service.ts | 74 ++++++++++++++++++- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/conversations/conversations.controller.ts b/src/conversations/conversations.controller.ts index 8801b1a..e97ae5b 100644 --- a/src/conversations/conversations.controller.ts +++ b/src/conversations/conversations.controller.ts @@ -204,6 +204,45 @@ export class ConversationsController { }; } + @Get('/unseen/:conversationId') + @UseGuards(JwtAuthGuard) + @ApiCookieAuth() + @ApiOperation({ + summary: 'Get count of unseen messages in a specific conversation for the authenticated user', + description: 'Retrieves the total number of unseen messages across all conversations', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Unseen messages count retrieved successfully', + schema: { + example: { + status: 'success', + unseenCount: 5, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: 'Unauthorized - Token missing or invalid', + schema: ErrorResponseDto.schemaExample( + 'Authentication token is missing or invalid', + 'Unauthorized', + ), + }) + async getConversationUnseenMessagesCount( + @CurrentUser() user: AuthenticatedUser, + @Param('conversationId', ParseIntPipe) conversationId: number, + ) { + const unseenCount = await this.conversationsService.getConversationUnseenMessagesCount( + conversationId, + user.id, + ); + return { + status: 'success', + unseenCount, + }; + } + @Get('/:conversationId') @UseGuards(JwtAuthGuard) @ApiCookieAuth() diff --git a/src/conversations/conversations.service.ts b/src/conversations/conversations.service.ts index 7a2e8a0..1f37511 100644 --- a/src/conversations/conversations.service.ts +++ b/src/conversations/conversations.service.ts @@ -173,9 +173,26 @@ export class ConversationsService { }), ]); + // Fetch unseen counts for all conversations + const unseenCounts = await Promise.all( + conversations.map((conv) => + this.prismaService.message.count({ + where: { + conversationId: conv.id, + isSeen: false, + senderId: { + not: userId, + }, + isDeletedU1: userId === conv.User1.id ? undefined : false, + isDeletedU2: userId === conv.User2.id ? undefined : false, + }, + }), + ), + ); + // Transform Messages to messages and filter based on user const transformedConversations = conversations.map( - ({ Messages, User1, User2, ...conversation }) => { + ({ Messages, User1, User2, ...conversation }, index) => { const isUser1 = userId === User1.id; // Find the first message that's not deleted for this user @@ -185,6 +202,7 @@ export class ConversationsService { return { ...conversation, + unseenCount: unseenCounts[index], lastMessage: lastVisibleMessage ? { id: lastVisibleMessage.id, @@ -325,10 +343,24 @@ export class ConversationsService { isUser1 ? !msg.isDeletedU1 : !msg.isDeletedU2, ); + // Fetch exact unseen count from database + const unseenCount = await this.prismaService.message.count({ + where: { + conversationId, + isSeen: false, + senderId: { + not: userId, + }, + isDeletedU1: isUser1 ? undefined : false, + isDeletedU2: isUser1 ? false : undefined, + }, + }); + const transformedConversation = { id: conversation.id, updatedAt: conversation.updatedAt, createdAt: conversation.createdAt, + unseenCount, lastMessage: lastVisibleMessage ? { id: lastVisibleMessage.id, @@ -356,4 +388,44 @@ export class ConversationsService { return { data: transformedConversation }; } + + async getConversationUnseenMessagesCount(conversationId: number, userId: number) { + const conversation = await this.prismaService.conversation.findUnique({ + where: { id: conversationId }, + select: { + User1: { + select: { + id: true, + }, + }, + User2: { + select: { + id: true, + }, + }, + }, + }); + + if (!conversation) { + throw new ConflictException('Conversation not found'); + } + + const isUser1 = userId === conversation.User1.id; + + if (!isUser1 && userId !== conversation.User2.id) { + throw new ConflictException('You are not part of this conversation'); + } + + return this.prismaService.message.count({ + where: { + conversationId, + isSeen: false, + isDeletedU1: isUser1 ? undefined : false, + isDeletedU2: isUser1 ? false : undefined, + senderId: { + not: userId, + }, + }, + }); + } } From d93d1dd9768332a7ebe08a0bd2d935b1c2c51cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9COmarNabil005=E2=80=9D?= Date: Wed, 3 Dec 2025 12:24:16 +0200 Subject: [PATCH 2/2] refactor and extra logic for cross-platform --- src/conversations/conversations.service.ts | 31 +++---------------- .../helpers/unseen-message.helper.ts | 15 +++++++++ src/messages/messages.gateway.ts | 3 +- src/messages/messages.service.ts | 20 +++++++++++- 4 files changed, 40 insertions(+), 29 deletions(-) create mode 100644 src/conversations/helpers/unseen-message.helper.ts diff --git a/src/conversations/conversations.service.ts b/src/conversations/conversations.service.ts index 1f37511..a6290b4 100644 --- a/src/conversations/conversations.service.ts +++ b/src/conversations/conversations.service.ts @@ -2,6 +2,7 @@ import { ConflictException, Inject, Injectable } from '@nestjs/common'; import { CreateConversationDto } from './dto/create-conversation.dto'; import { PrismaService } from 'src/prisma/prisma.service'; import { Services } from 'src/utils/constants'; +import { getUnseenMessageCountWhere } from './helpers/unseen-message.helper'; @Injectable() export class ConversationsService { @@ -177,15 +178,7 @@ export class ConversationsService { const unseenCounts = await Promise.all( conversations.map((conv) => this.prismaService.message.count({ - where: { - conversationId: conv.id, - isSeen: false, - senderId: { - not: userId, - }, - isDeletedU1: userId === conv.User1.id ? undefined : false, - isDeletedU2: userId === conv.User2.id ? undefined : false, - }, + where: getUnseenMessageCountWhere(conv.id, userId, userId === conv.User1.id), }), ), ); @@ -345,15 +338,7 @@ export class ConversationsService { // Fetch exact unseen count from database const unseenCount = await this.prismaService.message.count({ - where: { - conversationId, - isSeen: false, - senderId: { - not: userId, - }, - isDeletedU1: isUser1 ? undefined : false, - isDeletedU2: isUser1 ? false : undefined, - }, + where: getUnseenMessageCountWhere(conversationId, userId, isUser1), }); const transformedConversation = { @@ -417,15 +402,7 @@ export class ConversationsService { } return this.prismaService.message.count({ - where: { - conversationId, - isSeen: false, - isDeletedU1: isUser1 ? undefined : false, - isDeletedU2: isUser1 ? false : undefined, - senderId: { - not: userId, - }, - }, + where: getUnseenMessageCountWhere(conversationId, userId, isUser1), }); } } diff --git a/src/conversations/helpers/unseen-message.helper.ts b/src/conversations/helpers/unseen-message.helper.ts new file mode 100644 index 0000000..71a9d1a --- /dev/null +++ b/src/conversations/helpers/unseen-message.helper.ts @@ -0,0 +1,15 @@ +export function getUnseenMessageCountWhere( + conversationId: number, + userId: number, + isUser1: boolean, +) { + return { + conversationId, + isSeen: false, + senderId: { + not: userId, + }, + isDeletedU1: isUser1 ? false : undefined, + isDeletedU2: isUser1 ? undefined : false, + }; +} diff --git a/src/messages/messages.gateway.ts b/src/messages/messages.gateway.ts index baef92d..1014720 100644 --- a/src/messages/messages.gateway.ts +++ b/src/messages/messages.gateway.ts @@ -185,7 +185,7 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect const recipientId = userId === participants.user1Id ? participants.user2Id : participants.user1Id; - const message = await this.messagesService.create(createMessageDto); + const { message, unseenCount } = await this.messagesService.create(createMessageDto); // Emit to conversation room this.server @@ -218,6 +218,7 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect return { status: 'success', data: message, + unseenCount, }; } catch (error) { console.error(`Error creating message: ${error.message}`); diff --git a/src/messages/messages.service.ts b/src/messages/messages.service.ts index 201f556..5606a16 100644 --- a/src/messages/messages.service.ts +++ b/src/messages/messages.service.ts @@ -11,6 +11,7 @@ import { UpdateMessageDto } from './dto/update-message.dto'; import { PrismaService } from '../prisma/prisma.service'; import { RemoveMessageDto } from './dto/remove-message.dto'; import { Services } from 'src/utils/constants'; +import { getUnseenMessageCountWhere } from 'src/conversations/helpers/unseen-message.helper'; @Injectable() export class MessagesService { @@ -31,8 +32,23 @@ export class MessagesService { throw new Error('Conversation not found'); } + const isUser1 = senderId === conversation.user1Id; + + if (!isUser1 && senderId !== conversation.user2Id) { + throw new ForbiddenException('You are not part of this conversation'); + } + + // invert sender to get unseen count at receiver side + const unseenCount = await this.prismaService.message.count({ + where: getUnseenMessageCountWhere( + createMessageDto.conversationId, + isUser1 ? conversation.user2Id : conversation.user1Id, + !isUser1, + ), + }); + // Create the message and update conversation timestamp in a transaction - return this.prismaService.$transaction(async (prisma) => { + const message = await this.prismaService.$transaction(async (prisma) => { await prisma.conversation.update({ where: { id: conversationId }, data: {}, // Empty update triggers @updatedAt @@ -54,6 +70,8 @@ export class MessagesService { }, }); }); + + return { message, unseenCount }; } async getConversationUsers(