Show progress dialog during delete

This commit is contained in:
Scott Nonnenberg 2021-01-12 16:42:15 -08:00 committed by GitHub
parent 8c25ffd6f5
commit 8116a8561d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 135 additions and 130 deletions

View file

@ -38,6 +38,7 @@ describe('KeyChangeListener', () => {
after(async () => { after(async () => {
await window.Signal.Data.removeAllMessagesInConversation(convo.id, { await window.Signal.Data.removeAllMessagesInConversation(convo.id, {
logId: phoneNumberWithKeyChange,
MessageCollection: Whisper.MessageCollection, MessageCollection: Whisper.MessageCollection,
}); });
await window.Signal.Data.removeConversation(convo.id, { await window.Signal.Data.removeConversation(convo.id, {
@ -78,6 +79,7 @@ describe('KeyChangeListener', () => {
}); });
after(async () => { after(async () => {
await window.Signal.Data.removeAllMessagesInConversation(groupConvo.id, { await window.Signal.Data.removeAllMessagesInConversation(groupConvo.id, {
logId: phoneNumberWithKeyChange,
MessageCollection: Whisper.MessageCollection, MessageCollection: Whisper.MessageCollection,
}); });
await window.Signal.Data.removeConversation(groupConvo.id, { await window.Signal.Data.removeConversation(groupConvo.id, {

View file

@ -240,7 +240,7 @@ type WhatIsThis = import('./window.d').WhatIsThis;
if (_.isNumber(preMessageReceiverStatus)) { if (_.isNumber(preMessageReceiverStatus)) {
return preMessageReceiverStatus; return preMessageReceiverStatus;
} }
return -1; return WebSocket.CLOSED;
}; };
window.Whisper.events = _.clone(window.Backbone.Events); window.Whisper.events = _.clone(window.Backbone.Events);
let accountManager: typeof window.textsecure.AccountManager; let accountManager: typeof window.textsecure.AccountManager;
@ -1604,7 +1604,17 @@ type WhatIsThis = import('./window.d').WhatIsThis;
// Maybe refresh remote configuration when we become active // Maybe refresh remote configuration when we become active
window.registerForActive(async () => { window.registerForActive(async () => {
await window.Signal.RemoteConfig.maybeRefreshRemoteConfig(); try {
await window.Signal.RemoteConfig.maybeRefreshRemoteConfig();
} catch (error) {
if (error && window._.isNumber(error.code)) {
window.log.warn(
`registerForActive: Failed to to refresh remote config. Code: ${error.code}`
);
return;
}
throw error;
}
}); });
// Listen for changes to the `desktop.clientExpiration` remote flag // Listen for changes to the `desktop.clientExpiration` remote flag

View file

@ -4013,6 +4013,7 @@ export class ConversationModel extends window.Backbone.Model<
window.Signal.Data.updateConversation(this.attributes); window.Signal.Data.updateConversation(this.attributes);
await window.Signal.Data.removeAllMessagesInConversation(this.id, { await window.Signal.Data.removeAllMessagesInConversation(this.id, {
logId: this.idForLogging(),
MessageCollection: window.Whisper.MessageCollection, MessageCollection: window.Whisper.MessageCollection,
}); });
} }

View file

@ -150,6 +150,7 @@ const dataInterface: ClientInterface = {
saveMessage, saveMessage,
saveMessages, saveMessages,
removeMessage, removeMessage,
removeMessages,
getUnreadByConversation, getUnreadByConversation,
getMessageBySender, getMessageBySender,
@ -225,7 +226,6 @@ const dataInterface: ClientInterface = {
// Client-side only, and test-only // Client-side only, and test-only
_removeConversations, _removeConversations,
_removeMessages,
_cleanData, _cleanData,
_jobs, _jobs,
}; };
@ -903,8 +903,8 @@ async function removeMessage(
} }
// Note: this method will not clean up external files, just delete from SQL // Note: this method will not clean up external files, just delete from SQL
async function _removeMessages(ids: Array<string>) { async function removeMessages(ids: Array<string>) {
await channels.removeMessage(ids); await channels.removeMessages(ids);
} }
async function getMessageById( async function getMessageById(
@ -1074,15 +1074,23 @@ async function migrateConversationMessages(
async function removeAllMessagesInConversation( async function removeAllMessagesInConversation(
conversationId: string, conversationId: string,
{ {
logId,
MessageCollection, MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType } }: {
logId: string;
MessageCollection: typeof MessageModelCollectionType;
}
) { ) {
let messages; let messages;
do { do {
// Yes, we really want the await in the loop. We're deleting 100 at a const chunkSize = 20;
window.log.info(
`removeAllMessagesInConversation/${logId}: Fetching chunk of ${chunkSize} messages`
);
// Yes, we really want the await in the loop. We're deleting a chunk at a
// time so we don't use too much memory. // time so we don't use too much memory.
messages = await getOlderMessagesByConversation(conversationId, { messages = await getOlderMessagesByConversation(conversationId, {
limit: 100, limit: chunkSize,
MessageCollection, MessageCollection,
}); });
@ -1092,13 +1100,17 @@ async function removeAllMessagesInConversation(
const ids = messages.map((message: MessageModel) => message.id); const ids = messages.map((message: MessageModel) => message.id);
window.log.info(`removeAllMessagesInConversation/${logId}: Cleanup...`);
// Note: It's very important that these models are fully hydrated because // Note: It's very important that these models are fully hydrated because
// we need to delete all associated on-disk files along with the database delete. // we need to delete all associated on-disk files along with the database delete.
await Promise.all( const queue = new window.PQueue({ concurrency: 3, timeout: 1000 * 60 * 2 });
messages.map(async (message: MessageModel) => message.cleanup()) queue.addAll(
messages.map((message: MessageModel) => async () => message.cleanup())
); );
await queue.onIdle();
await channels.removeMessage(ids); window.log.info(`removeAllMessagesInConversation/${logId}: Deleting...`);
await channels.removeMessages(ids);
} while (messages.length > 0); } while (messages.length > 0);
} }

View file

@ -238,7 +238,8 @@ export type ServerInterface = DataInterface & {
conversationId: string conversationId: string
) => Promise<Array<MessageType>>; ) => Promise<Array<MessageType>>;
removeConversation: (id: Array<string> | string) => Promise<void>; removeConversation: (id: Array<string> | string) => Promise<void>;
removeMessage: (id: Array<string> | string) => Promise<void>; removeMessage: (id: string) => Promise<void>;
removeMessages: (ids: Array<string>) => Promise<void>;
saveMessage: ( saveMessage: (
data: MessageType, data: MessageType,
options: { forceSave?: boolean } options: { forceSave?: boolean }
@ -266,51 +267,41 @@ export type ServerInterface = DataInterface & {
}; };
export type ClientInterface = DataInterface & { export type ClientInterface = DataInterface & {
getAllConversations: ({ getAllConversations: (options: {
ConversationCollection,
}: {
ConversationCollection: typeof ConversationModelCollectionType; ConversationCollection: typeof ConversationModelCollectionType;
}) => Promise<ConversationModelCollectionType>; }) => Promise<ConversationModelCollectionType>;
getAllGroupsInvolvingId: ( getAllGroupsInvolvingId: (
id: string, id: string,
{ options: {
ConversationCollection,
}: {
ConversationCollection: typeof ConversationModelCollectionType; ConversationCollection: typeof ConversationModelCollectionType;
} }
) => Promise<ConversationModelCollectionType>; ) => Promise<ConversationModelCollectionType>;
getAllPrivateConversations: ({ getAllPrivateConversations: (options: {
ConversationCollection,
}: {
ConversationCollection: typeof ConversationModelCollectionType; ConversationCollection: typeof ConversationModelCollectionType;
}) => Promise<ConversationModelCollectionType>; }) => Promise<ConversationModelCollectionType>;
getConversationById: ( getConversationById: (
id: string, id: string,
{ Conversation }: { Conversation: typeof ConversationModel } options: { Conversation: typeof ConversationModel }
) => Promise<ConversationModel>; ) => Promise<ConversationModel>;
getExpiredMessages: ({ getExpiredMessages: (options: {
MessageCollection,
}: {
MessageCollection: typeof MessageModelCollectionType; MessageCollection: typeof MessageModelCollectionType;
}) => Promise<MessageModelCollectionType>; }) => Promise<MessageModelCollectionType>;
getMessageById: ( getMessageById: (
id: string, id: string,
{ Message }: { Message: typeof MessageModel } options: { Message: typeof MessageModel }
) => Promise<MessageType | undefined>; ) => Promise<MessageType | undefined>;
getMessageBySender: ( getMessageBySender: (
options: { data: {
source: string; source: string;
sourceUuid: string; sourceUuid: string;
sourceDevice: string; sourceDevice: string;
sent_at: number; sent_at: number;
}, },
{ Message }: { Message: typeof MessageModel } options: { Message: typeof MessageModel }
) => Promise<MessageModel | null>; ) => Promise<MessageModel | null>;
getMessagesBySentAt: ( getMessagesBySentAt: (
sentAt: number, sentAt: number,
{ options: { MessageCollection: typeof MessageModelCollectionType }
MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType }
) => Promise<MessageModelCollectionType>; ) => Promise<MessageModelCollectionType>;
getOlderMessagesByConversation: ( getOlderMessagesByConversation: (
conversationId: string, conversationId: string,
@ -341,39 +332,33 @@ export type ClientInterface = DataInterface & {
Message: typeof MessageModel; Message: typeof MessageModel;
} }
) => Promise<MessageModel | undefined>; ) => Promise<MessageModel | undefined>;
getNextExpiringMessage: ({ getNextExpiringMessage: (options: {
Message,
}: {
Message: typeof MessageModel; Message: typeof MessageModel;
}) => Promise<MessageModel | null>; }) => Promise<MessageModel | null>;
getNextTapToViewMessageToAgeOut: ({ getNextTapToViewMessageToAgeOut: (options: {
Message,
}: {
Message: typeof MessageModel; Message: typeof MessageModel;
}) => Promise<MessageModel | null>; }) => Promise<MessageModel | null>;
getOutgoingWithoutExpiresAt: ({ getOutgoingWithoutExpiresAt: (options: {
MessageCollection,
}: {
MessageCollection: typeof MessageModelCollectionType; MessageCollection: typeof MessageModelCollectionType;
}) => Promise<MessageModelCollectionType>; }) => Promise<MessageModelCollectionType>;
getTapToViewMessagesNeedingErase: ({ getTapToViewMessagesNeedingErase: (options: {
MessageCollection,
}: {
MessageCollection: typeof MessageModelCollectionType; MessageCollection: typeof MessageModelCollectionType;
}) => Promise<MessageModelCollectionType>; }) => Promise<MessageModelCollectionType>;
getUnreadByConversation: ( getUnreadByConversation: (
conversationId: string, conversationId: string,
{ options: { MessageCollection: typeof MessageModelCollectionType }
MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType }
) => Promise<MessageModelCollectionType>; ) => Promise<MessageModelCollectionType>;
removeConversation: ( removeConversation: (
id: string, id: string,
{ Conversation }: { Conversation: typeof ConversationModel } options: { Conversation: typeof ConversationModel }
) => Promise<void>; ) => Promise<void>;
removeMessage: ( removeMessage: (
id: string, id: string,
{ Message }: { Message: typeof MessageModel } options: { Message: typeof MessageModel }
) => Promise<void>;
removeMessages: (
ids: Array<string>,
options: { Message: typeof MessageModel }
) => Promise<void>; ) => Promise<void>;
saveMessage: ( saveMessage: (
data: MessageType, data: MessageType,
@ -383,9 +368,7 @@ export type ClientInterface = DataInterface & {
// Test-only // Test-only
_getAllMessages: ({ _getAllMessages: (options: {
MessageCollection,
}: {
MessageCollection: typeof MessageModelCollectionType; MessageCollection: typeof MessageModelCollectionType;
}) => Promise<MessageModelCollectionType>; }) => Promise<MessageModelCollectionType>;
@ -394,9 +377,10 @@ export type ClientInterface = DataInterface & {
shutdown: () => Promise<void>; shutdown: () => Promise<void>;
removeAllMessagesInConversation: ( removeAllMessagesInConversation: (
conversationId: string, conversationId: string,
{ options: {
MessageCollection, logId: string;
}: { MessageCollection: typeof MessageModelCollectionType } MessageCollection: typeof MessageModelCollectionType;
}
) => Promise<void>; ) => Promise<void>;
removeOtherData: () => Promise<void>; removeOtherData: () => Promise<void>;
cleanupOrphanedAttachments: () => Promise<void>; cleanupOrphanedAttachments: () => Promise<void>;
@ -405,7 +389,6 @@ export type ClientInterface = DataInterface & {
// Client-side only, and test-only // Client-side only, and test-only
_removeConversations: (ids: Array<string>) => Promise<void>; _removeConversations: (ids: Array<string>) => Promise<void>;
_removeMessages: (ids: Array<string>) => Promise<void>;
_cleanData: (data: any, path?: string) => any; _cleanData: (data: any, path?: string) => any;
_jobs: { [id: string]: ClientJobType }; _jobs: { [id: string]: ClientJobType };
}; };

View file

@ -129,6 +129,7 @@ const dataInterface: ServerInterface = {
saveMessage, saveMessage,
saveMessages, saveMessages,
removeMessage, removeMessage,
removeMessages,
getUnreadByConversation, getUnreadByConversation,
getMessageBySender, getMessageBySender,
getMessageById, getMessageById,
@ -236,6 +237,7 @@ type PromisifiedSQLDatabase = {
statement: string, statement: string,
params?: { [key: string]: any } params?: { [key: string]: any }
) => Promise<Array<any>>; ) => Promise<Array<any>>;
on: (event: 'trace', handler: (sql: string) => void) => void;
}; };
function promisify(rawInstance: sql.Database): PromisifiedSQLDatabase { function promisify(rawInstance: sql.Database): PromisifiedSQLDatabase {
@ -244,6 +246,7 @@ function promisify(rawInstance: sql.Database): PromisifiedSQLDatabase {
run: pify(rawInstance.run.bind(rawInstance)), run: pify(rawInstance.run.bind(rawInstance)),
get: pify(rawInstance.get.bind(rawInstance)), get: pify(rawInstance.get.bind(rawInstance)),
all: pify(rawInstance.all.bind(rawInstance)), all: pify(rawInstance.all.bind(rawInstance)),
on: rawInstance.on.bind(rawInstance),
}; };
} }
@ -928,51 +931,51 @@ async function updateToSchemaVersion12(
try { try {
await instance.run(`CREATE TABLE sticker_packs( await instance.run(`CREATE TABLE sticker_packs(
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
key TEXT NOT NULL, key TEXT NOT NULL,
author STRING, author STRING,
coverStickerId INTEGER, coverStickerId INTEGER,
createdAt INTEGER, createdAt INTEGER,
downloadAttempts INTEGER, downloadAttempts INTEGER,
installedAt INTEGER, installedAt INTEGER,
lastUsed INTEGER, lastUsed INTEGER,
status STRING, status STRING,
stickerCount INTEGER, stickerCount INTEGER,
title STRING title STRING
);`); );`);
await instance.run(`CREATE TABLE stickers( await instance.run(`CREATE TABLE stickers(
id INTEGER NOT NULL, id INTEGER NOT NULL,
packId TEXT NOT NULL, packId TEXT NOT NULL,
emoji STRING, emoji STRING,
height INTEGER, height INTEGER,
isCoverOnly INTEGER, isCoverOnly INTEGER,
lastUsed INTEGER, lastUsed INTEGER,
path STRING, path STRING,
width INTEGER, width INTEGER,
PRIMARY KEY (id, packId), PRIMARY KEY (id, packId),
CONSTRAINT stickers_fk CONSTRAINT stickers_fk
FOREIGN KEY (packId) FOREIGN KEY (packId)
REFERENCES sticker_packs(id) REFERENCES sticker_packs(id)
ON DELETE CASCADE ON DELETE CASCADE
);`); );`);
await instance.run(`CREATE INDEX stickers_recents await instance.run(`CREATE INDEX stickers_recents
ON stickers ( ON stickers (
lastUsed lastUsed
) WHERE lastUsed IS NOT NULL;`); ) WHERE lastUsed IS NOT NULL;`);
await instance.run(`CREATE TABLE sticker_references( await instance.run(`CREATE TABLE sticker_references(
messageId STRING, messageId STRING,
packId TEXT, packId TEXT,
CONSTRAINT sticker_references_fk CONSTRAINT sticker_references_fk
FOREIGN KEY(packId) FOREIGN KEY(packId)
REFERENCES sticker_packs(id) REFERENCES sticker_packs(id)
ON DELETE CASCADE ON DELETE CASCADE
);`); );`);
await instance.run('PRAGMA user_version = 12;'); await instance.run('PRAGMA user_version = 12;');
await instance.run('COMMIT TRANSACTION;'); await instance.run('COMMIT TRANSACTION;');
@ -1685,24 +1688,26 @@ async function initialize({
try { try {
promisified = await openAndSetUpSQLCipher(databaseFilePath, { key }); promisified = await openAndSetUpSQLCipher(databaseFilePath, { key });
// promisified.on('trace', async statement => { // if (promisified) {
// if ( // promisified.on('trace', async statement => {
// !globalInstance || // if (
// statement.startsWith('--') || // !globalInstance ||
// statement.includes('COMMIT') || // statement.startsWith('--') ||
// statement.includes('BEGIN') || // statement.includes('COMMIT') ||
// statement.includes('ROLLBACK') // statement.includes('BEGIN') ||
// ) { // statement.includes('ROLLBACK')
// return; // ) {
// } // return;
// }
// // Note that this causes problems when attempting to commit transactions - this // // Note that this causes problems when attempting to commit transactions - this
// // statement is running, and we get at SQLITE_BUSY error. So we delay. // // statement is running, and we get at SQLITE_BUSY error. So we delay.
// await new Promise(resolve => setTimeout(resolve, 1000)); // await new Promise(resolve => setTimeout(resolve, 1000));
// const data = await db.get(`EXPLAIN QUERY PLAN ${statement}`); // const data = await promisified.get(`EXPLAIN QUERY PLAN ${statement}`);
// console._log(`EXPLAIN QUERY PLAN ${statement}\n`, data && data.detail); // console._log(`EXPLAIN QUERY PLAN ${statement}\n`, data && data.detail);
// }); // });
// }
await updateSchema(promisified); await updateSchema(promisified);
@ -2583,22 +2588,16 @@ async function saveMessages(
} }
saveMessages.needsSerial = true; saveMessages.needsSerial = true;
async function removeMessage(id: Array<string> | string) { async function removeMessage(id: string) {
const db = getInstance(); const db = getInstance();
if (!Array.isArray(id)) { await db.run('DELETE FROM messages WHERE id = $id;', { $id: id });
await db.run('DELETE FROM messages WHERE id = $id;', { $id: id }); }
return; async function removeMessages(ids: Array<string>) {
} const db = getInstance();
if (!id.length) {
throw new Error('removeMessages: No ids to delete!');
}
// Our node interface doesn't seem to allow you to replace one single ? with an array
await db.run( await db.run(
`DELETE FROM messages WHERE id IN ( ${id.map(() => '?').join(', ')} );`, `DELETE FROM messages WHERE id IN ( ${ids.map(() => '?').join(', ')} );`,
id ids
); );
} }

View file

@ -2873,16 +2873,14 @@ Whisper.ConversationView = Whisper.View.extend({
async destroyMessages() { async destroyMessages() {
try { try {
await this.confirm(window.i18n('deleteConversationConfirmation')); await this.confirm(window.i18n('deleteConversationConfirmation'));
try { this.longRunningTaskWrapper({
this.model.trigger('unload', 'delete messages'); name: 'destroymessages',
await this.model.destroyMessages(); task: async () => {
this.model.updateLastMessage(); this.model.trigger('unload', 'delete messages');
} catch (error) { await this.model.destroyMessages();
window.log.error( this.model.updateLastMessage();
'destroyMessages: Failed to successfully delete conversation', },
error && error.stack ? error.stack : error });
);
}
} catch (error) { } catch (error) {
// nothing to see here, user canceled out of dialog // nothing to see here, user canceled out of dialog
} }