Add routing for call page

Enable buttons to start a call.
Improve ConversationListItem context menu layout.
Move calling buttons from `Button.tsx` to `CallButtons.tsx`.
Add CallProvider

GitLab: #78
Change-Id: I921aa11383bf39fae18e59b01afb00dc66b0d5e6
diff --git a/client/src/components/Button.tsx b/client/src/components/Button.tsx
index 2325ecb..9264355 100644
--- a/client/src/components/Button.tsx
+++ b/client/src/components/Button.tsx
@@ -15,7 +15,7 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { QuestionMark } from '@mui/icons-material';
+import { QuestionMark, RadioButtonChecked, RadioButtonUnchecked } from '@mui/icons-material';
 import { Box, ClickAwayListener, IconButton, IconButtonProps, Popper, SvgIconProps } from '@mui/material';
 import { styled } from '@mui/material/styles';
 import EmojiPicker, { IEmojiData } from 'emoji-picker-react';
@@ -26,32 +26,22 @@
   Arrow3Icon,
   ArrowIcon,
   AudioCallIcon,
-  CallEndIcon,
   CameraIcon,
   CameraInBubbleIcon,
   CancelIcon,
-  ChatBubbleIcon,
   CrossedEyeIcon,
   CrossIcon,
   EmojiIcon,
-  ExtensionIcon,
   EyeIcon,
   FolderIcon,
-  FullscreenIcon,
-  GroupAddIcon,
   InfoIcon,
   ListIcon,
-  MicroIcon,
   MicroInBubbleIcon,
   PaperClipIcon,
   PenIcon,
   PeopleWithPlusSignIcon,
-  RecordingIcon,
   SaltireIcon,
-  ScreenShareIcon,
   VideoCallIcon,
-  VideoCameraIcon,
-  VolumeIcon,
 } from './SvgIcon';
 import CustomTooltip from './Tooltip';
 
@@ -157,81 +147,27 @@
   },
 }));
 
