Skip to content

Commit

Permalink
🚀 completely moved chat from polling to socketio successfully
Browse files Browse the repository at this point in the history
  • Loading branch information
sinanptm committed Oct 6, 2024
1 parent b7e5bb0 commit 190836e
Show file tree
Hide file tree
Showing 9 changed files with 260 additions and 228 deletions.
2 changes: 1 addition & 1 deletion client/app/(patient)/chats/@chatList/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
1 change: 1 addition & 0 deletions client/lib/hooks/useChats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
Expand Down
3 changes: 3 additions & 0 deletions client/lib/hooks/useMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -17,6 +18,7 @@ const useMessages = ({ role, chatId }: Props) => {
const [chat, setChat] = useState<IChat>()
const [error, setError] = useState<CustomError | null>();
const { setCredentials } = useAuth();
const router = useRouter();

const connectSocket = useCallback(() => {
if (socketRef.current) return;
Expand Down Expand Up @@ -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" });
Expand Down
129 changes: 129 additions & 0 deletions server/src/presentation/events/ChatSocketEvents.ts
Original file line number Diff line number Diff line change
@@ -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<void>) {
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);
}
}
}
}
52 changes: 52 additions & 0 deletions server/src/presentation/events/NotificationSocketEvents.ts
Original file line number Diff line number Diff line change
@@ -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<void>) {
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);
}
}
}
}
57 changes: 57 additions & 0 deletions server/src/presentation/events/VideoSocketEvents.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}
}
Loading

1 comment on commit 190836e

@vercel
Copy link

@vercel vercel bot commented on 190836e Oct 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.