Add wrapper comp to WebRtcProvider, CallProvider

In `CallProvider` and `WebRtcProvider`, add a wrapper component that show a loading page if certain variables aren't set (webSocket, webRtcConnection).
This makes it so that we do not need to check that they are undefined in the actual code.

In `WebRtcProvider`, put some listeners in their separate `useEffect` to avoid re-binding them unecessarily.

Change-Id: I25f0ea57455bf3eb7705b69793fe3f2949c52916
diff --git a/client/src/contexts/CallProvider.tsx b/client/src/contexts/CallProvider.tsx
index e602508..e99399d 100644
--- a/client/src/contexts/CallProvider.tsx
+++ b/client/src/contexts/CallProvider.tsx
@@ -19,13 +19,14 @@
 import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
 import { Navigate, useNavigate } from 'react-router-dom';
 
+import LoadingPage from '../components/Loading';
 import { useUrlParams } from '../hooks/useUrlParams';
 import { CallRouteParams } from '../router';
 import { callTimeoutMs } from '../utils/constants';
 import { SetState, WithChildren } from '../utils/utils';
 import { ConversationContext } from './ConversationProvider';
 import { WebRtcContext } from './WebRtcProvider';
-import { WebSocketContext } from './WebSocketProvider';
+import { IWebSocketContext, WebSocketContext } from './WebSocketProvider';
 
 export type CallRole = 'caller' | 'receiver';
 
