blob: d49a3672c4a506f0dce737c096212e4617b924f7 [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 */
simonf929a362022-11-18 16:53:45 -050018import { Box, 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,
simonf929a362022-11-18 16:53:45 -050025 useEffect,
Gabriel Rochon8321a0d2022-11-06 23:18:36 -050026 useLayoutEffect,
27 useMemo,
28 useRef,
29 useState,
30} from 'react';
simon33c06182022-11-02 17:39:31 -040031import Draggable from 'react-draggable';
Gabriel Rochone3ec0d22022-10-08 14:27:03 -040032
simon33c06182022-11-02 17:39:31 -040033import { ExpandableButtonProps } from '../components/Button';
Gabriel Rochone3ec0d22022-10-08 14:27:03 -040034import {
35 CallingChatButton,
36 CallingEndButton,
37 CallingExtensionButton,
simon33c06182022-11-02 17:39:31 -040038 CallingFullScreenButton,
Gabriel Rochone3ec0d22022-10-08 14:27:03 -040039 CallingGroupButton,
40 CallingMicButton,
simon33c06182022-11-02 17:39:31 -040041 CallingMoreVerticalButton,
Gabriel Rochone3ec0d22022-10-08 14:27:03 -040042 CallingRecordButton,
43 CallingScreenShareButton,
44 CallingVideoCameraButton,
45 CallingVolumeButton,
simon1170c322022-10-31 14:51:31 -040046} from '../components/CallButtons';
simonf929a362022-11-18 16:53:45 -050047import { CallContext, CallStatus } from '../contexts/CallProvider';
48import { CallPending } from './CallPending';
Gabriel Rochone3ec0d22022-10-08 14:27:03 -040049
simon1170c322022-10-31 14:51:31 -040050export default () => {
simonf929a362022-11-18 16:53:45 -050051 const { callRole, callStatus } = useContext(CallContext);
52
53 if (callStatus === CallStatus.InCall) {
54 return <CallInterface />;
55 }
simon1170c322022-10-31 14:51:31 -040056 return (
simonf929a362022-11-18 16:53:45 -050057 <CallPending
58 pending={callRole}
59 caller={callStatus === CallStatus.Ringing ? 'calling' : 'connecting'}
60 medium="audio"
61 />
simon1170c322022-10-31 14:51:31 -040062 );
63};
64
simon33c06182022-11-02 17:39:31 -040065interface Props {
66 children?: ReactNode;
67}
68
simon1170c322022-10-31 14:51:31 -040069const CallInterface = () => {
simonf929a362022-11-18 16:53:45 -050070 const { isVideoOn, localStream, remoteStream } = useContext(CallContext);
simon33c06182022-11-02 17:39:31 -040071 const gridItemRef = useRef(null);
simonf929a362022-11-18 16:53:45 -050072 const remoteVideoRef = useRef<HTMLVideoElement | null>(null);
73 const localVideoRef = useRef<HTMLVideoElement | null>(null);
74
75 useEffect(() => {
76 if (localStream && localVideoRef.current) {
77 localVideoRef.current.srcObject = localStream;
78 }
79 }, [localStream]);
80
81 useEffect(() => {
82 if (remoteStream && remoteVideoRef.current) {
83 remoteVideoRef.current.srcObject = remoteStream;
84 }
85 }, [remoteStream]);
simonce2c0c42022-11-02 17:39:31 -040086
Gabriel Rochone3ec0d22022-10-08 14:27:03 -040087 return (
88 <>
Gabriel Rochon8321a0d2022-11-06 23:18:36 -050089 {/* Guest video, takes the whole screen */}
simonce2c0c42022-11-02 17:39:31 -040090 <video
91 ref={remoteVideoRef}
92 autoPlay
simonf929a362022-11-18 16:53:45 -050093 style={{ zIndex: -1, backgroundColor: 'black', width: '100%', height: '100%', position: 'absolute' }}
simonce2c0c42022-11-02 17:39:31 -040094 />
Gabriel Rochone3ec0d22022-10-08 14:27:03 -040095 <Stack
96 position="absolute"
97 direction="column"
98 spacing={1}
99 margin={2}
100 sx={{ left: 0, right: 0, top: 0, bottom: 0 }}
101 >
102 {/* Top panel with guest information */}
103 <Box>
104 <CallInterfaceInformation />
105 </Box>
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500106 {/* Local video, with empty space to be moved around and stickied to walls */}
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400107 <Box height="100%">
simonf929a362022-11-18 16:53:45 -0500108 <Draggable bounds="parent" nodeRef={localVideoRef ?? undefined}>
109 <video
110 ref={localVideoRef}
111 autoPlay
112 style={{
113 position: 'absolute',
114 right: 0,
115 zIndex: 2,
116 borderRadius: '12px',
117 minWidth: '25%',
118 minHeight: '25%',
119 maxWidth: '50%',
120 maxHeight: '50%',
121 visibility: isVideoOn ? 'visible' : 'hidden',
122 }}
123 />
124 </Draggable>
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400125 </Box>
126 {/* Bottom panel with calling buttons */}
simon33c06182022-11-02 17:39:31 -0400127 <Grid container>
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400128 <Grid item xs />
simon33c06182022-11-02 17:39:31 -0400129 <Grid item sx={{ display: 'flex', justifyContent: 'center' }}>
130 <div>
131 <CallInterfacePrimaryButtons />
132 </div>
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400133 </Grid>
simon33c06182022-11-02 17:39:31 -0400134 <Grid item xs sx={{ display: 'flex', justifyContent: 'flex-end' }} ref={gridItemRef}>
135 <CallInterfaceSecondaryButtons gridItemRef={gridItemRef} />
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400136 </Grid>
137 </Grid>
138 </Stack>
139 </>
140 );
simon1170c322022-10-31 14:51:31 -0400141};
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400142
143const CallInterfaceInformation = () => {
144 return (
145 <Stack direction="row" justifyContent="space-between" alignItems="center">
146 <Typography color="white" component="p">
147 Alain Thérieur
148 </Typography>
149 <Typography color="white" component="p">
150 01:23
151 </Typography>
152 </Stack>
153 );
154};
155
156const CallInterfacePrimaryButtons = () => {
157 return (
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500158 <Card sx={{ backgroundColor: '#00000088', overflow: 'visible' }}>
159 <Stack direction="row" justifyContent="center" alignItems="center">
simon33c06182022-11-02 17:39:31 -0400160 <CallingMicButton />
simon9a8fe202022-11-15 18:25:49 -0500161 <CallingEndButton />
simon33c06182022-11-02 17:39:31 -0400162 <CallingVideoCameraButton />
163 </Stack>
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400164 </Card>
165 );
166};
167
simon33c06182022-11-02 17:39:31 -0400168const SECONDARY_BUTTONS = [
169 CallingVolumeButton,
170 CallingGroupButton,
171 CallingChatButton,
172 CallingScreenShareButton,
173 CallingRecordButton,
174 CallingExtensionButton,
175 CallingFullScreenButton,
176];
177
178const CallInterfaceSecondaryButtons = (props: Props & { gridItemRef: React.RefObject<HTMLElement> }) => {
179 const stackRef = useRef<HTMLElement>(null);
180
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500181 const [initialMeasurementDone, setInitialMeasurementDone] = useState(false);
simon33c06182022-11-02 17:39:31 -0400182 const [hiddenStackCount, setHiddenStackCount] = useState(0);
183 const [hiddenMenuVisible, setHiddenMenuVisible] = useState(false);
184
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500185 const calculateStackCount = useCallback(() => {
186 if (stackRef?.current && props.gridItemRef?.current) {
187 const buttonWidth = stackRef.current.children[0].clientWidth;
188 const availableSpace = props.gridItemRef.current.clientWidth;
189 let availableButtons = Math.floor((availableSpace - 1) / buttonWidth);
190 if (availableButtons < SECONDARY_BUTTONS.length) {
191 availableButtons -= 1; // Leave room for CallingMoreVerticalButton
simon33c06182022-11-02 17:39:31 -0400192 }
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500193 setHiddenStackCount(SECONDARY_BUTTONS.length - availableButtons);
194 }
195 }, [props.gridItemRef]);
196
197 useLayoutEffect(() => {
198 // Run once, at the beginning, for initial measurements
199 if (!initialMeasurementDone) {
200 calculateStackCount();
201 setInitialMeasurementDone(true);
202 }
203
204 const onResize = () => {
205 calculateStackCount();
simon33c06182022-11-02 17:39:31 -0400206 };
207 window.addEventListener('resize', onResize);
208 return () => {
209 window.removeEventListener('resize', onResize);
210 };
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500211 }, [calculateStackCount, initialMeasurementDone]);
simon33c06182022-11-02 17:39:31 -0400212
213 const { displayedButtons, hiddenButtons } = useMemo(() => {
214 const displayedButtons: ComponentType<ExpandableButtonProps>[] = [];
215 const hiddenButtons: ComponentType<ExpandableButtonProps>[] = [];
216 SECONDARY_BUTTONS.forEach((button, i) => {
217 if (i < SECONDARY_BUTTONS.length - hiddenStackCount) {
218 displayedButtons.push(button);
219 } else {
220 hiddenButtons.push(button);
221 }
222 });
223
224 return {
225 displayedButtons,
226 hiddenButtons,
227 };
228 }, [hiddenStackCount]);
229
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400230 return (
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500231 <Card sx={{ backgroundColor: '#00000088', overflow: 'visible', height: '100%' }}>
232 <Stack direction="row" justifyContent="center" alignItems="center" height="100%" ref={stackRef}>
233 {initialMeasurementDone &&
234 displayedButtons.map((SecondaryButton, i) => (
235 <Fragment key={i}>
simon9a8fe202022-11-15 18:25:49 -0500236 <SecondaryButton />
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500237 </Fragment>
238 ))}
239 {(!!hiddenButtons.length || !initialMeasurementDone) && (
simon9a8fe202022-11-15 18:25:49 -0500240 <CallingMoreVerticalButton isVertical onClick={() => setHiddenMenuVisible(!hiddenMenuVisible)} />
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500241 )}
242 </Stack>
243
244 {!!hiddenButtons.length && hiddenMenuVisible && (
245 <Box sx={{ position: 'absolute', right: 0, bottom: '50px' }}>
246 <Card sx={{ backgroundColor: '#00000088', overflow: 'visible', justifyContent: 'flex-end' }}>
simon33c06182022-11-02 17:39:31 -0400247 <Stack
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500248 direction="column"
249 justifyContent="flex-end"
250 alignItems="flex-end"
251 sx={{ bottom: 0, right: 0, height: '100%' }}
simon33c06182022-11-02 17:39:31 -0400252 >
253 {hiddenButtons.map((SecondaryButton, i) => (
254 <Fragment key={i}>
simon4e7445c2022-11-16 21:18:46 -0500255 <SecondaryButton isVertical />
simon33c06182022-11-02 17:39:31 -0400256 </Fragment>
257 ))}
258 </Stack>
259 </Card>
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500260 </Box>
261 )}
Gabriel Rochone3ec0d22022-10-08 14:27:03 -0400262 </Card>
263 );
264};