Receive rings for group calls

This commit is contained in:
Evan Hahn
2021-08-20 11:06:15 -05:00
committed by GitHub
parent fe040a2873
commit 79c976668b
27 changed files with 2112 additions and 359 deletions

View File

@@ -12,7 +12,6 @@ import { has, omit } from 'lodash';
import { getOwn } from '../../util/getOwn';
import { getPlatform } from '../selectors/user';
import { missingCaseError } from '../../util/missingCaseError';
import { notify } from '../../services/notify';
import { calling } from '../../services/calling';
import { StateType as RootStateType } from '../reducer';
import {
@@ -29,10 +28,6 @@ import {
} from '../../types/Calling';
import { callingTones } from '../../util/callingTones';
import { requestCameraPermissions } from '../../util/callingPermissions';
import {
bounceAppIconStart,
bounceAppIconStop,
} from '../../shims/bounceAppIcon';
import { sleep } from '../../util/sleep';
import { LatestQueue } from '../../util/LatestQueue';
@@ -68,6 +63,16 @@ export type DirectCallStateType = {
hasRemoteVideo?: boolean;
};
type GroupCallRingStateType =
| {
ringId?: undefined;
ringerUuid?: undefined;
}
| {
ringId: bigint;
ringerUuid: string;
};
export type GroupCallStateType = {
callMode: CallMode.Group;
conversationId: string;
@@ -75,7 +80,7 @@ export type GroupCallStateType = {
joinState: GroupCallJoinState;
peekInfo: GroupCallPeekInfoType;
remoteParticipants: Array<GroupCallParticipantInfoType>;
};
} & GroupCallRingStateType;
export type ActiveCallStateType = {
conversationId: string;
@@ -120,6 +125,11 @@ export type CancelCallType = {
conversationId: string;
};
type CancelIncomingGroupCallRingType = {
conversationId: string;
ringId: bigint;
};
export type DeclineCallType = {
conversationId: string;
};
@@ -150,11 +160,17 @@ export type KeyChangeOkType = {
conversationId: string;
};
export type IncomingCallType = {
export type IncomingDirectCallType = {
conversationId: string;
isVideoCall: boolean;
};
type IncomingGroupCallType = {
conversationId: string;
ringId: bigint;
ringerUuid: string;
};
type PeekNotConnectedGroupCallType = {
conversationId: string;
};
@@ -237,18 +253,28 @@ export const isAnybodyElseInGroupCall = (
ourUuid: string
): boolean => uuids.some(id => id !== ourUuid);
const getGroupCallRingState = (
call: Readonly<undefined | GroupCallStateType>
): GroupCallRingStateType =>
call?.ringId === undefined
? {}
: { ringId: call.ringId, ringerUuid: call.ringerUuid };
// Actions
const ACCEPT_CALL_PENDING = 'calling/ACCEPT_CALL_PENDING';
const CANCEL_CALL = 'calling/CANCEL_CALL';
const CANCEL_INCOMING_GROUP_CALL_RING =
'calling/CANCEL_INCOMING_GROUP_CALL_RING';
const SHOW_CALL_LOBBY = 'calling/SHOW_CALL_LOBBY';
const CALL_STATE_CHANGE_FULFILLED = 'calling/CALL_STATE_CHANGE_FULFILLED';
const CHANGE_IO_DEVICE_FULFILLED = 'calling/CHANGE_IO_DEVICE_FULFILLED';
const CLOSE_NEED_PERMISSION_SCREEN = 'calling/CLOSE_NEED_PERMISSION_SCREEN';
const DECLINE_CALL = 'calling/DECLINE_CALL';
const DECLINE_DIRECT_CALL = 'calling/DECLINE_DIRECT_CALL';
const GROUP_CALL_STATE_CHANGE = 'calling/GROUP_CALL_STATE_CHANGE';
const HANG_UP = 'calling/HANG_UP';
const INCOMING_CALL = 'calling/INCOMING_CALL';
const INCOMING_DIRECT_CALL = 'calling/INCOMING_DIRECT_CALL';
const INCOMING_GROUP_CALL = 'calling/INCOMING_GROUP_CALL';
const MARK_CALL_TRUSTED = 'calling/MARK_CALL_TRUSTED';
const MARK_CALL_UNTRUSTED = 'calling/MARK_CALL_UNTRUSTED';
const OUTGOING_CALL = 'calling/OUTGOING_CALL';
@@ -279,6 +305,11 @@ type CancelCallActionType = {
type: 'calling/CANCEL_CALL';
};
type CancelIncomingGroupCallRingActionType = {
type: 'calling/CANCEL_INCOMING_GROUP_CALL_RING';
payload: CancelIncomingGroupCallRingType;
};
type CallLobbyActionType = {
type: 'calling/SHOW_CALL_LOBBY';
payload: ShowCallLobbyType;
@@ -300,7 +331,7 @@ type CloseNeedPermissionScreenActionType = {
};
type DeclineCallActionType = {
type: 'calling/DECLINE_CALL';
type: 'calling/DECLINE_DIRECT_CALL';
payload: DeclineCallType;
};
@@ -314,9 +345,14 @@ type HangUpActionType = {
payload: HangUpType;
};
type IncomingCallActionType = {
type: 'calling/INCOMING_CALL';
payload: IncomingCallType;
type IncomingDirectCallActionType = {
type: 'calling/INCOMING_DIRECT_CALL';
payload: IncomingDirectCallType;
};
type IncomingGroupCallActionType = {
type: 'calling/INCOMING_GROUP_CALL';
payload: IncomingGroupCallType;
};
type KeyChangedActionType = {
@@ -417,6 +453,7 @@ type ToggleSpeakerViewActionType = {
export type CallingActionType =
| AcceptCallPendingActionType
| CancelCallActionType
| CancelIncomingGroupCallRingActionType
| CallLobbyActionType
| CallStateChangeFulfilledActionType
| ChangeIODeviceFulfilledActionType
@@ -424,7 +461,8 @@ export type CallingActionType =
| DeclineCallActionType
| GroupCallStateChangeActionType
| HangUpActionType
| IncomingCallActionType
| IncomingDirectCallActionType
| IncomingGroupCallActionType
| KeyChangedActionType
| KeyChangeOkActionType
| OutgoingCallActionType
@@ -450,17 +488,30 @@ export type CallingActionType =
function acceptCall(
payload: AcceptCallType
): ThunkAction<void, RootStateType, unknown, AcceptCallPendingActionType> {
return async dispatch => {
return async (dispatch, getState) => {
const { conversationId, asVideoCall } = payload;
const call = getOwn(getState().calling.callsByConversation, conversationId);
if (!call) {
window.log.error('Trying to accept a non-existent call');
return;
}
switch (call.callMode) {
case CallMode.Direct:
await calling.acceptDirectCall(conversationId, asVideoCall);
break;
case CallMode.Group:
calling.joinGroupCall(conversationId, true, asVideoCall);
break;
default:
throw missingCaseError(call);
}
dispatch({
type: ACCEPT_CALL_PENDING,
payload,
});
try {
await calling.accept(payload.conversationId, payload.asVideoCall);
} catch (err) {
window.log.error(`Failed to acceptCall: ${err.stack}`);
}
};
}
@@ -473,16 +524,7 @@ function callStateChange(
CallStateChangeFulfilledActionType
> {
return async dispatch => {
const { callState, isIncoming, title, isVideoCall } = payload;
if (callState === CallState.Ringing && isIncoming) {
await callingTones.playRingtone();
await showCallNotification(title, isVideoCall);
bounceAppIconStart();
}
if (callState !== CallState.Ringing) {
await callingTones.stopRingtone();
bounceAppIconStop();
}
const { callState } = payload;
if (callState === CallState.Ended) {
await callingTones.playEndCall();
ipcRenderer.send('close-screen-share-controller');
@@ -519,30 +561,6 @@ function changeIODevice(
};
}
async function showCallNotification(
title: string,
isVideoCall: boolean
): Promise<void> {
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,
});
}
function closeNeedPermissionScreen(): CloseNeedPermissionScreenActionType {
return {
type: CLOSE_NEED_PERMISSION_SCREEN,
@@ -558,15 +576,59 @@ function cancelCall(payload: CancelCallType): CancelCallActionType {
};
}
function declineCall(payload: DeclineCallType): DeclineCallActionType {
calling.decline(payload.conversationId);
function cancelIncomingGroupCallRing(
payload: CancelIncomingGroupCallRingType
): CancelIncomingGroupCallRingActionType {
return {
type: DECLINE_CALL,
type: CANCEL_INCOMING_GROUP_CALL_RING,
payload,
};
}
function declineCall(
payload: DeclineCallType
): ThunkAction<
void,
RootStateType,
unknown,
CancelIncomingGroupCallRingActionType | DeclineCallActionType
> {
return (dispatch, getState) => {
const { conversationId } = payload;
const call = getOwn(getState().calling.callsByConversation, conversationId);
if (!call) {
window.log.error('Trying to decline a non-existent call');
return;
}
switch (call.callMode) {
case CallMode.Direct:
calling.declineDirectCall(conversationId);
dispatch({
type: DECLINE_DIRECT_CALL,
payload,
});
break;
case CallMode.Group: {
const { ringId } = call;
if (ringId === undefined) {
window.log.error('Trying to decline a group call without a ring ID');
} else {
calling.declineGroupCall(conversationId, ringId);
dispatch({
type: CANCEL_INCOMING_GROUP_CALL_RING,
payload: { conversationId, ringId },
});
}
break;
}
default:
throw missingCaseError(call);
}
};
}
function getPresentingSources(): ThunkAction<
void,
RootStateType,
@@ -697,11 +759,20 @@ function keyChangeOk(
};
}
function receiveIncomingCall(
payload: IncomingCallType
): IncomingCallActionType {
function receiveIncomingDirectCall(
payload: IncomingDirectCallType
): IncomingDirectCallActionType {
return {
type: INCOMING_CALL,
type: INCOMING_DIRECT_CALL,
payload,
};
}
function receiveIncomingGroupCall(
payload: IncomingGroupCallType
): IncomingGroupCallActionType {
return {
type: INCOMING_GROUP_CALL,
payload,
};
}
@@ -718,8 +789,6 @@ function openSystemPreferencesAction(): ThunkAction<
}
function outgoingCall(payload: StartDirectCallType): OutgoingCallActionType {
callingTones.playRingtone();
return {
type: OUTGOING_CALL,
payload,
@@ -1033,6 +1102,7 @@ export const actions = {
acceptCall,
callStateChange,
cancelCall,
cancelIncomingGroupCallRing,
changeIODevice,
closeNeedPermissionScreen,
declineCall,
@@ -1044,7 +1114,8 @@ export const actions = {
openSystemPreferencesAction,
outgoingCall,
peekNotConnectedGroupCall,
receiveIncomingCall,
receiveIncomingDirectCall,
receiveIncomingGroupCall,
refreshIODevices,
remoteSharingScreenChange,
remoteVideoChange,
@@ -1083,14 +1154,12 @@ export function getEmptyState(): CallingStateType {
};
}
function getExistingPeekInfo(
function getGroupCall(
conversationId: string,
state: CallingStateType
): undefined | GroupCallPeekInfoType {
const existingCall = getOwn(state.callsByConversation, conversationId);
return existingCall?.callMode === CallMode.Group
? existingCall.peekInfo
: undefined;
state: Readonly<CallingStateType>
): undefined | GroupCallStateType {
const call = getOwn(state.callsByConversation, conversationId);
return call?.callMode === CallMode.Group ? call : undefined;
}
function removeConversationFromState(
@@ -1112,33 +1181,38 @@ export function reducer(
const { callsByConversation } = state;
if (action.type === SHOW_CALL_LOBBY) {
const { conversationId } = action.payload;
let call: DirectCallStateType | GroupCallStateType;
switch (action.payload.callMode) {
case CallMode.Direct:
call = {
callMode: CallMode.Direct,
conversationId: action.payload.conversationId,
conversationId,
isIncoming: false,
isVideoCall: action.payload.hasLocalVideo,
};
break;
case CallMode.Group:
case CallMode.Group: {
// We expect to be in this state briefly. The Calling service should update the
// call state shortly.
const existingCall = getGroupCall(conversationId, state);
call = {
callMode: CallMode.Group,
conversationId: action.payload.conversationId,
conversationId,
connectionState: action.payload.connectionState,
joinState: action.payload.joinState,
peekInfo: action.payload.peekInfo ||
getExistingPeekInfo(action.payload.conversationId, state) || {
existingCall?.peekInfo || {
uuids: action.payload.remoteParticipants.map(({ uuid }) => uuid),
maxDevices: Infinity,
deviceCount: action.payload.remoteParticipants.length,
},
remoteParticipants: action.payload.remoteParticipants,
...getGroupCallRingState(existingCall),
};
break;
}
default:
throw missingCaseError(action.payload);
}
@@ -1229,11 +1303,32 @@ export function reducer(
}
}
if (action.type === DECLINE_CALL) {
if (action.type === CANCEL_INCOMING_GROUP_CALL_RING) {
const { conversationId, ringId } = action.payload;
const groupCall = getGroupCall(conversationId, state);
if (!groupCall || groupCall.ringId !== ringId) {
return state;
}
if (groupCall.connectionState === GroupCallConnectionState.NotConnected) {
return removeConversationFromState(state, conversationId);
}
return {
...state,
callsByConversation: {
...callsByConversation,
[conversationId]: omit(groupCall, ['ringId', 'ringerUuid']),
},
};
}
if (action.type === DECLINE_DIRECT_CALL) {
return removeConversationFromState(state, action.payload.conversationId);
}
if (action.type === INCOMING_CALL) {
if (action.type === INCOMING_DIRECT_CALL) {
return {
...state,
callsByConversation: {
@@ -1249,6 +1344,52 @@ export function reducer(
};
}
if (action.type === INCOMING_GROUP_CALL) {
const { conversationId, ringId, ringerUuid } = action.payload;
let groupCall: GroupCallStateType;
const existingGroupCall = getGroupCall(conversationId, state);
if (existingGroupCall) {
if (existingGroupCall.ringerUuid) {
window.log.info('Group call was already ringing');
return state;
}
if (existingGroupCall.joinState !== GroupCallJoinState.NotJoined) {
window.log.info("Got a ring for a call we're already in");
return state;
}
groupCall = {
...existingGroupCall,
ringId,
ringerUuid,
};
} else {
groupCall = {
callMode: CallMode.Group,
conversationId,
connectionState: GroupCallConnectionState.NotConnected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
uuids: [],
maxDevices: Infinity,
deviceCount: 0,
},
remoteParticipants: [],
ringId,
ringerUuid,
};
}
return {
...state,
callsByConversation: {
...callsByConversation,
[conversationId]: groupCall,
},
};
}
if (action.type === OUTGOING_CALL) {
return {
...state,
@@ -1333,8 +1474,11 @@ export function reducer(
remoteParticipants,
} = action.payload;
const existingCall = getGroupCall(conversationId, state);
const existingRingState = getGroupCallRingState(existingCall);
const newPeekInfo = peekInfo ||
getExistingPeekInfo(conversationId, state) || {
existingCall?.peekInfo || {
uuids: remoteParticipants.map(({ uuid }) => uuid),
maxDevices: Infinity,
deviceCount: remoteParticipants.length,
@@ -1348,7 +1492,10 @@ export function reducer(
? undefined
: state.activeCallState;
if (!isAnybodyElseInGroupCall(newPeekInfo, ourUuid)) {
if (
!isAnybodyElseInGroupCall(newPeekInfo, ourUuid) &&
(!existingCall || !existingCall.ringerUuid)
) {
return {
...state,
callsByConversation: omit(callsByConversation, conversationId),
@@ -1366,6 +1513,13 @@ export function reducer(
: state.activeCallState;
}
let newRingState: GroupCallRingStateType;
if (joinState === GroupCallJoinState.NotJoined) {
newRingState = existingRingState;
} else {
newRingState = {};
}
return {
...state,
callsByConversation: {
@@ -1377,6 +1531,7 @@ export function reducer(
joinState,
peekInfo: newPeekInfo,
remoteParticipants,
...newRingState,
},
},
activeCallState: newActiveCallState,
@@ -1386,26 +1541,22 @@ export function reducer(
if (action.type === PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED) {
const { conversationId, peekInfo, ourConversationId } = action.payload;
const existingCall = getOwn(state.callsByConversation, conversationId) || {
const existingCall: GroupCallStateType = getGroupCall(
conversationId,
state
) || {
callMode: CallMode.Group,
conversationId,
connectionState: GroupCallConnectionState.NotConnected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
conversationIds: [],
uuids: [],
maxDevices: Infinity,
deviceCount: 0,
},
remoteParticipants: [],
};
if (existingCall.callMode !== CallMode.Group) {
window.log.error(
'Unexpected state: trying to update a non-group call. Doing nothing'
);
return state;
}
// This action should only update non-connected group calls. It's not necessarily a
// mistake if this action is dispatched "over" a connected call. Here's a valid
// sequence of events:
@@ -1419,7 +1570,10 @@ export function reducer(
return state;
}
if (!isAnybodyElseInGroupCall(peekInfo, ourConversationId)) {
if (
!isAnybodyElseInGroupCall(peekInfo, ourConversationId) ||
!existingCall.ringerUuid
) {
return removeConversationFromState(state, conversationId);
}