Skip to content

Commit 7f98155

Browse files
authored
feat(chat): DMs done (#108)
1 parent cee4b59 commit 7f98155

File tree

12 files changed

+563
-58
lines changed

12 files changed

+563
-58
lines changed

src/auth/auth.module.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import { GitHubStrategy } from './strategies/github.strategy';
1111
import { CaptchaService } from './captcha.service';
1212
import { UsernameService } from './username.service';
1313
import { UserModule } from 'src/user/user.module';
14-
import { RedisModule } from '@nestjs-modules/ioredis';
15-
import { RedisService } from 'src/redis/redis.service';
1614
import { VerificationModule } from 'src/verification/verification.module';
1715
import { CommunicationModule } from 'src/communication/communication.module';
1816
import { EmailService } from 'src/communication/email.service';
@@ -39,7 +37,6 @@ import { EmailJobsService } from 'src/background-jobs/email/email.service';
3937
}),
4038
PassportModule,
4139
UserModule,
42-
RedisModule,
4340
VerificationModule,
4441
CommunicationModule,
4542
BackgroundJobsModule,
@@ -50,7 +47,6 @@ import { EmailJobsService } from 'src/background-jobs/email/email.service';
5047
JwtStrategy,
5148
GitHubStrategy,
5249
UserRepository,
53-
RedisService,
5450
VerificationService,
5551
EmailService,
5652
EmailJobsService,

src/auth/auth.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export class AuthService {
114114
if (!unverified_user) {
115115
throw new NotFoundException(ERROR_MESSAGES.USER_NOT_FOUND);
116116
} else {
117+
console.log(`pass ${password} vs ${unverified_user.password}`);
117118
const is_password_valid = await bcrypt.compare(password, unverified_user.password);
118119
if (!is_password_valid) {
119120
throw new UnauthorizedException(ERROR_MESSAGES.WRONG_PASSWORD);

src/chat/chat.module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ import { Message } from '../messages/entities/message.entity';
1212

1313
controllers: [ChatController],
1414
providers: [ChatService, ChatRepository, PaginationService],
15+
exports: [ChatRepository],
1516
})
1617
export class ChatModule {}

src/messages/messages.controller.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
get_messages_swagger,
2525
send_message_swagger,
2626
update_message_swagger,
27+
websocket_docs_swagger,
2728
} from './messages.swagger';
2829
import { MessagesService } from './messages.service';
2930
import { JwtAuthGuard } from 'src/auth/guards/jwt.guard';
@@ -35,6 +36,39 @@ import { JwtAuthGuard } from 'src/auth/guards/jwt.guard';
3536
export class MessagesController {
3637
constructor(private readonly messages_service: MessagesService) {}
3738

39+
@ApiOperation(websocket_docs_swagger.operation)
40+
@ApiOkResponse(websocket_docs_swagger.responses.success)
41+
@Get('websocket-docs')
42+
async socketDocs() {
43+
return {
44+
message: 'WebSocket documentation available in Swagger UI',
45+
namespace: '/messages',
46+
events: {
47+
client_to_server: [
48+
'join_chat',
49+
'leave_chat',
50+
'send_message',
51+
'update_message',
52+
'delete_message',
53+
'typing_start',
54+
'typing_stop',
55+
],
56+
server_to_client: [
57+
'unread_chats_summary',
58+
'new_message',
59+
'message_updated',
60+
'message_deleted',
61+
'user_typing',
62+
'user_stopped_typing',
63+
'joined_chat',
64+
'left_chat',
65+
'message_sent',
66+
'error',
67+
],
68+
},
69+
};
70+
}
71+
3872
@ApiOperation(send_message_swagger.operation)
3973
@ApiParam(send_message_swagger.params.chat_id)
4074
@ApiBody({ type: SendMessageDto })

src/messages/messages.gateway.ts

Lines changed: 73 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { UseGuards } from '@nestjs/common';
1212
import { JwtService } from '@nestjs/jwt';
1313
import { ConfigService } from '@nestjs/config';
1414
import { MessagesService } from './messages.service';
15+
import { ChatRepository } from 'src/chat/chat.repository';
1516
import { GetMessagesQueryDto, SendMessageDto, UpdateMessageDto } from './dto';
1617
import { WsJwtGuard } from 'src/auth/guards/ws-jwt.guard';
1718

@@ -36,6 +37,7 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
3637
private userSockets = new Map<string, Set<string>>();
3738
constructor(
3839
private readonly messages_service: MessagesService,
40+
private readonly chat_repository: ChatRepository,
3941
private readonly jwt_service: JwtService,
4042
private readonly config_service: ConfigService
4143
) {}
@@ -52,12 +54,34 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
5254
this.userSockets.get(user_id)?.add(client.id);
5355

5456
console.log(`Client connected: ${client.id} (User: ${user_id})`);
57+
58+
// Send unread messages count to newly connected client
59+
await this.sendUnreadChatsOnConnection(client, user_id);
5560
} catch (error) {
5661
console.error('Connection error:', error);
5762
client.disconnect();
5863
}
5964
}
6065

