diff --git a/ts/state/ducks/calling.ts b/ts/state/ducks/calling.ts index 4c557c333c36..f127404915e9 100644 --- a/ts/state/ducks/calling.ts +++ b/ts/state/ducks/calling.ts @@ -76,7 +76,11 @@ import { ToastType } from '../../types/Toast'; import type { ShowToastActionType } from './toast'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; import { useBoundActions } from '../../hooks/useBoundActions'; -import { isAnybodyElseInGroupCall } from './callingHelpers'; +import { + isAnybodyElseInGroupCall, + isAnybodyInGroupCall, + MAX_CALL_PARTICIPANTS_FOR_DEFAULT_MUTE, +} from './callingHelpers'; import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog'; import { isGroupOrAdhocCallMode, @@ -2215,7 +2219,8 @@ const _startCallLinkLobby = async ({ const callLobbyData = await calling.startCallLinkLobby({ callLinkRootKey, adminPasskey, - hasLocalAudio: groupCallDeviceCount < 8, + hasLocalAudio: + groupCallDeviceCount < MAX_CALL_PARTICIPANTS_FOR_DEFAULT_MUTE, }); if (!callLobbyData) { return; @@ -2314,7 +2319,8 @@ function startCallingLobby({ const callLobbyData = await calling.startCallingLobby({ conversation, - hasLocalAudio: groupCallDeviceCount < 8, + hasLocalAudio: + groupCallDeviceCount < MAX_CALL_PARTICIPANTS_FOR_DEFAULT_MUTE, hasLocalVideo: isVideoCall, }); if (!callLobbyData) { @@ -3114,6 +3120,17 @@ export function reducer( hasLocalAudio, hasLocalVideo, }; + + // The first time we detect call participants in the lobby, check participant count + // and mute ourselves if over the threshold. + if ( + joinState === GroupCallJoinState.NotJoined && + !isAnybodyInGroupCall(existingCall?.peekInfo) && + newPeekInfo.deviceCount >= MAX_CALL_PARTICIPANTS_FOR_DEFAULT_MUTE && + newActiveCallState?.hasLocalAudio + ) { + newActiveCallState.hasLocalAudio = false; + } } else { newActiveCallState = state.activeCallState; } diff --git a/ts/state/ducks/callingHelpers.ts b/ts/state/ducks/callingHelpers.ts index 1fbc224b58d6..444e80406139 100644 --- a/ts/state/ducks/callingHelpers.ts +++ b/ts/state/ducks/callingHelpers.ts @@ -14,6 +14,8 @@ import type { GroupCallStateType, } from './calling'; +export const MAX_CALL_PARTICIPANTS_FOR_DEFAULT_MUTE = 8; + // In theory, there could be multiple incoming calls, or an incoming call while there's // an active call. In practice, the UI is not ready for this, and RingRTC doesn't // support it for direct calls. diff --git a/ts/test-electron/state/ducks/calling_test.ts b/ts/test-electron/state/ducks/calling_test.ts index 488f0f29d5b4..1749e41abfd2 100644 --- a/ts/test-electron/state/ducks/calling_test.ts +++ b/ts/test-electron/state/ducks/calling_test.ts @@ -137,6 +137,28 @@ describe('calling duck', () => { }, }; + const stateWithNotJoinedGroupCall: CallingStateType = { + ...getEmptyState(), + callsByConversation: { + 'fake-group-call-conversation-id': { + callMode: CallMode.Group, + conversationId: 'fake-group-call-conversation-id', + connectionState: GroupCallConnectionState.NotConnected, + joinState: GroupCallJoinState.NotJoined, + localDemuxId: 1, + peekInfo: { + acis: [], + pendingAcis: [], + creatorAci, + eraId: 'xyz', + maxDevices: 16, + deviceCount: 0, + }, + remoteParticipants: [], + } satisfies GroupCallStateType, + }, + }; + const stateWithIncomingGroupCall: CallingStateType = { ...stateWithGroupCall, callsByConversation: { @@ -151,21 +173,23 @@ describe('calling duck', () => { }, }; + const groupCallActiveCallState: ActiveCallStateType = { + callMode: CallMode.Group, + conversationId: 'fake-group-call-conversation-id', + hasLocalAudio: true, + hasLocalVideo: false, + localAudioLevel: 0, + viewMode: CallViewMode.Paginated, + showParticipantsList: false, + outgoingRing: false, + pip: false, + settingsDialogOpen: false, + joinedAt: null, + }; + const stateWithActiveGroupCall: CallingStateTypeWithActiveCall = { ...stateWithGroupCall, - activeCallState: { - callMode: CallMode.Group, - conversationId: 'fake-group-call-conversation-id', - hasLocalAudio: true, - hasLocalVideo: false, - localAudioLevel: 0, - viewMode: CallViewMode.Paginated, - showParticipantsList: false, - outgoingRing: false, - pip: false, - settingsDialogOpen: false, - joinedAt: null, - }, + activeCallState: groupCallActiveCallState, }; const ourAci = generateAci(); @@ -1319,6 +1343,84 @@ describe('calling duck', () => { assert.isFalse(result.activeCallState?.outgoingRing); }); + + it('mutes self in lobby when getting peek info with a lot of devices', () => { + const result = reducer( + { + ...stateWithNotJoinedGroupCall, + activeCallState: groupCallActiveCallState, + }, + getAction({ + callMode: CallMode.Group, + conversationId: 'fake-group-call-conversation-id', + connectionState: GroupCallConnectionState.Connecting, + joinState: GroupCallJoinState.NotJoined, + localDemuxId: 1, + hasLocalAudio: true, + hasLocalVideo: true, + peekInfo: { + acis: Array(20).map(generateAci), + pendingAcis: [], + maxDevices: 16, + deviceCount: 20, + }, + remoteParticipants: [], + }) + ); + + assert.isFalse(result.activeCallState?.hasLocalAudio); + }); + + it('does not mute self when getting peek info with few devices', () => { + const result = reducer( + { + ...stateWithNotJoinedGroupCall, + activeCallState: groupCallActiveCallState, + }, + getAction({ + callMode: CallMode.Group, + conversationId: 'fake-group-call-conversation-id', + connectionState: GroupCallConnectionState.Connecting, + joinState: GroupCallJoinState.NotJoined, + localDemuxId: 1, + hasLocalAudio: true, + hasLocalVideo: true, + peekInfo: { + acis: [ACI_1], + pendingAcis: [], + maxDevices: 16, + deviceCount: 1, + }, + remoteParticipants: [], + }) + ); + + assert.isTrue(result.activeCallState?.hasLocalAudio); + }); + + it('does not mute self when connected with many devices', () => { + const result = reducer( + stateWithActiveGroupCall, + getAction({ + callMode: CallMode.Group, + conversationId: 'fake-group-call-conversation-id', + connectionState: GroupCallConnectionState.Connected, + joinState: GroupCallJoinState.Joined, + localDemuxId: 1, + hasLocalAudio: true, + hasLocalVideo: true, + peekInfo: { + acis: Array(20).map(generateAci), + pendingAcis: [], + maxDevices: 16, + deviceCount: 20, + }, + remoteParticipants: [], + }) + ); + + assert.isTrue(result.activeCallState?.hasLocalAudio); + }); }); describe('handleCallLinkUpdate', () => {