When a group call starts, show an OS notification

This commit is contained in:
Evan Hahn 2021-09-24 09:01:01 -05:00 committed by GitHub
parent 68b711b360
commit 9aa0de5b6c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 105 additions and 28 deletions

View file

@ -41,6 +41,7 @@ import {
} from '../state/ducks/calling'; } from '../state/ducks/calling';
import { getConversationCallMode } from '../state/ducks/conversations'; import { getConversationCallMode } from '../state/ducks/conversations';
import { isConversationTooBigToRing } from '../conversations/isConversationTooBigToRing'; import { isConversationTooBigToRing } from '../conversations/isConversationTooBigToRing';
import { isMe } from '../util/whatTypeOfConversation';
import { import {
AudioDevice, AudioDevice,
AvailableIODevicesType, AvailableIODevicesType,
@ -79,7 +80,11 @@ import { callingMessageToProto } from '../util/callingMessageToProto';
import { getSendOptions } from '../util/getSendOptions'; import { getSendOptions } from '../util/getSendOptions';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
import dataInterface from '../sql/Client'; import dataInterface from '../sql/Client';
import { notificationService } from './notifications'; import {
notificationService,
NotificationSetting,
FALLBACK_NOTIFICATION_TITLE,
} from './notifications';
import * as log from '../logging/log'; import * as log from '../logging/log';
const { const {
@ -2015,6 +2020,58 @@ export class CallingClass {
conversation.updateCallHistoryForGroupCall(peekInfo.eraId, creatorUuid); conversation.updateCallHistoryForGroupCall(peekInfo.eraId, creatorUuid);
} }
public notifyForGroupCall(
conversationId: string,
creatorBytes: undefined | Readonly<Uint8Array>
): 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<void> { private async cleanExpiredGroupCallRingsAndLoop(): Promise<void> {
try { try {
await cleanExpiredGroupCallRings(); await cleanExpiredGroupCallRings();

View file

@ -254,6 +254,28 @@ export const getActiveCall = ({
activeCallState && activeCallState &&
getOwn(callsByConversation, activeCallState.conversationId); 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<CallsByConversationType>,
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 = ( export const isAnybodyElseInGroupCall = (
{ uuids }: Readonly<GroupCallPeekInfoType>, { uuids }: Readonly<GroupCallPeekInfoType>,
ourUuid: string ourUuid: string
@ -837,6 +859,7 @@ function peekNotConnectedGroupCall(
queue.add(async () => { queue.add(async () => {
const state = getState(); const state = getState();
const { ourUuid } = state.user;
// We make sure we're not trying to peek at a connected (or connecting, or // 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 // reconnecting) call. Because this is asynchronous, it's possible that the call
@ -872,14 +895,34 @@ function peekNotConnectedGroupCall(
calling.updateCallHistoryForGroupCall(conversationId, peekInfo); calling.updateCallHistoryForGroupCall(conversationId, peekInfo);
const formattedPeekInfo = calling.formatGroupCallPeekInfoForRedux(
peekInfo
);
dispatch({ dispatch({
type: PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED, type: PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED,
payload: { payload: {
conversationId, conversationId,
peekInfo: calling.formatGroupCallPeekInfoForRedux(peekInfo), peekInfo: formattedPeekInfo,
ourConversationId: state.user.ourConversationId, 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);
}); });
}; };
} }

View file

@ -9,16 +9,10 @@ import {
CallsByConversationType, CallsByConversationType,
DirectCallStateType, DirectCallStateType,
GroupCallStateType, GroupCallStateType,
isAnybodyElseInGroupCall, getIncomingCall as getIncomingCallHelper,
} from '../ducks/calling'; } from '../ducks/calling';
import {
CallMode,
CallState,
GroupCallConnectionState,
} from '../../types/Calling';
import { getUserUuid } from './user'; import { getUserUuid } from './user';
import { getOwn } from '../../util/getOwn'; import { getOwn } from '../../util/getOwn';
import { missingCaseError } from '../../util/missingCaseError';
export type CallStateType = DirectCallStateType | GroupCallStateType; export type CallStateType = DirectCallStateType | GroupCallStateType;
@ -62,29 +56,12 @@ export const isInCall = createSelector(
(call: CallStateType | undefined): boolean => Boolean(call) (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( export const getIncomingCall = createSelector(
getCallsByConversation, getCallsByConversation,
getUserUuid, getUserUuid,
( (
callsByConversation: CallsByConversationType, callsByConversation: CallsByConversationType,
ourUuid: string ourUuid: string
): undefined | DirectCallStateType | GroupCallStateType => { ): undefined | DirectCallStateType | GroupCallStateType =>
return Object.values(callsByConversation).find(call => { getIncomingCallHelper(callsByConversation, ourUuid)
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);
}
});
}
); );