Remove non-null assertion in ConversationProvider

- Add `createOptionalContext` that is used by `AuthContext` and `ConversationContext` to create a context with a hook
  that can be used to retrieve its value and throw an error if it's undefined.
- In `router.tsx`, put `Messenger` inside `ConversationProvider`.
- In `ConversationListItem`, use the conversationId from the `ConversationContext ` instead of the url params.
- Fix bug in `CallInterface` with fullscreen.
- Remove unecessary useEffect dependency in `NotificationManager`.

Change-Id: Ib5f0ae6a0a34cdbdb02f871e36194376d945230d
diff --git a/client/src/components/CallChatDrawer.tsx b/client/src/components/CallChatDrawer.tsx
index d2040d7..ef5cf04 100644
--- a/client/src/components/CallChatDrawer.tsx
+++ b/client/src/components/CallChatDrawer.tsx
@@ -19,7 +19,7 @@
 import { useContext } from 'react';
 
 import { CallContext } from '../contexts/CallProvider';
-import { ConversationContext } from '../contexts/ConversationProvider';
+import { useConversationContext } from '../contexts/ConversationProvider';
 import ChatInterface from '../pages/ChatInterface';
 import { CloseButton } from './Button';
 
@@ -45,7 +45,7 @@
 
 const CallChatDrawerHeader = () => {
   const { setIsChatShown } = useContext(CallContext);
-  const { conversation } = useContext(ConversationContext);
+  const { conversation } = useConversationContext();
 
   // TODO: Improve this to support multiple members
   const contact = conversation.getFirstMember().contact;
diff --git a/client/src/components/ConversationListItem.tsx b/client/src/components/ConversationListItem.tsx
index ebf0c93..41cfbe4 100644
--- a/client/src/components/ConversationListItem.tsx
+++ b/client/src/components/ConversationListItem.tsx
@@ -22,13 +22,12 @@
 import { useNavigate } from 'react-router-dom';
 
 import { useAuthContext } from '../contexts/AuthProvider';
+import { useConversationContext } from '../contexts/ConversationProvider';
 import { MessengerContext } from '../contexts/MessengerProvider';
 import { useStartCall } from '../hooks/useStartCall';
-import { useUrlParams } from '../hooks/useUrlParams';
 import { Conversation } from '../models/Conversation';
 import { setRefreshFromSlice } from '../redux/appSlice';
 import { useAppDispatch } from '../redux/hooks';
-import { ConversationRouteParams } from '../router';
 import ContextMenu, { ContextMenuHandler, useContextMenuHandler } from './ContextMenu';
 import ConversationAvatar from './ConversationAvatar';
 import { ConfirmationDialog, DialogContentList, InfosDialog, useDialogHandler } from './Dialog';
@@ -48,13 +47,12 @@
 };
 
 export default function ConversationListItem({ conversation }: ConversationListItemProps) {
-  const {
-    urlParams: { conversationId },
-  } = useUrlParams<ConversationRouteParams>();
+  const conversationContext = useConversationContext(true);
+  const conversationId = conversationContext?.conversationId;
   const contextMenuHandler = useContextMenuHandler();
   const { newContactId, setNewContactId } = useContext(MessengerContext);
 
-  const pathId = conversationId || newContactId;
+  const pathId = conversationId ?? newContactId;
   const isSelected = conversation.getDisplayUri() === pathId;
 
   const navigate = useNavigate();
diff --git a/client/src/components/ConversationView.tsx b/client/src/components/ConversationView.tsx
index 0319574..075ca6e 100644
--- a/client/src/components/ConversationView.tsx
+++ b/client/src/components/ConversationView.tsx
@@ -16,11 +16,11 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { Divider, Stack, Typography } from '@mui/material';
-import { useContext, useMemo } from 'react';
+import { useMemo } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import { useAuthContext } from '../contexts/AuthProvider';
-import { ConversationContext } from '../contexts/ConversationProvider';
+import { useConversationContext } from '../contexts/ConversationProvider';
 import { useStartCall } from '../hooks/useStartCall';
 import { ConversationMember } from '../models/Conversation';
 import ChatInterface from '../pages/ChatInterface';
@@ -43,7 +43,7 @@
 
 const ConversationHeader = () => {
   const { account } = useAuthContext();
-  const { conversation, conversationId } = useContext(ConversationContext);
+  const { conversation, conversationId } = useConversationContext();
   const { t } = useTranslation();
 
   const members = conversation.getMembers();
diff --git a/client/src/contexts/AuthProvider.tsx b/client/src/contexts/AuthProvider.tsx
index 06bf1bb..d0d88cc 100644
--- a/client/src/contexts/AuthProvider.tsx
+++ b/client/src/contexts/AuthProvider.tsx
@@ -17,10 +17,11 @@
  */
 import axios, { AxiosInstance } from 'axios';
 import { HttpStatusCode } from 'jami-web-common';
-import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
 import { useNavigate } from 'react-router-dom';
 
 import ProcessingRequest from '../components/ProcessingRequest';
+import { createOptionalContext } from '../hooks/createOptionalContext';
 import { Account } from '../models/Account';
 import { apiUrl } from '../utils/constants';
 import { WithChildren } from '../utils/utils';
@@ -33,7 +34,9 @@
   axiosInstance: AxiosInstance;
 }
 
-const AuthContext = createContext<IAuthContext | undefined>(undefined);
+const optionalAuthContext = createOptionalContext<IAuthContext>('AuthContext');
+const AuthContext = optionalAuthContext.Context;
+export const useAuthContext = optionalAuthContext.useOptionalContext;
 
 export default ({ children }: WithChildren) => {
   const [token, setToken] = useState<string | undefined>();
@@ -109,13 +112,3 @@
     </AuthContext.Provider>
   );
 };
