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/components/ConversationListItem.tsx b/client/src/components/ConversationListItem.tsx
index a1e6693..48e9004 100644
--- a/client/src/components/ConversationListItem.tsx
+++ b/client/src/components/ConversationListItem.tsx
@@ -23,6 +23,7 @@
import { useNavigate, useParams } from 'react-router-dom';
import { useAuthContext } from '../contexts/AuthProvider';
+import { useStartCall } from '../hooks/useStartCall';
import { setRefreshFromSlice } from '../redux/appSlice';
import { useAppDispatch } from '../redux/hooks';
import ContextMenu, { ContextMenuHandler, useContextMenuHandler } from './ContextMenu';
@@ -51,7 +52,12 @@
const isSelected = conversation.getDisplayUri() === pathId;
const navigate = useNavigate();
const userId = conversation?.getFirstMember()?.contact.getUri();
- const uri = conversation.getId() ? `/conversation/${conversation.getId()}` : `/conversation/add-contact/${userId}`;
+
+ // TODO: Improve this component. conversationId should never be undefined.
+ // (https://git.jami.net/savoirfairelinux/jami-web/-/issues/171)
+ const uri = conversation.getId()
+ ? `/conversation/${conversation.getId()}`
+ : `/conversation/add-contact?newContactId=${userId}`;
return (
<Box onContextMenu={contextMenuHandler.handleAnchorPosition}>
<ConversationMenu
@@ -90,6 +96,8 @@
const navigate = useNavigate();
+ const startCall = useStartCall();
+
const getContactDetails = useCallback(async () => {
const controller = new AbortController();
try {
@@ -102,6 +110,8 @@
}
}, [axiosInstance, userId]);
+ const conversationId = conversation.getId();
+
const menuOptions: PopoverListItemData[] = useMemo(
() => [
{
@@ -115,14 +125,20 @@
label: t('conversation_start_audiocall'),
Icon: AudioCallIcon,
onClick: () => {
- navigate(`/account/call/${conversation.getId()}`);
+ if (conversationId) {
+ startCall(conversationId);
+ }
},
},
{
label: t('conversation_start_videocall'),
Icon: VideoCallIcon,
onClick: () => {
- navigate(`call/${conversation.getId()}?video=true`);
+ if (conversationId) {
+ startCall(conversationId, {
+ isVideoOn: true,
+ });
+ }
},
},
...(isSelected
@@ -160,7 +176,6 @@
},
],
[
- conversation,
navigate,
uri,
isSelected,
@@ -169,6 +184,8 @@
blockContactDialogHandler,
removeContactDialogHandler,
t,
+ startCall,
+ conversationId,
]
);
diff --git a/client/src/components/ConversationView.tsx b/client/src/components/ConversationView.tsx
index c638295..52215e8 100644
--- a/client/src/components/ConversationView.tsx
+++ b/client/src/components/ConversationView.tsx
@@ -22,6 +22,7 @@
import { useAuthContext } from '../contexts/AuthProvider';
import { ConversationContext } from '../contexts/ConversationProvider';
+import { useStartCall } from '../hooks/useStartCall';
import ChatInterface from '../pages/ChatInterface';
import { translateEnumeration, TranslateEnumerationOptions } from '../utils/translations';
import { AddParticipantButton, ShowOptionsMenuButton, StartAudioCallButton, StartVideoCallButton } from './Button';
@@ -57,7 +58,7 @@
const ConversationHeader = ({ account, members, adminTitle }: ConversationHeaderProps) => {
const { t } = useTranslation();
- const { beginCall } = useContext(ConversationContext);
+ const { conversationId } = useContext(ConversationContext);
const title = useMemo(() => {
if (adminTitle) {
@@ -82,6 +83,8 @@
return translateEnumeration<ConversationMember>(members, options);
}, [account, members, adminTitle, t]);
+ const startCall = useStartCall();
+
return (
<Stack direction="row" padding="16px" overflow="hidden">
<Stack flex={1} justifyContent="center" whiteSpace="nowrap" overflow="hidden">
@@ -90,8 +93,14 @@
</Typography>
</Stack>
<Stack direction="row" spacing="20px">
- <StartAudioCallButton onClick={() => beginCall()} />
- <StartVideoCallButton onClick={() => beginCall()} />
+ <StartAudioCallButton onClick={() => startCall(conversationId)} />
+ <StartVideoCallButton
+ onClick={() =>
+ startCall(conversationId, {
+ isVideoOn: true,
+ })
+ }
+ />
<AddParticipantButton />
<ShowOptionsMenuButton />
</Stack>
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}
diff --git a/client/src/hooks/useStartCall.ts b/client/src/hooks/useStartCall.ts
new file mode 100644
index 0000000..2df6a01
--- /dev/null
+++ b/client/src/hooks/useStartCall.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 { useCallback } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import { CallStatus } from '../contexts/CallProvider';
+import { CallRouteParams } from '../router';
+
+export const useStartCall = () => {
+ const navigate = useNavigate();
+
+ return useCallback(
+ (conversationId: string, state?: Partial<CallRouteParams['state']>) => {
+ navigate(`/conversation/${conversationId}/call?role=caller`, {
+ state: {
+ callStatus: CallStatus.Default,
+ ...state,
+ },
+ });
+ },
+ [navigate]
+ );
+};
diff --git a/client/src/hooks/useUrlParams.ts b/client/src/hooks/useUrlParams.ts
index f21b60b..243a8c5 100644
--- a/client/src/hooks/useUrlParams.ts
+++ b/client/src/hooks/useUrlParams.ts
@@ -18,13 +18,14 @@
import { useMemo } from 'react';
import { useLocation, useParams } from 'react-router-dom';
-export type RouteParams<U = Record<string, string>, Q = Record<string, string>> = {
- urlParams: U;
- queryParams: Q;
+export type RouteParams<UrlParams = Record<string, string>, QueryParams = Record<string, string>, State = any> = {
+ urlParams: UrlParams;
+ queryParams: QueryParams;
+ state?: State;
};
export const useUrlParams = <T extends RouteParams>() => {
- const { search } = useLocation();
+ const { search, state } = useLocation();
const urlParams = useParams() as T['urlParams'];
return useMemo(() => {
@@ -32,6 +33,7 @@
return {
queryParams,
urlParams,
+ state: state as T['state'],
};
- }, [search, urlParams]);
+ }, [search, urlParams, state]);
};
diff --git a/client/src/managers/NotificationManager.tsx b/client/src/managers/NotificationManager.tsx
index 4154c33..dacc3c1 100644
--- a/client/src/managers/NotificationManager.tsx
+++ b/client/src/managers/NotificationManager.tsx
@@ -20,6 +20,7 @@
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';
@@ -38,7 +39,11 @@
const callBeginListener = (data: CallAction) => {
console.info('Received event on CallBegin', data);
- navigate(`/conversation/${data.conversationId}/call?role=receiver`);
+ navigate(`/conversation/${data.conversationId}/call?role=receiver`, {
+ state: {
+ callStatus: CallStatus.Ringing,
+ },
+ });
};
webSocket.bind(WebSocketMessageType.CallBegin, callBeginListener);
diff --git a/client/src/pages/CallInterface.tsx b/client/src/pages/CallInterface.tsx
index c5c0922..b6266ac 100644
--- a/client/src/pages/CallInterface.tsx
+++ b/client/src/pages/CallInterface.tsx
@@ -54,7 +54,7 @@
return (
<CallPending
pending={callRole}
- caller={callStatus === CallStatus.Ringing ? 'calling' : 'connecting'}
+ caller={callStatus === CallStatus.Connecting ? 'connecting' : 'calling'}
medium="audio"
/>
);
diff --git a/client/src/pages/JamiRegistration.tsx b/client/src/pages/JamiRegistration.tsx
index 089c5d8..619b230 100644
--- a/client/src/pages/JamiRegistration.tsx
+++ b/client/src/pages/JamiRegistration.tsx
@@ -126,7 +126,7 @@
if (canCreate) {
setIsCreatingUser(true);
- createAccount();
+ await createAccount();
} else {
if (usernameError || username.length === 0) {
setUsernameStatus('registration_failed');
diff --git a/client/src/pages/Messenger.tsx b/client/src/pages/Messenger.tsx
index c2bc1e7..7f10e41 100644
--- a/client/src/pages/Messenger.tsx
+++ b/client/src/pages/Messenger.tsx
@@ -43,9 +43,11 @@
const [searchQuery, setSearchQuery] = useState('');
const [searchResult, setSearchResults] = useState<Conversation | undefined>(undefined);
- const {
- urlParams: { contactId },
- } = useUrlParams<AddContactRouteParams>();
+ const { urlParams } = useUrlParams<AddContactRouteParams>();
+
+ // TODO: Rework the contact adding logic so that adding a contact does not make the current conversationId undefined.
+ // The newContactId should not come from the route, but from a state.
+ const newContactId = urlParams?.contactId;
const accountId = account.getId();
@@ -101,7 +103,7 @@
<Stack flexGrow={0} flexShrink={0} overflow="auto">
<Header />
<NewContactForm onChange={setSearchQuery} />
- {contactId && <AddContactPage contactId={contactId} />}
+ {newContactId && <AddContactPage contactId={newContactId} />}
{conversations ? (
<ConversationList search={searchResult} conversations={conversations} accountId={accountId} />
) : (
diff --git a/client/src/router.tsx b/client/src/router.tsx
index 8e53188..5b6b71d 100644
--- a/client/src/router.tsx
+++ b/client/src/router.tsx
@@ -21,7 +21,7 @@
import ContactList from './components/ContactList';
import ConversationView from './components/ConversationView';
import AuthProvider from './contexts/AuthProvider';
-import CallProvider, { CallRole } from './contexts/CallProvider';
+import CallProvider, { CallRole, CallStatus } from './contexts/CallProvider';
import ConversationProvider from './contexts/ConversationProvider';
import WebRtcProvider from './contexts/WebRtcProvider';
import WebSocketProvider from './contexts/WebSocketProvider';
@@ -37,12 +37,17 @@
import { ThemeDemonstrator } from './themes/ThemeDemonstrator';
export type ConversationRouteParams = RouteParams<{ conversationId: string }, Record<string, never>>;
+
export type AddContactRouteParams = RouteParams<{ contactId: string }, Record<string, never>>;
-/**
- * Route parameters for the call routes.
- */
-export type CallRouteParams = RouteParams<{ conversationId: string }, { role?: CallRole }>;
+export type CallRouteParams = RouteParams<
+ { conversationId?: string },
+ { role?: CallRole },
+ {
+ isVideoOn?: boolean;
+ callStatus: CallStatus;
+ }
+>;
export const router = createBrowserRouter(
createRoutesFromElements(
@@ -64,7 +69,10 @@
>
<Route index element={<Messenger />} />
<Route path="conversation" element={<Messenger />}>
- <Route path="add-contact/:contactId" />
+ {/* TODO: Remove this route. Adding a contact should not change the route, we should instead use an internal
+ state in the Messenger component
+ */}
+ <Route path="add-contact" element={<div></div>} />
<Route
path=":conversationId"
element={