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

View file

@ -13,14 +13,13 @@ import { CallingPip } from './CallingPip';
import { IncomingCallBar } from './IncomingCallBar';
import type {
ActiveCallType,
CallingConversationType,
CallViewMode,
GroupCallConnectionState,
GroupCallVideoRequest,
} from '../types/Calling';
import {
CallEndedReason,
CallState,
GroupCallConnectionState,
GroupCallJoinState,
} from '../types/Calling';
import { CallMode } from '../types/CallDisposition';
@ -90,7 +89,7 @@ export type PropsType = {
) => VideoFrameSource;
getIsSharingPhoneNumberWithEverybody: () => boolean;
getPresentingSources: () => void;
incomingCall: DirectIncomingCall | GroupIncomingCall | null;
ringingCall: DirectIncomingCall | GroupIncomingCall | null;
renderDeviceSelection: () => JSX.Element;
renderReactionPicker: (
props: React.ComponentProps<typeof SmartReactionPicker>
@ -140,7 +139,6 @@ export type PropsType = {
toggleCallLinkPendingParticipantModal: (contactId: string) => void;
toggleScreenRecordingPermissionsDialog: () => unknown;
toggleSettings: () => void;
isConversationTooBigToRing: boolean;
pauseVoiceNotePlayer: () => void;
} & Pick<ReactionPickerProps, 'renderEmojiPicker'>;
@ -153,9 +151,9 @@ type ActiveCallManagerPropsType = {
| 'bounceAppIconStop'
| 'declineCall'
| 'hasInitialLoadCompleted'
| 'incomingCall'
| 'notifyForCall'
| 'playRingtone'
| 'ringingCall'
| 'setIsCallActive'
| 'stopRingtone'
| 'isConversationTooBigToRing'
@ -544,8 +542,6 @@ export function CallManager({
hangUpActiveCall,
hasInitialLoadCompleted,
i18n,
incomingCall,
isConversationTooBigToRing,
getIsSharingPhoneNumberWithEverybody,
me,
notifyForCall,
@ -556,6 +552,7 @@ export function CallManager({
renderDeviceSelection,
renderEmojiPicker,
renderReactionPicker,
ringingCall,
selectPresentingSource,
sendGroupCallRaiseHand,
sendGroupCallReaction,
@ -583,16 +580,13 @@ export function CallManager({
setIsCallActive(isCallActive);
}, [isCallActive, setIsCallActive]);
const shouldRing = getShouldRing({
activeCall,
incomingCall,
isConversationTooBigToRing,
hasInitialLoadCompleted,
});
// It's important not to use the ringingCall itself, because that changes
const ringingCallId = ringingCall?.conversation.id;
useEffect(() => {
if (shouldRing) {
if (hasInitialLoadCompleted && ringingCallId) {
log.info('CallManager: Playing ringtone');
playRingtone();
return () => {
log.info('CallManager: Stopping ringtone');
stopRingtone();
@ -601,7 +595,7 @@ export function CallManager({
stopRingtone();
return noop;
}, [shouldRing, playRingtone, stopRingtone]);
}, [hasInitialLoadCompleted, playRingtone, ringingCallId, stopRingtone]);
const mightBeRingingOutgoingGroupCall =
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.
if (incomingCall) {
if (ringingCall) {
return (
<IncomingCallBar
acceptCall={acceptCall}
@ -689,107 +683,10 @@ export function CallManager({
declineCall={declineCall}
i18n={i18n}
notifyForCall={notifyForCall}
{...incomingCall}
{...ringingCall}
/>
);
}
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
// 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 type { CallingConversationType } from '../../types/Calling';
import type { AciString } from '../../types/ServiceId';
import { missingCaseError } from '../../util/missingCaseError';
import type {
DirectCallStateType,
CallsByConversationType,
GroupCallPeekInfoType,
GroupCallStateType,
GroupCallParticipantInfoType,
ActiveCallStateType,
} from './calling';
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
// an active call. In practice, the UI is not ready for this, and RingRTC doesn't
// 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>,
activeCallState: ActiveCallStateType | undefined,
ourAci: AciString
): undefined | DirectCallStateType | GroupCallStateType =>
Object.values(callsByConversation).find(call => {
switch (call.callMode) {
case CallMode.Direct:
return call.isIncoming && call.callState === CallState.Ringing;
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);
): DirectCallStateType | GroupCallStateType | undefined => {
const callList = Object.values(callsByConversation);
const ringingDirect = callList.find(call => {
if (call.callMode !== CallMode.Direct) {
return false;
}
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 = (
peekInfo: undefined | Readonly<Pick<GroupCallPeekInfoType, 'acis'>>,
ourAci: AciString
@ -60,3 +92,28 @@ export const isGroupCallActiveOnServer = (
): boolean => {
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,
ActiveCallStateType,
} from '../ducks/calling';
import { getIncomingCall as getIncomingCallHelper } from '../ducks/callingHelpers';
import { getRingingCall as getRingingCallHelper } from '../ducks/callingHelpers';
import type { PresentedSource } from '../../types/Calling';
import { CallMode } from '../../types/CallDisposition';
import { isCallLinkAdmin, type CallLinkType } from '../../types/CallLink';
@ -152,25 +152,27 @@ export const isInFullScreenCall = createSelector(
Boolean(activeCallState && !activeCallState.pip)
);
export const getIncomingCall = createSelector(
export const getRingingCall = createSelector(
getCallsByConversation,
getActiveCallState,
getUserACI,
(
callsByConversation: CallsByConversationType,
activeCallState: ActiveCallStateType | undefined,
ourAci: AciString | undefined
): undefined | DirectCallStateType | GroupCallStateType => {
if (!ourAci) {
return undefined;
}
return getIncomingCallHelper(callsByConversation, ourAci);
return getRingingCallHelper(callsByConversation, activeCallState, ourAci);
}
);
export const areAnyCallsActiveOrRinging = createSelector(
getActiveCall,
getIncomingCall,
(activeCall, incomingCall): boolean => Boolean(activeCall || incomingCall)
getRingingCall,
(activeCall, ringingCall): boolean => Boolean(activeCall || ringingCall)
);
export const getPresentingSource = createSelector(

View file

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

View file

@ -17,7 +17,7 @@ import {
getCallsByConversation,
getCallSelector,
getHasAnyAdminCallLinks,
getIncomingCall,
getRingingCall,
isInCall,
} from '../../../state/selectors/calling';
import type {
@ -181,15 +181,15 @@ describe('state/selectors/calling', () => {
});
});
describe('getIncomingCall', () => {
describe('getRingingCall', () => {
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', () => {
assert.isUndefined(getIncomingCall(getCallingState(stateWithDirectCall)));
assert.isUndefined(getRingingCall(getCallingState(stateWithDirectCall)));
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', () => {
assert.deepEqual(
getIncomingCall(getCallingState(stateWithIncomingDirectCall)),
getRingingCall(getCallingState(stateWithIncomingDirectCall)),
incomingDirectCall
);
});
it('returns an incoming group call', () => {
assert.deepEqual(
getIncomingCall(getCallingState(stateWithIncomingGroupCall)),
getRingingCall(getCallingState(stateWithIncomingGroupCall)),
incomingGroupCall
);
});