Update message receipt processing & add tests
This commit is contained in:
parent
5e733059b9
commit
c8099171e2
2 changed files with 215 additions and 14 deletions
|
@ -30,7 +30,6 @@ import {
|
|||
RECEIPT_BATCHER_WAIT_MS,
|
||||
} from '../types/Receipt';
|
||||
import { drop } from '../util/drop';
|
||||
import { strictAssert } from '../util/assert';
|
||||
|
||||
const { deleteSentProtoRecipient } = dataInterface;
|
||||
|
||||
|
@ -85,7 +84,6 @@ const processReceiptBatcher = createWaitBatcher({
|
|||
receipt: MessageReceiptAttributesType
|
||||
): void {
|
||||
const existing = receiptsByMessageId.get(message.id);
|
||||
|
||||
if (!existing) {
|
||||
window.MessageCache.toMessageAttributes(message);
|
||||
receiptsByMessageId.set(message.id, [receipt]);
|
||||
|
@ -146,12 +144,13 @@ const processReceiptBatcher = createWaitBatcher({
|
|||
}
|
||||
}
|
||||
|
||||
for (const [
|
||||
messageId,
|
||||
receiptsForMessage,
|
||||
] of receiptsByMessageId.entries()) {
|
||||
drop(processReceiptsForMessage(messageId, receiptsForMessage));
|
||||
}
|
||||
await Promise.all(
|
||||
[...receiptsByMessageId.entries()].map(
|
||||
([messageId, receiptsForMessage]) => {
|
||||
return processReceiptsForMessage(messageId, receiptsForMessage);
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -230,7 +229,7 @@ function updateMessageWithReceipts(
|
|||
for (const receipt of receiptsToProcess) {
|
||||
updatedMessage = {
|
||||
...updatedMessage,
|
||||
...updateMessageSendStateWithReceipt(message.id, receipt),
|
||||
...updateMessageSendStateWithReceipt(updatedMessage, receipt),
|
||||
};
|
||||
}
|
||||
return { updatedMessage, validReceipts: receiptsToProcess };
|
||||
|
@ -443,15 +442,11 @@ function getNewSendStateByConversationId(
|
|||
}
|
||||
|
||||
function updateMessageSendStateWithReceipt(
|
||||
messageId: string,
|
||||
message: MessageAttributesType,
|
||||
receipt: MessageReceiptAttributesType
|
||||
): Partial<MessageAttributesType> {
|
||||
const { messageSentAt } = receipt;
|
||||
|
||||
// Get message from cache to make sure we have most recent
|
||||
const message = window.MessageCache.accessAttributes(messageId);
|
||||
strictAssert(message, 'Message should exist in cache');
|
||||
|
||||
const newAttributes: Partial<MessageAttributesType> = {};
|
||||
|
||||
const newEditHistory = (message.editHistory ?? []).map(edit => {
|
||||
|
|
206
ts/test-electron/MessageReceipts_test.ts
Normal file
206
ts/test-electron/MessageReceipts_test.ts
Normal file
|
@ -0,0 +1,206 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import uuid from 'uuid';
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { type AciString, generateAci } from '../types/ServiceId';
|
||||
import type { MessageAttributesType } from '../model-types';
|
||||
import { SendStatus } from '../messages/MessageSendState';
|
||||
import {
|
||||
type MessageReceiptAttributesType,
|
||||
MessageReceiptType,
|
||||
onReceipt,
|
||||
} from '../messageModifiers/MessageReceipts';
|
||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||
|
||||
describe('MessageReceipts', () => {
|
||||
let ourAci: AciString;
|
||||
|
||||
beforeEach(async () => {
|
||||
ourAci = generateAci();
|
||||
await window.textsecure.storage.put('uuid_id', `${ourAci}.1`);
|
||||
await window.textsecure.storage.put('read-receipt-setting', true);
|
||||
await window.ConversationController.load();
|
||||
});
|
||||
|
||||
function generateReceipt(
|
||||
sourceConversationId: string,
|
||||
messageSentAt: number,
|
||||
type: MessageReceiptType
|
||||
): MessageReceiptAttributesType {
|
||||
return {
|
||||
envelopeId: uuid(),
|
||||
messageSentAt,
|
||||
receiptTimestamp: 1,
|
||||
removeFromMessageReceiverCache: () => null,
|
||||
sourceConversationId,
|
||||
sourceDevice: 1,
|
||||
sourceServiceId: generateAci(),
|
||||
type,
|
||||
wasSentEncrypted: true,
|
||||
};
|
||||
}
|
||||
it('processes all receipts in a batch', async () => {
|
||||
const id = uuid();
|
||||
const sentAt = Date.now();
|
||||
|
||||
const messageAttributes: MessageAttributesType = {
|
||||
conversationId: uuid(),
|
||||
id,
|
||||
received_at: 1,
|
||||
sent_at: sentAt,
|
||||
timestamp: sentAt,
|
||||
type: 'outgoing',
|
||||
sendStateByConversationId: {
|
||||
aaaa: {
|
||||
status: SendStatus.Sent,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
bbbb: {
|
||||
status: SendStatus.Sent,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
cccc: {
|
||||
status: SendStatus.Sent,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
dddd: {
|
||||
status: SendStatus.Sent,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await window.Signal.Data.saveMessage(messageAttributes, {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
onReceipt(generateReceipt('aaaa', sentAt, MessageReceiptType.Delivery)),
|
||||
onReceipt(generateReceipt('bbbb', sentAt, MessageReceiptType.Delivery)),
|
||||
onReceipt(generateReceipt('cccc', sentAt, MessageReceiptType.Read)),
|
||||
onReceipt(generateReceipt('aaaa', sentAt, MessageReceiptType.Read)),
|
||||
]);
|
||||
|
||||
const messageFromDatabase = await window.Signal.Data.getMessageById(id);
|
||||
const savedSendState = messageFromDatabase?.sendStateByConversationId;
|
||||
|
||||
assert.equal(savedSendState?.aaaa.status, SendStatus.Read, 'aaaa');
|
||||
assert.equal(savedSendState?.bbbb.status, SendStatus.Delivered, 'bbbb');
|
||||
assert.equal(savedSendState?.cccc.status, SendStatus.Read, 'cccc');
|
||||
assert.equal(savedSendState?.dddd.status, SendStatus.Sent, 'dddd');
|
||||
});
|
||||
|
||||
it('updates sendStateByConversationId for edits', async () => {
|
||||
const id = uuid();
|
||||
const sentAt = Date.now();
|
||||
const editedSentAt = sentAt + 1000;
|
||||
const defaultSendState = {
|
||||
aaaa: {
|
||||
status: SendStatus.Sent,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
bbbb: {
|
||||
status: SendStatus.Sent,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
cccc: {
|
||||
status: SendStatus.Sent,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
dddd: {
|
||||
status: SendStatus.Sent,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
const messageAttributes: MessageAttributesType = {
|
||||
conversationId: uuid(),
|
||||
id,
|
||||
received_at: 1,
|
||||
sent_at: sentAt,
|
||||
timestamp: sentAt,
|
||||
editMessageTimestamp: editedSentAt,
|
||||
type: 'outgoing',
|
||||
sendStateByConversationId: defaultSendState,
|
||||
editHistory: [
|
||||
{
|
||||
sendStateByConversationId: defaultSendState,
|
||||
timestamp: editedSentAt,
|
||||
},
|
||||
{
|
||||
sendStateByConversationId: defaultSendState,
|
||||
timestamp: sentAt,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await window.Signal.Data.saveMessage(messageAttributes, {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
});
|
||||
await window.Signal.Data.saveEditedMessage(messageAttributes, ourAci, {
|
||||
conversationId: messageAttributes.conversationId,
|
||||
messageId: messageAttributes.id,
|
||||
readStatus: ReadStatus.Read,
|
||||
sentAt: editedSentAt,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
// send receipts for original message
|
||||
onReceipt(generateReceipt('aaaa', sentAt, MessageReceiptType.Delivery)),
|
||||
onReceipt(generateReceipt('bbbb', sentAt, MessageReceiptType.Delivery)),
|
||||
onReceipt(generateReceipt('cccc', sentAt, MessageReceiptType.Read)),
|
||||
onReceipt(generateReceipt('aaaa', sentAt, MessageReceiptType.Read)),
|
||||
|
||||
// and send receipts for edited message
|
||||
onReceipt(
|
||||
generateReceipt('aaaa', editedSentAt, MessageReceiptType.Delivery)
|
||||
),
|
||||
onReceipt(
|
||||
generateReceipt('bbbb', editedSentAt, MessageReceiptType.Delivery)
|
||||
),
|
||||
onReceipt(generateReceipt('cccc', editedSentAt, MessageReceiptType.Read)),
|
||||
onReceipt(generateReceipt('bbbb', editedSentAt, MessageReceiptType.Read)),
|
||||
]);
|
||||
|
||||
const messageFromDatabase = await window.Signal.Data.getMessageById(id);
|
||||
const rootSendState = messageFromDatabase?.sendStateByConversationId;
|
||||
|
||||
assert.deepEqual(
|
||||
rootSendState,
|
||||
messageFromDatabase?.editHistory?.[0].sendStateByConversationId,
|
||||
'edit history version should match root version'
|
||||
);
|
||||
assert.equal(rootSendState?.aaaa.status, SendStatus.Delivered, 'aaaa');
|
||||
assert.equal(rootSendState?.bbbb.status, SendStatus.Read, 'bbbb');
|
||||
assert.equal(rootSendState?.cccc.status, SendStatus.Read, 'cccc');
|
||||
assert.equal(rootSendState?.dddd.status, SendStatus.Sent, 'dddd');
|
||||
|
||||
const originalMessageSendState =
|
||||
messageFromDatabase?.editHistory?.[1].sendStateByConversationId;
|
||||
|
||||
assert.equal(
|
||||
originalMessageSendState?.aaaa.status,
|
||||
SendStatus.Read,
|
||||
'original-aaaa'
|
||||
);
|
||||
assert.equal(
|
||||
originalMessageSendState?.bbbb.status,
|
||||
SendStatus.Delivered,
|
||||
'original-bbbb'
|
||||
);
|
||||
assert.equal(
|
||||
originalMessageSendState?.cccc.status,
|
||||
SendStatus.Read,
|
||||
'original-cccc'
|
||||
);
|
||||
assert.equal(
|
||||
originalMessageSendState?.dddd.status,
|
||||
SendStatus.Sent,
|
||||
'original-dddd'
|
||||
);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue