Fix components rerendering unnecessarily

Before, some providers (WebRtcProvider, CallProvider...) were rendered conditionally.
This was causing all their children to re-render when the provider rendered.

Instead of using conditional rendering, these providers now use `ConditionalContextProvider`.
It always renders the provider, but sets its value to `initialValue` if the dependencies are not all defined.
If all dependencies are defined, the value is the result of the `useProviderValue` hook.

Changes:
- New file: `ConditionalContextProvider`
- New file: `HookComponent` - Component that calls a hook when mounted and calls a `callback` function with the hook result as argument
- For `WebRtcProvider` and `CallProvider`, use `createOptionalContext` to create the context and `ConditionalContextProvider` to provide their value only when the conditions are met
- In `WebRtcProvider`, set the webRtcConnection to undefined when not in a call
- For all providers, wrap their `value` prop in `useMemo` to avoid unnecessary rerenders

Change-Id: Ide947e216d54599aabc71cf4bd026bd20d6e0daf
diff --git a/client/src/components/CallButtons.tsx b/client/src/components/CallButtons.tsx
index db0f530..9243c33 100644
--- a/client/src/components/CallButtons.tsx
+++ b/client/src/components/CallButtons.tsx
@@ -18,9 +18,9 @@
 
 import { IconButton, IconButtonProps, PaletteColor } from '@mui/material';
 import { styled, Theme } from '@mui/material/styles';
-import { ChangeEvent, useContext, useMemo } from 'react';
+import { ChangeEvent, useMemo } from 'react';
 
-import { CallContext, CallStatus, VideoStatus } from '../contexts/CallProvider';
+import { CallStatus, useCallContext, VideoStatus } from '../contexts/CallProvider';
 import {
   ExpandableButton,
   ExpandableButtonProps,
@@ -88,7 +88,7 @@
 });
 
 export const CallingChatButton = (props: ExpandableButtonProps) => {
-  const { setIsChatShown } = useContext(CallContext);
+  const { setIsChatShown } = useCallContext();
   return (
     <CallButton
       aria-label="chat"
@@ -102,7 +102,7 @@
 };
 
 export const CallingEndButton = (props: ExpandableButtonProps) => {
-  const { endCall } = useContext(CallContext);
+  const { endCall } = useCallContext();
   return (
     <ColoredCallButton
       paletteColor={(theme) => theme.palette.error}
@@ -121,7 +121,7 @@
 };
 
 export const CallingFullScreenButton = (props: ExpandableButtonProps) => {
-  const { setIsFullscreen } = useContext(CallContext);
+  const { setIsFullscreen } = useCallContext();
   return (
     <CallButton
       aria-label="full screen"
@@ -153,7 +153,7 @@
 };
 
 const ToggleScreenShareIconButton = (props: IconButtonProps) => {
-  const { videoStatus, updateVideoStatus } = useContext(CallContext);
+  const { videoStatus, updateVideoStatus } = useCallContext();
 
   return (
     <ToggleIconButton
@@ -169,7 +169,7 @@
 };
 
 const useMediaDeviceExpandMenuOptions = (kind: MediaDeviceKind): ExpandMenuRadioOption[] | undefined => {
-  const { currentMediaDeviceIds, mediaDevices } = useContext(CallContext);
+  const { currentMediaDeviceIds, mediaDevices } = useCallContext();
 
   const options = useMemo(
     () =>
@@ -224,7 +224,7 @@
 };
 
 const ToggleAudioCameraIconButton = (props: IconButtonProps) => {
-  const { isAudioOn, setIsAudioOn } = useContext(CallContext);
+  const { isAudioOn, setIsAudioOn } = useCallContext();
   return (
     <ToggleIconButton
       IconOn={MicroIcon}
@@ -250,7 +250,7 @@
 };
 
 const ToggleVideoCameraIconButton = (props: IconButtonProps) => {
-  const { videoStatus, updateVideoStatus } = useContext(CallContext);
+  const { videoStatus, updateVideoStatus } = useCallContext();
   return (
     <ToggleIconButton
       IconOn={VideoCameraIcon}
@@ -266,7 +266,7 @@
 
 // Calling pending/receiving interface
 export const CallingCancelButton = (props: IconButtonProps) => {
-  const { endCall } = useContext(CallContext);
+  const { endCall } = useCallContext();
 
   return (
     <ColoredCallButton
@@ -282,7 +282,7 @@
 };
 
 export const CallingAnswerAudioButton = (props: IconButtonProps) => {
-  const { acceptCall, callStatus } = useContext(CallContext);
+  const { acceptCall, callStatus } = useCallContext();
 
   return (
     <ColoredCallButton
@@ -299,7 +299,7 @@
 };
 
 export const CallingAnswerVideoButton = (props: IconButtonProps) => {
-  const { acceptCall, callStatus } = useContext(CallContext);
+  const { acceptCall, callStatus } = useCallContext();
   return (
     <ColoredCallButton
       disabled={callStatus === CallStatus.Connecting || callStatus === CallStatus.Loading}
@@ -315,7 +315,7 @@
 };
 
 export const CallingRefuseButton = (props: IconButtonProps) => {
-  const { endCall } = useContext(CallContext);
+  const { endCall } = useCallContext();
   return (
     <ColoredCallButton
       aria-label="refuse call"