66+
/**
67+
* Notify client about chats with unread messages when they connect
68+
* Frontend should then request full message history for these chats
69+
*/
70+
private async sendUnreadChatsOnConnection(client: IAuthenticatedSocket, user_id: string) {
71+
try {
72+
const unread_chats = await this.messages_service.getUnreadChatsForUser(user_id);
73+
74+
if (unread_chats.length > 0) {
75+
client.emit('unread_chats_summary', {
76+
chats: unread_chats,
77+
message: 'You have unread messages in these chats',
78+
});
79+
}
80+
} catch (error) {
81+
console.error('Error sending unread chats on connection:', error);
82+
}
83+
}
84+
6185
handleDisconnect(client: IAuthenticatedSocket) {
6286
const user_id = client.user?.user_id;
6387
if (user_id) {
@@ -81,7 +105,12 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
81105
const user_id = client.user!.user_id;
82106
const { chat_id } = data;
83107

84-
await this.messages_service.validateChatParticipation(user_id, chat_id);
108+
const chat = await this.messages_service.validateChatParticipation(user_id, chat_id);
109+
110+
// Reset unread count for this user when they join the chat
111+
const unread_field =
112+
chat.user1_id === user_id ? 'unread_count_user1' : 'unread_count_user2';
113+
await this.chat_repository.update({ id: chat_id }, { [unread_field]: 0 });
85114

86115
await client.join(chat_id);
87116
return {
@@ -128,21 +157,29 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
128157
const user_id = client.user!.user_id;
129158
const { chat_id, message } = data;
130159

131-
const result = await this.messages_service.sendMessage(user_id, chat_id, message);
160+
// Check if recipient is actively in the chat room
161+
const chat = await this.messages_service.validateChatParticipation(user_id, chat_id);
162+
const recipient_id = chat.user1_id === user_id ? chat.user2_id : chat.user1_id;
163+
console.log('Recipient ID:', recipient_id);
164+
const is_recipient_in_room = await this.isUserInChatRoom(recipient_id, chat_id);
132165

133-
// Emit to the chat room (for users already in the room)
134-
this.server.to(chat_id).emit('new_message', {
135-
chat_id: chat_id,
136-
message: result,
137-
});
166+
const result = await this.messages_service.sendMessage(
167+
user_id,
168+
chat_id,
169+
message,
170+
is_recipient_in_room
171+
);
138172

139-
// Also emit directly to the recipient (in case they're not in the room yet)
140-
const recipient_id = result.recipient_id;
141173
if (recipient_id) {
142174
this.emitToUser(recipient_id, 'new_message', {
143175
chat_id,
144176
message: result,
145177
});
178+
} else {
179+
this.server.to(chat_id).emit('new_message', {
180+
chat_id,
181+
message: result,
182+
});
146183
}
147184

148185
return {
@@ -157,29 +194,6 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
157194
}
158195
}
159196

160-
@SubscribeMessage('get_messages')
161-
async handleGetMessages(
162-
@ConnectedSocket() client: IAuthenticatedSocket,
163-
@MessageBody() data: { chat_id: string; query?: GetMessagesQueryDto }
164-
) {
165-
try {
166-
const user_id = client.user!.user_id;
167-
const { chat_id, query } = data;
168-
169-
const messages = await this.messages_service.getMessages(user_id, chat_id, query || {});
170-
171-
return {
172-
event: 'messages_retrieved',
173-
data: messages,
174-
};
175-
} catch (error) {
176-
return {
177-
event: 'error',
178-
data: { message: error.message || 'Failed to retrieve messages' },
179-
};
180-
}
181-
}
182-
183197
@SubscribeMessage('update_message')
184198
async handleUpdateMessage(
185199
@ConnectedSocket() client: IAuthenticatedSocket,
@@ -196,11 +210,6 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
196210
message_id,
197211
update
198212
);
199-
this.server.to(chat_id).emit('message_updated', {
200-
chat_id,
201-
message_id,
202-
message: result,
203-
});
204213

205214
const recipient_id = result.recipient_id;
206215
if (recipient_id) {
@@ -209,6 +218,12 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
209218
message_id,
210219
message: result,
211220
});
221+
} else {
222+
this.server.to(chat_id).emit('message_updated', {
223+
chat_id,
224+
message_id,
225+
message: result,
226+
});
212227
}
213228

