Add unbind function to WebSocketContext

Change WebSocketCallbacks to have sets of callbacks instead of arrays.

Change-Id: I48439d0ff9b8188f47e0530d7d91f78c6fae8868
diff --git a/client/src/contexts/WebRTCProvider.tsx b/client/src/contexts/WebRTCProvider.tsx
index 7dc1611..4801387 100644
--- a/client/src/contexts/WebRTCProvider.tsx
+++ b/client/src/contexts/WebRTCProvider.tsx
@@ -16,7 +16,13 @@
  * <https://www.gnu.org/licenses/>.
  */
 
-import { WebSocketMessageType } from 'jami-web-common';
+import {
+  AccountTextMessage,
+  WebRTCAnswerMessage,
+  WebRTCIceCandidate,
+  WebRTCOfferMessage,
+  WebSocketMessageType,
+} from 'jami-web-common';
 import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
 
 import { WithChildren } from '../utils/utils';
@@ -178,7 +184,7 @@
       return;
     }
 
-    webSocket.bind(WebSocketMessageType.WebRTCOffer, async (data) => {
+    const webRTCOfferListener = async (data: AccountTextMessage<WebRTCOfferMessage>) => {
       if (webRTCConnection) {
         await webRTCConnection.setRemoteDescription(new RTCSessionDescription(data.message.sdp));
         const mySdp = await webRTCConnection.createAnswer({
@@ -194,17 +200,27 @@
           },
         });
       }
-    });
+    };
 
-    webSocket.bind(WebSocketMessageType.WebRTCAnswer, async (data) => {
+    const webRTCAnswerListener = async (data: AccountTextMessage<WebRTCAnswerMessage>) => {
       await webRTCConnection.setRemoteDescription(new RTCSessionDescription(data.message.sdp));
       console.log('get answer');
-    });
+    };
 
-    webSocket.bind(WebSocketMessageType.IceCandidate, async (data) => {
+    const iceCandidateListener = async (data: AccountTextMessage<WebRTCIceCandidate>) => {
       await webRTCConnection.addIceCandidate(new RTCIceCandidate(data.message.candidate));
       console.log('webRTCConnection : candidate add success');
-    });
+    };
+
+    webSocket.bind(WebSocketMessageType.WebRTCOffer, webRTCOfferListener);
+    webSocket.bind(WebSocketMessageType.WebRTCAnswer, webRTCAnswerListener);
+    webSocket.bind(WebSocketMessageType.IceCandidate, iceCandidateListener);
+
+    return () => {
+      webSocket.unbind(WebSocketMessageType.WebRTCOffer, webRTCOfferListener);
+      webSocket.unbind(WebSocketMessageType.WebRTCAnswer, webRTCAnswerListener);
+      webSocket.unbind(WebSocketMessageType.IceCandidate, iceCandidateListener);
+    };
   }, [account, contactId, webSocket, webRTCConnection]);
 
   const setAudioStatus = useCallback((isOn: boolean) => {
diff --git a/client/src/contexts/WebSocketProvider.tsx b/client/src/contexts/WebSocketProvider.tsx
index e6e246b..b90f609 100644
--- a/client/src/contexts/WebSocketProvider.tsx
+++ b/client/src/contexts/WebSocketProvider.tsx
@@ -15,16 +15,29 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { WebSocketCallbacks, WebSocketMessage, WebSocketMessageTable, WebSocketMessageType } from 'jami-web-common';
+import {
+  buildWebSocketCallbacks,
+  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';
 
+type BindFunction = <T extends WebSocketMessageType>(
+  type: T,
+  callback: (data: WebSocketMessageTable[T]) => void
+) => void;
+type SendFunction = <T extends WebSocketMessageType>(type: T, data: WebSocketMessageTable[T]) => void;
+
 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;
+  bind: BindFunction;
+  unbind: BindFunction;
+  send: SendFunction;
 }
 
 export const WebSocketContext = createContext<IWebSocketContext | undefined>(undefined);
@@ -32,29 +45,28 @@
 export default ({ children }: WithChildren) => {
   const [isConnected, setIsConnected] = useState(false);
   const webSocketRef = useRef<WebSocket>();
-  const callbacksRef = useRef<WebSocketCallbacks>({
-    [WebSocketMessageType.ConversationMessage]: [],
-    [WebSocketMessageType.ConversationView]: [],
-    [WebSocketMessageType.WebRTCOffer]: [],
-    [WebSocketMessageType.WebRTCAnswer]: [],
-    [WebSocketMessageType.IceCandidate]: [],
-  });
+  const callbacksRef = useRef<WebSocketCallbacks>(buildWebSocketCallbacks());
 
   const { token: accessToken } = useAuthContext();
 
-  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 bind: BindFunction = useCallback((type, callback) => {
+    const callbacks = callbacksRef.current[type];
+    callbacks.add(callback);
+  }, []);
+
+  const unbind: BindFunction = useCallback((type, callback) => {
+    const callbacks = callbacksRef.current[type];
+    callbacks.delete(callback);
+  }, []);
+
+  const send: SendFunction = useCallback(
+    (type, data) => {
+      if (isConnected) {
+        webSocketRef.current?.send(JSON.stringify({ type, data }));
+      }
+    },
+    [isConnected]
+  );
 
   const connect = useCallback(() => {
     const url = new URL(apiUrl);
@@ -72,7 +84,7 @@
       console.debug('WebSocket disconnected');
       setIsConnected(false);
       for (const callbacks of Object.values(callbacksRef.current)) {
-        callbacks.length = 0;
+        callbacks.clear();
       }
       setTimeout(connect, 1000);
     };
@@ -119,5 +131,19 @@
 
   useEffect(connect, [connect]);
 
-  return <WebSocketContext.Provider value={isConnected ? context : undefined}>{children}</WebSocketContext.Provider>;
+  return (
+    <WebSocketContext.Provider
+      value={
+        isConnected
+          ? {
+              bind,
+              unbind,
+              send,
+            }
+          : undefined
+      }
+    >
+      {children}
+    </WebSocketContext.Provider>
+  );
 };
diff --git a/client/src/pages/ChatInterface.tsx b/client/src/pages/ChatInterface.tsx
index 12659e9..f226fec 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, WebSocketMessageType } from 'jami-web-common';
+import { ConversationMember, ConversationMessage, Message, WebSocketMessageType } from 'jami-web-common';
 import { useCallback, useContext, useEffect, useState } from 'react';
 import { useDropzone } from 'react-dropzone';
 
@@ -88,12 +88,17 @@
 
   useEffect(() => {
     if (webSocket) {
-      webSocket.bind(WebSocketMessageType.ConversationMessage, ({ message }) => {
+      const conversationMessageListener = ({ message }: ConversationMessage) => {
         console.log('newMessage');
         setMessages((messages) => addMessage(messages, message));
-      });
+      };
+
+      webSocket.bind(WebSocketMessageType.ConversationMessage, conversationMessageListener);
+      return () => {
+        webSocket.unbind(WebSocketMessageType.ConversationMessage, conversationMessageListener);
+      };
     }
-  }, [conversationId, webSocket]);
+  }, [webSocket]);
 
   if (isLoading) {
     return <LoadingPage />;