Implement proper WebRTC call management

This is a big CR that implements the whole proper call logic.

-- Other Contributors --

This CR was created in pair programming with:

-  Charlie Duquette <charlie_duquette@hotmail.fr>

-- New files --

- `ConversationProvider`: Provides the conversation object to its children. Also contains the function to begin a call (first step in the flow) which sends the `BeginCall` event.
- `CallProvider`: Contains the call logic that was previously in WebRTCProvider. From now on, WebRTCProvider contains only the WebRTC logic, while CallProvider
  contains everything related to the call (get the media devices, start/accept calls...)
- `NotificationManager`: Wrapper component to bind the WebSocket CallBegin event listener. That listener will fire when a `BeginCall` event is received and will then redirect the user to the call receiving page.

-- New routes --

When a `conversationId` is included in the URL, all pages are inside a `<ConversationProvider />`.

When starting a call, the caller is redirected to:

> http://localhost:3000/conversation/:conversationId/call?role=caller

When receiving a call, the receiver is redirected to:

> http://localhost:3000/conversation/:conversationId/call?role=receiver&hostId={HOST_ID}

When the user is in a `.../call` route, the `WebRTCContext` and
`CallContext` are provided

The `hostId` is the account URI of the caller. It's used when the receiver wants to send their answer back to the caller.

```
/
|-- login: <Welcome />
|-- settings: <AccountSettings />
|-- ...
`-- conversation: <Messenger />
    |-- add-contact/:contactId
    `-- :conversationId: <ConversationProvider />
        |-- /: <ConversationView />
        `-- /call: <WebRTCProvider>
                     <CallProvider>
                       <CallInterface />
                     </CallProvider>
                    </WebRTCProvider>
