Improve permission handling in call flow

Improve permission handling by asking the user to give mic and camera permissions before sending `CallBegin` or `CallAccept` for the caller and receiver respectively.
Followed the flow described here: https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Connectivity#session_descriptions

CallProvider:

- Change functions order to place listeners under the function that sends the corresponding WebSocket message.
- Replace `Default` CallStatus with `Loading` for when asking user permissions before sending the `CallBegin`/`CallAccept` message.
- Remove `localStream` and `remoteStream` from `CallContext`. They are now available only in `WebRtcContext`.
- Replace `setAudioStatus` and `setVideoStatus` with `setIsAudioOn` and `setIsVideoOn`. A `useEffect` is now used to disable the tracks when the audio/video status changes.

WebRtcProvider:

- Move WebRTC connection close logic to WebRtcProvider
- Remove `webRtcConnection` from `WebRtcContext`. `WebRtcProvider` is now in charge of setting everything related to the WebRTC Connection.

UI:

- Add `CallPermissionDenied` page for when permissions are denied.
- Rework `CallPending` to display `Loading...` when waiting for user permissions

Change-Id: I48153577cca4c73cdb9b81d2fa78cfdfe2e06d69
diff --git a/client/src/pages/CallInterface.tsx b/client/src/pages/CallInterface.tsx
index 4daa1cd..beef803 100644
--- a/client/src/pages/CallInterface.tsx
+++ b/client/src/pages/CallInterface.tsx
@@ -29,7 +29,6 @@
   useState,
 } from 'react';
 import Draggable from 'react-draggable';
-import { useLocation } from 'react-router-dom';
 
 import { ExpandableButtonProps } from '../components/Button';
 import {
@@ -48,12 +47,12 @@
 import CallChatDrawer from '../components/CallChatDrawer';
 import { CallContext, CallStatus } from '../contexts/CallProvider';
 import { ConversationContext } from '../contexts/ConversationProvider';
+import { WebRtcContext } from '../contexts/WebRtcProvider';
 import { CallPending } from './CallPending';
 
 export default () => {
-  const { callRole, callStatus, isChatShown, isFullscreen } = useContext(CallContext);
+  const { callStatus, isChatShown, isFullscreen } = useContext(CallContext);
   const callInterfaceRef = useRef<HTMLDivElement>();
-  const { state } = useLocation();
 
   useEffect(() => {
     if (!callInterfaceRef.current) {
@@ -68,13 +67,7 @@
   }, [isFullscreen]);
 
   if (callStatus !== CallStatus.InCall) {
-    return (
-      <CallPending
-        pending={callRole}
-        caller={callStatus === CallStatus.Connecting ? 'connecting' : 'calling'}
-        medium={state?.isVideoOn ? 'video' : 'audio'}
-      />
-    );
+    return <CallPending />;
   }
 
   return (
@@ -90,7 +83,8 @@
 }
 
 const CallInterface = () => {
-  const { isVideoOn, localStream, remoteStream } = useContext(CallContext);
+  const { localStream, remoteStreams } = useContext(WebRtcContext);
+  const { isVideoOn } = useContext(CallContext);
   const gridItemRef = useRef(null);
   const remoteVideoRef = useRef<HTMLVideoElement | null>(null);
   const localVideoRef = useRef<HTMLVideoElement | null>(null);
@@ -102,10 +96,13 @@
   }, [localStream]);
 
   useEffect(() => {
+    // TODO: For now, `remoteStream` is the first remote stream in the array.
+    //       There should only be one in the array, but we should make sure this is right.
+    const remoteStream = remoteStreams?.at(0);
     if (remoteStream && remoteVideoRef.current) {
       remoteVideoRef.current.srcObject = remoteStream;
     }
-  }, [remoteStream]);
+  }, [remoteStreams]);
 
   return (
     <Box display="flex" flexGrow={1}>