diff --git a/prisma/schema.prisma b/prisma/schema.prisma index df9aa53..f3e43f7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -488,6 +488,12 @@ model Conversation { userUnreadCount Int @default(0) // Unread count for the user agentUnreadCount Int @default(0) // Unread count for the agent + // Mute & Favorite (per-user) + userMuted Boolean @default(false) + agentMuted Boolean @default(false) + userFavorited Boolean @default(false) + agentFavorited Boolean @default(false) + // Timestamps createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/src/messages/messages.controller.ts b/src/messages/messages.controller.ts index fb3f208..a387b96 100644 --- a/src/messages/messages.controller.ts +++ b/src/messages/messages.controller.ts @@ -218,6 +218,36 @@ export class MessagesController { return this.messagesService.markMessagesAsRead(conversationId, userId); } + // ========================================== + // TOGGLE MUTE + // ========================================== + @Patch('conversations/:id/mute') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Mute or unmute conversation notifications' }) + @ApiParam({ name: 'id', description: 'Conversation ID' }) + async toggleMute( + @Param('id') conversationId: string, + @CurrentUser('id') userId: string, + @Body() dto: { muted: boolean }, + ) { + return this.messagesService.toggleMute(conversationId, userId, dto.muted); + } + + // ========================================== + // TOGGLE FAVORITE + // ========================================== + @Patch('conversations/:id/favorite') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Star or unstar a conversation' }) + @ApiParam({ name: 'id', description: 'Conversation ID' }) + async toggleFavorite( + @Param('id') conversationId: string, + @CurrentUser('id') userId: string, + @Body() dto: { favorited: boolean }, + ) { + return this.messagesService.toggleFavorite(conversationId, userId, dto.favorited); + } + // ========================================== // CLEAR CHAT (delete messages, keep conversation) // ========================================== diff --git a/src/messages/messages.service.ts b/src/messages/messages.service.ts index 048b613..e73bc70 100644 --- a/src/messages/messages.service.ts +++ b/src/messages/messages.service.ts @@ -722,4 +722,64 @@ export class MessagesService { return result._sum.userUnreadCount || 0; } } + + /** + * Toggle mute status for a conversation + */ + async toggleMute(conversationId: string, userId: string, muted: boolean) { + const conversation = await this.prisma.conversation.findUnique({ + where: { id: conversationId }, + include: { agentProfile: { select: { userId: true } } }, + }); + + if (!conversation) throw new NotFoundException('Conversation not found'); + + const isUser = conversation.userId === userId; + const isAgent = conversation.agentProfile.userId === userId; + if (!isUser && !isAgent) throw new ForbiddenException('You are not part of this conversation'); + + const updated = await this.prisma.conversation.update({ + where: { id: conversationId }, + data: isUser ? { userMuted: muted } : { agentMuted: muted }, + }); + + return { id: updated.id, muted: isUser ? updated.userMuted : updated.agentMuted }; + } + + /** + * Toggle favorite status for a conversation + */ + async toggleFavorite(conversationId: string, userId: string, favorited: boolean) { + const conversation = await this.prisma.conversation.findUnique({ + where: { id: conversationId }, + include: { agentProfile: { select: { userId: true } } }, + }); + + if (!conversation) throw new NotFoundException('Conversation not found'); + + const isUser = conversation.userId === userId; + const isAgent = conversation.agentProfile.userId === userId; + if (!isUser && !isAgent) throw new ForbiddenException('You are not part of this conversation'); + + const updated = await this.prisma.conversation.update({ + where: { id: conversationId }, + data: isUser ? { userFavorited: favorited } : { agentFavorited: favorited }, + }); + + return { id: updated.id, favorited: isUser ? updated.userFavorited : updated.agentFavorited }; + } + + /** + * Check if a conversation is muted for a specific user + */ + async isConversationMuted(conversationId: string, userId: string): Promise { + const conversation = await this.prisma.conversation.findUnique({ + where: { id: conversationId }, + include: { agentProfile: { select: { userId: true } } }, + }); + if (!conversation) return false; + + const isUser = conversation.userId === userId; + return isUser ? conversation.userMuted : conversation.agentMuted; + } } diff --git a/src/notifications/notifications.service.ts b/src/notifications/notifications.service.ts index 1e1b3e6..17f588d 100644 --- a/src/notifications/notifications.service.ts +++ b/src/notifications/notifications.service.ts @@ -347,6 +347,18 @@ export class NotificationsService implements OnModuleInit { messagePreview: string; conversationId: string; }) { + // Check if the recipient has muted this conversation + const conversation = await this.prisma.conversation.findUnique({ + where: { id: event.conversationId }, + include: { agentProfile: { select: { userId: true } } }, + }); + + if (conversation) { + const isUser = conversation.userId === event.recipientUserId; + const isMuted = isUser ? conversation.userMuted : conversation.agentMuted; + if (isMuted) return; // Skip notification for muted conversations + } + const prefix = await this.getRoutePrefix(event.recipientUserId); const actionUrl = `${prefix}/message?conversationId=${event.conversationId}`;