Skip to content

Commit 32e0022

Browse files
authored
Merge pull request oxen-io#2532 from Bilb/fix-deleted-messages-all-at-once
To merge once theming is done: handle deleted messages & deleted reacts all at once
2 parents 9efe084 + 1ce8fd5 commit 32e0022

File tree

14 files changed

+607
-118
lines changed

14 files changed

+607
-118
lines changed

ts/components/conversation/message/message-item/GenericReadableMessage.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import _ from 'lodash';
88
import { Data } from '../../../../data/data';
99
import { MessageRenderingProps } from '../../../../models/messageType';
1010
import { getConversationController } from '../../../../session/conversations';
11-
import { messageExpired } from '../../../../state/ducks/conversations';
11+
import { messagesExpired } from '../../../../state/ducks/conversations';
1212
import {
1313
getGenericReadableMessageSelectorProps,
1414
getIsMessageSelected,
@@ -68,10 +68,12 @@ function useIsExpired(props: ExpiringProps) {
6868
await Data.removeMessage(messageId);
6969
if (convoId) {
7070
dispatch(
71-
messageExpired({
72-
conversationKey: convoId,
73-
messageId,
74-
})
71+
messagesExpired([
72+
{
73+
conversationKey: convoId,
74+
messageId,
75+
},
76+
])
7577
);
7678
const convo = getConversationController().get(convoId);
7779
convo?.updateLastMessage();

ts/data/data.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export const Data = {
139139
saveMessage,
140140
saveMessages,
141141
removeMessage,
142-
_removeMessages,
142+
removeMessagesByIds,
143143
getMessageIdsFromServerIds,
144144
getMessageById,
145145
getMessageBySenderAndSentAt,
@@ -396,9 +396,13 @@ async function removeMessage(id: string): Promise<void> {
396396
}
397397
}
398398

399-
// Note: this method will not clean up external files, just delete from SQL
400-
async function _removeMessages(ids: Array<string>): Promise<void> {
401-
await channels.removeMessage(ids);
399+
/**
400+
* Note: this method will not clean up external files, just delete from SQL.
401+
* Files are cleaned up on app start if they are not linked to any messages
402+
*
403+
*/
404+
async function removeMessagesByIds(ids: Array<string>): Promise<void> {
405+
await channels.removeMessagesByIds(ids);
402406
}
403407

404408
async function getMessageIdsFromServerIds(
@@ -644,7 +648,7 @@ async function removeAllMessagesInConversation(conversationId: string): Promise<
644648
await Promise.all(messages.map(message => message.cleanup()));
645649

646650
// eslint-disable-next-line no-await-in-loop
647-
await channels.removeMessage(ids);
651+
await channels.removeMessagesByIds(ids);
648652
} while (messages.length > 0);
649653
}
650654

ts/data/dataInit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const channelsToMake = new Set([
3838
'saveSeenMessageHashes',
3939
'saveMessages',
4040
'removeMessage',
41-
'_removeMessages',
41+
'removeMessagesByIds',
4242
'getUnreadByConversation',
4343
'markAllAsReadByConversationNoExpiration',
4444
'getUnreadCountByConversation',

ts/mains/main_renderer.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,18 +217,23 @@ async function start() {
217217
);
218218

219219
window.log.info(`Cleanup: Found ${messagesForCleanup.length} messages for cleanup`);
220+
221+
const idsToCleanUp: Array<string> = [];
220222
await Promise.all(
221-
messagesForCleanup.map(async (message: MessageModel) => {
223+
messagesForCleanup.map((message: MessageModel) => {
222224
const sentAt = message.get('sent_at');
223225

224226
if (message.hasErrors()) {
225227
return;
226228
}
227229

228230
window.log.info(`Cleanup: Deleting unsent message ${sentAt}`);
229-
await Data.removeMessage(message.id);
231+
idsToCleanUp.push(message.id);
230232
})
231233
);
234+
if (idsToCleanUp.length) {
235+
await Data.removeMessagesByIds(idsToCleanUp);
236+
}
232237
window.log.info('Cleanup: complete');
233238

234239
window.log.info('listening for registration events');

ts/models/conversation.ts

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -249,12 +249,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
249249
await deleteExternalFilesOfConversation(this.attributes);
250250
}
251251

252-
public async onExpired(_message: MessageModel) {
253-
await this.updateLastMessage();
254-
255-
// removeMessage();
256-
}
257-
258252
public getGroupAdmins(): Array<string> {
259253
const groupAdmins = this.get('groupAdmins');
260254

@@ -478,26 +472,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
478472
return true;
479473
}
480474

481-
public async onReadMessage(message: MessageModel, readAt: number) {
482-
// We mark as read everything older than this message - to clean up old stuff
483-
// still marked unread in the database. If the user generally doesn't read in
484-
// the desktop app, so the desktop app only gets read syncs, we can very
485-
// easily end up with messages never marked as read (our previous early read
486-
// sync handling, read syncs never sent because app was offline)
487-
488-
// We queue it because we often get a whole lot of read syncs at once, and
489-
// their markRead calls could very easily overlap given the async pull from DB.
490-
491-
// Lastly, we don't send read syncs for any message marked read due to a read
492-
// sync. That's a notification explosion we don't need.
493-
return this.queueJob(() =>
494-
this.markReadBouncy(message.get('received_at') as any, {
495-
sendReadReceipts: false,
496-
readAt,
497-
})
498-
);
499-
}
500-
501475
public async getUnreadCount() {
502476
const unreadCount = await Data.getUnreadCountByConversation(this.id);
503477

@@ -1693,15 +1667,17 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
16931667
return this.get('type') === ConversationTypeEnum.GROUP;
16941668
}
16951669

1696-
public async removeMessage(messageId: any) {
1670+
public async removeMessage(messageId: string) {
16971671
await Data.removeMessage(messageId);
16981672
this.updateLastMessage();
16991673

17001674
window.inboxStore?.dispatch(
1701-
conversationActions.messageDeleted({
1702-
conversationKey: this.id,
1703-
messageId,
1704-
})
1675+
conversationActions.messagesDeleted([
1676+
{
1677+
conversationKey: this.id,
1678+
messageId,
1679+
},
1680+
])
17051681
);
17061682
}
17071683

ts/models/message.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
125125
throw new Error('A message always needs to have an conversationId.');
126126
}
127127

128-
// this.on('expired', this.onExpired);
129128
if (!attributes.skipTimerInit) {
130129
void this.setToExpire();
131130
}

ts/node/sql.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -945,21 +945,44 @@ function saveMessages(arrayOfMessages: Array<any>) {
945945
}
946946

947947
function removeMessage(id: string, instance?: BetterSqlite3.Database) {
948-
if (!Array.isArray(id)) {
949-
assertGlobalInstanceOrInstance(instance)
950-
.prepare(`DELETE FROM ${MESSAGES_TABLE} WHERE id = $id;`)
951-
.run({ id });
948+
if (!isString(id)) {
949+
throw new Error('removeMessage: only takes single message to delete!');
950+
952951
return;
953952
}
954953

955-
if (!id.length) {
956-
throw new Error('removeMessages: No ids to delete!');
954+
assertGlobalInstanceOrInstance(instance)
955+
.prepare(`DELETE FROM ${MESSAGES_TABLE} WHERE id = $id;`)
956+
.run({ id });
957+
}
958+
959+
function removeMessagesByIds(ids: Array<string>, instance?: BetterSqlite3.Database) {
960+
if (!Array.isArray(ids)) {
961+
throw new Error('removeMessagesByIds only allowed an array of strings');
962+
}
963+
964+
if (!ids.length) {
965+
throw new Error('removeMessagesByIds: No ids to delete!');
957966
}
958967

959968
// Our node interface doesn't seem to allow you to replace one single ? with an array
960969
assertGlobalInstanceOrInstance(instance)
961-
.prepare(`DELETE FROM ${MESSAGES_TABLE} WHERE id IN ( ${id.map(() => '?').join(', ')} );`)
962-
.run(id);
970+
.prepare(`DELETE FROM ${MESSAGES_TABLE} WHERE id IN ( ${ids.map(() => '?').join(', ')} );`)
971+
.run(ids);
972+
}
973+
974+
function removeAllMessagesInConversation(
975+
conversationId: string,
976+
instance?: BetterSqlite3.Database
977+
) {
978+
if (!conversationId) {
979+
return;
980+
}
981+
982+
// Our node interface doesn't seem to allow you to replace one single ? with an array
983+
assertGlobalInstanceOrInstance(instance)
984+
.prepare(`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = $conversationId`)
985+
.run({ conversationId });
963986
}
964987

965988
function getMessageIdsFromServerIds(serverIds: Array<string | number>, conversationId: string) {
@@ -2440,6 +2463,8 @@ export const sqlNode = {
24402463
updateLastHash,
24412464
saveMessages,
24422465
removeMessage,
2466+
removeMessagesByIds,
2467+
removeAllMessagesInConversation,
24432468
getUnreadByConversation,
24442469
markAllAsReadByConversationNoExpiration,
24452470
getUnreadCountByConversation,

ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import _, { compact, isArray, isNumber, isObject, pick } from 'lodash';
1+
import _, { compact, isArray, isEmpty, isNumber, isObject, pick } from 'lodash';
22
import { OpenGroupData } from '../../../../data/opengroups';
33
import { handleOpenGroupV4Message } from '../../../../receiver/opengroup';
44
import { OpenGroupRequestCommonType } from '../opengroupV2/ApiUtil';
@@ -35,6 +35,8 @@ import { ConversationTypeEnum } from '../../../../models/conversationAttributes'
3535
import { createSwarmMessageSentFromUs } from '../../../../models/messageFactory';
3636
import { Data } from '../../../../data/data';
3737
import { processMessagesUsingCache } from './sogsV3MutationCache';
38+
import { destroyMessagesAndUpdateRedux } from '../../../../util/expiringMessages';
39+
import { sogsRollingDeletions } from './sogsRollingDeletions';
3840

3941
/**
4042
* Get the convo matching those criteria and make sure it is an opengroup convo, or return null.
@@ -159,34 +161,38 @@ const handleSogsV3DeletedMessages = async (
159161
serverUrl: string,
160162
roomId: string
161163
) => {
162-
const deletions = messages.filter(m => Boolean(m.deleted));
163-
const exceptDeletion = messages.filter(m => !m.deleted);
164-
if (!deletions.length) {
165-
return messages;
164+
const messagesDeleted = messages.filter(m => Boolean(m.deleted));
165+
const messagesWithoutDeleted = messages.filter(m => !m.deleted);
166+
if (!messagesDeleted.length) {
167+
return messagesWithoutDeleted;
166168
}
167-
const allIdsRemoved = deletions.map(m => m.id);
169+
170+
const allIdsRemoved = messagesDeleted.map(m => m.id);
171+
168172
try {
169173
const convoId = getOpenGroupV2ConversationId(serverUrl, roomId);
170174
const convo = getConversationController().get(convoId);
171175
const messageIds = await Data.getMessageIdsFromServerIds(allIdsRemoved, convo.id);
172176

173-
// we shouldn't get too many messages to delete at a time, so no need to add a function to remove multiple messages for now
177+
allIdsRemoved.forEach(removedId => {
178+
sogsRollingDeletions.addMessageDeletedId(convoId, removedId);
179+
});
174180

175-
await Promise.all(
176-
(messageIds || []).map(async id => {
177-
if (convo) {
178-
await convo.removeMessage(id);
179-
}
180-
await Data.removeMessage(id);
181-
})
182-
);
181+
if (messageIds && messageIds.length) {
182+
await destroyMessagesAndUpdateRedux(
183+
messageIds.map(messageId => ({
184+
conversationKey: convoId,
185+
messageId,
186+
}))
187+
);
188+
}
183189
} catch (e) {
184190
window?.log?.warn('handleDeletions failed:', e);
185191
}
186-
return exceptDeletion;
192+
return messagesWithoutDeleted;
187193
};
188194

189-
// tslint:disable-next-line: cyclomatic-complexity
195+
// tslint:disable-next-line: max-func-body-length cyclomatic-complexity
190196
const handleMessagesResponseV4 = async (
191197
messages: Array<OpenGroupMessageV4>,
192198
serverUrl: string,
@@ -284,6 +290,7 @@ const handleMessagesResponseV4 = async (
284290

285291
const incomingMessageSeqNo = compact(messages.map(n => n.seqno));
286292
const maxNewMessageSeqNo = Math.max(...incomingMessageSeqNo);
293+
287294
for (let index = 0; index < messagesWithResolvedBlindedIdsIfFound.length; index++) {
288295
const msgToHandle = messagesWithResolvedBlindedIdsIfFound[index];
289296
try {
@@ -309,13 +316,24 @@ const handleMessagesResponseV4 = async (
309316
await OpenGroupData.saveV2OpenGroupRoom(roomInfosRefreshed);
310317

311318
const messagesWithReactions = messages.filter(m => m.reactions !== undefined);
319+
312320
if (messagesWithReactions.length > 0) {
313321
const conversationId = getOpenGroupV2ConversationId(serverUrl, roomId);
314322
const groupConvo = getConversationController().get(conversationId);
315323
if (groupConvo && groupConvo.isOpenGroupV2()) {
316-
for (const message of messagesWithReactions) {
324+
for (const messageWithReaction of messagesWithReactions) {
325+
if (isEmpty(messageWithReaction.reactions)) {
326+
/*
327+
* When a message is deleted from the server, we get the deleted event as a data: null on the message itself
328+
* and an update on its reactions.
329+
* But, because we just deleted that message, we can skip trying to update its reactions: it's not in the DB anymore.
330+
*/
331+
if (sogsRollingDeletions.hasMessageDeletedId(conversationId, messageWithReaction.id)) {
332+
continue;
333+
}
334+
}
317335
void groupConvo.queueJob(async () => {
318-
await processMessagesUsingCache(serverUrl, roomId, message);
336+
await processMessagesUsingCache(serverUrl, roomId, messageWithReaction);
319337
});
320338
}
321339
}
@@ -526,6 +544,7 @@ export const handleBatchPollResults = async (
526544
break;
527545
case 'pollInfo':
528546
await handlePollInfoResponse(subResponse.code, subResponse.body, serverUrl);
547+
529548
break;
530549
case 'inbox':
531550
await handleInboxOutboxMessages(subResponse.body, serverUrl, false);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { RingBuffer } from '../../../utils/RingBuffer';
2+
3+
const rollingDeletedMessageIds: Map<string, RingBuffer<number>> = new Map();
4+
5+
const addMessageDeletedId = (conversationId: string, messageDeletedId: number) => {
6+
if (!rollingDeletedMessageIds.has(conversationId)) {
7+
rollingDeletedMessageIds.set(
8+
conversationId,
9+
new RingBuffer<number>(sogsRollingDeletions.getPerRoomCount())
10+
);
11+
}
12+
const ringBuffer = rollingDeletedMessageIds.get(conversationId);
13+
if (!ringBuffer) {
14+
return;
15+
}
16+
ringBuffer.insert(messageDeletedId);
17+
};
18+
19+
const hasMessageDeletedId = (conversationId: string, messageDeletedId: number) => {
20+
if (!rollingDeletedMessageIds.has(conversationId)) {
21+
return false;
22+
}
23+
24+
const messageIdWasDeletedRecently = rollingDeletedMessageIds
25+
?.get(conversationId)
26+
?.has(messageDeletedId);
27+
28+
return messageIdWasDeletedRecently;
29+
};
30+
31+
/**
32+
* emptyMessageDeleteIds should only be used for testing purposes.
33+
*/
34+
const emptyMessageDeleteIds = () => {
35+
rollingDeletedMessageIds.clear();
36+
};
37+
38+
export const sogsRollingDeletions = {
39+
addMessageDeletedId,
40+
hasMessageDeletedId,
41+
emptyMessageDeleteIds,
42+
getPerRoomCount,
43+
};
44+
45+
// keep 2000 deleted message ids in memory
46+
function getPerRoomCount() {
47+
return 2000;
48+
}

0 commit comments

Comments
 (0)