When a group call starts, show an OS notification
This commit is contained in:
parent
68b711b360
commit
9aa0de5b6c
3 changed files with 105 additions and 28 deletions
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue