Skip to content

Commit d7cfbb6

Browse files
Notify clients about updated/received chats
1 parent 288e6f5 commit d7cfbb6

File tree

4 files changed

+113
-17
lines changed

4 files changed

+113
-17
lines changed

src/api/v1/chats/chats.router.ts

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as Storage from '@/lib/storage';
66
import * as Service from './chats.service';
77
import * as Middlewares from '@/middlewares';
88
import { Request, Response, Router } from 'express';
9+
import logger from '@/lib/logger';
910

1011
export const chatsRouter = Router();
1112

@@ -28,32 +29,53 @@ chatsRouter.get('/', Middlewares.authValidator, async (req, res) => {
2829
const userId = Utils.getCurrentUserIdFromReq(req)!;
2930
const filters = Utils.getBasePaginationFiltersFromReqQuery(req);
3031
const chats = await Service.getUserChats(userId, filters);
31-
res.json(chats);
32+
res.json(chats).on('finish', () => {
33+
const rooms = Service.getOtherChatsMemberIds(userId, chats);
34+
Utils.emitToRoomsIfAny({ rooms, event: 'chats:received', volatile: true });
35+
});
3236
});
3337

3438
chatsRouter.get('/members/:profileId', Middlewares.authValidator, async (req, res) => {
3539
const userId = Utils.getCurrentUserIdFromReq(req)!;
3640
const chats = await Service.getUserChatsByMember(userId, req.params.profileId);
37-
res.json(chats);
41+
res.json(chats).on('finish', () => {
42+
const rooms = Service.getOtherChatsMemberIds(userId, chats);
43+
Utils.emitToRoomsIfAny({ rooms, event: 'chats:received', volatile: true });
44+
});
3845
});
3946

4047
chatsRouter.get('/:id', Middlewares.authValidator, async (req, res) => {
48+
const chatId = req.params.id;
4149
const userId = Utils.getCurrentUserIdFromReq(req)!;
42-
const chat = await Service.getUserChatById(userId, req.params.id);
43-
res.json(chat);
50+
const chat = await Service.getUserChatById(userId, chatId);
51+
res.json(chat).on('finish', () => {
52+
Service.getOtherChatMemberIds(userId, chatId)
53+
.then((rooms) => Utils.emitToRoomsIfAny({ rooms, event: 'chats:received', volatile: true }))
54+
.catch((error: unknown) => logger.error('Failed to broadcast "chats:received"', error));
55+
});
4456
});
4557

4658
chatsRouter.get('/:id/messages', Middlewares.authValidator, async (req, res) => {
59+
const chatId = req.params.id;
4760
const userId = Utils.getCurrentUserIdFromReq(req)!;
4861
const filters = Utils.getBasePaginationFiltersFromReqQuery(req);
49-
const messages = await Service.getUserChatMessages(userId, req.params.id, filters);
50-
res.json(messages);
62+
const messages = await Service.getUserChatMessages(userId, chatId, filters);
63+
res.json(messages).on('finish', () => {
64+
Service.getOtherChatMemberIds(userId, chatId)
65+
.then((rooms) => Utils.emitToRoomsIfAny({ rooms, event: 'chats:received', volatile: true }))
66+
.catch((error: unknown) => logger.error('Failed to broadcast "chats:received"', error));
67+
});
5168
});
5269

5370
chatsRouter.get('/:id/messages/:msgId', Middlewares.authValidator, async (req, res) => {
71+
const chatId = req.params.id;
5472
const userId = Utils.getCurrentUserIdFromReq(req)!;
55-
const msg = await Service.getUserChatMessageById(userId, req.params.id, req.params.msgId);
56-
res.json(msg);
73+
const msg = await Service.getUserChatMessageById(userId, chatId, req.params.msgId);
74+
res.json(msg).on('finish', () => {
75+
Service.getOtherChatMemberIds(userId, chatId)
76+
.then((rooms) => Utils.emitToRoomsIfAny({ rooms, event: 'chats:received', volatile: true }))
77+
.catch((error: unknown) => logger.error('Failed to broadcast "chats:received"', error));
78+
});
5779
});
5880

5981
chatsRouter.post(
@@ -67,33 +89,65 @@ chatsRouter.post(
6789
);
6890
const preparedImageData = await prepareImageData(req, chatData.message.imagedata, user);
6991
const createdChat = await Service.createChat(user, chatData, ...preparedImageData);
70-
res.status(201).json(createdChat);
92+
res
93+
.status(201)
94+
.json(createdChat)
95+
.on('finish', () => {
96+
Service.getOtherChatMemberIds(user.id, createdChat.id)
97+
.then((rooms) =>
98+
Utils.emitToRoomsIfAny({ rooms, event: 'chats:updated', volatile: true }),
99+
)
100+
.catch((error: unknown) => logger.error('Failed to broadcast "chats:updated"', error));
101+
});
71102
},
72103
);
73104

74105
chatsRouter.patch('/:id/seen', Middlewares.authValidator, async (req, res) => {
106+
const chatId = req.params.id;
75107
const userId = Utils.getCurrentUserIdFromReq(req)!;
76-
res.json(await Service.updateProfileChatLastSeenDate(userId, req.params.id));
108+
res.json(await Service.updateProfileChatLastSeenDate(userId, chatId)).on('finish', () => {
109+
Service.getOtherChatMemberIds(userId, chatId)
110+
.then((rooms) => Utils.emitToRoomsIfAny({ rooms, event: 'chats:updated', volatile: true }))
111+
.catch((error: unknown) => logger.error('Failed to broadcast "chats:updated"', error));
112+
});
77113
});
78114

