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 />;
diff --git a/common/src/types/websocket-callbacks.ts b/common/src/types/websocket-callbacks.ts
index da372f9..e9f3e2a 100644
--- a/common/src/types/websocket-callbacks.ts
+++ b/common/src/types/websocket-callbacks.ts
@@ -19,6 +19,16 @@
 import { WebSocketMessageType } from '../enums/websocket-message-type.js';
 import { WebSocketMessageTable } from '../interfaces/websocket-message.js';
 
+export type WebSocketCallback<T extends WebSocketMessageType> = (data: WebSocketMessageTable[T]) => void;
 export type WebSocketCallbacks = {
-  [key in WebSocketMessageType]: ((data: WebSocketMessageTable[key]) => void)[];
+  [key in WebSocketMessageType]: Set<WebSocketCallback<key>>;
+};
+
+export const buildWebSocketCallbacks = (): WebSocketCallbacks => {
+  const webSocketCallback = {} as WebSocketCallbacks;
+  for (const type of Object.values(WebSocketMessageType)) {
+    webSocketCallback[type] = new Set<WebSocketCallback<typeof type>>();
+  }
+
+  return webSocketCallback;
 };
diff --git a/server/src/websocket/websocket-server.ts b/server/src/websocket/websocket-server.ts
index 6750129..ff28bb9 100644
--- a/server/src/websocket/websocket-server.ts
+++ b/server/src/websocket/websocket-server.ts
@@ -18,7 +18,14 @@
 import { IncomingMessage } from 'node:http';
 import { Duplex } from 'node:stream';
 
-import { WebSocketCallbacks, WebSocketMessage, WebSocketMessageTable, WebSocketMessageType } from 'jami-web-common';
+import {
+  buildWebSocketCallbacks,
+  WebSocketCallback,
+  WebSocketCallbacks,
+  WebSocketMessage,
+  WebSocketMessageTable,
+  WebSocketMessageType,
+} from 'jami-web-common';
 import log from 'loglevel';
 import { Service } from 'typedi';
 import { URL } from 'whatwg-url';
@@ -30,13 +37,7 @@
 export class WebSocketServer {
   private wss = new WebSocket.WebSocketServer({ noServer: true });
   private sockets = new Map<string, WebSocket.WebSocket[]>();
-  private callbacks: WebSocketCallbacks = {
-    [WebSocketMessageType.ConversationMessage]: [],
-    [WebSocketMessageType.ConversationView]: [],
-    [WebSocketMessageType.WebRTCOffer]: [],
-    [WebSocketMessageType.WebRTCAnswer]: [],
-    [WebSocketMessageType.IceCandidate]: [],
-  };
+  private callbacks: WebSocketCallbacks = buildWebSocketCallbacks();
 
   constructor() {
     this.wss.on('connection', (ws: WebSocket.WebSocket, _request: IncomingMessage, accountId: string) => {
@@ -108,8 +109,8 @@
     }
   }
 
-  bind<T extends WebSocketMessageType>(type: T, callback: (data: WebSocketMessageTable[T]) => void): void {
-    this.callbacks[type].push(callback);
+  bind<T extends WebSocketMessageType>(type: T, callback: WebSocketCallback<T>): void {
+    this.callbacks[type].add(callback);
   }
 
   send<T extends WebSocketMessageType>(accountId: string, type: T, data: WebSocketMessageTable[T]): boolean {