Skip to content

Commit f28f6cf

Browse files
committed
feat: add group chat functionality with message handling and UI integration
- Introduced GroupChatMessage model in Prisma schema for storing chat messages. - Created GroupChat module, controller, service, and gateway for handling chat logic and WebSocket connections. - Implemented GroupChat component in frontend for real-time messaging, file attachments, and message display. - Updated ProjectManagement component to include GroupChat view mode and navigation. - Enhanced Sidebar with Group Chat menu option for easy access.
1 parent 68a3e60 commit f28f6cf

9 files changed

Lines changed: 1122 additions & 3 deletions

File tree

backend/prisma/schema.prisma

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,21 @@ model Invite {
187187
updatedAt DateTime?
188188
}
189189

190+
model GroupChatMessage {
191+
id String @id @default(cuid())
192+
workspaceId String
193+
memberId String
194+
memberName String
195+
memberPhoto String?
196+
content String @db.Text
197+
attachments Json?
198+
replyTo Json?
199+
createdAt DateTime @default(now())
200+
201+
@@index([workspaceId, createdAt])
202+
@@map("group_chat_message")
203+
}
204+
190205
enum Role {
191206
USER
192207
ADMIN

backend/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { SharedModule } from "./common/shared.module";
1616
import { UploadModule } from "./upload/upload.module";
1717
import { ProjectManagementModule } from "./project-management/project-management.module";
1818
import { EmailModule } from "./email/email.module";
19+
import { GroupChatModule } from "./group-chat/group-chat.module";
1920
import { APP_GUARD } from "@nestjs/core";
2021

2122
@Module({
@@ -35,6 +36,7 @@ import { APP_GUARD } from "@nestjs/core";
3536
UploadModule,
3637
ProjectManagementModule,
3738
EmailModule,
39+
GroupChatModule,
3840
],
3941
controllers: [AppController, AskController],
4042
providers: [
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {
2+
Controller,
3+
Get,
4+
Query,
5+
DefaultValuePipe,
6+
ParseIntPipe,
7+
} from "@nestjs/common";
8+
import { GroupChatService } from "./group-chat.service";
9+
10+
@Controller("api/group-chat")
11+
export class GroupChatController {
12+
constructor(private readonly chatService: GroupChatService) {}
13+
14+
/** GET /api/group-chat/messages?workspaceId=xxx&limit=60 */
15+
@Get("messages")
16+
getMessages(
17+
@Query("workspaceId") workspaceId: string,
18+
@Query("limit", new DefaultValuePipe(60), ParseIntPipe) limit: number,
19+
) {
20+
return this.chatService.getMessages(workspaceId, Math.min(limit, 200));
21+
}
22+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import {
2+
WebSocketGateway,
3+
WebSocketServer,
4+
SubscribeMessage,
5+
MessageBody,
6+
ConnectedSocket,
7+
OnGatewayInit,
8+
OnGatewayConnection,
9+
OnGatewayDisconnect,
10+
} from "@nestjs/websockets";
11+
import { Server, Socket } from "socket.io";
12+
import { Logger } from "@nestjs/common";
13+
import { GroupChatService } from "./group-chat.service";
14+
15+
@WebSocketGateway({ cors: { origin: "*" } })
16+
export class GroupChatGateway
17+
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
18+
{
19+
@WebSocketServer()
20+
server: Server;
21+
22+
private readonly logger = new Logger(GroupChatGateway.name);
23+
24+
constructor(private readonly chatService: GroupChatService) {}
25+
26+
afterInit() {
27+
this.logger.log("GroupChatGateway initialised");
28+
}
29+
30+
handleConnection(client: Socket) {
31+
this.logger.debug(`Client connected: ${client.id}`);
32+
}
33+
34+
handleDisconnect(client: Socket) {
35+
this.logger.debug(`Client disconnected: ${client.id}`);
36+
}
37+
38+
/** Client joins a workspace room */
39+
@SubscribeMessage("group-chat:join")
40+
handleJoin(
41+
@MessageBody() data: { workspaceId: string },
42+
@ConnectedSocket() client: Socket,
43+
) {
44+
if (!data?.workspaceId) return;
45+
client.join(`ws:${data.workspaceId}`);
46+
this.logger.debug(`${client.id} joined room ws:${data.workspaceId}`);
47+
}
48+
49+
/** Client leaves a workspace room */
50+
@SubscribeMessage("group-chat:leave")
51+
handleLeave(
52+
@MessageBody() data: { workspaceId: string },
53+
@ConnectedSocket() client: Socket,
54+
) {
55+
if (!data?.workspaceId) return;
56+
client.leave(`ws:${data.workspaceId}`);
57+
}
58+
59+
/** Client sends a message (content and/or attachments) */
60+
@SubscribeMessage("group-chat:send")
61+
async handleSend(
62+
@MessageBody()
63+
data: {
64+
workspaceId: string;
65+
memberId: string;
66+
memberName: string;
67+
memberPhoto?: string;
68+
content: string;
69+
attachments?: Array<{
70+
url: string;
71+
name: string;
72+
type: string;
73+
size: number;
74+
}>;
75+
replyTo?: {
76+
id: string;
77+
memberName: string;
78+
content: string;
79+
attachments?: Array<{
80+
url: string;
81+
name: string;
82+
type: string;
83+
size: number;
84+
}> | null;
85+
};
86+
},
87+
@ConnectedSocket() client: Socket,
88+
) {
89+
const hasContent = data?.content?.trim();
90+
const hasAttachments =
91+
Array.isArray(data?.attachments) && data.attachments.length > 0;
92+
if (!data?.workspaceId || (!hasContent && !hasAttachments)) return;
93+
94+
const saved = await this.chatService.saveMessage({
95+
workspaceId: data.workspaceId,
96+
memberId: data.memberId,
97+
memberName: data.memberName,
98+
memberPhoto: data.memberPhoto ?? null,
99+
content: data.content?.trim() ?? "",
100+
attachments: hasAttachments ? data.attachments : undefined,
101+
replyTo: data.replyTo ?? undefined,
102+
});
103+
104+
// broadcast to everyone in the room (including sender)
105+
this.server.to(`ws:${data.workspaceId}`).emit("group-chat:message", saved);
106+
}
107+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Module } from "@nestjs/common";
2+
import { GroupChatController } from "./group-chat.controller";
3+
import { GroupChatService } from "./group-chat.service";
4+
import { GroupChatGateway } from "./group-chat.gateway";
5+
import { PrismaClient } from "@prisma/client";
6+
7+
@Module({
8+
controllers: [GroupChatController],
9+
providers: [
10+
GroupChatService,
11+
GroupChatGateway,
12+
{
13+
provide: PrismaClient,
14+
useValue: new PrismaClient(),
15+
},
16+
],
17+
})
18+
export class GroupChatModule {}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Injectable } from "@nestjs/common";
2+
import { PrismaClient } from "@prisma/client";
3+
4+
@Injectable()
5+
export class GroupChatService {
6+
constructor(private readonly prisma: PrismaClient) {}
7+
8+
async getMessages(workspaceId: string, limit = 60) {
9+
const rows = await this.prisma.groupChatMessage.findMany({
10+
where: { workspaceId },
11+
orderBy: { createdAt: "desc" },
12+
take: limit,
13+
});
14+
// return in chronological order for the client
15+
return rows.reverse();
16+
}
17+
18+
async saveMessage(data: {
19+
workspaceId: string;
20+
memberId: string;
21+
memberName: string;
22+
memberPhoto?: string | null;
23+
content: string;
24+
attachments?: any;
25+
replyTo?: any;
26+
}) {
27+
return this.prisma.groupChatMessage.create({ data });
28+
}
29+
}

0 commit comments

Comments
 (0)