@@ -87,12 +88,33 @@
 export const CallContext = createContext<ICallContext>(defaultCallContext);
 
 export default ({ children }: WithChildren) => {
+  const webSocket = useContext(WebSocketContext);
+  const { webRtcConnection } = useContext(WebRtcContext);
+
+  if (!webSocket || !webRtcConnection) {
+    return <LoadingPage />;
+  }
+
+  return (
+    <CallProvider webSocket={webSocket} webRtcConnection={webRtcConnection}>
+      {children}
+    </CallProvider>
+  );
+};
+
+const CallProvider = ({
+  children,
+  webSocket,
+  webRtcConnection,
+}: WithChildren & {
+  webSocket: IWebSocketContext;
+  webRtcConnection: RTCPeerConnection;
+}) => {
   const {
     queryParams: { role: callRole },
     state: routeState,
   } = useUrlParams<CallRouteParams>();
-  const webSocket = useContext(WebSocketContext);
-  const { webRtcConnection, remoteStreams, sendWebRtcOffer, isConnected } = useContext(WebRtcContext);
+  const { remoteStreams, sendWebRtcOffer, isConnected } = useContext(WebRtcContext);
   const { conversationId, conversation } = useContext(ConversationContext);
   const navigate = useNavigate();
 
@@ -153,7 +175,7 @@
   }, []);
 
   useEffect(() => {
-    if (localStream && webRtcConnection) {
+    if (localStream) {
       for (const track of localStream.getTracks()) {
         webRtcConnection.addTrack(track, localStream);
       }
@@ -191,10 +213,6 @@
   );
 
   useEffect(() => {
-    if (!webSocket) {
-      return;
-    }
-
     if (callRole === 'caller' && callStatus === CallStatus.Default) {
       const callBegin: CallBegin = {
         contactId: contactUri,
@@ -221,10 +239,6 @@
   }, []);
 
   useEffect(() => {
-    if (!webSocket || !webRtcConnection) {
-      return;
-    }
-
     if (callRole === 'caller' && callStatus === CallStatus.Ringing) {
       const callAcceptListener = (data: CallAction) => {
         console.info('Received event on CallAccept', data);
@@ -254,10 +268,6 @@
   }, [callRole, webSocket, webRtcConnection, sendWebRtcOffer, callStatus, conversationId]);
 
   const quitCall = useCallback(() => {
-    if (!webRtcConnection) {
-      throw new Error('Could not quit call: webRtcConnection is not defined');
-    }
-
     const localTracks = localStream?.getTracks();
     if (localTracks) {
       for (const track of localTracks) {
@@ -270,10 +280,6 @@
   }, [webRtcConnection, localStream, navigate, conversationId]);
 
   useEffect(() => {
-    if (!webSocket) {
-      return;
-    }
-
     const callEndListener = (data: CallAction) => {
       console.info('Received event on CallEnd', data);
       if (data.conversationId !== conversationId) {
@@ -302,10 +308,6 @@
 
   const acceptCall = useCallback(
     (withVideoOn: boolean) => {
-      if (!webSocket) {
-        throw new Error('Could not accept call');
-      }
-
       const callAccept: CallAction = {
         contactId: contactUri,
         conversationId,
@@ -320,10 +322,6 @@
   );
 
   const endCall = useCallback(() => {
-    if (!webSocket) {
-      throw new Error('Could not end call');
-    }
-
     const callEnd: CallAction = {
       contactId: contactUri,
       conversationId,
diff --git a/client/src/contexts/WebRtcProvider.tsx b/client/src/contexts/WebRtcProvider.tsx
index 13a30a2..397bd83 100644
--- a/client/src/contexts/WebRtcProvider.tsx
+++ b/client/src/contexts/WebRtcProvider.tsx
@@ -19,10 +19,11 @@
 import { WebRtcIceCandidate, WebRtcSdp, WebSocketMessageType } from 'jami-web-common';
 import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
 
+import LoadingPage from '../components/Loading';
 import { WithChildren } from '../utils/utils';
 import { useAuthContext } from './AuthProvider';
 import { ConversationContext } from './ConversationProvider';
-import { WebSocketContext } from './WebSocketProvider';
+import { IWebSocketContext, WebSocketContext } from './WebSocketProvider';
 
 interface IWebRtcContext {
   isConnected: boolean;
@@ -44,14 +45,8 @@
 
 export default ({ children }: WithChildren) => {
   const { account } = useAuthContext();
-  const webSocket = useContext(WebSocketContext);
-  const { conversation, conversationId } = useContext(ConversationContext);
   const [webRtcConnection, setWebRtcConnection] = useState<RTCPeerConnection | undefined>();
-  const [remoteStreams, setRemoteStreams] = useState<readonly MediaStream[]>();
-  const [isConnected, setIsConnected] = useState(false);
-
-  // TODO: This logic will have to change to support multiple people in a call
-  const contactUri = useMemo(() => conversation.getFirstMember().contact.getUri(), [conversation]);
+  const webSocket = useContext(WebSocketContext);
 
   useEffect(() => {
     if (!webRtcConnection && account) {
@@ -75,12 +70,34 @@
     }
   }, [account, webRtcConnection]);
 
+  if (!webRtcConnection || !webSocket) {
+    return <LoadingPage />;
+  }
+
+  return (
+    <WebRtcProvider webRtcConnection={webRtcConnection} webSocket={webSocket}>
+      {children}
+    </WebRtcProvider>
+  );
+};
+
+const WebRtcProvider = ({
+  children,
+  webRtcConnection,
+  webSocket,
+}: WithChildren & {
+  webRtcConnection: RTCPeerConnection;
+  webSocket: IWebSocketContext;
+}) => {
+  const { conversation, conversationId } = useContext(ConversationContext);
+  const [remoteStreams, setRemoteStreams] = useState<readonly MediaStream[]>();
+  const [isConnected, setIsConnected] = useState(false);
+
+  // TODO: This logic will have to change to support multiple people in a call
+  const contactUri = useMemo(() => conversation.getFirstMember().contact.getUri(), [conversation]);
+
   const sendWebRtcOffer = useCallback(
     async (sdp: RTCSessionDescriptionInit) => {
-      if (!webRtcConnection || !webSocket) {
-        throw new Error('Could not send WebRTC offer');
-      }
-
       const webRtcOffer: WebRtcSdp = {
         contactId: contactUri,
         conversationId: conversationId,
@@ -96,10 +113,6 @@
 
   const sendWebRtcAnswer = useCallback(
     (sdp: RTCSessionDescriptionInit) => {
-      if (!webRtcConnection || !webSocket) {
-        throw new Error('Could not send WebRTC answer');
-      }
-
       const webRtcAnswer: WebRtcSdp = {
         contactId: contactUri,
         conversationId: conversationId,
@@ -109,14 +122,12 @@
       console.info('Sending WebRtcAnswer', webRtcAnswer);
       webSocket.send(WebSocketMessageType.WebRtcAnswer, webRtcAnswer);
     },
-    [contactUri, conversationId, webRtcConnection, webSocket]
+    [contactUri, conversationId, webSocket]
   );
 
-  useEffect(() => {
-    if (!webSocket || !webRtcConnection) {
-      return;
-    }
+  /* WebSocket Listeners */
 
+  useEffect(() => {
     const webRtcOfferListener = async (data: WebRtcSdp) => {
       console.info('Received event on WebRtcOffer', data);
       if (data.conversationId !== conversationId) {
@@ -143,9 +154,17 @@
 
       await webRtcConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
     };
+    webSocket.bind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
+    webSocket.bind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
 
+    return () => {
+      webSocket.unbind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
+      webSocket.unbind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
+    };
+  }, [webSocket, webRtcConnection, sendWebRtcAnswer, conversationId]);
+
+  useEffect(() => {
     const webRtcIceCandidateListener = async (data: WebRtcIceCandidate) => {
-      console.info('Received event on WebRtcIceCandidate', data);
       if (data.conversationId !== conversationId) {
         console.warn('Wrong incoming conversationId, ignoring action');
         return;
@@ -154,28 +173,17 @@
       await webRtcConnection.addIceCandidate(data.candidate);
     };
 
-    webSocket.bind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
-    webSocket.bind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
     webSocket.bind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
 
     return () => {
-      webSocket.unbind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
-      webSocket.unbind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
       webSocket.unbind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
     };
-  }, [webSocket, webRtcConnection, sendWebRtcAnswer, conversationId]);
+  }, [webRtcConnection, webSocket, conversationId]);
+
+  /* WebRTC Listeners */
 
   useEffect(() => {
-    if (!webRtcConnection || !webSocket) {
-      return;
-    }
-
     const iceCandidateEventListener = (event: RTCPeerConnectionIceEvent) => {
-      console.info('Received WebRTC event on icecandidate', event);
-      if (!contactUri) {
-        throw new Error('Could not handle WebRTC event on icecandidate: contactUri is not defined');
-      }
-
       if (event.candidate) {
         const webRtcIceCandidate: WebRtcIceCandidate = {
           contactId: contactUri,
@@ -183,32 +191,38 @@
           candidate: event.candidate,
         };
 
-        console.info('Sending WebRtcIceCandidate', webRtcIceCandidate);
         webSocket.send(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidate);
       }
     };
 
+    webRtcConnection.addEventListener('icecandidate', iceCandidateEventListener);
+
+    return () => {
+      webRtcConnection.removeEventListener('icecandidate', iceCandidateEventListener);
+    };
+  }, [webRtcConnection, webSocket, contactUri, conversationId]);
+
+  useEffect(() => {
     const trackEventListener = (event: RTCTrackEvent) => {
       console.info('Received WebRTC event on track', event);
       setRemoteStreams(event.streams);
     };
 
-    const iceConnectionStateChangeEventListener = () => {
+    const iceConnectionStateChangeEventListener = (event: Event) => {
+      console.info(`Received WebRTC event on iceconnectionstatechange: ${webRtcConnection.iceConnectionState}`, event);
       setIsConnected(
         webRtcConnection.iceConnectionState === 'connected' || webRtcConnection.iceConnectionState === 'completed'
       );
     };
 
-    webRtcConnection.addEventListener('icecandidate', iceCandidateEventListener);
     webRtcConnection.addEventListener('track', trackEventListener);
     webRtcConnection.addEventListener('iceconnectionstatechange', iceConnectionStateChangeEventListener);
 
     return () => {
-      webRtcConnection.removeEventListener('icecandidate', iceCandidateEventListener);
       webRtcConnection.removeEventListener('track', trackEventListener);
       webRtcConnection.removeEventListener('iceconnectionstatechange', iceConnectionStateChangeEventListener);
     };
-  }, [webRtcConnection, webSocket, contactUri, conversationId]);
+  }, [webRtcConnection]);
 
   return (
     <WebRtcContext.Provider