-export const CallingChatButton = (props: IconButtonProps) => {
-  return (
-    <IconButton {...props} aria-label="chat" sx={{ color: 'white' }}>
-      <ChatBubbleIcon />
-    </IconButton>
-  );
+export type ToggleIconButtonProps = IconButtonProps & {
+  selected: boolean;
+  toggle: () => void;
+  IconOn?: ComponentType<SvgIconProps>;
+  IconOff?: ComponentType<SvgIconProps>;
 };
-
-export const CallingEndButton = (props: IconButtonProps) => {
+export const ToggleIconButton = ({
+  IconOn = RadioButtonChecked,
+  IconOff = RadioButtonUnchecked,
+  selected,
+  toggle,
+  ...props
+}: ToggleIconButtonProps) => {
   return (
-    <IconButton {...props} aria-label="end call" sx={{ color: 'white', backgroundColor: 'red' }}>
-      <CallEndIcon />
-    </IconButton>
-  );
-};
-
-export const CallingExtensionButton = (props: IconButtonProps) => {
-  return (
-    <IconButton {...props} aria-label="extensions" sx={{ color: 'white' }}>
-      <ExtensionIcon />
-    </IconButton>
-  );
-};
-
-export const CallingFullscreenButton = (props: IconButtonProps) => {
-  return (
-    <IconButton {...props} aria-label="fullscreen" sx={{ color: 'white' }}>
-      <FullscreenIcon />
-    </IconButton>
-  );
-};
-
-export const CallingGroupButton = (props: IconButtonProps) => {
-  return (
-    <IconButton {...props} aria-label="group options" sx={{ color: 'white' }}>
-      <GroupAddIcon />
-    </IconButton>
-  );
-};
-
-export const CallingMicButton = (props: IconButtonProps) => {
-  return (
-    <IconButton {...props} aria-label="microphone options" sx={{ color: 'white' }}>
-      <MicroIcon />
-    </IconButton>
-  );
-};
-
-export const CallingRecordButton = (props: IconButtonProps) => {
-  return (
-    <IconButton {...props} aria-label="recording options" sx={{ color: 'white' }}>
-      <RecordingIcon />
-    </IconButton>
-  );
-};
-
-export const CallingScreenShareButton = (props: IconButtonProps) => {
-  return (
-    <IconButton {...props} aria-label="screen share" sx={{ color: 'white' }}>
-      <ScreenShareIcon />
-    </IconButton>
-  );
-};
-
-export const CallingVideoCameraButton = (props: IconButtonProps) => {
-  return (
-    <IconButton {...props} aria-label="camera options" sx={{ color: 'white' }}>
-      <VideoCameraIcon />
-    </IconButton>
-  );
-};
-export const CallingVolumeButton = (props: IconButtonProps) => {
-  return (
-    <IconButton {...props} aria-label="volume options" sx={{ color: 'white' }}>
-      <VolumeIcon />
+    <IconButton
+      {...props}
+      onClick={() => {
+        toggle();
+      }}
+    >
+      {selected ? <IconOn /> : <IconOff />}
     </IconButton>
   );
 };
diff --git a/client/src/components/CallButtons.tsx b/client/src/components/CallButtons.tsx
new file mode 100644
index 0000000..6cc3cb0
--- /dev/null
+++ b/client/src/components/CallButtons.tsx
@@ -0,0 +1,132 @@
+/*
+ * 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 { IconButton, IconButtonProps } from '@mui/material';
+import { styled } from '@mui/material/styles';
+import React, { useContext } from 'react';
+
+import { CallContext } from '../contexts/CallProvider';
+import { ToggleIconButton, ToggleIconButtonProps } from './Button';
+import {
+  CallEndIcon,
+  ChatBubbleIcon,
+  ExtensionIcon,
+  FullscreenIcon,
+  GroupAddIcon,
+  MicroIcon,
+  MicroOffIcon,
+  RecordingIcon,
+  ScreenShareIcon,
+  VideoCameraIcon,
+  VideoCameraOffIcon,
+  VolumeIcon,
+} from './SvgIcon';
+
+export const CallingChatButton = (props: IconButtonProps) => {
+  return (
+    <IconButton {...props} aria-label="chat" sx={{ color: 'white' }}>
+      <ChatBubbleIcon />
+    </IconButton>
+  );
+};
+export const CallingEndButton = styled((props: IconButtonProps) => {
+  return (
+    <IconButton {...props} aria-label="end call">
+      <CallEndIcon />
+    </IconButton>
+  );
+})(() => ({
+  color: 'white',
+  backgroundColor: 'red',
+  '&:hover': {
+    backgroundColor: 'darkred',
+  },
+}));
+export const CallingExtensionButton = (props: IconButtonProps) => {
+  return (
+    <IconButton {...props} aria-label="extensions" sx={{ color: 'white' }}>
+      <ExtensionIcon />
+    </IconButton>
+  );
+};
+export const CallingFullscreenButton = (props: IconButtonProps) => {
+  return (
+    <IconButton {...props} aria-label="fullscreen" sx={{ color: 'white' }}>
+      <FullscreenIcon />
+    </IconButton>
+  );
+};
+export const CallingGroupButton = (props: IconButtonProps) => {
+  return (
+    <IconButton {...props} aria-label="group options" sx={{ color: 'white' }}>
+      <GroupAddIcon />
+    </IconButton>
+  );
+};
+export const CallingRecordButton = (props: IconButtonProps) => {
+  return (
+    <IconButton {...props} aria-label="recording options" sx={{ color: 'white' }}>
+      <RecordingIcon />
+    </IconButton>
+  );
+};
+export const CallingScreenShareButton = (props: IconButtonProps) => {
+  return (
+    <IconButton {...props} aria-label="screen share" sx={{ color: 'white' }}>
+      <ScreenShareIcon />
+    </IconButton>
+  );
+};
+export const CallingVolumeButton = (props: IconButtonProps) => {
+  return (
+    <IconButton {...props} aria-label="volume options" sx={{ color: 'white' }}>
+      <VolumeIcon />
+    </IconButton>
+  );
+};
+
+export const CallingMicButton = (props: Partial<ToggleIconButtonProps>) => {
+  const { micOn, setMicOn } = useContext(CallContext);
+
+  return (
+    <ToggleIconButton
+      aria-label="microphone options"
+      sx={{ color: 'white' }}
+      IconOn={MicroIcon}
+      IconOff={MicroOffIcon}
+      selected={micOn}
+      toggle={() => setMicOn((s) => !s)}
+      {...props}
+    />
+  );
+};
+
+export const CallingVideoCameraButton = (props: Partial<ToggleIconButtonProps>) => {
+  const { camOn, setCamOn } = useContext(CallContext);
+
+  return (
+    <ToggleIconButton
+      aria-label="camera options"
+      sx={{ color: 'white' }}
+      IconOn={VideoCameraIcon}
+      IconOff={VideoCameraOffIcon}
+      selected={camOn}
+      toggle={() => setCamOn((s) => !s)}
+      {...props}
+    />
+  );
+};
diff --git a/client/src/components/ConversationListItem.tsx b/client/src/components/ConversationListItem.tsx
index bab36b0..9dab62e 100644
--- a/client/src/components/ConversationListItem.tsx
+++ b/client/src/components/ConversationListItem.tsx
@@ -15,10 +15,21 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { Box, ListItem, ListItemAvatar, ListItemText, Stack, StackProps, Typography } from '@mui/material';
+import {
+  Box,
+  ListItem,
+  ListItemAvatar,
+  ListItemIcon,
+  ListItemText,
+  Menu,
+  MenuItem,
+  Stack,
+  Typography,
+} from '@mui/material';
 import { Conversation } from 'jami-web-common';
 import { QRCodeCanvas } from 'qrcode.react';
 import { MouseEvent, useState } from 'react';
+import { Trans } from 'react-i18next';
 import Modal from 'react-modal';
 import { useNavigate, useParams } from 'react-router-dom';
 
@@ -26,32 +37,8 @@
 import { setRefreshFromSlice } from '../redux/appSlice';
 import { useAppDispatch } from '../redux/hooks';
 import ConversationAvatar from './ConversationAvatar';
-import { RemoveContactIcon, VideoCallIcon } from './SvgIcon';
-import { AudioCallIcon, BlockContactIcon, ContactDetailsIcon, CrossIcon, MessageIcon } from './SvgIcon';
-
-const customStyles: Modal.Styles = {
-  content: {
-    // right: "auto",
-    // bottom: "auto",
-    // // marginRight: "-50%",
-    // transform: "translate(-50%, -50%)",
-    // padding: "16px"
-
-    // top: "1364px",
-    left: '94px',
-    width: '180px',
-    height: '262px',
-    background: '#FFFFFF 0% 0% no-repeat padding-box',
-    boxShadow: '3px 3px 7px #00000029',
-    borderRadius: '5px 20px 20px 20px',
-    opacity: '1',
-
-    textAlign: 'left',
-    font: 'normal normal normal 12px/26px Ubuntu',
-    letterSpacing: '0px',
-    color: '#000000',
-  },
-};
+import { CancelIcon, RemoveContactIcon, VideoCallIcon } from './SvgIcon';
+import { AudioCallIcon, BlockContactIcon, ContactDetailsIcon, MessageIcon } from './SvgIcon';
 
 const cancelStyles: Modal.Styles = {
   content: {
@@ -87,18 +74,6 @@
   },
 };
 
-const stackStyles: StackProps = {
-  flexDirection: 'row',
-  marginBottom: '4px',
-  spacing: '40px',
-  flex: 1,
-  alignItems: 'center',
-};
-
-const iconTextStyle = {
-  marginRight: '10px',
-};
-
 const iconColor = '#005699';
 
 type ConversationListItemProps = {
@@ -113,21 +88,21 @@
   const isSelected = conversation.getDisplayUri() === pathId;
   const navigate = useNavigate();
 
-  const [modalIsOpen, setIsOpen] = useState(false);
+  const [menuAnchorEl, setMenuAnchorEl] = useState<HTMLElement | null>(null);
   const [modalDetailsIsOpen, setModalDetailsIsOpen] = useState(false);
   const [modalDeleteIsOpen, setModalDeleteIsOpen] = useState(false);
   const [blockOrRemove, setBlockOrRemove] = useState(true);
   const [userId, setUserId] = useState(conversation?.getFirstMember()?.contact.getUri());
-  const [isSwarm, setIsSwarm] = useState('true');
+  const [isSwarm, setIsSwarm] = useState(true);
 
-  const openModal = (e: MouseEvent<HTMLDivElement>) => {
+  const openMenu = (e: MouseEvent<HTMLDivElement>) => {
     e.preventDefault();
     console.log(e);
-    setIsOpen(true);
+    setMenuAnchorEl(e.currentTarget);
   };
   const openModalDetails = () => setModalDetailsIsOpen(true);
   const openModalDelete = () => setModalDeleteIsOpen(true);
-  const closeModal = () => setIsOpen(false);
+  const closeModal = () => setMenuAnchorEl(null);
   const closeModalDetails = () => setModalDetailsIsOpen(false);
   const closeModalDelete = () => setModalDeleteIsOpen(false);
 
@@ -170,97 +145,110 @@
 
   const uri = conversation.getId() ? `conversation/${conversation.getId()}` : `addContact/${userId}`;
   return (
-    <div onContextMenu={openModal}>
+    <div onContextMenu={openMenu}>
       <div>
-        <Modal
-          isOpen={modalIsOpen}
-          //   onAfterOpen={afterOpenModal}
-          onRequestClose={closeModal}
-          style={customStyles}
-          contentLabel="Example Modal"
-        >
-          <Stack
+        <Menu open={!!menuAnchorEl} onClose={closeModal} anchorEl={menuAnchorEl}>
+          <MenuItem
             onClick={() => {
               navigate(`/account/${conversation.getAccountId()}/${uri}`);
               closeModal();
             }}
-            {...stackStyles}
           >
-            <div style={{ ...iconTextStyle }}>
+            <ListItemIcon>
               <MessageIcon style={{ color: iconColor }} />
-            </div>
-            Message
-          </Stack>
-          <Stack {...stackStyles}>
-            <div style={{ ...iconTextStyle }}>
-              <AudioCallIcon style={{ color: iconColor }} />
-            </div>
-            Démarrer appel audio
-          </Stack>
-
-          <Stack {...stackStyles}>
-            <div style={{ ...iconTextStyle }}>
-              <VideoCallIcon style={{ color: iconColor }} />
-            </div>
-            Démarrer appel vidéo
-          </Stack>
-
-          <Stack
-            {...stackStyles}
+            </ListItemIcon>
+            <ListItemText>
+              <Trans i18nKey="conversation_message" />
+            </ListItemText>
+          </MenuItem>
+          <MenuItem
             onClick={() => {
-              navigate(`/account/${conversation.getAccountId()}/`);
-              closeModal();
+              navigate(`/account/${conversation.getAccountId()}/call/${conversation.getId()}`);
             }}
           >
-            <div style={{ ...iconTextStyle }}>
-              <CrossIcon style={{ color: iconColor }} />
-            </div>
-            Fermer la conversation
-          </Stack>
+            <ListItemIcon>
+              <AudioCallIcon style={{ color: iconColor }} />
+            </ListItemIcon>
+            <ListItemText>
+              <Trans i18nKey="conversation_start_audiocall" />
+            </ListItemText>
+          </MenuItem>
 
-          <Stack
+          <MenuItem
+            onClick={() => {
+              navigate(`/account/${conversation.getAccountId()}/call/${conversation.getId()}?video=true`);
+            }}
+          >
+            <ListItemIcon>
+              <VideoCallIcon style={{ color: iconColor }} />
+            </ListItemIcon>
+            <ListItemText>
+              <Trans i18nKey="conversation_start_videocall" />
+            </ListItemText>
+          </MenuItem>
+
+          {isSelected && (
+            <MenuItem
+              onClick={() => {
+                navigate(`/account/${conversation.getAccountId()}/`);
+                closeModal();
+              }}
+            >
+              <ListItemIcon>
+                <CancelIcon style={{ color: iconColor }} />
+              </ListItemIcon>
+              <ListItemText>
+                <Trans i18nKey="conversation_close" />
+              </ListItemText>
+            </MenuItem>
+          )}
+
+          <MenuItem
             onClick={() => {
               console.log('open details contact for: ');
               closeModal();
               openModalDetails();
               getContactDetails();
             }}
-            {...stackStyles}
           >
-            <div style={{ ...iconTextStyle }}>
+            <ListItemIcon>
               <ContactDetailsIcon style={{ color: iconColor }} />
-            </div>
-            Détails de la conversation
-          </Stack>
+            </ListItemIcon>
+            <ListItemText>
+              <Trans i18nKey="conversation_details" />
+            </ListItemText>
+          </MenuItem>
 
-          <Stack
+          <MenuItem
             onClick={() => {
               setBlockOrRemove(true);
               closeModal();
               openModalDelete();
             }}
-            {...stackStyles}
           >
-            <div style={{ ...iconTextStyle }}>
+            <ListItemIcon>
               <BlockContactIcon style={{ color: iconColor }} />
-            </div>
-            Bloquer le contact
-          </Stack>
+            </ListItemIcon>
+            <ListItemText>
+              <Trans i18nKey="conversation_block_contact" />
+            </ListItemText>
+          </MenuItem>
 
-          <Stack
+          <MenuItem
             onClick={() => {
               setBlockOrRemove(false);
               closeModal();
               openModalDelete();
             }}
-            {...stackStyles}
           >
-            <div style={{ ...iconTextStyle }}>
+            <ListItemIcon>
               <RemoveContactIcon style={{ color: iconColor }} />
-            </div>
-            Supprimer contact
-          </Stack>
-        </Modal>
+            </ListItemIcon>
+            <ListItemText>
+              <Trans i18nKey="conversation_delete_contact" />
+            </ListItemText>
+          </MenuItem>
+        </Menu>
       </div>
 
       <div>
@@ -338,7 +326,7 @@
               </div>
 
               <Typography variant="caption">
-                <div style={{ fontWeight: 'bold' }}> {isSwarm}</div>
+                <div style={{ fontWeight: 'bold' }}> {String(isSwarm)}</div>
               </Typography>
             </Stack>
           </Stack>
diff --git a/client/src/components/ConversationView.tsx b/client/src/components/ConversationView.tsx
index a6c0d35..d4b8f90 100644
--- a/client/src/components/ConversationView.tsx
+++ b/client/src/components/ConversationView.tsx
@@ -19,6 +19,7 @@
 import { Account, Conversation, ConversationMember, Message } from 'jami-web-common';
 import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router';
 
 import { SocketContext } from '../contexts/Socket';
 import { useAccountQuery } from '../services/Account';
@@ -105,6 +106,7 @@
           account={account}
           members={conversation.getMembers()}
           adminTitle={conversation.infos.title as string}
+          conversationId={conversationId}
         />
       </Stack>
       <Divider
@@ -130,12 +132,14 @@
 
 type ConversationHeaderProps = {
   account: Account;
+  conversationId: string;
   members: ConversationMember[];
   adminTitle: string | undefined;
 };
 
-const ConversationHeader = ({ account, members, adminTitle }: ConversationHeaderProps) => {
+const ConversationHeader = ({ account, members, adminTitle, conversationId }: ConversationHeaderProps) => {
   const { t } = useTranslation();
+  const navigate = useNavigate();
 
   const title = useMemo(() => {
     if (adminTitle) {
@@ -160,6 +164,14 @@
     return translateEnumeration<ConversationMember>(members, options);
   }, [account, members, adminTitle, t]);
 
+  const startCall = (withVideo = false) => {
+    let url = `/account/${account.getId()}/call/${conversationId}`;
+    if (withVideo) {
+      url += '?video=true';
+    }
+    navigate(url);
+  };
+
   return (
     <Stack direction="row">
       <Stack flex={1} justifyContent="center" whiteSpace="nowrap" overflow="hidden">
@@ -168,8 +180,8 @@
         </Typography>
       </Stack>
       <Stack direction="row" spacing="20px">
-        <StartAudioCallButton />
-        <StartVideoCallButton />
+        <StartAudioCallButton onClick={() => startCall(false)} />
+        <StartVideoCallButton onClick={() => startCall(true)} />
         <AddParticipantButton />
         <ShowOptionsMenuButton />
       </Stack>
diff --git a/client/src/components/SvgIcon.tsx b/client/src/components/SvgIcon.tsx
index 92875b7..d300aa7 100644
--- a/client/src/components/SvgIcon.tsx
+++ b/client/src/components/SvgIcon.tsx
@@ -407,6 +407,33 @@
   );
 };
 
+export const MicroOffIcon = (props: SvgIconProps) => {
+  return (
+    <SvgIcon {...props} viewBox="0 0 24 24">
+      <g>
+        <path
+          d="M12,16.1c1.1,0,2.1-0.4,2.8-1.2c0.8-0.8,1.2-1.9,1.2-2.8V6c0-1.1-0.4-2.1-1.2-2.8c-1.5-1.5-4.2-1.5-5.8,0
+          C8.4,3.8,8,4.9,8,6v6.2c0,1.1,0.4,2.1,1.2,2.8C9.9,15.7,10.8,16.1,12,16.1z M9.4,6c0-0.7,0.3-1.3,0.8-1.9c0.5-0.5,1.1-0.8,1.9-0.8
+          c0.7,0,1.3,0.3,1.9,0.8c0.5,0.5,0.8,1.1,0.8,1.9v6.2c0,0.7-0.3,1.3-0.8,1.9c-1.1,1.1-2.7,1.1-3.6,0c-0.5-0.5-0.8-1.1-0.8-1.9
+          C9.4,12.1,9.4,6,9.4,6z"
+        />
+        <path
+          d="M18.6,8.8c-0.3-0.3-0.7-0.3-1.1,0c0,0.1-0.1,0.3-0.1,0.5v2.8c0,3.1-2.4,5.5-5.5,5.5s-5.5-2.4-5.5-5.5V9.3
+          c0-0.4-0.3-0.7-0.7-0.7l0,0c-0.1-0.1-0.3,0-0.4,0.1C5.1,8.9,5.1,9,5.1,9.3v2.8c0,3.5,2.7,6.4,6.2,6.8v1.7h-2
+          c-0.4,0-0.7,0.3-0.7,0.7c0,0.4,0.3,0.7,0.7,0.7h5.6c0.4,0,0.7-0.3,0.7-0.7c0-0.4-0.3-0.7-0.7-0.7h-2.1V19c3.5-0.4,6.2-3.4,6.2-6.8
+          V9.3C18.9,9,18.9,9,18.6,8.8z"
+        />
+      </g>
+      <g>
+        <path
+          d="M5.5,21.9c-0.1,0-0.3,0-0.4-0.1c-0.3-0.3-0.4-0.7-0.3-1.1L17.7,2.6c0.3-0.3,0.7-0.4,1.1-0.3C19,2.6,19.1,3,19,3.4L6,21.6
+          C5.9,21.8,5.6,21.9,5.5,21.9z"
+        />
+      </g>
+    </SvgIcon>
+  );
+};
+
 export const MicroInBubbleIcon = (props: SvgIconProps) => {
   return (
     <SvgIcon {...props} viewBox="0 0 25 25">
@@ -629,6 +656,24 @@
   );
 };
 
+export const VideoCameraOffIcon = (props: SvgIconProps) => {
+  return (
+    <SvgIcon {...props} viewBox="0 0 24 24">
+      <g id="Icones_Outline" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
+        <g id="Camera-Off" stroke="#ffffff" strokeWidth="1.5">
+          <line x1="4.5" y1="18.5" x2="15.5" y2="6.5" id="Line" strokeLinecap="round"></line>
+          <g id="Ico_Camera" transform="translate(3.000000, 5.000000)" fillRule="nonzero" strokeLinejoin="round">
+            <path
+              d="M17.739656,2.05124654 C17.62808,1.97368421 17.4421199,1.93490305 17.1817759,2.0900277 L13.4997675,4.41689751 L13.4997675,1.15927978 C13.4997675,0.849030471 13.3138075,0.5 12.7187355,0.5 L0.929800093,0.5 C0.371920037,0.538781163 0,0.810249307 0,1.19806094 L0,13.8407202 C0,14.2285319 0.371920037,14.5 0.929800093,14.5 L12.7559275,14.5 C13.3138075,14.5 13.5369596,14.1509695 13.5369596,13.8407202 L13.5369596,10.5831025 L17.2189679,12.9487535 C17.4421199,13.1038781 17.62808,13.065097 17.739656,12.9875346 C18,12.83241 18,12.5221607 18,12.4058172 L18,2.63296399 C18,2.5166205 18,2.20637119 17.739656,2.05124654 Z"
+              id="Path"
+            ></path>
+          </g>
+        </g>
+      </g>
+    </SvgIcon>
+  );
+};
+
 export const VolumeIcon = (props: SvgIconProps) => {
   return (
     <SvgIcon {...props} viewBox="0 0 24 24">
diff --git a/client/src/contexts/CallProvider.tsx b/client/src/contexts/CallProvider.tsx
new file mode 100644
index 0000000..0716c8c
--- /dev/null
+++ b/client/src/contexts/CallProvider.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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, useState } from 'react';
+
+import { SetState, WithChildren } from '../utils/utils';
+
+interface ICallContext {
+  micOn: boolean;
+  setMicOn: SetState<boolean>;
+  camOn: boolean;
+  setCamOn: SetState<boolean>;
+}
+
+const defaultCallContext: ICallContext = {
+  micOn: false,
+  setMicOn: () => {},
+  camOn: false,
+  setCamOn: () => {},
+};
+
+export const CallContext = createContext<ICallContext>(defaultCallContext);
+
+type CallProviderProps = WithChildren & {
+  micOn?: boolean;
+  camOn?: boolean;
+};
+
+export default ({
+  children,
+  micOn: _micOn = defaultCallContext.micOn,
+  camOn: _camOn = defaultCallContext.camOn,
+}: CallProviderProps) => {
+  const [micOn, setMicOn] = useState(_micOn);
+  const [camOn, setCamOn] = useState(_camOn);
+
+  return (
+    <CallContext.Provider
+      value={{
+        micOn,
+        setMicOn,
+        camOn,
+        setCamOn,
+      }}
+    >
+      {children}
+    </CallContext.Provider>
+  );
+};
diff --git a/client/src/locale/en/translation.json b/client/src/locale/en/translation.json
index a0bae10..9a3982f 100644
--- a/client/src/locale/en/translation.json
+++ b/client/src/locale/en/translation.json
@@ -1,4 +1,11 @@
 {
+  "conversation_message": "Message",
+  "conversation_start_audiocall": "Start audio call",
+  "conversation_start_videocall": "Start video call",
+  "conversation_close": "Close this conversation",
+  "conversation_details": "Conversation details",
+  "conversation_block_contact": "Block this contact",
+  "conversation_delete_contact": "Delete this contact",
   "conversation_title_one": "{{member0}}",
   "conversation_title_two": "{{member0}} and {{member1}}",
   "conversation_title_three": "{{member0}}, {{member1}} and {{member2}}",
diff --git a/client/src/locale/fr/translation.json b/client/src/locale/fr/translation.json
index 0da1c1a..0bc65b2 100644
--- a/client/src/locale/fr/translation.json
+++ b/client/src/locale/fr/translation.json
@@ -1,4 +1,11 @@
 {
+  "conversation_message": "Envoyer un message",
+  "conversation_start_audiocall": "Démarrer appel audio",
+  "conversation_start_videocall": "Démarrer appel vidéo",
+  "conversation_close": "Fermer la conversation",
+  "conversation_details": "Détails de la conversation",
+  "conversation_block_contact": "Bloquer le contact",
+  "conversation_delete_contact": "Supprimer le contact",
   "conversation_title_one": "{{member0}}",
   "conversation_title_two": "{{member0}} et {{member1}}",
   "conversation_title_three": "{{member0}}, {{member1}} et {{member2}}",
diff --git a/client/src/pages/CallInterface.tsx b/client/src/pages/CallInterface.tsx
index 61c90bb..adc12c7 100644
--- a/client/src/pages/CallInterface.tsx
+++ b/client/src/pages/CallInterface.tsx
@@ -16,6 +16,7 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { Box, Card, Grid, Stack, Typography } from '@mui/material';
+import React from 'react';
 
 import {
   CallingChatButton,
@@ -28,9 +29,24 @@
   CallingScreenShareButton,
   CallingVideoCameraButton,
   CallingVolumeButton,
-} from '../components/Button';
+} from '../components/CallButtons';
+import CallProvider from '../contexts/CallProvider';
+import { useUrlParams } from '../utils/hooks';
+import { CallRouteParams } from './JamiMessenger';
 
-export default function CallInterface() {
+export default () => {
+  const {
+    queryParams: { video },
+  } = useUrlParams<CallRouteParams>();
+
+  return (
+    <CallProvider camOn={video === 'true'}>
+      <CallInterface />
+    </CallProvider>
+  );
+};
+
+const CallInterface = () => {
   return (
     <>
       <Box sx={{ backgroundColor: 'blue', width: '100%', height: '100%', position: 'absolute' }}>
@@ -77,7 +93,7 @@
       </Stack>
     </>
   );
-}
+};
 
 const CallInterfaceInformation = () => {
   return (
diff --git a/client/src/pages/JamiMessenger.tsx b/client/src/pages/JamiMessenger.tsx
index 197a3fb..2cb485b 100644
--- a/client/src/pages/JamiMessenger.tsx
+++ b/client/src/pages/JamiMessenger.tsx
@@ -17,13 +17,18 @@
  */
 import { Route, Routes } from 'react-router-dom';
 
+import { RouteParams } from '../utils/hooks';
+import CallInterface from './CallInterface';
 import Messenger from './Messenger';
 
+export type CallRouteParams = RouteParams<{ conversationId: string }, { video?: 'true' }>;
+
 export default function JamiMessenger() {
   return (
     <Routes>
       <Route path="addContact/:contactId" element={<Messenger />} />
       <Route path="conversation/:conversationId" element={<Messenger />} />
+      <Route path="call/:conversationId" element={<CallInterface />} />
       <Route path="*" element={<Messenger />} />
     </Routes>
   );
diff --git a/client/src/utils/hooks.ts b/client/src/utils/hooks.ts
new file mode 100644
index 0000000..790d4ab
--- /dev/null
+++ b/client/src/utils/hooks.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 { useMemo } from 'react';
+import { useLocation, useParams } from 'react-router-dom';
+
+export interface RouteParams<U = Record<string, string>, Q = Record<string, string>> {
+  urlParams: U;
+  queryParams: Q;
+}
+
+export const useUrlParams = <T extends RouteParams>() => {
+  const { search } = useLocation();
+  const urlParams = useParams() as T['urlParams'];
+
+  return useMemo(() => {
+    const queryParams = Object.fromEntries(new URLSearchParams(search)) as T['queryParams'];
+    return {
+      queryParams,
+      urlParams,
+    };
+  }, [search, urlParams]);
+};
diff --git a/client/src/utils/utils.ts b/client/src/utils/utils.ts
index 8f2f1cb..ffe37a4 100644
--- a/client/src/utils/utils.ts
+++ b/client/src/utils/utils.ts
@@ -15,8 +15,10 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { ReactNode } from 'react';
+import { Dispatch, ReactNode, SetStateAction } from 'react';
 
 export type WithChildren = {
   children: ReactNode;
 };
+
+export type SetState<T> = Dispatch<SetStateAction<T>>;