Ensure that call events don't override existing activeCallState

This commit is contained in:
Scott Nonnenberg
2025-02-28 10:25:45 +10:00
committed by GitHub
parent 3f0f307c22
commit a00086f416
2 changed files with 242 additions and 38 deletions

View File

@@ -2274,12 +2274,18 @@ const _startCallLinkLobby = async ({
dispatch(togglePip());
} else {
log.warn(
`${logId}: Attempted to start lobby while already waiting for it!`
`${logId}: Attempted to start lobby while already waiting for this call!`
);
}
return;
}
if (activeCallState) {
if (activeCallState.state !== 'Active') {
log.warn(
`${logId}: Call wasn't active; still showing leave call modal`,
activeCallState
);
}
dispatch(
toggleConfirmLeaveCallModal({
type: 'adhoc-rootKey',
@@ -2462,12 +2468,18 @@ function startCallingLobby({
dispatch(togglePip());
} else {
log.warn(
`${logId}: Attempted to start lobby while already waiting for it!`
`${logId}: Attempted to start lobby while already waiting for this call!`
);
}
return;
}
if (activeCallState) {
if (activeCallState.state !== 'Active') {
log.warn(
`${logId}: Call wasn't active; still showing leave call modal`,
activeCallState
);
}
dispatch(
toggleConfirmLeaveCallModal({
type: 'conversation',
@@ -2550,8 +2562,12 @@ function startCall(
log.info(`${logId}: starting, mode ${callMode}`);
if (activeCallState?.state === 'Waiting') {
log.error(`${logId}: Call is not ready; `);
if (
!activeCallState ||
activeCallState?.state === 'Waiting' ||
activeCallState?.conversationId !== conversationId
) {
log.error(`${logId}: Call is not ready`, activeCallState);
return;
}
@@ -2840,6 +2856,14 @@ export function reducer(
if (action.type === WAITING_FOR_CALLING_LOBBY) {
const { conversationId } = action.payload;
if (state.activeCallState) {
log.warn(
`${action.type}: Already have an active call!`,
state.activeCallState
);
return state;
}
return {
...state,
activeCallState: {
@@ -2848,9 +2872,18 @@ export function reducer(
},
};
}
if (action.type === WAITING_FOR_CALL_LINK_LOBBY) {
const { roomId } = action.payload;
if (state.activeCallState) {
log.warn(
`${action.type}: Already have an active call!`,
state.activeCallState
);
return state;
}
return {
...state,
activeCallState: {
@@ -2859,24 +2892,45 @@ export function reducer(
},
};
}
if (action.type === CALL_LOBBY_FAILED) {
const { conversationId } = action.payload;
const { activeCallState } = state;
if (!activeCallState || activeCallState.conversationId !== conversationId) {
if (
!activeCallState ||
activeCallState.conversationId !== conversationId ||
activeCallState.state !== 'Waiting'
) {
log.warn(
`${action.type}: Active call does not match target conversation`
`${action.type}: Active call does not match target conversation`,
activeCallState
);
return state;
}
return removeConversationFromState(state, conversationId);
}
if (
action.type === START_CALLING_LOBBY ||
action.type === START_CALL_LINK_LOBBY
) {
const { callMode, conversationId } = action.payload;
const { activeCallState } = state;
if (
!activeCallState ||
activeCallState.conversationId !== conversationId ||
activeCallState.state !== 'Waiting'
) {
log.warn(
`${action.type}: Active call does not match target conversation`,
activeCallState
);
return state;
}
let call: DirectCallStateType | GroupCallStateType;
let newAdhocCalls: AdhocCallsType;
let outgoingRing: boolean;
@@ -2987,13 +3041,28 @@ export function reducer(
}
if (action.type === START_DIRECT_CALL) {
const { conversationId } = action.payload;
const { activeCallState } = state;
if (
activeCallState &&
(activeCallState.state === 'Waiting' ||
activeCallState.conversationId !== conversationId)
) {
log.warn(
`${action.type}: Cannot start call; activeCall doesn't match conversation`,
activeCallState
);
return state;
}
return {
...state,
callsByConversation: {
...callsByConversation,
[action.payload.conversationId]: {
[conversationId]: {
callMode: CallMode.Direct,
conversationId: action.payload.conversationId,
conversationId,
callState: CallState.Prering,
isIncoming: false,
isVideoCall: action.payload.hasLocalVideo,
@@ -3002,7 +3071,7 @@ export function reducer(
activeCallState: {
state: 'Active',
callMode: CallMode.Direct,
conversationId: action.payload.conversationId,
conversationId,
hasLocalAudio: action.payload.hasLocalAudio,
hasLocalVideo: action.payload.hasLocalVideo,
localAudioLevel: 0,
@@ -3017,21 +3086,24 @@ export function reducer(
}
if (action.type === ACCEPT_CALL_PENDING) {
const call = getOwn(
state.callsByConversation,
action.payload.conversationId
);
const { conversationId } = action.payload;
const call = getOwn(state.callsByConversation, conversationId);
if (!call) {
log.warn('Unable to accept a non-existent call');
return state;
}
const { activeCallState } = state;
if (!activeCallState || activeCallState.conversationId !== conversationId) {
log.warn(`${action.type}: Active call didn't match:`, activeCallState);
}
return {
...state,
activeCallState: {
state: 'Active',
callMode: call.callMode,
conversationId: action.payload.conversationId,
conversationId,
hasLocalAudio: true,
hasLocalVideo: action.payload.asVideoCall,
localAudioLevel: 0,
@@ -3129,13 +3201,27 @@ export function reducer(
}
if (action.type === INCOMING_DIRECT_CALL) {
const { conversationId } = action.payload;
const { activeCallState } = state;
if (activeCallState && activeCallState.conversationId !== conversationId) {
log.warn(
`${action.type}: activeCallState didn't match conversation; overriding.`,
activeCallState
);
}
return {
...state,
activeCallState: {
state: 'Waiting',
conversationId,
},
callsByConversation: {
...callsByConversation,
[action.payload.conversationId]: {
[conversationId]: {
callMode: CallMode.Direct,
conversationId: action.payload.conversationId,
conversationId,
callState: CallState.Prering,
isIncoming: true,
isVideoCall: action.payload.isVideoCall,
@@ -3197,13 +3283,27 @@ export function reducer(
}
if (action.type === OUTGOING_CALL) {
const { conversationId } = action.payload;
const { activeCallState } = state;
if (
activeCallState &&
(activeCallState.state === 'Waiting' ||
activeCallState.conversationId !== conversationId)
) {
log.warn(
`${action.type}: Cannot start call; activeCall doesn't match conversation`
);
return state;
}
return {
...state,
callsByConversation: {
...callsByConversation,
[action.payload.conversationId]: {
[conversationId]: {
callMode: CallMode.Direct,
conversationId: action.payload.conversationId,
conversationId,
callState: CallState.Prering,
isIncoming: false,
isVideoCall: action.payload.hasLocalVideo,
@@ -3212,7 +3312,7 @@ export function reducer(
activeCallState: {
state: 'Active',
callMode: CallMode.Direct,
conversationId: action.payload.conversationId,
conversationId,
hasLocalAudio: action.payload.hasLocalAudio,
hasLocalVideo: action.payload.hasLocalVideo,
localAudioLevel: 0,

View File

@@ -5,7 +5,10 @@ import { assert } from 'chai';
import * as sinon from 'sinon';
import { cloneDeep, noop } from 'lodash';
import type { PeekInfo } from '@signalapp/ringrtc';
import type { StateType as RootStateType } from '../../../state/reducer';
import type {
StateType as RootStateType,
StateType,
} from '../../../state/reducer';
import { reducer as rootReducer } from '../../../state/reducer';
import { noopAction } from '../../../state/ducks/noop';
import type {
@@ -199,7 +202,7 @@ describe('calling duck', () => {
const ourAci = generateAci();
const getEmptyRootState = () => {
const getEmptyRootState = (): StateType => {
const rootState = rootReducer(undefined, noopAction());
return {
...rootState,
@@ -2274,10 +2277,11 @@ describe('calling duck', () => {
const waitingAction = dispatch.getCall(0).args[0];
assert.equal(waitingAction.type, 'calling/WAITING_FOR_CALLING_LOBBY');
const waitingState = reducer(callingState, waitingAction);
const action = dispatch.getCall(1).args[0];
return reducer(callingState, action);
const startLobbyAction = dispatch.getCall(1).args[0];
assert.equal(startLobbyAction.type, 'calling/START_CALLING_LOBBY');
return reducer(waitingState, startLobbyAction);
};
it('saves a direct call and makes it active', async () => {
@@ -2563,17 +2567,40 @@ describe('calling duck', () => {
it('asks the calling service to start an outgoing direct call', async function (this: Mocha.Context) {
const dispatch = sinon.spy();
const emptyState = getEmptyRootState();
const conversationId = '123';
const startState: StateType = {
...emptyState,
calling: {
...emptyState.calling,
activeCallState: {
state: 'Active',
callMode: CallMode.Direct,
conversationId,
hasLocalAudio: true,
hasLocalVideo: false,
localAudioLevel: 0,
viewMode: CallViewMode.Sidebar,
joinedAt: null,
outgoingRing: true,
pip: false,
settingsDialogOpen: false,
showParticipantsList: false,
},
},
};
await startCall({
callMode: CallMode.Direct,
conversationId: '123',
conversationId,
hasLocalAudio: true,
hasLocalVideo: false,
})(dispatch, getEmptyRootState, null);
})(dispatch, () => startState, null);
sinon.assert.calledOnce(this.callingStartOutgoingDirectCall);
sinon.assert.calledWith(
this.callingStartOutgoingDirectCall,
'123',
conversationId,
true,
false
);
@@ -2583,34 +2610,87 @@ describe('calling duck', () => {
it('asks the calling service to join a group call', async function (this: Mocha.Context) {
const dispatch = sinon.spy();
const emptyState = getEmptyRootState();
const conversationId = '123';
const startState: StateType = {
...emptyState,
calling: {
...emptyState.calling,
activeCallState: {
state: 'Active',
callMode: CallMode.Group,
conversationId,
hasLocalAudio: true,
hasLocalVideo: false,
localAudioLevel: 0,
viewMode: CallViewMode.Sidebar,
joinedAt: null,
outgoingRing: true,
pip: false,
settingsDialogOpen: false,
showParticipantsList: false,
},
},
};
await startCall({
callMode: CallMode.Group,
conversationId: '123',
conversationId,
hasLocalAudio: true,
hasLocalVideo: false,
})(dispatch, getEmptyRootState, null);
})(dispatch, () => startState, null);
sinon.assert.calledOnce(this.callingJoinGroupCall);
sinon.assert.calledWith(this.callingJoinGroupCall, '123', true, false);
sinon.assert.calledWith(
this.callingJoinGroupCall,
conversationId,
true,
false
);
sinon.assert.notCalled(this.callingStartOutgoingDirectCall);
});
it('saves direct calls and makes them active', async () => {
const dispatch = sinon.spy();
const emptyState = getEmptyRootState();
const conversationId = '123';
const startState: StateType = {
...emptyState,
calling: {
...emptyState.calling,
activeCallState: {
state: 'Active',
callMode: CallMode.Direct,
conversationId,
hasLocalAudio: true,
hasLocalVideo: false,
localAudioLevel: 0,
viewMode: CallViewMode.Sidebar,
joinedAt: null,
outgoingRing: true,
pip: false,
settingsDialogOpen: false,
showParticipantsList: false,
},
},
};
await startCall({
callMode: CallMode.Direct,
conversationId: 'fake-conversation-id',
conversationId,
hasLocalAudio: true,
hasLocalVideo: false,
})(dispatch, getEmptyRootState, null);
})(dispatch, () => startState, null);
const action = dispatch.getCall(0).args[0];
const result = reducer(getEmptyState(), action);
const result = reducer(startState.calling, action);
assert.deepEqual(result.callsByConversation['fake-conversation-id'], {
assert.deepEqual(result.callsByConversation[conversationId], {
callMode: CallMode.Direct,
conversationId: 'fake-conversation-id',
conversationId,
callState: CallState.Prering,
isIncoming: false,
isVideoCall: false,
@@ -2618,7 +2698,7 @@ describe('calling duck', () => {
assert.deepEqual(result.activeCallState, {
state: 'Active',
callMode: CallMode.Direct,
conversationId: 'fake-conversation-id',
conversationId,
hasLocalAudio: true,
hasLocalVideo: false,
localAudioLevel: 0,
@@ -2633,12 +2713,36 @@ describe('calling duck', () => {
it("doesn't dispatch any actions for group calls", async () => {
const dispatch = sinon.spy();
const emptyState = getEmptyRootState();
const conversationId = '123';
const startState: StateType = {
...emptyState,
calling: {
...emptyState.calling,
activeCallState: {
state: 'Active',
callMode: CallMode.Group,
conversationId,
hasLocalAudio: true,
hasLocalVideo: false,
localAudioLevel: 0,
viewMode: CallViewMode.Sidebar,
joinedAt: null,
outgoingRing: true,
pip: false,
settingsDialogOpen: false,
showParticipantsList: false,
},
},
};
await startCall({
callMode: CallMode.Group,
conversationId: '123',
conversationId,
hasLocalAudio: true,
hasLocalVideo: false,
})(dispatch, getEmptyRootState, null);
})(dispatch, () => startState, null);
sinon.assert.notCalled(dispatch);
});