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';
|
2022-02-23 18:48:40 +00:00
|
|
|
import { getIntl, getTheme } from '../selectors/user';
|
2020-11-06 17:36:37 +00:00
|
|
|
import { getMe, getConversationSelector } from '../selectors/conversations';
|
2020-12-02 18:14:03 +00:00
|
|
|
import { getActiveCall } from '../ducks/calling';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { ConversationType } from '../ducks/conversations';
|
2020-11-13 19:57:55 +00:00
|
|
|
import { getIncomingCall } from '../selectors/calling';
|
2021-08-25 21:42:51 +00:00
|
|
|
import { isGroupCallOutboundRingEnabled } from '../../util/isGroupCallOutboundRingEnabled';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type {
|
2020-12-02 18:14:03 +00:00
|
|
|
ActiveCallType,
|
2020-11-20 20:14:07 +00:00
|
|
|
GroupCallRemoteParticipantType,
|
|
|
|
} from '../../types/Calling';
|
2021-10-26 22:59:08 +00:00
|
|
|
import type { UUIDStringType } from '../../types/UUID';
|
2021-10-26 19:15:33 +00:00
|
|
|
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';
|
2021-10-26 19:15:33 +00:00
|
|
|
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';
|
2021-09-17 18:27:53 +00:00
|
|
|
import * as log from '../../logging/log';
|
2021-11-17 21:58:34 +00:00
|
|
|
import { getPreferredBadgeSelector } from '../selectors/badges';
|
2022-10-24 20:46:36 +00:00
|
|
|
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 {
|
2020-10-26 21:32:46 +00:00
|
|
|
return <SmartCallingDeviceSelection />;
|
2020-08-27 00:03:42 +00:00
|
|
|
}
|
|
|
|
|
2022-03-02 18:24:28 +00:00
|
|
|
function renderSafetyNumberViewer(props: SafetyNumberProps): JSX.Element {
|
2020-12-08 19:37:04 +00:00
|
|
|
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,
|
2023-03-28 18:26:46 +00:00
|
|
|
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,
|
2023-05-08 19:59:36 +00:00
|
|
|
// 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;
|
2020-11-06 17:36:37 +00:00
|
|
|
const { activeCallState } = calling;
|
|
|
|
|
|
|
|
if (!activeCallState) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const call = getActiveCall(calling);
|
|
|
|
if (!call) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.error('There was an active call state but no corresponding call');
|
2020-11-06 17:36:37 +00:00
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2020-11-17 15:07:53 +00:00
|
|
|
const conversationSelector = getConversationSelector(state);
|
|
|
|
const conversation = conversationSelector(activeCallState.conversationId);
|
2020-11-06 17:36:37 +00:00
|
|
|
if (!conversation) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.error('The active call has no corresponding conversation');
|
2020-11-06 17:36:37 +00:00
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2020-12-02 18:14:03 +00:00
|
|
|
const conversationSelectorByUuid = memoize<
|
2021-10-26 22:59:08 +00:00
|
|
|
(uuid: UUIDStringType) => undefined | ConversationType
|
2020-12-02 18:14:03 +00:00
|
|
|
>(uuid => {
|
2022-08-09 21:39:00 +00:00
|
|
|
const convoForUuid = window.ConversationController.lookupOrCreate({
|
2020-12-02 18:14:03 +00:00
|
|
|
uuid,
|
2022-12-03 01:05:27 +00:00
|
|
|
reason: 'CallManager.mapStateToActiveCallProp',
|
2020-11-20 20:14:07 +00:00
|
|
|
});
|
2022-08-09 21:39:00 +00:00
|
|
|
return convoForUuid ? conversationSelector(convoForUuid.id) : undefined;
|
2020-12-02 18:14:03 +00:00
|
|
|
});
|
2020-11-20 20:14:07 +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,
|
2022-05-25 18:03:27 +00:00
|
|
|
viewMode: activeCallState.viewMode,
|
2020-12-02 18:14:03 +00:00
|
|
|
joinedAt: activeCallState.joinedAt,
|
2021-08-25 21:42:51 +00:00
|
|
|
outgoingRing: activeCallState.outgoingRing,
|
2020-12-02 18:14:03 +00:00
|
|
|
pip: activeCallState.pip,
|
2021-05-20 21:54:03 +00:00
|
|
|
presentingSource: activeCallState.presentingSource,
|
|
|
|
presentingSourcesAvailable: activeCallState.presentingSourcesAvailable,
|
2020-12-02 18:14:03 +00:00
|
|
|
settingsDialogOpen: activeCallState.settingsDialogOpen,
|
2021-05-20 21:54:03 +00:00
|
|
|
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:
|
2021-08-10 23:16:18 +00:00
|
|
|
if (
|
|
|
|
call.isIncoming &&
|
|
|
|
(call.callState === CallState.Prering ||
|
|
|
|
call.callState === CallState.Ringing)
|
|
|
|
) {
|
2021-08-10 17:15:35 +00:00
|
|
|
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),
|
2021-05-20 21:54:03 +00:00
|
|
|
presenting: Boolean(call.isSharingScreen),
|
|
|
|
title: conversation.title,
|
|
|
|
uuid: conversation.uuid,
|
2020-12-02 18:14:03 +00:00
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
case CallMode.Group: {
|
2020-12-08 19:37:04 +00:00
|
|
|
const conversationsWithSafetyNumberChanges: Array<ConversationType> = [];
|
2021-08-17 21:45:18 +00:00
|
|
|
const groupMembers: Array<ConversationType> = [];
|
2020-12-02 18:14:03 +00:00
|
|
|
const remoteParticipants: Array<GroupCallRemoteParticipantType> = [];
|
2020-12-08 19:37:04 +00:00
|
|
|
const peekedParticipants: Array<ConversationType> = [];
|
2020-12-02 18:14:03 +00:00
|
|
|
|
2021-08-17 21:45:18 +00:00
|
|
|
const { memberships = [] } = conversation;
|
2022-02-08 19:18:51 +00:00
|
|
|
|
|
|
|
// Active calls should have peek info, but TypeScript doesn't know that so we have a
|
|
|
|
// fallback.
|
|
|
|
const {
|
|
|
|
peekInfo = {
|
|
|
|
deviceCount: 0,
|
|
|
|
maxDevices: Infinity,
|
|
|
|
uuids: [],
|
|
|
|
},
|
|
|
|
} = call;
|
|
|
|
|
2021-08-17 21:45:18 +00:00
|
|
|
for (let i = 0; i < memberships.length; i += 1) {
|
2021-10-26 22:59:08 +00:00
|
|
|
const { uuid } = memberships[i];
|
2021-08-17 21:45:18 +00:00
|
|
|
|
2021-10-26 22:59:08 +00:00
|
|
|
const member = conversationSelector(uuid);
|
2021-08-17 21:45:18 +00:00
|
|
|
if (!member) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.error('Group member has no corresponding conversation');
|
2021-08-17 21:45:18 +00:00
|
|
|
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 = conversationSelectorByUuid(
|
|
|
|
remoteParticipant.uuid
|
|
|
|
);
|
2020-11-17 15:07:53 +00:00
|
|
|
if (!remoteConversation) {
|
2021-09-17 18:27:53 +00:00
|
|
|
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({
|
2020-12-08 19:37:04 +00:00
|
|
|
...remoteConversation,
|
2020-11-19 18:13:36 +00:00
|
|
|
demuxId: remoteParticipant.demuxId,
|
2020-11-17 15:07:53 +00:00
|
|
|
hasRemoteAudio: remoteParticipant.hasRemoteAudio,
|
|
|
|
hasRemoteVideo: remoteParticipant.hasRemoteVideo,
|
2021-05-20 21:54:03 +00:00
|
|
|
presenting: remoteParticipant.presenting,
|
|
|
|
sharingScreen: remoteParticipant.sharingScreen,
|
2020-12-01 23:52:09 +00:00
|
|
|
speakerTime: remoteParticipant.speakerTime,
|
2020-11-19 18:13:36 +00:00
|
|
|
videoAspectRatio: remoteParticipant.videoAspectRatio,
|
2020-11-17 15:07:53 +00:00
|
|
|
});
|
|
|
|
}
|
2020-12-02 01:30:25 +00:00
|
|
|
|
2020-12-08 19:37:04 +00:00
|
|
|
for (
|
|
|
|
let i = 0;
|
|
|
|
i < activeCallState.safetyNumberChangedUuids.length;
|
|
|
|
i += 1
|
|
|
|
) {
|
|
|
|
const uuid = activeCallState.safetyNumberChangedUuids[i];
|
|
|
|
|
|
|
|
const remoteConversation = conversationSelectorByUuid(uuid);
|
|
|
|
if (!remoteConversation) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.error('Remote participant has no corresponding conversation');
|
2020-12-08 19:37:04 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
conversationsWithSafetyNumberChanges.push(remoteConversation);
|
|
|
|
}
|
|
|
|
|
2022-02-08 19:18:51 +00:00
|
|
|
for (let i = 0; i < peekInfo.uuids.length; i += 1) {
|
|
|
|
const peekedParticipantUuid = peekInfo.uuids[i];
|
2020-11-17 15:07:53 +00:00
|
|
|
|
2020-12-02 18:14:03 +00:00
|
|
|
const peekedConversation = conversationSelectorByUuid(
|
|
|
|
peekedParticipantUuid
|
|
|
|
);
|
|
|
|
if (!peekedConversation) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.error('Remote participant has no corresponding conversation');
|
2020-12-02 18:14:03 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-12-08 19:37:04 +00:00
|
|
|
peekedParticipants.push(peekedConversation);
|
2020-12-02 18:14:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...baseResult,
|
|
|
|
callMode: CallMode.Group,
|
|
|
|
connectionState: call.connectionState,
|
2020-12-08 19:37:04 +00:00
|
|
|
conversationsWithSafetyNumberChanges,
|
2022-02-08 19:18:51 +00:00
|
|
|
deviceCount: peekInfo.deviceCount,
|
2021-08-17 21:45:18 +00:00
|
|
|
groupMembers,
|
2022-10-24 20:46:36 +00:00
|
|
|
isConversationTooBigToRing: isConversationTooBigToRing(conversation),
|
2020-12-02 18:14:03 +00:00
|
|
|
joinState: call.joinState,
|
2022-02-08 19:18:51 +00:00
|
|
|
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
|
|
|
};
|
|
|
|
|
2020-11-06 17:36:37 +00:00
|
|
|
const mapStateToIncomingCallProp = (state: StateType) => {
|
2020-12-02 18:11:54 +00:00
|
|
|
const call = getIncomingCall(state);
|
2020-11-06 17:36:37 +00:00
|
|
|
if (!call) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const conversation = getConversationSelector(state)(call.conversationId);
|
|
|
|
if (!conversation) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.error('The incoming call has no corresponding conversation');
|
2020-11-06 17:36:37 +00:00
|
|
|
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.ringerUuid) {
|
2021-09-17 18:27:53 +00:00
|
|
|
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.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);
|
|
|
|
}
|
2020-11-06 17:36:37 +00:00
|
|
|
};
|
|
|
|
|
2022-12-20 02:10:51 +00:00
|
|
|
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-11-06 17:36:37 +00:00
|
|
|
|
2020-06-04 18:16:19 +00:00
|
|
|
const smart = connect(mapStateToProps, mapDispatchToProps);
|
|
|
|
|
|
|
|
export const SmartCallManager = smart(CallManager);
|