Centralize logic for finding/fetching the ringing call

This commit is contained in:
Scott Nonnenberg 2024-10-25 10:46:54 +10:00 committed by GitHub
parent 6888bb9cba
commit 1ce3988579
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 131 additions and 173 deletions

View file

@ -105,7 +105,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
hangUpActiveCall: action('hang-up-active-call'), hangUpActiveCall: action('hang-up-active-call'),
hasInitialLoadCompleted: true, hasInitialLoadCompleted: true,
i18n, i18n,
incomingCall: null, ringingCall: null,
callLink: storyProps.callLink ?? undefined, callLink: storyProps.callLink ?? undefined,
me: { me: {
...getDefaultConversation({ ...getDefaultConversation({
@ -148,7 +148,6 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
'toggle-screen-recording-permissions-dialog' 'toggle-screen-recording-permissions-dialog'
), ),
toggleSettings: action('toggle-settings'), toggleSettings: action('toggle-settings'),
isConversationTooBigToRing: false,
pauseVoiceNotePlayer: action('pause-audio-player'), pauseVoiceNotePlayer: action('pause-audio-player'),
}); });
@ -243,7 +242,7 @@ export function RingingDirectCall(): JSX.Element {
return ( return (
<CallManager <CallManager
{...createProps({ {...createProps({
incomingCall: { ringingCall: {
callMode: CallMode.Direct as const, callMode: CallMode.Direct as const,
conversation: getConversation(), conversation: getConversation(),
isVideoCall: true, isVideoCall: true,
@ -257,7 +256,7 @@ export function RingingGroupCall(): JSX.Element {
return ( return (
<CallManager <CallManager
{...createProps({ {...createProps({
incomingCall: { ringingCall: {
callMode: CallMode.Group as const, callMode: CallMode.Group as const,
connectionState: GroupCallConnectionState.NotConnected, connectionState: GroupCallConnectionState.NotConnected,
joinState: GroupCallJoinState.NotJoined, joinState: GroupCallJoinState.NotJoined,

View file

@ -13,14 +13,13 @@ import { CallingPip } from './CallingPip';
import { IncomingCallBar } from './IncomingCallBar'; import { IncomingCallBar } from './IncomingCallBar';
import type { import type {
ActiveCallType, ActiveCallType,
CallingConversationType,
CallViewMode, CallViewMode,
GroupCallConnectionState,
GroupCallVideoRequest, GroupCallVideoRequest,
} from '../types/Calling'; } from '../types/Calling';
import { import {
CallEndedReason, CallEndedReason,
CallState, CallState,
GroupCallConnectionState,
GroupCallJoinState, GroupCallJoinState,
} from '../types/Calling'; } from '../types/Calling';
import { CallMode } from '../types/CallDisposition'; import { CallMode } from '../types/CallDisposition';
@ -90,7 +89,7 @@ export type PropsType = {
) => VideoFrameSource; ) => VideoFrameSource;
getIsSharingPhoneNumberWithEverybody: () => boolean; getIsSharingPhoneNumberWithEverybody: () => boolean;
getPresentingSources: () => void; getPresentingSources: () => void;
incomingCall: DirectIncomingCall | GroupIncomingCall | null; ringingCall: DirectIncomingCall | GroupIncomingCall | null;
renderDeviceSelection: () => JSX.Element; renderDeviceSelection: () => JSX.Element;
renderReactionPicker: ( renderReactionPicker: (
props: React.ComponentProps<typeof SmartReactionPicker> props: React.ComponentProps<typeof SmartReactionPicker>
@ -140,7 +139,6 @@ export type PropsType = {
toggleCallLinkPendingParticipantModal: (contactId: string) => void; toggleCallLinkPendingParticipantModal: (contactId: string) => void;
toggleScreenRecordingPermissionsDialog: () => unknown; toggleScreenRecordingPermissionsDialog: () => unknown;
toggleSettings: () => void; toggleSettings: () => void;
isConversationTooBigToRing: boolean;
pauseVoiceNotePlayer: () => void; pauseVoiceNotePlayer: () => void;
} & Pick<ReactionPickerProps, 'renderEmojiPicker'>; } & Pick<ReactionPickerProps, 'renderEmojiPicker'>;
@ -153,9 +151,9 @@ type ActiveCallManagerPropsType = {
| 'bounceAppIconStop' | 'bounceAppIconStop'
| 'declineCall' | 'declineCall'
| 'hasInitialLoadCompleted' | 'hasInitialLoadCompleted'
| 'incomingCall'
| 'notifyForCall' | 'notifyForCall'
| 'playRingtone' | 'playRingtone'
| 'ringingCall'
| 'setIsCallActive' | 'setIsCallActive'
| 'stopRingtone' | 'stopRingtone'
| 'isConversationTooBigToRing' | 'isConversationTooBigToRing'
@ -544,8 +542,6 @@ export function CallManager({
hangUpActiveCall, hangUpActiveCall,
hasInitialLoadCompleted, hasInitialLoadCompleted,
i18n, i18n,
incomingCall,
isConversationTooBigToRing,
getIsSharingPhoneNumberWithEverybody, getIsSharingPhoneNumberWithEverybody,
me, me,
notifyForCall, notifyForCall,
@ -556,6 +552,7 @@ export function CallManager({
renderDeviceSelection, renderDeviceSelection,
renderEmojiPicker, renderEmojiPicker,
renderReactionPicker, renderReactionPicker,
ringingCall,
selectPresentingSource, selectPresentingSource,
sendGroupCallRaiseHand, sendGroupCallRaiseHand,
sendGroupCallReaction, sendGroupCallReaction,
@ -583,16 +580,13 @@ export function CallManager({
setIsCallActive(isCallActive); setIsCallActive(isCallActive);
}, [isCallActive, setIsCallActive]); }, [isCallActive, setIsCallActive]);
const shouldRing = getShouldRing({ // It's important not to use the ringingCall itself, because that changes
activeCall, const ringingCallId = ringingCall?.conversation.id;
incomingCall,
isConversationTooBigToRing,
hasInitialLoadCompleted,
});
useEffect(() => { useEffect(() => {
if (shouldRing) { if (hasInitialLoadCompleted && ringingCallId) {
log.info('CallManager: Playing ringtone'); log.info('CallManager: Playing ringtone');
playRingtone(); playRingtone();
return () => { return () => {
log.info('CallManager: Stopping ringtone'); log.info('CallManager: Stopping ringtone');
stopRingtone(); stopRingtone();
@ -601,7 +595,7 @@ export function CallManager({
stopRingtone(); stopRingtone();
return noop; return noop;
}, [shouldRing, playRingtone, stopRingtone]); }, [hasInitialLoadCompleted, playRingtone, ringingCallId, stopRingtone]);
const mightBeRingingOutgoingGroupCall = const mightBeRingingOutgoingGroupCall =
isGroupOrAdhocActiveCall(activeCall) && isGroupOrAdhocActiveCall(activeCall) &&
@ -680,7 +674,7 @@ export function CallManager({
} }
// In the future, we may want to show the incoming call bar when a call is active. // In the future, we may want to show the incoming call bar when a call is active.
if (incomingCall) { if (ringingCall) {
return ( return (
<IncomingCallBar <IncomingCallBar
acceptCall={acceptCall} acceptCall={acceptCall}
@ -689,107 +683,10 @@ export function CallManager({
declineCall={declineCall} declineCall={declineCall}
i18n={i18n} i18n={i18n}
notifyForCall={notifyForCall} notifyForCall={notifyForCall}
{...incomingCall} {...ringingCall}
/> />
); );
} }
return 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<GroupCallParticipantInfoType>
): boolean {
return remoteParticipants.length > 0;
}
function isLonelyGroup(conversation: CallingConversationType): boolean {
return (conversation.sortedGroupMembers?.length ?? 0) < 2;
}
function getShouldRing({
activeCall,
incomingCall,
isConversationTooBigToRing,
hasInitialLoadCompleted,
}: Readonly<
Pick<
PropsType,
| 'activeCall'
| 'incomingCall'
| 'isConversationTooBigToRing'
| 'hasInitialLoadCompleted'
>
>): boolean {
if (!hasInitialLoadCompleted) {
return false;
}
if (incomingCall != null) {
// don't ring a large group
if (isConversationTooBigToRing) {
return false;
}
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)
);
}
// Adhoc calls can't be incoming.
throw missingCaseError(incomingCall);
}
if (activeCall != null) {
switch (activeCall.callMode) {
case CallMode.Direct:
return (
activeCall.callState === CallState.Prering ||
activeCall.callState === CallState.Ringing
);
case CallMode.Group:
case CallMode.Adhoc:
return (
activeCall.outgoingRing &&
isConnected(activeCall.connectionState) &&
isJoined(activeCall.joinState) &&
!hasRemoteParticipants(activeCall.remoteParticipants) &&
!isLonelyGroup(activeCall.conversation)
);
default:
throw missingCaseError(activeCall);
}
}
return false;
}

View file

@ -3,15 +3,22 @@
// Note that this file should not important any binary addons or Node.js modules // Note that this file should not important any binary addons or Node.js modules
// because it can be imported by storybook // because it can be imported by storybook
import { CallState, GroupCallConnectionState } from '../../types/Calling'; import {
CallState,
GroupCallConnectionState,
GroupCallJoinState,
} from '../../types/Calling';
import { CallMode } from '../../types/CallDisposition'; import { CallMode } from '../../types/CallDisposition';
import type { CallingConversationType } from '../../types/Calling';
import type { AciString } from '../../types/ServiceId'; import type { AciString } from '../../types/ServiceId';
import { missingCaseError } from '../../util/missingCaseError';
import type { import type {
DirectCallStateType, DirectCallStateType,
CallsByConversationType, CallsByConversationType,
GroupCallPeekInfoType, GroupCallPeekInfoType,
GroupCallStateType, GroupCallStateType,
GroupCallParticipantInfoType,
ActiveCallStateType,
} from './calling'; } from './calling';
export const MAX_CALL_PARTICIPANTS_FOR_DEFAULT_MUTE = 8; export const MAX_CALL_PARTICIPANTS_FOR_DEFAULT_MUTE = 8;
@ -19,28 +26,53 @@ export const MAX_CALL_PARTICIPANTS_FOR_DEFAULT_MUTE = 8;
// In theory, there could be multiple incoming calls, or an incoming call while there's // 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 // an active call. In practice, the UI is not ready for this, and RingRTC doesn't
// support it for direct calls. // support it for direct calls.
export const getIncomingCall = ( // Adhoc calls can not be incoming, so we don't look for them here.
export const getRingingCall = (
callsByConversation: Readonly<CallsByConversationType>, callsByConversation: Readonly<CallsByConversationType>,
activeCallState: ActiveCallStateType | undefined,
ourAci: AciString ourAci: AciString
): undefined | DirectCallStateType | GroupCallStateType => ): DirectCallStateType | GroupCallStateType | undefined => {
Object.values(callsByConversation).find(call => { const callList = Object.values(callsByConversation);
switch (call.callMode) { const ringingDirect = callList.find(call => {
case CallMode.Direct: if (call.callMode !== CallMode.Direct) {
return call.isIncoming && call.callState === CallState.Ringing; return false;
case CallMode.Group:
return (
call.ringerAci &&
call.connectionState === GroupCallConnectionState.NotConnected &&
isAnybodyElseInGroupCall(call.peekInfo, ourAci)
);
case CallMode.Adhoc:
// Adhoc calls cannot be incoming.
return;
default:
throw missingCaseError(call);
} }
return isRinging(call.callState) && call.callEndedReason == null;
}); });
if (ringingDirect) {
return ringingDirect;
}
return callList.find(call => {
if (call.callMode !== CallMode.Group) {
return false;
}
// Outgoing - ringerAci is not set for outgoing group calls
if (
activeCallState?.state === 'Active' &&
activeCallState.outgoingRing &&
activeCallState.conversationId === call.conversationId &&
isConnected(call.connectionState) &&
isJoined(call.joinState) &&
!hasRemoteParticipants(call.remoteParticipants)
) {
return true;
}
// Incoming
return (
call.ringerAci &&
call.ringerAci !== ourAci &&
!isConnected(call.connectionState) &&
!isJoined(call.joinState) &&
isAnybodyElseInGroupCall(call.peekInfo, ourAci)
);
});
};
export const isAnybodyElseInGroupCall = ( export const isAnybodyElseInGroupCall = (
peekInfo: undefined | Readonly<Pick<GroupCallPeekInfoType, 'acis'>>, peekInfo: undefined | Readonly<Pick<GroupCallPeekInfoType, 'acis'>>,
ourAci: AciString ourAci: AciString
@ -60,3 +92,28 @@ export const isGroupCallActiveOnServer = (
): boolean => { ): boolean => {
return Boolean(peekInfo?.eraId); return Boolean(peekInfo?.eraId);
}; };
export function isLonelyGroup(conversation: CallingConversationType): boolean {
return (conversation.sortedGroupMembers?.length ?? 0) < 2;
}
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<GroupCallParticipantInfoType>
): boolean {
return remoteParticipants.length > 0;
}

View file

@ -13,7 +13,7 @@ import type {
GroupCallStateType, GroupCallStateType,
ActiveCallStateType, ActiveCallStateType,
} from '../ducks/calling'; } from '../ducks/calling';
import { getIncomingCall as getIncomingCallHelper } from '../ducks/callingHelpers'; import { getRingingCall as getRingingCallHelper } from '../ducks/callingHelpers';
import type { PresentedSource } from '../../types/Calling'; import type { PresentedSource } from '../../types/Calling';
import { CallMode } from '../../types/CallDisposition'; import { CallMode } from '../../types/CallDisposition';
import { isCallLinkAdmin, type CallLinkType } from '../../types/CallLink'; import { isCallLinkAdmin, type CallLinkType } from '../../types/CallLink';
@ -152,25 +152,27 @@ export const isInFullScreenCall = createSelector(
Boolean(activeCallState && !activeCallState.pip) Boolean(activeCallState && !activeCallState.pip)
); );
export const getIncomingCall = createSelector( export const getRingingCall = createSelector(
getCallsByConversation, getCallsByConversation,
getActiveCallState,
getUserACI, getUserACI,
( (
callsByConversation: CallsByConversationType, callsByConversation: CallsByConversationType,
activeCallState: ActiveCallStateType | undefined,
ourAci: AciString | undefined ourAci: AciString | undefined
): undefined | DirectCallStateType | GroupCallStateType => { ): undefined | DirectCallStateType | GroupCallStateType => {
if (!ourAci) { if (!ourAci) {
return undefined; return undefined;
} }
return getIncomingCallHelper(callsByConversation, ourAci); return getRingingCallHelper(callsByConversation, activeCallState, ourAci);
} }
); );
export const areAnyCallsActiveOrRinging = createSelector( export const areAnyCallsActiveOrRinging = createSelector(
getActiveCall, getActiveCall,
getIncomingCall, getRingingCall,
(activeCall, incomingCall): boolean => Boolean(activeCall || incomingCall) (activeCall, ringingCall): boolean => Boolean(activeCall || ringingCall)
); );
export const getPresentingSource = createSelector( export const getPresentingSource = createSelector(

View file

@ -48,15 +48,16 @@ import {
getActiveCallState, getActiveCallState,
getAvailableCameras, getAvailableCameras,
getCallLinkSelector, getCallLinkSelector,
getIncomingCall, getRingingCall,
} from '../selectors/calling'; } from '../selectors/calling';
import { getConversationSelector, getMe } from '../selectors/conversations'; import { getConversationSelector, getMe } from '../selectors/conversations';
import { getIntl } from '../selectors/user'; import { getIntl, getUserACI } from '../selectors/user';
import { SmartCallingDeviceSelection } from './CallingDeviceSelection'; import { SmartCallingDeviceSelection } from './CallingDeviceSelection';
import { renderEmojiPicker } from './renderEmojiPicker'; import { renderEmojiPicker } from './renderEmojiPicker';
import { renderReactionPicker } from './renderReactionPicker'; import { renderReactionPicker } from './renderReactionPicker';
import { isSharingPhoneNumberWithEverybody as getIsSharingPhoneNumberWithEverybody } from '../../util/phoneNumberSharingMode'; import { isSharingPhoneNumberWithEverybody as getIsSharingPhoneNumberWithEverybody } from '../../util/phoneNumberSharingMode';
import { useGlobalModalActions } from '../ducks/globalModals'; import { useGlobalModalActions } from '../ducks/globalModals';
import { isLonelyGroup } from '../ducks/callingHelpers';
function renderDeviceSelection(): JSX.Element { function renderDeviceSelection(): JSX.Element {
return <SmartCallingDeviceSelection />; return <SmartCallingDeviceSelection />;
@ -364,56 +365,62 @@ const mapStateToCallLinkProp = (state: StateType): CallLinkType | undefined => {
return callLink; return callLink;
}; };
const mapStateToIncomingCallProp = ( const mapStateToRingingCallProp = (
state: StateType state: StateType
): DirectIncomingCall | GroupIncomingCall | null => { ): DirectIncomingCall | GroupIncomingCall | null => {
const call = getIncomingCall(state); const ourAci = getUserACI(state);
if (!call) { const ringingCall = getRingingCall(state);
if (!ringingCall) {
return null; return null;
} }
const conversation = getConversationSelector(state)(call.conversationId); const conversation = getConversationSelector(state)(
ringingCall.conversationId
);
if (!conversation) { if (!conversation) {
log.error('The incoming call has no corresponding conversation'); log.error('The incoming call has no corresponding conversation');
return null; return null;
} }
switch (call.callMode) { switch (ringingCall.callMode) {
case CallMode.Direct: case CallMode.Direct:
return { return {
callMode: CallMode.Direct as const, callMode: CallMode.Direct as const,
callState: call.callState, callState: ringingCall.callState,
callEndedReason: call.callEndedReason, callEndedReason: ringingCall.callEndedReason,
conversation, conversation,
isVideoCall: call.isVideoCall, isVideoCall: ringingCall.isVideoCall,
}; };
case CallMode.Group: { case CallMode.Group: {
if (!call.ringerAci) { if (getIsConversationTooBigToRing(conversation)) {
log.error('The incoming group call has no ring state'); return null;
}
if (isLonelyGroup(conversation)) {
return null; return null;
} }
const conversationSelector = getConversationSelector(state); const conversationSelector = getConversationSelector(state);
const ringer = conversationSelector(call.ringerAci); const ringer = conversationSelector(ringingCall.ringerAci || ourAci);
const otherMembersRung = (conversation.sortedGroupMembers ?? []).filter( const otherMembersRung = (conversation.sortedGroupMembers ?? []).filter(
c => c.id !== ringer.id && !c.isMe c => c.id !== ringer.id && !c.isMe
); );
return { return {
callMode: CallMode.Group as const, callMode: CallMode.Group as const,
connectionState: call.connectionState, connectionState: ringingCall.connectionState,
joinState: call.joinState, joinState: ringingCall.joinState,
conversation, conversation,
otherMembersRung, otherMembersRung,
ringer, ringer,
remoteParticipants: call.remoteParticipants, remoteParticipants: ringingCall.remoteParticipants,
}; };
} }
case CallMode.Adhoc: case CallMode.Adhoc:
log.error('Cannot handle an incoming adhoc call'); log.error('Cannot handle an incoming adhoc call');
return null; return null;
default: default:
throw missingCaseError(call); throw missingCaseError(ringingCall);
} }
}; };
@ -421,13 +428,10 @@ export const SmartCallManager = memo(function SmartCallManager() {
const i18n = useSelector(getIntl); const i18n = useSelector(getIntl);
const activeCall = useSelector(mapStateToActiveCallProp); const activeCall = useSelector(mapStateToActiveCallProp);
const callLink = useSelector(mapStateToCallLinkProp); const callLink = useSelector(mapStateToCallLinkProp);
const incomingCall = useSelector(mapStateToIncomingCallProp); const ringingCall = useSelector(mapStateToRingingCallProp);
const availableCameras = useSelector(getAvailableCameras); const availableCameras = useSelector(getAvailableCameras);
const hasInitialLoadCompleted = useSelector(getHasInitialLoadCompleted); const hasInitialLoadCompleted = useSelector(getHasInitialLoadCompleted);
const me = useSelector(getMe); const me = useSelector(getMe);
const isConversationTooBigToRing = incomingCall
? getIsConversationTooBigToRing(incomingCall.conversation)
: false;
const { const {
approveUser, approveUser,
@ -493,8 +497,6 @@ export const SmartCallManager = memo(function SmartCallManager() {
hangUpActiveCall={hangUpActiveCall} hangUpActiveCall={hangUpActiveCall}
hasInitialLoadCompleted={hasInitialLoadCompleted} hasInitialLoadCompleted={hasInitialLoadCompleted}
i18n={i18n} i18n={i18n}
incomingCall={incomingCall}
isConversationTooBigToRing={isConversationTooBigToRing}
me={me} me={me}
notifyForCall={notifyForCall} notifyForCall={notifyForCall}
openSystemPreferencesAction={openSystemPreferencesAction} openSystemPreferencesAction={openSystemPreferencesAction}
@ -504,6 +506,7 @@ export const SmartCallManager = memo(function SmartCallManager() {
renderDeviceSelection={renderDeviceSelection} renderDeviceSelection={renderDeviceSelection}
renderEmojiPicker={renderEmojiPicker} renderEmojiPicker={renderEmojiPicker}
renderReactionPicker={renderReactionPicker} renderReactionPicker={renderReactionPicker}
ringingCall={ringingCall}
sendGroupCallRaiseHand={sendGroupCallRaiseHand} sendGroupCallRaiseHand={sendGroupCallRaiseHand}
sendGroupCallReaction={sendGroupCallReaction} sendGroupCallReaction={sendGroupCallReaction}
selectPresentingSource={selectPresentingSource} selectPresentingSource={selectPresentingSource}

View file

@ -17,7 +17,7 @@ import {
getCallsByConversation, getCallsByConversation,
getCallSelector, getCallSelector,
getHasAnyAdminCallLinks, getHasAnyAdminCallLinks,
getIncomingCall, getRingingCall,
isInCall, isInCall,
} from '../../../state/selectors/calling'; } from '../../../state/selectors/calling';
import type { import type {
@ -181,15 +181,15 @@ describe('state/selectors/calling', () => {
}); });
}); });
describe('getIncomingCall', () => { describe('getRingingCall', () => {
it('returns undefined if there are no calls', () => { it('returns undefined if there are no calls', () => {
assert.isUndefined(getIncomingCall(getEmptyRootState())); assert.isUndefined(getRingingCall(getEmptyRootState()));
}); });
it('returns undefined if there is no incoming call', () => { it('returns undefined if there is no incoming call', () => {
assert.isUndefined(getIncomingCall(getCallingState(stateWithDirectCall))); assert.isUndefined(getRingingCall(getCallingState(stateWithDirectCall)));
assert.isUndefined( assert.isUndefined(
getIncomingCall(getCallingState(stateWithActiveDirectCall)) getRingingCall(getCallingState(stateWithActiveDirectCall))
); );
}); });
@ -209,19 +209,19 @@ describe('state/selectors/calling', () => {
}, },
}; };
assert.isUndefined(getIncomingCall(getCallingState(state))); assert.isUndefined(getRingingCall(getCallingState(state)));
}); });
it('returns an incoming direct call', () => { it('returns an incoming direct call', () => {
assert.deepEqual( assert.deepEqual(
getIncomingCall(getCallingState(stateWithIncomingDirectCall)), getRingingCall(getCallingState(stateWithIncomingDirectCall)),
incomingDirectCall incomingDirectCall
); );
}); });
it('returns an incoming group call', () => { it('returns an incoming group call', () => {
assert.deepEqual( assert.deepEqual(
getIncomingCall(getCallingState(stateWithIncomingGroupCall)), getRingingCall(getCallingState(stateWithIncomingGroupCall)),
incomingGroupCall incomingGroupCall
); );
}); });