diff --git a/client/app/(patient)/chats/@chatList/page.tsx b/client/app/(patient)/chats/@chatList/page.tsx index 9932ce7e..2b3bb726 100644 --- a/client/app/(patient)/chats/@chatList/page.tsx +++ b/client/app/(patient)/chats/@chatList/page.tsx @@ -16,7 +16,7 @@ const Page = () => { if (chats) { setLoading(false) } - setDoctors(prev => data?.items.map(({ _id, image, name }) => ({ _id, name, profilePicture: image })) || []) + setDoctors(data?.items.map(({ _id, image, name }) => ({ _id, name, profilePicture: image })) || []) }, [chats, data]); const handleCloseModal = () => { diff --git a/client/lib/hooks/useChats.ts b/client/lib/hooks/useChats.ts index 5c0c8f75..7eab4868 100644 --- a/client/lib/hooks/useChats.ts +++ b/client/lib/hooks/useChats.ts @@ -58,6 +58,7 @@ const useChats = ({ role, messagePath }: Props) => { : setCredentials("patientToken", refreshedToken); socket.emit("authenticate", { token: refreshedToken }); setError(null); + router.refresh(); } catch (err) { console.error("Failed to refresh token", err); setError({ message: "Failed to refresh token" }); diff --git a/client/lib/hooks/useMessages.ts b/client/lib/hooks/useMessages.ts index a4efe51a..f50bc6a3 100644 --- a/client/lib/hooks/useMessages.ts +++ b/client/lib/hooks/useMessages.ts @@ -5,6 +5,7 @@ import { CustomError } from "@/types" import connectSocketIO from "../socket.io/connectSocketIO" import { useAuth } from "./useAuth" import refreshToken from "../socket.io/refreshToken" +import { useRouter } from "next/navigation" type Props = { role: "patient" | "doctor"; @@ -17,6 +18,7 @@ const useMessages = ({ role, chatId }: Props) => { const [chat, setChat] = useState() const [error, setError] = useState(); const { setCredentials } = useAuth(); + const router = useRouter(); const connectSocket = useCallback(() => { if (socketRef.current) return; @@ -67,6 +69,7 @@ const useMessages = ({ role, chatId }: Props) => { : setCredentials("patientToken", refreshedToken); socket.emit("authenticate", { token: refreshedToken }); setError(null); + router.refresh(); } catch (err) { console.error("Failed to refresh token", err); setError({ message: "Failed to refresh token" }); diff --git a/server/src/presentation/events/ChatSocketEvents.ts b/server/src/presentation/events/ChatSocketEvents.ts new file mode 100644 index 00000000..fe5502b4 --- /dev/null +++ b/server/src/presentation/events/ChatSocketEvents.ts @@ -0,0 +1,129 @@ +import { Socket, Namespace } from "socket.io"; +import CreateChatUseCase from "../../use_case/chat/CreateChatUseCase"; +import { TokenPayload, UserRole, StatusCode } from "../../types"; +import GetChatUseCase from "../../use_case/chat/GetChatUseCase"; +import CustomError from "../../domain/entities/CustomError"; + +export default class ChatSocketEvents { + constructor( + private io: Namespace, + private createChatUseCase: CreateChatUseCase, + private getChatUseCase: GetChatUseCase + ) {} + + public initializeEvents(socket: Socket) { + socket.on("joinRoom", async (chatId: string) => { + await this.handleError(socket, async () => { + await this.joinChatRoom(socket, chatId); + }); + }); + + socket.on("getChats", async () => { + await this.handleError(socket, async () => { + await this.getChats(socket); + }); + }); + + socket.on("markReceived", async ({ chatId, receiverId }) => { + await this.handleError(socket, async () => { + await this.updateReceived(chatId, receiverId); + }); + }); + + socket.on("getMessages", async (chatId: string) => { + await this.handleError(socket, async () => { + await this.getMessages(socket, chatId); + }); + }); + + socket.on("getPatients", async () => { + await this.handleError(socket, async () => { + await this.getPatients(socket); + }); + }); + + socket.on("createChat", async (receiverId: string) => { + await this.handleError(socket, async () => { + await this.createChat(socket, receiverId); + }); + }); + + socket.on("createMessage", async ({ chatId, receiverId, message }) => { + await this.handleError(socket, async () => { + await this.createMessage(socket, chatId, receiverId, message); + }); + }); + } + + private async joinChatRoom(socket: Socket, chatId: string) { + const user = socket.data.user as TokenPayload; + const isAuthorized = await this.getChatUseCase.isAuthorizedInChat(chatId, user.id); + if (!isAuthorized) { + throw new CustomError("Unauthorized to join this chat", StatusCode.Forbidden); + } + + socket.join(chatId); + socket.emit("joinedRoom", chatId); + } + + private async createChat(socket: Socket, receiverId: string) { + const user = socket.data.user as TokenPayload; + const doctorId = user.role === UserRole.Doctor ? user.id : receiverId; + const patientId = user.role === UserRole.Patient ? user.id : receiverId; + + const chatId = await this.createChatUseCase.createChat(doctorId, patientId); + socket.join(chatId.toString()); + + socket.emit("joinedRoom", chatId.toString()); + } + + async updateReceived(chatId: string, receiverId: string) { + await this.getChatUseCase.markReceived(chatId, receiverId); + this.io.to(chatId).emit("received", { chatId }); + } + + private async createMessage(socket: Socket, chatId: string, receiverId: string, message: string) { + const user = socket.data.user as TokenPayload; + const createdMessage = await this.createChatUseCase.createMessage(chatId, receiverId, message, user.id); + this.io.to(chatId).emit("newMessage", createdMessage); + await this.getChats(socket); + } + + private async getChats(socket: Socket) { + const user = socket.data.user as TokenPayload; + let chats; + + if (user.role === UserRole.Doctor) { + chats = await this.getChatUseCase.getAllChatsWithDoctorId(user.id); + } else { + chats = await this.getChatUseCase.getAllChatsWithPatientId(user.id); + } + + socket.emit("chats", chats); + } + + private async getMessages(socket: Socket, chatId: string) { + const { chat, messages } = await this.getChatUseCase.getMessagesOfChat(chatId); + socket.emit("messages", messages); + socket.emit("chat", chat); + await this.getChats(socket); + } + + private async getPatients(socket: Socket) { + const patients = await this.getChatUseCase.getPatientsDoctor(); + socket.emit("patients", patients); + } + + private async handleError(socket: Socket, handler: () => Promise) { + try { + await handler(); + } catch (error) { + if (error instanceof CustomError) { + socket.emit("error", { message: error.message, statusCode: error.statusCode }); + } else { + socket.emit("error", { message: "An unexpected error occurred" }); + console.error("Unexpected error:", error); + } + } + } +} diff --git a/server/src/presentation/events/NotificationSocketEvents.ts b/server/src/presentation/events/NotificationSocketEvents.ts new file mode 100644 index 00000000..3b21a203 --- /dev/null +++ b/server/src/presentation/events/NotificationSocketEvents.ts @@ -0,0 +1,52 @@ +import { Socket, Namespace } from "socket.io"; +import NotificationUseCase from "../../use_case/notification/NotificationUseCae"; +import CustomError from "../../domain/entities/CustomError"; +import { TokenPayload, UserRole } from "../../types"; +import logger from "../../utils/logger"; + +export default class NotificationSocketEvents { + constructor( + private io: Namespace, + private notificationUseCase: NotificationUseCase + ) {} + + public initializeEvents(socket: Socket) { + socket.on("getNotifications", () => this.handleErrors(socket, this.getNotifications(socket))); + socket.on("clearNotification", (notificationId: string) => this.handleErrors(socket, this.clearOne(socket, notificationId))); + socket.on("clearAllNotifications", (notificationIds: string[]) => this.handleErrors(socket, this.clearAll(socket, notificationIds))); + } + + private async getNotifications(socket: Socket) { + const user = socket.data.user as TokenPayload; + let notifications; + if (user.role === UserRole.Patient) { + notifications = await this.notificationUseCase.getAllPatient(user.id); + } else if (user.role === UserRole.Doctor) { + notifications = await this.notificationUseCase.getAllDoctor(user.id); + } + socket.emit("notifications", notifications); + } + + private async clearOne(socket: Socket, notificationId: string) { + await this.notificationUseCase.clearOne(notificationId); + socket.emit("notificationCleared", notificationId); + } + + private async clearAll(socket: Socket, notificationIds: string[]) { + await this.notificationUseCase.clearAll(notificationIds); + socket.emit("notificationsCleared", notificationIds); + } + + private async handleErrors(socket: Socket, handler: Promise) { + try { + await handler; + } catch (error) { + if (error instanceof CustomError) { + socket.emit("error", error.message); + } else { + socket.emit("error", "An unexpected error occurred"); + logger.error(error); + } + } + } +} diff --git a/server/src/presentation/events/VideoSocketEvents.ts b/server/src/presentation/events/VideoSocketEvents.ts new file mode 100644 index 00000000..c4c1d53b --- /dev/null +++ b/server/src/presentation/events/VideoSocketEvents.ts @@ -0,0 +1,57 @@ +import { Socket, Namespace } from "socket.io"; +import UpdateAppointmentUseCase from "../../use_case/appointment/UpdateAppointmentUseCase"; +import logger from "../../utils/logger"; + +export default class VideoSocketEvents { + constructor( + private io: Namespace, + private updateAppointmentUseCase: UpdateAppointmentUseCase + ) { } + + public initializeEvents(socket: Socket) { + socket.on("join-room", async (roomId: string) => { + if (roomId) { + if (socket.data.user.role === 'doctor') { + await this.updateAppointmentUseCase.updateCompleteSection(roomId, socket.data.user.id!); + } + socket.join(roomId); + } else { + socket.emit("error", { message: "Room ID is required to join the room" }); + } + }); + + socket.on("signal", (signalData: any, roomId: string) => { + try { + socket.to(roomId).emit("signal", signalData); + } catch (error: any) { + logger.error(`Error handling signal from socket ${socket.id}: ${error.message}`); + socket.emit("error", { message: `Error handling signal from socket ${socket.id}: ${error.message}` }); + } + }); + + socket.on("disconnect", () => { + this.handleUserDisconnection(socket); + }); + + socket.on("leave-room", (roomId: string) => { + if (roomId) { + socket.leave(roomId); + this.notifyRoomAboutLeaving(roomId, socket.id); + } else { + logger.warn(`Socket ${socket.id} attempted to leave a room without a roomId`); + } + }); + } + + private notifyRoomAboutLeaving(roomId: string, socketId: string) { + this.io.to(roomId).emit("leave-room", { userId: socketId }); + } + + private handleUserDisconnection(socket: Socket) { + const rooms = Array.from(socket.rooms).filter(room => room !== socket.id); + rooms.forEach((roomId) => { + socket.leave(roomId); + this.notifyRoomAboutLeaving(roomId, socket.id); + }); + } +} diff --git a/server/src/presentation/socket/socketManagers/ChatSocketManager.ts b/server/src/presentation/socket/socketManagers/ChatSocketManager.ts index 67a53ab6..a9284705 100644 --- a/server/src/presentation/socket/socketManagers/ChatSocketManager.ts +++ b/server/src/presentation/socket/socketManagers/ChatSocketManager.ts @@ -1,21 +1,23 @@ -import { Server, Socket, Namespace } from "socket.io"; +import { Server, Namespace } from "socket.io"; import ITokenService from "../../../domain/interface/services/ITokenService"; import CreateChatUseCase from "../../../use_case/chat/CreateChatUseCase"; import GetChatUseCase from "../../../use_case/chat/GetChatUseCase"; -import { StatusCode, TokenPayload, UserRole } from "../../../types"; import CustomError from "../../../domain/entities/CustomError"; -import logger from "../../../utils/logger"; +import ChatSocketEvents from "../../events/ChatSocketEvents"; +import { StatusCode } from "../../../types"; export default class ChatSocketManager { private io: Namespace; + private chatSocketEvents: ChatSocketEvents; constructor( io: Server, private tokenService: ITokenService, - private createChatUseCase: CreateChatUseCase, - private getChatUseCase: GetChatUseCase + createChatUseCase: CreateChatUseCase, + getChatUseCase: GetChatUseCase ) { this.io = io.of("/chat"); + this.chatSocketEvents = new ChatSocketEvents(this.io, createChatUseCase, getChatUseCase); this.configureMiddleware(); this.initializeChatNamespace(); } @@ -36,128 +38,8 @@ export default class ChatSocketManager { } private initializeChatNamespace() { - this.io.on("connection", (socket: Socket) => { - this.initializeEvents(socket); + this.io.on("connection", (socket) => { + this.chatSocketEvents.initializeEvents(socket); }); } - - private initializeEvents(socket: Socket) { - socket.on("joinRoom", async (chatId: string) => { - await this.handleError(socket, async () => { - await this.joinChatRoom(socket, chatId); - }); - }); - - socket.on("getChats", async () => { - await this.handleError(socket, async () => { - await this.getChats(socket); - }); - }); - - socket.on("markReceived", async ({ chatId, receiverId }) => { - await this.handleError(socket, async () => { - await this.updateReceived(chatId, receiverId); - }); - }) - - socket.on("getMessages", async (chatId: string) => { - await this.handleError(socket, async () => { - await this.getMessages(socket, chatId); - }); - }); - - socket.on("getPatients", async () => { - await this.handleError(socket, async () => { - await this.getPatients(socket); - }); - }); - - socket.on("createChat", async (receiverId: string) => { - await this.handleError(socket, async () => { - await this.createChat(socket, receiverId); - }); - }); - - socket.on("createMessage", async ({ chatId, receiverId, message }) => { - await this.handleError(socket, async () => { - await this.createMessage(socket, chatId, receiverId, message); - }); - }); - - } - - private async joinChatRoom(socket: Socket, chatId: string) { - const user = socket.data.user as TokenPayload; - const isAuthorized = await this.getChatUseCase.isAuthorizedInChat(chatId, user.id); - if (!isAuthorized) { - throw new CustomError("Unauthorized to join this chat", StatusCode.Forbidden); - } - - socket.join(chatId); - socket.emit("joinedRoom", chatId); - } - - private async createChat(socket: Socket, receiverId: string) { - const user = socket.data.user as TokenPayload; - const doctorId = user.role === UserRole.Doctor ? user.id : receiverId; - const patientId = user.role === UserRole.Patient ? user.id : receiverId; - - const chatId = await this.createChatUseCase.createChat(doctorId, patientId); - socket.join(chatId.toString()); - - socket.emit("joinedRoom", chatId.toString()); - } - - async updateReceived(chatId: string, receiverId: string) { - await this.getChatUseCase.markReceived(chatId, receiverId); - this.io.to(chatId).emit("received", { chatId }) - } - - private async createMessage(socket: Socket, chatId: string, receiverId: string, message: string) { - const user = socket.data.user as TokenPayload; - - const createdMessage = await this.createChatUseCase.createMessage(chatId, receiverId, message, user.id); - this.io.to(chatId).emit("newMessage", createdMessage); - - await this.getChats(socket); - } - - private async getChats(socket: Socket) { - const user = socket.data.user as TokenPayload; - let chats; - - if (user.role === UserRole.Doctor) { - chats = await this.getChatUseCase.getAllChatsWithDoctorId(user.id); - } else { - chats = await this.getChatUseCase.getAllChatsWithPatientId(user.id); - } - - socket.emit("chats", chats); - } - - private async getMessages(socket: Socket, chatId: string) { - const { chat, messages } = await this.getChatUseCase.getMessagesOfChat(chatId); - socket.emit("messages", messages); - socket.emit("chat", chat); - - await this.getChats(socket); - } - - private async getPatients(socket: Socket) { - const patients = await this.getChatUseCase.getPatientsDoctor(); - socket.emit("patients", patients); - } - - private async handleError(socket: Socket, handler: () => Promise) { - try { - await handler(); - } catch (error) { - if (error instanceof CustomError) { - socket.emit("error", { message: error.message, statusCode: error.statusCode }); - } else { - socket.emit("error", { message: "An unexpected error occurred" }); - logger.error("Unexpected error:", error); - } - } - } } diff --git a/server/src/presentation/socket/socketManagers/NotificationSocketManager.ts b/server/src/presentation/socket/socketManagers/NotificationSocketManager.ts index 2b7a87ae..f76a6355 100644 --- a/server/src/presentation/socket/socketManagers/NotificationSocketManager.ts +++ b/server/src/presentation/socket/socketManagers/NotificationSocketManager.ts @@ -1,12 +1,11 @@ import { Namespace, Server, Socket } from "socket.io"; import NotificationUseCase from "../../../use_case/notification/NotificationUseCae"; import ITokenService from "../../../domain/interface/services/ITokenService"; -import CustomError from "../../../domain/entities/CustomError"; -import { TokenPayload, UserRole } from "../../../types"; -import logger from "../../../utils/logger"; +import NotificationSocketEvents from "../../events/NotificationSocketEvents"; export default class NotificationSocketManager { private io: Namespace; + private notificationSocketEvents: NotificationSocketEvents; constructor( io: Server, @@ -14,6 +13,7 @@ export default class NotificationSocketManager { private tokenService: ITokenService ) { this.io = io.of("/notification"); + this.notificationSocketEvents = new NotificationSocketEvents(this.io, notificationUseCase); this.setupMiddleware(); this.initializeNotificationNamespace(); } @@ -47,57 +47,11 @@ export default class NotificationSocketManager { }); if (socket.data.user) { - this.initializeEvents(socket); + this.notificationSocketEvents.initializeEvents(socket); } else { socket.emit("error", "User is not authenticated"); socket.disconnect(); } }); } - - private initializeEvents(socket: Socket) { - socket.on("getNotifications", - () => this.handleErrors(socket, this.getNotifications(socket)) - ); - socket.on("clearNotification", - (notificationId: string) => this.handleErrors(socket, this.clearOne(socket, notificationId)) - ); - socket.on("clearAllNotifications", - (notificationIds: string[]) => this.handleErrors(socket, this.clearAll(socket, notificationIds)) - ); - } - - private async getNotifications(socket: Socket) { - const user = socket.data.user as TokenPayload; - let notifications; - if (user.role === UserRole.Patient) { - notifications = await this.notificationUseCase.getAllPatient(user.id); - } else if (user.role === UserRole.Doctor) { - notifications = await this.notificationUseCase.getAllDoctor(user.id); - } - socket.emit("notifications", notifications); - } - - private async clearOne(socket: Socket, notificationId: string) { - await this.notificationUseCase.clearOne(notificationId); - socket.emit("notificationCleared", notificationId); - } - - private async clearAll(socket: Socket, notificationIds: string[]) { - await this.notificationUseCase.clearAll(notificationIds); - socket.emit("notificationsCleared", notificationIds); - } - - private async handleErrors(socket: Socket, handler: Promise) { - try { - await handler; - } catch (error) { - if (error instanceof CustomError) { - socket.emit("error", error.message); - } else { - socket.emit("error", "An unexpected error occurred"); - logger.error(error); - } - } - } } diff --git a/server/src/presentation/socket/socketManagers/VideoSocketManager.ts b/server/src/presentation/socket/socketManagers/VideoSocketManager.ts index 071a9b5e..823b92ae 100644 --- a/server/src/presentation/socket/socketManagers/VideoSocketManager.ts +++ b/server/src/presentation/socket/socketManagers/VideoSocketManager.ts @@ -1,10 +1,12 @@ import { Server, Socket, Namespace } from "socket.io"; import UpdateAppointmentUseCase from "../../../use_case/appointment/UpdateAppointmentUseCase"; import ITokenService from "../../../domain/interface/services/ITokenService"; +import VideoSocketEvents from "../../events/VideoSocketEvents"; import logger from "../../../utils/logger"; export default class VideoSocketManager { private io: Namespace; + private videoSocketEvents: VideoSocketEvents; constructor( io: Server, @@ -12,6 +14,7 @@ export default class VideoSocketManager { private tokenService: ITokenService ) { this.io = io.of("/video"); + this.videoSocketEvents = new VideoSocketEvents(this.io, updateAppointmentUseCase); this.initializeVideoNamespace(); this.io.use((socket: Socket, next) => { const token = socket.handshake.auth.token; @@ -33,56 +36,7 @@ export default class VideoSocketManager { private initializeVideoNamespace() { this.io.on("connection", (socket: Socket) => { - - socket.on("join-room", async (roomId: string) => { - if (roomId) { - if (socket.data.user.role === 'doctor') { - await this.updateAppointmentUseCase.updateCompleteSection(roomId, socket.data.user.id!); - } - socket.join(roomId); - } else { - logger.warn(`Socket ${socket.id} attempted to join a room without a roomId`); - socket.emit("error", { message: "Room ID is required to join the room" }); - } - }); - - socket.on("signal", (signalData: any, roomId: string) => { - try { - if (signalData && roomId) { - socket.to(roomId).emit("signal", signalData); - } else { - logger.warn(`Invalid signal data or roomId from socket ${socket.id}`); - } - } catch (error: any) { - logger.error(`Error handling signal from socket ${socket.id}: ${error.message}`); - socket.emit("error", { message: `Error handling signal from socket ${socket.id}: ${error.message}` }); - } - }); - - socket.on("disconnect", () => { - this.handleUserDisconnection(socket); - }); - - socket.on("leave-room", (roomId: string) => { - if (roomId) { - socket.leave(roomId); - this.notifyRoomAboutLeaving(roomId, socket.id); - } else { - logger.warn(`Socket ${socket.id} attempted to leave a room without a roomId`); - } - }); - }); - } - - private notifyRoomAboutLeaving(roomId: string, socketId: string) { - this.io.to(roomId).emit("leave-room", { userId: socketId }); - } - - private handleUserDisconnection(socket: Socket) { - const rooms = Array.from(socket.rooms).filter(room => room !== socket.id); - rooms.forEach((roomId) => { - socket.leave(roomId); - this.notifyRoomAboutLeaving(roomId, socket.id); + this.videoSocketEvents.initializeEvents(socket); }); } }