feat: add read receipt handling based on user preferences

- Integrated `UsersService` into `MessagesService` for retrieving user preferences.
- Updated `markAsRead` functionality to respect `showReadReceipts` preference.
- Enhanced real-time read receipt notifications via `EventsGateway`.
- Added `markAsRead` method to the frontend message service.
This commit is contained in:
Mathis HERRIOT
2026-01-29 18:20:18 +01:00
parent 779bb5c112
commit f882a70343
3 changed files with 52 additions and 3 deletions

View File

@@ -1,6 +1,7 @@
import { ForbiddenException } from "@nestjs/common"; import { ForbiddenException } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing"; import { Test, TestingModule } from "@nestjs/testing";
import { EventsGateway } from "../realtime/events.gateway"; import { EventsGateway } from "../realtime/events.gateway";
import { UsersService } from "../users/users.service";
import { MessagesService } from "./messages.service"; import { MessagesService } from "./messages.service";
import { MessagesRepository } from "./repositories/messages.repository"; import { MessagesRepository } from "./repositories/messages.repository";
@@ -16,6 +17,7 @@ describe("MessagesService", () => {
createMessage: jest.fn(), createMessage: jest.fn(),
findAllConversations: jest.fn(), findAllConversations: jest.fn(),
isParticipant: jest.fn(), isParticipant: jest.fn(),
getParticipants: jest.fn(),
findMessagesByConversationId: jest.fn(), findMessagesByConversationId: jest.fn(),
markAsRead: jest.fn(), markAsRead: jest.fn(),
countUnreadMessages: jest.fn(), countUnreadMessages: jest.fn(),
@@ -25,12 +27,17 @@ describe("MessagesService", () => {
sendToUser: jest.fn(), sendToUser: jest.fn(),
}; };
const mockUsersService = {
findOne: jest.fn(),
};
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
MessagesService, MessagesService,
{ provide: MessagesRepository, useValue: mockMessagesRepository }, { provide: MessagesRepository, useValue: mockMessagesRepository },
{ provide: EventsGateway, useValue: mockEventsGateway }, { provide: EventsGateway, useValue: mockEventsGateway },
{ provide: UsersService, useValue: mockUsersService },
], ],
}).compile(); }).compile();

View File

@@ -1,5 +1,6 @@
import { ForbiddenException, Injectable } from "@nestjs/common"; import { ForbiddenException, Injectable } from "@nestjs/common";
import { EventsGateway } from "../realtime/events.gateway"; import { EventsGateway } from "../realtime/events.gateway";
import { UsersService } from "../users/users.service";
import type { CreateMessageDto } from "./dto/create-message.dto"; import type { CreateMessageDto } from "./dto/create-message.dto";
import { MessagesRepository } from "./repositories/messages.repository"; import { MessagesRepository } from "./repositories/messages.repository";
@@ -8,6 +9,7 @@ export class MessagesService {
constructor( constructor(
private readonly messagesRepository: MessagesRepository, private readonly messagesRepository: MessagesRepository,
private readonly eventsGateway: EventsGateway, private readonly eventsGateway: EventsGateway,
private readonly usersService: UsersService,
) {} ) {}
async sendMessage(senderId: string, dto: CreateMessageDto) { async sendMessage(senderId: string, dto: CreateMessageDto) {
@@ -62,8 +64,24 @@ export class MessagesService {
throw new ForbiddenException("You are not part of this conversation"); throw new ForbiddenException("You are not part of this conversation");
} }
// Marquer comme lus // Récupérer les préférences de l'utilisateur actuel
await this.messagesRepository.markAsRead(conversationId, userId); const user = await this.usersService.findOne(userId);
// Marquer comme lus seulement si l'utilisateur l'autorise
if (user?.showReadReceipts) {
await this.messagesRepository.markAsRead(conversationId, userId);
// Notifier l'expéditeur que les messages ont été lus
const participants =
await this.messagesRepository.getParticipants(conversationId);
const otherParticipant = participants.find((p) => p.userId !== userId);
if (otherParticipant) {
this.eventsGateway.sendToUser(otherParticipant.userId, "messages_read", {
conversationId,
readerId: userId,
});
}
}
return this.messagesRepository.findMessagesByConversationId(conversationId); return this.messagesRepository.findMessagesByConversationId(conversationId);
} }
@@ -76,6 +94,26 @@ export class MessagesService {
if (!isParticipant) { if (!isParticipant) {
throw new ForbiddenException("You are not part of this conversation"); throw new ForbiddenException("You are not part of this conversation");
} }
return this.messagesRepository.markAsRead(conversationId, userId);
const user = await this.usersService.findOne(userId);
if (!user?.showReadReceipts) return;
const result = await this.messagesRepository.markAsRead(
conversationId,
userId,
);
// Notifier l'autre participant
const participants =
await this.messagesRepository.getParticipants(conversationId);
const otherParticipant = participants.find((p) => p.userId !== userId);
if (otherParticipant) {
this.eventsGateway.sendToUser(otherParticipant.userId, "messages_read", {
conversationId,
readerId: userId,
});
}
return result;
} }
} }

View File

@@ -55,4 +55,8 @@ export const MessageService = {
}); });
return data; return data;
}, },
async markAsRead(conversationId: string): Promise<void> {
await api.patch(`/messages/conversations/${conversationId}/read`);
},
}; };