/* * Wire * Copyright (C) 2020 Wire Swiss GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * */ import React, {useEffect, useState} from 'react'; import {DefaultConversationRoleName} from '@wireapp/api-client/lib/conversation/'; import {TabIndex} from '@wireapp/react-ui-kit/lib/types/enums'; import classNames from 'classnames'; import {container} from 'tsyringe'; import {CALL_TYPE} from '@wireapp/avs'; import { Checkbox, CheckboxLabel, EmojiIcon, GridIcon, IconButton, IconButtonVariant, Select, } from '@wireapp/react-ui-kit'; import {useCallAlertState} from 'Components/calling/useCallAlertState'; import {ConversationClassifiedBar} from 'Components/ClassifiedBar/ClassifiedBar'; import * as Icon from 'Components/Icon'; import {ModalComponent} from 'Components/Modals/ModalComponent'; import {CallingRepository} from 'src/script/calling/CallingRepository'; import {Config} from 'src/script/Config'; import {isCallViewOption} from 'src/script/guards/CallView'; import {isMediaDevice} from 'src/script/guards/MediaDevice'; import {useActiveWindowMatchMedia} from 'src/script/hooks/useActiveWindowMatchMedia'; import {useToggleState} from 'src/script/hooks/useToggleState'; import {MediaDeviceType} from 'src/script/media/MediaDeviceType'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; import {isDetachedCallingFeatureEnabled} from 'Util/isDetachedCallingFeatureEnabled'; import {handleKeyDown, isEscapeKey} from 'Util/KeyboardUtil'; import {t} from 'Util/LocalizerUtil'; import {preventFocusOutside} from 'Util/util'; import {CallingParticipantList} from './CallingCell/CallIngParticipantList'; import {Duration} from './Duration'; import { videoControlActiveStyles, videoControlInActiveStyles, videoControlDisabledStyles, paginationButtonStyles, classifiedBarStyles, headerActionsWrapperStyles, paginationWrapperStyles, videoTopBarStyles, } from './FullscreenVideoCall.styles'; import {GroupVideoGrid} from './GroupVideoGrid'; import {Pagination} from './Pagination'; import type {Call} from '../../calling/Call'; import {CallingViewMode, CallState, MuteState} from '../../calling/CallState'; import {Participant} from '../../calling/Participant'; import type {Grid} from '../../calling/videoGridHandler'; import type {Conversation} from '../../entity/Conversation'; import {ElectronDesktopCapturerSource, MediaDevicesHandler} from '../../media/MediaDevicesHandler'; import {TeamState} from '../../team/TeamState'; import {CallViewTab} from '../../view_model/CallingViewModel'; enum BlurredBackgroundStatus { OFF = 'bluroff', ON = 'bluron', } export interface FullscreenVideoCallProps { activeCallViewTab: string; call: Call; canShareScreen: boolean; changePage: (newPage: number, call: Call) => void; conversation: Conversation; isChoosingScreen: boolean; isMuted: boolean; leave: (call: Call) => void; maximizedParticipant: Participant | null; callingRepository: CallingRepository; mediaDevicesHandler: MediaDevicesHandler; muteState: MuteState; setActiveCallViewTab: (tab: CallViewTab) => void; setMaximizedParticipant: (call: Call, participant: Participant | null) => void; switchCameraInput: (deviceId: string) => void; switchMicrophoneInput: (deviceId: string) => void; switchSpeakerOutput: (deviceId: string) => void; switchBlurredBackground: (status: boolean) => void; teamState?: TeamState; callState?: CallState; toggleCamera: (call: Call) => void; toggleMute: (call: Call, muteState: boolean) => void; toggleScreenshare: (call: Call) => void; sendEmoji: (emoji: string, call: Call) => void; videoGrid: Grid; } const EMOJIS_LIST = ['👍', '🎉', '❤️', '😂', '😮', '👏', '🤔', '😢', '👎']; const LOCAL_STORAGE_KEY_FOR_SCREEN_SHARING_CONFIRM_MODAL = 'DO_NOT_ASK_AGAIN_FOR_SCREEN_SHARING_CONFIRM_MODAL'; const FullscreenVideoCall: React.FC = ({ call, canShareScreen, conversation, isChoosingScreen, sendEmoji, isMuted, muteState, mediaDevicesHandler, callingRepository, videoGrid, maximizedParticipant, activeCallViewTab, switchCameraInput, switchMicrophoneInput, switchSpeakerOutput, switchBlurredBackground, setMaximizedParticipant, setActiveCallViewTab, toggleMute, toggleCamera, toggleScreenshare, leave, changePage, teamState = container.resolve(TeamState), callState = container.resolve(CallState), }) => { const [isConfirmCloseModalOpen, setIsConfirmCloseModalOpen] = useState(false); const [showEmojisBar, setShowEmojisBar] = useState(false); const [disabledEmojis, setDisabledEmojis] = useState([]); const selfParticipant = call.getSelfParticipant(); const {sharesScreen: selfSharesScreen, sharesCamera: selfSharesCamera} = useKoSubscribableChildren(selfParticipant, [ 'sharesScreen', 'sharesCamera', ]); const {blurredVideoStream} = useKoSubscribableChildren(selfParticipant, ['blurredVideoStream']); const hasBlurredBackground = !!blurredVideoStream; const { activeSpeakers, currentPage, pages: callPages, startedAt, participants, } = useKoSubscribableChildren(call, ['activeSpeakers', 'currentPage', 'pages', 'startedAt', 'participants']); const {display_name: conversationName} = useKoSubscribableChildren(conversation, ['display_name']); const {isVideoCallingEnabled, classifiedDomains} = useKoSubscribableChildren(teamState, [ 'isVideoCallingEnabled', 'classifiedDomains', ]); const { [MediaDeviceType.VIDEO_INPUT]: currentCameraDevice, [MediaDeviceType.AUDIO_INPUT]: currentMicrophoneDevice, [MediaDeviceType.AUDIO_OUTPUT]: currentSpeakerDevice, } = useKoSubscribableChildren(mediaDevicesHandler.currentDeviceId, [ MediaDeviceType.VIDEO_INPUT, MediaDeviceType.AUDIO_INPUT, MediaDeviceType.AUDIO_OUTPUT, ]); const {videoinput, audioinput, audiooutput} = useKoSubscribableChildren(mediaDevicesHandler.availableDevices, [ MediaDeviceType.VIDEO_INPUT, MediaDeviceType.AUDIO_INPUT, MediaDeviceType.AUDIO_OUTPUT, ]); const {selfUser, roles} = useKoSubscribableChildren(conversation, ['selfUser', 'roles']); const {emojis, viewMode, isScreenSharingSourceFromDetachedWindow} = useKoSubscribableChildren(callState, [ 'emojis', 'viewMode', 'isScreenSharingSourceFromDetachedWindow', ]); const [audioOptionsOpen, setAudioOptionsOpen] = useState(false); const [videoOptionsOpen, setVideoOptionsOpen] = useState(false); const minimize = () => { const isSharingScreen = call?.getSelfParticipant().sharesScreen(); const hasAlreadyConfirmed = localStorage.getItem(LOCAL_STORAGE_KEY_FOR_SCREEN_SHARING_CONFIRM_MODAL) === 'true'; if (isSharingScreen && isScreenSharingSourceFromDetachedWindow && !hasAlreadyConfirmed) { setIsConfirmCloseModalOpen(true); return; } callingRepository.setViewModeMinimized(); }; const openPopup = () => callingRepository.setViewModeDetached(); const [isParticipantsListOpen, toggleParticipantsList] = useToggleState(false); const [isCallViewOpen, toggleCallView] = useToggleState(false); useEffect(() => { const onKeyDown = (event: KeyboardEvent): void => { if (viewMode !== CallingViewMode.FULL_SCREEN) { return; } event.preventDefault(); event.stopPropagation(); preventFocusOutside(event, 'video-calling'); }; document.addEventListener('keydown', onKeyDown); return () => { document.removeEventListener('keydown', onKeyDown); }; }, [viewMode]); const showToggleVideo = isVideoCallingEnabled && (call.initialType === CALL_TYPE.VIDEO || conversation.supportsVideoCall(call.isConference)); const showSwitchMicrophone = audioinput.length > 1; const callViewOptions = [ { label: t('videoCallOverlayViewModeLabel'), options: [ { label: t('videoCallOverlayViewModeAll'), value: CallViewTab.ALL, }, { label: t('videoCallOverlayViewModeSpeakers'), value: CallViewTab.SPEAKERS, }, ], }, ]; const selectedCallViewOption = callViewOptions[0].options.find(option => option.value === activeCallViewTab) ?? callViewOptions[0].options[0]; const audioOptions = [ { label: t('videoCallaudioInputMicrophone'), options: audioinput.map((device: MediaDeviceInfo | ElectronDesktopCapturerSource) => { return isMediaDevice(device) ? { label: device.label, value: `${device.deviceId}-input`, dataUieName: `${device.deviceId}-input`, id: device.deviceId, } : { label: device.name, value: `${device.id}-input`, dataUieName: `${device.id}-input`, id: device.id, }; }), }, { label: t('videoCallaudioOutputSpeaker'), options: audiooutput.map((device: MediaDeviceInfo | ElectronDesktopCapturerSource) => { return isMediaDevice(device) ? { label: device.label, value: `${device.deviceId}-output`, dataUieName: `${device.deviceId}-output`, id: device.deviceId, } : { label: device.name, value: `${device.id}-output`, dataUieName: `${device.id}-output`, id: device.id, }; }), }, ]; const [selectedAudioOptions, setSelectedAudioOptions] = useState(() => [currentMicrophoneDevice, currentSpeakerDevice].flatMap( (device, index) => audioOptions[index].options.find(item => item.id === device) ?? audioOptions[index].options[0], ), ); const updateAudioOptions = (selectedOption: string, input: boolean) => { const microphone = input ? audioOptions[0].options.find(item => item.value === selectedOption) ?? selectedAudioOptions[0] : selectedAudioOptions[0]; const speaker = !input ? audioOptions[1].options.find(item => item.value === selectedOption) ?? selectedAudioOptions[1] : selectedAudioOptions[1]; setSelectedAudioOptions([microphone, speaker]); switchMicrophoneInput(microphone.id); switchSpeakerOutput(speaker.id); }; const isBlurredBackgroundEnabled = Config.getConfig().FEATURE.ENABLE_BLUR_BACKGROUND; const blurredBackgroundOptions = { label: t('videoCallbackgroundBlurHeadline'), options: [ { // Blurring is not possible if webgl context is not available isDisabled: !document.createElement('canvas').getContext('webgl2'), label: t('videoCallbackgroundBlur'), value: BlurredBackgroundStatus.ON, dataUieName: 'blur', id: BlurredBackgroundStatus.ON, }, { label: t('videoCallbackgroundNotBlurred'), value: BlurredBackgroundStatus.OFF, dataUieName: 'no-blur', id: BlurredBackgroundStatus.OFF, }, ], }; const videoOptions = [ { label: t('videoCallvideoInputCamera'), options: videoinput.map((device: MediaDeviceInfo | ElectronDesktopCapturerSource) => { return isMediaDevice(device) ? { label: device.label, value: device.deviceId, dataUieName: device.deviceId, id: device.deviceId, } : { label: device.name, value: device.id, dataUieName: device.id, id: device.id, }; }), }, ...(isBlurredBackgroundEnabled ? [blurredBackgroundOptions] : []), ]; const selectedVideoOptions = [currentCameraDevice, hasBlurredBackground] .flatMap(device => videoOptions.flatMap(options => options.options.filter(item => item.id === device)) ?? []) .concat(hasBlurredBackground ? blurredBackgroundOptions.options[0] : blurredBackgroundOptions.options[1]); const updateVideoOptions = (selectedOption: string | BlurredBackgroundStatus) => { const camera = videoOptions[0].options.find(item => item.value === selectedOption) ?? selectedVideoOptions[0]; if (selectedOption === BlurredBackgroundStatus.ON) { switchBlurredBackground(true); } else if (selectedOption === BlurredBackgroundStatus.OFF) { switchBlurredBackground(false); } else { switchCameraInput(camera.id); } }; const onEmojiClick = (selectedEmoji: string) => { setDisabledEmojis(prev => [...prev, selectedEmoji]); sendEmoji(selectedEmoji, call); setTimeout(() => { setDisabledEmojis(prev => [...prev].filter(emoji => emoji !== selectedEmoji)); }, CallingRepository.EMOJI_TIME_OUT_DURATION); }; const {showAlert, isGroupCall, clearShowAlert} = useCallAlertState(); const totalPages = callPages.length; // To be changed when design chooses a breakpoint, the conditional can be integrated to the ui-kit directly const horizontalSmBreakpoint = useActiveWindowMatchMedia('max-width: 680px'); const horizontalXsBreakpoint = useActiveWindowMatchMedia('max-width: 500px'); const callGroupStartedAlert = t(isGroupCall ? 'startedVideoGroupCallingAlert' : 'startedVideoCallingAlert', { conversationName, cameraStatus: t(selfSharesCamera ? 'cameraStatusOn' : 'cameraStatusOff'), }); const onGoingGroupCallAlert = t(isGroupCall ? 'ongoingGroupVideoCall' : 'ongoingVideoCall', { conversationName, cameraStatus: t(selfSharesCamera ? 'cameraStatusOn' : 'cameraStatusOff'), }); const isModerator = selfUser && roles[selfUser.id] === DefaultConversationRoleName.WIRE_ADMIN; return (
{horizontalSmBreakpoint && ( )} {/* Calling conversation name and duration */}
{ if (showAlert) { element?.focus(); } }} onBlur={() => clearShowAlert()} >

{conversationName}

{muteState === MuteState.REMOTE_MUTED && (
{t('muteStateRemoteMute')}
)}
{!maximizedParticipant && activeCallViewTab === CallViewTab.ALL && totalPages > 1 && (
changePage(newPage, call)} />
)} {isDetachedCallingFeatureEnabled() && viewMode !== CallingViewMode.DETACHED_WINDOW && ( )}
setMaximizedParticipant(call, participant)} /> {classifiedDomains && ( )}
{!isChoosingScreen && (
{emojis.map(({id, emoji, left, from}) => (
))}
    {!horizontalSmBreakpoint && (
  • )}
  • {showSwitchMicrophone && (
  • )}
  • {!horizontalXsBreakpoint && (
    {participants.length > 2 && (