From 422ebf1bc85c8ea8a79cd266a9272acb4e7f38a3 Mon Sep 17 00:00:00 2001 From: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com> Date: Thu, 4 Jan 2024 14:16:33 -0800 Subject: [PATCH] Add additional checks/logs to ringtones --- ts/components/CallManager.stories.tsx | 4 + ts/components/CallManager.tsx | 111 +++++++++++++++++++------- ts/state/smart/CallManager.tsx | 19 ++++- 3 files changed, 102 insertions(+), 32 deletions(-) diff --git a/ts/components/CallManager.stories.tsx b/ts/components/CallManager.stories.tsx index 31ec3c3728..b00cf064b8 100644 --- a/ts/components/CallManager.stories.tsx +++ b/ts/components/CallManager.stories.tsx @@ -73,6 +73,7 @@ const createProps = (storyProps: Partial = {}): PropsType => ({ getPresentingSources: action('get-presenting-sources'), hangUpActiveCall: action('hang-up-active-call'), i18n, + incomingCall: null, isGroupCallRaiseHandEnabled: true, isGroupCallReactionsEnabled: true, keyChangeOk: action('key-change-ok'), @@ -189,6 +190,8 @@ export function RingingGroupCall(): JSX.Element { {...createProps({ incomingCall: { callMode: CallMode.Group as const, + connectionState: GroupCallConnectionState.NotConnected, + joinState: GroupCallJoinState.NotJoined, conversation: { ...getConversation(), type: 'group', @@ -201,6 +204,7 @@ export function RingingGroupCall(): JSX.Element { { firstName: 'Summer', title: 'Summer Smith' }, ], ringer: { firstName: 'Rick', title: 'Rick Sanchez' }, + remoteParticipants: [], }, })} /> diff --git a/ts/components/CallManager.tsx b/ts/components/CallManager.tsx index ec038444c1..b2e455a40d 100644 --- a/ts/components/CallManager.tsx +++ b/ts/components/CallManager.tsx @@ -32,6 +32,7 @@ import type { AcceptCallType, CancelCallType, DeclineCallType, + GroupCallParticipantInfoType, KeyChangeOkType, SendGroupCallRaiseHandType, SendGroupCallReactionType, @@ -47,9 +48,28 @@ import { missingCaseError } from '../util/missingCaseError'; import { CallingToastProvider } from './CallingToast'; import type { SmartReactionPicker } from '../state/smart/ReactionPicker'; import type { Props as ReactionPickerProps } from './conversation/ReactionPicker'; +import * as log from '../logging/log'; const GROUP_CALL_RING_DURATION = 60 * 1000; +export type DirectIncomingCall = Readonly<{ + callMode: CallMode.Direct; + callState?: CallState; + callEndedReason?: CallEndedReason; + conversation: ConversationType; + isVideoCall: boolean; +}>; + +export type GroupIncomingCall = Readonly<{ + callMode: CallMode.Group; + connectionState: GroupCallConnectionState; + joinState: GroupCallJoinState; + conversation: ConversationType; + otherMembersRung: Array>; + ringer: Pick; + remoteParticipants: Array; +}>; + export type PropsType = { activeCall?: ActiveCallType; availableCameras: Array; @@ -62,18 +82,7 @@ export type PropsType = { ) => VideoFrameSource; getPreferredBadge: PreferredBadgeSelectorType; getPresentingSources: () => void; - incomingCall?: - | { - callMode: CallMode.Direct; - conversation: ConversationType; - isVideoCall: boolean; - } - | { - callMode: CallMode.Group; - conversation: ConversationType; - otherMembersRung: Array>; - ringer: Pick; - }; + incomingCall: DirectIncomingCall | GroupIncomingCall | null; keyChangeOk: (_: KeyChangeOkType) => void; renderDeviceSelection: () => JSX.Element; renderReactionPicker: ( @@ -431,8 +440,10 @@ export function CallManager(props: PropsType): JSX.Element | null { const shouldRing = getShouldRing(props); useEffect(() => { if (shouldRing) { + log.info('CallManager: Playing ringtone'); playRingtone(); return () => { + log.info('CallManager: Stopping ringtone'); stopRingtone(); }; } @@ -486,6 +497,31 @@ export function CallManager(props: PropsType): JSX.Element | null { return null; } +function isRinging(callState: CallState | undefined): boolean { + return callState === CallState.Prering || callState === CallState.Ringing; +} + +function isConnected(connectionState: GroupCallConnectionState): boolean { + return ( + connectionState === GroupCallConnectionState.Connecting || + connectionState === GroupCallConnectionState.Connected + ); +} + +function isJoined(joinState: GroupCallJoinState): boolean { + return joinState !== GroupCallJoinState.NotJoined; +} + +function hasRemoteParticipants( + remoteParticipants: Array +): boolean { + return remoteParticipants.length > 0; +} + +function isLonelyGroup(conversation: ConversationType): boolean { + return (conversation.sortedGroupMembers?.length ?? 0) < 2; +} + function getShouldRing({ activeCall, incomingCall, @@ -493,35 +529,54 @@ function getShouldRing({ }: Readonly< Pick >): boolean { - if (incomingCall) { + if (incomingCall != null) { // don't ring a large group if (isConversationTooBigToRing) { return false; } - return !activeCall; + if (activeCall != null) { + return false; + } + + if (incomingCall.callMode === CallMode.Direct) { + return ( + isRinging(incomingCall.callState) && + incomingCall.callEndedReason == null + ); + } + + if (incomingCall.callMode === CallMode.Group) { + return ( + !isConnected(incomingCall.connectionState) && + !isJoined(incomingCall.joinState) && + !isLonelyGroup(incomingCall.conversation) + ); + } + + throw missingCaseError(incomingCall); } - if (!activeCall) { - return false; - } - - switch (activeCall.callMode) { - case CallMode.Direct: + if (activeCall != null) { + if (activeCall.callMode === CallMode.Direct) { return ( activeCall.callState === CallState.Prering || activeCall.callState === CallState.Ringing ); - case CallMode.Group: + } + + if (activeCall.callMode === 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 + isConnected(activeCall.connectionState) && + isJoined(activeCall.joinState) && + !hasRemoteParticipants(activeCall.remoteParticipants) && + !isLonelyGroup(activeCall.conversation) ); - default: - throw missingCaseError(activeCall); + } + + throw missingCaseError(activeCall); } + + return false; } diff --git a/ts/state/smart/CallManager.tsx b/ts/state/smart/CallManager.tsx index f0f8684748..4c55d14953 100644 --- a/ts/state/smart/CallManager.tsx +++ b/ts/state/smart/CallManager.tsx @@ -5,6 +5,10 @@ import React from 'react'; import { connect } from 'react-redux'; import { memoize } from 'lodash'; import { mapDispatchToProps } from '../actions'; +import type { + DirectIncomingCall, + GroupIncomingCall, +} from '../../components/CallManager'; import { CallManager } from '../../components/CallManager'; import { calling as callingService } from '../../services/calling'; import { getIntl, getTheme } from '../selectors/user'; @@ -320,29 +324,33 @@ const mapStateToActiveCallProp = ( } }; -const mapStateToIncomingCallProp = (state: StateType) => { +const mapStateToIncomingCallProp = ( + state: StateType +): DirectIncomingCall | GroupIncomingCall | null => { const call = getIncomingCall(state); if (!call) { - return undefined; + return null; } const conversation = getConversationSelector(state)(call.conversationId); if (!conversation) { log.error('The incoming call has no corresponding conversation'); - return undefined; + return null; } switch (call.callMode) { case CallMode.Direct: return { callMode: CallMode.Direct as const, + callState: call.callState, + callEndedReason: call.callEndedReason, conversation, isVideoCall: call.isVideoCall, }; case CallMode.Group: { if (!call.ringerAci) { log.error('The incoming group call has no ring state'); - return undefined; + return null; } const conversationSelector = getConversationSelector(state); @@ -353,9 +361,12 @@ const mapStateToIncomingCallProp = (state: StateType) => { return { callMode: CallMode.Group as const, + connectionState: call.connectionState, + joinState: call.joinState, conversation, otherMembersRung, ringer, + remoteParticipants: call.remoteParticipants, }; } default: