Add secondary features to the calling interface UI

GitLab: #58

Change-Id: I86c813e63f74e5a36b92a2f4af8ca61a72381040
diff --git a/client/src/pages/CallInterface.tsx b/client/src/pages/CallInterface.tsx
index e729474..14feb43 100644
--- a/client/src/pages/CallInterface.tsx
+++ b/client/src/pages/CallInterface.tsx
@@ -16,15 +16,18 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { Box, Button, Card, Grid, Stack, Typography } from '@mui/material';
-import { useContext } from 'react';
+import { ComponentType, Fragment, ReactNode, useContext, useLayoutEffect, useMemo, useRef, useState } from 'react';
+import Draggable from 'react-draggable';
 
+import { ExpandableButtonProps } from '../components/Button';
 import {
   CallingChatButton,
   CallingEndButton,
   CallingExtensionButton,
-  CallingFullscreenButton,
+  CallingFullScreenButton,
   CallingGroupButton,
   CallingMicButton,
+  CallingMoreVerticalButton,
   CallingRecordButton,
   CallingScreenShareButton,
   CallingVideoCameraButton,
@@ -38,7 +41,6 @@
   const {
     queryParams: { video },
   } = useUrlParams<CallRouteParams>();
-
   return (
     <WebRTCProvider isVideoOn={video === 'true'}>
       <CallInterface />
@@ -46,8 +48,23 @@
   );
 };
 
+export enum SecondaryButtons {
+  Volume = 1,
+  Group,
+  Chat,
+  ScreenShare,
+  Record,
+  Extension,
+  FullScreen,
+}
+
+interface Props {
+  children?: ReactNode;
+}
+
 const CallInterface = () => {
   const { localVideoRef, remoteVideoRef, isVideoOn } = useContext(WebRTCContext);
+  const gridItemRef = useRef(null);
 
   return (
     <>
@@ -70,30 +87,34 @@
         {/* Guest video, with empty space to be moved around and stickied to walls */}
         <Box height="100%">
           {isVideoOn && (
-            <video
-              ref={localVideoRef}
-              autoPlay
-              style={{
-                position: 'absolute',
-                right: 0,
-                zIndex: 2,
-                borderRadius: '12px',
-                minWidth: '25%',
-                minHeight: '25%',
-                maxWidth: '50%',
-                maxHeight: '50%',
-              }}
-            />
+            <Draggable bounds="parent">
+              <video
+                ref={localVideoRef}
+                autoPlay
+                style={{
+                  position: 'absolute',
+                  right: 0,
+                  zIndex: 2,
+                  borderRadius: '12px',
+                  minWidth: '25%',
+                  minHeight: '25%',
+                  maxWidth: '50%',
+                  maxHeight: '50%',
+                }}
+              />
+            </Draggable>
           )}
         </Box>
         {/* Bottom panel with calling buttons */}
-        <Grid container justifyContent="space-between">
+        <Grid container>
           <Grid item xs />
-          <Grid item>
-            <CallInterfacePrimaryButtons />
+          <Grid item sx={{ display: 'flex', justifyContent: 'center' }}>
+            <div>
+              <CallInterfacePrimaryButtons />
+            </div>
           </Grid>
-          <Grid item xs sx={{ display: 'flex', justifyContent: 'flex-end' }}>
-            <CallInterfaceSecondaryButtons />
+          <Grid item xs sx={{ display: 'flex', justifyContent: 'flex-end' }} ref={gridItemRef}>
+            <CallInterfaceSecondaryButtons gridItemRef={gridItemRef} />
           </Grid>
         </Grid>
       </Stack>
@@ -118,33 +139,100 @@
   const { sendWebRTCOffer } = useContext(WebRTCContext);
 
   return (
-    <Card sx={{ backgroundColor: 'black', textAlign: 'center' }}>
-      <Button
-        variant="contained"
-        onClick={() => {
-          sendWebRTCOffer();
-        }}
-      >
-        {/* TODO: Remove this button and make calling automatic (https://git.jami.net/savoirfairelinux/jami-web/-/issues/91)*/}
-        Call
-      </Button>
-      <CallingMicButton />
-      <CallingEndButton />
-      <CallingVideoCameraButton />
+    <Card sx={{ backgroundColor: '#00000088', overflow: 'visible', textAlign: 'center' }}>
+      <Stack direction="row" justifyContent="flex-end" alignItems="flex-end">
+        <Button
+          variant="contained"
+          onClick={() => {
+            sendWebRTCOffer();
+          }}
+        >
+          {/* TODO: Remove this button and make calling automatic (https://git.jami.net/savoirfairelinux/jami-web/-/issues/91)*/}
+          Call
+        </Button>
+        <CallingMicButton />
+        <CallingEndButton />
+        <CallingVideoCameraButton />
+      </Stack>
     </Card>
   );
 };
 
-const CallInterfaceSecondaryButtons = () => {
+const SECONDARY_BUTTONS = [
+  CallingVolumeButton,
+  CallingGroupButton,
+  CallingChatButton,
+  CallingScreenShareButton,
+  CallingRecordButton,
+  CallingExtensionButton,
+  CallingFullScreenButton,
+];
+
+const CallInterfaceSecondaryButtons = (props: Props & { gridItemRef: React.RefObject<HTMLElement> }) => {
+  const stackRef = useRef<HTMLElement>(null);
+
+  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);
+      }
+    };
+    window.addEventListener('resize', onResize);
+    return () => {
+      window.removeEventListener('resize', onResize);
+    };
+  }, [props.gridItemRef]);
+
+  const { displayedButtons, hiddenButtons } = useMemo(() => {
+    const displayedButtons: ComponentType<ExpandableButtonProps>[] = [];
+    const hiddenButtons: ComponentType<ExpandableButtonProps>[] = [];
+    SECONDARY_BUTTONS.forEach((button, i) => {
+      if (i < SECONDARY_BUTTONS.length - hiddenStackCount) {
+        displayedButtons.push(button);
+      } else {
+        hiddenButtons.push(button);
+      }
+    });
+
+    return {
+      displayedButtons,
+      hiddenButtons,
+    };
+  }, [hiddenStackCount]);
+
   return (
-    <Card style={{ backgroundColor: 'black' }}>
-      <CallingVolumeButton />
-      <CallingGroupButton />
-      <CallingChatButton />
-      <CallingScreenShareButton />
-      <CallingRecordButton />
-      <CallingExtensionButton />
-      <CallingFullscreenButton />
+    <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)} />
+            <Stack
+              direction="column-reverse"
+              sx={{ bottom: 0, right: 0, height: '100%', position: 'absolute', top: '-40px' }}
+            >
+              {hiddenButtons.map((SecondaryButton, i) => (
+                <Fragment key={i}>
+                  <SecondaryButton key={i} />
+                </Fragment>
+              ))}
+            </Stack>
+          </Card>
+        )}
+      </Stack>
     </Card>
   );
 };