Group calls: mute in the lobby if joining a large call

This commit is contained in:
Evan Hahn 2022-01-07 12:01:23 -06:00 committed by GitHub
parent 09af7eeece
commit f8bbf5c998
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 499 additions and 279 deletions

View File

@ -1447,6 +1447,10 @@
"message": "Return to Call",
"description": "Button label in the call lobby for returning to a call"
},
"calling__lobby-automatically-muted-because-there-are-a-lot-of-people": {
"message": "Your microphone is muted due to the size of the call",
"description": "Shown in a call lobby toast if there are a lot of people already on the call"
},
"calling__call-is-full": {
"message": "Call is full",
"description": "Text in the call lobby when you can't join because the call is full"

View File

@ -1,4 +1,4 @@
// Copyright 2018-2021 Signal Messenger, LLC
// Copyright 2018-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// Using BEM syntax explained here: https://csswizardry.com/2013/01/mindbemding-getting-your-head-round-bem-syntax/
@ -4285,28 +4285,6 @@ button.module-image__border-overlay:focus {
fill-mode: forwards;
}
}
&__toast {
@include button-reset();
@include font-body-1-bold;
background-color: $color-gray-75;
border-radius: 8px;
color: $color-white;
max-width: 80%;
opacity: 1;
padding: 12px;
position: absolute;
text-align: center;
top: 12px;
transition: top 200ms ease-out, opacity 200ms ease-out;
user-select: none;
z-index: $z-index-above-above-base;
&--hidden {
opacity: 0;
top: 5px;
}
}
}
.module-calling-tools {

View File

@ -0,0 +1,24 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.CallingToast {
@include button-reset();
@include font-body-1-bold;
background-color: $color-gray-75;
border-radius: 8px;
color: $color-white;
max-width: 80%;
opacity: 1;
padding: 12px;
position: absolute;
text-align: center;
top: 12px;
transition: top 200ms ease-out, opacity 200ms ease-out;
user-select: none;
z-index: $z-index-above-above-base;
&--hidden {
opacity: 0;
top: 5px;
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2014-2021 Signal Messenger, LLC
// Copyright 2014-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// Global Settings, Variables, and Mixins
@ -43,6 +43,7 @@
@import './components/CallingPreCallInfo.scss';
@import './components/CallingScreenSharingController.scss';
@import './components/CallingSelectPresentingSourcesModal.scss';
@import './components/CallingToast.scss';
@import './components/ChatColorPicker.scss';
@import './components/Checkbox.scss';
@import './components/CompositionArea.scss';

View File

@ -1,4 +1,4 @@
// Copyright 2020-2021 Signal Messenger, LLC
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
@ -51,11 +51,11 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
(isGroupCall ? times(3, () => getDefaultConversation()) : undefined),
hasLocalAudio: boolean(
'hasLocalAudio',
overrideProps.hasLocalAudio || false
overrideProps.hasLocalAudio ?? true
),
hasLocalVideo: boolean(
'hasLocalVideo',
overrideProps.hasLocalVideo || false
overrideProps.hasLocalVideo ?? false
),
i18n,
isGroupCall,
@ -122,9 +122,9 @@ story.add('Local Video', () => {
return <CallingLobby {...props} />;
});
story.add('Local Video', () => {
story.add('Initially muted', () => {
const props = createProps({
hasLocalVideo: true,
hasLocalAudio: false,
});
return <CallingLobby {...props} />;
});

View File

@ -1,4 +1,4 @@
// Copyright 2020-2021 Signal Messenger, LLC
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
@ -13,6 +13,7 @@ import { CallingButton, CallingButtonType } from './CallingButton';
import { TooltipPlacement } from './Tooltip';
import { CallBackgroundBlur } from './CallBackgroundBlur';
import { CallingHeader } from './CallingHeader';
import { CallingToast, DEFAULT_LIFETIME } from './CallingToast';
import { CallingPreCallInfo, RingMode } from './CallingPreCallInfo';
import {
CallingLobbyJoinButton,
@ -92,6 +93,21 @@ export const CallingLobby = ({
toggleSettings,
outgoingRing,
}: PropsType): JSX.Element => {
const [isMutedToastVisible, setIsMutedToastVisible] = React.useState(
!hasLocalAudio
);
React.useEffect(() => {
if (!isMutedToastVisible) {
return;
}
const timeout = setTimeout(() => {
setIsMutedToastVisible(false);
}, DEFAULT_LIFETIME);
return () => {
clearTimeout(timeout);
};
}, [isMutedToastVisible]);
const localVideoRef = React.useRef<null | HTMLVideoElement>(null);
const shouldShowLocalVideo = hasLocalVideo && availableCameras.length > 0;
@ -221,6 +237,15 @@ export const CallingLobby = ({
/>
)}
<CallingToast
isVisible={isMutedToastVisible}
onClick={() => setIsMutedToastVisible(false)}
>
{i18n(
'calling__lobby-automatically-muted-because-there-are-a-lot-of-people'
)}
</CallingToast>
<CallingHeader
i18n={i18n}
isGroupCall={isGroupCall}

View File

@ -0,0 +1,27 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { FunctionComponent } from 'react';
import React from 'react';
import classNames from 'classnames';
type PropsType = {
isVisible: boolean;
onClick: () => unknown;
};
export const DEFAULT_LIFETIME = 5000;
export const CallingToast: FunctionComponent<PropsType> = ({
isVisible,
onClick,
children,
}) => (
<button
className={classNames('CallingToast', !isVisible && 'CallingToast--hidden')}
type="button"
onClick={onClick}
>
{children}
</button>
);

View File

@ -1,12 +1,12 @@
// Copyright 2020-2021 Signal Messenger, LLC
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import type { ActiveCallType } from '../types/Calling';
import { CallMode, GroupCallConnectionState } from '../types/Calling';
import type { ConversationType } from '../state/ducks/conversations';
import type { LocalizerType } from '../types/Util';
import { CallingToast, DEFAULT_LIFETIME } from './CallingToast';
type PropsType = {
activeCall: ActiveCallType;
@ -101,8 +101,6 @@ function useScreenSharingToast({ activeCall, i18n }: PropsType): ToastType {
return result;
}
const DEFAULT_DELAY = 5000;
// In the future, this component should show toasts when users join or leave. See
// DESKTOP-902.
export const CallingToastManager: React.FC<PropsType> = props => {
@ -131,7 +129,7 @@ export const CallingToastManager: React.FC<PropsType> = props => {
if (timeoutRef && timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(dismissToast, DEFAULT_DELAY);
timeoutRef.current = setTimeout(dismissToast, DEFAULT_LIFETIME);
}
setToastMessage(toast.message);
@ -144,17 +142,9 @@ export const CallingToastManager: React.FC<PropsType> = props => {
};
}, [dismissToast, setToastMessage, timeoutRef, toast]);
const isVisible = Boolean(toastMessage);
return (
<button
className={classNames('module-ongoing-call__toast', {
'module-ongoing-call__toast--hidden': !isVisible,
})}
type="button"
onClick={dismissToast}
>
<CallingToast isVisible={Boolean(toastMessage)} onClick={dismissToast}>
{toastMessage}
</button>
</CallingToast>
);
};

View File

@ -1,4 +1,4 @@
// Copyright 2020-2021 Signal Messenger, LLC
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { DesktopCapturerSource } from 'electron';
@ -39,10 +39,11 @@ import { uniqBy, noop } from 'lodash';
import type {
ActionsType as UxActionsType,
GroupCallParticipantInfoType,
GroupCallPeekInfoType,
} from '../state/ducks/calling';
import type { ConversationType } from '../state/ducks/conversations';
import { getConversationCallMode } from '../state/ducks/conversations';
import { isConversationTooBigToRing } from '../conversations/isConversationTooBigToRing';
import { isMe } from '../util/whatTypeOfConversation';
import type {
AvailableIODevicesType,
@ -99,6 +100,7 @@ import {
FALLBACK_NOTIFICATION_TITLE,
} from './notifications';
import * as log from '../logging/log';
import { assert } from '../util/assert';
const {
processGroupCallRingRequest,
@ -308,30 +310,47 @@ export class CallingClass {
RingRTC.setSelfUuid(Buffer.from(uuidToBytes(ourUuid)));
}
async startCallingLobby(
conversationId: string,
isVideoCall: boolean
): Promise<void> {
async startCallingLobby({
conversation,
hasLocalAudio,
hasLocalVideo,
}: Readonly<{
conversation: Readonly<ConversationType>;
hasLocalAudio: boolean;
hasLocalVideo: boolean;
}>): Promise<
| undefined
| ({ hasLocalAudio: boolean; hasLocalVideo: boolean } & (
| { callMode: CallMode.Direct }
| {
callMode: CallMode.Group;
connectionState: GroupCallConnectionState;
joinState: GroupCallJoinState;
peekInfo?: GroupCallPeekInfoType;
remoteParticipants: Array<GroupCallParticipantInfoType>;
}
))
> {
log.info('CallingClass.startCallingLobby()');
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
log.error('Could not find conversation, cannot start call lobby');
return;
}
const conversationProps = conversation.format();
const callMode = getConversationCallMode(conversationProps);
const callMode = getConversationCallMode(conversation);
switch (callMode) {
case CallMode.None:
log.error('Conversation does not support calls, new call not allowed.');
return;
case CallMode.Direct:
if (!this.getRemoteUserIdFromConversation(conversation)) {
case CallMode.Direct: {
const conversationModel = window.ConversationController.get(
conversation.id
);
if (
!conversationModel ||
!this.getRemoteUserIdFromConversation(conversationModel)
) {
log.error('Missing remote user identifier, new call not allowed.');
return;
}
break;
}
case CallMode.Group:
break;
default:
@ -348,7 +367,7 @@ export class CallingClass {
return;
}
const haveMediaPermissions = await this.requestPermissions(isVideoCall);
const haveMediaPermissions = await this.requestPermissions(hasLocalVideo);
if (!haveMediaPermissions) {
log.info('Permissions were denied, new call not allowed.');
return;
@ -374,51 +393,53 @@ export class CallingClass {
// is fixed. See DESKTOP-1032.
await this.startDeviceReselectionTimer();
const enableLocalCameraIfNecessary = hasLocalVideo
? () => this.enableLocalCamera()
: noop;
switch (callMode) {
case CallMode.Direct:
this.uxActions.showCallLobby({
// We could easily support this in the future if we need to.
assert(
hasLocalAudio,
'Expected local audio to be enabled for direct call lobbies'
);
enableLocalCameraIfNecessary();
return {
callMode: CallMode.Direct,
conversationId: conversationProps.id,
hasLocalAudio: true,
hasLocalVideo: isVideoCall,
});
break;
hasLocalAudio,
hasLocalVideo,
};
case CallMode.Group: {
if (
!conversationProps.groupId ||
!conversationProps.publicParams ||
!conversationProps.secretParams
!conversation.groupId ||
!conversation.publicParams ||
!conversation.secretParams
) {
log.error(
'Conversation is missing required parameters. Cannot connect group call'
);
return;
}
const groupCall = this.connectGroupCall(conversationProps.id, {
groupId: conversationProps.groupId,
publicParams: conversationProps.publicParams,
secretParams: conversationProps.secretParams,
const groupCall = this.connectGroupCall(conversation.id, {
groupId: conversation.groupId,
publicParams: conversation.publicParams,
secretParams: conversation.secretParams,
});
groupCall.setOutgoingAudioMuted(false);
groupCall.setOutgoingVideoMuted(!isVideoCall);
groupCall.setOutgoingAudioMuted(!hasLocalAudio);
groupCall.setOutgoingVideoMuted(!hasLocalVideo);
this.uxActions.showCallLobby({
enableLocalCameraIfNecessary();
return {
callMode: CallMode.Group,
conversationId: conversationProps.id,
isConversationTooBigToRing:
isConversationTooBigToRing(conversationProps),
...this.formatGroupCallForRedux(groupCall),
});
break;
};
}
default:
throw missingCaseError(callMode);
}
if (isVideoCall) {
this.enableLocalCamera();
}
}
stopCallingLobby(conversationId?: string): void {
@ -443,7 +464,6 @@ export class CallingClass {
}
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
log.error('Could not find conversation, cannot start call');
this.stopCallingLobby();

View File

@ -1,4 +1,4 @@
// Copyright 2020-2021 Signal Messenger, LLC
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { ipcRenderer } from 'electron';
@ -38,6 +38,7 @@ import { LatestQueue } from '../../util/LatestQueue';
import type { UUIDStringType } from '../../types/UUID';
import type { ConversationChangedActionType } from './conversations';
import * as log from '../../logging/log';
import { strictAssert } from '../../util/assert';
// State
@ -223,7 +224,7 @@ export type StartCallingLobbyType = {
isVideoCall: boolean;
};
export type ShowCallLobbyType =
type StartCallingLobbyPayloadType =
| {
callMode: CallMode.Direct;
conversationId: string;
@ -299,7 +300,7 @@ 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 START_CALLING_LOBBY = 'calling/START_CALLING_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';
@ -344,9 +345,9 @@ type CancelIncomingGroupCallRingActionType = {
payload: CancelIncomingGroupCallRingType;
};
type CallLobbyActionType = {
type: 'calling/SHOW_CALL_LOBBY';
payload: ShowCallLobbyType;
type StartCallingLobbyActionType = {
type: 'calling/START_CALLING_LOBBY';
payload: StartCallingLobbyPayloadType;
};
type CallStateChangeFulfilledActionType = {
@ -460,8 +461,8 @@ type SetOutgoingRingActionType = {
};
type ShowCallLobbyActionType = {
type: 'calling/SHOW_CALL_LOBBY';
payload: ShowCallLobbyType;
type: 'calling/START_CALLING_LOBBY';
payload: StartCallingLobbyPayloadType;
};
type StartDirectCallActionType = {
@ -493,7 +494,7 @@ export type CallingActionType =
| AcceptCallPendingActionType
| CancelCallActionType
| CancelIncomingGroupCallRingActionType
| CallLobbyActionType
| StartCallingLobbyActionType
| CallStateChangeFulfilledActionType
| ChangeIODeviceFulfilledActionType
| CloseNeedPermissionScreenActionType
@ -1081,20 +1082,50 @@ function setOutgoingRing(payload: boolean): SetOutgoingRingActionType {
};
}
function startCallingLobby(
payload: StartCallingLobbyType
): ThunkAction<void, RootStateType, unknown, never> {
return () => {
calling.startCallingLobby(payload.conversationId, payload.isVideoCall);
};
}
function startCallingLobby({
conversationId,
isVideoCall,
}: StartCallingLobbyType): ThunkAction<
void,
RootStateType,
unknown,
StartCallingLobbyActionType
> {
return async (dispatch, getState) => {
const state = getState();
const conversation = getOwn(
state.conversations.conversationLookup,
conversationId
);
strictAssert(
conversation,
"startCallingLobby: can't start lobby without a conversation"
);
// TODO: This action should be replaced with an action dispatched in the
// `startCallingLobby` thunk.
function showCallLobby(payload: ShowCallLobbyType): CallLobbyActionType {
return {
type: SHOW_CALL_LOBBY,
payload,
// The group call device count is considered 0 for a direct call.
const groupCall = getGroupCall(conversationId, state.calling);
const groupCallDeviceCount =
groupCall?.peekInfo.deviceCount ||
groupCall?.remoteParticipants.length ||
0;
const callLobbyData = await calling.startCallingLobby({
conversation,
hasLocalAudio: groupCallDeviceCount < 8,
hasLocalVideo: isVideoCall,
});
if (!callLobbyData) {
return;
}
dispatch({
type: START_CALLING_LOBBY,
payload: {
...callLobbyData,
conversationId,
isConversationTooBigToRing: isConversationTooBigToRing(conversation),
},
});
};
}
@ -1207,7 +1238,6 @@ export const actions = {
setPresenting,
setRendererCanvas,
setOutgoingRing,
showCallLobby,
startCall,
startCallingLobby,
toggleParticipants,
@ -1261,7 +1291,7 @@ export function reducer(
): CallingStateType {
const { callsByConversation } = state;
if (action.type === SHOW_CALL_LOBBY) {
if (action.type === START_CALLING_LOBBY) {
const { conversationId } = action.payload;
let call: DirectCallStateType | GroupCallStateType;

View File

@ -1,8 +1,10 @@
// Copyright 2020-2021 Signal Messenger, LLC
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import * as sinon from 'sinon';
import { cloneDeep, noop } from 'lodash';
import type { StateType as RootStateType } from '../../../state/reducer';
import { reducer as rootReducer } from '../../../state/reducer';
import { noopAction } from '../../../state/ducks/noop';
import type {
@ -25,6 +27,8 @@ import {
} from '../../../types/Calling';
import { UUID } from '../../../types/UUID';
import type { UUIDStringType } from '../../../types/UUID';
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
import type { UnwrapPromise } from '../../../types/Util';
describe('calling duck', () => {
const stateWithDirectCall: CallingStateType = {
@ -1606,48 +1610,175 @@ describe('calling duck', () => {
});
});
describe('showCallLobby', () => {
const { showCallLobby } = actions;
describe('startCallingLobby', () => {
const { startCallingLobby } = actions;
it('saves a direct call and makes it active', () => {
const result = reducer(
getEmptyState(),
showCallLobby({
callMode: CallMode.Direct,
let rootState: RootStateType;
let startCallingLobbyStub: sinon.SinonStub;
beforeEach(function beforeEach() {
startCallingLobbyStub = this.sandbox
.stub(callingService, 'startCallingLobby')
.resolves();
const emptyRootState = getEmptyRootState();
rootState = {
...emptyRootState,
conversations: {
...emptyRootState.conversations,
conversationLookup: {
'fake-conversation-id': getDefaultConversation(),
},
},
};
});
describe('thunk', () => {
it('asks the calling service to start the lobby', async () => {
await startCallingLobby({
conversationId: 'fake-conversation-id',
isVideoCall: true,
})(noop, () => rootState, null);
sinon.assert.calledOnce(startCallingLobbyStub);
});
it('requests audio by default', async () => {
await startCallingLobby({
conversationId: 'fake-conversation-id',
isVideoCall: true,
})(noop, () => rootState, null);
sinon.assert.calledWithMatch(startCallingLobbyStub, {
hasLocalAudio: true,
});
});
it("doesn't request audio if the group call already has 8 devices", async () => {
await startCallingLobby({
conversationId: 'fake-conversation-id',
isVideoCall: true,
})(
noop,
() => {
const callingState = cloneDeep(stateWithGroupCall);
callingState.callsByConversation[
'fake-group-call-conversation-id'
].peekInfo.deviceCount = 8;
return { ...rootState, calling: callingState };
},
null
);
sinon.assert.calledWithMatch(startCallingLobbyStub, {
hasLocalVideo: true,
});
});
it('requests video when starting a video call', async () => {
await startCallingLobby({
conversationId: 'fake-conversation-id',
isVideoCall: true,
})(noop, () => rootState, null);
sinon.assert.calledWithMatch(startCallingLobbyStub, {
hasLocalVideo: true,
});
});
it("doesn't request video when not a video call", async () => {
await startCallingLobby({
conversationId: 'fake-conversation-id',
isVideoCall: false,
})(noop, () => rootState, null);
sinon.assert.calledWithMatch(startCallingLobbyStub, {
hasLocalVideo: false,
});
});
it('dispatches an action if the calling lobby returns something', async () => {
startCallingLobbyStub.resolves({
callMode: CallMode.Direct,
hasLocalAudio: true,
hasLocalVideo: true,
})
);
});
assert.deepEqual(result.callsByConversation['fake-conversation-id'], {
callMode: CallMode.Direct,
conversationId: 'fake-conversation-id',
isIncoming: false,
isVideoCall: true,
const dispatch = sinon.stub();
await startCallingLobby({
conversationId: 'fake-conversation-id',
isVideoCall: true,
})(dispatch, () => rootState, null);
sinon.assert.calledOnce(dispatch);
});
assert.deepEqual(result.activeCallState, {
conversationId: 'fake-conversation-id',
hasLocalAudio: true,
hasLocalVideo: true,
isInSpeakerView: false,
showParticipantsList: false,
safetyNumberChangedUuids: [],
pip: false,
settingsDialogOpen: false,
outgoingRing: true,
it("doesn't dispatch an action if the calling lobby returns nothing", async () => {
const dispatch = sinon.stub();
await startCallingLobby({
conversationId: 'fake-conversation-id',
isVideoCall: true,
})(dispatch, () => rootState, null);
sinon.assert.notCalled(dispatch);
});
});
it('saves a group call and makes it active', () => {
const result = reducer(
getEmptyState(),
showCallLobby({
callMode: CallMode.Group,
describe('action', () => {
const getState = async (
callingState: CallingStateType,
callingServiceResult: UnwrapPromise<
ReturnType<typeof callingService.startCallingLobby>
>,
conversationId = 'fake-conversation-id'
): Promise<CallingStateType> => {
startCallingLobbyStub.resolves(callingServiceResult);
const dispatch = sinon.stub();
await startCallingLobby({
conversationId,
isVideoCall: true,
})(dispatch, () => ({ ...rootState, calling: callingState }), null);
const action = dispatch.getCall(0).args[0];
return reducer(callingState, action);
};
it('saves a direct call and makes it active', async () => {
const result = await getState(getEmptyState(), {
callMode: CallMode.Direct as const,
hasLocalAudio: true,
hasLocalVideo: true,
});
assert.deepEqual(result.callsByConversation['fake-conversation-id'], {
callMode: CallMode.Direct,
conversationId: 'fake-conversation-id',
isIncoming: false,
isVideoCall: true,
});
assert.deepEqual(result.activeCallState, {
conversationId: 'fake-conversation-id',
hasLocalAudio: true,
hasLocalVideo: true,
isConversationTooBigToRing: false,
isInSpeakerView: false,
showParticipantsList: false,
safetyNumberChangedUuids: [],
pip: false,
settingsDialogOpen: false,
outgoingRing: true,
});
});
it('saves a group call and makes it active', async () => {
const result = await getState(getEmptyState(), {
callMode: CallMode.Group,
hasLocalAudio: true,
hasLocalVideo: true,
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
@ -1668,74 +1799,63 @@ describe('calling duck', () => {
videoAspectRatio: 4 / 3,
},
],
})
);
});
assert.deepEqual(result.callsByConversation['fake-conversation-id'], {
callMode: CallMode.Group,
conversationId: 'fake-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
uuids: [creatorUuid],
creatorUuid,
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
},
remoteParticipants: [
{
uuid: remoteUuid,
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
});
assert.deepEqual(
result.activeCallState?.conversationId,
'fake-conversation-id'
);
assert.isFalse(result.activeCallState?.outgoingRing);
});
it('chooses fallback peek info if none is sent and there is no existing call', () => {
const result = reducer(
getEmptyState(),
showCallLobby({
assert.deepEqual(result.callsByConversation['fake-conversation-id'], {
callMode: CallMode.Group,
conversationId: 'fake-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
uuids: [creatorUuid],
creatorUuid,
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
},
remoteParticipants: [
{
uuid: remoteUuid,
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
});
assert.deepEqual(
result.activeCallState?.conversationId,
'fake-conversation-id'
);
assert.isFalse(result.activeCallState?.outgoingRing);
});
it('chooses fallback peek info if none is sent and there is no existing call', async () => {
const result = await getState(getEmptyState(), {
callMode: CallMode.Group,
conversationId: 'fake-group-call-conversation-id',
hasLocalAudio: true,
hasLocalVideo: true,
isConversationTooBigToRing: false,
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: undefined,
remoteParticipants: [],
})
);
});
const call =
result.callsByConversation['fake-group-call-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
uuids: [],
maxDevices: Infinity,
deviceCount: 0,
const call = result.callsByConversation['fake-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
uuids: [],
maxDevices: Infinity,
deviceCount: 0,
});
});
});
it("doesn't overwrite an existing group call's peek info if none was sent", () => {
const result = reducer(
stateWithGroupCall,
showCallLobby({
it("doesn't overwrite an existing group call's peek info if none was sent", async () => {
const result = await getState(stateWithGroupCall, {
callMode: CallMode.Group,
conversationId: 'fake-group-call-conversation-id',
hasLocalAudio: true,
hasLocalVideo: true,
isConversationTooBigToRing: false,
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: undefined,
@ -1750,29 +1870,36 @@ describe('calling duck', () => {
videoAspectRatio: 4 / 3,
},
],
})
);
});
const call =
result.callsByConversation['fake-group-call-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
uuids: [creatorUuid],
creatorUuid,
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
const call =
result.callsByConversation['fake-group-call-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
uuids: [creatorUuid],
creatorUuid,
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
});
});
});
it("can overwrite an existing group call's peek info", () => {
const result = reducer(
stateWithGroupCall,
showCallLobby({
it("can overwrite an existing group call's peek info", async () => {
const state = {
...getEmptyState(),
callsByConversation: {
'fake-conversation-id': {
...stateWithGroupCall.callsByConversation[
'fake-group-call-conversation-id'
],
conversationId: 'fake-conversation-id',
},
},
};
const result = await getState(state, {
callMode: CallMode.Group,
conversationId: 'fake-group-call-conversation-id',
hasLocalAudio: true,
hasLocalVideo: true,
isConversationTooBigToRing: false,
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
@ -1793,65 +1920,62 @@ describe('calling duck', () => {
videoAspectRatio: 4 / 3,
},
],
})
);
});
const call =
result.callsByConversation['fake-group-call-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
uuids: [differentCreatorUuid],
creatorUuid: differentCreatorUuid,
eraId: 'abc',
maxDevices: 5,
deviceCount: 1,
const call = result.callsByConversation['fake-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
uuids: [differentCreatorUuid],
creatorUuid: differentCreatorUuid,
eraId: 'abc',
maxDevices: 5,
deviceCount: 1,
});
});
});
it("doesn't overwrite an existing group call's ring state if it was set previously", () => {
const result = reducer(
{
...stateWithGroupCall,
callsByConversation: {
'fake-group-call-conversation-id': {
...stateWithGroupCall.callsByConversation[
'fake-group-call-conversation-id'
],
ringId: BigInt(987),
ringerUuid,
it("doesn't overwrite an existing group call's ring state if it was set previously", async () => {
const result = await getState(
{
...stateWithGroupCall,
callsByConversation: {
'fake-group-call-conversation-id': {
...stateWithGroupCall.callsByConversation[
'fake-group-call-conversation-id'
],
ringId: BigInt(987),
ringerUuid,
},
},
},
},
showCallLobby({
callMode: CallMode.Group,
conversationId: 'fake-group-call-conversation-id',
hasLocalAudio: true,
hasLocalVideo: true,
isConversationTooBigToRing: false,
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: undefined,
remoteParticipants: [
{
uuid: remoteUuid,
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
})
);
const call =
result.callsByConversation['fake-group-call-conversation-id'];
// It'd be nice to do this with an assert, but Chai doesn't understand it.
if (call?.callMode !== CallMode.Group) {
throw new Error('Expected to find a group call');
}
{
callMode: CallMode.Group,
hasLocalAudio: true,
hasLocalVideo: true,
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: undefined,
remoteParticipants: [
{
uuid: remoteUuid,
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
}
);
const call =
result.callsByConversation['fake-group-call-conversation-id'];
// It'd be nice to do this with an assert, but Chai doesn't understand it.
if (call?.callMode !== CallMode.Group) {
throw new Error('Expected to find a group call');
}
assert.strictEqual(call.ringId, BigInt(987));
assert.strictEqual(call.ringerUuid, ringerUuid);
assert.strictEqual(call.ringId, BigInt(987));
assert.strictEqual(call.ringerUuid, ringerUuid);
});
});
});

View File

@ -1,4 +1,4 @@
// Copyright 2020-2021 Signal Messenger, LLC
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable camelcase */
@ -657,7 +657,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
async onOutgoingVideoCallInConversation(): Promise<void> {
log.info('onOutgoingVideoCallInConversation: about to start a video call');
const isVideoCall = true;
if (this.model.get('announcementsOnly') && !this.model.areWeAdmin()) {
showToast(ToastCannotStartGroupCall);
@ -668,10 +667,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
log.info(
'onOutgoingVideoCallInConversation: call is deemed "safe". Making call'
);
await window.Signal.Services.calling.startCallingLobby(
this.model.id,
isVideoCall
);
window.reduxActions.calling.startCallingLobby({
conversationId: this.model.id,
isVideoCall: true,
});
log.info('onOutgoingVideoCallInConversation: started the call');
} else {
log.info(
@ -683,16 +682,14 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
async onOutgoingAudioCallInConversation(): Promise<void> {
log.info('onOutgoingAudioCallInConversation: about to start an audio call');
const isVideoCall = false;
if (await this.isCallSafe()) {
log.info(
'onOutgoingAudioCallInConversation: call is deemed "safe". Making call'
);
await window.Signal.Services.calling.startCallingLobby(
this.model.id,
isVideoCall
);
window.reduxActions.calling.startCallingLobby({
conversationId: this.model.id,
isVideoCall: false,
});
log.info('onOutgoingAudioCallInConversation: started the call');
} else {
log.info(