-
-export function useAuthContext(dontThrowIfUndefined: true): IAuthContext | undefined;
-export function useAuthContext(): IAuthContext;
-export function useAuthContext(dontThrowIfUndefined?: true) {
-  const authContext = useContext(AuthContext);
-  if (!authContext && !dontThrowIfUndefined) {
-    throw new Error('AuthContext is not provided');
-  }
-  return authContext;
-}
diff --git a/client/src/contexts/CallProvider.tsx b/client/src/contexts/CallProvider.tsx
index 13e0d24..39aad07 100644
--- a/client/src/contexts/CallProvider.tsx
+++ b/client/src/contexts/CallProvider.tsx
@@ -25,7 +25,7 @@
 import { CallRouteParams } from '../router';
 import { callTimeoutMs } from '../utils/constants';
 import { SetState, WithChildren } from '../utils/utils';
-import { ConversationContext } from './ConversationProvider';
+import { useConversationContext } from './ConversationProvider';
 import { MediaDevicesInfo, MediaInputKind, WebRtcContext } from './WebRtcProvider';
 import { IWebSocketContext, WebSocketContext } from './WebSocketProvider';
 
@@ -124,7 +124,7 @@
   const { state: routeState } = useUrlParams<CallRouteParams>();
   const { localStream, sendWebRtcOffer, iceConnectionState, closeConnection, getMediaDevices, updateLocalStream } =
     useContext(WebRtcContext);
-  const { conversationId, conversation } = useContext(ConversationContext);
+  const { conversationId, conversation } = useConversationContext();
   const navigate = useNavigate();
 
   const [mediaDevices, setMediaDevices] = useState(defaultCallContext.mediaDevices);
diff --git a/client/src/contexts/ConversationProvider.tsx b/client/src/contexts/ConversationProvider.tsx
index 25137c0..b927278 100644
--- a/client/src/contexts/ConversationProvider.tsx
+++ b/client/src/contexts/ConversationProvider.tsx
@@ -16,9 +16,10 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { ConversationView, WebSocketMessageType } from 'jami-web-common';
-import { createContext, useContext, useEffect, useState } from 'react';
+import { useContext, useEffect, useState } from 'react';
 
 import LoadingPage from '../components/Loading';
+import { createOptionalContext } from '../hooks/createOptionalContext';
 import { useUrlParams } from '../hooks/useUrlParams';
 import { Conversation } from '../models/Conversation';
 import { ConversationRouteParams } from '../router';
@@ -27,12 +28,14 @@
 import { useAuthContext } from './AuthProvider';
 import { WebSocketContext } from './WebSocketProvider';
 
