signal-desktop/ts/state/smart/CallManager.tsx

357 lines
11 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2020 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
2020-08-27 00:03:42 +00:00
import React from 'react';
2020-06-04 18:16:19 +00:00
import { connect } from 'react-redux';
2020-12-02 18:14:03 +00:00
import { memoize } from 'lodash';
2020-06-04 18:16:19 +00:00
import { mapDispatchToProps } from '../actions';
import { CallManager } from '../../components/CallManager';
2020-11-13 19:57:55 +00:00
import { calling as callingService } from '../../services/calling';
import { getIntl, getTheme } from '../selectors/user';
import { getMe, getConversationSelector } from '../selectors/conversations';
2020-12-02 18:14:03 +00:00
import { getActiveCall } from '../ducks/calling';
import type { ConversationType } from '../ducks/conversations';
2020-11-13 19:57:55 +00:00
import { getIncomingCall } from '../selectors/calling';
import { isGroupCallOutboundRingEnabled } from '../../util/isGroupCallOutboundRingEnabled';
import type {
2020-12-02 18:14:03 +00:00
ActiveCallType,
GroupCallRemoteParticipantType,
} from '../../types/Calling';
import type { AciString } from '../../types/ServiceId';
import { CallMode, CallState } from '../../types/Calling';
import type { StateType } from '../reducer';
2020-12-02 18:14:03 +00:00
import { missingCaseError } from '../../util/missingCaseError';
2020-08-27 00:03:42 +00:00
import { SmartCallingDeviceSelection } from './CallingDeviceSelection';
2022-03-02 18:24:28 +00:00
import type { SafetyNumberProps } from '../../components/SafetyNumberChangeDialog';
import { SmartSafetyNumberViewer } from './SafetyNumberViewer';
2021-08-20 16:06:15 +00:00
import { callingTones } from '../../util/callingTones';
import {
bounceAppIconStart,
bounceAppIconStop,
} from '../../shims/bounceAppIcon';
2021-09-23 18:16:09 +00:00
import {
FALLBACK_NOTIFICATION_TITLE,
NotificationSetting,
2023-08-01 16:06:29 +00:00
NotificationType,
2021-09-23 18:16:09 +00:00
notificationService,
} from '../../services/notifications';
import * as log from '../../logging/log';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { isConversationTooBigToRing } from '../../conversations/isConversationTooBigToRing';
2023-08-01 16:06:29 +00:00
import { strictAssert } from '../../util/assert';
2020-08-27 00:03:42 +00:00
function renderDeviceSelection(): JSX.Element {
return <SmartCallingDeviceSelection />;
2020-08-27 00:03:42 +00:00
}
2022-03-02 18:24:28 +00:00
function renderSafetyNumberViewer(props: SafetyNumberProps): JSX.Element {
return <SmartSafetyNumberViewer {...props} />;
}
2021-11-11 22:43:05 +00:00
const getGroupCallVideoFrameSource =
callingService.getGroupCallVideoFrameSource.bind(callingService);
2020-11-13 19:57:55 +00:00
2021-08-20 16:06:15 +00:00
async function notifyForCall(
2023-08-01 16:06:29 +00:00
conversationId: string,
2021-08-20 16:06:15 +00:00
title: string,
isVideoCall: boolean
): Promise<void> {
const shouldNotify =
2022-07-05 16:44:53 +00:00
!window.SignalContext.activeWindowService.isActive() &&
window.Events.getCallSystemNotification();
2021-08-20 16:06:15 +00:00
if (!shouldNotify) {
return;
}
2021-09-23 18:16:09 +00:00
let notificationTitle: string;
const notificationSetting = notificationService.getNotificationSetting();
switch (notificationSetting) {
case NotificationSetting.Off:
case NotificationSetting.NoNameOrMessage:
notificationTitle = FALLBACK_NOTIFICATION_TITLE;
break;
case NotificationSetting.NameOnly:
case NotificationSetting.NameAndMessage:
notificationTitle = title;
break;
default:
log.error(missingCaseError(notificationSetting));
notificationTitle = FALLBACK_NOTIFICATION_TITLE;
break;
}
2023-08-01 16:06:29 +00:00
const conversation = window.ConversationController.get(conversationId);
strictAssert(conversation, 'notifyForCall: conversation not found');
const { url, absolutePath } = await conversation.getAvatarOrIdenticon();
2021-09-23 18:16:09 +00:00
notificationService.notify({
2023-08-01 16:06:29 +00:00
conversationId,
2021-09-23 18:16:09 +00:00
title: notificationTitle,
2023-08-01 16:06:29 +00:00
iconPath: absolutePath,
iconUrl: url,
message: isVideoCall
2023-03-30 00:03:25 +00:00
? window.i18n('icu:incomingVideoCall')
: window.i18n('icu:incomingAudioCall'),
2023-06-14 20:55:50 +00:00
sentAt: 0,
// The ringtone plays so we don't need sound for the notification
silent: true,
2023-08-01 16:06:29 +00:00
type: NotificationType.IncomingCall,
2021-08-20 16:06:15 +00:00
});
}
const playRingtone = callingTones.playRingtone.bind(callingTones);
const stopRingtone = callingTones.stopRingtone.bind(callingTones);
2020-12-02 18:14:03 +00:00
const mapStateToActiveCallProp = (
state: StateType
): undefined | ActiveCallType => {
2020-10-08 01:25:33 +00:00
const { calling } = state;
const { activeCallState } = calling;
if (!activeCallState) {
return undefined;
}
const call = getActiveCall(calling);
if (!call) {
log.error('There was an active call state but no corresponding call');
return undefined;
}
2020-11-17 15:07:53 +00:00
const conversationSelector = getConversationSelector(state);
const conversation = conversationSelector(activeCallState.conversationId);
if (!conversation) {
log.error('The active call has no corresponding conversation');
return undefined;
}
const conversationSelectorByAci = memoize<
(aci: AciString) => undefined | ConversationType
>(aci => {
const convoForAci = window.ConversationController.lookupOrCreate({
uuid: aci,
reason: 'CallManager.mapStateToActiveCallProp',
});
return convoForAci ? conversationSelector(convoForAci.id) : undefined;
2020-12-02 18:14:03 +00:00
});
2020-12-02 18:14:03 +00:00
const baseResult = {
conversation,
hasLocalAudio: activeCallState.hasLocalAudio,
hasLocalVideo: activeCallState.hasLocalVideo,
2022-05-19 03:28:51 +00:00
localAudioLevel: activeCallState.localAudioLevel,
viewMode: activeCallState.viewMode,
2020-12-02 18:14:03 +00:00
joinedAt: activeCallState.joinedAt,
outgoingRing: activeCallState.outgoingRing,
2020-12-02 18:14:03 +00:00
pip: activeCallState.pip,
presentingSource: activeCallState.presentingSource,
presentingSourcesAvailable: activeCallState.presentingSourcesAvailable,
2020-12-02 18:14:03 +00:00
settingsDialogOpen: activeCallState.settingsDialogOpen,
showNeedsScreenRecordingPermissionsWarning: Boolean(
activeCallState.showNeedsScreenRecordingPermissionsWarning
),
2020-12-02 18:14:03 +00:00
showParticipantsList: activeCallState.showParticipantsList,
};
2020-11-17 15:07:53 +00:00
2020-12-02 18:14:03 +00:00
switch (call.callMode) {
case CallMode.Direct:
if (
call.isIncoming &&
(call.callState === CallState.Prering ||
call.callState === CallState.Ringing)
) {
return;
}
2020-12-02 18:14:03 +00:00
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,
2020-12-02 18:14:03 +00:00
},
],
};
case CallMode.Group: {
const conversationsWithSafetyNumberChanges: Array<ConversationType> = [];
const groupMembers: Array<ConversationType> = [];
2020-12-02 18:14:03 +00:00
const remoteParticipants: Array<GroupCallRemoteParticipantType> = [];
const peekedParticipants: Array<ConversationType> = [];
2020-12-02 18:14:03 +00:00
const { memberships = [] } = conversation;
// Active calls should have peek info, but TypeScript doesn't know that so we have a
// fallback.
const {
peekInfo = {
deviceCount: 0,
maxDevices: Infinity,
acis: [],
},
} = call;
for (let i = 0; i < memberships.length; i += 1) {
2021-10-26 22:59:08 +00:00
const { uuid } = memberships[i];
2021-10-26 22:59:08 +00:00
const member = conversationSelector(uuid);
if (!member) {
log.error('Group member has no corresponding conversation');
continue;
}
groupMembers.push(member);
}
2020-12-02 18:14:03 +00:00
for (let i = 0; i < call.remoteParticipants.length; i += 1) {
const remoteParticipant = call.remoteParticipants[i];
const remoteConversation = conversationSelectorByAci(
remoteParticipant.aci
2020-12-02 18:14:03 +00:00
);
2020-11-17 15:07:53 +00:00
if (!remoteConversation) {
log.error('Remote participant has no corresponding conversation');
2020-12-02 18:14:03 +00:00
continue;
2020-11-17 15:07:53 +00:00
}
2020-12-02 18:14:03 +00:00
remoteParticipants.push({
...remoteConversation,
demuxId: remoteParticipant.demuxId,
2020-11-17 15:07:53 +00:00
hasRemoteAudio: remoteParticipant.hasRemoteAudio,
hasRemoteVideo: remoteParticipant.hasRemoteVideo,
presenting: remoteParticipant.presenting,
sharingScreen: remoteParticipant.sharingScreen,
speakerTime: remoteParticipant.speakerTime,
videoAspectRatio: remoteParticipant.videoAspectRatio,
2020-11-17 15:07:53 +00:00
});
}
for (
let i = 0;
i < activeCallState.safetyNumberChangedAcis.length;
i += 1
) {
const aci = activeCallState.safetyNumberChangedAcis[i];
const remoteConversation = conversationSelectorByAci(aci);
if (!remoteConversation) {
log.error('Remote participant has no corresponding conversation');
continue;
}
conversationsWithSafetyNumberChanges.push(remoteConversation);
}
for (let i = 0; i < peekInfo.acis.length; i += 1) {
const peekedParticipantAci = peekInfo.acis[i];
2020-11-17 15:07:53 +00:00
const peekedConversation =
conversationSelectorByAci(peekedParticipantAci);
2020-12-02 18:14:03 +00:00
if (!peekedConversation) {
log.error('Remote participant has no corresponding conversation');
2020-12-02 18:14:03 +00:00
continue;
}
peekedParticipants.push(peekedConversation);
2020-12-02 18:14:03 +00:00
}
return {
...baseResult,
callMode: CallMode.Group,
connectionState: call.connectionState,
conversationsWithSafetyNumberChanges,
deviceCount: peekInfo.deviceCount,
groupMembers,
isConversationTooBigToRing: isConversationTooBigToRing(conversation),
2020-12-02 18:14:03 +00:00
joinState: call.joinState,
maxDevices: peekInfo.maxDevices,
2020-12-02 18:14:03 +00:00
peekedParticipants,
remoteParticipants,
2022-05-19 03:28:51 +00:00
remoteAudioLevels: call.remoteAudioLevels || new Map<number, number>(),
2020-12-02 18:14:03 +00:00
};
}
default:
throw missingCaseError(call);
}
2020-06-04 18:16:19 +00:00
};
const mapStateToIncomingCallProp = (state: StateType) => {
const call = getIncomingCall(state);
if (!call) {
return undefined;
}
const conversation = getConversationSelector(state)(call.conversationId);
if (!conversation) {
log.error('The incoming call has no corresponding conversation');
return undefined;
}
2021-08-20 16:06:15 +00:00
switch (call.callMode) {
case CallMode.Direct:
return {
callMode: CallMode.Direct as const,
conversation,
isVideoCall: call.isVideoCall,
};
case CallMode.Group: {
if (!call.ringerAci) {
log.error('The incoming group call has no ring state');
2021-08-20 16:06:15 +00:00
return undefined;
}
const conversationSelector = getConversationSelector(state);
const ringer = conversationSelector(call.ringerAci);
2021-08-20 16:06:15 +00:00
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) => {
const incomingCall = mapStateToIncomingCallProp(state);
return {
activeCall: mapStateToActiveCallProp(state),
bounceAppIconStart,
bounceAppIconStop,
availableCameras: state.calling.availableCameras,
getGroupCallVideoFrameSource,
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
isGroupCallOutboundRingEnabled: isGroupCallOutboundRingEnabled(),
incomingCall,
me: getMe(state),
notifyForCall,
playRingtone,
stopRingtone,
renderDeviceSelection,
renderSafetyNumberViewer,
theme: getTheme(state),
isConversationTooBigToRing: incomingCall
? isConversationTooBigToRing(incomingCall.conversation)
: false,
};
};
2020-06-04 18:16:19 +00:00
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartCallManager = smart(CallManager);