Fine-tune appearance of buttons in calling interface UI

GitLab: #97

Change-Id: Iecf16921171196547e2a392ba6826971ca23ac47
diff --git a/client/src/components/Button.tsx b/client/src/components/Button.tsx
index 8e695cb..268cab6 100644
--- a/client/src/components/Button.tsx
+++ b/client/src/components/Button.tsx
@@ -106,86 +106,102 @@
 };
 
 export type ExpandableButtonProps = IconButtonProps & {
+  hidden?: boolean;
   Icon?: ComponentType<SvgIconProps>;
   expandMenuOptions?: (ExpandMenuOption | ExpandMenuRadioOption)[];
+  IconButtonComp?: ComponentType<IconButtonProps>;
 };
 
-export const ExpandableButton = styled(({ Icon, expandMenuOptions = [], ...props }: ExpandableButtonProps) => {
+export const ExpandableButton = ({
+  hidden,
+  Icon,
+  expandMenuOptions = undefined,
+  IconButtonComp = IconButton,
+  ...props
+}: ExpandableButtonProps) => {
   const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
   const handleClose = () => {
     setAnchorEl(null);
   };
 
-  const hasExpandMenuOptions = expandMenuOptions.length > 0;
   return (
     <Box>
-      <Menu
-        anchorEl={anchorEl}
-        open={!!anchorEl}
-        onClose={handleClose}
-        anchorOrigin={{
-          vertical: 'top',
-          horizontal: 'center',
-        }}
-        transformOrigin={{
-          vertical: 'bottom',
-          horizontal: 'center',
-        }}
-      >
-        {expandMenuOptions.map((option, id) => {
-          if ('options' in option) {
-            const { options, defaultSelectedOption } = option;
-            return (
-              <RadioGroup key={id} defaultValue={defaultSelectedOption}>
-                {options.map(({ description, key }) => {
-                  return (
-                    <MenuItem key={key}>
-                      <ListItemIcon>
-                        <Radio value={key} />
-                      </ListItemIcon>
-                      <ListItemText>{description}</ListItemText>
-                    </MenuItem>
-                  );
-                })}
-              </RadioGroup>
-            );
-          }
+      {expandMenuOptions && (
+        <Menu
+          anchorEl={anchorEl}
+          open={!!anchorEl}
+          onClose={handleClose}
+          anchorOrigin={{
+            vertical: !hidden ? 'top' : 'center',
+            horizontal: !hidden ? 'center' : 'left',
+          }}
+          transformOrigin={{
+            vertical: !hidden ? 'bottom' : 'center',
+            horizontal: !hidden ? 'center' : 'right',
+          }}
+        >
+          {expandMenuOptions?.map((option, id) => {
+            if ('options' in option) {
+              const { options, defaultSelectedOption } = option;
+              return (
+                <RadioGroup key={id} defaultValue={defaultSelectedOption}>
+                  {options.map(({ description, key }) => {
+                    return (
+                      <MenuItem key={key}>
+                        <ListItemIcon>
+                          <Radio value={key} />
+                        </ListItemIcon>
+                        <ListItemText>{description}</ListItemText>
+                      </MenuItem>
+                    );
+                  })}
+                </RadioGroup>
+              );
+            }
 
-          return (
-            <MenuItem key={id} onClick={handleClose}>
-              <ListItemIcon>{option.icon}</ListItemIcon>
-              <ListItemText>{option.description}</ListItemText>
-            </MenuItem>
-          );
-        })}
-      </Menu>
-      <Box position="relative">
-        {hasExpandMenuOptions && (
+            return (
+              <MenuItem key={id} onClick={handleClose}>
+                <ListItemIcon>{option.icon}</ListItemIcon>
+                <ListItemText>{option.description}</ListItemText>
+              </MenuItem>
+            );
+          })}
+        </Menu>
+      )}
+      <Box
+        position="relative"
+        display="flex"
+        justifyContent="center"
+        alignItems="center"
+        onClick={(e) => setAnchorEl(e.currentTarget)}
+      >
+        {expandMenuOptions && (
           <IconButton
             {...props}
-            onClick={(e) => setAnchorEl(e.currentTarget)}
             aria-label="expand options"
             size="small"
             sx={{
+              transform: !hidden ? 'scale(0.5)' : 'scale(0.5) rotate(-90deg)',
               position: 'absolute',
-              left: 0,
-              right: 0,
-              top: '-50%',
-              color: 'white',
+              top: !hidden ? '-50%' : 0,
+              left: hidden ? '-50%' : 0,
+              width: '100%',
+              height: '100%',
             }}
           >
-            <ExpandLessIcon sx={{ backgroundColor: '#555555', borderRadius: '5px' }} />
+            <ExpandLessIcon
+              sx={{
+                backgroundColor: '#444444',
+                borderRadius: '5px',
+              }}
+            />
           </IconButton>
         )}
-        <IconButton {...props} disableRipple={true}>
-          {Icon && <Icon fontSize="inherit" />}
-        </IconButton>
+        <IconButtonComp {...props}>{Icon && <Icon />}</IconButtonComp>
       </Box>
     </Box>
   );
-})(({ theme }) => ({
-  color: 'white',
-}));
+};
 
 export const CancelPictureButton = (props: IconButtonProps) => {
   return <RoundButton {...props} aria-label="remove picture" Icon={CancelIcon} size="large" />;
@@ -273,6 +289,10 @@
   return (
     <IconButton
       {...props}
+      sx={{
+        color: selected ? 'white' : 'red',
+        ...props.sx,
+      }}
       onClick={() => {
         toggle();
       }}
diff --git a/client/src/components/CallButtons.tsx b/client/src/components/CallButtons.tsx
index 8c1660d..2392c00 100644
--- a/client/src/components/CallButtons.tsx
+++ b/client/src/components/CallButtons.tsx
@@ -16,11 +16,12 @@
  * <https://www.gnu.org/licenses/>.
  */
 
+import { styled } from '@mui/material/styles';
 import React, { useContext } from 'react';
 import { Trans } from 'react-i18next';
 
 import { WebRTCContext } from '../contexts/WebRTCProvider';
-import { ExpandableButton, ExpandableButtonProps, ToggleIconButton, ToggleIconButtonProps } from './Button';
+import { ExpandableButton, ExpandableButtonProps, ToggleIconButton } from './Button';
 import {
   CallEndIcon,
   ChatBubbleIcon,
@@ -38,39 +39,59 @@
   VideoCameraIcon,
   VideoCameraOffIcon,
   VolumeIcon,
+  WindowIcon,
 } from './SvgIcon';
 
+const CallButton = styled((props: ExpandableButtonProps) => {
+  return <ExpandableButton {...props} />;
+})({
+  '&:hover': {
+    backgroundColor: 'rgba(255, 255, 255, 0.15)',
+  },
+  color: 'white',
+});
+
+const ColoredCallButton = styled((props: ExpandableButtonProps) => {
+  return <ExpandableButton {...props} />;
+})({
+  color: 'white',
+  backgroundColor: '#a30000',
+  '&:hover': {
+    backgroundColor: '#ff0000',
+  },
+});
+
 export const CallingChatButton = (props: ExpandableButtonProps) => {
-  return <ExpandableButton aria-label="chat" Icon={ChatBubbleIcon} {...props} />;
+  return <CallButton aria-label="chat" Icon={ChatBubbleIcon} {...props} />;
 };
 
 export const CallingEndButton = (props: ExpandableButtonProps) => {
-  return <ExpandableButton aria-label="call end" Icon={CallEndIcon} sx={{ backgroundColor: 'red' }} {...props} />;
+  return <ColoredCallButton sx={{}} aria-label="call end" Icon={CallEndIcon} {...props} />;
 };
 
 export const CallingExtensionButton = (props: ExpandableButtonProps) => {
-  return <ExpandableButton aria-label="extensions" Icon={ExtensionIcon} {...props} />;
+  return <CallButton aria-label="extensions" Icon={ExtensionIcon} {...props} />;
 };
 
 export const CallingFullScreenButton = (props: ExpandableButtonProps) => {
-  return <ExpandableButton aria-label="full screen" Icon={FullScreenIcon} {...props} />;
+  return <CallButton aria-label="full screen" Icon={FullScreenIcon} {...props} />;
 };
 
 export const CallingGroupButton = (props: ExpandableButtonProps) => {
-  return <ExpandableButton aria-label="group options" Icon={GroupAddIcon} {...props} />;
+  return <CallButton aria-label="group options" Icon={GroupAddIcon} {...props} />;
 };
 
 export const CallingMoreVerticalButton = (props: ExpandableButtonProps) => {
-  return <ExpandableButton aria-label="more vertical" Icon={MoreVerticalIcon} {...props} />;
+  return <CallButton aria-label="more vertical" Icon={MoreVerticalIcon} {...props} />;
 };
 
 export const CallingRecordButton = (props: ExpandableButtonProps) => {
-  return <ExpandableButton aria-label="recording options" Icon={RecordingIcon} {...props} />;
+  return <CallButton aria-label="recording options" Icon={RecordingIcon} {...props} />;
 };
 
 export const CallingScreenShareButton = (props: ExpandableButtonProps) => {
   return (
-    <ExpandableButton
+    <CallButton
       aria-label="screen share"
       Icon={ScreenShareArrowIcon}
       expandMenuOptions={[
@@ -79,6 +100,10 @@
           icon: <ScreenShareRegularIcon />,
         },
         {
+          description: <Trans i18nKey="share_window" />,
+          icon: <WindowIcon />,
+        },
+        {
           description: <Trans i18nKey="share_screen_area" />,
           icon: <ScreenShareScreenAreaIcon />,
         },
@@ -94,7 +119,7 @@
 
 export const CallingVolumeButton = (props: ExpandableButtonProps) => {
   return (
-    <ExpandableButton
+    <CallButton
       aria-label="volume options"
       Icon={VolumeIcon}
       expandMenuOptions={[
@@ -111,33 +136,42 @@
     />
   );
 };
-export const CallingMicButton = (props: Partial<ToggleIconButtonProps>) => {
+
+export const CallingMicButton = (props: ExpandableButtonProps) => {
   const { isAudioOn, setAudioStatus } = useContext(WebRTCContext);
 
   return (
-    <ToggleIconButton
+    <CallButton
       aria-label="microphone options"
-      sx={{ color: 'white' }}
-      IconOn={MicroIcon}
-      IconOff={MicroOffIcon}
-      selected={isAudioOn}
-      toggle={() => setAudioStatus(!isAudioOn)}
+      IconButtonComp={(props) => (
+        <ToggleIconButton
+          IconOn={MicroIcon}
+          IconOff={MicroOffIcon}
+          selected={isAudioOn}
+          toggle={() => setAudioStatus(!isAudioOn)}
+          {...props}
+        />
+      )}
       {...props}
     />
   );
 };
 
-export const CallingVideoCameraButton = (props: Partial<ToggleIconButtonProps>) => {
+export const CallingVideoCameraButton = (props: ExpandableButtonProps) => {
   const { isVideoOn, setVideoStatus } = useContext(WebRTCContext);
 
   return (
-    <ToggleIconButton
+    <CallButton
       aria-label="camera options"
-      sx={{ color: 'white' }}
-      IconOn={VideoCameraIcon}
-      IconOff={VideoCameraOffIcon}
-      selected={isVideoOn}
-      toggle={() => setVideoStatus(!isVideoOn)}
+      IconButtonComp={(props) => (
+        <ToggleIconButton
+          IconOn={VideoCameraIcon}
+          IconOff={VideoCameraOffIcon}
+          selected={isVideoOn}
+          toggle={() => setVideoStatus(!isVideoOn)}
+          {...props}
+        />
+      )}
       {...props}
     />
   );
diff --git a/client/src/components/SvgIcon.tsx b/client/src/components/SvgIcon.tsx
index 0b8b9ed..88423b1 100644
--- a/client/src/components/SvgIcon.tsx
+++ b/client/src/components/SvgIcon.tsx
@@ -799,3 +799,15 @@
     </SvgIcon>
   );
 };
+
+export const WindowIcon = (props: SvgIconProps) => {
+  return (
+    <SvgIcon {...props} viewBox="0 0 24 24">
+      <path
+        d="M20.5,0.8h-17C2,0.8,0.8,2,0.8,3.5v17c0,1.5,1.2,2.7,2.7,2.7h17c1.5,0,2.7-1.2,2.7-2.7v-17C23.2,2,22,0.8,20.5,0.8z
+    M3.5,2.2h17c0.7,0,1.3,0.6,1.3,1.3v0.1H2.2V3.5C2.2,2.8,2.8,2.2,3.5,2.2z M7.7,21.8H3.5c-0.7,0-1.3-0.6-1.3-1.3V5h5.5h1.4h12.7
+   v15.5c0,0.7-0.6,1.3-1.3,1.3H9.1H7.7z"
+      />
+    </SvgIcon>
+  );
+};
diff --git a/client/src/locale/en/translation.json b/client/src/locale/en/translation.json
index 049bc55..18bbf31 100644
--- a/client/src/locale/en/translation.json
+++ b/client/src/locale/en/translation.json
@@ -49,6 +49,7 @@
   "registration_success": "You've successfully registered! — Logging you in...",
 
   "share_screen": "Share your screen",
+  "share_window": "Share window",
   "share_screen_area": "Share an area of your screen",
   "share_file": "Share a file",
 
diff --git a/client/src/locale/fr/translation.json b/client/src/locale/fr/translation.json
index 346f0aa..44f747c 100644
--- a/client/src/locale/fr/translation.json
+++ b/client/src/locale/fr/translation.json
@@ -29,6 +29,7 @@
   "messages_scroll_to_end": "Faire défiler jusqu'à la fin de la conversation",
 
   "share_screen": "Partager votre écran",
+  "share_window": "Partager la fenêtre",
   "share_screen_area": "Partager une partie de l'écran",
   "share_file": "Partager le fichier",
 
diff --git a/client/src/pages/CallInterface.tsx b/client/src/pages/CallInterface.tsx
index 43d6211..b22821c 100644
--- a/client/src/pages/CallInterface.tsx
+++ b/client/src/pages/CallInterface.tsx
@@ -16,7 +16,17 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { Box, Button, Card, Grid, Stack, Typography } from '@mui/material';
-import { ComponentType, Fragment, ReactNode, useContext, useLayoutEffect, useMemo, useRef, useState } from 'react';
+import {
+  ComponentType,
+  Fragment,
+  ReactNode,
+  useCallback,
+  useContext,
+  useLayoutEffect,
+  useMemo,
+  useRef,
+  useState,
+} from 'react';
 import Draggable from 'react-draggable';
 
 import { ExpandableButtonProps } from '../components/Button';
@@ -49,16 +59,6 @@
   );
 };
 
-export enum SecondaryButtons {
-  Volume = 1,
-  Group,
-  Chat,
-  ScreenShare,
-  Record,
-  Extension,
-  FullScreen,
-}
-
 interface Props {
   children?: ReactNode;
 }
@@ -69,6 +69,7 @@
 
   return (
     <>
+      {/* Guest video, takes the whole screen */}
       <video
         ref={remoteVideoRef}
         autoPlay
@@ -85,10 +86,10 @@
         <Box>
           <CallInterfaceInformation />
         </Box>
-        {/* Guest video, with empty space to be moved around and stickied to walls */}
+        {/* Local video, with empty space to be moved around and stickied to walls */}
         <Box height="100%">
           {isVideoOn && (
-            <Draggable bounds="parent">
+            <Draggable bounds="parent" nodeRef={localVideoRef ?? undefined}>
               <video
                 ref={localVideoRef}
                 autoPlay
@@ -140,10 +141,11 @@
   const { sendWebRTCOffer } = useContext(WebRTCContext);
 
   return (
-    <Card sx={{ backgroundColor: '#00000088', overflow: 'visible', textAlign: 'center' }}>
-      <Stack direction="row" justifyContent="flex-end" alignItems="flex-end">
+    <Card sx={{ backgroundColor: '#00000088', overflow: 'visible' }}>
+      <Stack direction="row" justifyContent="center" alignItems="center">
         <Button
           variant="contained"
+          size="small"
           onClick={() => {
             sendWebRTCOffer();
           }}
@@ -152,7 +154,7 @@
           Call
         </Button>
         <CallingMicButton />
-        <CallingEndButton />
+        <CallingEndButton hidden={false} />
         <CallingVideoCameraButton />
       </Stack>
     </Card>
@@ -172,26 +174,37 @@
 const CallInterfaceSecondaryButtons = (props: Props & { gridItemRef: React.RefObject<HTMLElement> }) => {
   const stackRef = useRef<HTMLElement>(null);
 
+  const [initialMeasurementDone, setInitialMeasurementDone] = useState(false);
   const [hiddenStackCount, setHiddenStackCount] = useState(0);
   const [hiddenMenuVisible, setHiddenMenuVisible] = useState(false);
 
-  useLayoutEffect(() => {
-    const onResize = () => {
-      if (stackRef?.current && props.gridItemRef?.current) {
-        const buttonWidth = stackRef.current.children[0].clientWidth;
-        const availableSpace = props.gridItemRef.current.clientWidth;
-        let availableButtons = Math.floor((availableSpace - 1) / buttonWidth);
-        if (availableButtons < SECONDARY_BUTTONS.length) {
-          availableButtons -= 1; // Leave room for CallingMoreVerticalButton
-        }
-        setHiddenStackCount(SECONDARY_BUTTONS.length - availableButtons);
+  const calculateStackCount = useCallback(() => {
+    if (stackRef?.current && props.gridItemRef?.current) {
+      const buttonWidth = stackRef.current.children[0].clientWidth;
+      const availableSpace = props.gridItemRef.current.clientWidth;
+      let availableButtons = Math.floor((availableSpace - 1) / buttonWidth);
+      if (availableButtons < SECONDARY_BUTTONS.length) {
+        availableButtons -= 1; // Leave room for CallingMoreVerticalButton
       }
+      setHiddenStackCount(SECONDARY_BUTTONS.length - availableButtons);
+    }
+  }, [props.gridItemRef]);
+
+  useLayoutEffect(() => {
+    // Run once, at the beginning, for initial measurements
+    if (!initialMeasurementDone) {
+      calculateStackCount();
+      setInitialMeasurementDone(true);
+    }
+
+    const onResize = () => {
+      calculateStackCount();
     };
     window.addEventListener('resize', onResize);
     return () => {
       window.removeEventListener('resize', onResize);
     };
-  }, [props.gridItemRef]);
+  }, [calculateStackCount, initialMeasurementDone]);
 
   const { displayedButtons, hiddenButtons } = useMemo(() => {
     const displayedButtons: ComponentType<ExpandableButtonProps>[] = [];
@@ -211,29 +224,37 @@
   }, [hiddenStackCount]);
 
   return (
-    <Card sx={{ backgroundColor: '#00000088', overflow: 'visible' }}>
-      <Stack direction="row" justifyContent="flex-end" alignItems="flex-end" ref={stackRef}>
-        {displayedButtons.map((SecondaryButton, i) => (
-          <Fragment key={i}>
-            <SecondaryButton />
-          </Fragment>
-        ))}
-        {!!hiddenButtons.length && (
-          <Card sx={{ position: 'relative', backgroundColor: '#00000088', overflow: 'visible' }}>
-            <CallingMoreVerticalButton onClick={() => setHiddenMenuVisible(!hiddenMenuVisible)} />
+    <Card sx={{ backgroundColor: '#00000088', overflow: 'visible', height: '100%' }}>
+      <Stack direction="row" justifyContent="center" alignItems="center" height="100%" ref={stackRef}>
+        {initialMeasurementDone &&
+          displayedButtons.map((SecondaryButton, i) => (
+            <Fragment key={i}>
+              <SecondaryButton hidden={false} />
+            </Fragment>
+          ))}
+        {(!!hiddenButtons.length || !initialMeasurementDone) && (
+          <CallingMoreVerticalButton hidden={true} onClick={() => setHiddenMenuVisible(!hiddenMenuVisible)} />
+        )}
+      </Stack>
+
+      {!!hiddenButtons.length && hiddenMenuVisible && (
+        <Box sx={{ position: 'absolute', right: 0, bottom: '50px' }}>
+          <Card sx={{ backgroundColor: '#00000088', overflow: 'visible', justifyContent: 'flex-end' }}>
             <Stack
-              direction="column-reverse"
-              sx={{ bottom: 0, right: 0, height: '100%', position: 'absolute', top: '-40px' }}
+              direction="column"
+              justifyContent="flex-end"
+              alignItems="flex-end"
+              sx={{ bottom: 0, right: 0, height: '100%' }}
             >
               {hiddenButtons.map((SecondaryButton, i) => (
                 <Fragment key={i}>
-                  <SecondaryButton key={i} />
+                  <SecondaryButton hidden={true} />
                 </Fragment>
               ))}
             </Stack>
           </Card>
-        )}
-      </Stack>
+        </Box>
+      )}
     </Card>
   );
 };