```

-- Call flow --

1. Caller:

- Clicks "Call" button
- Sends `BeginCall` event
- Redirects to call page `/call?role=caller`
- Sets `callStatus` to "Ringing"

2. Receiver:

- Receieves `BeginCall` event
- The callback in `NotificationManager` is called
- Redirects to the call receiving page `/conversation/{CONVERSATION_ID}/call?role=receiver`

3. Receiver:

- Clicks the "Answer call" button
- Sends a `CallAccept` event
- Sets `callStatus` to "Connecting"

4. Caller:

- Receives `CallAccept` event
- The callback in `CallProvider` is called
- Sets `callStatus` to "Connecting"
- Creates WebRTC Offer
- Sends `WebRTCOffer` event containing the offer SDP

5. Receiver:

- Receives `WebRTCOffer` event
- The callback in `WebRTCProvider` is called
- Sets WebRTC remote description.
- WebRTC `icecandidate` event fired. Sends `IceCandidate` WebSocket event
- Creates WebRTC answer
- Sends `WebRTCAnswer` event
- Sets WebRTC local description
- Sets connected status to true. Call page now shows the call interface

6. Caller:

- Receives `WebRTCAnswer` event
- Sets WebRTC local description
- Sets WebRTC remote description
- WebRTC `icecandidate` event fired. Sends `IceCandidate` WebSocket event
- Sets connected status to true. Call page now shows the call interface

-- Misc Changes --

- Improve CallPending and CallInterface UI
- Move `useUrlParams` hook from the (now deleted) `client/src/utils/hooks.ts` file to `client/src/hooks/useUrlParams.ts`
- Disable StrictMode. This was causing issues, because some event would be sent twice. There is a TODO comment to fix the problem and reenable it.
- Improvements in server `webrtc-handler.ts`. This is still a WIP
- Rename dash-case client files to camelCase

GitLab: #70
Change-Id: I6c75f6b867e8acb9ccaaa118b0123bba30431f78
diff --git a/client/src/components/CallButtons.tsx b/client/src/components/CallButtons.tsx
index 594a1cc..7ccdd91 100644
--- a/client/src/components/CallButtons.tsx
+++ b/client/src/components/CallButtons.tsx
@@ -16,12 +16,14 @@
  * <https://www.gnu.org/licenses/>.
  */
 
-import { styled } from '@mui/material/styles';
+import { IconButton, IconButtonProps, PaletteColor } from '@mui/material';
+import { styled, Theme } from '@mui/material/styles';
 import React, { useContext, useMemo } from 'react';
 import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router-dom';
 
-import { WebRTCContext } from '../contexts/WebRTCProvider';
-import { ExpandableButton, ExpandableButtonProps, ToggleIconButton } from './Button';
+import { CallContext } from '../contexts/CallProvider';
+import { ExpandableButton, ExpandableButtonProps, ShapedButtonProps, ToggleIconButton } from './Button';
 import {
   CallEndIcon,
   ChatBubbleIcon,
@@ -44,8 +46,6 @@
   WindowIcon,
 } from './SvgIcon';
 
-type ColoredCallButtonColor = 'red' | 'green';
-
 const CallButton = styled((props: ExpandableButtonProps) => {
   return <ExpandableButton {...props} />;
 })({
@@ -57,19 +57,28 @@
 
 const ColoredCallButton = styled(
   ({
-    buttonColor,
+    paletteColor,
+    Icon,
     ...props
-  }: ExpandableButtonProps & {
-    buttonColor: ColoredCallButtonColor;
+  }: ShapedButtonProps & {
+    paletteColor?: PaletteColor | ((theme: Theme) => PaletteColor);
   }) => {
-    return <ExpandableButton {...props} />;
+    return (
+      <IconButton {...props} disableRipple={true}>
+        <Icon fontSize="inherit" />
+      </IconButton>
+    );
   }
-)(({ buttonColor }) => {
+)(({ theme, paletteColor = theme.palette.primary }) => {
+  if (typeof paletteColor === 'function') {
+    paletteColor = paletteColor(theme);
+  }
+
   return {
-    color: 'white',
-    backgroundColor: buttonColor === 'green' ? '#183722' : '#5E070D',
+    color: paletteColor.contrastText,
+    backgroundColor: paletteColor.dark,
     '&:hover': {
-      backgroundColor: buttonColor === 'green' ? '#0B8271' : '#CC0022',
+      backgroundColor: paletteColor.main,
     },
   };
 });
@@ -79,7 +88,19 @@
 };
 
 export const CallingEndButton = (props: ExpandableButtonProps) => {
-  return <ColoredCallButton buttonColor="red" aria-label="call end" Icon={CallEndIcon} {...props} />;
+  const navigate = useNavigate();
+  return (
+    <ColoredCallButton
+      paletteColor={(theme) => theme.palette.error}
+      onClick={() => {
+        // TODO: Send event to end call
+        navigate('/');
+      }}
+      aria-label="call end"
+      Icon={CallEndIcon}
+      {...props}
+    />
+  );
 };
 
 export const CallingExtensionButton = (props: ExpandableButtonProps) => {
@@ -132,7 +153,7 @@
 };
 
 const useMediaDeviceExpandMenuOptions = (kind: MediaDeviceKind) => {
-  const { mediaDevices } = useContext(WebRTCContext);
+  const { mediaDevices } = useContext(CallContext);
 
   return useMemo(
     () =>
@@ -162,7 +183,6 @@
 };
 
 export const CallingMicButton = (props: ExpandableButtonProps) => {
-  const { isAudioOn, setAudioStatus } = useContext(WebRTCContext);
   const options = useMediaDeviceExpandMenuOptions('audioinput');
 
   return (
@@ -173,22 +193,26 @@
           options,
         },
       ]}
-      IconButtonComp={(props) => (
-        <ToggleIconButton
-          IconOn={MicroIcon}
-          IconOff={MicroOffIcon}
-          selected={isAudioOn}
-          toggle={() => setAudioStatus(!isAudioOn)}
-          {...props}
-        />
-      )}
+      IconButtonComp={ToggleAudioCameraIconButton}
+      {...props}
+    />
+  );
+};
+
+const ToggleAudioCameraIconButton = (props: IconButtonProps) => {
+  const { isAudioOn, setAudioStatus } = useContext(CallContext);
+  return (
+    <ToggleIconButton
+      IconOn={MicroIcon}
+      IconOff={MicroOffIcon}
+      selected={isAudioOn}
+      toggle={() => setAudioStatus(!isAudioOn)}
       {...props}
     />
   );
 };
 
 export const CallingVideoCameraButton = (props: ExpandableButtonProps) => {
-  const { isVideoOn, setVideoStatus } = useContext(WebRTCContext);
   const options = useMediaDeviceExpandMenuOptions('videoinput');
 
   return (
@@ -199,29 +223,85 @@
           options,
         },
       ]}
-      IconButtonComp={(props) => (
-        <ToggleIconButton
-          IconOn={VideoCameraIcon}
-          IconOff={VideoCameraOffIcon}
-          selected={isVideoOn}
-          toggle={() => setVideoStatus(!isVideoOn)}
-          {...props}
-        />
-      )}
+      IconButtonComp={ToggleVideoCameraIconButton}
+      {...props}
+    />
+  );
+};
+
+const ToggleVideoCameraIconButton = (props: IconButtonProps) => {
+  const { isVideoOn, setVideoStatus } = useContext(CallContext);
+  return (
+    <ToggleIconButton
+      IconOn={VideoCameraIcon}
+      IconOff={VideoCameraOffIcon}
+      selected={isVideoOn}
+      toggle={() => setVideoStatus(!isVideoOn)}
       {...props}
     />
   );
 };
 
 // Calling pending/receiving interface
-export const CallingAnswerAudioButton = (props: ExpandableButtonProps) => {
-  return <ColoredCallButton aria-label="answer audio" buttonColor="green" Icon={PlaceAudioCallIcon} {...props} />;
+export const CallingCancelButton = (props: IconButtonProps) => {
+  const navigate = useNavigate();
+
+  return (
+    <ColoredCallButton
+      aria-label="cancel call"
+      onClick={() => {
+        navigate(-1);
+      }}
+      Icon={CallEndIcon}
+      paletteColor={(theme) => theme.palette.error}
+      {...props}
+    />
+  );
 };
 
-export const CallingAnswerVideoButton = (props: ExpandableButtonProps) => {
-  return <ColoredCallButton aria-label="answer video" buttonColor="green" Icon={VideoCameraIcon} {...props} />;
+export const CallingAnswerAudioButton = (props: IconButtonProps) => {
+  const { acceptCall } = useContext(CallContext);
+
+  return (
+    <ColoredCallButton
+      aria-label="answer call audio"
+      onClick={() => {
+        acceptCall();
+      }}
+      Icon={PlaceAudioCallIcon}
+      paletteColor={(theme) => theme.palette.success}
+      {...props}
+    />
+  );
 };
 
-export const CallingRefuseButton = (props: ExpandableButtonProps) => {
-  return <ColoredCallButton aria-label="reject" buttonColor="red" Icon={RoundCloseIcon} {...props} />;
+export const CallingAnswerVideoButton = (props: IconButtonProps) => {
+  const { acceptCall } = useContext(CallContext);
+
+  return (
+    <ColoredCallButton
+      aria-label="answer call video"
+      onClick={() => {
+        acceptCall();
+      }}
+      paletteColor={(theme) => theme.palette.success}
+      Icon={VideoCameraIcon}
+      {...props}
+    />
+  );
+};
+
+export const CallingRefuseButton = (props: IconButtonProps) => {
+  const navigate = useNavigate();
+  return (
+    <ColoredCallButton
+      aria-label="refuse call"
+      onClick={() => {
+        navigate(-1);
+      }}
+      paletteColor={(theme) => theme.palette.error}
+      Icon={RoundCloseIcon}
+      {...props}
+    />
+  );
 };