-interface IConversationProvider {
+interface IConversationContext {
   conversationId: string;
   conversation: Conversation;
 }
 
-export const ConversationContext = createContext<IConversationProvider>(undefined!);
+const optionalConversationContext = createOptionalContext<IConversationContext>('ConversationContext');
+const ConversationContext = optionalConversationContext.Context;
+export const useConversationContext = optionalConversationContext.useOptionalContext;
 
 export default ({ children }: WithChildren) => {
   const {
diff --git a/client/src/contexts/WebRtcProvider.tsx b/client/src/contexts/WebRtcProvider.tsx
index 4cd6263..59999b9 100644
--- a/client/src/contexts/WebRtcProvider.tsx
+++ b/client/src/contexts/WebRtcProvider.tsx
@@ -22,7 +22,7 @@
 import LoadingPage from '../components/Loading';
 import { WithChildren } from '../utils/utils';
 import { useAuthContext } from './AuthProvider';
-import { ConversationContext } from './ConversationProvider';
+import { useConversationContext } from './ConversationProvider';
 import { IWebSocketContext, WebSocketContext } from './WebSocketProvider';
 
 export type MediaDevicesInfo = Record<MediaDeviceKind, MediaDeviceInfo[]>;
@@ -99,7 +99,7 @@
   webRtcConnection: RTCPeerConnection;
   webSocket: IWebSocketContext;
 }) => {
-  const { conversation, conversationId } = useContext(ConversationContext);
+  const { conversation, conversationId } = useConversationContext();
   const [localStream, setLocalStream] = useState<MediaStream>();
   const [remoteStreams, setRemoteStreams] = useState<readonly MediaStream[]>();
   const [iceConnectionState, setIceConnectionState] = useState<RTCIceConnectionState | undefined>();
diff --git a/client/src/hooks/createOptionalContext.ts b/client/src/hooks/createOptionalContext.ts
new file mode 100644
index 0000000..73c4258
--- /dev/null
+++ b/client/src/hooks/createOptionalContext.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import { createContext, useContext } from 'react';
+
+export const createOptionalContext = <T>(displayName: string) => {
+  const Context = createContext<T | undefined>(undefined);
+  Context.displayName = displayName;
+
+  function useOptionalContext(noThrow: true): T | undefined;
+  function useOptionalContext(noThrow?: false): T;
+  function useOptionalContext(noThrow?: boolean) {
+    const value = useContext(Context);
+    if (value === undefined && !noThrow) {
+      throw new Error(`The context ${Context.displayName} is not provided`);
+    }
+    return value;
+  }
+
+  return {
+    Context,
+    useOptionalContext,
+  };
+};
diff --git a/client/src/managers/NotificationManager.tsx b/client/src/managers/NotificationManager.tsx
index 63eccc6..907efb5 100644
--- a/client/src/managers/NotificationManager.tsx
+++ b/client/src/managers/NotificationManager.tsx
@@ -19,7 +19,6 @@
 import { useContext, useEffect } from 'react';
 import { useNavigate } from 'react-router-dom';
 
-import { useAuthContext } from '../contexts/AuthProvider';
 import { CallStatus } from '../contexts/CallProvider';
 import { WebSocketContext } from '../contexts/WebSocketProvider';
 import { WithChildren } from '../utils/utils';
@@ -30,7 +29,6 @@
 export default ({ children }: WithChildren) => {
   const webSocket = useContext(WebSocketContext);
   const navigate = useNavigate();
-  const { axiosInstance } = useAuthContext();
 
   useEffect(() => {
     if (!webSocket) {
@@ -53,7 +51,7 @@
     return () => {
       webSocket.unbind(WebSocketMessageType.CallBegin, callBeginListener);
     };
-  }, [webSocket, navigate, axiosInstance]);
+  }, [webSocket, navigate]);
 
   return <>{children}</>;
 };
diff --git a/client/src/pages/CallInterface.tsx b/client/src/pages/CallInterface.tsx
index 54c9b10..f21c49c 100644
--- a/client/src/pages/CallInterface.tsx
+++ b/client/src/pages/CallInterface.tsx
@@ -47,7 +47,7 @@
 } from '../components/CallButtons';
 import CallChatDrawer from '../components/CallChatDrawer';
 import { CallContext, CallStatus } from '../contexts/CallProvider';
-import { ConversationContext } from '../contexts/ConversationProvider';
+import { useConversationContext } from '../contexts/ConversationProvider';
 import { WebRtcContext } from '../contexts/WebRtcProvider';
 import { VideoElementWithSinkId } from '../utils/utils';
 import { CallPending } from './CallPending';
