// Copyright 2020-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; import { connect } from 'react-redux'; import { memoize } from 'lodash'; import { mapDispatchToProps } from '../actions'; import { CallManager } from '../../components/CallManager'; import { calling as callingService } from '../../services/calling'; import { getUserUuid, getIntl } from '../selectors/user'; import { getMe, getConversationSelector } from '../selectors/conversations'; import { getActiveCall } from '../ducks/calling'; import { ConversationType } from '../ducks/conversations'; import { getIncomingCall } from '../selectors/calling'; import { ActiveCallType, CallMode, GroupCallRemoteParticipantType, } from '../../types/Calling'; import { StateType } from '../reducer'; import { missingCaseError } from '../../util/missingCaseError'; import { SmartCallingDeviceSelection } from './CallingDeviceSelection'; import { SmartSafetyNumberViewer, Props as SafetyNumberViewerProps, } from './SafetyNumberViewer'; function renderDeviceSelection(): JSX.Element { return ; } function renderSafetyNumberViewer(props: SafetyNumberViewerProps): JSX.Element { return ; } const getGroupCallVideoFrameSource = callingService.getGroupCallVideoFrameSource.bind( callingService ); const mapStateToActiveCallProp = ( state: StateType ): undefined | ActiveCallType => { const { calling } = state; const { activeCallState } = calling; if (!activeCallState) { return undefined; } const call = getActiveCall(calling); if (!call) { window.log.error( 'There was an active call state but no corresponding call' ); return undefined; } const conversationSelector = getConversationSelector(state); const conversation = conversationSelector(activeCallState.conversationId); if (!conversation) { window.log.error('The active call has no corresponding conversation'); return undefined; } const conversationSelectorByUuid = memoize< (uuid: string) => undefined | ConversationType >(uuid => { const conversationId = window.ConversationController.ensureContactIds({ uuid, }); return conversationId ? conversationSelector(conversationId) : undefined; }); const baseResult = { conversation, hasLocalAudio: activeCallState.hasLocalAudio, hasLocalVideo: activeCallState.hasLocalVideo, isInSpeakerView: activeCallState.isInSpeakerView, joinedAt: activeCallState.joinedAt, pip: activeCallState.pip, presentingSource: activeCallState.presentingSource, presentingSourcesAvailable: activeCallState.presentingSourcesAvailable, settingsDialogOpen: activeCallState.settingsDialogOpen, showNeedsScreenRecordingPermissionsWarning: Boolean( activeCallState.showNeedsScreenRecordingPermissionsWarning ), showParticipantsList: activeCallState.showParticipantsList, }; switch (call.callMode) { case CallMode.Direct: return { ...baseResult, callEndedReason: call.callEndedReason, callMode: CallMode.Direct, callState: call.callState, peekedParticipants: [], remoteParticipants: [ { hasRemoteVideo: Boolean(call.hasRemoteVideo), presenting: Boolean(call.isSharingScreen), title: conversation.title, uuid: conversation.uuid, }, ], }; case CallMode.Group: { const conversationsWithSafetyNumberChanges: Array = []; const remoteParticipants: Array = []; const peekedParticipants: Array = []; for (let i = 0; i < call.remoteParticipants.length; i += 1) { const remoteParticipant = call.remoteParticipants[i]; const remoteConversation = conversationSelectorByUuid( remoteParticipant.uuid ); if (!remoteConversation) { window.log.error( 'Remote participant has no corresponding conversation' ); continue; } remoteParticipants.push({ ...remoteConversation, demuxId: remoteParticipant.demuxId, hasRemoteAudio: remoteParticipant.hasRemoteAudio, hasRemoteVideo: remoteParticipant.hasRemoteVideo, presenting: remoteParticipant.presenting, sharingScreen: remoteParticipant.sharingScreen, speakerTime: remoteParticipant.speakerTime, videoAspectRatio: remoteParticipant.videoAspectRatio, }); } for ( let i = 0; i < activeCallState.safetyNumberChangedUuids.length; i += 1 ) { const uuid = activeCallState.safetyNumberChangedUuids[i]; const remoteConversation = conversationSelectorByUuid(uuid); if (!remoteConversation) { window.log.error( 'Remote participant has no corresponding conversation' ); continue; } conversationsWithSafetyNumberChanges.push(remoteConversation); } for (let i = 0; i < call.peekInfo.uuids.length; i += 1) { const peekedParticipantUuid = call.peekInfo.uuids[i]; const peekedConversation = conversationSelectorByUuid( peekedParticipantUuid ); if (!peekedConversation) { window.log.error( 'Remote participant has no corresponding conversation' ); continue; } peekedParticipants.push(peekedConversation); } return { ...baseResult, callMode: CallMode.Group, connectionState: call.connectionState, conversationsWithSafetyNumberChanges, deviceCount: call.peekInfo.deviceCount, joinState: call.joinState, maxDevices: call.peekInfo.maxDevices, peekedParticipants, remoteParticipants, }; } default: throw missingCaseError(call); } }; const mapStateToIncomingCallProp = (state: StateType) => { const call = getIncomingCall(state); if (!call) { return undefined; } const conversation = getConversationSelector(state)(call.conversationId); if (!conversation) { window.log.error('The incoming call has no corresponding conversation'); return undefined; } return { call, conversation, }; }; const mapStateToProps = (state: StateType) => ({ activeCall: mapStateToActiveCallProp(state), availableCameras: state.calling.availableCameras, getGroupCallVideoFrameSource, i18n: getIntl(state), incomingCall: mapStateToIncomingCallProp(state), me: { ...getMe(state), // `getMe` returns a `ConversationType` which might not have a UUID, at least // according to the type. This ensures one is set. uuid: getUserUuid(state), }, renderDeviceSelection, renderSafetyNumberViewer, }); const smart = connect(mapStateToProps, mapDispatchToProps); export const SmartCallManager = smart(CallManager);