79115
chatsRouter.post(
80116
'/:id/messages',
81117
Middlewares.authValidator,
82118
Middlewares.createFileProcessor('image'),
83119
async (req: Request, res: Response) => {
120+
const chatId = req.params.id;
84121
const user = Utils.getCurrentUserFromReq(req)!;
85122
const { imagedata, ...msgData } = (
86123
req.file ? Schema.optionalMessageSchema : Schema.messageSchema
87124
).parse(req.body);
88125
const preparedImageData = await prepareImageData(req, imagedata, user);
89-
const msgArgs = [user, req.params.id, msgData, ...preparedImageData] as const;
126+
const msgArgs = [user, chatId, msgData, ...preparedImageData] as const;
90127
const createdMessage = await Service.createUserChatMessage(...msgArgs);
91-
res.status(201).json(createdMessage);
128+
res
129+
.status(201)
130+
.json(createdMessage)
131+
.on('finish', () => {
132+
Service.getOtherChatMemberIds(user.id, chatId)
133+
.then((rooms) =>
134+
Utils.emitToRoomsIfAny({ rooms, event: 'chats:updated', volatile: true }),
135+
)
136+
.catch((error: unknown) => logger.error('Failed to broadcast "chats:updated"', error));
137+
});
92138
},
93139
);
94140

95141
chatsRouter.delete('/:id', Middlewares.authValidator, async (req, res) => {
142+
const chatId = req.params.id;
96143
const userId = Utils.getCurrentUserIdFromReq(req)!;
97-
await Service.deleteChat(userId, req.params.id);
98-
res.status(204).send();
144+
await Service.deleteChat(userId, chatId);
145+
res
146+
.status(204)
147+
.send()
148+
.on('finish', () => {
149+
Service.getOtherChatMemberIds(userId, chatId)
150+
.then((rooms) => Utils.emitToRoomsIfAny({ rooms, event: 'chats:updated', volatile: true }))
151+
.catch((error: unknown) => logger.error('Failed to broadcast "chats:updated"', error));
152+
});
99153
});

src/api/v1/chats/chats.service.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,13 @@ export const deleteChat = async (userId: User['id'], chatId: Chat['id']) => {
173173
if (chat.profiles.length < 2) {
174174
await tx.chat.delete({ where: { id: chatId } });
175175
} else {
176-
const profile = chat.profiles.find((p) => p.profile?.userId === userId);
177-
if (profile) {
178-
const { profileName } = profile;
176+
let profileName: string | undefined;
177+
const otherMembersIds: string[] = [];
178+
for (const cp of chat.profiles) {
179+
if (!profileName && cp.profile?.userId === userId) profileName = cp.profileName;
180+
else if (cp.profileId) otherMembersIds.push(cp.profileId);
181+
}
182+
if (profileName) {
179183
await tx.profilesChats.delete({
180184
where: { profileName_chatId: { chatId, profileName } },
181185
});
@@ -241,6 +245,33 @@ export const getUserChatById = async (userId: User['id'], chatId: Chat['id']) =>
241245
);
242246
};
243247

248+
export const getOtherChatMemberIds = async (userId: User['id'], chatId: Chat['id']) => {
249+
const chat = await db.chat.findUnique({
250+
where: { id: chatId },
251+
select: { profiles: { select: { profile: true } } },
252+
});
253+
const otherMembersIds: string[] = [];
254+
if (chat) {
255+
for (const { profile } of chat.profiles) {
256+
if (profile && profile.userId !== userId) otherMembersIds.push(profile.id);
257+
}
258+
}
259+
return otherMembersIds;
260+
};
261+
262+
export const getOtherChatsMemberIds = (
263+
userId: User['id'],
264+
chats: Awaited<ReturnType<typeof getUserChats>>,
265+
) => {
266+
const chatsMemberIds: string[] = [];
267+
for (const chat of chats) {
268+
for (const { profile } of chat.profiles) {
269+
if (profile && profile.userId !== userId) chatsMemberIds.push(profile.id);
270+
}
271+
}
272+
return chatsMemberIds;
273+
};
274+
244275
export const getUserChatsByMember = async (userId: User['id'], memberIdOrUsername: string) => {
245276
return await Utils.handleDBKnownErrors(
246277
db.$transaction(async (tx) => {

src/lib/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as Storage from '@/lib/storage';
44
import * as AppError from '@/lib/app-error';
55
import { Prisma } from '@/../prisma/client';
66
import { Request } from 'express';
7+
import { io } from '@/lib/io';
78
import { z } from 'zod';
89
import ms from 'ms';
910
import db from '@/lib/db';
@@ -13,6 +14,10 @@ export const lowerCase = <T extends string>(s: T): Lowercase<T> => {
1314
return s.toLowerCase() as Lowercase<T>;
1415
};
1516

17+
export const emitToRoomsIfAny = ({ event, rooms, volatile }: Types.SocketEventData) => {
18+
if (rooms.length) (volatile ? io.volatile : io).to(rooms).emit(event);
19+
};
20+
1621
export const createJwtForUser = (user: Types.PublicUser): string => {
1722
const { id, isAdmin } = user;
1823
const payload: Types.AppJwtPayload = { id, isAdmin };

src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,9 @@ export interface EvaluationResult {
156156
evaluation: Record<string, boolean>;
157157
finder: CharacterFinder;
158158
}
159+
160+
export interface SocketEventData {
161+
event: string;
162+
rooms: string[];
163+
volatile?: boolean;
164+
}

0 commit comments

Comments
 (0)