Synchronous delete call link
This commit is contained in:
parent
e60df56500
commit
42cc5e0013
23 changed files with 443 additions and 135 deletions
|
@ -1633,6 +1633,10 @@
|
|||
"messageformat": "Signal Call",
|
||||
"description": "Default title for Signal call links."
|
||||
},
|
||||
"icu:calling__call-link-delete-failed": {
|
||||
"messageformat": "Can't delete link. Check your connection and try again.",
|
||||
"description": "Default title for Signal call links."
|
||||
},
|
||||
"icu:calling__join-request-denied": {
|
||||
"messageformat": "Your request to join this call has been denied.",
|
||||
"description": "Error message when a request to join a call link call was rejected by a call admin."
|
||||
|
@ -7298,7 +7302,11 @@
|
|||
"description": "Calls Tab > Confirm Clear Call History Dialog > Title"
|
||||
},
|
||||
"icu:CallsTab__ConfirmClearCallHistory__Body": {
|
||||
"messageformat": "This will permanently delete all call history",
|
||||
"messageformat": "This will permanently delete all call history.",
|
||||
"description": "Calls Tab > Confirm Clear Call History Dialog > Body Text"
|
||||
},
|
||||
"icu:CallsTab__ConfirmClearCallHistory__Body--call-links": {
|
||||
"messageformat": "This will permanently delete all call history. Call links you’ve created will no longer work for people who have them. ",
|
||||
"description": "Calls Tab > Confirm Clear Call History Dialog > Body Text"
|
||||
},
|
||||
"icu:CallsTab__ConfirmClearCallHistory__ConfirmButton": {
|
||||
|
@ -7309,6 +7317,14 @@
|
|||
"messageformat": "Call history cleared",
|
||||
"description": "Calls Tab > Clear Call History > Toast"
|
||||
},
|
||||
"icu:CallsTab__ClearCallHistoryError--call-links": {
|
||||
"messageformat": "Not all call links could be deleted. Check your connection and try again.",
|
||||
"description": "Calls Tab > Clear Call History > Error modal when we failed to delete call links."
|
||||
},
|
||||
"icu:CallsTab__ClearCallHistoryError": {
|
||||
"messageformat": "Not all call history could be cleared. Check your connection and try again.",
|
||||
"description": "Calls Tab > Clear Call History > Generic error modal"
|
||||
},
|
||||
"icu:CallsTab__EmptyStateText--with-icon-2": {
|
||||
"messageformat": "Click <newCallButtonIcon></newCallButtonIcon> to start a new voice or video call.",
|
||||
"description": "Calls Tab > When no call is selected > Empty state > Call to action text"
|
||||
|
|
|
@ -49,6 +49,7 @@ type CallsTabProps = Readonly<{
|
|||
getCallLink: (id: string) => CallLinkType | undefined;
|
||||
getConversation: (id: string) => ConversationType | void;
|
||||
hangUpActiveCall: (reason: string) => void;
|
||||
hasAnyAdminCallLinks: boolean;
|
||||
hasFailedStorySends: boolean;
|
||||
hasPendingUpdate: boolean;
|
||||
i18n: LocalizerType;
|
||||
|
@ -105,6 +106,7 @@ export function CallsTab({
|
|||
getCallLink,
|
||||
getConversation,
|
||||
hangUpActiveCall,
|
||||
hasAnyAdminCallLinks,
|
||||
hasFailedStorySends,
|
||||
hasPendingUpdate,
|
||||
i18n,
|
||||
|
@ -370,7 +372,9 @@ export function CallsTab({
|
|||
},
|
||||
]}
|
||||
>
|
||||
{i18n('icu:CallsTab__ConfirmClearCallHistory__Body')}
|
||||
{hasAnyAdminCallLinks
|
||||
? i18n('icu:CallsTab__ConfirmClearCallHistory__Body--call-links')
|
||||
: i18n('icu:CallsTab__ConfirmClearCallHistory__Body')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -10,7 +10,7 @@ import { Button, ButtonVariant } from './Button';
|
|||
export type PropsType = {
|
||||
buttonVariant?: ButtonVariant;
|
||||
description?: string;
|
||||
title?: string;
|
||||
title?: string | null;
|
||||
|
||||
onClose: () => void;
|
||||
i18n: LocalizerType;
|
||||
|
@ -40,7 +40,7 @@ export function ErrorModal(props: PropsType): JSX.Element {
|
|||
modalName="ErrorModal"
|
||||
i18n={i18n}
|
||||
onClose={onClose}
|
||||
title={title || i18n('icu:ErrorModal--title')}
|
||||
title={title == null ? undefined : i18n('icu:ErrorModal--title')}
|
||||
modalFooter={footer}
|
||||
>
|
||||
<div className="module-error-modal__description">
|
||||
|
|
|
@ -53,12 +53,16 @@ export type PropsType = {
|
|||
renderEditNicknameAndNoteModal: () => JSX.Element;
|
||||
// ErrorModal
|
||||
errorModalProps:
|
||||
| { buttonVariant?: ButtonVariant; description?: string; title?: string }
|
||||
| {
|
||||
buttonVariant?: ButtonVariant;
|
||||
description?: string;
|
||||
title?: string | null;
|
||||
}
|
||||
| undefined;
|
||||
renderErrorModal: (opts: {
|
||||
buttonVariant?: ButtonVariant;
|
||||
description?: string;
|
||||
title?: string;
|
||||
title?: string | null;
|
||||
}) => JSX.Element;
|
||||
// DeleteMessageModal
|
||||
deleteMessagesProps: DeleteMessagesPropsType | undefined;
|
||||
|
|
|
@ -9,8 +9,6 @@ import {
|
|||
type JobManagerJobResultType,
|
||||
type JobManagerJobType,
|
||||
} from './JobManager';
|
||||
import { calling } from '../services/calling';
|
||||
import { callLinkFromRecord } from '../util/callLinksRingrtc';
|
||||
|
||||
// Type for adding a new job
|
||||
export type NewCallLinkDeleteJobType = {
|
||||
|
@ -28,7 +26,7 @@ export type CallLinkDeleteJobType = CoreCallLinkDeleteJobType &
|
|||
const MAX_CONCURRENT_JOBS = 5;
|
||||
|
||||
const DEFAULT_RETRY_CONFIG = {
|
||||
maxAttempts: Infinity,
|
||||
maxAttempts: 10,
|
||||
backoffConfig: {
|
||||
// 1 min, 5 min, 25 min, (max) 1 day
|
||||
multiplier: 5,
|
||||
|
@ -37,19 +35,24 @@ const DEFAULT_RETRY_CONFIG = {
|
|||
},
|
||||
};
|
||||
|
||||
type CallLinkDeleteManagerParamsType =
|
||||
type CallLinkFinalizeDeleteManagerParamsType =
|
||||
JobManagerParamsType<CoreCallLinkDeleteJobType>;
|
||||
|
||||
function getJobId(job: CoreCallLinkDeleteJobType): string {
|
||||
return job.roomId;
|
||||
}
|
||||
|
||||
export class CallLinkDeleteManager extends JobManager<CoreCallLinkDeleteJobType> {
|
||||
// The purpose of this job is to finalize local DB delete of call links and
|
||||
// associated call history, after we confirm storage sync.
|
||||
// It does *not* delete the call link from the server -- this should be done
|
||||
// synchronously and prior to running this job, so we can show confirmation
|
||||
// or error to the user.
|
||||
export class CallLinkFinalizeDeleteManager extends JobManager<CoreCallLinkDeleteJobType> {
|
||||
jobs: Map<string, CallLinkDeleteJobType> = new Map();
|
||||
private static _instance: CallLinkDeleteManager | undefined;
|
||||
override logPrefix = 'CallLinkDeleteManager';
|
||||
private static _instance: CallLinkFinalizeDeleteManager | undefined;
|
||||
override logPrefix = 'CallLinkFinalizeDeleteManager';
|
||||
|
||||
static defaultParams: CallLinkDeleteManagerParamsType = {
|
||||
static defaultParams: CallLinkFinalizeDeleteManagerParamsType = {
|
||||
markAllJobsInactive: () => Promise.resolve(),
|
||||
getNextJobs,
|
||||
saveJob,
|
||||
|
@ -61,7 +64,7 @@ export class CallLinkDeleteManager extends JobManager<CoreCallLinkDeleteJobType>
|
|||
maxConcurrentJobs: MAX_CONCURRENT_JOBS,
|
||||
};
|
||||
|
||||
constructor(params: CallLinkDeleteManagerParamsType) {
|
||||
constructor(params: CallLinkFinalizeDeleteManagerParamsType) {
|
||||
super({
|
||||
...params,
|
||||
getNextJobs: ({ limit, timestamp }) =>
|
||||
|
@ -103,40 +106,43 @@ export class CallLinkDeleteManager extends JobManager<CoreCallLinkDeleteJobType>
|
|||
roomIds.forEach(roomId => this.addJob({ roomId }, options));
|
||||
}
|
||||
|
||||
static get instance(): CallLinkDeleteManager {
|
||||
if (!CallLinkDeleteManager._instance) {
|
||||
CallLinkDeleteManager._instance = new CallLinkDeleteManager(
|
||||
CallLinkDeleteManager.defaultParams
|
||||
);
|
||||
static get instance(): CallLinkFinalizeDeleteManager {
|
||||
if (!CallLinkFinalizeDeleteManager._instance) {
|
||||
CallLinkFinalizeDeleteManager._instance =
|
||||
new CallLinkFinalizeDeleteManager(
|
||||
CallLinkFinalizeDeleteManager.defaultParams
|
||||
);
|
||||
}
|
||||
return CallLinkDeleteManager._instance;
|
||||
return CallLinkFinalizeDeleteManager._instance;
|
||||
}
|
||||
|
||||
static async start(): Promise<void> {
|
||||
await CallLinkDeleteManager.instance.enqueueAllDeletedCallLinks();
|
||||
await CallLinkDeleteManager.instance.start();
|
||||
await CallLinkFinalizeDeleteManager.instance.enqueueAllDeletedCallLinks();
|
||||
await CallLinkFinalizeDeleteManager.instance.start();
|
||||
}
|
||||
|
||||
static async stop(): Promise<void> {
|
||||
return CallLinkDeleteManager._instance?.stop();
|
||||
return CallLinkFinalizeDeleteManager._instance?.stop();
|
||||
}
|
||||
|
||||
static async addJob(
|
||||
newJob: CoreCallLinkDeleteJobType,
|
||||
options?: { delay: number }
|
||||
): Promise<void> {
|
||||
return CallLinkDeleteManager.instance.addJob(newJob, options);
|
||||
return CallLinkFinalizeDeleteManager.instance.addJob(newJob, options);
|
||||
}
|
||||
|
||||
static async enqueueAllDeletedCallLinks(options?: {
|
||||
delay: number;
|
||||
}): Promise<void> {
|
||||
return CallLinkDeleteManager.instance.enqueueAllDeletedCallLinks(options);
|
||||
return CallLinkFinalizeDeleteManager.instance.enqueueAllDeletedCallLinks(
|
||||
options
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function getNextJobs(
|
||||
this: CallLinkDeleteManager,
|
||||
this: CallLinkFinalizeDeleteManager,
|
||||
{
|
||||
limit,
|
||||
timestamp,
|
||||
|
@ -162,7 +168,7 @@ async function getNextJobs(
|
|||
}
|
||||
|
||||
async function saveJob(
|
||||
this: CallLinkDeleteManager,
|
||||
this: CallLinkFinalizeDeleteManager,
|
||||
job: CallLinkDeleteJobType
|
||||
): Promise<void> {
|
||||
const { roomId } = job;
|
||||
|
@ -170,14 +176,10 @@ async function saveJob(
|
|||
}
|
||||
|
||||
async function removeJob(
|
||||
this: CallLinkDeleteManager,
|
||||
this: CallLinkFinalizeDeleteManager,
|
||||
job: CallLinkDeleteJobType
|
||||
): Promise<void> {
|
||||
const logId = `CallLinkDeleteJobType/removeJob/${getJobId(job)}`;
|
||||
const { roomId } = job;
|
||||
await DataWriter.finalizeDeleteCallLink(job.roomId);
|
||||
log.info(`${logId}: Finalized local delete`);
|
||||
this.jobs.delete(roomId);
|
||||
this.jobs.delete(job.roomId);
|
||||
}
|
||||
|
||||
async function runJob(
|
||||
|
@ -191,10 +193,8 @@ async function runJob(
|
|||
log.warn(`${logId}: Call link gone from DB`);
|
||||
return { status: 'finished' };
|
||||
}
|
||||
if (callLinkRecord.adminKey == null) {
|
||||
log.error(
|
||||
`${logId}: No admin key available, deletion on server not possible. Giving up.`
|
||||
);
|
||||
if (callLinkRecord.deleted !== 1) {
|
||||
log.error(`${logId}: Call link not marked deleted. Giving up.`);
|
||||
return { status: 'finished' };
|
||||
}
|
||||
|
||||
|
@ -204,10 +204,7 @@ async function runJob(
|
|||
return { status: 'retry' };
|
||||
}
|
||||
|
||||
// Delete link on calling server. May 200 or 404 and both are OK.
|
||||
// Errs if call link is active or network is unavailable.
|
||||
const callLink = callLinkFromRecord(callLinkRecord);
|
||||
await calling.deleteCallLink(callLink);
|
||||
log.info(`${logId}: Deleted call link on server`);
|
||||
await DataWriter.finalizeDeleteCallLink(job.roomId);
|
||||
log.info(`${logId}: Finalized local delete`);
|
||||
return { status: 'finished' };
|
||||
}
|
|
@ -97,10 +97,7 @@ export class CallLinkRefreshJobQueue extends JobQueue<CallLinkRefreshJobData> {
|
|||
`${logId}: Call link not found on server and deleteLocallyIfMissingOnCallingServer; deleting local call link`
|
||||
);
|
||||
// This will leave a storage service record, and it's up to primary to delete it
|
||||
await DataWriter.beginDeleteCallLink(roomId, {
|
||||
storageNeedsSync: false,
|
||||
});
|
||||
await DataWriter.finalizeDeleteCallLink(roomId);
|
||||
await DataWriter.deleteCallLinkAndHistory(roomId);
|
||||
window.reduxActions.calling.handleCallLinkDelete({ roomId });
|
||||
} else {
|
||||
log.info(`${logId}: Call link not found on server, ignoring`);
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import type { WebAPIType } from '../textsecure/WebAPI';
|
||||
import { drop } from '../util/drop';
|
||||
import { CallLinkDeleteManager } from './CallLinkDeleteManager';
|
||||
import { CallLinkFinalizeDeleteManager } from './CallLinkFinalizeDeleteManager';
|
||||
|
||||
import { callLinkRefreshJobQueue } from './callLinkRefreshJobQueue';
|
||||
import { conversationJobQueue } from './conversationJobQueue';
|
||||
|
@ -43,7 +43,7 @@ export function initializeAllJobQueues({
|
|||
drop(removeStorageKeyJobQueue.streamJobs());
|
||||
drop(reportSpamJobQueue.streamJobs());
|
||||
drop(callLinkRefreshJobQueue.streamJobs());
|
||||
drop(CallLinkDeleteManager.start());
|
||||
drop(CallLinkFinalizeDeleteManager.start());
|
||||
}
|
||||
|
||||
export async function shutdownAllJobQueues(): Promise<void> {
|
||||
|
@ -57,6 +57,6 @@ export async function shutdownAllJobQueues(): Promise<void> {
|
|||
viewOnceOpenJobQueue.shutdown(),
|
||||
removeStorageKeyJobQueue.shutdown(),
|
||||
reportSpamJobQueue.shutdown(),
|
||||
CallLinkDeleteManager.stop(),
|
||||
CallLinkFinalizeDeleteManager.stop(),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -2075,12 +2075,8 @@ export async function mergeCallLinkRecord(
|
|||
// Another device deleted the link and uploaded to storage, and we learned about it
|
||||
log.info(`${logId}: Discovered deleted call link, deleting locally`);
|
||||
details.push('deleting locally');
|
||||
await DataWriter.beginDeleteCallLink(roomId, {
|
||||
storageNeedsSync: false,
|
||||
deletedAt,
|
||||
});
|
||||
// No need to delete via RingRTC as we assume the originating device did that already
|
||||
await DataWriter.finalizeDeleteCallLink(roomId);
|
||||
await DataWriter.deleteCallLinkAndHistory(roomId);
|
||||
window.reduxActions.calling.handleCallLinkDelete({ roomId });
|
||||
} else if (!deletedAt && localCallLinkDbRecord.deleted === 1) {
|
||||
// Not deleted in storage, but we've marked it as deleted locally.
|
||||
|
|
|
@ -44,7 +44,6 @@ import type {
|
|||
import type { SyncTaskType } from '../util/syncTasks';
|
||||
import type { AttachmentBackupJobType } from '../types/AttachmentBackup';
|
||||
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||
import type { DeleteCallLinkOptions } from './server/callLinks';
|
||||
|
||||
export type ReadableDB = Database & { __readable_db: never };
|
||||
export type WritableDB = ReadableDB & { __writable_db: never };
|
||||
|
@ -584,6 +583,7 @@ type ReadableInterface = {
|
|||
getAllCallLinks: () => ReadonlyArray<CallLinkType>;
|
||||
getCallLinkByRoomId: (roomId: string) => CallLinkType | undefined;
|
||||
getCallLinkRecordByRoomId: (roomId: string) => CallLinkRecord | undefined;
|
||||
getAllAdminCallLinks(): ReadonlyArray<CallLinkType>;
|
||||
getAllCallLinkRecordsWithAdminKey(): ReadonlyArray<CallLinkRecord>;
|
||||
getAllMarkedDeletedCallLinkRoomIds(): ReadonlyArray<string>;
|
||||
getMessagesBetween: (
|
||||
|
@ -813,8 +813,10 @@ type WritableInterface = {
|
|||
roomId: string,
|
||||
callLinkState: CallLinkStateType
|
||||
): CallLinkType;
|
||||
beginDeleteAllCallLinks(): void;
|
||||
beginDeleteCallLink(roomId: string, options: DeleteCallLinkOptions): void;
|
||||
beginDeleteAllCallLinks(): boolean;
|
||||
beginDeleteCallLink(roomId: string): boolean;
|
||||
deleteCallHistoryByRoomId(roomid: string): void;
|
||||
deleteCallLinkAndHistory(roomId: string): void;
|
||||
finalizeDeleteCallLink(roomId: string): void;
|
||||
_removeAllCallLinks(): void;
|
||||
deleteCallLinkFromSync(roomId: string): void;
|
||||
|
|
|
@ -180,6 +180,9 @@ import {
|
|||
updateCallLinkAdminKeyByRoomId,
|
||||
updateCallLinkState,
|
||||
beginDeleteAllCallLinks,
|
||||
deleteCallHistoryByRoomId,
|
||||
deleteCallLinkAndHistory,
|
||||
getAllAdminCallLinks,
|
||||
getAllCallLinkRecordsWithAdminKey,
|
||||
getAllMarkedDeletedCallLinkRoomIds,
|
||||
finalizeDeleteCallLink,
|
||||
|
@ -313,6 +316,7 @@ export const DataReader: ServerReadableInterface = {
|
|||
getAllCallLinks,
|
||||
getCallLinkByRoomId,
|
||||
getCallLinkRecordByRoomId,
|
||||
getAllAdminCallLinks,
|
||||
getAllCallLinkRecordsWithAdminKey,
|
||||
getAllMarkedDeletedCallLinkRoomIds,
|
||||
getMessagesBetween,
|
||||
|
@ -451,6 +455,8 @@ export const DataWriter: ServerWritableInterface = {
|
|||
updateCallLinkState,
|
||||
beginDeleteAllCallLinks,
|
||||
beginDeleteCallLink,
|
||||
deleteCallHistoryByRoomId,
|
||||
deleteCallLinkAndHistory,
|
||||
finalizeDeleteCallLink,
|
||||
_removeAllCallLinks,
|
||||
deleteCallLinkFromSync,
|
||||
|
@ -3543,6 +3549,14 @@ function _removeAllCallHistory(db: WritableDB): void {
|
|||
db.prepare(query).run(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes call history by marking it deleted. Tombstoning is needed in case sync messages
|
||||
* come in around the same time, to prevent reappearance of deleted call history.
|
||||
* Limitation: History for admin call links is skipped. Admin call links need to be
|
||||
* deleted on the calling server first, before we can clear local history.
|
||||
*
|
||||
* @returns ReadonlyArray<string>: message ids of call history messages
|
||||
*/
|
||||
function clearCallHistory(
|
||||
db: WritableDB,
|
||||
target: CallLogEventTarget
|
||||
|
@ -3555,17 +3569,33 @@ function clearCallHistory(
|
|||
}
|
||||
const { timestamp } = callHistory;
|
||||
|
||||
// Admin call links are deleted separately after server confirmation
|
||||
const [selectAdminCallLinksQuery, selectAdminCallLinksParams] = sql`
|
||||
SELECT roomId
|
||||
FROM callLinks
|
||||
WHERE callLinks.adminKey IS NOT NULL;
|
||||
`;
|
||||
|
||||
const adminCallLinkIds: ReadonlyArray<string> = db
|
||||
.prepare(selectAdminCallLinksQuery)
|
||||
.pluck()
|
||||
.all(selectAdminCallLinksParams);
|
||||
const adminCallLinkIdsFragment = sqlJoin(adminCallLinkIds);
|
||||
|
||||
const [selectCallsQuery, selectCallsParams] = sql`
|
||||
SELECT callsHistory.callId
|
||||
FROM callsHistory
|
||||
WHERE
|
||||
-- Prior calls
|
||||
(callsHistory.timestamp <= ${timestamp})
|
||||
-- Unused call links
|
||||
OR (
|
||||
callsHistory.mode IS ${CALL_MODE_ADHOC} AND
|
||||
callsHistory.status IS ${CALL_STATUS_PENDING}
|
||||
);
|
||||
(
|
||||
-- Prior calls
|
||||
(callsHistory.timestamp <= ${timestamp})
|
||||
-- Unused call links
|
||||
OR (
|
||||
callsHistory.mode IS ${CALL_MODE_ADHOC} AND
|
||||
callsHistory.status IS ${CALL_STATUS_PENDING}
|
||||
)
|
||||
) AND
|
||||
callsHistory.peerId NOT IN (${adminCallLinkIdsFragment});
|
||||
`;
|
||||
|
||||
const deletedCallIds: ReadonlyArray<string> = db
|
||||
|
|
28
ts/sql/migrations/1230-call-links-admin-key-index.ts
Normal file
28
ts/sql/migrations/1230-call-links-admin-key-index.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { Database } from '@signalapp/better-sqlite3';
|
||||
import type { LoggerType } from '../../types/Logging';
|
||||
|
||||
export const version = 1230;
|
||||
|
||||
export function updateToSchemaVersion1230(
|
||||
currentVersion: number,
|
||||
db: Database,
|
||||
logger: LoggerType
|
||||
): void {
|
||||
if (currentVersion >= 1230) {
|
||||
return;
|
||||
}
|
||||
|
||||
db.transaction(() => {
|
||||
db.exec(`
|
||||
DROP INDEX IF EXISTS callLinks_adminKey;
|
||||
|
||||
CREATE INDEX callLinks_adminKey
|
||||
ON callLinks (adminKey);
|
||||
`);
|
||||
|
||||
db.pragma('user_version = 1230');
|
||||
})();
|
||||
logger.info('updateToSchemaVersion1230: success!');
|
||||
}
|
|
@ -98,10 +98,11 @@ import { updateToSchemaVersion1180 } from './1180-add-attachment-download-source
|
|||
import { updateToSchemaVersion1190 } from './1190-call-links-storage';
|
||||
import { updateToSchemaVersion1200 } from './1200-attachment-download-source-index';
|
||||
import { updateToSchemaVersion1210 } from './1210-call-history-started-id';
|
||||
import { updateToSchemaVersion1220 } from './1220-blob-sessions';
|
||||
import {
|
||||
updateToSchemaVersion1220,
|
||||
updateToSchemaVersion1230,
|
||||
version as MAX_VERSION,
|
||||
} from './1220-blob-sessions';
|
||||
} from './1230-call-links-admin-key-index';
|
||||
|
||||
function updateToSchemaVersion1(
|
||||
currentVersion: number,
|
||||
|
@ -2069,6 +2070,7 @@ export const SCHEMA_VERSIONS = [
|
|||
updateToSchemaVersion1200,
|
||||
updateToSchemaVersion1210,
|
||||
updateToSchemaVersion1220,
|
||||
updateToSchemaVersion1230,
|
||||
];
|
||||
|
||||
export class DBVersionFromFutureError extends Error {
|
||||
|
|
|
@ -20,7 +20,7 @@ import type { ReadableDB, WritableDB } from '../Interface';
|
|||
import { prepare } from '../Server';
|
||||
import { sql } from '../util';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { CallStatusValue } from '../../types/CallDisposition';
|
||||
import { CallStatusValue, DirectCallStatus } from '../../types/CallDisposition';
|
||||
import { parseStrict, parseUnknown } from '../../util/schemas';
|
||||
|
||||
export function callLinkExists(db: ReadableDB, roomId: string): boolean {
|
||||
|
@ -190,7 +190,10 @@ function assertRoomIdMatchesRootKey(roomId: string, rootKey: string): void {
|
|||
);
|
||||
}
|
||||
|
||||
function deleteCallHistoryByRoomId(db: WritableDB, roomId: string) {
|
||||
export function deleteCallHistoryByRoomId(
|
||||
db: WritableDB,
|
||||
roomId: string
|
||||
): void {
|
||||
const [
|
||||
markCallHistoryDeleteByPeerIdQuery,
|
||||
markCallHistoryDeleteByPeerIdParams,
|
||||
|
@ -222,17 +225,14 @@ export function deleteCallLinkFromSync(db: WritableDB, roomId: string): void {
|
|||
})();
|
||||
}
|
||||
|
||||
export type DeleteCallLinkOptions = {
|
||||
storageNeedsSync: boolean;
|
||||
deletedAt?: number;
|
||||
};
|
||||
|
||||
export function beginDeleteCallLink(
|
||||
db: WritableDB,
|
||||
roomId: string,
|
||||
options: DeleteCallLinkOptions
|
||||
): void {
|
||||
db.transaction(() => {
|
||||
/**
|
||||
* Deletes a non-admin call link from the local database, or if it's an admin call link,
|
||||
* then marks it for deletion and storage sync.
|
||||
*
|
||||
* @returns boolean: True if storage sync is needed; False if not
|
||||
*/
|
||||
export function beginDeleteCallLink(db: WritableDB, roomId: string): boolean {
|
||||
return db.transaction(() => {
|
||||
// If adminKey is null, then we should delete the call link
|
||||
const [deleteNonAdminCallLinksQuery, deleteNonAdminCallLinksParams] = sql`
|
||||
DELETE FROM callLinks
|
||||
|
@ -244,35 +244,62 @@ export function beginDeleteCallLink(
|
|||
.prepare(deleteNonAdminCallLinksQuery)
|
||||
.run(deleteNonAdminCallLinksParams);
|
||||
|
||||
// Skip this query if the call is already deleted
|
||||
if (result.changes === 0) {
|
||||
const { storageNeedsSync } = options;
|
||||
const deletedAt = options.deletedAt ?? new Date().getTime();
|
||||
|
||||
// If the admin key is not null, we should mark it for deletion
|
||||
const [markAdminCallLinksDeletedQuery, markAdminCallLinksDeletedParams] =
|
||||
sql`
|
||||
UPDATE callLinks
|
||||
SET
|
||||
deleted = 1,
|
||||
deletedAt = ${deletedAt},
|
||||
storageNeedsSync = ${storageNeedsSync ? 1 : 0}
|
||||
WHERE adminKey IS NOT NULL
|
||||
AND roomId = ${roomId};
|
||||
`;
|
||||
|
||||
db.prepare(markAdminCallLinksDeletedQuery).run(
|
||||
markAdminCallLinksDeletedParams
|
||||
);
|
||||
// If we successfully deleted the call link, then it was a non-admin call link
|
||||
// and we're done
|
||||
if (result.changes !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
deleteCallHistoryByRoomId(db, roomId);
|
||||
const deletedAt = new Date().getTime();
|
||||
|
||||
// If the admin key is not null, we should mark it for deletion
|
||||
const [markAdminCallLinksDeletedQuery, markAdminCallLinksDeletedParams] =
|
||||
sql`
|
||||
UPDATE callLinks
|
||||
SET
|
||||
deleted = 1,
|
||||
deletedAt = ${deletedAt},
|
||||
storageNeedsSync = 1
|
||||
WHERE adminKey IS NOT NULL
|
||||
AND deleted IS NOT 1
|
||||
AND roomId = ${roomId};
|
||||
`;
|
||||
|
||||
const deleteAdminLinkResult = db
|
||||
.prepare(markAdminCallLinksDeletedQuery)
|
||||
.run(markAdminCallLinksDeletedParams);
|
||||
return deleteAdminLinkResult.changes > 0;
|
||||
})();
|
||||
}
|
||||
|
||||
export function beginDeleteAllCallLinks(db: WritableDB): void {
|
||||
const deletedAt = new Date().getTime();
|
||||
export function deleteCallLinkAndHistory(db: WritableDB, roomId: string): void {
|
||||
db.transaction(() => {
|
||||
const [deleteCallLinkQuery, deleteCallLinkParams] = sql`
|
||||
DELETE FROM callLinks
|
||||
WHERE roomId = ${roomId};
|
||||
`;
|
||||
db.prepare(deleteCallLinkQuery).run(deleteCallLinkParams);
|
||||
|
||||
const [deleteCallHistoryQuery, clearCallHistoryParams] = sql`
|
||||
UPDATE callsHistory
|
||||
SET
|
||||
status = ${DirectCallStatus.Deleted},
|
||||
timestamp = ${Date.now()}
|
||||
WHERE peerId = ${roomId};
|
||||
`;
|
||||
db.prepare(deleteCallHistoryQuery).run(clearCallHistoryParams);
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all non-admin call link from the local database, and marks all admin call links
|
||||
* for deletion and storage sync.
|
||||
*
|
||||
* @returns boolean: True if storage sync is needed; False if not
|
||||
*/
|
||||
export function beginDeleteAllCallLinks(db: WritableDB): boolean {
|
||||
const deletedAt = new Date().getTime();
|
||||
return db.transaction(() => {
|
||||
const [markAdminCallLinksDeletedQuery, markAdminCallLinksDeletedParams] =
|
||||
sql`
|
||||
UPDATE callLinks
|
||||
|
@ -280,12 +307,13 @@ export function beginDeleteAllCallLinks(db: WritableDB): void {
|
|||
deleted = 1,
|
||||
deletedAt = ${deletedAt},
|
||||
storageNeedsSync = 1
|
||||
WHERE adminKey IS NOT NULL;
|
||||
WHERE adminKey IS NOT NULL
|
||||
AND deleted IS NOT 1;
|
||||
`;
|
||||
|
||||
db.prepare(markAdminCallLinksDeletedQuery).run(
|
||||
markAdminCallLinksDeletedParams
|
||||
);
|
||||
const markAdminCallLinksDeletedResult = db
|
||||
.prepare(markAdminCallLinksDeletedQuery)
|
||||
.run(markAdminCallLinksDeletedParams);
|
||||
|
||||
const [deleteNonAdminCallLinksQuery] = sql`
|
||||
DELETE FROM callLinks
|
||||
|
@ -293,6 +321,9 @@ export function beginDeleteAllCallLinks(db: WritableDB): void {
|
|||
`;
|
||||
|
||||
db.prepare(deleteNonAdminCallLinksQuery).run();
|
||||
|
||||
// If admin call links were marked deleted, then storage will need sync
|
||||
return markAdminCallLinksDeletedResult.changes > 0;
|
||||
})();
|
||||
}
|
||||
|
||||
|
@ -311,6 +342,14 @@ export function getAllCallLinkRecordsWithAdminKey(
|
|||
.map((item: unknown) => parseUnknown(callLinkRecordSchema, item));
|
||||
}
|
||||
|
||||
export function getAllAdminCallLinks(
|
||||
db: ReadableDB
|
||||
): ReadonlyArray<CallLinkType> {
|
||||
return getAllCallLinkRecordsWithAdminKey(db).map((record: CallLinkRecord) =>
|
||||
callLinkFromRecord(record)
|
||||
);
|
||||
}
|
||||
|
||||
export function getAllMarkedDeletedCallLinkRoomIds(
|
||||
db: ReadableDB
|
||||
): ReadonlyArray<string> {
|
||||
|
|
|
@ -15,7 +15,10 @@ import type { ToastActionType } from './toast';
|
|||
import { showToast } from './toast';
|
||||
import { DataReader, DataWriter } from '../../sql/Client';
|
||||
import { ToastType } from '../../types/Toast';
|
||||
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||
import {
|
||||
ClearCallHistoryResult,
|
||||
type CallHistoryDetails,
|
||||
} from '../../types/CallDisposition';
|
||||
import * as log from '../../logging/log';
|
||||
import * as Errors from '../../types/errors';
|
||||
import { drop } from '../../util/drop';
|
||||
|
@ -29,6 +32,11 @@ import {
|
|||
loadCallHistory,
|
||||
} from '../../services/callHistoryLoader';
|
||||
import { makeLookup } from '../../util/makeLookup';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { ButtonVariant } from '../../components/Button';
|
||||
import type { ShowErrorModalActionType } from './globalModals';
|
||||
import { SHOW_ERROR_MODAL } from './globalModals';
|
||||
|
||||
export type CallHistoryState = ReadonlyDeep<{
|
||||
// This informs the app that underlying call history data has changed.
|
||||
|
@ -191,14 +199,42 @@ function clearAllCallHistory(): ThunkAction<
|
|||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
CallHistoryReset | ToastActionType
|
||||
CallHistoryReset | ToastActionType | ShowErrorModalActionType
|
||||
> {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
const latestCall = getCallHistoryLatestCall(getState());
|
||||
if (latestCall != null) {
|
||||
await clearCallHistoryDataAndSync(latestCall);
|
||||
if (latestCall == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await clearCallHistoryDataAndSync(latestCall);
|
||||
if (result === ClearCallHistoryResult.Success) {
|
||||
dispatch(showToast({ toastType: ToastType.CallHistoryCleared }));
|
||||
} else if (result === ClearCallHistoryResult.Error) {
|
||||
const i18n = getIntl(getState());
|
||||
dispatch({
|
||||
type: SHOW_ERROR_MODAL,
|
||||
payload: {
|
||||
title: null,
|
||||
description: i18n('icu:CallsTab__ClearCallHistoryError'),
|
||||
buttonVariant: ButtonVariant.Primary,
|
||||
},
|
||||
});
|
||||
} else if (result === ClearCallHistoryResult.ErrorDeletingCallLinks) {
|
||||
const i18n = getIntl(getState());
|
||||
dispatch({
|
||||
type: SHOW_ERROR_MODAL,
|
||||
payload: {
|
||||
title: null,
|
||||
description: i18n(
|
||||
'icu:CallsTab__ClearCallHistoryError--call-links'
|
||||
),
|
||||
buttonVariant: ButtonVariant.Primary,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw missingCaseError(result);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Error clearing call history', Errors.toLogFormat(error));
|
||||
|
|
|
@ -36,10 +36,11 @@ import type {
|
|||
PresentedSource,
|
||||
PresentableSource,
|
||||
} from '../../types/Calling';
|
||||
import type {
|
||||
CallLinkRestrictions,
|
||||
CallLinkStateType,
|
||||
CallLinkType,
|
||||
import {
|
||||
isCallLinkAdmin,
|
||||
type CallLinkRestrictions,
|
||||
type CallLinkStateType,
|
||||
type CallLinkType,
|
||||
} from '../../types/CallLink';
|
||||
import {
|
||||
CALLING_REACTIONS_LIFETIME,
|
||||
|
@ -110,7 +111,7 @@ import {
|
|||
getPresentingSource,
|
||||
} from '../selectors/calling';
|
||||
import { storageServiceUploadJob } from '../../services/storage';
|
||||
import { CallLinkDeleteManager } from '../../jobs/CallLinkDeleteManager';
|
||||
import { CallLinkFinalizeDeleteManager } from '../../jobs/CallLinkFinalizeDeleteManager';
|
||||
import { callLinkRefreshJobQueue } from '../../jobs/callLinkRefreshJobQueue';
|
||||
|
||||
// State
|
||||
|
@ -2125,13 +2126,50 @@ function createCallLink(
|
|||
|
||||
function deleteCallLink(
|
||||
roomId: string
|
||||
): ThunkAction<void, RootStateType, unknown, HandleCallLinkDeleteActionType> {
|
||||
return async dispatch => {
|
||||
await DataWriter.beginDeleteCallLink(roomId, { storageNeedsSync: true });
|
||||
storageServiceUploadJob({ reason: 'deleteCallLink' });
|
||||
// Wait for storage service sync before finalizing delete
|
||||
drop(CallLinkDeleteManager.addJob({ roomId }, { delay: 10000 }));
|
||||
dispatch(handleCallLinkDelete({ roomId }));
|
||||
): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
HandleCallLinkDeleteActionType | ShowErrorModalActionType
|
||||
> {
|
||||
return async (dispatch, getState) => {
|
||||
const callLink = await DataReader.getCallLinkByRoomId(roomId);
|
||||
if (!callLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isStorageSyncNeeded = await DataWriter.beginDeleteCallLink(roomId);
|
||||
if (isStorageSyncNeeded) {
|
||||
storageServiceUploadJob({ reason: 'deleteCallLink' });
|
||||
}
|
||||
try {
|
||||
if (isCallLinkAdmin(callLink)) {
|
||||
// This throws if call link is active or network is unavailable.
|
||||
await calling.deleteCallLink(callLink);
|
||||
// Wait for storage service sync before finalizing delete.
|
||||
drop(
|
||||
CallLinkFinalizeDeleteManager.addJob(
|
||||
{ roomId: callLink.roomId },
|
||||
{ delay: 10000 }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await DataWriter.deleteCallHistoryByRoomId(callLink.roomId);
|
||||
dispatch(handleCallLinkDelete({ roomId }));
|
||||
} catch (error) {
|
||||
log.warn('clearCallHistory: Failed to delete call link', error);
|
||||
|
||||
const i18n = getIntl(getState());
|
||||
dispatch({
|
||||
type: SHOW_ERROR_MODAL,
|
||||
payload: {
|
||||
title: null,
|
||||
description: i18n('icu:calling__call-link-delete-failed'),
|
||||
buttonVariant: ButtonVariant.Primary,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -3985,5 +4023,14 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === HANDLE_CALL_LINK_DELETE) {
|
||||
const { roomId } = action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
callLinks: omit(state.callLinks, roomId),
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ export type GlobalModalsStateType = ReadonlyDeep<{
|
|||
errorModalProps?: {
|
||||
buttonVariant?: ButtonVariant;
|
||||
description?: string;
|
||||
title?: string;
|
||||
title?: string | null;
|
||||
};
|
||||
forwardMessagesProps?: ForwardMessagesPropsType;
|
||||
gv2MigrationProps?: MigrateToGV2PropsType;
|
||||
|
@ -338,7 +338,7 @@ export type ShowErrorModalActionType = ReadonlyDeep<{
|
|||
payload: {
|
||||
buttonVariant?: ButtonVariant;
|
||||
description?: string;
|
||||
title?: string;
|
||||
title?: string | null;
|
||||
};
|
||||
}>;
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import type {
|
|||
import { getIncomingCall as getIncomingCallHelper } from '../ducks/callingHelpers';
|
||||
import type { PresentedSource } from '../../types/Calling';
|
||||
import { CallMode } from '../../types/CallDisposition';
|
||||
import type { CallLinkType } from '../../types/CallLink';
|
||||
import { isCallLinkAdmin, type CallLinkType } from '../../types/CallLink';
|
||||
import { getUserACI } from './user';
|
||||
import { getOwn } from '../../util/getOwn';
|
||||
import type { AciString } from '../../types/ServiceId';
|
||||
|
@ -96,6 +96,11 @@ export const getAllCallLinks = createSelector(
|
|||
(lookup): Array<CallLinkType> => Object.values(lookup)
|
||||
);
|
||||
|
||||
export const getHasAnyAdminCallLinks = createSelector(
|
||||
getAllCallLinks,
|
||||
(callLinks): boolean => callLinks.some(callLink => isCallLinkAdmin(callLink))
|
||||
);
|
||||
|
||||
export type CallSelectorType = (
|
||||
conversationId: string
|
||||
) => CallStateType | undefined;
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
getAllCallLinks,
|
||||
getCallSelector,
|
||||
getCallLinkSelector,
|
||||
getHasAnyAdminCallLinks,
|
||||
} from '../selectors/calling';
|
||||
import { useCallHistoryActions } from '../ducks/callHistory';
|
||||
import { getCallHistoryEdition } from '../selectors/callHistory';
|
||||
|
@ -151,6 +152,7 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
|||
const getAdhocCall = useSelector(getAdhocCallSelector);
|
||||
const getCall = useSelector(getCallSelector);
|
||||
const getCallLink = useSelector(getCallLinkSelector);
|
||||
const hasAnyAdminCallLinks = useSelector(getHasAnyAdminCallLinks);
|
||||
|
||||
const activeCall = useSelector(getActiveCallState);
|
||||
const callHistoryEdition = useSelector(getCallHistoryEdition);
|
||||
|
@ -242,6 +244,7 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
|||
callHistoryEdition={callHistoryEdition}
|
||||
canCreateCallLinks={canCreateCallLinks}
|
||||
hangUpActiveCall={hangUpActiveCall}
|
||||
hasAnyAdminCallLinks={hasAnyAdminCallLinks}
|
||||
hasFailedStorySends={hasFailedStorySends}
|
||||
hasPendingUpdate={hasPendingUpdate}
|
||||
i18n={i18n}
|
||||
|
|
|
@ -174,7 +174,7 @@ export const SmartGlobalModalContainer = memo(
|
|||
}: {
|
||||
buttonVariant?: ButtonVariant;
|
||||
description?: string;
|
||||
title?: string;
|
||||
title?: string | null;
|
||||
}) => (
|
||||
<ErrorModal
|
||||
buttonVariant={buttonVariant}
|
||||
|
|
|
@ -16,6 +16,7 @@ import { generateAci } from '../../../types/ServiceId';
|
|||
import {
|
||||
getCallsByConversation,
|
||||
getCallSelector,
|
||||
getHasAnyAdminCallLinks,
|
||||
getIncomingCall,
|
||||
isInCall,
|
||||
} from '../../../state/selectors/calling';
|
||||
|
@ -25,6 +26,10 @@ import type {
|
|||
GroupCallStateType,
|
||||
} from '../../../state/ducks/calling';
|
||||
import { getEmptyState } from '../../../state/ducks/calling';
|
||||
import {
|
||||
FAKE_CALL_LINK,
|
||||
FAKE_CALL_LINK_WITH_ADMIN_KEY,
|
||||
} from '../../../test-both/helpers/fakeCallLink';
|
||||
|
||||
const OUR_ACI = generateAci();
|
||||
const ACI_1 = generateAci();
|
||||
|
@ -118,6 +123,20 @@ describe('state/selectors/calling', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const stateWithCallLink: CallingStateType = {
|
||||
...getEmptyState(),
|
||||
callLinks: {
|
||||
[FAKE_CALL_LINK.roomId]: FAKE_CALL_LINK,
|
||||
},
|
||||
};
|
||||
|
||||
const stateWithAdminCallLink: CallingStateType = {
|
||||
...getEmptyState(),
|
||||
callLinks: {
|
||||
[FAKE_CALL_LINK_WITH_ADMIN_KEY.roomId]: FAKE_CALL_LINK_WITH_ADMIN_KEY,
|
||||
},
|
||||
};
|
||||
|
||||
describe('getCallsByConversation', () => {
|
||||
it('returns state.calling.callsByConversation', () => {
|
||||
assert.deepEqual(getCallsByConversation(getEmptyRootState()), {});
|
||||
|
@ -217,4 +236,22 @@ describe('state/selectors/calling', () => {
|
|||
assert.isTrue(isInCall(getCallingState(stateWithActiveDirectCall)));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHasAnyAdminCallLinks', () => {
|
||||
it('returns true with admin call links', () => {
|
||||
assert.isTrue(
|
||||
getHasAnyAdminCallLinks(getCallingState(stateWithAdminCallLink))
|
||||
);
|
||||
});
|
||||
|
||||
it('returns false with only non-admin call links', () => {
|
||||
assert.isFalse(
|
||||
getHasAnyAdminCallLinks(getCallingState(stateWithCallLink))
|
||||
);
|
||||
});
|
||||
|
||||
it('returns false without any call links', () => {
|
||||
assert.isFalse(getHasAnyAdminCallLinks(getEmptyRootState()));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -99,6 +99,7 @@ import {
|
|||
} from '../types/CallDisposition';
|
||||
import {
|
||||
getBytesForPeerId,
|
||||
getCallIdForProto,
|
||||
getProtoForCallHistory,
|
||||
} from '../util/callDisposition';
|
||||
import { MAX_MESSAGE_COUNT } from '../util/deleteForMe.types';
|
||||
|
@ -1612,7 +1613,7 @@ export default class MessageSender {
|
|||
type: Proto.SyncMessage.CallLogEvent.Type.CLEAR,
|
||||
timestamp: Long.fromNumber(latestCall.timestamp),
|
||||
peerId: getBytesForPeerId(latestCall),
|
||||
callId: Long.fromString(latestCall.callId),
|
||||
callId: getCallIdForProto(latestCall),
|
||||
});
|
||||
|
||||
const syncMessage = MessageSender.createSyncMessage();
|
||||
|
|
|
@ -186,6 +186,12 @@ export type CallHistoryPagination = Readonly<{
|
|||
limit: number;
|
||||
}>;
|
||||
|
||||
export enum ClearCallHistoryResult {
|
||||
Success = 'Success',
|
||||
Error = 'Error',
|
||||
ErrorDeletingCallLinks = 'ErrorDeletingCallLinks',
|
||||
}
|
||||
|
||||
const ringerIdSchema = z.union([aciSchema, z.string(), z.null()]);
|
||||
|
||||
const callModeSchema = z.nativeEnum(CallMode);
|
||||
|
|
|
@ -35,6 +35,7 @@ import {
|
|||
CallStatusValue,
|
||||
callLogEventNormalizeSchema,
|
||||
CallLogEvent,
|
||||
ClearCallHistoryResult,
|
||||
} from '../types/CallDisposition';
|
||||
import type { AciString } from '../types/ServiceId';
|
||||
import { isAciString } from './isAciString';
|
||||
|
@ -67,8 +68,9 @@ import type { ConversationModel } from '../models/conversations';
|
|||
import { drop } from './drop';
|
||||
import { sendCallLinkUpdateSync } from './sendCallLinkUpdateSync';
|
||||
import { storageServiceUploadJob } from '../services/storage';
|
||||
import { CallLinkDeleteManager } from '../jobs/CallLinkDeleteManager';
|
||||
import { CallLinkFinalizeDeleteManager } from '../jobs/CallLinkFinalizeDeleteManager';
|
||||
import { parsePartial, parseStrict } from './schemas';
|
||||
import { calling } from '../services/calling';
|
||||
|
||||
// utils
|
||||
// -----
|
||||
|
@ -347,6 +349,23 @@ export function getBytesForPeerId(callHistory: CallHistoryDetails): Uint8Array {
|
|||
return peerId;
|
||||
}
|
||||
|
||||
export function getCallIdForProto(
|
||||
callHistory: CallHistoryDetails
|
||||
): Long.Long | undefined {
|
||||
try {
|
||||
return Long.fromString(callHistory.callId);
|
||||
} catch (error) {
|
||||
// When CallHistory is a placeholder record for call links, then the history item's
|
||||
// callId is invalid. We will ignore it and only send the timestamp.
|
||||
if (callHistory.mode === CallMode.Adhoc) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// For other calls, we expect a valid callId.
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function getProtoForCallHistory(
|
||||
callHistory: CallHistoryDetails
|
||||
): Proto.SyncMessage.ICallEvent | null {
|
||||
|
@ -361,7 +380,7 @@ export function getProtoForCallHistory(
|
|||
|
||||
return new Proto.SyncMessage.CallEvent({
|
||||
peerId: getBytesForPeerId(callHistory),
|
||||
callId: Long.fromString(callHistory.callId),
|
||||
callId: getCallIdForProto(callHistory),
|
||||
type: typeToProto[callHistory.type],
|
||||
direction: directionToProto[callHistory.direction],
|
||||
event,
|
||||
|
@ -1343,24 +1362,63 @@ export function updateDeletedMessages(messageIds: ReadonlyArray<string>): void {
|
|||
|
||||
export async function clearCallHistoryDataAndSync(
|
||||
latestCall: CallHistoryDetails
|
||||
): Promise<void> {
|
||||
): Promise<ClearCallHistoryResult> {
|
||||
try {
|
||||
log.info(
|
||||
`clearCallHistory: Clearing call history before (${latestCall.callId}, ${latestCall.timestamp})`
|
||||
);
|
||||
// This skips call history for admin call links.
|
||||
const messageIds = await DataWriter.clearCallHistory(latestCall);
|
||||
await DataWriter.beginDeleteAllCallLinks();
|
||||
storageServiceUploadJob({ reason: 'clearCallHistoryDataAndSync' });
|
||||
// Wait for storage sync before finalizing delete
|
||||
drop(CallLinkDeleteManager.enqueueAllDeletedCallLinks({ delay: 10000 }));
|
||||
const isStorageSyncNeeded = await DataWriter.beginDeleteAllCallLinks();
|
||||
if (isStorageSyncNeeded) {
|
||||
storageServiceUploadJob({ reason: 'clearCallHistoryDataAndSync' });
|
||||
}
|
||||
updateDeletedMessages(messageIds);
|
||||
log.info('clearCallHistory: Queueing sync message');
|
||||
await singleProtoJobQueue.add(
|
||||
MessageSender.getClearCallHistoryMessage(latestCall)
|
||||
);
|
||||
|
||||
const adminCallLinks = await DataReader.getAllAdminCallLinks();
|
||||
const callLinkCount = adminCallLinks.length;
|
||||
if (callLinkCount > 0) {
|
||||
log.info(`clearCallHistory: Deleting ${callLinkCount} admin call links`);
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
for (const callLink of adminCallLinks) {
|
||||
try {
|
||||
// This throws if call link is active or network is unavailable.
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await calling.deleteCallLink(callLink);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await DataWriter.deleteCallHistoryByRoomId(callLink.roomId);
|
||||
// Wait for storage service sync before finalizing delete.
|
||||
drop(
|
||||
CallLinkFinalizeDeleteManager.addJob(
|
||||
{ roomId: callLink.roomId },
|
||||
{ delay: 10000 }
|
||||
)
|
||||
);
|
||||
successCount += 1;
|
||||
} catch (error) {
|
||||
log.warn('clearCallHistory: Failed to delete admin call link', error);
|
||||
failCount += 1;
|
||||
}
|
||||
}
|
||||
log.info(
|
||||
`clearCallHistory: Deleted admin call links, success=${successCount} failed=${failCount}`
|
||||
);
|
||||
|
||||
if (failCount > 0) {
|
||||
return ClearCallHistoryResult.ErrorDeletingCallLinks;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('clearCallHistory: Failed to clear call history', error);
|
||||
return ClearCallHistoryResult.Error;
|
||||
}
|
||||
|
||||
return ClearCallHistoryResult.Success;
|
||||
}
|
||||
|
||||
export async function markAllCallHistoryReadAndSync(
|
||||
|
|
Loading…
Reference in a new issue