Better handle group call ring race conditions
This commit is contained in:
parent
629b5c3f6a
commit
a88243f26d
9 changed files with 169 additions and 116 deletions
|
@ -103,6 +103,7 @@ import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation';
|
|||
import { BackOff, FIBONACCI_TIMEOUTS } from './util/BackOff';
|
||||
import { AppViewType } from './state/ducks/app';
|
||||
import type { BadgesStateType } from './state/ducks/badges';
|
||||
import { areAnyCallsActiveOrRinging } from './state/selectors/calling';
|
||||
import { badgeImageFileDownloader } from './badges/badgeImageFileDownloader';
|
||||
import { actionCreators } from './state/actions';
|
||||
import { Deletes } from './messageModifiers/Deletes';
|
||||
|
@ -1049,7 +1050,11 @@ export async function startApp(): Promise<void> {
|
|||
window.reduxActions.updates
|
||||
);
|
||||
window.Signal.Services.calling.initialize(
|
||||
window.reduxActions.calling,
|
||||
{
|
||||
...window.reduxActions.calling,
|
||||
areAnyCallsActiveOrRinging: () =>
|
||||
areAnyCallsActiveOrRinging(window.reduxStore.getState()),
|
||||
},
|
||||
window.getSfuUrl()
|
||||
);
|
||||
window.reduxActions.expiration.hydrateExpirationStatus(
|
||||
|
|
|
@ -38,7 +38,7 @@ import {
|
|||
import { uniqBy, noop } from 'lodash';
|
||||
|
||||
import type {
|
||||
ActionsType as UxActionsType,
|
||||
ActionsType as CallingReduxActionsType,
|
||||
GroupCallParticipantInfoType,
|
||||
GroupCallPeekInfoType,
|
||||
} from '../state/ducks/calling';
|
||||
|
@ -55,7 +55,6 @@ import {
|
|||
CallMode,
|
||||
GroupCallConnectionState,
|
||||
GroupCallJoinState,
|
||||
ProcessGroupCallRingRequestResult,
|
||||
} from '../types/Calling';
|
||||
import {
|
||||
AudioDeviceModule,
|
||||
|
@ -102,9 +101,9 @@ import * as log from '../logging/log';
|
|||
import { assertDev } from '../util/assert';
|
||||
|
||||
const {
|
||||
processGroupCallRingRequest,
|
||||
processGroupCallRingCancelation,
|
||||
cleanExpiredGroupCallRings,
|
||||
processGroupCallRingCancellation,
|
||||
cleanExpiredGroupCallRingCancellations,
|
||||
wasGroupCallRingPreviouslyCanceled,
|
||||
} = dataInterface;
|
||||
|
||||
const RINGRTC_HTTP_METHOD_TO_OUR_HTTP_METHOD: Map<
|
||||
|
@ -129,6 +128,24 @@ enum GroupCallUpdateMessageState {
|
|||
SentLeft,
|
||||
}
|
||||
|
||||
type CallingReduxInterface = Pick<
|
||||
CallingReduxActionsType,
|
||||
| 'callStateChange'
|
||||
| 'cancelIncomingGroupCallRing'
|
||||
| 'groupCallAudioLevelsChange'
|
||||
| 'groupCallStateChange'
|
||||
| 'outgoingCall'
|
||||
| 'receiveIncomingDirectCall'
|
||||
| 'receiveIncomingGroupCall'
|
||||
| 'refreshIODevices'
|
||||
| 'remoteSharingScreenChange'
|
||||
| 'remoteVideoChange'
|
||||
| 'setPresenting'
|
||||
| 'startCallingLobby'
|
||||
> & {
|
||||
areAnyCallsActiveOrRinging(): boolean;
|
||||
};
|
||||
|
||||
function isScreenSource(source: PresentedSource): boolean {
|
||||
return source.id.startsWith('screen');
|
||||
}
|
||||
|
@ -254,7 +271,7 @@ export class CallingClass {
|
|||
|
||||
readonly videoRenderer: CanvasVideoRenderer;
|
||||
|
||||
private uxActions?: UxActionsType;
|
||||
private reduxInterface?: CallingReduxInterface;
|
||||
|
||||
private sfuUrl?: string;
|
||||
|
||||
|
@ -281,9 +298,9 @@ export class CallingClass {
|
|||
this.callsByConversation = {};
|
||||
}
|
||||
|
||||
initialize(uxActions: UxActionsType, sfuUrl: string): void {
|
||||
this.uxActions = uxActions;
|
||||
if (!uxActions) {
|
||||
initialize(reduxInterface: CallingReduxInterface, sfuUrl: string): void {
|
||||
this.reduxInterface = reduxInterface;
|
||||
if (!reduxInterface) {
|
||||
throw new Error('CallingClass.initialize: Invalid uxActions.');
|
||||
}
|
||||
|
||||
|
@ -321,7 +338,7 @@ export class CallingClass {
|
|||
});
|
||||
|
||||
ipcRenderer.on('stop-screen-share', () => {
|
||||
uxActions.setPresenting();
|
||||
reduxInterface.setPresenting();
|
||||
});
|
||||
|
||||
ipcRenderer.on('quit', () => {
|
||||
|
@ -390,7 +407,7 @@ export class CallingClass {
|
|||
throw missingCaseError(callMode);
|
||||
}
|
||||
|
||||
if (!this.uxActions) {
|
||||
if (!this.reduxInterface) {
|
||||
log.error('Missing uxActions, new call not allowed.');
|
||||
return;
|
||||
}
|
||||
|
@ -492,7 +509,7 @@ export class CallingClass {
|
|||
): Promise<void> {
|
||||
log.info('CallingClass.startOutgoingDirectCall()');
|
||||
|
||||
if (!this.uxActions) {
|
||||
if (!this.reduxInterface) {
|
||||
throw new Error('Redux actions not available');
|
||||
}
|
||||
|
||||
|
@ -544,7 +561,7 @@ export class CallingClass {
|
|||
RingRTC.setVideoRenderer(call.callId, this.videoRenderer);
|
||||
this.attachToCall(conversation, call);
|
||||
|
||||
this.uxActions.outgoingCall({
|
||||
this.reduxInterface.outgoingCall({
|
||||
conversationId: conversation.id,
|
||||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
|
@ -715,7 +732,7 @@ export class CallingClass {
|
|||
}
|
||||
const localAudioLevel = groupCall.getLocalDeviceState().audioLevel;
|
||||
|
||||
this.uxActions?.groupCallAudioLevelsChange({
|
||||
this.reduxInterface?.groupCallAudioLevelsChange({
|
||||
conversationId,
|
||||
localAudioLevel,
|
||||
remoteDeviceStates,
|
||||
|
@ -985,7 +1002,7 @@ export class CallingClass {
|
|||
conversationId: string,
|
||||
groupCall: GroupCall
|
||||
): void {
|
||||
this.uxActions?.groupCallStateChange({
|
||||
this.reduxInterface?.groupCallStateChange({
|
||||
conversationId,
|
||||
...this.formatGroupCallForRedux(groupCall),
|
||||
});
|
||||
|
@ -1247,8 +1264,8 @@ export class CallingClass {
|
|||
icon: 'images/icons/v2/video-solid-24.svg',
|
||||
message: window.i18n('calling__presenting--notification-body'),
|
||||
onNotificationClick: () => {
|
||||
if (this.uxActions) {
|
||||
this.uxActions.setPresenting();
|
||||
if (this.reduxInterface) {
|
||||
this.reduxInterface.setPresenting();
|
||||
}
|
||||
},
|
||||
silent: true,
|
||||
|
@ -1365,7 +1382,7 @@ export class CallingClass {
|
|||
|
||||
await this.selectPreferredDevices(newSettings);
|
||||
this.lastMediaDeviceSettings = newSettings;
|
||||
this.uxActions?.refreshIODevices(newSettings);
|
||||
this.reduxInterface?.refreshIODevices(newSettings);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1703,34 +1720,31 @@ export class CallingClass {
|
|||
let shouldRing = false;
|
||||
|
||||
if (update === RingUpdate.Requested) {
|
||||
const processResult = await processGroupCallRingRequest(ringId);
|
||||
switch (processResult) {
|
||||
case ProcessGroupCallRingRequestResult.ShouldRing:
|
||||
shouldRing = true;
|
||||
break;
|
||||
case ProcessGroupCallRingRequestResult.RingWasPreviouslyCanceled:
|
||||
RingRTC.cancelGroupRing(groupIdBytes, ringId, null);
|
||||
break;
|
||||
case ProcessGroupCallRingRequestResult.ThereIsAnotherActiveRing:
|
||||
RingRTC.cancelGroupRing(groupIdBytes, ringId, RingCancelReason.Busy);
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(processResult);
|
||||
if (await wasGroupCallRingPreviouslyCanceled(ringId)) {
|
||||
RingRTC.cancelGroupRing(groupIdBytes, ringId, null);
|
||||
} else if (this.areAnyCallsActiveOrRinging()) {
|
||||
RingRTC.cancelGroupRing(groupIdBytes, ringId, RingCancelReason.Busy);
|
||||
} else if (window.Events.getIncomingCallNotification()) {
|
||||
shouldRing = true;
|
||||
} else {
|
||||
log.info(
|
||||
'Incoming calls are disabled. Ignoring group call ring request'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await processGroupCallRingCancelation(ringId);
|
||||
await processGroupCallRingCancellation(ringId);
|
||||
}
|
||||
|
||||
if (shouldRing) {
|
||||
log.info('handleGroupCallRingUpdate: ringing');
|
||||
this.uxActions?.receiveIncomingGroupCall({
|
||||
this.reduxInterface?.receiveIncomingGroupCall({
|
||||
conversationId,
|
||||
ringId,
|
||||
ringerUuid,
|
||||
});
|
||||
} else {
|
||||
log.info('handleGroupCallRingUpdate: canceling any existing ring');
|
||||
this.uxActions?.cancelIncomingGroupCallRing({
|
||||
this.reduxInterface?.cancelIncomingGroupCallRing({
|
||||
conversationId,
|
||||
ringId,
|
||||
});
|
||||
|
@ -1782,7 +1796,7 @@ export class CallingClass {
|
|||
private async handleIncomingCall(call: Call): Promise<CallSettings | null> {
|
||||
log.info('CallingClass.handleIncomingCall()');
|
||||
|
||||
if (!this.uxActions || !this.localDeviceId) {
|
||||
if (!this.reduxInterface || !this.localDeviceId) {
|
||||
log.error('Missing required objects, ignoring incoming call.');
|
||||
return null;
|
||||
}
|
||||
|
@ -1815,7 +1829,7 @@ export class CallingClass {
|
|||
|
||||
this.attachToCall(conversation, call);
|
||||
|
||||
this.uxActions.receiveIncomingDirectCall({
|
||||
this.reduxInterface.receiveIncomingDirectCall({
|
||||
conversationId: conversation.id,
|
||||
isVideoCall: call.isVideoCall,
|
||||
});
|
||||
|
@ -1866,8 +1880,8 @@ export class CallingClass {
|
|||
private attachToCall(conversation: ConversationModel, call: Call): void {
|
||||
this.callsByConversation[conversation.id] = call;
|
||||
|
||||
const { uxActions } = this;
|
||||
if (!uxActions) {
|
||||
const { reduxInterface } = this;
|
||||
if (!reduxInterface) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1883,7 +1897,7 @@ export class CallingClass {
|
|||
this.lastMediaDeviceSettings = undefined;
|
||||
delete this.callsByConversation[conversation.id];
|
||||
}
|
||||
uxActions.callStateChange({
|
||||
reduxInterface.callStateChange({
|
||||
conversationId: conversation.id,
|
||||
acceptedTime,
|
||||
callState: call.state,
|
||||
|
@ -1896,7 +1910,7 @@ export class CallingClass {
|
|||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
call.handleRemoteVideoEnabled = () => {
|
||||
uxActions.remoteVideoChange({
|
||||
reduxInterface.remoteVideoChange({
|
||||
conversationId: conversation.id,
|
||||
hasVideo: call.remoteVideoEnabled,
|
||||
});
|
||||
|
@ -1904,7 +1918,7 @@ export class CallingClass {
|
|||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
call.handleRemoteSharingScreen = () => {
|
||||
uxActions.remoteSharingScreenChange({
|
||||
reduxInterface.remoteSharingScreenChange({
|
||||
conversationId: conversation.id,
|
||||
isSharingScreen: Boolean(call.remoteSharingScreen),
|
||||
});
|
||||
|
@ -2189,7 +2203,7 @@ export class CallingClass {
|
|||
icon: 'images/icons/v2/video-solid-24.svg',
|
||||
message: notificationMessage,
|
||||
onNotificationClick: () => {
|
||||
this.uxActions?.startCallingLobby({
|
||||
this.reduxInterface?.startCallingLobby({
|
||||
conversationId: conversation.id,
|
||||
isVideoCall: true,
|
||||
});
|
||||
|
@ -2199,9 +2213,13 @@ export class CallingClass {
|
|||
});
|
||||
}
|
||||
|
||||
private areAnyCallsActiveOrRinging(): boolean {
|
||||
return this.reduxInterface?.areAnyCallsActiveOrRinging() ?? false;
|
||||
}
|
||||
|
||||
private async cleanExpiredGroupCallRingsAndLoop(): Promise<void> {
|
||||
try {
|
||||
await cleanExpiredGroupCallRings();
|
||||
await cleanExpiredGroupCallRingCancellations();
|
||||
} catch (err: unknown) {
|
||||
// These errors are ignored here. They should be logged elsewhere and it's okay if
|
||||
// we don't do a cleanup this time.
|
||||
|
|
|
@ -10,7 +10,6 @@ import type {
|
|||
import type { StoredJob } from '../jobs/types';
|
||||
import type { ReactionType } from '../types/Reactions';
|
||||
import type { ConversationColorType, CustomColorType } from '../types/Colors';
|
||||
import type { ProcessGroupCallRingRequestResult } from '../types/Calling';
|
||||
import type { StorageAccessType } from '../types/Storage.d';
|
||||
import type { AttachmentType } from '../types/Attachment';
|
||||
import type { BodyRangesType, BytesToStrings } from '../types/Util';
|
||||
|
@ -689,11 +688,9 @@ export type DataInterface = {
|
|||
insertJob(job: Readonly<StoredJob>): Promise<void>;
|
||||
deleteJob(id: string): Promise<void>;
|
||||
|
||||
processGroupCallRingRequest(
|
||||
ringId: bigint
|
||||
): Promise<ProcessGroupCallRingRequestResult>;
|
||||
processGroupCallRingCancelation(ringId: bigint): Promise<void>;
|
||||
cleanExpiredGroupCallRings(): Promise<void>;
|
||||
wasGroupCallRingPreviouslyCanceled(ringId: bigint): Promise<boolean>;
|
||||
processGroupCallRingCancellation(ringId: bigint): Promise<void>;
|
||||
cleanExpiredGroupCallRingCancellations(): Promise<void>;
|
||||
|
||||
getMaxMessageCounter(): Promise<number | undefined>;
|
||||
|
||||
|
|
|
@ -46,7 +46,6 @@ import { parseIntOrThrow } from '../util/parseIntOrThrow';
|
|||
import * as durations from '../util/durations';
|
||||
import { formatCountForLogging } from '../logging/formatCountForLogging';
|
||||
import type { ConversationColorType, CustomColorType } from '../types/Colors';
|
||||
import { ProcessGroupCallRingRequestResult } from '../types/Calling';
|
||||
import { RemoveAllConfiguration } from '../types/RemoveAllConfiguration';
|
||||
import type { BadgeType, BadgeImageType } from '../badges/types';
|
||||
import { parseBadgeCategory } from '../badges/BadgeCategory';
|
||||
|
@ -333,9 +332,9 @@ const dataInterface: ServerInterface = {
|
|||
insertJob,
|
||||
deleteJob,
|
||||
|
||||
processGroupCallRingRequest,
|
||||
processGroupCallRingCancelation,
|
||||
cleanExpiredGroupCallRings,
|
||||
wasGroupCallRingPreviouslyCanceled,
|
||||
processGroupCallRingCancellation,
|
||||
cleanExpiredGroupCallRingCancellations,
|
||||
|
||||
getMaxMessageCounter,
|
||||
|
||||
|
@ -4819,7 +4818,7 @@ async function removeAll(): Promise<void> {
|
|||
DELETE FROM badges;
|
||||
DELETE FROM conversations;
|
||||
DELETE FROM emojis;
|
||||
DELETE FROM groupCallRings;
|
||||
DELETE FROM groupCallRingCancellations;
|
||||
DELETE FROM identityKeys;
|
||||
DELETE FROM items;
|
||||
DELETE FROM jobs;
|
||||
|
@ -5425,69 +5424,36 @@ async function deleteJob(id: string): Promise<void> {
|
|||
db.prepare<Query>('DELETE FROM jobs WHERE id = $id').run({ id });
|
||||
}
|
||||
|
||||
async function processGroupCallRingRequest(
|
||||
async function wasGroupCallRingPreviouslyCanceled(
|
||||
ringId: bigint
|
||||
): Promise<ProcessGroupCallRingRequestResult> {
|
||||
): Promise<boolean> {
|
||||
const db = getInstance();
|
||||
|
||||
return db.transaction(() => {
|
||||
let result: ProcessGroupCallRingRequestResult;
|
||||
|
||||
const wasRingPreviouslyCanceled = Boolean(
|
||||
db
|
||||
.prepare<Query>(
|
||||
`
|
||||
SELECT 1 FROM groupCallRings
|
||||
WHERE ringId = $ringId AND isActive = 0
|
||||
LIMIT 1;
|
||||
`
|
||||
)
|
||||
.pluck(true)
|
||||
.get({ ringId })
|
||||
);
|
||||
|
||||
if (wasRingPreviouslyCanceled) {
|
||||
result = ProcessGroupCallRingRequestResult.RingWasPreviouslyCanceled;
|
||||
} else {
|
||||
const isThereAnotherActiveRing = Boolean(
|
||||
db
|
||||
.prepare<EmptyQuery>(
|
||||
`
|
||||
SELECT 1 FROM groupCallRings
|
||||
WHERE isActive = 1
|
||||
LIMIT 1;
|
||||
`
|
||||
)
|
||||
.pluck(true)
|
||||
.get()
|
||||
return db
|
||||
.prepare<Query>(
|
||||
`
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM groupCallRingCancellations
|
||||
WHERE ringId = $ringId
|
||||
AND createdAt >= $ringsOlderThanThisAreIgnored
|
||||
);
|
||||
if (isThereAnotherActiveRing) {
|
||||
result = ProcessGroupCallRingRequestResult.ThereIsAnotherActiveRing;
|
||||
} else {
|
||||
result = ProcessGroupCallRingRequestResult.ShouldRing;
|
||||
}
|
||||
|
||||
db.prepare<Query>(
|
||||
`
|
||||
INSERT OR IGNORE INTO groupCallRings (ringId, isActive, createdAt)
|
||||
VALUES ($ringId, 1, $createdAt);
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
})();
|
||||
`
|
||||
)
|
||||
.pluck()
|
||||
.get({
|
||||
ringId,
|
||||
ringsOlderThanThisAreIgnored: Date.now() - MAX_GROUP_CALL_RING_AGE,
|
||||
});
|
||||
}
|
||||
|
||||
async function processGroupCallRingCancelation(ringId: bigint): Promise<void> {
|
||||
async function processGroupCallRingCancellation(ringId: bigint): Promise<void> {
|
||||
const db = getInstance();
|
||||
|
||||
db.prepare<Query>(
|
||||
`
|
||||
INSERT INTO groupCallRings (ringId, isActive, createdAt)
|
||||
VALUES ($ringId, 0, $createdAt)
|
||||
ON CONFLICT (ringId) DO
|
||||
UPDATE SET isActive = 0;
|
||||
INSERT INTO groupCallRingCancellations (ringId, createdAt)
|
||||
VALUES ($ringId, $createdAt)
|
||||
ON CONFLICT (ringId) DO NOTHING;
|
||||
`
|
||||
).run({ ringId, createdAt: Date.now() });
|
||||
}
|
||||
|
@ -5496,12 +5462,12 @@ async function processGroupCallRingCancelation(ringId: bigint): Promise<void> {
|
|||
// that, it doesn't really matter what the value is.
|
||||
const MAX_GROUP_CALL_RING_AGE = 30 * durations.MINUTE;
|
||||
|
||||
async function cleanExpiredGroupCallRings(): Promise<void> {
|
||||
async function cleanExpiredGroupCallRingCancellations(): Promise<void> {
|
||||
const db = getInstance();
|
||||
|
||||
db.prepare<Query>(
|
||||
`
|
||||
DELETE FROM groupCallRings
|
||||
DELETE FROM groupCallRingCancellations
|
||||
WHERE createdAt < $expiredRingTime;
|
||||
`
|
||||
).run({
|
||||
|
|
33
ts/sql/migrations/69-group-call-ring-cancellations.ts
Normal file
33
ts/sql/migrations/69-group-call-ring-cancellations.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { Database } from 'better-sqlite3';
|
||||
|
||||
import type { LoggerType } from '../../types/Logging';
|
||||
|
||||
export default function updateToSchemaVersion69(
|
||||
currentVersion: number,
|
||||
db: Database,
|
||||
logger: LoggerType
|
||||
): void {
|
||||
if (currentVersion >= 69) {
|
||||
return;
|
||||
}
|
||||
|
||||
db.transaction(() => {
|
||||
db.exec(
|
||||
`
|
||||
DROP TABLE IF EXISTS groupCallRings;
|
||||
|
||||
CREATE TABLE groupCallRingCancellations(
|
||||
ringId INTEGER PRIMARY KEY,
|
||||
createdAt INTEGER NOT NULL
|
||||
);
|
||||
`
|
||||
);
|
||||
|
||||
db.pragma('user_version = 69');
|
||||
})();
|
||||
|
||||
logger.info('updateToSchemaVersion69: success!');
|
||||
}
|
|
@ -44,6 +44,7 @@ import updateToSchemaVersion65 from './65-add-storage-id-to-stickers';
|
|||
import updateToSchemaVersion66 from './66-add-pni-signature-to-sent-protos';
|
||||
import updateToSchemaVersion67 from './67-add-story-to-unprocessed';
|
||||
import updateToSchemaVersion68 from './68-drop-deprecated-columns';
|
||||
import updateToSchemaVersion69 from './69-group-call-ring-cancellations';
|
||||
|
||||
function updateToSchemaVersion1(
|
||||
currentVersion: number,
|
||||
|
@ -1950,6 +1951,7 @@ export const SCHEMA_VERSIONS = [
|
|||
updateToSchemaVersion66,
|
||||
updateToSchemaVersion67,
|
||||
updateToSchemaVersion68,
|
||||
updateToSchemaVersion69,
|
||||
];
|
||||
|
||||
export function updateSchema(db: Database, logger: LoggerType): void {
|
||||
|
|
|
@ -80,6 +80,12 @@ export const getIncomingCall = createSelector(
|
|||
}
|
||||
);
|
||||
|
||||
export const areAnyCallsActiveOrRinging = createSelector(
|
||||
getActiveCall,
|
||||
getIncomingCall,
|
||||
(activeCall, incomingCall): boolean => Boolean(activeCall || incomingCall)
|
||||
);
|
||||
|
||||
export const isInSpeakerView = (
|
||||
call: Pick<ActiveCallStateType, 'viewMode'> | undefined
|
||||
): boolean => {
|
||||
|
|
|
@ -2390,4 +2390,36 @@ describe('SQL migrations test', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateToSchemaVersion69', () => {
|
||||
beforeEach(() => {
|
||||
updateToVersion(69);
|
||||
});
|
||||
|
||||
it('removes the legacy groupCallRings table', () => {
|
||||
const tableCount = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT COUNT(*) FROM sqlite_schema
|
||||
WHERE type = "table"
|
||||
AND name = "groupCallRings"
|
||||
`
|
||||
)
|
||||
.pluck();
|
||||
|
||||
assert.strictEqual(tableCount.get(), 0);
|
||||
});
|
||||
|
||||
it('adds the groupCallRingCancellations table', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
db.exec(
|
||||
`
|
||||
INSERT INTO groupCallRingCancellations
|
||||
(ringId, createdAt)
|
||||
VALUES (1, 2);
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -196,9 +196,3 @@ export type ChangeIODevicePayloadType =
|
|||
| { type: CallingDeviceType.CAMERA; selectedDevice: string }
|
||||
| { type: CallingDeviceType.MICROPHONE; selectedDevice: AudioDevice }
|
||||
| { type: CallingDeviceType.SPEAKER; selectedDevice: AudioDevice };
|
||||
|
||||
export enum ProcessGroupCallRingRequestResult {
|
||||
ShouldRing,
|
||||
RingWasPreviouslyCanceled,
|
||||
ThereIsAnotherActiveRing,
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue