signal-desktop/ts/test-electron/sql/markRead_test.ts

847 lines
22 KiB
TypeScript
Raw Normal View History

// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { v4 as generateUuid } from 'uuid';
2024-07-22 18:16:33 +00:00
import { DataReader, DataWriter } from '../../sql/Client';
import { generateAci } from '../../types/ServiceId';
import type { ReactionType } from '../../types/Reactions';
import { ReactionReadStatus } from '../../types/Reactions';
2022-11-16 20:18:02 +00:00
import { DurationInSeconds } from '../../util/durations';
import type { MessageAttributesType } from '../../model-types.d';
import { ReadStatus } from '../../messages/MessageReadStatus';
2024-07-22 18:16:33 +00:00
const { _getAllReactions, _getAllMessages, getTotalUnreadForConversation } =
DataReader;
const {
_removeAllMessages,
_removeAllReactions,
addReaction,
saveMessages,
getUnreadByConversationAndMarkRead,
getUnreadReactionsAndMarkRead,
2024-07-22 18:16:33 +00:00
} = DataWriter;
const UNREAD_REACTION = { readStatus: ReactionReadStatus.Unread };
describe('sql/markRead', () => {
beforeEach(async () => {
await _removeAllMessages();
await _removeAllReactions();
});
it('properly finds and reads unread messages in current conversation', async () => {
assert.lengthOf(await _getAllMessages(), 0);
const start = Date.now();
const readAt = start + 20;
const conversationId = generateUuid();
const ourAci = generateAci();
const oldest: MessageAttributesType = {
id: generateUuid(),
body: 'message 1',
type: 'incoming',
conversationId,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
readStatus: ReadStatus.Read,
};
const oldestUnread: MessageAttributesType = {
id: generateUuid(),
body: 'message 2',
type: 'incoming',
conversationId,
sent_at: start + 2,
received_at: start + 2,
timestamp: start + 2,
readStatus: ReadStatus.Unread,
};
const unreadInAnotherConvo: MessageAttributesType = {
id: generateUuid(),
body: 'message 3',
type: 'incoming',
conversationId: generateUuid(),
sent_at: start + 3,
received_at: start + 3,
timestamp: start + 3,
readStatus: ReadStatus.Unread,
};
const unread: MessageAttributesType = {
id: generateUuid(),
body: 'message 4',
type: 'incoming',
conversationId,
sent_at: start + 4,
received_at: start + 4,
timestamp: start + 4,
readStatus: ReadStatus.Unread,
};
const unreadStory: MessageAttributesType = {
id: generateUuid(),
body: 'message 5',
type: 'story',
conversationId,
sent_at: start + 5,
received_at: start + 5,
timestamp: start + 5,
readStatus: ReadStatus.Unread,
storyId: generateUuid(),
};
const unreadStoryReply: MessageAttributesType = {
id: generateUuid(),
body: 'message 6',
type: 'incoming',
conversationId,
sent_at: start + 6,
received_at: start + 6,
timestamp: start + 6,
readStatus: ReadStatus.Unread,
storyId: generateUuid(),
};
const newestUnread: MessageAttributesType = {
id: generateUuid(),
body: 'message 7',
type: 'incoming',
conversationId,
sent_at: start + 7,
received_at: start + 7,
timestamp: start + 7,
readStatus: ReadStatus.Unread,
};
await saveMessages(
[
oldest,
oldestUnread,
unreadInAnotherConvo,
unread,
unreadStory,
unreadStoryReply,
newestUnread,
],
{
forceSave: true,
ourAci,
}
);
assert.lengthOf(await _getAllMessages(), 7);
assert.strictEqual(
await getTotalUnreadForConversation(conversationId, {
storyId: undefined,
includeStoryReplies: false,
}),
3,
'no stories/unread count - before'
);
const markedRead = await getUnreadByConversationAndMarkRead({
conversationId,
newestUnreadAt: unreadStoryReply.received_at,
readAt,
includeStoryReplies: false,
});
assert.lengthOf(markedRead, 2, 'no stories/two messages marked read');
assert.strictEqual(
await getTotalUnreadForConversation(conversationId, {
storyId: undefined,
includeStoryReplies: false,
}),
1,
'no stories/unread count - after'
);
// Sorted in descending order
assert.strictEqual(
markedRead[0].id,
unread.id,
'no stories/first should be "unread" message'
);
assert.strictEqual(
markedRead[1].id,
oldestUnread.id,
'no stories/second should be oldestUnread'
);
const markedRead2 = await getUnreadByConversationAndMarkRead({
conversationId,
newestUnreadAt: newestUnread.received_at,
readAt,
includeStoryReplies: true,
});
assert.lengthOf(markedRead2, 2, 'with stories/two messages marked read');
assert.strictEqual(
markedRead2[0].id,
newestUnread.id,
'with stories/should be newestUnread'
);
assert.strictEqual(
markedRead2[1].id,
unreadStoryReply.id,
'with stories/should be unreadStoryReply'
);
assert.strictEqual(
await getTotalUnreadForConversation(conversationId, {
storyId: undefined,
includeStoryReplies: true,
}),
0,
'with stories/unread count'
);
});
it('properly finds and reads unread messages in story', async () => {
assert.lengthOf(await _getAllMessages(), 0);
const start = Date.now();
const readAt = start + 20;
const conversationId = generateUuid();
const storyId = generateUuid();
const ourAci = generateAci();
const message1: MessageAttributesType = {
id: generateUuid(),
body: 'message 1',
type: 'story',
conversationId,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
readStatus: ReadStatus.Read,
storyId,
};
const message2: MessageAttributesType = {
id: generateUuid(),
body: 'message 2',
type: 'incoming',
conversationId,
sent_at: start + 2,
received_at: start + 2,
timestamp: start + 2,
readStatus: ReadStatus.Unread,
storyId,
};
const message3: MessageAttributesType = {
id: generateUuid(),
body: 'message 3',
type: 'incoming',
conversationId,
sent_at: start + 3,
received_at: start + 3,
timestamp: start + 3,
readStatus: ReadStatus.Unread,
storyId: generateUuid(),
};
const message4: MessageAttributesType = {
id: generateUuid(),
body: 'message 4',
type: 'incoming',
conversationId,
sent_at: start + 4,
received_at: start + 4,
timestamp: start + 4,
readStatus: ReadStatus.Unread,
storyId,
};
const message5: MessageAttributesType = {
id: generateUuid(),
body: 'message 5',
type: 'incoming',
conversationId,
sent_at: start + 5,
received_at: start + 5,
timestamp: start + 5,
readStatus: ReadStatus.Unread,
storyId: generateUuid(),
};
const message6: MessageAttributesType = {
id: generateUuid(),
body: 'message 6',
type: 'incoming',
conversationId,
sent_at: start + 6,
received_at: start + 6,
timestamp: start + 6,
readStatus: ReadStatus.Unread,
storyId: generateUuid(),
};
const message7: MessageAttributesType = {
id: generateUuid(),
body: 'message 7',
type: 'incoming',
conversationId,
sent_at: start + 7,
received_at: start + 7,
timestamp: start + 7,
readStatus: ReadStatus.Unread,
storyId,
};
await saveMessages(
[message1, message2, message3, message4, message5, message6, message7],
{
forceSave: true,
ourAci,
}
);
assert.lengthOf(await _getAllMessages(), 7);
const markedRead = await getUnreadByConversationAndMarkRead({
conversationId,
newestUnreadAt: message7.received_at,
readAt,
storyId,
includeStoryReplies: false,
});
assert.lengthOf(markedRead, 3, 'three messages marked read');
// Sorted in descending order
assert.strictEqual(
markedRead[0].id,
message7.id,
'first should be message7'
);
assert.strictEqual(
markedRead[1].id,
message4.id,
'first should be message4'
);
assert.strictEqual(
markedRead[2].id,
message2.id,
'second should be message2'
);
});
it('properly starts disappearing message timer, even if message is already read', async () => {
const now = Date.now();
assert.lengthOf(await _getAllMessages(), 0);
const start = Date.now();
const readAt = start + 20;
const conversationId = generateUuid();
2022-11-16 20:18:02 +00:00
const expireTimer = DurationInSeconds.fromSeconds(15);
const ourAci = generateAci();
const message1: MessageAttributesType = {
id: generateUuid(),
body: 'message 1',
type: 'incoming',
conversationId,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
expireTimer,
expirationStartTimestamp: start + 1,
readStatus: ReadStatus.Read,
};
const message2: MessageAttributesType = {
id: generateUuid(),
body: 'message 2',
type: 'incoming',
conversationId,
sent_at: start + 2,
received_at: start + 2,
timestamp: start + 2,
expireTimer,
readStatus: ReadStatus.Read,
};
const message3: MessageAttributesType = {
id: generateUuid(),
body: 'message 3',
type: 'incoming',
conversationId: generateUuid(),
sent_at: start + 3,
received_at: start + 3,
timestamp: start + 3,
expireTimer,
readStatus: ReadStatus.Unread,
};
const message4: MessageAttributesType = {
id: generateUuid(),
body: 'message 4',
type: 'incoming',
conversationId,
sent_at: start + 4,
received_at: start + 4,
timestamp: start + 4,
expireTimer,
readStatus: ReadStatus.Unread,
};
const message5: MessageAttributesType = {
id: generateUuid(),
body: 'message 5',
type: 'incoming',
conversationId,
sent_at: start + 5,
received_at: start + 5,
timestamp: start + 5,
readStatus: ReadStatus.Unread,
};
await saveMessages([message1, message2, message3, message4, message5], {
forceSave: true,
ourAci,
});
assert.strictEqual(
await getTotalUnreadForConversation(conversationId, {
storyId: undefined,
includeStoryReplies: true,
}),
2,
'unread count'
);
assert.lengthOf(await _getAllMessages(), 5);
const markedRead = await getUnreadByConversationAndMarkRead({
conversationId,
newestUnreadAt: message4.received_at,
readAt,
includeStoryReplies: false,
now,
});
assert.lengthOf(markedRead, 1, 'one message marked read');
assert.strictEqual(
markedRead[0].id,
message4.id,
'first should be message4'
);
assert.strictEqual(
await getTotalUnreadForConversation(conversationId, {
storyId: undefined,
includeStoryReplies: true,
}),
1,
'unread count'
);
const allMessages = await _getAllMessages();
const sorted = allMessages.sort(
(left, right) => left.timestamp - right.timestamp
);
assert.strictEqual(sorted[1].id, message2.id, 'checking message 2');
assert.isAtMost(
sorted[1].expirationStartTimestamp ?? Infinity,
now,
'checking message 2 expirationStartTimestamp'
);
assert.strictEqual(sorted[3].id, message4.id, 'checking message 4');
assert.isAtMost(
sorted[3].expirationStartTimestamp ?? Infinity,
now,
'checking message 4 expirationStartTimestamp'
);
});
it('properly finds and reads unread reactions in current conversation', async () => {
assert.lengthOf(await _getAllReactions(), 0);
const start = Date.now();
const conversationId = generateUuid();
const storyId = generateUuid();
const ourAci = generateAci();
2021-12-21 20:01:09 +00:00
const pad: Array<MessageAttributesType> = Array.from({ length: 4 }, _ => {
return {
id: generateUuid(),
2021-12-21 20:01:09 +00:00
body: 'pad message',
type: 'incoming',
conversationId,
sent_at: start - 1,
received_at: start - 1,
timestamp: start - 1,
};
});
const message1: MessageAttributesType = {
id: generateUuid(),
body: 'message 1',
type: 'incoming',
conversationId,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
};
const message2: MessageAttributesType = {
id: generateUuid(),
body: 'message 2',
type: 'incoming',
conversationId,
sent_at: start + 2,
received_at: start + 2,
timestamp: start + 2,
storyId,
};
const message3: MessageAttributesType = {
id: generateUuid(),
body: 'message 3',
type: 'incoming',
conversationId: generateUuid(),
sent_at: start + 3,
received_at: start + 3,
timestamp: start + 3,
};
const message4: MessageAttributesType = {
id: generateUuid(),
body: 'message 4',
type: 'incoming',
conversationId,
sent_at: start + 4,
received_at: start + 4,
timestamp: start + 4,
};
const message5: MessageAttributesType = {
id: generateUuid(),
body: 'message 5',
type: 'incoming',
conversationId,
sent_at: start + 5,
received_at: start + 5,
timestamp: start + 5,
};
2021-12-21 20:01:09 +00:00
await saveMessages(
[...pad, message1, message2, message3, message4, message5],
{
forceSave: true,
ourAci,
2021-12-21 20:01:09 +00:00
}
);
assert.lengthOf(await _getAllMessages(), pad.length + 5);
const reaction1: ReactionType = {
conversationId,
emoji: '🎉',
fromId: generateUuid(),
messageId: message1.id,
messageReceivedAt: message1.received_at,
2023-08-16 20:54:39 +00:00
targetAuthorAci: generateAci(),
targetTimestamp: start,
timestamp: start,
};
const reaction2: ReactionType = {
conversationId,
emoji: '🚀',
fromId: generateUuid(),
messageId: message2.id,
messageReceivedAt: message2.received_at,
2023-08-16 20:54:39 +00:00
targetAuthorAci: generateAci(),
targetTimestamp: start,
timestamp: start,
};
const reaction3: ReactionType = {
conversationId: generateUuid(),
emoji: '☀️',
fromId: generateUuid(),
messageId: message3.id,
messageReceivedAt: message3.received_at,
2023-08-16 20:54:39 +00:00
targetAuthorAci: generateAci(),
targetTimestamp: start,
timestamp: start,
};
const reaction4: ReactionType = {
conversationId,
emoji: '❤️‍🔥',
fromId: generateUuid(),
messageId: message4.id,
messageReceivedAt: message4.received_at,
2023-08-16 20:54:39 +00:00
targetAuthorAci: generateAci(),
targetTimestamp: start,
timestamp: start,
};
const reaction5: ReactionType = {
conversationId,
emoji: '🆒',
fromId: generateUuid(),
messageId: message5.id,
messageReceivedAt: message5.received_at,
2023-08-16 20:54:39 +00:00
targetAuthorAci: generateAci(),
targetTimestamp: start,
timestamp: start,
};
await addReaction(reaction1, UNREAD_REACTION);
await addReaction(reaction2, UNREAD_REACTION);
await addReaction(reaction3, UNREAD_REACTION);
await addReaction(reaction4, UNREAD_REACTION);
await addReaction(reaction5, UNREAD_REACTION);
assert.lengthOf(await _getAllReactions(), 5);
const markedRead = await getUnreadReactionsAndMarkRead({
conversationId,
newestUnreadAt: reaction4.messageReceivedAt,
});
assert.lengthOf(markedRead, 2, 'two reactions marked read');
// Sorted in descending order
assert.strictEqual(
markedRead[0].messageId,
reaction4.messageId,
'first should be reaction4'
);
assert.strictEqual(
markedRead[1].messageId,
reaction1.messageId,
'second should be reaction1'
);
const markedRead2 = await getUnreadReactionsAndMarkRead({
conversationId,
newestUnreadAt: reaction5.messageReceivedAt,
});
assert.lengthOf(markedRead2, 1);
assert.strictEqual(
markedRead2[0].messageId,
reaction5.messageId,
'should be reaction5'
);
});
it('properly finds and reads unread reactions in story', async () => {
assert.lengthOf(await _getAllReactions(), 0);
const start = Date.now();
const conversationId = generateUuid();
const storyId = generateUuid();
const ourAci = generateAci();
const message1: MessageAttributesType = {
id: generateUuid(),
body: 'message 1',
type: 'incoming',
conversationId,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
storyId,
};
const message2: MessageAttributesType = {
id: generateUuid(),
body: 'message 2',
type: 'incoming',
conversationId,
sent_at: start + 2,
received_at: start + 2,
timestamp: start + 2,
storyId: generateUuid(),
};
const message3: MessageAttributesType = {
id: generateUuid(),
body: 'message 3',
type: 'incoming',
conversationId: generateUuid(),
sent_at: start + 3,
received_at: start + 3,
timestamp: start + 3,
};
const message4: MessageAttributesType = {
id: generateUuid(),
body: 'message 4',
type: 'incoming',
conversationId,
sent_at: start + 4,
received_at: start + 4,
timestamp: start + 4,
storyId,
};
const message5: MessageAttributesType = {
id: generateUuid(),
body: 'message 5',
type: 'incoming',
conversationId,
sent_at: start + 5,
received_at: start + 5,
timestamp: start + 5,
storyId,
};
await saveMessages([message1, message2, message3, message4, message5], {
forceSave: true,
ourAci,
});
assert.lengthOf(await _getAllMessages(), 5);
const reaction1: ReactionType = {
conversationId,
emoji: '🎉',
fromId: generateUuid(),
messageId: message1.id,
messageReceivedAt: message1.received_at,
2023-08-16 20:54:39 +00:00
targetAuthorAci: generateAci(),
targetTimestamp: start,
timestamp: start,
};
const reaction2: ReactionType = {
conversationId,
emoji: '🚀',
fromId: generateUuid(),
messageId: message2.id,
messageReceivedAt: message2.received_at,
2023-08-16 20:54:39 +00:00
targetAuthorAci: generateAci(),
targetTimestamp: start,
timestamp: start,
};
const reaction3: ReactionType = {
conversationId: generateUuid(),
emoji: '☀️',
fromId: generateUuid(),
messageId: message3.id,
messageReceivedAt: message3.received_at,
2023-08-16 20:54:39 +00:00
targetAuthorAci: generateAci(),
targetTimestamp: start,
timestamp: start,
};
const reaction4: ReactionType = {
conversationId,
emoji: '❤️‍🔥',
fromId: generateUuid(),
messageId: message4.id,
messageReceivedAt: message4.received_at,
2023-08-16 20:54:39 +00:00
targetAuthorAci: generateAci(),
targetTimestamp: start,
timestamp: start,
};
const reaction5: ReactionType = {
conversationId,
emoji: '🆒',
fromId: generateUuid(),
messageId: message5.id,
messageReceivedAt: message5.received_at,
2023-08-16 20:54:39 +00:00
targetAuthorAci: generateAci(),
targetTimestamp: start,
timestamp: start,
};
await addReaction(reaction1, UNREAD_REACTION);
await addReaction(reaction2, UNREAD_REACTION);
await addReaction(reaction3, UNREAD_REACTION);
await addReaction(reaction4, UNREAD_REACTION);
await addReaction(reaction5, UNREAD_REACTION);
assert.lengthOf(await _getAllReactions(), 5);
const markedRead = await getUnreadReactionsAndMarkRead({
conversationId,
newestUnreadAt: reaction4.messageReceivedAt,
storyId,
});
assert.lengthOf(markedRead, 2, 'two reactions marked read');
// Sorted in descending order
assert.strictEqual(
markedRead[0].messageId,
reaction4.messageId,
'first should be reaction4'
);
assert.strictEqual(
markedRead[1].messageId,
reaction1.messageId,
'second should be reaction1'
);
const markedRead2 = await getUnreadReactionsAndMarkRead({
conversationId,
newestUnreadAt: reaction5.messageReceivedAt,
storyId,
});
assert.lengthOf(markedRead2, 1);
assert.strictEqual(
markedRead2[0].messageId,
reaction5.messageId,
'should be reaction5'
);
});
2022-04-20 23:33:38 +00:00
it('does not include group story replies', async () => {
assert.lengthOf(await _getAllMessages(), 0);
const start = Date.now();
const readAt = start + 20;
const conversationId = generateUuid();
const storyId = generateUuid();
const ourAci = generateAci();
2022-04-20 23:33:38 +00:00
const message1: MessageAttributesType = {
id: generateUuid(),
2022-04-20 23:33:38 +00:00
body: 'message 1',
type: 'story',
conversationId,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
readStatus: ReadStatus.Read,
};
const message2: MessageAttributesType = {
id: generateUuid(),
2022-04-20 23:33:38 +00:00
body: 'message 2',
type: 'incoming',
conversationId,
sent_at: start + 2,
received_at: start + 2,
timestamp: start + 2,
readStatus: ReadStatus.Unread,
storyId,
};
const message3: MessageAttributesType = {
id: generateUuid(),
2022-04-20 23:33:38 +00:00
body: 'message 3',
type: 'incoming',
conversationId,
sent_at: start + 3,
received_at: start + 3,
timestamp: start + 3,
readStatus: ReadStatus.Unread,
};
const message4: MessageAttributesType = {
id: generateUuid(),
2022-04-20 23:33:38 +00:00
body: 'message 4',
type: 'incoming',
conversationId,
sent_at: start + 4,
received_at: start + 4,
timestamp: start + 4,
readStatus: ReadStatus.Unread,
storyId,
};
await saveMessages([message1, message2, message3, message4], {
forceSave: true,
ourAci,
2022-04-20 23:33:38 +00:00
});
assert.lengthOf(await _getAllMessages(), 4);
const markedRead = await getUnreadByConversationAndMarkRead({
conversationId,
includeStoryReplies: false,
2022-04-20 23:33:38 +00:00
newestUnreadAt: message4.received_at,
readAt,
});
assert.lengthOf(markedRead, 1, '1 message marked read');
// Sorted in descending order
assert.strictEqual(
markedRead[0].id,
message3.id,
'first should be message3'
);
});
});