Send received message through WebSocket

- send received message from through WebSocket
- remove SocketIO from client

GitLab: #96
Change-Id: I7a8eec04010f0773428f914792c13decef393ebf
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>;
 };