import { Dispatch } from "@reduxjs/toolkit"; import { setBannerUrl, setLogoUrl, setPrimaryColor, setUserProfilePictureUrl, } from "core/reducers/appSlice"; import { store } from "core/store/store"; import { BrowserTabSession, Constants } from "core/utils/utils"; import { WebSocketReceivedMessagesCmds } from "core/utils/webSocket"; import { addLessonPageContent, deleteLessonPageContent, updateLessonPageContent, updateLessonPageContentPosition, } from "features/Lessons/LessonPage/lessonPageSlice"; import { addLessonContent, deleteLessonContent, setLessonThumbnailTitle, setLessonThumbnailUrl, setPageEditorLessonState, updateLessonContent, updateLessonContentPosition, } from "features/Lessons/LessonPageEditor/lessonPageEditorSlice"; import { addLesson, updateLessonPreviewThumbnail, updateLessonPreviewTitle, updateLessonState, } from "features/Lessons/lessonsSlice"; import { addLikedQuestion, addQuestion, countDownLikedQuestion, countUpLikedQuestion, deleteLikedQuestion, } from "features/Lessons/Questions/lessonQuestionSlice"; import { addTeamMember, deleteTeamMember, updateTeamMemberRole, } from "features/Team/teamSlice"; import { setProfilePictureUrl } from "features/UserProfile/userProfileSlice"; interface WebSocketMessage { Cmd: number; Body: any; } class WebSocketService { private url: string; private socket: WebSocket | null = null; private reconnectInterval: number = 2000; // in ms private offlineQueue: WebSocketMessage[] = []; private firstConnect: boolean = true; private messageHandler: | ((message: WebSocketMessage, dispatch: Dispatch) => void) | null = null; constructor(url: string) { this.url = url; } private dispatch: Dispatch | null = null; public connect(): void { this.socket = new WebSocket( `${this.url}?auth=${localStorage.getItem( "session" )}&bts=${BrowserTabSession}` ); this.socket.onopen = () => { console.log("WebSocket connected", this.firstConnect); // Send all messages from the offline queue this.offlineQueue.forEach((message) => this.send(message)); this.offlineQueue = []; // Dispatch event to notify that the WebSocket connection is established if (!this.firstConnect) { document.dispatchEvent(webSocketConnectionEvent); } else { this.firstConnect = false; } }; this.socket.onmessage = (event: MessageEvent) => { const data: WebSocketMessage = JSON.parse(event.data); if (this.messageHandler) { this.messageHandler(data, this.dispatch!); } else { console.error("No handler defined for WebSocket messages"); } }; this.socket.onclose = () => { console.log("WebSocket disconnected. Reconnecting..."); setTimeout(() => this.connect(), this.reconnectInterval); }; this.socket.onerror = (error: Event) => { console.error("WebSocket error:", error); }; } public setHandler( handler: (message: WebSocketMessage, dispatch: Dispatch) => void, dispatch: Dispatch ): void { this.messageHandler = handler; this.dispatch = dispatch; } public send(message: WebSocketMessage): void { if (this.socket && this.socket.readyState === WebSocket.OPEN) { this.socket.send( JSON.stringify({ Cmd: message.Cmd, Body: message.Body, }) ); } else { this.offlineQueue.push(message); } } public disconnect(): void { if (this.socket) { this.socket.close(); } } } const webSocketConnectionEventName = "WebSocketConnectionEvent"; const webSocketConnectionEvent = new CustomEvent(webSocketConnectionEventName, { detail: "wsReconnect", }); export function addWebSocketReconnectListener(callback: () => void): void { document.addEventListener(webSocketConnectionEventName, callback); } export function removeWebSocketReconnectListener(callback: () => void): void { document.removeEventListener(webSocketConnectionEventName, callback); } const webSocketService = new WebSocketService(Constants.WS_ADDRESS); export default webSocketService; export function WebSocketMessageHandler( message: WebSocketMessage, dispatch: Dispatch ) { const { Cmd, Body } = message; console.log("WebSocketMessageHandler", Cmd, Body); switch (Cmd) { case WebSocketReceivedMessagesCmds.SettingsUpdated: dispatch(setPrimaryColor(Body.PrimaryColor)); break; case WebSocketReceivedMessagesCmds.SettingsUpdatedLogo: dispatch(setLogoUrl(Body)); break; case WebSocketReceivedMessagesCmds.SettingsUpdatedBanner: dispatch(setBannerUrl(Body)); break; case WebSocketReceivedMessagesCmds.SettingsUpdatedSubdomain: localStorage.removeItem("session"); window.location.href = `${ window.location.protocol }//${Body}.${window.location.hostname.split(".").slice(1).join(".")}`; break; case WebSocketReceivedMessagesCmds.TeamAddedMember: dispatch(addTeamMember(Body)); break; case WebSocketReceivedMessagesCmds.TeamUpdatedMemberRole: dispatch(updateTeamMemberRole(Body)); break; case WebSocketReceivedMessagesCmds.TeamDeletedMember: dispatch(deleteTeamMember(Body)); break; case WebSocketReceivedMessagesCmds.LessonCreated: dispatch(addLesson(Body)); break; case WebSocketReceivedMessagesCmds.LessonPreviewTitleUpdated: dispatch(updateLessonPreviewTitle(Body)); if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) { dispatch(setLessonThumbnailTitle(Body.Title)); } break; case WebSocketReceivedMessagesCmds.LessonPreviewThumbnailUpdated: dispatch(updateLessonPreviewThumbnail(Body)); if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) { dispatch(setLessonThumbnailUrl(Body.ThumbnailUrl)); } break; case WebSocketReceivedMessagesCmds.LessonStateUpdated: dispatch(updateLessonState(Body)); if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) { dispatch(setPageEditorLessonState(Body.State)); } break; case WebSocketReceivedMessagesCmds.LessonAddedContent: if (Body.LessonId === store.getState().lessonPage.currentLessonId) { dispatch(addLessonPageContent(Body)); } if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) { dispatch(addLessonContent(Body)); } break; case WebSocketReceivedMessagesCmds.LessonDeletedContent: if (Body.LessonId === store.getState().lessonPage.currentLessonId) { dispatch(deleteLessonPageContent(Body.ContentId)); } if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) { dispatch(deleteLessonContent(Body.ContentId)); } break; case WebSocketReceivedMessagesCmds.LessonContentUpdated: if (Body.LessonId === store.getState().lessonPage.currentLessonId) { dispatch( updateLessonPageContent({ id: Body.ContentId, data: Body.Data, }) ); } if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) { dispatch( updateLessonContent({ id: Body.ContentId, data: Body.Data, }) ); } break; case WebSocketReceivedMessagesCmds.LessonContentUpdatedPosition: if (Body.LessonId === store.getState().lessonPage.currentLessonId) { dispatch( updateLessonPageContentPosition({ contentId: Body.ContentId, position: Body.Position, }) ); } if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) { dispatch( updateLessonContentPosition({ contentId: Body.ContentId, position: Body.Position, }) ); } break; case WebSocketReceivedMessagesCmds.LessonContentFileUpdated: if (Body.LessonId === store.getState().lessonPage.currentLessonId) { dispatch( updateLessonPageContent({ id: Body.ContentId, data: Body.Data, }) ); } if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) { dispatch( updateLessonContent({ id: Body.ContentId, data: Body.Data, }) ); } break; case WebSocketReceivedMessagesCmds.UserProfilePictureUpdated: dispatch(setProfilePictureUrl(Body)); dispatch(setUserProfilePictureUrl(Body)); break; case WebSocketReceivedMessagesCmds.LessonQuestionCreated: if (Body.LessonId === store.getState().lessonPage.currentLessonId) { dispatch(addQuestion(Body)); } break; case WebSocketReceivedMessagesCmds.LessonQuestionLiked: dispatch(addLikedQuestion(Body)); break; case WebSocketReceivedMessagesCmds.LessonQuestionCountUpLikes: dispatch(countUpLikedQuestion(Body)); break; case WebSocketReceivedMessagesCmds.LessonQuestionDisliked: dispatch(deleteLikedQuestion(Body)); break; case WebSocketReceivedMessagesCmds.LessonQuestionCountDownLikes: dispatch(countDownLikedQuestion(Body)); break; default: console.error("Unknown message type:", Cmd); } }