blob: 2ecc4fc636186f76303f94cdcec1a0b80849227c [file] [log] [blame]
simon1170c322022-10-31 14:51:31 -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 */
simon33c06182022-11-02 17:39:31 -040018
simonf929a362022-11-18 16:53:45 -050019import { IconButton, IconButtonProps, PaletteColor } from '@mui/material';
20import { styled, Theme } from '@mui/material/styles';
simon492e8402022-11-29 16:48:37 -050021import { ChangeEvent, useContext, useMemo } from 'react';
simon4e7445c2022-11-16 21:18:46 -050022import { useTranslation } from 'react-i18next';
simon1170c322022-10-31 14:51:31 -040023
Charlie9c8779e2022-11-29 20:10:56 -050024import { CallContext, CallStatus } from '../contexts/CallProvider';
simon9076a9a2022-11-29 17:13:01 -050025import {
26 ExpandableButton,
27 ExpandableButtonProps,
28 ExpandMenuRadioOption,
29 ShapedButtonProps,
30 ToggleIconButton,
31} from './Button';
simon1170c322022-10-31 14:51:31 -040032import {
33 CallEndIcon,
34 ChatBubbleIcon,
35 ExtensionIcon,
simon33c06182022-11-02 17:39:31 -040036 FileIcon,
37 FullScreenIcon,
simon1170c322022-10-31 14:51:31 -040038 GroupAddIcon,
39 MicroIcon,
40 MicroOffIcon,
simon33c06182022-11-02 17:39:31 -040041 MoreVerticalIcon,
simon2d3b6532022-11-08 21:01:57 -050042 PlaceAudioCallIcon,
simon1170c322022-10-31 14:51:31 -040043 RecordingIcon,
simon2d3b6532022-11-08 21:01:57 -050044 RoundCloseIcon,
simon33c06182022-11-02 17:39:31 -040045 ScreenShareArrowIcon,
46 ScreenShareRegularIcon,
47 ScreenShareScreenAreaIcon,
simon1170c322022-10-31 14:51:31 -040048 VideoCameraIcon,
49 VideoCameraOffIcon,
50 VolumeIcon,
Gabriel Rochon8321a0d2022-11-06 23:18:36 -050051 WindowIcon,
simon1170c322022-10-31 14:51:31 -040052} from './SvgIcon';
53
Gabriel Rochon8321a0d2022-11-06 23:18:36 -050054const CallButton = styled((props: ExpandableButtonProps) => {
55 return <ExpandableButton {...props} />;
56})({
simon2d3b6532022-11-08 21:01:57 -050057 color: 'white',
Gabriel Rochon8321a0d2022-11-06 23:18:36 -050058 '&:hover': {
59 backgroundColor: 'rgba(255, 255, 255, 0.15)',
60 },
Gabriel Rochon8321a0d2022-11-06 23:18:36 -050061});
62
simon2d3b6532022-11-08 21:01:57 -050063const ColoredCallButton = styled(
64 ({
simonf929a362022-11-18 16:53:45 -050065 paletteColor,
66 Icon,
simon2d3b6532022-11-08 21:01:57 -050067 ...props
simonf929a362022-11-18 16:53:45 -050068 }: ShapedButtonProps & {
69 paletteColor?: PaletteColor | ((theme: Theme) => PaletteColor);
simon2d3b6532022-11-08 21:01:57 -050070 }) => {
simonf929a362022-11-18 16:53:45 -050071 return (
72 <IconButton {...props} disableRipple={true}>
73 <Icon fontSize="inherit" />
74 </IconButton>
75 );
simon2d3b6532022-11-08 21:01:57 -050076 }
simonf929a362022-11-18 16:53:45 -050077)(({ theme, paletteColor = theme.palette.primary }) => {
78 if (typeof paletteColor === 'function') {
79 paletteColor = paletteColor(theme);
80 }
81
simon2d3b6532022-11-08 21:01:57 -050082 return {
simonf929a362022-11-18 16:53:45 -050083 color: paletteColor.contrastText,
84 backgroundColor: paletteColor.dark,
simon2d3b6532022-11-08 21:01:57 -050085 '&:hover': {
simonf929a362022-11-18 16:53:45 -050086 backgroundColor: paletteColor.main,
simon2d3b6532022-11-08 21:01:57 -050087 },
88 };
Gabriel Rochon8321a0d2022-11-06 23:18:36 -050089});
90
simon33c06182022-11-02 17:39:31 -040091export const CallingChatButton = (props: ExpandableButtonProps) => {
simonf9d78f22022-11-25 15:47:15 -050092 const { setIsChatShown } = useContext(CallContext);
93 return (
94 <CallButton
95 aria-label="chat"
96 Icon={ChatBubbleIcon}
97 onClick={() => {
98 setIsChatShown((v) => !v);
99 }}
100 {...props}
101 />
102 );
simon1170c322022-10-31 14:51:31 -0400103};
simon33c06182022-11-02 17:39:31 -0400104
105export const CallingEndButton = (props: ExpandableButtonProps) => {
simonaccd8022022-11-24 15:04:53 -0500106 const { endCall } = useContext(CallContext);
simonf929a362022-11-18 16:53:45 -0500107 return (
108 <ColoredCallButton
109 paletteColor={(theme) => theme.palette.error}
110 onClick={() => {
simonaccd8022022-11-24 15:04:53 -0500111 endCall();
simonf929a362022-11-18 16:53:45 -0500112 }}
113 aria-label="call end"
114 Icon={CallEndIcon}
115 {...props}
116 />
117 );
simon1170c322022-10-31 14:51:31 -0400118};
simon33c06182022-11-02 17:39:31 -0400119
120export const CallingExtensionButton = (props: ExpandableButtonProps) => {
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500121 return <CallButton aria-label="extensions" Icon={ExtensionIcon} {...props} />;
simon1170c322022-10-31 14:51:31 -0400122};
simon33c06182022-11-02 17:39:31 -0400123
124export const CallingFullScreenButton = (props: ExpandableButtonProps) => {
simon2a5cf142022-11-25 15:47:35 -0500125 const { setIsFullscreen } = useContext(CallContext);
126 return (
127 <CallButton
128 aria-label="full screen"
129 Icon={FullScreenIcon}
130 onClick={() => {
131 setIsFullscreen((v) => !v);
132 }}
133 {...props}
134 />
135 );
simon1170c322022-10-31 14:51:31 -0400136};
simon33c06182022-11-02 17:39:31 -0400137
138export const CallingGroupButton = (props: ExpandableButtonProps) => {
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500139 return <CallButton aria-label="group options" Icon={GroupAddIcon} {...props} />;
simon1170c322022-10-31 14:51:31 -0400140};
simon33c06182022-11-02 17:39:31 -0400141
142export const CallingMoreVerticalButton = (props: ExpandableButtonProps) => {
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500143 return <CallButton aria-label="more vertical" Icon={MoreVerticalIcon} {...props} />;
simon1170c322022-10-31 14:51:31 -0400144};
simon33c06182022-11-02 17:39:31 -0400145
146export const CallingRecordButton = (props: ExpandableButtonProps) => {
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500147 return <CallButton aria-label="recording options" Icon={RecordingIcon} {...props} />;
simon33c06182022-11-02 17:39:31 -0400148};
149
150export const CallingScreenShareButton = (props: ExpandableButtonProps) => {
simon4e7445c2022-11-16 21:18:46 -0500151 const { t } = useTranslation();
simon1170c322022-10-31 14:51:31 -0400152 return (
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500153 <CallButton
simon33c06182022-11-02 17:39:31 -0400154 aria-label="screen share"
155 Icon={ScreenShareArrowIcon}
156 expandMenuOptions={[
157 {
simon4e7445c2022-11-16 21:18:46 -0500158 description: t('share_screen'),
simon33c06182022-11-02 17:39:31 -0400159 icon: <ScreenShareRegularIcon />,
160 },
161 {
simon4e7445c2022-11-16 21:18:46 -0500162 description: t('share_window'),
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500163 icon: <WindowIcon />,
164 },
165 {
simon4e7445c2022-11-16 21:18:46 -0500166 description: t('share_screen_area'),
simon33c06182022-11-02 17:39:31 -0400167 icon: <ScreenShareScreenAreaIcon />,
168 },
169 {
simon4e7445c2022-11-16 21:18:46 -0500170 description: t('share_file'),
simon33c06182022-11-02 17:39:31 -0400171 icon: <FileIcon />,
172 },
173 ]}
174 {...props}
175 />
simon1170c322022-10-31 14:51:31 -0400176 );
177};
178
simon9076a9a2022-11-29 17:13:01 -0500179const useMediaDeviceExpandMenuOptions = (kind: MediaDeviceKind): ExpandMenuRadioOption[] | undefined => {
simon492e8402022-11-29 16:48:37 -0500180 const { currentMediaDeviceIds, mediaDevices } = useContext(CallContext);
simon9a8fe202022-11-15 18:25:49 -0500181
simon9076a9a2022-11-29 17:13:01 -0500182 const options = useMemo(
simon9a8fe202022-11-15 18:25:49 -0500183 () =>
184 mediaDevices[kind].map((device) => ({
185 key: device.deviceId,
186 description: device.label,
187 })),
188 [mediaDevices, kind]
189 );
simon9076a9a2022-11-29 17:13:01 -0500190
simon492e8402022-11-29 16:48:37 -0500191 const currentDevice = currentMediaDeviceIds[kind];
192
193 if (options.length === 0) {
194 return undefined;
195 }
196 return [
197 {
198 options,
199 value: currentDevice.id ?? '',
200 onChange: (e: ChangeEvent<HTMLInputElement>) => {
201 currentDevice.setId(e.target.value);
202 },
203 },
204 ];
simon9a8fe202022-11-15 18:25:49 -0500205};
206
simon33c06182022-11-02 17:39:31 -0400207export const CallingVolumeButton = (props: ExpandableButtonProps) => {
simon9a8fe202022-11-15 18:25:49 -0500208 const options = useMediaDeviceExpandMenuOptions('audiooutput');
simon492e8402022-11-29 16:48:37 -0500209 const { remoteVideoRef } = useContext(CallContext);
simon9a8fe202022-11-15 18:25:49 -0500210
simon492e8402022-11-29 16:48:37 -0500211 // Audio out options are only available on chrome and other browsers that support `setSinkId`
212 // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId#browser_compatibility
213 const hasSetSinkId = remoteVideoRef.current?.setSinkId != null;
214
215 return (
216 <CallButton
217 aria-label="volume options"
218 Icon={VolumeIcon}
219 expandMenuOptions={hasSetSinkId ? options : undefined}
220 {...props}
221 />
222 );
simon33c06182022-11-02 17:39:31 -0400223};
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500224
225export const CallingMicButton = (props: ExpandableButtonProps) => {
simon9a8fe202022-11-15 18:25:49 -0500226 const options = useMediaDeviceExpandMenuOptions('audioinput');
simon1170c322022-10-31 14:51:31 -0400227
228 return (
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500229 <CallButton
simon1170c322022-10-31 14:51:31 -0400230 aria-label="microphone options"
simon9076a9a2022-11-29 17:13:01 -0500231 expandMenuOptions={options}
simonf929a362022-11-18 16:53:45 -0500232 IconButtonComp={ToggleAudioCameraIconButton}
233 {...props}
234 />
235 );
236};
237
238const ToggleAudioCameraIconButton = (props: IconButtonProps) => {
simon9076a9a2022-11-29 17:13:01 -0500239 const { isAudioOn, setIsAudioOn } = useContext(CallContext);
simonf929a362022-11-18 16:53:45 -0500240 return (
241 <ToggleIconButton
242 IconOn={MicroIcon}
243 IconOff={MicroOffIcon}
244 selected={isAudioOn}
simon9076a9a2022-11-29 17:13:01 -0500245 toggle={() => setIsAudioOn((v) => !v)}
simon1170c322022-10-31 14:51:31 -0400246 {...props}
247 />
248 );
249};
250
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500251export const CallingVideoCameraButton = (props: ExpandableButtonProps) => {
simon9a8fe202022-11-15 18:25:49 -0500252 const options = useMediaDeviceExpandMenuOptions('videoinput');
253
simon1170c322022-10-31 14:51:31 -0400254 return (
Gabriel Rochon8321a0d2022-11-06 23:18:36 -0500255 <CallButton
simon1170c322022-10-31 14:51:31 -0400256 aria-label="camera options"
simon9076a9a2022-11-29 17:13:01 -0500257 expandMenuOptions={options}
simonf929a362022-11-18 16:53:45 -0500258 IconButtonComp={ToggleVideoCameraIconButton}
259 {...props}
260 />
261 );
262};
263
264const ToggleVideoCameraIconButton = (props: IconButtonProps) => {
simon9076a9a2022-11-29 17:13:01 -0500265 const { isVideoOn, setIsVideoOn } = useContext(CallContext);
simonf929a362022-11-18 16:53:45 -0500266 return (
267 <ToggleIconButton
268 IconOn={VideoCameraIcon}
269 IconOff={VideoCameraOffIcon}
270 selected={isVideoOn}
simon9076a9a2022-11-29 17:13:01 -0500271 toggle={() => setIsVideoOn((v) => !v)}
simon1170c322022-10-31 14:51:31 -0400272 {...props}
273 />
274 );
275};
simon2d3b6532022-11-08 21:01:57 -0500276
277// Calling pending/receiving interface
simonf929a362022-11-18 16:53:45 -0500278export const CallingCancelButton = (props: IconButtonProps) => {
simonaccd8022022-11-24 15:04:53 -0500279 const { endCall } = useContext(CallContext);
simonf929a362022-11-18 16:53:45 -0500280
281 return (
282 <ColoredCallButton
283 aria-label="cancel call"
284 onClick={() => {
simonaccd8022022-11-24 15:04:53 -0500285 endCall();
simonf929a362022-11-18 16:53:45 -0500286 }}
287 Icon={CallEndIcon}
288 paletteColor={(theme) => theme.palette.error}
289 {...props}
290 />
291 );
simon2d3b6532022-11-08 21:01:57 -0500292};
293
simonf929a362022-11-18 16:53:45 -0500294export const CallingAnswerAudioButton = (props: IconButtonProps) => {
Charlie9c8779e2022-11-29 20:10:56 -0500295 const { acceptCall, callStatus } = useContext(CallContext);
simonf929a362022-11-18 16:53:45 -0500296
297 return (
298 <ColoredCallButton
Charlie9c8779e2022-11-29 20:10:56 -0500299 disabled={callStatus === CallStatus.Loading || callStatus === CallStatus.Connecting}
simonf929a362022-11-18 16:53:45 -0500300 aria-label="answer call audio"
301 onClick={() => {
MichelleSS55164202022-11-25 18:36:14 -0500302 acceptCall(false);
simonf929a362022-11-18 16:53:45 -0500303 }}
304 Icon={PlaceAudioCallIcon}
305 paletteColor={(theme) => theme.palette.success}
306 {...props}
307 />
308 );
simon2d3b6532022-11-08 21:01:57 -0500309};
310
simonf929a362022-11-18 16:53:45 -0500311export const CallingAnswerVideoButton = (props: IconButtonProps) => {
Charlie9c8779e2022-11-29 20:10:56 -0500312 const { acceptCall, callStatus } = useContext(CallContext);
simonf929a362022-11-18 16:53:45 -0500313 return (
314 <ColoredCallButton
Charlie9c8779e2022-11-29 20:10:56 -0500315 disabled={callStatus === CallStatus.Connecting || callStatus === CallStatus.Loading}
simonf929a362022-11-18 16:53:45 -0500316 aria-label="answer call video"
317 onClick={() => {
MichelleSS55164202022-11-25 18:36:14 -0500318 acceptCall(true);
simonf929a362022-11-18 16:53:45 -0500319 }}
320 paletteColor={(theme) => theme.palette.success}
321 Icon={VideoCameraIcon}
322 {...props}
323 />
324 );
325};
326
327export const CallingRefuseButton = (props: IconButtonProps) => {
simonaccd8022022-11-24 15:04:53 -0500328 const { endCall } = useContext(CallContext);
simonf929a362022-11-18 16:53:45 -0500329 return (
330 <ColoredCallButton
331 aria-label="refuse call"
332 onClick={() => {
simonaccd8022022-11-24 15:04:53 -0500333 endCall();
simonf929a362022-11-18 16:53:45 -0500334 }}
335 paletteColor={(theme) => theme.palette.error}
336 Icon={RoundCloseIcon}
337 {...props}
338 />
339 );
simon2d3b6532022-11-08 21:01:57 -0500340};