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/CallPending.tsx b/client/src/pages/CallPending.tsx
index 23cbd50..fde276e 100644
--- a/client/src/pages/CallPending.tsx
+++ b/client/src/pages/CallPending.tsx
@@ -19,6 +19,7 @@
 import { Box, CircularProgress, Grid, IconButtonProps, Stack, Typography } from '@mui/material';
 import { ComponentType, ReactNode, useContext, useMemo } from 'react';
 import { useTranslation } from 'react-i18next';
+import { useLocation } from 'react-router-dom';
 
 import {
   CallingAnswerAudioButton,
@@ -27,21 +28,12 @@
   CallingRefuseButton,
 } from '../components/CallButtons';
 import ConversationAvatar from '../components/ConversationAvatar';
+import { CallContext, CallStatus } from '../contexts/CallProvider';
 import { ConversationContext } from '../contexts/ConversationProvider';
 
-export type CallPendingProps = {
-  pending: PendingStatus;
-  caller?: CallerStatus;
-  medium?: CommunicationMedium;
-};
-
-type PendingStatus = 'caller' | 'receiver';
-type CallerStatus = 'calling' | 'connecting';
-type CommunicationMedium = 'audio' | 'video';
-
-export const CallPending = (props: CallPendingProps) => {
+export const CallPending = () => {
   const { conversation } = useContext(ConversationContext);
-
+  const { callRole } = useContext(CallContext);
   return (
     <Stack
       direction="column"
@@ -91,11 +83,7 @@
           />
         </Box>
       </Box>
-      {props.pending === 'caller' ? (
-        <CallPendingCallerInterface {...props} />
-      ) : (
-        <CallPendingReceiverInterface {...props} />
-      )}
+      {callRole === 'caller' ? <CallPendingCallerInterface /> : <CallPendingReceiverInterface />}
     </Stack>
   );
 };
@@ -133,20 +121,28 @@
   );
 };
 
-export const CallPendingCallerInterface = ({ caller }: CallPendingProps) => {
+export const CallPendingCallerInterface = () => {
+  const { callStatus } = useContext(CallContext);
   const { t } = useTranslation();
   const { conversation } = useContext(ConversationContext);
   const memberName = useMemo(() => conversation.getFirstMember().contact.getRegisteredName(), [conversation]);
 
+  let title = t('loading');
+
+  switch (callStatus) {
+    case CallStatus.Ringing:
+      title = t('calling', {
+        member0: memberName,
+      });
+      break;
+    case CallStatus.Connecting:
+      title = t('connecting');
+      break;
+  }
+
   return (
     <CallPendingDetails
-      title={
-        caller === 'calling'
-          ? t('calling', {
-              member0: memberName,
-            })
-          : t('connecting')
-      }
+      title={title}
       buttons={[
         {
           ButtonComponent: CallingCancelButton,
@@ -157,21 +153,31 @@
   );
 };
 
-export const CallPendingReceiverInterface = ({ medium, caller }: CallPendingProps) => {
+export const CallPendingReceiverInterface = () => {
+  const { state } = useLocation();
+  const { callStatus } = useContext(CallContext);
+
   const { t } = useTranslation();
   const { conversation } = useContext(ConversationContext);
   const memberName = useMemo(() => conversation.getFirstMember().contact.getRegisteredName(), [conversation]);
 
+  let title = t('loading');
+
+  switch (callStatus) {
+    case CallStatus.Ringing:
+      title = t('incoming_call', {
+        context: state?.isVideoOn ? 'video' : 'audio',
+        member0: memberName,
+      });
+      break;
+    case CallStatus.Connecting:
+      title = t('connecting');
+      break;
+  }
+
   return (
     <CallPendingDetails
-      title={
-        caller === 'connecting'
-          ? t('connecting')
-          : t('incoming_call', {
-              context: medium,
-              member0: memberName,
-            })
-      }
+      title={title}
       buttons={[
         {
           ButtonComponent: CallingRefuseButton,