214229
return {
@@ -233,10 +248,6 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
233248
const { chat_id, message_id } = data;
234249

235250
const result = await this.messages_service.deleteMessage(user_id, chat_id, message_id);
236-
this.server.to(chat_id).emit('message_deleted', {
237-
chat_id,
238-
message_id,
239-
});
240251

241252
const recipient_id = result.recipient_id;
242253
if (recipient_id) {
@@ -245,6 +256,11 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
245256
message_id,
246257
message: result,
247258
});
259+
} else {
260+
this.server.to(chat_id).emit('message_deleted', {
261+
chat_id,
262+
message_id,
263+
});
248264
}
249265

250266
return {
@@ -322,4 +338,20 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
322338
});
323339
}
324340
}
341+
342+
async isUserInChatRoom(user_id: string, chat_id: string): Promise<boolean> {
343+
const socket_ids = this.userSockets.get(user_id);
344+
if (!socket_ids) return false;
345+
// Check if any of the user's sockets are in the chat room
346+
const sockets_in_room = await this.server.in(chat_id).fetchSockets();
347+
const room_socket_ids = new Set(sockets_in_room.map((s) => s.id));
348+
349+
for (const socket_id of socket_ids) {
350+
if (room_socket_ids.has(socket_id)) {
351+
console.log(`User ${user_id} is in chat room ${chat_id} via socket ${socket_id}`);
352+
return true;
353+
}
354+
}
355+
return false;
356+
}
325357
}

src/messages/messages.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Message } from './entities/message.entity';
88
import { Chat } from 'src/chat/entities/chat.entity';
99
import { MessagesGateway } from './messages.gateway';
1010
import { JwtModule } from '@nestjs/jwt';
11+
import { ChatModule } from 'src/chat/chat.module';
1112

1213
@Module({
1314
imports: [
@@ -16,6 +17,7 @@ import { JwtModule } from '@nestjs/jwt';
1617
secret: process.env.JWT_TOKEN_SECRET,
1718
signOptions: { expiresIn: '7d' },
1819
}),
20+
ChatModule,
1921
],
2022
providers: [MessagesService, MessageRepository, PaginationService, MessagesGateway],
2123
controllers: [MessagesController],

src/messages/messages.repository.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,20 @@ export class MessageRepository extends Repository<Message> {
1515
super(Message, data_source.createEntityManager());
1616
}
1717

18-
async createMessage(sender_id: string, chat_id: string, dto: SendMessageDto): Promise<Message> {
18+
async createMessage(
19+
sender_id: string,
20+
chat_id: string,
21+
dto: SendMessageDto,
22+
is_read: boolean = false
23+
): Promise<Message> {
1924
try {
2025
const message = this.create({
2126
sender_id,
2227
chat_id,
2328
content: dto.content,
2429
message_type: dto.message_type || MessageType.TEXT,
2530
reply_to_message_id: dto.reply_to_message_id || null,
31+
is_read: is_read,
2632
});
2733

2834
const saved_message = await this.save(message);

0 commit comments

Comments
 (0)