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 { 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; default: console.error('Unknown message type:', Cmd); } }