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 () => {
try {
await window.Signal.RemoteConfig.maybeRefreshRemoteConfig(); 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),
}; };
} }
@ -1685,6 +1688,7 @@ async function initialize({
try { try {
promisified = await openAndSetUpSQLCipher(databaseFilePath, { key }); promisified = await openAndSetUpSQLCipher(databaseFilePath, { key });
// if (promisified) {
// promisified.on('trace', async statement => { // promisified.on('trace', async statement => {
// if ( // if (
// !globalInstance || // !globalInstance ||
@ -1700,9 +1704,10 @@ async function initialize({
// // 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;
} }
if (!id.length) { async function removeMessages(ids: Array<string>) {
throw new Error('removeMessages: No ids to delete!'); const db = getInstance();
}
// 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({
name: 'destroymessages',
task: async () => {
this.model.trigger('unload', 'delete messages'); this.model.trigger('unload', 'delete messages');
await this.model.destroyMessages(); await this.model.destroyMessages();
this.model.updateLastMessage(); this.model.updateLastMessage();
} catch (error) { },
window.log.error( });
'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
} }