diff --git a/ts/services/calling.ts b/ts/services/calling.ts index 3a21fd0fb..81324aa90 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -41,6 +41,7 @@ import { } from '../state/ducks/calling'; import { getConversationCallMode } from '../state/ducks/conversations'; import { isConversationTooBigToRing } from '../conversations/isConversationTooBigToRing'; +import { isMe } from '../util/whatTypeOfConversation'; import { AudioDevice, AvailableIODevicesType, @@ -79,7 +80,11 @@ import { callingMessageToProto } from '../util/callingMessageToProto'; import { getSendOptions } from '../util/getSendOptions'; import { SignalService as Proto } from '../protobuf'; import dataInterface from '../sql/Client'; -import { notificationService } from './notifications'; +import { + notificationService, + NotificationSetting, + FALLBACK_NOTIFICATION_TITLE, +} from './notifications'; import * as log from '../logging/log'; const { @@ -2015,6 +2020,58 @@ export class CallingClass { conversation.updateCallHistoryForGroupCall(peekInfo.eraId, creatorUuid); } + public notifyForGroupCall( + conversationId: string, + creatorBytes: undefined | Readonly + ): void { + const creatorUuid = creatorBytes + ? arrayBufferToUuid(typedArrayToArrayBuffer(creatorBytes)) + : undefined; + const creatorConversation = window.ConversationController.get(creatorUuid); + if (creatorConversation && isMe(creatorConversation.attributes)) { + return; + } + + let notificationTitle: string; + let notificationMessage: string; + + switch (notificationService.getNotificationSetting()) { + case NotificationSetting.Off: + return; + case NotificationSetting.NoNameOrMessage: + notificationTitle = FALLBACK_NOTIFICATION_TITLE; + notificationMessage = window.i18n( + 'calling__call-notification__started-by-someone' + ); + break; + default: { + const conversation = window.ConversationController.get(conversationId); + // These fallbacks exist just in case something unexpected goes wrong. + notificationTitle = + conversation?.getTitle() || FALLBACK_NOTIFICATION_TITLE; + notificationMessage = creatorConversation + ? window.i18n('calling__call-notification__started', [ + creatorConversation.getTitle(), + ]) + : window.i18n('calling__call-notification__started-by-someone'); + break; + } + } + + notificationService.notify({ + icon: 'images/icons/v2/video-solid-24.svg', + message: notificationMessage, + onNotificationClick: () => { + this.uxActions?.startCallingLobby({ + conversationId, + isVideoCall: true, + }); + }, + silent: false, + title: notificationTitle, + }); + } + private async cleanExpiredGroupCallRingsAndLoop(): Promise { try { await cleanExpiredGroupCallRings(); diff --git a/ts/state/ducks/calling.ts b/ts/state/ducks/calling.ts index 3cbfa443c..60fcbca5e 100644 --- a/ts/state/ducks/calling.ts +++ b/ts/state/ducks/calling.ts @@ -254,6 +254,28 @@ export const getActiveCall = ({ activeCallState && getOwn(callsByConversation, activeCallState.conversationId); +// 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 = ( + callsByConversation: Readonly, + ourUuid: string +): 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.ringerUuid && + call.connectionState === GroupCallConnectionState.NotConnected && + isAnybodyElseInGroupCall(call.peekInfo, ourUuid) + ); + default: + throw missingCaseError(call); + } + }); + export const isAnybodyElseInGroupCall = ( { uuids }: Readonly, ourUuid: string @@ -837,6 +859,7 @@ function peekNotConnectedGroupCall( queue.add(async () => { const state = getState(); + const { ourUuid } = state.user; // We make sure we're not trying to peek at a connected (or connecting, or // reconnecting) call. Because this is asynchronous, it's possible that the call @@ -872,14 +895,34 @@ function peekNotConnectedGroupCall( calling.updateCallHistoryForGroupCall(conversationId, peekInfo); + const formattedPeekInfo = calling.formatGroupCallPeekInfoForRedux( + peekInfo + ); + dispatch({ type: PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED, payload: { conversationId, - peekInfo: calling.formatGroupCallPeekInfoForRedux(peekInfo), + peekInfo: formattedPeekInfo, ourConversationId: state.user.ourConversationId, }, }); + + // We want to show "Alice started a group call" only if a call isn't ringing or + // active. We wait a moment to make sure that we don't accidentally show a ring + // notification followed swiftly by a less urgent notification. + if (!isAnybodyElseInGroupCall(formattedPeekInfo, ourUuid)) { + return; + } + await sleep(1000); + const newCallingState = getState().calling; + if ( + getActiveCall(newCallingState)?.conversationId === conversationId || + getIncomingCall(newCallingState.callsByConversation, ourUuid) + ) { + return; + } + calling.notifyForGroupCall(conversationId, peekInfo.creator); }); }; } diff --git a/ts/state/selectors/calling.ts b/ts/state/selectors/calling.ts index ba85e379e..b4e66c917 100644 --- a/ts/state/selectors/calling.ts +++ b/ts/state/selectors/calling.ts @@ -9,16 +9,10 @@ import { CallsByConversationType, DirectCallStateType, GroupCallStateType, - isAnybodyElseInGroupCall, + getIncomingCall as getIncomingCallHelper, } from '../ducks/calling'; -import { - CallMode, - CallState, - GroupCallConnectionState, -} from '../../types/Calling'; import { getUserUuid } from './user'; import { getOwn } from '../../util/getOwn'; -import { missingCaseError } from '../../util/missingCaseError'; export type CallStateType = DirectCallStateType | GroupCallStateType; @@ -62,29 +56,12 @@ export const isInCall = createSelector( (call: CallStateType | undefined): boolean => Boolean(call) ); -// 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 = createSelector( getCallsByConversation, getUserUuid, ( callsByConversation: CallsByConversationType, ourUuid: string - ): undefined | DirectCallStateType | GroupCallStateType => { - return Object.values(callsByConversation).find(call => { - switch (call.callMode) { - case CallMode.Direct: - return call.isIncoming && call.callState === CallState.Ringing; - case CallMode.Group: - return ( - call.ringerUuid && - call.connectionState === GroupCallConnectionState.NotConnected && - isAnybodyElseInGroupCall(call.peekInfo, ourUuid) - ); - default: - throw missingCaseError(call); - } - }); - } + ): undefined | DirectCallStateType | GroupCallStateType => + getIncomingCallHelper(callsByConversation, ourUuid) );