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>
);
};