// Copyright 2020-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { useCallback } from 'react'; import { CallNeedPermissionScreen } from './CallNeedPermissionScreen'; import { CallScreen } from './CallScreen'; import { CallingLobby } from './CallingLobby'; import { CallingParticipantsList } from './CallingParticipantsList'; import { CallingPip } from './CallingPip'; import { IncomingCallBar } from './IncomingCallBar'; import { SafetyNumberChangeDialog, SafetyNumberProps, } from './SafetyNumberChangeDialog'; import { ActiveCallType, CallEndedReason, CallMode, CallState, GroupCallJoinState, GroupCallVideoRequest, VideoFrameSource, } from '../types/Calling'; import { ConversationType } from '../state/ducks/conversations'; import { AcceptCallType, CancelCallType, DeclineCallType, DirectCallStateType, HangUpType, KeyChangeOkType, SetGroupCallVideoRequestType, SetLocalAudioType, SetLocalPreviewType, SetLocalVideoType, SetRendererCanvasType, StartCallType, } from '../state/ducks/calling'; import { LocalizerType } from '../types/Util'; import { missingCaseError } from '../util/missingCaseError'; type MeType = ConversationType & { uuid: string; }; export type PropsType = { activeCall?: ActiveCallType; availableCameras: Array; cancelCall: (_: CancelCallType) => void; closeNeedPermissionScreen: () => void; getGroupCallVideoFrameSource: ( conversationId: string, demuxId: number ) => VideoFrameSource; incomingCall?: { call: DirectCallStateType; conversation: ConversationType; }; keyChangeOk: (_: KeyChangeOkType) => void; renderDeviceSelection: () => JSX.Element; renderSafetyNumberViewer: (props: SafetyNumberProps) => JSX.Element; startCall: (payload: StartCallType) => void; toggleParticipants: () => void; acceptCall: (_: AcceptCallType) => void; declineCall: (_: DeclineCallType) => void; i18n: LocalizerType; me: MeType; setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void; setLocalAudio: (_: SetLocalAudioType) => void; setLocalVideo: (_: SetLocalVideoType) => void; setLocalPreview: (_: SetLocalPreviewType) => void; setRendererCanvas: (_: SetRendererCanvasType) => void; hangUp: (_: HangUpType) => void; togglePip: () => void; toggleSettings: () => void; toggleSpeakerView: () => void; }; type ActiveCallManagerPropsType = PropsType & { activeCall: ActiveCallType; }; const ActiveCallManager: React.FC = ({ activeCall, availableCameras, cancelCall, closeNeedPermissionScreen, hangUp, i18n, keyChangeOk, getGroupCallVideoFrameSource, me, renderDeviceSelection, renderSafetyNumberViewer, setGroupCallVideoRequest, setLocalAudio, setLocalPreview, setLocalVideo, setRendererCanvas, startCall, toggleParticipants, togglePip, toggleSettings, toggleSpeakerView, }) => { const { conversation, hasLocalAudio, hasLocalVideo, joinedAt, peekedParticipants, pip, settingsDialogOpen, showParticipantsList, } = activeCall; const cancelActiveCall = useCallback(() => { cancelCall({ conversationId: conversation.id }); }, [cancelCall, conversation.id]); const joinActiveCall = useCallback(() => { startCall({ callMode: activeCall.callMode, conversationId: conversation.id, hasLocalAudio, hasLocalVideo, }); }, [ startCall, activeCall.callMode, conversation.id, hasLocalAudio, hasLocalVideo, ]); const getGroupCallVideoFrameSourceForActiveCall = useCallback( (demuxId: number) => { return getGroupCallVideoFrameSource(conversation.id, demuxId); }, [getGroupCallVideoFrameSource, conversation.id] ); const setGroupCallVideoRequestForConversation = useCallback( (resolutions: Array) => { setGroupCallVideoRequest({ conversationId: conversation.id, resolutions, }); }, [setGroupCallVideoRequest, conversation.id] ); let isCallFull: boolean; let showCallLobby: boolean; switch (activeCall.callMode) { case CallMode.Direct: { const { callState, callEndedReason } = activeCall; const ended = callState === CallState.Ended; if ( ended && callEndedReason === CallEndedReason.RemoteHangupNeedPermission ) { return ( ); } showCallLobby = !callState; isCallFull = false; break; } case CallMode.Group: { showCallLobby = activeCall.joinState === GroupCallJoinState.NotJoined; isCallFull = activeCall.deviceCount >= activeCall.maxDevices; break; } default: throw missingCaseError(activeCall); } if (showCallLobby) { return ( <> {settingsDialogOpen && renderDeviceSelection()} {showParticipantsList && activeCall.callMode === CallMode.Group ? ( ) : null} ); } if (pip) { return ( ); } const groupCallParticipantsForParticipantsList = activeCall.callMode === CallMode.Group ? [ ...activeCall.remoteParticipants.map(participant => ({ ...participant, hasAudio: participant.hasRemoteAudio, hasVideo: participant.hasRemoteVideo, })), { ...me, hasAudio: hasLocalAudio, hasVideo: hasLocalVideo, }, ] : []; return ( <> {settingsDialogOpen && renderDeviceSelection()} {showParticipantsList && activeCall.callMode === CallMode.Group ? ( ) : null} {activeCall.callMode === CallMode.Group && activeCall.conversationsWithSafetyNumberChanges.length ? ( { hangUp({ conversationId: activeCall.conversation.id }); }} onConfirm={() => { keyChangeOk({ conversationId: activeCall.conversation.id }); }} renderSafetyNumber={renderSafetyNumberViewer} /> ) : null} ); }; export const CallManager: React.FC = props => { const { activeCall, incomingCall, acceptCall, declineCall, i18n } = props; if (activeCall) { // `props` should logically have an `activeCall` at this point, but TypeScript can't // figure that out, so we pass it in again. return ; } // In the future, we may want to show the incoming call bar when a call is active. if (incomingCall) { return ( ); } return null; };