Change conversation during call
- Add `CallManagerProvider` to manage calls when the user navigates away from the call interface.
- Delete `NotificationManager`. Move its logic to `CallManagerProvider`.
- Rework routing
- Rework `ConversationProvider` and `useConversationQuery` to remove
unecessary states
GitLab: #172
Change-Id: I4a786a3dd52159680e5712e598d9b831525fb63f
diff --git a/client/src/contexts/CallManagerProvider.tsx b/client/src/contexts/CallManagerProvider.tsx
new file mode 100644
index 0000000..4261f07
--- /dev/null
+++ b/client/src/contexts/CallManagerProvider.tsx
@@ -0,0 +1,116 @@
+/*
+ * 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 { CallBegin, WebSocketMessageType } from 'jami-web-common';
+import { createContext, useCallback, useContext, useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import { Conversation } from '../models/conversation';
+import { useConversationQuery } from '../services/conversationQueries';
+import { SetState, WithChildren } from '../utils/utils';
+import CallProvider, { CallRole } from './CallProvider';
+import WebRtcProvider from './WebRtcProvider';
+import { WebSocketContext } from './WebSocketProvider';
+
+type CallData = {
+ conversationId: string;
+ role: CallRole;
+ withVideoOn?: boolean;
+};
+
+type ICallManagerContext = {
+ callData: CallData | undefined;
+ callConversation: Conversation | undefined;
+
+ startCall: SetState<CallData | undefined>;
+ exitCall: () => void;
+};
+
+export const CallManagerContext = createContext<ICallManagerContext>(undefined!);
+CallManagerContext.displayName = 'CallManagerContext';
+
+export default ({ children }: WithChildren) => {
+ const [callData, setCallData] = useState<CallData>();
+ const webSocket = useContext(WebSocketContext);
+ const navigate = useNavigate();
+ const conversationId = callData?.conversationId;
+ const { conversation } = useConversationQuery(conversationId);
+
+ const failStartCall = useCallback(() => {
+ throw new Error('Cannot start call: Already in a call');
+ }, []);
+
+ const startCall = !callData ? setCallData : failStartCall;
+
+ const exitCall = useCallback(() => {
+ if (!callData) {
+ return;
+ }
+
+ setCallData(undefined);
+ // TODO: write in chat that the call ended
+ }, [callData]);
+
+ useEffect(() => {
+ if (callData) {
+ // TODO: Currently, we simply do not bind the CallBegin listener if already in a call.
+ // In the future, we should handle receiving a call while already in another.
+ return;
+ }
+ if (!webSocket) {
+ return;
+ }
+
+ const callBeginListener = ({ conversationId, withVideoOn }: CallBegin) => {
+ startCall({ conversationId: conversationId, role: 'receiver', withVideoOn });
+ navigate(`/conversation/${conversationId}`);
+ };
+
+ webSocket.bind(WebSocketMessageType.CallBegin, callBeginListener);
+
+ return () => {
+ webSocket.unbind(WebSocketMessageType.CallBegin, callBeginListener);
+ };
+ }, [webSocket, navigate, startCall, callData]);
+
+ return (
+ <CallManagerContext.Provider
+ value={{
+ startCall,
+ callData,
+ callConversation: conversation,
+ exitCall,
+ }}
+ >
+ <CallManagerProvider>{children}</CallManagerProvider>
+ </CallManagerContext.Provider>
+ );
+};
+
+const CallManagerProvider = ({ children }: WithChildren) => {
+ const { callData } = useContext(CallManagerContext);
+
+ if (!callData) {
+ return <>{children}</>;
+ }
+
+ return (
+ <WebRtcProvider>
+ <CallProvider>{children}</CallProvider>
+ </WebRtcProvider>
+ );
+};
diff --git a/client/src/contexts/CallProvider.tsx b/client/src/contexts/CallProvider.tsx
index 88951a9..b20d0e7 100644
--- a/client/src/contexts/CallProvider.tsx
+++ b/client/src/contexts/CallProvider.tsx
@@ -17,15 +17,13 @@
*/
import { CallAction, CallBegin, WebSocketMessageType } from 'jami-web-common';
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
-import { Navigate, useNavigate } from 'react-router-dom';
+import { Navigate } from 'react-router-dom';
import LoadingPage from '../components/Loading';
-import { useUrlParams } from '../hooks/useUrlParams';
-import CallPermissionDenied from '../pages/CallPermissionDenied';
-import { CallRouteParams } from '../router';
+import { Conversation } from '../models/conversation';
import { callTimeoutMs } from '../utils/constants';
import { AsyncSetState, SetState, WithChildren } from '../utils/utils';
-import { useConversationContext } from './ConversationProvider';
+import { CallManagerContext } from './CallManagerProvider';
import { MediaDevicesInfo, MediaInputKind, WebRtcContext } from './WebRtcProvider';
import { IWebSocketContext, WebSocketContext } from './WebSocketProvider';
@@ -113,21 +111,30 @@
export default ({ children }: WithChildren) => {
const webSocket = useContext(WebSocketContext);
+ const { callConversation, callData } = useContext(CallManagerContext);
- if (!webSocket) {
+ if (!webSocket || !callConversation || !callData?.conversationId) {
return <LoadingPage />;
}
- return <CallProvider webSocket={webSocket}>{children}</CallProvider>;
+ return (
+ <CallProvider webSocket={webSocket} conversation={callConversation} conversationId={callData?.conversationId}>
+ {children}
+ </CallProvider>
+ );
};
const CallProvider = ({
children,
+ conversation,
+ conversationId,
webSocket,
}: WithChildren & {
webSocket: IWebSocketContext;
+ conversation: Conversation;
+ conversationId: string;
}) => {
- const { state: routeState } = useUrlParams<CallRouteParams>();
+ const { callData, exitCall } = useContext(CallManagerContext);
const {
localStream,
updateScreenShare,
@@ -137,8 +144,6 @@
getMediaDevices,
updateLocalStream,
} = useContext(WebRtcContext);
- const { conversationId, conversation } = useConversationContext();
- const navigate = useNavigate();
const [mediaDevices, setMediaDevices] = useState(defaultCallContext.mediaDevices);
const [audioInputDeviceId, setAudioInputDeviceId] = useState<string>();
@@ -149,8 +154,8 @@
const [videoStatus, setVideoStatus] = useState(VideoStatus.Off);
const [isChatShown, setIsChatShown] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
- const [callStatus, setCallStatus] = useState(routeState?.callStatus);
- const [callRole] = useState(routeState?.role);
+ const [callStatus, setCallStatus] = useState(CallStatus.Default);
+ const [callRole] = useState(callData?.role);
const [callStartTime, setCallStartTime] = useState<number | undefined>(undefined);
// TODO: This logic will have to change to support multiple people in a call. Could we move this logic to the server?
@@ -243,7 +248,7 @@
useEffect(() => {
if (callRole === 'caller' && callStatus === CallStatus.Default) {
- const withVideoOn = routeState?.isVideoOn ?? false;
+ const withVideoOn = callData?.withVideoOn ?? false;
setCallStatus(CallStatus.Loading);
updateLocalStream()
.then(() => {
@@ -263,7 +268,7 @@
setCallStatus(CallStatus.PermissionsDenied);
});
}
- }, [webSocket, updateLocalStream, callRole, callStatus, contactUri, conversationId, routeState]);
+ }, [webSocket, updateLocalStream, callRole, callStatus, contactUri, conversationId, callData]);
const acceptCall = useCallback(
(withVideoOn: boolean) => {
@@ -319,9 +324,9 @@
console.info('Sending CallEnd', callEnd);
closeConnection();
webSocket.send(WebSocketMessageType.CallEnd, callEnd);
- navigate(`/conversation/${conversationId}`);
+ exitCall();
// TODO: write in chat that the call ended
- }, [webSocket, contactUri, conversationId, closeConnection, navigate]);
+ }, [webSocket, contactUri, conversationId, closeConnection, exitCall]);
useEffect(() => {
const callEndListener = (data: CallAction) => {
@@ -332,7 +337,7 @@
}
closeConnection();
- navigate(`/conversation/${conversationId}`);
+ exitCall();
// TODO: write in chat that the call ended
};
@@ -340,7 +345,7 @@
return () => {
webSocket.unbind(WebSocketMessageType.CallEnd, callEndListener);
};
- }, [webSocket, navigate, conversationId, closeConnection]);
+ }, [webSocket, exitCall, conversationId, closeConnection]);
useEffect(() => {
if (
@@ -401,14 +406,7 @@
};
}, [updateLocalStream, audioInputDeviceId, audioOutputDeviceId, videoDeviceId]);
- useEffect(() => {
- navigate('.', {
- replace: true,
- state: {},
- });
- }, [navigate]);
-
- if (!callRole || callStatus === undefined) {
+ if (!callData || !callRole) {
console.error('Invalid route. Redirecting...');
return <Navigate to={'/'} />;
}
@@ -433,7 +431,7 @@
endCall,
}}
>
- {callStatus === CallStatus.PermissionsDenied ? <CallPermissionDenied /> : children}
+ {children}
</CallContext.Provider>
);
};
diff --git a/client/src/contexts/ConversationProvider.tsx b/client/src/contexts/ConversationProvider.tsx
index 8d5f7b5..e2aa4bc 100644
--- a/client/src/contexts/ConversationProvider.tsx
+++ b/client/src/contexts/ConversationProvider.tsx
@@ -16,7 +16,7 @@
* <https://www.gnu.org/licenses/>.
*/
import { ConversationView, WebSocketMessageType } from 'jami-web-common';
-import { useContext, useEffect, useState } from 'react';
+import { useContext, useEffect } from 'react';
import LoadingPage from '../components/Loading';
import { createOptionalContext } from '../hooks/createOptionalContext';
@@ -43,26 +43,8 @@
} = useUrlParams<ConversationRouteParams>();
const { accountId } = useAuthContext();
const webSocket = useContext(WebSocketContext);
- const [isLoading, setIsLoading] = useState(false);
- const [isError, setIsError] = useState(false);
- const [conversation, setConversation] = useState<Conversation | undefined>();
- const conversationQuery = useConversationQuery(conversationId!);
-
- useEffect(() => {
- if (conversationQuery.isSuccess) {
- const conversation = Conversation.fromInterface(conversationQuery.data);
- setConversation(conversation);
- }
- }, [accountId, conversationQuery.isSuccess, conversationQuery.data]);
-
- useEffect(() => {
- setIsLoading(conversationQuery.isLoading);
- }, [conversationQuery.isLoading]);
-
- useEffect(() => {
- setIsError(conversationQuery.isError);
- }, [conversationQuery.isError]);
+ const { conversation, isLoading, isError } = useConversationQuery(conversationId!);
useEffect(() => {
if (!conversation || !conversationId || !webSocket) {
diff --git a/client/src/contexts/WebRtcProvider.tsx b/client/src/contexts/WebRtcProvider.tsx
index b7733bc..bb99717 100644
--- a/client/src/contexts/WebRtcProvider.tsx
+++ b/client/src/contexts/WebRtcProvider.tsx
@@ -20,9 +20,10 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import LoadingPage from '../components/Loading';
+import { Conversation } from '../models/conversation';
import { WithChildren } from '../utils/utils';
import { useAuthContext } from './AuthProvider';
-import { useConversationContext } from './ConversationProvider';
+import { CallManagerContext } from './CallManagerProvider';
import { IWebSocketContext, WebSocketContext } from './WebSocketProvider';
export type MediaDevicesInfo = Record<MediaDeviceKind, MediaDeviceInfo[]>;
@@ -61,6 +62,7 @@
const { account } = useAuthContext();
const [webRtcConnection, setWebRtcConnection] = useState<RTCPeerConnection | undefined>();
const webSocket = useContext(WebSocketContext);
+ const { callConversation, callData } = useContext(CallManagerContext);
useEffect(() => {
if (!webRtcConnection && account) {
@@ -84,12 +86,17 @@
}
}, [account, webRtcConnection]);
- if (!webRtcConnection || !webSocket) {
+ if (!webRtcConnection || !webSocket || !callConversation || !callData?.conversationId) {
return <LoadingPage />;
}
return (
- <WebRtcProvider webRtcConnection={webRtcConnection} webSocket={webSocket}>
+ <WebRtcProvider
+ webRtcConnection={webRtcConnection}
+ webSocket={webSocket}
+ conversation={callConversation}
+ conversationId={callData.conversationId}
+ >
{children}
</WebRtcProvider>
);
@@ -97,13 +104,16 @@
const WebRtcProvider = ({
children,
+ conversation,
+ conversationId,
webRtcConnection,
webSocket,
}: WithChildren & {
webRtcConnection: RTCPeerConnection;
webSocket: IWebSocketContext;
+ conversation: Conversation;
+ conversationId: string;
}) => {
- const { conversation, conversationId } = useConversationContext();
const [localStream, setLocalStream] = useState<MediaStream>();
const [screenShareLocalStream, setScreenShareLocalStream] = useState<MediaStream>();
const [remoteStreams, setRemoteStreams] = useState<readonly MediaStream[]>();