End call when connection is lost

- Automatically end call when WebRTC connection is lost
- Remove state when in call

GitLab: 182
Change-Id: I259eccda82db7304542bef863c429c9313b4145a
diff --git a/client/src/components/ConversationListItem.tsx b/client/src/components/ConversationListItem.tsx
index 03685c3..e9e97b8 100644
--- a/client/src/components/ConversationListItem.tsx
+++ b/client/src/components/ConversationListItem.tsx
@@ -141,7 +141,7 @@
         Icon: AudioCallIcon,
         onClick: () => {
           if (conversationId) {
-            startCall(conversationId);
+            startCall(conversationId, 'caller');
           }
         },
       },
@@ -150,7 +150,7 @@
         Icon: VideoCallIcon,
         onClick: () => {
           if (conversationId) {
-            startCall(conversationId, {
+            startCall(conversationId, 'caller', {
               isVideoOn: true,
             });
           }
diff --git a/client/src/components/ConversationView.tsx b/client/src/components/ConversationView.tsx
index 064efcf..146f049 100644
--- a/client/src/components/ConversationView.tsx
+++ b/client/src/components/ConversationView.tsx
@@ -83,10 +83,10 @@
         </Typography>
       </Stack>
       <Stack direction="row" spacing="20px">
-        <StartAudioCallButton onClick={() => startCall(conversationId)} />
+        <StartAudioCallButton onClick={() => startCall(conversationId, 'caller')} />
         <StartVideoCallButton
           onClick={() =>
-            startCall(conversationId, {
+            startCall(conversationId, 'caller', {
               isVideoOn: true,
             })
           }
diff --git a/client/src/contexts/CallProvider.tsx b/client/src/contexts/CallProvider.tsx
index e99399d..f54b75f 100644
--- a/client/src/contexts/CallProvider.tsx
+++ b/client/src/contexts/CallProvider.tsx
@@ -110,11 +110,8 @@
   webSocket: IWebSocketContext;
   webRtcConnection: RTCPeerConnection;
 }) => {
-  const {
-    queryParams: { role: callRole },
-    state: routeState,
-  } = useUrlParams<CallRouteParams>();
-  const { remoteStreams, sendWebRtcOffer, isConnected } = useContext(WebRtcContext);
+  const { state: routeState } = useUrlParams<CallRouteParams>();
+  const { remoteStreams, sendWebRtcOffer, iceConnectionState } = useContext(WebRtcContext);
   const { conversationId, conversation } = useContext(ConversationContext);
   const navigate = useNavigate();
 
@@ -128,6 +125,7 @@
   const [isChatShown, setIsChatShown] = useState(false);
   const [isFullscreen, setIsFullscreen] = useState(false);
   const [callStatus, setCallStatus] = useState(routeState?.callStatus);
+  const [callRole] = useState(routeState?.role);
   const [callStartTime, setCallStartTime] = useState<Date | undefined>(undefined);
 
   // TODO: This logic will have to change to support multiple people in a call. Could we move this logic to the server?
@@ -298,13 +296,16 @@
   }, [webSocket, navigate, conversationId, quitCall]);
 
   useEffect(() => {
-    if (callStatus === CallStatus.Connecting && isConnected) {
+    if (
+      callStatus === CallStatus.Connecting &&
+      (iceConnectionState === 'connected' || iceConnectionState === 'completed')
+    ) {
       console.info('Changing call status to InCall');
       setCallStatus(CallStatus.InCall);
       setVideoStatus(isVideoOn);
       setCallStartTime(new Date());
     }
-  }, [isConnected, callStatus, setVideoStatus, isVideoOn]);
+  }, [iceConnectionState, callStatus, setVideoStatus, isVideoOn]);
 
   const acceptCall = useCallback(
     (withVideoOn: boolean) => {
@@ -334,6 +335,13 @@
   }, [webSocket, contactUri, conversationId, quitCall]);
 
   useEffect(() => {
+    if (iceConnectionState === 'disconnected') {
+      console.info('ICE connection disconnected');
+      endCall();
+    }
+  }, [iceConnectionState, callStatus, setVideoStatus, isVideoOn, endCall]);
+
+  useEffect(() => {
     const checkStatusTimeout = () => {
       if (callStatus !== CallStatus.InCall) {
         endCall();
@@ -346,6 +354,13 @@
     };
   }, [callStatus, endCall]);
 
+  useEffect(() => {
+    navigate('.', {
+      replace: true,
+      state: {},
+    });
+  }, [navigate]);
+
   if (!callRole || callStatus === undefined) {
     console.error('Invalid route. Redirecting...');
     return <Navigate to={'/'} />;
diff --git a/client/src/contexts/WebRtcProvider.tsx b/client/src/contexts/WebRtcProvider.tsx
index 397bd83..63e2ca2 100644
--- a/client/src/contexts/WebRtcProvider.tsx
+++ b/client/src/contexts/WebRtcProvider.tsx
@@ -26,7 +26,7 @@
 import { IWebSocketContext, WebSocketContext } from './WebSocketProvider';
 
 interface IWebRtcContext {
-  isConnected: boolean;
+  iceConnectionState: RTCIceConnectionState | undefined;
 
   remoteStreams: readonly MediaStream[] | undefined;
   webRtcConnection: RTCPeerConnection | undefined;
@@ -35,7 +35,7 @@
 }
 
 const defaultWebRtcContext: IWebRtcContext = {
-  isConnected: false,
+  iceConnectionState: undefined,
   remoteStreams: undefined,
   webRtcConnection: undefined,
   sendWebRtcOffer: async () => {},
@@ -91,7 +91,7 @@
 }) => {
   const { conversation, conversationId } = useContext(ConversationContext);
   const [remoteStreams, setRemoteStreams] = useState<readonly MediaStream[]>();
-  const [isConnected, setIsConnected] = useState(false);
+  const [iceConnectionState, setIceConnectionState] = useState<RTCIceConnectionState | undefined>();
 
   // TODO: This logic will have to change to support multiple people in a call
   const contactUri = useMemo(() => conversation.getFirstMember().contact.getUri(), [conversation]);
@@ -208,11 +208,9 @@
       setRemoteStreams(event.streams);
     };
 
-    const iceConnectionStateChangeEventListener = (event: Event) => {
-      console.info(`Received WebRTC event on iceconnectionstatechange: ${webRtcConnection.iceConnectionState}`, event);
-      setIsConnected(
-        webRtcConnection.iceConnectionState === 'connected' || webRtcConnection.iceConnectionState === 'completed'
-      );
+    const iceConnectionStateChangeEventListener = () => {
+      console.info('ICE connection state changed:', webRtcConnection.iceConnectionState);
+      setIceConnectionState(webRtcConnection.iceConnectionState);
     };
 
     webRtcConnection.addEventListener('track', trackEventListener);
@@ -227,7 +225,7 @@
   return (
     <WebRtcContext.Provider
       value={{
-        isConnected,
+        iceConnectionState,
         remoteStreams,
         webRtcConnection,
         sendWebRtcOffer,
diff --git a/client/src/hooks/useStartCall.ts b/client/src/hooks/useStartCall.ts
index 2df6a01..518c2a8 100644
--- a/client/src/hooks/useStartCall.ts
+++ b/client/src/hooks/useStartCall.ts
@@ -18,17 +18,18 @@
 import { useCallback } from 'react';
 import { useNavigate } from 'react-router-dom';
 
-import { CallStatus } from '../contexts/CallProvider';
+import { CallRole, CallStatus } from '../contexts/CallProvider';
 import { CallRouteParams } from '../router';
 
 export const useStartCall = () => {
   const navigate = useNavigate();
 
   return useCallback(
-    (conversationId: string, state?: Partial<CallRouteParams['state']>) => {
-      navigate(`/conversation/${conversationId}/call?role=caller`, {
+    (conversationId: string, role: CallRole = 'caller', state?: Partial<CallRouteParams['state']>) => {
+      navigate(`/conversation/${conversationId}/call`, {
         state: {
           callStatus: CallStatus.Default,
+          role,
           ...state,
         },
       });
diff --git a/client/src/managers/NotificationManager.tsx b/client/src/managers/NotificationManager.tsx
index 5362f54..c515320 100644
--- a/client/src/managers/NotificationManager.tsx
+++ b/client/src/managers/NotificationManager.tsx
@@ -39,8 +39,9 @@
 
     const callBeginListener = (data: CallBegin) => {
       console.info('Received event on CallBegin', data);
-      navigate(`/conversation/${data.conversationId}/call?role=receiver`, {
+      navigate(`/conversation/${data.conversationId}/call`, {
         state: {
+          role: 'receiver',
           callStatus: CallStatus.Ringing,
           isVideoOn: data.withVideoOn,
         },
diff --git a/client/src/router.tsx b/client/src/router.tsx
index 714adde..7bbddd4 100644
--- a/client/src/router.tsx
+++ b/client/src/router.tsx
@@ -41,8 +41,9 @@
 
 export type CallRouteParams = RouteParams<
   { conversationId?: string },
-  { role?: CallRole },
+  Record<string, never>,
   {
+    role: CallRole;
     isVideoOn?: boolean;
     callStatus: CallStatus;
   }