@@ -63,7 +63,7 @@
 
     if (isFullscreen && document.fullscreenElement === null) {
       callInterfaceRef.current.requestFullscreen();
-    } else if (!isFullscreen && document.fullscreenEnabled !== null) {
+    } else if (!isFullscreen && document.fullscreenElement !== null) {
       document.exitFullscreen();
     }
   }, [isFullscreen]);
@@ -187,7 +187,7 @@
 
 const CallInterfaceInformation = () => {
   const { callStartTime } = useContext(CallContext);
-  const { conversation } = useContext(ConversationContext);
+  const { conversation } = useConversationContext();
   const [elapsedTime, setElapsedTime] = useState(0);
   const memberName = useMemo(() => conversation.getFirstMember().contact.getRegisteredName(), [conversation]);
 
diff --git a/client/src/pages/CallPending.tsx b/client/src/pages/CallPending.tsx
index 4bdbef4..2cd57c6 100644
--- a/client/src/pages/CallPending.tsx
+++ b/client/src/pages/CallPending.tsx
@@ -29,13 +29,13 @@
 } from '../components/CallButtons';
 import ConversationAvatar from '../components/ConversationAvatar';
 import { CallContext, CallStatus } from '../contexts/CallProvider';
-import { ConversationContext } from '../contexts/ConversationProvider';
+import { useConversationContext } from '../contexts/ConversationProvider';
 import { WebRtcContext } from '../contexts/WebRtcProvider';
 import { VideoElementWithSinkId } from '../utils/utils';
 
 export const CallPending = () => {
   const { localStream } = useContext(WebRtcContext);
-  const { conversation } = useContext(ConversationContext);
+  const { conversation } = useConversationContext();
   const { callRole } = useContext(CallContext);
   const localVideoRef = useRef<VideoElementWithSinkId | null>(null);
 
@@ -148,7 +148,7 @@
 export const CallPendingCallerInterface = () => {
   const { callStatus } = useContext(CallContext);
   const { t } = useTranslation();
-  const { conversation } = useContext(ConversationContext);
+  const { conversation } = useConversationContext();
   const memberName = useMemo(() => conversation.getFirstMember().contact.getRegisteredName(), [conversation]);
 
   let title = t('loading');
@@ -182,7 +182,7 @@
   const { callStatus } = useContext(CallContext);
 
   const { t } = useTranslation();
-  const { conversation } = useContext(ConversationContext);
+  const { conversation } = useConversationContext();
   const memberName = useMemo(() => conversation.getFirstMember().contact.getRegisteredName(), [conversation]);
 
   let title = t('loading');
diff --git a/client/src/pages/ChatInterface.tsx b/client/src/pages/ChatInterface.tsx
index f6a637a..4ae9ce6 100644
--- a/client/src/pages/ChatInterface.tsx
+++ b/client/src/pages/ChatInterface.tsx
@@ -24,14 +24,14 @@
 import LoadingPage from '../components/Loading';
 import MessageList from '../components/MessageList';
 import SendMessageForm from '../components/SendMessageForm';
-import { ConversationContext } from '../contexts/ConversationProvider';
+import { useConversationContext } from '../contexts/ConversationProvider';
 import { WebSocketContext } from '../contexts/WebSocketProvider';
 import { useMessagesQuery, useSendMessageMutation } from '../services/conversationQueries';
 import { FileHandler } from '../utils/files';
 
 const ChatInterface = () => {
   const webSocket = useContext(WebSocketContext);
-  const { conversationId, conversation } = useContext(ConversationContext);
+  const { conversationId, conversation } = useConversationContext();
   const [messages, setMessages] = useState<Message[]>([]);
   const [isLoading, setIsLoading] = useState(true);
   const [error, setError] = useState(false);
diff --git a/client/src/router.tsx b/client/src/router.tsx
index 7bbddd4..8ed3403 100644
--- a/client/src/router.tsx
+++ b/client/src/router.tsx
@@ -79,11 +79,11 @@
           <Route
             path="conversation/:conversationId"
             element={
-              <Messenger>
-                <ConversationProvider>
+              <ConversationProvider>
+                <Messenger>
                   <Outlet />
-                </ConversationProvider>
-              </Messenger>
+                </Messenger>
+              </ConversationProvider>
             }
           >
             <Route index element={<ConversationView />} />