signal-desktop/ts/components/CallManager.tsx

465 lines
14 KiB
TypeScript
Raw Normal View History

// Copyright 2020-2022 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
2021-08-20 16:06:15 +00:00
import React, { useCallback, useEffect } from 'react';
import { noop } from 'lodash';
import type { VideoFrameSource } from 'ringrtc';
2020-10-01 19:09:15 +00:00
import { CallNeedPermissionScreen } from './CallNeedPermissionScreen';
import { CallScreen } from './CallScreen';
2020-11-17 15:07:53 +00:00
import { CallingLobby } from './CallingLobby';
import { CallingParticipantsList } from './CallingParticipantsList';
import { CallingSelectPresentingSourcesModal } from './CallingSelectPresentingSourcesModal';
2020-11-17 15:07:53 +00:00
import { CallingPip } from './CallingPip';
import { IncomingCallBar } from './IncomingCallBar';
import type { SafetyNumberProps } from './SafetyNumberChangeDialog';
import { SafetyNumberChangeDialog } from './SafetyNumberChangeDialog';
import type {
2020-12-02 18:14:03 +00:00
ActiveCallType,
GroupCallVideoRequest,
PresentedSource,
} from '../types/Calling';
import {
2020-11-17 15:07:53 +00:00
CallEndedReason,
2020-11-13 19:57:55 +00:00
CallMode,
CallState,
GroupCallConnectionState,
2020-11-13 19:57:55 +00:00
GroupCallJoinState,
} from '../types/Calling';
import type { ConversationType } from '../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type {
AcceptCallType,
2020-11-13 19:57:55 +00:00
CancelCallType,
DeclineCallType,
KeyChangeOkType,
SetGroupCallVideoRequestType,
2020-11-13 19:57:55 +00:00
SetLocalAudioType,
SetLocalPreviewType,
SetLocalVideoType,
SetRendererCanvasType,
2020-11-13 19:57:55 +00:00
StartCallType,
} from '../state/ducks/calling';
import type { LocalizerType, ThemeType } from '../types/Util';
2020-11-13 19:57:55 +00:00
import { missingCaseError } from '../util/missingCaseError';
2020-06-04 18:16:19 +00:00
const GROUP_CALL_RING_DURATION = 60 * 1000;
export type PropsType = {
2020-11-13 19:57:55 +00:00
activeCall?: ActiveCallType;
availableCameras: Array<MediaDeviceInfo>;
2020-11-13 19:57:55 +00:00
cancelCall: (_: CancelCallType) => void;
2020-10-01 19:09:15 +00:00
closeNeedPermissionScreen: () => void;
2020-11-13 19:57:55 +00:00
getGroupCallVideoFrameSource: (
conversationId: string,
demuxId: number
) => VideoFrameSource;
getPreferredBadge: PreferredBadgeSelectorType;
getPresentingSources: () => void;
2021-08-20 16:06:15 +00:00
incomingCall?:
| {
callMode: CallMode.Direct;
conversation: ConversationType;
isVideoCall: boolean;
}
| {
callMode: CallMode.Group;
conversation: ConversationType;
otherMembersRung: Array<Pick<ConversationType, 'firstName' | 'title'>>;
ringer: Pick<ConversationType, 'firstName' | 'title'>;
};
keyChangeOk: (_: KeyChangeOkType) => void;
2020-08-27 00:03:42 +00:00
renderDeviceSelection: () => JSX.Element;
renderSafetyNumberViewer: (props: SafetyNumberProps) => JSX.Element;
startCall: (payload: StartCallType) => void;
2020-10-08 01:25:33 +00:00
toggleParticipants: () => void;
acceptCall: (_: AcceptCallType) => void;
2021-08-20 16:06:15 +00:00
bounceAppIconStart: () => unknown;
bounceAppIconStop: () => unknown;
declineCall: (_: DeclineCallType) => void;
i18n: LocalizerType;
isGroupCallOutboundRingEnabled: boolean;
me: ConversationType;
2021-08-20 16:06:15 +00:00
notifyForCall: (title: string, isVideoCall: boolean) => unknown;
openSystemPreferencesAction: () => unknown;
2021-08-20 16:06:15 +00:00
playRingtone: () => unknown;
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
setIsCallActive: (_: boolean) => void;
setLocalAudio: (_: SetLocalAudioType) => void;
setLocalVideo: (_: SetLocalVideoType) => void;
setLocalPreview: (_: SetLocalPreviewType) => void;
setOutgoingRing: (_: boolean) => void;
setPresenting: (_?: PresentedSource) => void;
setRendererCanvas: (_: SetRendererCanvasType) => void;
2021-08-20 16:06:15 +00:00
stopRingtone: () => unknown;
hangUpActiveCall: () => void;
theme: ThemeType;
togglePip: () => void;
toggleScreenRecordingPermissionsDialog: () => unknown;
toggleSettings: () => void;
2021-01-08 22:57:54 +00:00
toggleSpeakerView: () => void;
};
2020-06-04 18:16:19 +00:00
type ActiveCallManagerPropsType = PropsType & {
2020-11-13 19:57:55 +00:00
activeCall: ActiveCallType;
};
2020-11-13 19:57:55 +00:00
const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
activeCall,
availableCameras,
2020-10-08 01:25:33 +00:00
cancelCall,
2020-10-01 19:09:15 +00:00
closeNeedPermissionScreen,
hangUpActiveCall,
2020-06-04 18:16:19 +00:00
i18n,
isGroupCallOutboundRingEnabled,
keyChangeOk,
2020-11-13 19:57:55 +00:00
getGroupCallVideoFrameSource,
getPreferredBadge,
getPresentingSources,
me,
openSystemPreferencesAction,
2020-08-27 00:03:42 +00:00
renderDeviceSelection,
renderSafetyNumberViewer,
setGroupCallVideoRequest,
2020-06-04 18:16:19 +00:00
setLocalAudio,
2020-08-27 00:03:42 +00:00
setLocalPreview,
2020-06-04 18:16:19 +00:00
setLocalVideo,
setPresenting,
2020-08-27 00:03:42 +00:00
setRendererCanvas,
setOutgoingRing,
2020-10-08 01:25:33 +00:00
startCall,
theme,
2020-10-08 01:25:33 +00:00
toggleParticipants,
2020-10-01 00:43:05 +00:00
togglePip,
toggleScreenRecordingPermissionsDialog,
2020-08-27 00:03:42 +00:00
toggleSettings,
2021-01-08 22:57:54 +00:00
toggleSpeakerView,
2020-11-13 19:57:55 +00:00
}) => {
const {
2020-11-17 15:07:53 +00:00
conversation,
2020-11-13 19:57:55 +00:00
hasLocalAudio,
hasLocalVideo,
2020-11-17 15:07:53 +00:00
joinedAt,
2020-12-02 18:14:03 +00:00
peekedParticipants,
2020-11-13 19:57:55 +00:00
pip,
presentingSourcesAvailable,
2020-11-17 15:07:53 +00:00
settingsDialogOpen,
showParticipantsList,
outgoingRing,
2020-12-02 18:14:03 +00:00
} = activeCall;
2020-11-13 19:57:55 +00:00
const cancelActiveCall = useCallback(() => {
cancelCall({ conversationId: conversation.id });
}, [cancelCall, conversation.id]);
const joinActiveCall = useCallback(() => {
startCall({
2020-12-02 18:14:03 +00:00
callMode: activeCall.callMode,
2020-11-13 19:57:55 +00:00
conversationId: conversation.id,
hasLocalAudio,
hasLocalVideo,
2020-11-13 19:57:55 +00:00
});
2020-12-02 18:14:03 +00:00
}, [
startCall,
activeCall.callMode,
conversation.id,
hasLocalAudio,
hasLocalVideo,
]);
2020-11-13 19:57:55 +00:00
const getGroupCallVideoFrameSourceForActiveCall = useCallback(
(demuxId: number) => {
return getGroupCallVideoFrameSource(conversation.id, demuxId);
},
[getGroupCallVideoFrameSource, conversation.id]
);
const setGroupCallVideoRequestForConversation = useCallback(
(resolutions: Array<GroupCallVideoRequest>) => {
setGroupCallVideoRequest({
conversationId: conversation.id,
resolutions,
});
},
[setGroupCallVideoRequest, conversation.id]
);
2020-12-02 18:14:03 +00:00
let isCallFull: boolean;
2020-11-13 19:57:55 +00:00
let showCallLobby: boolean;
let groupMembers:
| undefined
| Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
2020-12-02 18:14:03 +00:00
switch (activeCall.callMode) {
2020-11-13 19:57:55 +00:00
case CallMode.Direct: {
2020-12-02 18:14:03 +00:00
const { callState, callEndedReason } = activeCall;
2020-11-13 19:57:55 +00:00
const ended = callState === CallState.Ended;
if (
ended &&
callEndedReason === CallEndedReason.RemoteHangupNeedPermission
) {
return (
<CallNeedPermissionScreen
close={closeNeedPermissionScreen}
conversation={conversation}
i18n={i18n}
/>
);
}
2020-11-13 19:57:55 +00:00
showCallLobby = !callState;
2020-12-02 18:14:03 +00:00
isCallFull = false;
groupMembers = undefined;
2020-11-13 19:57:55 +00:00
break;
}
2020-11-13 19:57:55 +00:00
case CallMode.Group: {
2020-12-02 18:14:03 +00:00
showCallLobby = activeCall.joinState === GroupCallJoinState.NotJoined;
isCallFull = activeCall.deviceCount >= activeCall.maxDevices;
({ groupMembers } = activeCall);
2020-11-13 19:57:55 +00:00
break;
2020-10-01 00:43:05 +00:00
}
2020-11-13 19:57:55 +00:00
default:
2020-12-02 18:14:03 +00:00
throw missingCaseError(activeCall);
2020-11-13 19:57:55 +00:00
}
2020-10-01 00:43:05 +00:00
2020-11-13 19:57:55 +00:00
if (showCallLobby) {
2020-06-04 18:16:19 +00:00
return (
2020-08-27 00:03:42 +00:00
<>
2020-11-13 19:57:55 +00:00
<CallingLobby
availableCameras={availableCameras}
conversation={conversation}
groupMembers={groupMembers}
2020-08-27 00:03:42 +00:00
hasLocalAudio={hasLocalAudio}
hasLocalVideo={hasLocalVideo}
i18n={i18n}
2020-12-02 18:14:03 +00:00
isGroupCall={activeCall.callMode === CallMode.Group}
isGroupCallOutboundRingEnabled={isGroupCallOutboundRingEnabled}
isCallFull={isCallFull}
me={me}
2020-11-13 19:57:55 +00:00
onCallCanceled={cancelActiveCall}
onJoinCall={joinActiveCall}
outgoingRing={outgoingRing}
2020-12-02 18:14:03 +00:00
peekedParticipants={peekedParticipants}
2020-08-27 00:03:42 +00:00
setLocalPreview={setLocalPreview}
setLocalAudio={setLocalAudio}
setLocalVideo={setLocalVideo}
setOutgoingRing={setOutgoingRing}
2020-11-20 19:39:50 +00:00
showParticipantsList={showParticipantsList}
2020-11-13 19:57:55 +00:00
toggleParticipants={toggleParticipants}
2020-08-27 00:03:42 +00:00
toggleSettings={toggleSettings}
/>
{settingsDialogOpen && renderDeviceSelection()}
2020-12-02 18:14:03 +00:00
{showParticipantsList && activeCall.callMode === CallMode.Group ? (
2020-11-17 15:07:53 +00:00
<CallingParticipantsList
i18n={i18n}
onClose={toggleParticipants}
ourUuid={me.uuid}
2020-12-02 18:14:03 +00:00
participants={peekedParticipants}
2020-11-17 15:07:53 +00:00
/>
) : null}
2020-08-27 00:03:42 +00:00
</>
2020-06-04 18:16:19 +00:00
);
}
2020-11-17 15:07:53 +00:00
if (pip) {
2020-11-13 19:57:55 +00:00
return (
<CallingPip
activeCall={activeCall}
2020-11-17 15:07:53 +00:00
getGroupCallVideoFrameSource={getGroupCallVideoFrameSourceForActiveCall}
hangUpActiveCall={hangUpActiveCall}
2020-11-13 19:57:55 +00:00
hasLocalVideo={hasLocalVideo}
i18n={i18n}
setGroupCallVideoRequest={setGroupCallVideoRequestForConversation}
2020-11-13 19:57:55 +00:00
setLocalPreview={setLocalPreview}
setRendererCanvas={setRendererCanvas}
togglePip={togglePip}
toggleSpeakerView={toggleSpeakerView}
2020-11-13 19:57:55 +00:00
/>
);
}
2020-12-02 18:14:03 +00:00
const groupCallParticipantsForParticipantsList =
activeCall.callMode === CallMode.Group
? [
...activeCall.remoteParticipants.map(participant => ({
...participant,
hasRemoteAudio: participant.hasRemoteAudio,
hasRemoteVideo: participant.hasRemoteVideo,
presenting: participant.presenting,
2020-12-02 18:14:03 +00:00
})),
{
...me,
hasRemoteAudio: hasLocalAudio,
hasRemoteVideo: hasLocalVideo,
presenting: Boolean(activeCall.presentingSource),
2020-12-02 18:14:03 +00:00
},
]
: [];
2020-11-13 19:57:55 +00:00
return (
<>
<CallScreen
activeCall={activeCall}
getPresentingSources={getPresentingSources}
2020-11-13 19:57:55 +00:00
getGroupCallVideoFrameSource={getGroupCallVideoFrameSourceForActiveCall}
groupMembers={groupMembers}
hangUpActiveCall={hangUpActiveCall}
2020-11-13 19:57:55 +00:00
i18n={i18n}
joinedAt={joinedAt}
me={me}
openSystemPreferencesAction={openSystemPreferencesAction}
setGroupCallVideoRequest={setGroupCallVideoRequestForConversation}
2020-11-13 19:57:55 +00:00
setLocalPreview={setLocalPreview}
setRendererCanvas={setRendererCanvas}
setLocalAudio={setLocalAudio}
setLocalVideo={setLocalVideo}
setPresenting={setPresenting}
2020-11-17 15:07:53 +00:00
stickyControls={showParticipantsList}
toggleScreenRecordingPermissionsDialog={
toggleScreenRecordingPermissionsDialog
}
2020-11-17 15:07:53 +00:00
toggleParticipants={toggleParticipants}
2020-11-13 19:57:55 +00:00
togglePip={togglePip}
toggleSettings={toggleSettings}
2021-01-08 22:57:54 +00:00
toggleSpeakerView={toggleSpeakerView}
2020-11-13 19:57:55 +00:00
/>
{presentingSourcesAvailable && presentingSourcesAvailable.length ? (
<CallingSelectPresentingSourcesModal
i18n={i18n}
presentingSourcesAvailable={presentingSourcesAvailable}
setPresenting={setPresenting}
/>
) : null}
2020-11-13 19:57:55 +00:00
{settingsDialogOpen && renderDeviceSelection()}
2020-12-02 18:14:03 +00:00
{showParticipantsList && activeCall.callMode === CallMode.Group ? (
2020-11-17 15:07:53 +00:00
<CallingParticipantsList
i18n={i18n}
onClose={toggleParticipants}
ourUuid={me.uuid}
2020-12-02 18:14:03 +00:00
participants={groupCallParticipantsForParticipantsList}
2020-11-17 15:07:53 +00:00
/>
) : null}
{activeCall.callMode === CallMode.Group &&
activeCall.conversationsWithSafetyNumberChanges.length ? (
<SafetyNumberChangeDialog
confirmText={i18n('continueCall')}
contacts={activeCall.conversationsWithSafetyNumberChanges}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
onCancel={hangUpActiveCall}
onConfirm={() => {
keyChangeOk({ conversationId: activeCall.conversation.id });
}}
renderSafetyNumber={renderSafetyNumberViewer}
theme={theme}
/>
) : null}
2020-11-13 19:57:55 +00:00
</>
);
};
export const CallManager: React.FC<PropsType> = props => {
2021-08-20 16:06:15 +00:00
const {
acceptCall,
activeCall,
bounceAppIconStart,
bounceAppIconStop,
declineCall,
i18n,
incomingCall,
notifyForCall,
playRingtone,
stopRingtone,
setIsCallActive,
setOutgoingRing,
2021-08-20 16:06:15 +00:00
} = props;
const isCallActive = Boolean(activeCall);
useEffect(() => {
setIsCallActive(isCallActive);
}, [isCallActive, setIsCallActive]);
2021-08-20 16:06:15 +00:00
const shouldRing = getShouldRing(props);
useEffect(() => {
if (shouldRing) {
playRingtone();
return () => {
stopRingtone();
};
}
stopRingtone();
return noop;
}, [shouldRing, playRingtone, stopRingtone]);
2020-11-13 19:57:55 +00:00
2021-09-09 21:15:05 +00:00
const mightBeRingingOutgoingGroupCall =
activeCall?.callMode === CallMode.Group &&
activeCall.outgoingRing &&
activeCall.joinState !== GroupCallJoinState.NotJoined;
useEffect(() => {
2021-09-09 21:15:05 +00:00
if (!mightBeRingingOutgoingGroupCall) {
return noop;
}
const timeout = setTimeout(() => {
setOutgoingRing(false);
}, GROUP_CALL_RING_DURATION);
return () => {
clearTimeout(timeout);
};
2021-09-09 21:15:05 +00:00
}, [mightBeRingingOutgoingGroupCall, setOutgoingRing]);
2020-11-13 19:57:55 +00:00
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 <ActiveCallManager {...props} activeCall={activeCall} />;
}
// In the future, we may want to show the incoming call bar when a call is active.
if (incomingCall) {
2020-06-04 18:16:19 +00:00
return (
<IncomingCallBar
acceptCall={acceptCall}
2021-08-20 16:06:15 +00:00
bounceAppIconStart={bounceAppIconStart}
bounceAppIconStop={bounceAppIconStop}
2020-06-04 18:16:19 +00:00
declineCall={declineCall}
i18n={i18n}
2021-08-20 16:06:15 +00:00
notifyForCall={notifyForCall}
{...incomingCall}
2020-06-04 18:16:19 +00:00
/>
);
}
return null;
};
2021-08-20 16:06:15 +00:00
function getShouldRing({
activeCall,
incomingCall,
}: Readonly<Pick<PropsType, 'activeCall' | 'incomingCall'>>): boolean {
if (incomingCall) {
return !activeCall;
}
if (!activeCall) {
return false;
}
switch (activeCall.callMode) {
case CallMode.Direct:
return (
activeCall.callState === CallState.Prering ||
activeCall.callState === CallState.Ringing
);
case CallMode.Group:
return (
activeCall.outgoingRing &&
(activeCall.connectionState === GroupCallConnectionState.Connecting ||
activeCall.connectionState === GroupCallConnectionState.Connected) &&
activeCall.joinState !== GroupCallJoinState.NotJoined &&
!activeCall.remoteParticipants.length &&
(activeCall.conversation.sortedGroupMembers || []).length >= 2
);
2021-08-20 16:06:15 +00:00
default:
throw missingCaseError(activeCall);
}
}