Send received message through WebSocket
- send received message from through WebSocket
- remove SocketIO from client
GitLab: #96
Change-Id: I7a8eec04010f0773428f914792c13decef393ebf
diff --git a/client/package.json b/client/package.json
index 52106b2..9bed075 100644
--- a/client/package.json
+++ b/client/package.json
@@ -59,8 +59,7 @@
"react-modal": "^3.15.1",
"react-redux": "^8.0.2",
"react-router-dom": "^6.3.0",
- "react-waypoint": "^10.3.0",
- "socket.io-client": "^4.5.2"
+ "react-waypoint": "^10.3.0"
},
"devDependencies": {
"@types/node": "^18.7.13",
diff --git a/client/src/components/ConversationView.tsx b/client/src/components/ConversationView.tsx
index b09d24d..80e05c7 100644
--- a/client/src/components/ConversationView.tsx
+++ b/client/src/components/ConversationView.tsx
@@ -16,13 +16,13 @@
* <https://www.gnu.org/licenses/>.
*/
import { Divider, Stack, Typography } from '@mui/material';
-import { Account, Conversation, ConversationMember } from 'jami-web-common';
+import { Account, Conversation, ConversationMember, WebSocketMessageType } from 'jami-web-common';
import { useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router';
import { useAuthContext } from '../contexts/AuthProvider';
-import { SocketContext } from '../contexts/Socket';
+import { WebSocketContext } from '../contexts/WebSocketProvider';
import ChatInterface from '../pages/ChatInterface';
import { useConversationQuery } from '../services/Conversation';
import { translateEnumeration, TranslateEnumerationOptions } from '../utils/translations';
@@ -34,7 +34,7 @@
};
const ConversationView = ({ conversationId }: ConversationViewProps) => {
const { account } = useAuthContext();
- const socket = useContext(SocketContext);
+ const webSocket = useContext(WebSocketContext);
const [conversation, setConversation] = useState<Conversation | undefined>();
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(false);
@@ -59,15 +59,12 @@
}, [conversationQuery.isError]);
useEffect(() => {
- if (!conversation) return;
- console.log(`io set conversation ${conversationId} ` + socket);
- if (socket) {
- socket.emit('conversation', {
- accountId,
- conversationId,
- });
+ if (!conversation || !webSocket) {
+ return;
}
- }, [accountId, conversation, conversationId, socket]);
+ console.log(`set conversation ${conversationId} ` + webSocket);
+ webSocket.send(WebSocketMessageType.ConversationView, { accountId, conversationId });
+ }, [accountId, conversation, conversationId, webSocket]);
if (isLoading) {
return <LoadingPage />;
diff --git a/client/src/contexts/Socket.tsx b/client/src/contexts/Socket.tsx
deleted file mode 100644
index e397a43..0000000
--- a/client/src/contexts/Socket.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2022 Savoir-faire Linux Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program. If not, see
- * <https://www.gnu.org/licenses/>.
- */
-import { createContext, PropsWithChildren } from 'react';
-import { Socket } from 'socket.io-client';
-
-type ISocketContext = Socket;
-export const SocketContext = createContext<ISocketContext | undefined>(undefined);
-
-type SocketProviderProps = PropsWithChildren<{
- socket: Socket;
-}>;
-export const SocketProvider = ({ socket, children }: SocketProviderProps) => (
- <SocketContext.Provider value={socket}>{children}</SocketContext.Provider>
-);
diff --git a/client/src/contexts/WebRTCProvider.tsx b/client/src/contexts/WebRTCProvider.tsx
index 86950f7..eb52e6b 100644
--- a/client/src/contexts/WebRTCProvider.tsx
+++ b/client/src/contexts/WebRTCProvider.tsx
@@ -16,7 +16,7 @@
* <https://www.gnu.org/licenses/>.
*/
-import { WebRTCIceCandidate, WebRTCSDP, WebSocketMessage, WebSocketMessageType } from 'jami-web-common';
+import { WebSocketMessageType } from 'jami-web-common';
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
import { WithChildren } from '../utils/utils';
@@ -73,7 +73,7 @@
const contactId = _contactId;
const [webRTCConnection, setWebRTCConnection] = useState<RTCPeerConnection | undefined>();
const localStreamRef = useRef<MediaStream>();
- const socket = useContext(WebSocketContext);
+ const webSocket = useContext(WebSocketContext);
useEffect(() => {
if (!webRTCConnection) {
@@ -118,16 +118,13 @@
}
const icecandidateEventListener = (event: RTCPeerConnectionIceEvent) => {
- if (event.candidate && socket) {
+ if (event.candidate && webSocket) {
console.log('webRTCConnection : onicecandidate');
- socket.send({
- type: WebSocketMessageType.IceCandidate,
- data: {
- from: account.getId(),
- to: contactId,
- message: {
- candidate: event.candidate,
- },
+ webSocket.send(WebSocketMessageType.IceCandidate, {
+ from: account.getId(),
+ to: contactId,
+ message: {
+ candidate: event.candidate,
},
});
}
@@ -148,52 +145,41 @@
webRTCConnection.removeEventListener('icecandidate', icecandidateEventListener);
webRTCConnection.removeEventListener('track', trackEventListener);
};
- }, [webRTCConnection, isVideoOn, isAudioOn, socket, contactId, account]);
+ }, [webRTCConnection, isVideoOn, isAudioOn, webSocket, contactId, account]);
useEffect(() => {
- if (!webRTCConnection || !socket) {
+ if (!webRTCConnection || !webSocket) {
return;
}
- const sendWebRTCAnswer = async (message: WebSocketMessage) => {
- if (webRTCConnection && socket) {
- const remoteSdp: RTCSessionDescriptionInit = (message.data.message as WebRTCSDP).sdp;
- await webRTCConnection.setRemoteDescription(new RTCSessionDescription(remoteSdp));
+ webSocket.bind(WebSocketMessageType.WebRTCOffer, async (data) => {
+ if (webRTCConnection) {
+ await webRTCConnection.setRemoteDescription(new RTCSessionDescription(data.message.sdp));
const mySdp = await webRTCConnection.createAnswer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});
await webRTCConnection.setLocalDescription(new RTCSessionDescription(mySdp));
- socket.send({
- type: WebSocketMessageType.WebRTCAnswer,
- data: {
- from: account.getId(),
- to: contactId,
- message: {
- sdp: mySdp,
- },
+ webSocket.send(WebSocketMessageType.WebRTCAnswer, {
+ from: account.getId(),
+ to: contactId,
+ message: {
+ sdp: mySdp,
},
});
- console.log('get offer and aswering');
}
- };
+ });
- const handleWebRTCAnswer = async (message: WebSocketMessage) => {
- const remoteSdp: RTCSessionDescriptionInit = (message.data.message as WebRTCSDP).sdp;
- await webRTCConnection.setRemoteDescription(new RTCSessionDescription(remoteSdp));
+ webSocket.bind(WebSocketMessageType.WebRTCAnswer, async (data) => {
+ await webRTCConnection.setRemoteDescription(new RTCSessionDescription(data.message.sdp));
console.log('get answer');
- };
+ });
- const addIceCandidate = async (message: WebSocketMessage) => {
- const candidate: RTCIceCandidateInit = (message.data.message as WebRTCIceCandidate).candidate;
- await webRTCConnection.addIceCandidate(new RTCIceCandidate(candidate));
+ webSocket.bind(WebSocketMessageType.IceCandidate, async (data) => {
+ await webRTCConnection.addIceCandidate(new RTCIceCandidate(data.message.candidate));
console.log('webRTCConnection : candidate add success');
- };
-
- socket.bind(WebSocketMessageType.WebRTCOffer, sendWebRTCAnswer);
- socket.bind(WebSocketMessageType.WebRTCAnswer, handleWebRTCAnswer);
- socket.bind(WebSocketMessageType.IceCandidate, addIceCandidate);
- }, [account, contactId, socket, webRTCConnection]);
+ });
+ }, [account, contactId, webSocket, webRTCConnection]);
const setAudioStatus = useCallback((isOn: boolean) => {
setIsAudioOn(isOn);
@@ -210,24 +196,21 @@
}, []);
const sendWebRTCOffer = useCallback(async () => {
- if (webRTCConnection && socket) {
+ if (webRTCConnection && webSocket) {
const sdp = await webRTCConnection.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});
- socket.send({
- type: WebSocketMessageType.WebRTCOffer,
- data: {
- from: account.getId(),
- to: contactId,
- message: {
- sdp,
- },
+ webSocket.send(WebSocketMessageType.WebRTCOffer, {
+ from: account.getId(),
+ to: contactId,
+ message: {
+ sdp,
},
});
await webRTCConnection.setLocalDescription(new RTCSessionDescription(sdp));
}
- }, [account, contactId, socket, webRTCConnection]);
+ }, [account, contactId, webSocket, webRTCConnection]);
return (
<WebRTCContext.Provider
diff --git a/client/src/contexts/WebSocketProvider.tsx b/client/src/contexts/WebSocketProvider.tsx
index 86b794f..38317e2 100644
--- a/client/src/contexts/WebSocketProvider.tsx
+++ b/client/src/contexts/WebSocketProvider.tsx
@@ -15,18 +15,16 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-import { WebSocketMessage, WebSocketMessageType } from 'jami-web-common';
+import { WebSocketCallbacks, WebSocketMessage, WebSocketMessageTable, WebSocketMessageType } from 'jami-web-common';
import { createContext, useCallback, useEffect, useRef, useState } from 'react';
import { apiUrl } from '../utils/constants';
import { WithChildren } from '../utils/utils';
import { useAuthContext } from './AuthProvider';
-export type WebSocketMessageFn = (message: WebSocketMessage) => void;
-
-interface IWebSocketContext {
- bind: (type: WebSocketMessageType, callback: WebSocketMessageFn) => void;
- send: WebSocketMessageFn;
+export interface IWebSocketContext {
+ bind: <T extends WebSocketMessageType>(type: T, callback: (data: WebSocketMessageTable[T]) => void) => void;
+ send: <T extends WebSocketMessageType>(type: T, data: WebSocketMessageTable[T]) => void;
}
export const WebSocketContext = createContext<IWebSocketContext | undefined>(undefined);
@@ -34,71 +32,88 @@
export default ({ children }: WithChildren) => {
const [isConnected, setIsConnected] = useState(false);
const webSocketRef = useRef<WebSocket>();
- const callbacksRef = useRef(new Map<WebSocketMessageType, WebSocketMessageFn[]>());
+ const callbacksRef = useRef<WebSocketCallbacks>({
+ [WebSocketMessageType.ConversationMessage]: [],
+ [WebSocketMessageType.ConversationView]: [],
+ [WebSocketMessageType.WebRTCOffer]: [],
+ [WebSocketMessageType.WebRTCAnswer]: [],
+ [WebSocketMessageType.IceCandidate]: [],
+ });
const { token: accessToken } = useAuthContext();
- const bind = useCallback((type: WebSocketMessageType, messageCallback: WebSocketMessageFn) => {
- const messageCallbacks = callbacksRef.current.get(type);
- if (messageCallbacks) {
- messageCallbacks.push(messageCallback);
- } else {
- callbacksRef.current.set(type, [messageCallback]);
- }
- }, []);
+ const context: IWebSocketContext = {
+ bind: useCallback((type, callback) => {
+ callbacksRef.current[type].push(callback);
+ }, []),
+ send: useCallback(
+ (type, data) => {
+ if (isConnected) {
+ webSocketRef.current?.send(JSON.stringify({ type, data }));
+ }
+ },
+ [isConnected]
+ ),
+ };
- const send = useCallback(
- (message: WebSocketMessage) => {
- if (isConnected) {
- webSocketRef.current?.send(JSON.stringify(message));
- }
- },
- [isConnected]
- );
-
- const handleOnOpen = useCallback(() => setIsConnected(true), []);
-
- const handleOnClose = useCallback(() => {
- setIsConnected(false);
- callbacksRef.current.clear();
- }, []);
-
- const handleOnMessage = useCallback(({ data }: MessageEvent<string>) => {
- const message: WebSocketMessage = JSON.parse(data);
- const messageCallbacks = callbacksRef.current.get(message.type);
- if (messageCallbacks) {
- for (const messageCallback of messageCallbacks) {
- messageCallback(message);
- }
- } else {
- console.warn(`Unhandled message of type ${message.type}`);
- }
- }, []);
-
- const handleOnError = useCallback((event: Event) => {
- console.error('Closing WebSocket due to an error:', event);
- webSocketRef.current?.close();
- }, []);
-
- useEffect(() => {
+ const connect = useCallback(() => {
const url = new URL(apiUrl);
url.protocol = 'ws:';
url.searchParams.set('accessToken', accessToken);
const webSocket = new WebSocket(url);
- webSocket.onopen = handleOnOpen;
- webSocket.onclose = handleOnClose;
- webSocket.onmessage = handleOnMessage;
- webSocket.onerror = handleOnError;
+
+ webSocket.onopen = () => {
+ console.debug('WebSocket connected');
+ setIsConnected(true);
+ };
+
+ webSocket.onclose = () => {
+ console.debug('WebSocket disconnected');
+ setIsConnected(false);
+ for (const callbacks of Object.values(callbacksRef.current)) {
+ callbacks.length = 0;
+ }
+ setTimeout(connect, 1000);
+ };
+
+ webSocket.onmessage = <T extends WebSocketMessageType>({ data }: MessageEvent<string>) => {
+ console.debug('WebSocket received message', data);
+ const message: WebSocketMessage<T> = JSON.parse(data);
+ if (!message.type || !message.data) {
+ console.warn(`Incorrect format (require type and data) ${message}`);
+ return;
+ }
+ if (!Object.values(WebSocketMessageType).includes(message.type)) {
+ console.warn(`Unhandled message of type: ${message.type}`);
+ return;
+ }
+ const callbacks = callbacksRef.current[message.type];
+ for (const callback of callbacks) {
+ callback(message.data);
+ }
+ };
+
+ webSocket.onerror = (event: Event) => {
+ console.error('Closing WebSocket due to an error:', event);
+ webSocketRef.current?.close();
+ };
webSocketRef.current = webSocket;
- return () => webSocket.close();
- }, [accessToken, handleOnOpen, handleOnClose, handleOnMessage, handleOnError]);
+ return () => {
+ switch (webSocket.readyState) {
+ case webSocket.CONNECTING:
+ webSocket.onopen = () => webSocket.close();
+ break;
+ case webSocket.OPEN:
+ webSocket.close();
+ break;
+ }
+ };
+ }, [accessToken]);
- return isConnected ? (
- <WebSocketContext.Provider value={{ bind, send }}>{children}</WebSocketContext.Provider>
- ) : (
- <>{children}</>
- );
+ useEffect(connect, [connect]);
+
+ return <WebSocketContext.Provider value={isConnected ? context : undefined}>{children}</WebSocketContext.Provider>;
};
diff --git a/client/src/index.tsx b/client/src/index.tsx
index 7902c9b..5070685 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -25,9 +25,7 @@
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { RouterProvider } from 'react-router-dom';
-import socketio from 'socket.io-client';
-import { SocketProvider } from './contexts/Socket';
import { store } from './redux/store';
import { router } from './router';
import defaultTheme from './themes/Default';
@@ -40,8 +38,6 @@
},
});
-const socket = socketio();
-
const container = document.getElementById('root');
if (!container) {
throw new Error('Failed to get the root element');
@@ -51,11 +47,9 @@
<Provider store={store}>
<StrictMode>
<QueryClientProvider client={queryClient}>
- <SocketProvider socket={socket}>
- <ThemeProvider theme={defaultTheme}>
- <RouterProvider router={router} />
- </ThemeProvider>
- </SocketProvider>
+ <ThemeProvider theme={defaultTheme}>
+ <RouterProvider router={router} />
+ </ThemeProvider>
</QueryClientProvider>
</StrictMode>
</Provider>
diff --git a/client/src/pages/ChatInterface.tsx b/client/src/pages/ChatInterface.tsx
index b09f2fd..12659e9 100644
--- a/client/src/pages/ChatInterface.tsx
+++ b/client/src/pages/ChatInterface.tsx
@@ -16,7 +16,7 @@
* <https://www.gnu.org/licenses/>.
*/
import { Box, Divider, Stack } from '@mui/material';
-import { ConversationMember, Message } from 'jami-web-common';
+import { ConversationMember, Message, WebSocketMessageType } from 'jami-web-common';
import { useCallback, useContext, useEffect, useState } from 'react';
import { useDropzone } from 'react-dropzone';
@@ -24,7 +24,7 @@
import LoadingPage from '../components/Loading';
import MessageList from '../components/MessageList';
import SendMessageForm from '../components/SendMessageForm';
-import { SocketContext } from '../contexts/Socket';
+import { WebSocketContext } from '../contexts/WebSocketProvider';
import { useMessagesQuery, useSendMessageMutation } from '../services/Conversation';
import { FileHandler } from '../utils/files';
@@ -33,7 +33,7 @@
members: ConversationMember[];
};
const ChatInterface = ({ conversationId, members }: ChatInterfaceProps) => {
- const socket = useContext(SocketContext);
+ const webSocket = useContext(WebSocketContext);
const [messages, setMessages] = useState<Message[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(false);
@@ -87,14 +87,13 @@
const sendMessage = useCallback((message: string) => sendMessageMutation.mutate(message), [sendMessageMutation]);
useEffect(() => {
- if (socket) {
- socket.off('newMessage');
- socket.on('newMessage', (data) => {
+ if (webSocket) {
+ webSocket.bind(WebSocketMessageType.ConversationMessage, ({ message }) => {
console.log('newMessage');
- setMessages((messages) => addMessage(messages, data));
+ setMessages((messages) => addMessage(messages, message));
});
}
- }, [conversationId, socket]);
+ }, [conversationId, webSocket]);
if (isLoading) {
return <LoadingPage />;