Redirect user from call page when not in call

When a user tries to access a call page while not in a call, redirect the user to the home page.

Misc changes:

- Add route state to the call route that includes the CallStatus.
- CallProvider redirects to home if the callStatus isn't set (meaning
  the user isn't in a call).
- Remove `beginCall` function in `ConversationProvider`. Added `useStartCall` hook that redirects the user to the call page. The `CallProvider` automatically sends the `BeginCall` message when the user reaches the page for the first time.
- Reorder functions in CallProvider to have `useEffect` functions at the top

GitLab: #164
Change-Id: I6cec1b9f31cb308d92a69112f5b38d1bdf79e05f
diff --git a/client/src/contexts/CallProvider.tsx b/client/src/contexts/CallProvider.tsx
index a17e738..de2a6d7 100644
--- a/client/src/contexts/CallProvider.tsx
+++ b/client/src/contexts/CallProvider.tsx
@@ -29,6 +29,7 @@
 export type CallRole = 'caller' | 'receiver';
 
 export enum CallStatus {
+  Default,
   Ringing,
   Connecting,
   InCall,
@@ -65,7 +66,7 @@
   isVideoOn: false,
   setVideoStatus: () => {},
   callRole: 'caller',
-  callStatus: CallStatus.Ringing,
+  callStatus: CallStatus.Default,
 
   acceptCall: () => {},
 };
@@ -75,6 +76,7 @@
 export default ({ children }: WithChildren) => {
   const {
     queryParams: { role: callRole },
+    state: routeState,
   } = useUrlParams<CallRouteParams>();
   const webSocket = useContext(WebSocketContext);
   const { webRtcConnection, remoteStreams, sendWebRtcOffer, isConnected } = useContext(WebRtcContext);
@@ -87,9 +89,11 @@
 
   const [isAudioOn, setIsAudioOn] = useState(false);
   const [isVideoOn, setIsVideoOn] = useState(false);
-  const [callStatus, setCallStatus] = useState(CallStatus.Ringing);
+  const [callStatus, setCallStatus] = useState(routeState?.callStatus);
 
-  // TODO: This logic will have to change to support multiple people in a call
+  // TODO: This logic will have to change to support multiple people in a call. Could we move this logic to the server?
+  //       The client could make a single request with the conversationId, and the server would be tasked with sending
+  //       all the individual requests to the members of the conversation.
   const contactUri = useMemo(() => conversation.getFirstMember().contact.getUri(), [conversation]);
 
   useEffect(() => {
@@ -140,35 +144,22 @@
     }
   }, [localStream, webRtcConnection]);
 
-  const setAudioStatus = useCallback(
-    (isOn: boolean) => {
-      if (!localStream) {
-        return;
-      }
+  useEffect(() => {
+    if (!webSocket) {
+      return;
+    }
 
-      for (const track of localStream.getAudioTracks()) {
-        track.enabled = isOn;
-      }
+    if (callRole === 'caller' && callStatus === CallStatus.Default) {
+      const callBegin: CallAction = {
+        contactId: contactUri,
+        conversationId,
+      };
 
-      setIsAudioOn(isOn);
-    },
-    [localStream]
-  );
-
-  const setVideoStatus = useCallback(
-    (isOn: boolean) => {
-      if (!localStream) {
-        return;
-      }
-
-      for (const track of localStream.getVideoTracks()) {
-        track.enabled = isOn;
-      }
-
-      setIsVideoOn(isOn);
-    },
-    [localStream]
-  );
+      console.info('Sending CallBegin', callBegin);
+      webSocket.send(WebSocketMessageType.CallBegin, callBegin);
+      setCallStatus(CallStatus.Ringing);
+    }
+  }, [webSocket, callRole, callStatus, contactUri, conversationId]);
 
   useEffect(() => {
     if (!webSocket || !webRtcConnection) {
@@ -220,8 +211,38 @@
     setCallStatus(CallStatus.Connecting);
   }, [webSocket, contactUri, conversationId]);
 
-  if (!callRole) {
-    console.error('Call role not defined. Redirecting...');
+  const setAudioStatus = useCallback(
+    (isOn: boolean) => {
+      if (!localStream) {
+        return;
+      }
+
+      for (const track of localStream.getAudioTracks()) {
+        track.enabled = isOn;
+      }
+
+      setIsAudioOn(isOn);
+    },
+    [localStream]
+  );
+
+  const setVideoStatus = useCallback(
+    (isOn: boolean) => {
+      if (!localStream) {
+        return;
+      }
+
+      for (const track of localStream.getVideoTracks()) {
+        track.enabled = isOn;
+      }
+
+      setIsVideoOn(isOn);
+    },
+    [localStream]
+  );
+
+  if (!callRole || callStatus === undefined) {
+    console.error('Invalid route. Redirecting...');
     return <Navigate to={'/'} />;
   }
 
diff --git a/client/src/contexts/ConversationProvider.tsx b/client/src/contexts/ConversationProvider.tsx
index a282664..7d09881 100644
--- a/client/src/contexts/ConversationProvider.tsx
+++ b/client/src/contexts/ConversationProvider.tsx
@@ -15,9 +15,8 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { CallAction, Conversation, ConversationView, WebSocketMessageType } from 'jami-web-common';
-import { createContext, useCallback, useContext, useEffect, useState } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { Conversation, ConversationView, WebSocketMessageType } from 'jami-web-common';
+import { createContext, useContext, useEffect, useState } from 'react';
 
 import LoadingPage from '../components/Loading';
 import { useUrlParams } from '../hooks/useUrlParams';
@@ -30,8 +29,6 @@
 interface IConversationProvider {
   conversationId: string;
   conversation: Conversation;
-
-  beginCall: () => void;
 }
 
 export const ConversationContext = createContext<IConversationProvider>(undefined!);
@@ -45,9 +42,8 @@
   const [isLoading, setIsLoading] = useState(false);
   const [isError, setIsError] = useState(false);
   const [conversation, setConversation] = useState<Conversation | undefined>();
-  const navigate = useNavigate();
 
-  const conversationQuery = useConversationQuery(conversationId);
+  const conversationQuery = useConversationQuery(conversationId!);
 
   useEffect(() => {
     if (conversationQuery.isSuccess) {
@@ -64,28 +60,8 @@
     setIsError(conversationQuery.isError);
   }, [conversationQuery.isError]);
 
-  const beginCall = useCallback(() => {
-    if (!webSocket || !conversation) {
-      throw new Error('Could not begin call');
-    }
-
-    // TODO: Could we move this logic to the server? The client could make a single request with the conversationId,
-    // and the server is tasked with sending all the individual requests to the members of the conversation
-    for (const member of conversation.getMembers()) {
-      const callBegin: CallAction = {
-        contactId: member.contact.getUri(),
-        conversationId,
-      };
-
-      console.info('Sending CallBegin', callBegin);
-      webSocket.send(WebSocketMessageType.CallBegin, callBegin);
-    }
-
-    navigate(`/conversation/${conversationId}/call?role=caller`);
-  }, [conversationId, webSocket, conversation, navigate]);
-
   useEffect(() => {
-    if (!conversation || !webSocket) {
+    if (!conversation || !conversationId || !webSocket) {
       return;
     }
 
@@ -108,7 +84,6 @@
       value={{
         conversationId,
         conversation,
-        beginCall,
       }}
     >
       {children}