// 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 { isGroupCallOutboundRingEnabled } from '../../util/isGroupCallOutboundRingEnabled'; import { ActiveCallType, CallMode, CallState, GroupCallRemoteParticipantType, } from '../../types/Calling'; import { StateType } from '../reducer'; import { missingCaseError } from '../../util/missingCaseError'; import { SmartCallingDeviceSelection } from './CallingDeviceSelection'; import { SmartSafetyNumberViewer, Props as SafetyNumberViewerProps, } from './SafetyNumberViewer'; import { notify } from '../../services/notify'; import { callingTones } from '../../util/callingTones'; import { bounceAppIconStart, bounceAppIconStop, } from '../../shims/bounceAppIcon'; function renderDeviceSelection(): JSX.Element { return ; } function renderSafetyNumberViewer(props: SafetyNumberViewerProps): JSX.Element { return ; } const getGroupCallVideoFrameSource = callingService.getGroupCallVideoFrameSource.bind( callingService ); async function notifyForCall( title: string, isVideoCall: boolean ): Promise { const shouldNotify = !window.isActive() && window.Events.getCallSystemNotification(); if (!shouldNotify) { return; } notify({ title, icon: isVideoCall ? 'images/icons/v2/video-solid-24.svg' : 'images/icons/v2/phone-right-solid-24.svg', message: window.i18n( isVideoCall ? 'incomingVideoCall' : 'incomingAudioCall' ), onNotificationClick: () => { window.showWindow(); }, silent: false, }); } const playRingtone = callingTones.playRingtone.bind(callingTones); const stopRingtone = callingTones.stopRingtone.bind(callingTones); 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, outgoingRing: activeCallState.outgoingRing, 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: if ( call.isIncoming && (call.callState === CallState.Prering || call.callState === CallState.Ringing) ) { return; } 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 groupMembers: Array = []; const remoteParticipants: Array = []; const peekedParticipants: Array = []; const { memberships = [] } = conversation; for (let i = 0; i < memberships.length; i += 1) { const { conversationId } = memberships[i]; const member = conversationSelectorByUuid(conversationId); if (!member) { window.log.error('Group member has no corresponding conversation'); continue; } groupMembers.push(member); } 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, groupMembers, 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; } switch (call.callMode) { case CallMode.Direct: return { callMode: CallMode.Direct as const, conversation, isVideoCall: call.isVideoCall, }; case CallMode.Group: { if (!call.ringerUuid) { window.log.error('The incoming group call has no ring state'); return undefined; } const conversationSelector = getConversationSelector(state); const ringer = conversationSelector(call.ringerUuid); const otherMembersRung = (conversation.sortedGroupMembers ?? []).filter( c => c.id !== ringer.id && !c.isMe ); return { callMode: CallMode.Group as const, conversation, otherMembersRung, ringer, }; } default: throw missingCaseError(call); } }; const mapStateToProps = (state: StateType) => ({ activeCall: mapStateToActiveCallProp(state), bounceAppIconStart, bounceAppIconStop, availableCameras: state.calling.availableCameras, getGroupCallVideoFrameSource, i18n: getIntl(state), isGroupCallOutboundRingEnabled: isGroupCallOutboundRingEnabled(), 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), }, notifyForCall, playRingtone, stopRingtone, renderDeviceSelection, renderSafetyNumberViewer, }); const smart = connect(mapStateToProps, mapDispatchToProps); export const SmartCallManager = smart(CallManager);