blob: b22821cb1b3eb423564a92817635569886af5300 [file] [log] [blame]
Gabriel Rochone3ec0d22022-10-08 14:27:03 -04001/*
2 * Copyright (C) 2022 Savoir-faire Linux Inc.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation; either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public
15 * License along with this program. If not, see
16 * <https://www.gnu.org/licenses/>.
17 */
simonce2c0c42022-11-02 17:39:31 -040018import { Box, Button, Card, Grid, Stack, Typography } from '@mui/material';
Gabriel Rochon8321a0d2022-11-06 23:18:36 -050019import {
20 ComponentType,
21 Fragment,
22 ReactNode,
23 useCallback,
24 useContext,
25 useLayoutEffect,
26 useMemo,
27 useRef,
28 useState,
29} from 'react';
simon33c06182022-11-02 17:39:31 -040030import Draggable from 'react-draggable';
Gabriel Rochone3ec0d22022-10-08 14:27:03 -040031
simon33c06182022-11-02 17:39:31 -040032import { ExpandableButtonProps } from '../components/Button';
Gabriel Rochone3ec0d22022-10-08 14:27:03 -040033import {
34 CallingChatButton,
35 CallingEndButton,
36 CallingExtensionButton,
simon33c06182022-11-02 17:39:31 -040037 CallingFullScreenButton,
Gabriel Rochone3ec0d22022-10-08 14:27:03 -040038 CallingGroupButton,
39 CallingMicButton,
simon33c06182022-11-02 17:39:31 -040040 CallingMoreVerticalButton,
Gabriel Rochone3ec0d22022-10-08 14:27:03 -040041 CallingRecordButton,
42 CallingScreenShareButton,
43 CallingVideoCameraButton,
44 CallingVolumeButton,
simon1170c322022-10-31 14:51:31 -040045} from '../components/CallButtons';
simonce2c0c42022-11-02 17:39:31 -040046import WebRTCProvider, { WebRTCContext } from '../contexts/WebRTCProvider';
simon1170c322022-10-31 14:51:31 -040047import { useUrlParams } from '../utils/hooks';
48import { CallRouteParams } from './JamiMessenger';
Gabriel Rochone3ec0d22022-10-08 14:27:03 -040049
simon1170c322022-10-31 14:51:31 -040050export default () => {
51 const {
52 queryParams: { video },
53 } = useUrlParams<CallRouteParams>();
simon1170c322022-10-31 14:51:31 -040054 return (
Charlie461805e2022-11-09 10:40:15 -050055 //TODO: set contactID
56 <WebRTCProvider isVideoOn={video === 'true'} contactId={'contactIdToBeAdded'}>
simon1170c322022-10-31 14:51:31 -040057 <CallInterface />
simonce2c0c42022-11-02 17:39:31 -040058 </WebRTCProvider>
simon1170c322022-10-31 14:51:31 -040059 );
60};
61
simon33c06182022-11-02 17:39:31 -040062interface Props {
63 children?: ReactNode;
64}
65
simon1170c322022-10-31 14:51:31 -040066const CallInterface = () => {
simonce2c0c42022-11-02 17:39:31 -040067 const { localVideoRef, remoteVideoRef, isVideoOn } = useContext(WebRTCContext);
simon33c06182022-11-02 17:39:31 -040068 const gridItemRef = useRef(null);
simonce2c0c42022-11-02 17:39:31 -040069
Gabriel Rochone3ec0d22022-10-08 14:27:03 -040070 return (
71 <>
Gabriel Rochon8321a0d2022-11-06 23:18:36 -050072 {/* Guest video, takes the whole screen */}
simonce2c0c42022-11-02 17:39:31 -040073 <video
74 ref={remoteVideoRef}
75 autoPlay
76 style={{ backgroundColor: 'black', width: '100%', height: '100%', position: 'absolute' }}
77 />
Gabriel Rochone3ec0d22022-10-08 14:27:03 -040078 <Stack
79 position="absolute"
80 direction="column"
81 spacing={1}
82 margin={2}
83 sx={{ left: 0, right: 0, top: 0, bottom: 0 }}
84 >
85 {/* Top panel with guest information */}
86 <Box>
87 <CallInterfaceInformation />
88 </Box>
Gabriel Rochon8321a0d2022-11-06 23:18:36 -050089 {/* Local video, with empty space to be moved around and stickied to walls */}
Gabriel Rochone3ec0d22022-10-08 14:27:03 -040090 <Box height="100%">
simonce2c0c42022-11-02 17:39:31 -040091 {isVideoOn && (
Gabriel Rochon8321a0d2022-11-06 23:18:36 -050092 <Draggable bounds="parent" nodeRef={localVideoRef ?? undefined}>
simon33c06182022-11-02 17:39:31 -040093 <video
94 ref={localVideoRef}
95 autoPlay
96 style={{
97 position: 'absolute',
98 right: 0,
99 zIndex: 2,
100 borderRadius: '12px',
101 minWidth: '25%',
102 minHeight: '25%',
103 maxWidth: '50%',
104 maxHeight: '50%',
105 }}
106 />
107 </Draggable>
simonce2c0c42022-11-02 17:39:31 -0400108 )}
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400109 </Box>
110 {/* Bottom panel with calling buttons */}
simon33c06182022-11-02 17:39:31 -0400111 <Grid container>
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400112 <Grid item xs />
simon33c06182022-11-02 17:39:31 -0400113 <Grid item sx={{ display: 'flex', justifyContent: 'center' }}>
114 <div>
115 <CallInterfacePrimaryButtons />
116 </div>
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400117 </Grid>
simon33c06182022-11-02 17:39:31 -0400118 <Grid item xs sx={{ display: 'flex', justifyContent: 'flex-end' }} ref={gridItemRef}>
119 <CallInterfaceSecondaryButtons gridItemRef={gridItemRef} />
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400120 </Grid>
121 </Grid>
122 </Stack>
123 </>
124 );
simon1170c322022-10-31 14:51:31 -0400125};
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400126
127const CallInterfaceInformation = () => {
128 return (
129 <Stack direction="row" justifyContent="space-between" alignItems="center">
130 <Typography color="white" component="p">
131 Alain Thérieur
132 </Typography>
133 <Typography color="white" component="p">
134 01:23
135 </Typography>
136 </Stack>
137 );
138};
139
140const CallInterfacePrimaryButtons = () => {
simonce2c0c42022-11-02 17:39:31 -0400141 const { sendWebRTCOffer } = useContext(WebRTCContext);
142
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400143 return (
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500144 <Card sx={{ backgroundColor: '#00000088', overflow: 'visible' }}>
145 <Stack direction="row" justifyContent="center" alignItems="center">
simon33c06182022-11-02 17:39:31 -0400146 <Button
147 variant="contained"
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500148 size="small"
simon33c06182022-11-02 17:39:31 -0400149 onClick={() => {
150 sendWebRTCOffer();
151 }}
152 >
153 {/* TODO: Remove this button and make calling automatic (https://git.jami.net/savoirfairelinux/jami-web/-/issues/91)*/}
154 Call
155 </Button>
156 <CallingMicButton />
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500157 <CallingEndButton hidden={false} />
simon33c06182022-11-02 17:39:31 -0400158 <CallingVideoCameraButton />
159 </Stack>
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400160 </Card>
161 );
162};
163
simon33c06182022-11-02 17:39:31 -0400164const SECONDARY_BUTTONS = [
165 CallingVolumeButton,
166 CallingGroupButton,
167 CallingChatButton,
168 CallingScreenShareButton,
169 CallingRecordButton,
170 CallingExtensionButton,
171 CallingFullScreenButton,
172];
173
174const CallInterfaceSecondaryButtons = (props: Props & { gridItemRef: React.RefObject<HTMLElement> }) => {
175 const stackRef = useRef<HTMLElement>(null);
176
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500177 const [initialMeasurementDone, setInitialMeasurementDone] = useState(false);
simon33c06182022-11-02 17:39:31 -0400178 const [hiddenStackCount, setHiddenStackCount] = useState(0);
179 const [hiddenMenuVisible, setHiddenMenuVisible] = useState(false);
180
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500181 const calculateStackCount = useCallback(() => {
182 if (stackRef?.current && props.gridItemRef?.current) {
183 const buttonWidth = stackRef.current.children[0].clientWidth;
184 const availableSpace = props.gridItemRef.current.clientWidth;
185 let availableButtons = Math.floor((availableSpace - 1) / buttonWidth);
186 if (availableButtons < SECONDARY_BUTTONS.length) {
187 availableButtons -= 1; // Leave room for CallingMoreVerticalButton
simon33c06182022-11-02 17:39:31 -0400188 }
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500189 setHiddenStackCount(SECONDARY_BUTTONS.length - availableButtons);
190 }
191 }, [props.gridItemRef]);
192
193 useLayoutEffect(() => {
194 // Run once, at the beginning, for initial measurements
195 if (!initialMeasurementDone) {
196 calculateStackCount();
197 setInitialMeasurementDone(true);
198 }
199
200 const onResize = () => {
201 calculateStackCount();
simon33c06182022-11-02 17:39:31 -0400202 };
203 window.addEventListener('resize', onResize);
204 return () => {
205 window.removeEventListener('resize', onResize);
206 };
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500207 }, [calculateStackCount, initialMeasurementDone]);
simon33c06182022-11-02 17:39:31 -0400208
209 const { displayedButtons, hiddenButtons } = useMemo(() => {
210 const displayedButtons: ComponentType<ExpandableButtonProps>[] = [];
211 const hiddenButtons: ComponentType<ExpandableButtonProps>[] = [];
212 SECONDARY_BUTTONS.forEach((button, i) => {
213 if (i < SECONDARY_BUTTONS.length - hiddenStackCount) {
214 displayedButtons.push(button);
215 } else {
216 hiddenButtons.push(button);
217 }
218 });
219
220 return {
221 displayedButtons,
222 hiddenButtons,
223 };
224 }, [hiddenStackCount]);
225
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400226 return (
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500227 <Card sx={{ backgroundColor: '#00000088', overflow: 'visible', height: '100%' }}>
228 <Stack direction="row" justifyContent="center" alignItems="center" height="100%" ref={stackRef}>
229 {initialMeasurementDone &&
230 displayedButtons.map((SecondaryButton, i) => (
231 <Fragment key={i}>
232 <SecondaryButton hidden={false} />
233 </Fragment>
234 ))}
235 {(!!hiddenButtons.length || !initialMeasurementDone) && (
236 <CallingMoreVerticalButton hidden={true} onClick={() => setHiddenMenuVisible(!hiddenMenuVisible)} />
237 )}
238 </Stack>
239
240 {!!hiddenButtons.length && hiddenMenuVisible && (
241 <Box sx={{ position: 'absolute', right: 0, bottom: '50px' }}>
242 <Card sx={{ backgroundColor: '#00000088', overflow: 'visible', justifyContent: 'flex-end' }}>
simon33c06182022-11-02 17:39:31 -0400243 <Stack
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500244 direction="column"
245 justifyContent="flex-end"
246 alignItems="flex-end"
247 sx={{ bottom: 0, right: 0, height: '100%' }}
simon33c06182022-11-02 17:39:31 -0400248 >
249 {hiddenButtons.map((SecondaryButton, i) => (
250 <Fragment key={i}>
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500251 <SecondaryButton hidden={true} />
simon33c06182022-11-02 17:39:31 -0400252 </Fragment>
253 ))}
254 </Stack>
255 </Card>
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500256 </Box>
257 )}
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400258 </Card>
259 );
260};