Migration and data access functions for stories

This commit is contained in:
Scott Nonnenberg 2021-12-08 11:52:46 -08:00 committed by GitHub
parent 9f4a01c535
commit fdc9885baa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 3428 additions and 202 deletions

14
ts/model-types.d.ts vendored
View file

@ -50,6 +50,12 @@ export type LastMessageStatus =
type TaskResultType = any;
export type SenderKeyInfoType = {
createdAtDate: number;
distributionId: string;
memberDevices: Array<DeviceType>;
};
export type CustomError = Error & {
identifier?: string;
number?: string;
@ -131,6 +137,7 @@ export type MessageAttributesType = {
requiredProtocolVersion?: number;
retryOptions?: RetryOptions;
sourceDevice?: number;
storyId?: string;
supportedVersionAtReceive?: unknown;
synced?: boolean;
unidentifiedDeliveryReceived?: boolean;
@ -150,6 +157,7 @@ export type MessageAttributesType = {
| 'message-history-unsynced'
| 'outgoing'
| 'profile-change'
| 'story'
| 'timer-notification'
| 'universal-timer-notification'
| 'change-number-notification'
@ -301,11 +309,7 @@ export type ConversationAttributesType = {
secretParams?: string;
publicParams?: string;
revision?: number;
senderKeyInfo?: {
createdAtDate: number;
distributionId: string;
memberDevices: Array<DeviceType>;
};
senderKeyInfo?: SenderKeyInfoType;
// GroupV2 other fields
accessControl?: {

View file

@ -4554,14 +4554,14 @@ export class ConversationModel extends window.Backbone
}
async markRead(
newestUnreadId: number,
newestUnreadAt: number,
options: { readAt?: number; sendReadReceipts: boolean } = {
sendReadReceipts: true,
}
): Promise<void> {
await markConversationRead(this.attributes, newestUnreadId, options);
await markConversationRead(this.attributes, newestUnreadAt, options);
const unreadCount = await window.Signal.Data.getUnreadCountForConversation(
const unreadCount = await window.Signal.Data.getTotalUnreadForConversation(
this.id
);

View file

@ -80,6 +80,10 @@ import type {
StickerPackStatusType,
StickerPackType,
StickerType,
StoryDistributionType,
StoryDistributionMemberType,
StoryDistributionWithMembersType,
StoryReadType,
UnprocessedType,
UnprocessedUpdateType,
} from './Interface';
@ -217,13 +221,14 @@ const dataInterface: ClientInterface = {
saveMessages,
removeMessage,
removeMessages,
getUnreadCountForConversation,
getTotalUnreadForConversation,
getUnreadByConversationAndMarkRead,
getUnreadReactionsAndMarkRead,
markReactionAsRead,
removeReactionFromConversation,
addReaction,
_getAllReactions,
_removeAllReactions,
getMessageBySender,
getMessageById,
@ -236,6 +241,7 @@ const dataInterface: ClientInterface = {
getNextTapToViewMessageTimestampToAgeOut,
getTapToViewMessagesNeedingErase,
getOlderMessagesByConversation,
getOlderStories,
getNewerMessagesByConversation,
getLastConversationMessages,
getMessageMetricsForConversation,
@ -277,6 +283,19 @@ const dataInterface: ClientInterface = {
updateOrCreateBadges,
badgeImageFileDownloaded,
_getAllStoryDistributions,
_getAllStoryDistributionMembers,
_deleteAllStoryDistributions,
createNewStoryDistribution,
getAllStoryDistributionsWithMembers,
modifyStoryDistributionMembers,
deleteStoryDistribution,
_getAllStoryReads,
_deleteAllStoryReads,
addNewStoryRead,
getLastStoryReadsForAuthor,
removeAll,
removeAllConfiguration,
@ -300,6 +319,7 @@ const dataInterface: ClientInterface = {
// Test-only
_getAllMessages,
_removeAllMessages,
// Client-side only
@ -1190,6 +1210,9 @@ async function _getAllMessages({
return new MessageCollection(messages);
}
async function _removeAllMessages() {
await channels._removeAllMessages();
}
async function getAllMessageIds() {
const ids = await channels.getAllMessageIds();
@ -1224,27 +1247,28 @@ async function getMessageBySender(
return new Message(messages[0]);
}
async function getUnreadCountForConversation(conversationId: string) {
return channels.getUnreadCountForConversation(conversationId);
async function getTotalUnreadForConversation(
conversationId: string,
storyId?: UUIDStringType
) {
return channels.getTotalUnreadForConversation(conversationId, storyId);
}
async function getUnreadByConversationAndMarkRead(
conversationId: string,
newestUnreadId: number,
readAt?: number
) {
return channels.getUnreadByConversationAndMarkRead(
conversationId,
newestUnreadId,
readAt
);
async function getUnreadByConversationAndMarkRead(options: {
conversationId: string;
newestUnreadAt: number;
readAt?: number;
storyId?: UUIDStringType;
}) {
return channels.getUnreadByConversationAndMarkRead(options);
}
async function getUnreadReactionsAndMarkRead(
conversationId: string,
newestUnreadId: number
) {
return channels.getUnreadReactionsAndMarkRead(conversationId, newestUnreadId);
async function getUnreadReactionsAndMarkRead(options: {
conversationId: string;
newestUnreadAt: number;
storyId?: UUIDStringType;
}) {
return channels.getUnreadReactionsAndMarkRead(options);
}
async function markReactionAsRead(
@ -1270,6 +1294,9 @@ async function addReaction(reactionObj: ReactionType) {
async function _getAllReactions() {
return channels._getAllReactions();
}
async function _removeAllReactions() {
await channels._removeAllReactions();
}
function handleMessageJSON(messages: Array<MessageTypeUnhydrated>) {
return messages.map(message => JSON.parse(message.json));
@ -1279,16 +1306,18 @@ async function getOlderMessagesByConversation(
conversationId: string,
{
limit = 100,
MessageCollection,
messageId,
receivedAt = Number.MAX_VALUE,
sentAt = Number.MAX_VALUE,
messageId,
MessageCollection,
storyId,
}: {
limit?: number;
MessageCollection: typeof MessageModelCollectionType;
messageId?: string;
receivedAt?: number;
sentAt?: number;
messageId?: string;
MessageCollection: typeof MessageModelCollectionType;
storyId?: UUIDStringType;
}
) {
const messages = await channels.getOlderMessagesByConversation(
@ -1298,23 +1327,36 @@ async function getOlderMessagesByConversation(
receivedAt,
sentAt,
messageId,
storyId,
}
);
return new MessageCollection(handleMessageJSON(messages));
}
async function getOlderStories(options: {
conversationId?: string;
limit?: number;
receivedAt?: number;
sentAt?: number;
sourceUuid?: string;
}): Promise<Array<MessageType>> {
return channels.getOlderStories(options);
}
async function getNewerMessagesByConversation(
conversationId: string,
{
limit = 100,
MessageCollection,
receivedAt = 0,
sentAt = 0,
MessageCollection,
storyId,
}: {
limit?: number;
MessageCollection: typeof MessageModelCollectionType;
receivedAt?: number;
sentAt?: number;
MessageCollection: typeof MessageModelCollectionType;
storyId?: UUIDStringType;
}
) {
const messages = await channels.getNewerMessagesByConversation(
@ -1323,6 +1365,7 @@ async function getNewerMessagesByConversation(
limit,
receivedAt,
sentAt,
storyId,
}
);
@ -1349,9 +1392,13 @@ async function getLastConversationMessages({
hasUserInitiatedMessages,
};
}
async function getMessageMetricsForConversation(conversationId: string) {
async function getMessageMetricsForConversation(
conversationId: string,
storyId?: UUIDStringType
) {
const result = await channels.getMessageMetricsForConversation(
conversationId
conversationId,
storyId
);
return result;
@ -1597,6 +1644,63 @@ function badgeImageFileDownloaded(
return channels.badgeImageFileDownloaded(url, localPath);
}
// Story Distributions
async function _getAllStoryDistributions(): Promise<
Array<StoryDistributionType>
> {
return channels._getAllStoryDistributions();
}
async function _getAllStoryDistributionMembers(): Promise<
Array<StoryDistributionMemberType>
> {
return channels._getAllStoryDistributionMembers();
}
async function _deleteAllStoryDistributions(): Promise<void> {
await channels._deleteAllStoryDistributions();
}
async function createNewStoryDistribution(
story: StoryDistributionWithMembersType
): Promise<void> {
await channels.createNewStoryDistribution(story);
}
async function getAllStoryDistributionsWithMembers(): Promise<
Array<StoryDistributionWithMembersType>
> {
return channels.getAllStoryDistributionsWithMembers();
}
async function modifyStoryDistributionMembers(
id: string,
options: {
toAdd: Array<UUIDStringType>;
toRemove: Array<UUIDStringType>;
}
): Promise<void> {
await channels.modifyStoryDistributionMembers(id, options);
}
async function deleteStoryDistribution(id: UUIDStringType): Promise<void> {
await channels.deleteStoryDistribution(id);
}
// Story Reads
async function _getAllStoryReads(): Promise<Array<StoryReadType>> {
return channels._getAllStoryReads();
}
async function _deleteAllStoryReads(): Promise<void> {
await channels._deleteAllStoryReads();
}
async function addNewStoryRead(read: StoryReadType): Promise<void> {
return channels.addNewStoryRead(read);
}
async function getLastStoryReadsForAuthor(options: {
authorId: UUIDStringType;
conversationId?: UUIDStringType;
limit?: number;
}): Promise<Array<StoryReadType>> {
return channels.getLastStoryReadsForAuthor(options);
}
// Other
async function removeAll() {

View file

@ -9,6 +9,7 @@ import type {
ConversationModelCollectionType,
MessageAttributesType,
MessageModelCollectionType,
SenderKeyInfoType,
} from '../model-types.d';
import type { MessageModel } from '../models/messages';
import type { ConversationModel } from '../models/conversations';
@ -239,6 +240,31 @@ export type DeleteSentProtoRecipientOptionsType = Readonly<{
deviceId: number;
}>;
export type StoryDistributionType = Readonly<{
id: UUIDStringType;
name: string;
avatarUrlPath: string;
avatarKey: Uint8Array;
senderKeyInfo: SenderKeyInfoType | undefined;
}>;
export type StoryDistributionMemberType = Readonly<{
listId: UUIDStringType;
uuid: UUIDStringType;
}>;
export type StoryDistributionWithMembersType = Readonly<
{
members: Array<UUIDStringType>;
} & StoryDistributionType
>;
export type StoryReadType = Readonly<{
authorId: UUIDStringType;
conversationId: UUIDStringType;
storyId: UUIDStringType;
storyReadDate: number;
}>;
export type DataInterface = {
close: () => Promise<void>;
removeDB: () => Promise<void>;
@ -349,8 +375,16 @@ export type DataInterface = {
) => Promise<void>;
getMessageCount: (conversationId?: string) => Promise<number>;
getAllMessageIds: () => Promise<Array<string>>;
getOlderStories: (options: {
conversationId?: string;
limit?: number;
receivedAt?: number;
sentAt?: number;
sourceUuid?: string;
}) => Promise<Array<MessageType>>;
getMessageMetricsForConversation: (
conversationId: string
conversationId: string,
storyId?: UUIDStringType
) => Promise<ConversationMetricsType>;
hasGroupCallHistoryMessage: (
conversationId: string,
@ -361,21 +395,27 @@ export type DataInterface = {
currentId: string
) => Promise<void>;
getNextTapToViewMessageTimestampToAgeOut: () => Promise<undefined | number>;
_removeAllMessages: () => Promise<void>;
getUnreadCountForConversation: (conversationId: string) => Promise<number>;
getUnreadByConversationAndMarkRead: (
getTotalUnreadForConversation: (
conversationId: string,
newestUnreadId: number,
readAt?: number
) => Promise<
storyId?: UUIDStringType
) => Promise<number>;
getUnreadByConversationAndMarkRead: (options: {
conversationId: string;
newestUnreadAt: number;
readAt?: number;
storyId?: UUIDStringType;
}) => Promise<
Array<
Pick<MessageType, 'id' | 'source' | 'sourceUuid' | 'sent_at' | 'type'>
>
>;
getUnreadReactionsAndMarkRead: (
conversationId: string,
newestUnreadId: number
) => Promise<
getUnreadReactionsAndMarkRead: (options: {
conversationId: string;
newestUnreadAt: number;
storyId?: UUIDStringType;
}) => Promise<
Array<
Pick<ReactionType, 'targetAuthorUuid' | 'targetTimestamp' | 'messageId'>
>
@ -392,6 +432,7 @@ export type DataInterface = {
}) => Promise<void>;
addReaction: (reactionObj: ReactionType) => Promise<void>;
_getAllReactions: () => Promise<Array<ReactionType>>;
_removeAllReactions: () => Promise<void>;
getUnprocessedCount: () => Promise<number>;
getAllUnprocessed: () => Promise<Array<UnprocessedType>>;
@ -452,6 +493,35 @@ export type DataInterface = {
updateOrCreateBadges(badges: ReadonlyArray<BadgeType>): Promise<void>;
badgeImageFileDownloaded(url: string, localPath: string): Promise<void>;
_getAllStoryDistributions(): Promise<Array<StoryDistributionType>>;
_getAllStoryDistributionMembers(): Promise<
Array<StoryDistributionMemberType>
>;
_deleteAllStoryDistributions(): Promise<void>;
createNewStoryDistribution(
story: StoryDistributionWithMembersType
): Promise<void>;
getAllStoryDistributionsWithMembers(): Promise<
Array<StoryDistributionWithMembersType>
>;
modifyStoryDistributionMembers(
id: string,
options: {
toAdd: Array<UUIDStringType>;
toRemove: Array<UUIDStringType>;
}
): Promise<void>;
deleteStoryDistribution(id: UUIDStringType): Promise<void>;
_getAllStoryReads(): Promise<Array<StoryReadType>>;
_deleteAllStoryReads(): Promise<void>;
addNewStoryRead(read: StoryReadType): Promise<void>;
getLastStoryReadsForAuthor(options: {
authorId: UUIDStringType;
conversationId?: UUIDStringType;
limit?: number;
}): Promise<Array<StoryReadType>>;
removeAll: () => Promise<void>;
removeAllConfiguration: (type?: RemoveAllConfiguration) => Promise<void>;
@ -525,14 +595,20 @@ export type ServerInterface = DataInterface & {
conversationId: string,
options?: {
limit?: number;
messageId?: string;
receivedAt?: number;
sentAt?: number;
messageId?: string;
storyId?: UUIDStringType;
}
) => Promise<Array<MessageTypeUnhydrated>>;
getNewerMessagesByConversation: (
conversationId: string,
options?: { limit?: number; receivedAt?: number; sentAt?: number }
options?: {
limit?: number;
receivedAt?: number;
sentAt?: number;
storyId?: UUIDStringType;
}
) => Promise<Array<MessageTypeUnhydrated>>;
getLastConversationMessages: (options: {
conversationId: string;
@ -622,19 +698,21 @@ export type ClientInterface = DataInterface & {
conversationId: string,
options: {
limit?: number;
MessageCollection: typeof MessageModelCollectionType;
messageId?: string;
receivedAt?: number;
sentAt?: number;
MessageCollection: typeof MessageModelCollectionType;
storyId?: UUIDStringType;
}
) => Promise<MessageModelCollectionType>;
getNewerMessagesByConversation: (
conversationId: string,
options: {
limit?: number;
MessageCollection: typeof MessageModelCollectionType;
receivedAt?: number;
sentAt?: number;
MessageCollection: typeof MessageModelCollectionType;
storyId?: UUIDStringType;
}
) => Promise<MessageModelCollectionType>;
getLastConversationMessages: (options: {

View file

@ -14,6 +14,7 @@ import type { Dictionary } from 'lodash';
import {
forEach,
fromPairs,
groupBy,
isNil,
isNumber,
isString,
@ -75,19 +76,19 @@ import type {
ConversationType,
DeleteSentProtoRecipientOptionsType,
EmojiType,
IdentityKeyType,
IdentityKeyIdType,
IdentityKeyType,
ItemKeyType,
ItemType,
LastConversationMessagesServerType,
MessageMetricsType,
MessageType,
MessageTypeUnhydrated,
PreKeyType,
PreKeyIdType,
PreKeyType,
SearchResultMessageType,
SenderKeyType,
SenderKeyIdType,
SenderKeyType,
SentMessageDBType,
SentMessagesType,
SentProtoType,
@ -95,13 +96,17 @@ import type {
SentRecipientsDBType,
SentRecipientsType,
ServerInterface,
SessionType,
SessionIdType,
SignedPreKeyType,
SessionType,
SignedPreKeyIdType,
SignedPreKeyType,
StickerPackStatusType,
StickerPackType,
StickerType,
StoryDistributionMemberType,
StoryDistributionType,
StoryDistributionWithMembersType,
StoryReadType,
UnprocessedType,
UnprocessedUpdateType,
} from './Interface';
@ -207,17 +212,18 @@ const dataInterface: ServerInterface = {
saveMessages,
removeMessage,
removeMessages,
getUnreadCountForConversation,
getUnreadByConversationAndMarkRead,
getUnreadReactionsAndMarkRead,
markReactionAsRead,
addReaction,
removeReactionFromConversation,
_getAllReactions,
_removeAllReactions,
getMessageBySender,
getMessageById,
getMessagesById,
_getAllMessages,
_removeAllMessages,
getAllMessageIds,
getMessagesBySentAt,
getExpiredMessages,
@ -226,7 +232,9 @@ const dataInterface: ServerInterface = {
getNextTapToViewMessageTimestampToAgeOut,
getTapToViewMessagesNeedingErase,
getOlderMessagesByConversation,
getOlderStories,
getNewerMessagesByConversation,
getTotalUnreadForConversation,
getMessageMetricsForConversation,
getLastConversationMessages,
hasGroupCallHistoryMessage,
@ -267,6 +275,19 @@ const dataInterface: ServerInterface = {
updateOrCreateBadges,
badgeImageFileDownloaded,
_getAllStoryDistributions,
_getAllStoryDistributionMembers,
_deleteAllStoryDistributions,
createNewStoryDistribution,
getAllStoryDistributionsWithMembers,
modifyStoryDistributionMembers,
deleteStoryDistribution,
_getAllStoryReads,
_deleteAllStoryReads,
addNewStoryRead,
getLastStoryReadsForAuthor,
removeAll,
removeAllConfiguration,
@ -1674,8 +1695,6 @@ async function getMessageCount(conversationId?: string): Promise<number> {
function hasUserInitiatedMessages(conversationId: string): boolean {
const db = getInstance();
// We apply the limit in the sub-query so that `json_extract` wouldn't run
// for additional messages.
const row: { count: number } = db
.prepare<Query>(
`
@ -1687,14 +1706,15 @@ function hasUserInitiatedMessages(conversationId: string): boolean {
(type IS NULL
OR
type NOT IN (
'profile-change',
'verified-change',
'message-history-unsynced',
'keychange',
'group-v1-migration',
'universal-timer-notification',
'change-number-notification',
'group-v2-change'
'group-v1-migration',
'group-v2-change',
'keychange',
'message-history-unsynced',
'profile-change',
'story',
'universal-timer-notification',
'verified-change'
)
)
LIMIT 1
@ -1749,6 +1769,7 @@ function saveMessageSync(
source,
sourceUuid,
sourceDevice,
storyId,
type,
readStatus,
expireTimer,
@ -1775,6 +1796,7 @@ function saveMessageSync(
source: source || null,
sourceUuid: sourceUuid || null,
sourceDevice: sourceDevice || null,
storyId: storyId || null,
type: type || null,
readStatus: readStatus ?? null,
};
@ -1803,6 +1825,7 @@ function saveMessageSync(
source = $source,
sourceUuid = $sourceUuid,
sourceDevice = $sourceDevice,
storyId = $storyId,
type = $type,
readStatus = $readStatus
WHERE id = $id;
@ -1844,6 +1867,7 @@ function saveMessageSync(
source,
sourceUuid,
sourceDevice,
storyId,
type,
readStatus
) values (
@ -1866,6 +1890,7 @@ function saveMessageSync(
$source,
$sourceUuid,
$sourceDevice,
$storyId,
$type,
$readStatus
);
@ -1974,6 +1999,10 @@ async function _getAllMessages(): Promise<Array<MessageType>> {
return rows.map(row => jsonToObject(row.json));
}
async function _removeAllMessages(): Promise<void> {
const db = getInstance();
db.prepare<EmptyQuery>('DELETE from messages;').run();
}
async function getAllMessageIds(): Promise<Array<string>> {
const db = getInstance();
@ -2014,30 +2043,17 @@ async function getMessageBySender({
return rows.map(row => jsonToObject(row.json));
}
async function getUnreadCountForConversation(
conversationId: string
): Promise<number> {
const db = getInstance();
const row = db
.prepare<Query>(
`
SELECT COUNT(*) AS unreadCount FROM messages
WHERE readStatus = ${ReadStatus.Unread} AND
conversationId = $conversationId AND
type = 'incoming';
`
)
.get({
conversationId,
});
return row.unreadCount;
}
async function getUnreadByConversationAndMarkRead(
conversationId: string,
newestUnreadId: number,
readAt?: number
): Promise<
async function getUnreadByConversationAndMarkRead({
conversationId,
newestUnreadAt,
storyId,
readAt,
}: {
conversationId: string;
newestUnreadAt: number;
storyId?: UUIDStringType;
readAt?: number;
}): Promise<
Array<Pick<MessageType, 'id' | 'source' | 'sourceUuid' | 'sent_at' | 'type'>>
> {
const db = getInstance();
@ -2057,13 +2073,15 @@ async function getUnreadByConversationAndMarkRead(
) AND
expireTimer IS NOT NULL AND
conversationId = $conversationId AND
received_at <= $newestUnreadId;
storyId IS $storyId AND
received_at <= $newestUnreadAt;
`
).run({
conversationId,
expirationStartTimestamp,
jsonPatch: JSON.stringify({ expirationStartTimestamp }),
newestUnreadId,
newestUnreadAt,
storyId: storyId || null,
});
const rows = db
@ -2074,13 +2092,15 @@ async function getUnreadByConversationAndMarkRead(
WHERE
readStatus = ${ReadStatus.Unread} AND
conversationId = $conversationId AND
received_at <= $newestUnreadId
storyId IS $storyId AND
received_at <= $newestUnreadAt
ORDER BY received_at DESC, sent_at DESC;
`
)
.all({
conversationId,
newestUnreadId,
newestUnreadAt,
storyId: storyId || null,
});
db.prepare<Query>(
@ -2092,12 +2112,14 @@ async function getUnreadByConversationAndMarkRead(
WHERE
readStatus = ${ReadStatus.Unread} AND
conversationId = $conversationId AND
received_at <= $newestUnreadId;
storyId IS $storyId AND
received_at <= $newestUnreadAt;
`
).run({
conversationId,
jsonPatch: JSON.stringify({ readStatus: ReadStatus.Read }),
newestUnreadId,
newestUnreadAt,
storyId: storyId || null,
});
return rows.map(row => {
@ -2117,42 +2139,50 @@ async function getUnreadByConversationAndMarkRead(
})();
}
async function getUnreadReactionsAndMarkRead(
conversationId: string,
newestUnreadId: number
): Promise<
Array<
Pick<ReactionType, 'targetAuthorUuid' | 'targetTimestamp' | 'messageId'>
>
> {
type ReactionResultType = Pick<
ReactionType,
'targetAuthorUuid' | 'targetTimestamp' | 'messageId'
> & { rowid: number };
async function getUnreadReactionsAndMarkRead({
conversationId,
newestUnreadAt,
storyId,
}: {
conversationId: string;
newestUnreadAt: number;
storyId?: UUIDStringType;
}): Promise<Array<ReactionResultType>> {
const db = getInstance();
return db.transaction(() => {
const unreadMessages = db
const unreadMessages: Array<ReactionResultType> = db
.prepare<Query>(
`
SELECT targetAuthorUuid, targetTimestamp, messageId
FROM reactions WHERE
unread = 1 AND
conversationId = $conversationId AND
messageReceivedAt <= $newestUnreadId;
SELECT rowid, targetAuthorUuid, targetTimestamp, messageId
FROM reactions
JOIN messages on messages.id IS reactions.messageId
WHERE
unread IS NOT 0 AND
messages.conversationId IS $conversationId AND
messages.received_at <= $newestUnreadAt AND
messages.storyId IS $storyId
ORDER BY messageReceivedAt DESC;
`
)
.all({
conversationId,
newestUnreadId,
newestUnreadAt,
storyId: storyId || null,
});
db.prepare(
`
UPDATE reactions SET
unread = 0 WHERE
conversationId = $conversationId AND
messageReceivedAt <= $newestUnreadId;
`
).run({
conversationId,
newestUnreadId,
const idsToUpdate = unreadMessages.map(item => item.rowid);
batchMultiVarQuery(db, idsToUpdate, (ids: Array<number>): void => {
db.prepare<ArrayQuery>(
`
UPDATE reactions SET
unread = 0 WHERE rowid IN ( ${ids.map(() => '?').join(', ')} );
`
).run(idsToUpdate);
});
return unreadMessages;
@ -2275,69 +2305,95 @@ async function _getAllReactions(): Promise<Array<ReactionType>> {
const db = getInstance();
return db.prepare<EmptyQuery>('SELECT * from reactions;').all();
}
async function _removeAllReactions(): Promise<void> {
const db = getInstance();
db.prepare<EmptyQuery>('DELETE from reactions;').run();
}
async function getOlderMessagesByConversation(
conversationId: string,
{
limit = 100,
messageId,
receivedAt = Number.MAX_VALUE,
sentAt = Number.MAX_VALUE,
messageId,
storyId,
}: {
limit?: number;
messageId?: string;
receivedAt?: number;
sentAt?: number;
messageId?: string;
storyId?: UUIDStringType;
} = {}
): Promise<Array<MessageTypeUnhydrated>> {
const db = getInstance();
let rows: JSONRows;
if (messageId) {
rows = db
.prepare<Query>(
`
SELECT json FROM messages WHERE
conversationId = $conversationId AND
id != $messageId AND
(
(received_at = $received_at AND sent_at < $sent_at) OR
received_at < $received_at
)
ORDER BY received_at DESC, sent_at DESC
LIMIT $limit;
`
)
.all({
conversationId,
received_at: receivedAt,
sent_at: sentAt,
limit,
messageId,
});
} else {
rows = db
.prepare<Query>(
`
SELECT json FROM messages WHERE
return db
.prepare<Query>(
`
SELECT json FROM messages WHERE
conversationId = $conversationId AND
($messageId IS NULL OR id IS NOT $messageId) AND
type IS NOT 'story' AND
storyId IS $storyId AND
(
(received_at = $received_at AND sent_at < $sent_at) OR
received_at < $received_at
)
ORDER BY received_at DESC, sent_at DESC
LIMIT $limit;
`
)
.all({
conversationId,
received_at: receivedAt,
sent_at: sentAt,
limit,
});
}
ORDER BY received_at DESC, sent_at DESC
LIMIT $limit;
`
)
.all({
conversationId,
limit,
messageId: messageId || null,
received_at: receivedAt,
sent_at: sentAt,
storyId: storyId || null,
})
.reverse();
}
return rows.reverse();
async function getOlderStories({
conversationId,
limit = 10,
receivedAt = Number.MAX_VALUE,
sentAt,
sourceUuid,
}: {
conversationId?: string;
limit?: number;
receivedAt?: number;
sentAt?: number;
sourceUuid?: string;
}): Promise<Array<MessageType>> {
const db = getInstance();
const rows: JSONRows = db
.prepare<Query>(
`
SELECT json
FROM messages
WHERE
type IS 'story' AND
($conversationId IS NULL OR conversationId IS $conversationId) AND
($sourceUuid IS NULL OR sourceUuid IS $sourceUuid) AND
(received_at < $receivedAt
OR (received_at IS $receivedAt AND sent_at < $sentAt)
)
ORDER BY received_at DESC, sent_at DESC
LIMIT $limit;
`
)
.all({
conversationId: conversationId || null,
receivedAt,
sentAt: sentAt || null,
sourceUuid: sourceUuid || null,
limit,
});
return rows.map(row => jsonToObject(row.json));
}
async function getNewerMessagesByConversation(
@ -2346,7 +2402,13 @@ async function getNewerMessagesByConversation(
limit = 100,
receivedAt = 0,
sentAt = 0,
}: { limit?: number; receivedAt?: number; sentAt?: number } = {}
storyId,
}: {
limit?: number;
receivedAt?: number;
sentAt?: number;
storyId?: UUIDStringType;
} = {}
): Promise<Array<MessageTypeUnhydrated>> {
const db = getInstance();
const rows: JSONRows = db
@ -2354,6 +2416,8 @@ async function getNewerMessagesByConversation(
`
SELECT json FROM messages WHERE
conversationId = $conversationId AND
type IS NOT 'story' AND
storyId IS $storyId AND
(
(received_at = $received_at AND sent_at > $sent_at) OR
received_at > $received_at
@ -2364,28 +2428,33 @@ async function getNewerMessagesByConversation(
)
.all({
conversationId,
limit,
received_at: receivedAt,
sent_at: sentAt,
limit,
storyId: storyId || null,
});
return rows;
}
function getOldestMessageForConversation(
conversationId: string
conversationId: string,
storyId?: UUIDStringType
): MessageMetricsType | undefined {
const db = getInstance();
const row = db
.prepare<Query>(
`
SELECT * FROM messages WHERE
conversationId = $conversationId
conversationId = $conversationId AND
type IS NOT 'story' AND
storyId IS $storyId
ORDER BY received_at ASC, sent_at ASC
LIMIT 1;
`
)
.get({
conversationId,
storyId: storyId || null,
});
if (!row) {
@ -2395,20 +2464,24 @@ function getOldestMessageForConversation(
return row;
}
function getNewestMessageForConversation(
conversationId: string
conversationId: string,
storyId?: UUIDStringType
): MessageMetricsType | undefined {
const db = getInstance();
const row = db
.prepare<Query>(
`
SELECT * FROM messages WHERE
conversationId = $conversationId
conversationId = $conversationId AND
type IS NOT 'story' AND
storyId IS $storyId
ORDER BY received_at DESC, sent_at DESC
LIMIT 1;
`
)
.get({
conversationId,
storyId: storyId || null,
});
if (!row) {
@ -2435,13 +2508,14 @@ function getLastConversationActivity({
(type IS NULL
OR
type NOT IN (
'profile-change',
'verified-change',
'message-history-unsynced',
'keychange',
'change-number-notification',
'group-v1-migration',
'keychange',
'message-history-unsynced',
'profile-change',
'story',
'universal-timer-notification',
'change-number-notification'
'verified-change'
)
) AND
(
@ -2492,12 +2566,13 @@ function getLastConversationPreview({
type IS NULL
OR
type NOT IN (
'profile-change',
'verified-change',
'message-history-unsynced',
'change-number-notification',
'group-v1-migration',
'message-history-unsynced',
'profile-change',
'story',
'universal-timer-notification',
'change-number-notification'
'verified-change'
)
) AND NOT
(
@ -2548,7 +2623,8 @@ async function getLastConversationMessages({
}
function getOldestUnreadMessageForConversation(
conversationId: string
conversationId: string,
storyId?: UUIDStringType
): MessageMetricsType | undefined {
const db = getInstance();
const row = db
@ -2556,13 +2632,16 @@ function getOldestUnreadMessageForConversation(
`
SELECT * FROM messages WHERE
conversationId = $conversationId AND
readStatus = ${ReadStatus.Unread}
readStatus = ${ReadStatus.Unread} AND
type IS NOT 'story' AND
storyId IS $storyId
ORDER BY received_at ASC, sent_at ASC
LIMIT 1;
`
)
.get({
conversationId,
storyId: storyId || null,
});
if (!row) {
@ -2572,7 +2651,10 @@ function getOldestUnreadMessageForConversation(
return row;
}
function getTotalUnreadForConversation(conversationId: string): number {
async function getTotalUnreadForConversation(
conversationId: string,
storyId?: UUIDStringType
): Promise<number> {
const db = getInstance();
const row = db
.prepare<Query>(
@ -2581,11 +2663,14 @@ function getTotalUnreadForConversation(conversationId: string): number {
FROM messages
WHERE
conversationId = $conversationId AND
readStatus = ${ReadStatus.Unread};
readStatus = ${ReadStatus.Unread} AND
type IS NOT 'story' AND
storyId IS $storyId;
`
)
.get({
conversationId,
storyId: storyId || null,
});
if (!row) {
@ -2596,12 +2681,19 @@ function getTotalUnreadForConversation(conversationId: string): number {
}
async function getMessageMetricsForConversation(
conversationId: string
conversationId: string,
storyId?: UUIDStringType
): Promise<ConversationMetricsType> {
const oldest = getOldestMessageForConversation(conversationId);
const newest = getNewestMessageForConversation(conversationId);
const oldestUnread = getOldestUnreadMessageForConversation(conversationId);
const totalUnread = getTotalUnreadForConversation(conversationId);
const oldest = getOldestMessageForConversation(conversationId, storyId);
const newest = getNewestMessageForConversation(conversationId, storyId);
const oldestUnread = getOldestUnreadMessageForConversation(
conversationId,
storyId
);
const totalUnread = await getTotalUnreadForConversation(
conversationId,
storyId
);
return {
oldest: oldest ? pick(oldest, ['received_at', 'sent_at', 'id']) : undefined,
@ -3706,29 +3798,256 @@ async function getAllBadgeImageFileLocalPaths(): Promise<Set<string>> {
return new Set(localPaths);
}
type StoryDistributionForDatabase = Readonly<
{
senderKeyInfoJson: string | null;
} & Omit<StoryDistributionType, 'senderKeyInfo'>
>;
function hydrateStoryDistribution(
fromDatabase: StoryDistributionForDatabase
): StoryDistributionType {
return {
...omit(fromDatabase, 'senderKeyInfoJson'),
senderKeyInfo: fromDatabase.senderKeyInfoJson
? JSON.parse(fromDatabase.senderKeyInfoJson)
: undefined,
};
}
function freezeStoryDistribution(
story: StoryDistributionType
): StoryDistributionForDatabase {
return {
...omit(story, 'senderKeyInfo'),
senderKeyInfoJson: story.senderKeyInfo
? JSON.stringify(story.senderKeyInfo)
: null,
};
}
async function _getAllStoryDistributions(): Promise<
Array<StoryDistributionType>
> {
const db = getInstance();
const storyDistributions = db
.prepare<EmptyQuery>('SELECT * FROM storyDistributions;')
.all();
return storyDistributions.map(hydrateStoryDistribution);
}
async function _getAllStoryDistributionMembers(): Promise<
Array<StoryDistributionMemberType>
> {
const db = getInstance();
return db
.prepare<EmptyQuery>('SELECT * FROM storyDistributionMembers;')
.all();
}
async function _deleteAllStoryDistributions(): Promise<void> {
const db = getInstance();
db.prepare<EmptyQuery>('DELETE FROM storyDistributions;').run();
}
async function createNewStoryDistribution(
story: StoryDistributionWithMembersType
): Promise<void> {
const db = getInstance();
db.transaction(() => {
const payload = freezeStoryDistribution(story);
prepare(
db,
`
INSERT INTO storyDistributions(
id,
name,
avatarUrlPath,
avatarKey,
senderKeyInfoJson
) VALUES (
$id,
$name,
$avatarUrlPath,
$avatarKey,
$senderKeyInfoJson
);
`
).run(payload);
const { id: listId, members } = story;
const memberInsertStatement = prepare(
db,
`
INSERT OR REPLACE INTO storyDistributionMembers (
listId,
uuid
) VALUES (
$listId,
$uuid
);
`
);
for (const uuid of members) {
memberInsertStatement.run({
listId,
uuid,
});
}
})();
}
async function getAllStoryDistributionsWithMembers(): Promise<
Array<StoryDistributionWithMembersType>
> {
const allDistributions = await _getAllStoryDistributions();
const allMembers = await _getAllStoryDistributionMembers();
const byListId = groupBy(allMembers, member => member.listId);
return allDistributions.map(list => ({
...list,
members: (byListId[list.id] || []).map(member => member.uuid),
}));
}
async function modifyStoryDistributionMembers(
listId: string,
{
toAdd,
toRemove,
}: { toAdd: Array<UUIDStringType>; toRemove: Array<UUIDStringType> }
): Promise<void> {
const db = getInstance();
db.transaction(() => {
const memberInsertStatement = prepare(
db,
`
INSERT OR REPLACE INTO storyDistributionMembers (
listId,
uuid
) VALUES (
$listId,
$uuid
);
`
);
for (const uuid of toAdd) {
memberInsertStatement.run({
listId,
uuid,
});
}
batchMultiVarQuery(db, toRemove, (uuids: Array<UUIDStringType>) => {
db.prepare<ArrayQuery>(
`
DELETE FROM storyDistributionMembers
WHERE listId = ? AND uuid IN ( ${uuids.map(() => '?').join(', ')} );
`
).run([listId, ...uuids]);
});
})();
}
async function deleteStoryDistribution(id: UUIDStringType): Promise<void> {
const db = getInstance();
db.prepare<Query>('DELETE FROM storyDistributions WHERE id = $id;').run({
id,
});
}
async function _getAllStoryReads(): Promise<Array<StoryReadType>> {
const db = getInstance();
return db.prepare<EmptyQuery>('SELECT * FROM storyReads;').all();
}
async function _deleteAllStoryReads(): Promise<void> {
const db = getInstance();
db.prepare<EmptyQuery>('DELETE FROM storyReads;').run();
}
async function addNewStoryRead(read: StoryReadType): Promise<void> {
const db = getInstance();
prepare(
db,
`
INSERT OR REPLACE INTO storyReads(
authorId,
conversationId,
storyId,
storyReadDate
) VALUES (
$authorId,
$conversationId,
$storyId,
$storyReadDate
);
`
).run(read);
}
async function getLastStoryReadsForAuthor({
authorId,
conversationId,
limit: initialLimit,
}: {
authorId: UUIDStringType;
conversationId?: UUIDStringType;
limit?: number;
}): Promise<Array<StoryReadType>> {
const limit = initialLimit || 5;
const db = getInstance();
return db
.prepare<Query>(
`
SELECT * FROM storyReads
WHERE
authorId = $authorId AND
($conversationId IS NULL OR conversationId = $conversationId)
ORDER BY storyReadDate DESC
LIMIT $limit;
`
)
.all({
authorId,
conversationId: conversationId || null,
limit,
});
}
// All data in database
async function removeAll(): Promise<void> {
const db = getInstance();
db.transaction(() => {
db.exec(`
DELETE FROM badges;
DELETE FROM attachment_downloads;
DELETE FROM badgeImageFiles;
DELETE FROM badges;
DELETE FROM conversations;
DELETE FROM emojis;
DELETE FROM groupCallRings;
DELETE FROM identityKeys;
DELETE FROM items;
DELETE FROM jobs;
DELETE FROM jobs;
DELETE FROM messages_fts;
DELETE FROM messages;
DELETE FROM preKeys;
DELETE FROM reactions;
DELETE FROM senderKeys;
DELETE FROM sendLogMessageIds;
DELETE FROM sendLogPayloads;
DELETE FROM sendLogRecipients;
DELETE FROM sessions;
DELETE FROM signedPreKeys;
DELETE FROM unprocessed;
DELETE FROM attachment_downloads;
DELETE FROM messages_fts;
DELETE FROM stickers;
DELETE FROM sticker_packs;
DELETE FROM sticker_references;
DELETE FROM jobs;
DELETE FROM stickers;
DELETE FROM storyDistributionMembers;
DELETE FROM storyDistributions;
DELETE FROM storyReads;
DELETE FROM unprocessed;
`);
})();
}
@ -3743,12 +4062,15 @@ async function removeAllConfiguration(
db.exec(
`
DELETE FROM identityKeys;
DELETE FROM jobs;
DELETE FROM preKeys;
DELETE FROM senderKeys;
DELETE FROM sendLogMessageIds;
DELETE FROM sendLogPayloads;
DELETE FROM sendLogRecipients;
DELETE FROM sessions;
DELETE FROM signedPreKeys;
DELETE FROM unprocessed;
DELETE FROM jobs;
`
);
@ -3811,6 +4133,8 @@ async function getMessagesWithVisualMediaAttachments(
.prepare<Query>(
`
SELECT json FROM messages WHERE
type IS NOT 'story' AND
storyId IS NULL AND
conversationId = $conversationId AND
hasVisualMediaAttachments = 1
ORDER BY received_at DESC, sent_at DESC
@ -3834,6 +4158,8 @@ async function getMessagesWithFileAttachments(
.prepare<Query>(
`
SELECT json FROM messages WHERE
type IS NOT 'story' AND
storyId IS NULL AND
conversationId = $conversationId AND
hasFileAttachments = 1
ORDER BY received_at DESC, sent_at DESC

View file

@ -0,0 +1,133 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from 'better-sqlite3';
import type { LoggerType } from '../../types/Logging';
export default function updateToSchemaVersion45(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 45) {
return;
}
db.transaction(() => {
db.exec(
`
--- Add column to messages table
ALTER TABLE messages ADD COLUMN storyId STRING;
--- Update important message indices
DROP INDEX messages_conversation;
CREATE INDEX messages_conversation ON messages
(conversationId, type, storyId, received_at);
DROP INDEX messages_unread;
CREATE INDEX messages_unread ON messages
(conversationId, readStatus, type, storyId) WHERE readStatus IS NOT NULL;
--- Update attachment indices for All Media views
DROP INDEX messages_hasAttachments;
CREATE INDEX messages_hasAttachments
ON messages (conversationId, hasAttachments, received_at)
WHERE type IS NOT 'story' AND storyId IS NULL;
DROP INDEX messages_hasFileAttachments;
CREATE INDEX messages_hasFileAttachments
ON messages (conversationId, hasFileAttachments, received_at)
WHERE type IS NOT 'story' AND storyId IS NULL;
DROP INDEX messages_hasVisualMediaAttachments;
CREATE INDEX messages_hasVisualMediaAttachments
ON messages (conversationId, hasVisualMediaAttachments, received_at)
WHERE type IS NOT 'story' AND storyId IS NULL;
--- Message insert/update triggers to exclude stories and story replies
DROP TRIGGER messages_on_insert;
CREATE TRIGGER messages_on_insert AFTER INSERT ON messages
WHEN new.isViewOnce IS NOT 1 AND new.storyId IS NULL
BEGIN
INSERT INTO messages_fts
(rowid, body)
VALUES
(new.rowid, new.body);
END;
DROP TRIGGER messages_on_update;
CREATE TRIGGER messages_on_update AFTER UPDATE ON messages
WHEN
(new.body IS NULL OR old.body IS NOT new.body) AND
new.isViewOnce IS NOT 1 AND new.storyId IS NULL
BEGIN
DELETE FROM messages_fts WHERE rowid = old.rowid;
INSERT INTO messages_fts
(rowid, body)
VALUES
(new.rowid, new.body);
END;
--- Update delete trigger to remove storyReads
DROP TRIGGER messages_on_delete;
CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN
DELETE FROM messages_fts WHERE rowid = old.rowid;
DELETE FROM sendLogPayloads WHERE id IN (
SELECT payloadId FROM sendLogMessageIds
WHERE messageId = old.id
);
DELETE FROM reactions WHERE rowid IN (
SELECT rowid FROM reactions
WHERE messageId = old.id
);
DELETE FROM storyReads WHERE storyId = old.storyId;
END;
--- Story Read History
CREATE TABLE storyReads (
authorId STRING NOT NULL,
conversationId STRING NOT NULL,
storyId STRING NOT NULL,
storyReadDate NUMBER NOT NULL,
PRIMARY KEY (authorId, storyId)
);
CREATE INDEX storyReads_data ON storyReads (
storyReadDate, authorId, conversationId
);
--- Story Distribution Lists
CREATE TABLE storyDistributions(
id STRING PRIMARY KEY NOT NULL,
name TEXT,
avatarUrlPath TEXT,
avatarKey BLOB,
senderKeyInfoJson STRING
);
CREATE TABLE storyDistributionMembers(
listId STRING NOT NULL REFERENCES storyDistributions(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
uuid STRING NOT NULL,
PRIMARY KEY (listId, uuid)
)
`
);
db.pragma('user_version = 45');
})();
logger.info('updateToSchemaVersion45: success!');
}

View file

@ -20,6 +20,7 @@ import updateToSchemaVersion41 from './41-uuid-keys';
import updateToSchemaVersion42 from './42-stale-reactions';
import updateToSchemaVersion43 from './43-gv2-uuid';
import updateToSchemaVersion44 from './44-badges';
import updateToSchemaVersion45 from './45-stories';
function updateToSchemaVersion1(
currentVersion: number,
@ -1903,6 +1904,7 @@ export const SCHEMA_VERSIONS = [
updateToSchemaVersion42,
updateToSchemaVersion43,
updateToSchemaVersion44,
updateToSchemaVersion45,
];
export function updateSchema(db: Database, logger: LoggerType): void {

View file

@ -0,0 +1,266 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import dataInterface from '../../sql/Client';
import { UUID } from '../../types/UUID';
import type { UUIDStringType } from '../../types/UUID';
import type { MessageAttributesType } from '../../model-types.d';
const {
removeAll,
_getAllMessages,
saveMessages,
getMessagesWithVisualMediaAttachments,
getMessagesWithFileAttachments,
} = dataInterface;
function getUuid(): UUIDStringType {
return UUID.generate().toString();
}
describe('sql/allMedia', () => {
beforeEach(async () => {
await removeAll();
});
describe('getMessagesWithVisualMediaAttachments', () => {
it('returns messages matching with visual attachments', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now();
const conversationId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1',
type: 'outgoing',
conversationId,
sent_at: now - 20,
received_at: now - 20,
timestamp: now - 20,
hasVisualMediaAttachments: true,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: now - 10,
received_at: now - 10,
timestamp: now - 10,
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3',
type: 'outgoing',
conversationId: getUuid(),
sent_at: now,
received_at: now,
timestamp: now,
hasVisualMediaAttachments: true,
};
await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const searchResults = await getMessagesWithVisualMediaAttachments(
conversationId,
{ limit: 5 }
);
assert.lengthOf(searchResults, 1);
assert.strictEqual(searchResults[0].id, message1.id);
});
it('excludes stories and story replies', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now();
const conversationId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1',
type: 'outgoing',
conversationId,
sent_at: now - 20,
received_at: now - 20,
timestamp: now - 20,
hasVisualMediaAttachments: true,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: now - 10,
received_at: now - 10,
timestamp: now - 10,
storyId: getUuid(),
hasVisualMediaAttachments: true,
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3',
type: 'story',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
storyId: getUuid(),
hasVisualMediaAttachments: true,
};
await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const searchResults = await getMessagesWithVisualMediaAttachments(
conversationId,
{ limit: 5 }
);
assert.lengthOf(searchResults, 1);
assert.strictEqual(searchResults[0].id, message1.id);
});
});
describe('getMessagesWithFileAttachments', () => {
it('returns messages matching with visual attachments', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now();
const conversationId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1',
type: 'outgoing',
conversationId,
sent_at: now - 20,
received_at: now - 20,
timestamp: now - 20,
hasFileAttachments: true,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: now - 10,
received_at: now - 10,
timestamp: now - 10,
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3',
type: 'outgoing',
conversationId: getUuid(),
sent_at: now,
received_at: now,
timestamp: now,
hasFileAttachments: true,
};
await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const searchResults = await getMessagesWithFileAttachments(
conversationId,
{ limit: 5 }
);
assert.lengthOf(searchResults, 1);
assert.strictEqual(searchResults[0].id, message1.id);
});
it('excludes stories and story replies', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now();
const conversationId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1',
type: 'outgoing',
conversationId,
sent_at: now - 20,
received_at: now - 20,
timestamp: now - 20,
hasFileAttachments: true,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: now - 10,
received_at: now - 10,
timestamp: now - 10,
storyId: getUuid(),
hasFileAttachments: true,
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3',
type: 'story',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
storyId: getUuid(),
hasFileAttachments: true,
};
await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const searchResults = await getMessagesWithFileAttachments(
conversationId,
{ limit: 5 }
);
assert.lengthOf(searchResults, 1);
assert.strictEqual(searchResults[0].id, message1.id);
});
});
});

View file

@ -0,0 +1,210 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import dataInterface from '../../sql/Client';
import { UUID } from '../../types/UUID';
import type { UUIDStringType } from '../../types/UUID';
import type { MessageAttributesType } from '../../model-types.d';
const {
removeAll,
_getAllMessages,
saveMessages,
saveMessage,
searchMessages,
} = dataInterface;
function getUuid(): UUIDStringType {
return UUID.generate().toString();
}
describe('sql/fullTextSearch', () => {
beforeEach(async () => {
await removeAll();
});
it('returns messages matching query', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now();
const conversationId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1 - generic string',
type: 'outgoing',
conversationId,
sent_at: now - 20,
received_at: now - 20,
timestamp: now - 20,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2 - unique string',
type: 'outgoing',
conversationId,
sent_at: now - 10,
received_at: now - 10,
timestamp: now - 10,
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3 - generic string',
type: 'outgoing',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
};
await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const searchResults = await searchMessages('unique');
assert.lengthOf(searchResults, 1);
assert.strictEqual(searchResults[0].id, message2.id);
message3.body = 'message 3 - unique string';
await saveMessage(message3);
const searchResults2 = await searchMessages('unique');
assert.lengthOf(searchResults2, 2);
assert.strictEqual(searchResults2[0].id, message3.id);
assert.strictEqual(searchResults2[1].id, message2.id);
});
it('excludes messages with isViewOnce = true', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now();
const conversationId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1 - unique string',
type: 'outgoing',
conversationId,
sent_at: now - 20,
received_at: now - 20,
timestamp: now - 20,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2 - unique string',
type: 'outgoing',
conversationId,
sent_at: now - 10,
received_at: now - 10,
timestamp: now - 10,
isViewOnce: true,
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3 - generic string',
type: 'outgoing',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
isViewOnce: true,
};
await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const searchResults = await searchMessages('unique');
assert.lengthOf(searchResults, 1);
assert.strictEqual(searchResults[0].id, message1.id);
message1.body = 'message 3 - unique string';
await saveMessage(message3);
const searchResults2 = await searchMessages('unique');
assert.lengthOf(searchResults2, 1);
assert.strictEqual(searchResults2[0].id, message1.id);
});
it('excludes messages with storyId !== null', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now();
const conversationId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1 - unique string',
type: 'outgoing',
conversationId,
sent_at: now - 20,
received_at: now - 20,
timestamp: now - 20,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2 - unique string',
type: 'outgoing',
conversationId,
sent_at: now - 10,
received_at: now - 10,
timestamp: now - 10,
storyId: getUuid(),
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3 - generic string',
type: 'outgoing',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
storyId: getUuid(),
};
await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const searchResults = await searchMessages('unique');
assert.lengthOf(searchResults, 1);
assert.strictEqual(searchResults[0].id, message1.id);
message1.body = 'message 3 - unique string';
await saveMessage(message3);
const searchResults2 = await searchMessages('unique');
assert.lengthOf(searchResults2, 1);
assert.strictEqual(searchResults2[0].id, message1.id);
});
});

View file

@ -0,0 +1,738 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import dataInterface from '../../sql/Client';
import { UUID } from '../../types/UUID';
import type { UUIDStringType } from '../../types/UUID';
import type { ReactionType } from '../../types/Reactions';
import type { MessageAttributesType } from '../../model-types.d';
import { ReadStatus } from '../../messages/MessageReadStatus';
const {
_removeAllMessages,
_removeAllReactions,
_getAllReactions,
_getAllMessages,
addReaction,
saveMessages,
getTotalUnreadForConversation,
getUnreadByConversationAndMarkRead,
getUnreadReactionsAndMarkRead,
} = dataInterface;
function getUuid(): UUIDStringType {
return UUID.generate().toString();
}
describe('sql/markRead', () => {
beforeEach(async () => {
await _removeAllMessages();
await _removeAllReactions();
});
it('properly finds and reads unread messages in current conversation', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const start = Date.now();
const readAt = start + 20;
const conversationId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1',
type: 'incoming',
conversationId,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
readStatus: ReadStatus.Read,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2',
type: 'incoming',
conversationId,
sent_at: start + 2,
received_at: start + 2,
timestamp: start + 2,
readStatus: ReadStatus.Unread,
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3',
type: 'incoming',
conversationId: getUuid(),
sent_at: start + 3,
received_at: start + 3,
timestamp: start + 3,
readStatus: ReadStatus.Unread,
};
const message4: MessageAttributesType = {
id: getUuid(),
body: 'message 4',
type: 'incoming',
conversationId,
sent_at: start + 4,
received_at: start + 4,
timestamp: start + 4,
readStatus: ReadStatus.Unread,
};
const message5: MessageAttributesType = {
id: getUuid(),
body: 'message 5',
type: 'story',
conversationId,
sent_at: start + 5,
received_at: start + 5,
timestamp: start + 5,
readStatus: ReadStatus.Unread,
storyId: getUuid(),
};
const message6: MessageAttributesType = {
id: getUuid(),
body: 'message 6',
type: 'incoming',
conversationId,
sent_at: start + 6,
received_at: start + 6,
timestamp: start + 6,
readStatus: ReadStatus.Unread,
storyId: getUuid(),
};
const message7: MessageAttributesType = {
id: getUuid(),
body: 'message 7',
type: 'incoming',
conversationId,
sent_at: start + 7,
received_at: start + 7,
timestamp: start + 7,
readStatus: ReadStatus.Unread,
};
await saveMessages(
[message1, message2, message3, message4, message5, message6, message7],
{
forceSave: true,
}
);
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
7
);
assert.strictEqual(
await getTotalUnreadForConversation(conversationId),
3,
'uread count'
);
const markedRead = await getUnreadByConversationAndMarkRead({
conversationId,
newestUnreadAt: message4.received_at,
readAt,
});
assert.lengthOf(markedRead, 2, 'two messages marked read');
assert.strictEqual(
await getTotalUnreadForConversation(conversationId),
1,
'unread count'
);
// Sorted in descending order
assert.strictEqual(
markedRead[0].id,
message4.id,
'first should be message4'
);
assert.strictEqual(
markedRead[1].id,
message2.id,
'second should be message2'
);
const markedRead2 = await getUnreadByConversationAndMarkRead({
conversationId,
newestUnreadAt: message7.received_at,
readAt,
});
assert.lengthOf(markedRead2, 1, 'one message marked read');
assert.strictEqual(markedRead2[0].id, message7.id, 'should be message7');
assert.strictEqual(
await getTotalUnreadForConversation(conversationId),
0,
'unread count'
);
});
it('properly finds and reads unread messages in story', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const start = Date.now();
const readAt = start + 20;
const conversationId = getUuid();
const storyId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
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: getUuid(),
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: getUuid(),
body: 'message 3',
type: 'incoming',
conversationId,
sent_at: start + 3,
received_at: start + 3,
timestamp: start + 3,
readStatus: ReadStatus.Unread,
storyId: getUuid(),
};
const message4: MessageAttributesType = {
id: getUuid(),
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: getUuid(),
body: 'message 5',
type: 'incoming',
conversationId,
sent_at: start + 5,
received_at: start + 5,
timestamp: start + 5,
readStatus: ReadStatus.Unread,
storyId: getUuid(),
};
const message6: MessageAttributesType = {
id: getUuid(),
body: 'message 6',
type: 'incoming',
conversationId,
sent_at: start + 6,
received_at: start + 6,
timestamp: start + 6,
readStatus: ReadStatus.Unread,
storyId: getUuid(),
};
const message7: MessageAttributesType = {
id: getUuid(),
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,
}
);
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
7
);
const markedRead = await getUnreadByConversationAndMarkRead({
conversationId,
newestUnreadAt: message7.received_at,
readAt,
storyId,
});
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 () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const start = Date.now();
const readAt = start + 20;
const conversationId = getUuid();
const expireTimer = 15;
const message1: MessageAttributesType = {
id: getUuid(),
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: getUuid(),
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: getUuid(),
body: 'message 3',
type: 'incoming',
conversationId: getUuid(),
sent_at: start + 3,
received_at: start + 3,
timestamp: start + 3,
expireTimer,
readStatus: ReadStatus.Unread,
};
const message4: MessageAttributesType = {
id: getUuid(),
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: getUuid(),
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,
});
assert.strictEqual(
await getTotalUnreadForConversation(conversationId),
2,
'unread count'
);
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
5
);
const markedRead = await getUnreadByConversationAndMarkRead({
conversationId,
newestUnreadAt: message4.received_at,
readAt,
});
assert.lengthOf(markedRead, 1, 'one message marked read');
assert.strictEqual(
markedRead[0].id,
message4.id,
'first should be message4'
);
assert.strictEqual(
await getTotalUnreadForConversation(conversationId),
1,
'unread count'
);
const allMessages = await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
});
// Ascending order, since it's sorted by MessageCollection
assert.strictEqual(allMessages.at(1).id, message2.id);
assert.isAtMost(
allMessages.at(1).get('expirationStartTimestamp') ?? Infinity,
Date.now(),
'checking message 2 expirationStartTimestamp'
);
assert.strictEqual(allMessages.at(3).id, message4.id, 'checking message 4');
assert.isAtMost(
allMessages.at(3).get('expirationStartTimestamp') ?? Infinity,
Date.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 = getUuid();
const storyId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1',
type: 'incoming',
conversationId,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2',
type: 'incoming',
conversationId,
sent_at: start + 2,
received_at: start + 2,
timestamp: start + 2,
storyId,
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3',
type: 'incoming',
conversationId: getUuid(),
sent_at: start + 3,
received_at: start + 3,
timestamp: start + 3,
};
const message4: MessageAttributesType = {
id: getUuid(),
body: 'message 4',
type: 'incoming',
conversationId,
sent_at: start + 4,
received_at: start + 4,
timestamp: start + 4,
};
const message5: MessageAttributesType = {
id: getUuid(),
body: 'message 5',
type: 'incoming',
conversationId,
sent_at: start + 5,
received_at: start + 5,
timestamp: start + 5,
};
await saveMessages([message1, message2, message3, message4, message5], {
forceSave: true,
});
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
5
);
const reaction1: ReactionType = {
conversationId,
emoji: '🎉',
fromId: getUuid(),
messageId: message1.id,
messageReceivedAt: message1.received_at,
targetAuthorUuid: getUuid(),
targetTimestamp: start,
};
const reaction2: ReactionType = {
conversationId,
emoji: '🚀',
fromId: getUuid(),
messageId: message2.id,
messageReceivedAt: message2.received_at,
targetAuthorUuid: getUuid(),
targetTimestamp: start,
};
const reaction3: ReactionType = {
conversationId: getUuid(),
emoji: '☀️',
fromId: getUuid(),
messageId: message3.id,
messageReceivedAt: message3.received_at,
targetAuthorUuid: getUuid(),
targetTimestamp: start,
};
const reaction4: ReactionType = {
conversationId,
emoji: '❤️‍🔥',
fromId: getUuid(),
messageId: message4.id,
messageReceivedAt: message4.received_at,
targetAuthorUuid: getUuid(),
targetTimestamp: start,
};
const reaction5: ReactionType = {
conversationId,
emoji: '🆒',
fromId: getUuid(),
messageId: message5.id,
messageReceivedAt: message5.received_at,
targetAuthorUuid: getUuid(),
targetTimestamp: start,
};
await addReaction(reaction1);
await addReaction(reaction2);
await addReaction(reaction3);
await addReaction(reaction4);
await addReaction(reaction5);
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 = getUuid();
const storyId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1',
type: 'incoming',
conversationId,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
storyId,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2',
type: 'incoming',
conversationId,
sent_at: start + 2,
received_at: start + 2,
timestamp: start + 2,
storyId: getUuid(),
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3',
type: 'incoming',
conversationId: getUuid(),
sent_at: start + 3,
received_at: start + 3,
timestamp: start + 3,
};
const message4: MessageAttributesType = {
id: getUuid(),
body: 'message 4',
type: 'incoming',
conversationId,
sent_at: start + 4,
received_at: start + 4,
timestamp: start + 4,
storyId,
};
const message5: MessageAttributesType = {
id: getUuid(),
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,
});
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
5
);
const reaction1: ReactionType = {
conversationId,
emoji: '🎉',
fromId: getUuid(),
messageId: message1.id,
messageReceivedAt: message1.received_at,
targetAuthorUuid: getUuid(),
targetTimestamp: start,
};
const reaction2: ReactionType = {
conversationId,
emoji: '🚀',
fromId: getUuid(),
messageId: message2.id,
messageReceivedAt: message2.received_at,
targetAuthorUuid: getUuid(),
targetTimestamp: start,
};
const reaction3: ReactionType = {
conversationId: getUuid(),
emoji: '☀️',
fromId: getUuid(),
messageId: message3.id,
messageReceivedAt: message3.received_at,
targetAuthorUuid: getUuid(),
targetTimestamp: start,
};
const reaction4: ReactionType = {
conversationId,
emoji: '❤️‍🔥',
fromId: getUuid(),
messageId: message4.id,
messageReceivedAt: message4.received_at,
targetAuthorUuid: getUuid(),
targetTimestamp: start,
};
const reaction5: ReactionType = {
conversationId,
emoji: '🆒',
fromId: getUuid(),
messageId: message5.id,
messageReceivedAt: message5.received_at,
targetAuthorUuid: getUuid(),
targetTimestamp: start,
};
await addReaction(reaction1);
await addReaction(reaction2);
await addReaction(reaction3);
await addReaction(reaction4);
await addReaction(reaction5);
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'
);
});
});

View file

@ -23,7 +23,7 @@ const {
saveMessage,
} = dataInterface;
describe('sendLog', () => {
describe('sql/sendLog', () => {
beforeEach(async () => {
await removeAllSentProtos();
});

View file

@ -0,0 +1,244 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import dataInterface from '../../sql/Client';
import { UUID } from '../../types/UUID';
import type { UUIDStringType } from '../../types/UUID';
import type { MessageAttributesType } from '../../model-types.d';
const { removeAll, _getAllMessages, saveMessages, getOlderStories } =
dataInterface;
function getUuid(): UUIDStringType {
return UUID.generate().toString();
}
describe('sql/stories', () => {
beforeEach(async () => {
await removeAll();
});
describe('getOlderStories', () => {
it('returns N most recent stories overall, or in converation, or by author', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now();
const conversationId = getUuid();
const sourceUuid = getUuid();
const story1: MessageAttributesType = {
id: getUuid(),
body: 'story 1',
type: 'story',
conversationId,
sent_at: now - 20,
received_at: now - 20,
timestamp: now - 20,
sourceUuid: getUuid(),
};
const story2: MessageAttributesType = {
id: getUuid(),
body: 'story 2',
type: 'story',
conversationId: getUuid(),
sent_at: now - 10,
received_at: now - 10,
timestamp: now - 10,
sourceUuid,
};
const story3: MessageAttributesType = {
id: getUuid(),
body: 'message 3',
type: 'incoming',
conversationId: getUuid(),
sent_at: now,
received_at: now,
timestamp: now,
sourceUuid,
};
const story4: MessageAttributesType = {
id: getUuid(),
body: 'story 4',
type: 'story',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
sourceUuid: getUuid(),
};
const story5: MessageAttributesType = {
id: getUuid(),
body: 'story 5',
type: 'story',
conversationId: getUuid(),
sent_at: now,
received_at: now,
timestamp: now,
sourceUuid,
};
await saveMessages([story1, story2, story3, story4, story5], {
forceSave: true,
});
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
5
);
const stories = await getOlderStories({
limit: 5,
});
assert.lengthOf(stories, 4, 'expect four total stories');
// They are in DESC order
assert.strictEqual(
stories[0].id,
story5.id,
'stories first should be story5'
);
assert.strictEqual(
stories[3].id,
story1.id,
'stories last should be story1'
);
const storiesInConversation = await getOlderStories({
conversationId,
limit: 5,
});
assert.lengthOf(
storiesInConversation,
2,
'expect two stories in conversaton'
);
// They are in DESC order
assert.strictEqual(
storiesInConversation[0].id,
story4.id,
'storiesInConversation first should be story4'
);
assert.strictEqual(
storiesInConversation[1].id,
story1.id,
'storiesInConversation last should be story1'
);
const storiesByAuthor = await getOlderStories({
sourceUuid,
limit: 5,
});
assert.lengthOf(storiesByAuthor, 2, 'expect two stories by author');
// They are in DESC order
assert.strictEqual(
storiesByAuthor[0].id,
story5.id,
'storiesByAuthor first should be story5'
);
assert.strictEqual(
storiesByAuthor[1].id,
story2.id,
'storiesByAuthor last should be story2'
);
});
it('returns N stories older than provided receivedAt/sentAt', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const start = Date.now();
const conversationId = getUuid();
const story1: MessageAttributesType = {
id: getUuid(),
body: 'message 1',
type: 'incoming',
conversationId,
sent_at: start - 2,
received_at: start - 2,
timestamp: start - 2,
};
const story2: MessageAttributesType = {
id: getUuid(),
body: 'story 2',
type: 'story',
conversationId,
sent_at: start - 1,
received_at: start - 1,
timestamp: start - 1,
};
const story3: MessageAttributesType = {
id: getUuid(),
body: 'story 3',
type: 'story',
conversationId,
sent_at: start - 1,
received_at: start,
timestamp: start,
};
const story4: MessageAttributesType = {
id: getUuid(),
body: 'story 4',
type: 'story',
conversationId,
sent_at: start,
received_at: start,
timestamp: start,
};
const story5: MessageAttributesType = {
id: getUuid(),
body: 'story 5',
type: 'story',
conversationId,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
};
await saveMessages([story1, story2, story3, story4, story5], {
forceSave: true,
});
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
5
);
const stories = await getOlderStories({
receivedAt: story4.received_at,
sentAt: story4.sent_at,
limit: 5,
});
assert.lengthOf(stories, 2, 'expect two stories');
// They are in DESC order
assert.strictEqual(
stories[0].id,
story3.id,
'stories first should be story3'
);
assert.strictEqual(
stories[1].id,
story2.id,
'stories last should be story2'
);
});
});
});

View file

@ -0,0 +1,126 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import dataInterface from '../../sql/Client';
import { getRandomBytes } from '../../Crypto';
import { UUID } from '../../types/UUID';
import type { UUIDStringType } from '../../types/UUID';
import type { StoryDistributionWithMembersType } from '../../sql/Interface';
const {
_deleteAllStoryDistributions,
_getAllStoryDistributionMembers,
_getAllStoryDistributions,
createNewStoryDistribution,
deleteStoryDistribution,
getAllStoryDistributionsWithMembers,
modifyStoryDistributionMembers,
} = dataInterface;
function getUuid(): UUIDStringType {
return UUID.generate().toString();
}
describe('sql/storyDistribution', () => {
beforeEach(async () => {
await _deleteAllStoryDistributions();
});
it('roundtrips with create/fetch/delete', async () => {
const list: StoryDistributionWithMembersType = {
id: getUuid(),
name: 'My Story',
avatarUrlPath: getUuid(),
avatarKey: getRandomBytes(128),
members: [getUuid(), getUuid()],
senderKeyInfo: {
createdAtDate: Date.now(),
distributionId: getUuid(),
memberDevices: [],
},
};
await createNewStoryDistribution(list);
assert.lengthOf(await _getAllStoryDistributions(), 1);
assert.lengthOf(await _getAllStoryDistributionMembers(), 2);
const allHydratedLists = await getAllStoryDistributionsWithMembers();
assert.lengthOf(allHydratedLists, 1);
assert.deepEqual(allHydratedLists[0], list);
await deleteStoryDistribution(list.id);
assert.lengthOf(await _getAllStoryDistributions(), 0);
assert.lengthOf(await _getAllStoryDistributionMembers(), 0);
assert.lengthOf(await getAllStoryDistributionsWithMembers(), 0);
});
it('adds and removes with modifyStoryDistributionMembers', async () => {
const UUID_1 = getUuid();
const UUID_2 = getUuid();
const UUID_3 = getUuid();
const UUID_4 = getUuid();
const list: StoryDistributionWithMembersType = {
id: getUuid(),
name: 'My Story',
avatarUrlPath: getUuid(),
avatarKey: getRandomBytes(128),
members: [UUID_1, UUID_2],
senderKeyInfo: {
createdAtDate: Date.now(),
distributionId: getUuid(),
memberDevices: [],
},
};
await createNewStoryDistribution(list);
assert.lengthOf(await _getAllStoryDistributions(), 1);
assert.lengthOf(await _getAllStoryDistributionMembers(), 2);
await modifyStoryDistributionMembers(list.id, {
toAdd: [UUID_3, UUID_4],
toRemove: [UUID_1],
});
assert.lengthOf(await _getAllStoryDistributions(), 1);
assert.lengthOf(await _getAllStoryDistributionMembers(), 3);
const allHydratedLists = await getAllStoryDistributionsWithMembers();
assert.lengthOf(allHydratedLists, 1);
assert.deepEqual(allHydratedLists[0], {
...list,
members: [UUID_2, UUID_3, UUID_4],
});
});
it('eliminates duplicates without complaint in createNewStoryDistribution', async () => {
const UUID_1 = getUuid();
const UUID_2 = getUuid();
const list: StoryDistributionWithMembersType = {
id: getUuid(),
name: 'My Story',
avatarUrlPath: getUuid(),
avatarKey: getRandomBytes(128),
members: [UUID_1, UUID_1, UUID_2],
senderKeyInfo: {
createdAtDate: Date.now(),
distributionId: getUuid(),
memberDevices: [],
},
};
await createNewStoryDistribution(list);
assert.lengthOf(await _getAllStoryDistributions(), 1);
assert.lengthOf(await _getAllStoryDistributionMembers(), 2);
const allHydratedLists = await getAllStoryDistributionsWithMembers();
assert.lengthOf(allHydratedLists, 1);
assert.deepEqual(allHydratedLists[0].members, [UUID_1, UUID_2]);
});
});

View file

@ -0,0 +1,134 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import dataInterface from '../../sql/Client';
import { UUID } from '../../types/UUID';
import type { UUIDStringType } from '../../types/UUID';
import type { StoryReadType } from '../../sql/Interface';
const {
_getAllStoryReads,
_deleteAllStoryReads,
addNewStoryRead,
getLastStoryReadsForAuthor,
} = dataInterface;
function getUuid(): UUIDStringType {
return UUID.generate().toString();
}
describe('sql/storyReads', () => {
beforeEach(async () => {
await _deleteAllStoryReads();
});
it('roundtrips with create/fetch/delete', async () => {
assert.lengthOf(await _getAllStoryReads(), 0);
const read: StoryReadType = {
authorId: getUuid(),
conversationId: getUuid(),
storyId: getUuid(),
storyReadDate: Date.now(),
};
await addNewStoryRead(read);
const allReads = await _getAllStoryReads();
assert.lengthOf(allReads, 1);
assert.deepEqual(allReads[0], read);
});
describe('getLastStoryReadsForAuthor', () => {
it('returns n = limit items for author', async () => {
const now = Date.now();
const authorId = getUuid();
const read1: StoryReadType = {
authorId,
conversationId: getUuid(),
storyId: getUuid(),
storyReadDate: now - 20,
};
const read2: StoryReadType = {
authorId,
conversationId: getUuid(),
storyId: getUuid(),
storyReadDate: now - 10,
};
const read3: StoryReadType = {
authorId,
conversationId: getUuid(),
storyId: getUuid(),
storyReadDate: now,
};
const read4: StoryReadType = {
authorId: getUuid(),
conversationId: getUuid(),
storyId: getUuid(),
storyReadDate: now,
};
await addNewStoryRead(read1);
await addNewStoryRead(read2);
await addNewStoryRead(read3);
await addNewStoryRead(read4);
assert.lengthOf(await _getAllStoryReads(), 4);
const lastReads = await getLastStoryReadsForAuthor({
authorId,
limit: 2,
});
assert.lengthOf(lastReads, 2);
assert.deepEqual([read3, read2], lastReads);
});
it('returns only items in provided conversation', async () => {
const now = Date.now();
const authorId = getUuid();
const conversationId = getUuid();
const read1: StoryReadType = {
authorId,
conversationId,
storyId: getUuid(),
storyReadDate: now - 20,
};
const read2: StoryReadType = {
authorId,
conversationId,
storyId: getUuid(),
storyReadDate: now - 10,
};
const read3: StoryReadType = {
authorId,
conversationId: getUuid(),
storyId: getUuid(),
storyReadDate: now,
};
const read4: StoryReadType = {
authorId,
conversationId: getUuid(),
storyId: getUuid(),
storyReadDate: now,
};
await addNewStoryRead(read1);
await addNewStoryRead(read2);
await addNewStoryRead(read3);
await addNewStoryRead(read4);
assert.lengthOf(await _getAllStoryReads(), 4);
const lastReads = await getLastStoryReadsForAuthor({
authorId,
conversationId,
limit: 1,
});
assert.lengthOf(lastReads, 1);
assert.deepEqual([read2], lastReads);
});
});
});

View file

@ -0,0 +1,754 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import dataInterface from '../../sql/Client';
import { UUID } from '../../types/UUID';
import type { UUIDStringType } from '../../types/UUID';
import type { MessageAttributesType } from '../../model-types.d';
import { ReadStatus } from '../../messages/MessageReadStatus';
const {
removeAll,
_getAllMessages,
saveMessages,
getMessageMetricsForConversation,
getNewerMessagesByConversation,
getOlderMessagesByConversation,
} = dataInterface;
function getUuid(): UUIDStringType {
return UUID.generate().toString();
}
describe('sql/timelineFetches', () => {
beforeEach(async () => {
await removeAll();
});
describe('getOlderMessagesByConversation', () => {
it('returns N most recent messages', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now();
const conversationId = getUuid();
const storyId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1',
type: 'outgoing',
conversationId,
sent_at: now - 20,
received_at: now - 20,
timestamp: now - 20,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: now - 10,
received_at: now - 10,
timestamp: now - 10,
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3',
type: 'outgoing',
conversationId: getUuid(),
sent_at: now,
received_at: now,
timestamp: now,
};
const message4: MessageAttributesType = {
id: getUuid(),
body: 'message 4',
type: 'story',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
storyId,
};
const message5: MessageAttributesType = {
id: getUuid(),
body: 'message 5',
type: 'outgoing',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
storyId,
};
await saveMessages([message1, message2, message3, message4, message5], {
forceSave: true,
});
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
5
);
const messages = await getOlderMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5,
});
assert.lengthOf(messages, 2);
// They are not in DESC order because MessageCollection is sorting them
assert.strictEqual(messages.at(0).attributes.id, message1.id);
assert.strictEqual(messages.at(1).attributes.id, message2.id);
});
it('returns N most recent messages for a given story', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now();
const conversationId = getUuid();
const storyId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'story',
type: 'story',
conversationId,
sent_at: now - 20,
received_at: now - 20,
timestamp: now - 20,
storyId,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'story reply 1',
type: 'outgoing',
conversationId,
sent_at: now - 10,
received_at: now - 10,
timestamp: now - 10,
storyId,
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'normal message',
type: 'outgoing',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
};
await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const messages = await getOlderMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5,
storyId,
});
assert.lengthOf(messages, 1);
assert.strictEqual(messages.at(0).attributes.id, message2.id);
});
it('returns N messages older than provided received_at', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const target = Date.now();
const conversationId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1',
type: 'outgoing',
conversationId,
sent_at: target - 10,
received_at: target - 10,
timestamp: target - 10,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: target,
received_at: target,
timestamp: target,
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3',
type: 'outgoing',
conversationId,
sent_at: target + 10,
received_at: target + 10,
timestamp: target + 10,
};
await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const messages = await getOlderMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5,
receivedAt: target,
sentAt: target,
});
assert.lengthOf(messages, 1);
assert.strictEqual(messages.at(0).attributes.id, message1.id);
});
it('returns N older messages with received_at, lesser sent_at', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const target = Date.now();
const conversationId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1',
type: 'outgoing',
conversationId,
sent_at: target - 20,
received_at: target,
timestamp: target,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: target - 10,
received_at: target,
timestamp: target,
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3',
type: 'outgoing',
conversationId,
sent_at: target,
received_at: target,
timestamp: target,
};
await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const messages = await getOlderMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5,
receivedAt: target,
sentAt: target,
});
assert.lengthOf(messages, 2);
// They are not in DESC order because MessageCollection is sorting them
assert.strictEqual(messages.at(0).attributes.id, message1.id);
assert.strictEqual(messages.at(1).attributes.id, message2.id);
});
it('returns N older messages, same received_at/sent_at but excludes messageId', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const target = Date.now();
const conversationId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1',
type: 'outgoing',
conversationId,
sent_at: target - 10,
received_at: target,
timestamp: target,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: target - 10,
received_at: target,
timestamp: target,
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3',
type: 'outgoing',
conversationId,
sent_at: target,
received_at: target,
timestamp: target,
};
await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const messages = await getOlderMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5,
receivedAt: target,
sentAt: target,
messageId: message2.id,
});
assert.lengthOf(messages, 1);
assert.strictEqual(messages.at(0).attributes.id, message1.id);
});
});
describe('getNewerMessagesByConversation', () => {
it('returns N oldest messages with no parameters', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now();
const conversationId = getUuid();
const storyId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1',
type: 'outgoing',
conversationId: getUuid(),
sent_at: now,
received_at: now,
timestamp: now,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2',
type: 'story',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
storyId,
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3',
type: 'outgoing',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
storyId,
};
const message4: MessageAttributesType = {
id: getUuid(),
body: 'message 4',
type: 'outgoing',
conversationId,
sent_at: now + 10,
received_at: now + 10,
timestamp: now + 10,
};
const message5: MessageAttributesType = {
id: getUuid(),
body: 'message 5',
type: 'outgoing',
conversationId,
sent_at: now + 20,
received_at: now + 20,
timestamp: now + 20,
};
await saveMessages([message1, message2, message3, message4, message5], {
forceSave: true,
});
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
5
);
const messages = await getNewerMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5,
});
assert.lengthOf(messages, 2);
assert.strictEqual(messages.at(0).attributes.id, message4.id);
assert.strictEqual(messages.at(1).attributes.id, message5.id);
});
it('returns N oldest messages for a given story with no parameters', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now();
const conversationId = getUuid();
const storyId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1',
type: 'story',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
storyId,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: now + 10,
received_at: now + 10,
timestamp: now + 10,
storyId,
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3',
type: 'outgoing',
conversationId,
sent_at: now + 20,
received_at: now + 20,
timestamp: now + 20,
};
await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const messages = await getNewerMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5,
storyId,
});
assert.lengthOf(messages, 1);
assert.strictEqual(messages.at(0).attributes.id, message2.id);
});
it('returns N messages newer than provided received_at', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const target = Date.now();
const conversationId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1',
type: 'outgoing',
conversationId,
sent_at: target - 10,
received_at: target - 10,
timestamp: target - 10,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: target,
received_at: target,
timestamp: target,
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3',
type: 'outgoing',
conversationId,
sent_at: target + 10,
received_at: target + 10,
timestamp: target + 10,
};
await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const messages = await getNewerMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5,
receivedAt: target,
sentAt: target,
});
assert.lengthOf(messages, 1);
assert.strictEqual(messages.at(0).attributes.id, message3.id);
});
it('returns N newer messages with same received_at, greater sent_at', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const target = Date.now();
const conversationId = getUuid();
const message1: MessageAttributesType = {
id: getUuid(),
body: 'message 1',
type: 'outgoing',
conversationId,
sent_at: target,
received_at: target,
timestamp: target,
};
const message2: MessageAttributesType = {
id: getUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: target + 10,
received_at: target,
timestamp: target,
};
const message3: MessageAttributesType = {
id: getUuid(),
body: 'message 3',
type: 'outgoing',
conversationId,
sent_at: target + 20,
received_at: target,
timestamp: target,
};
await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const messages = await getNewerMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5,
receivedAt: target,
sentAt: target,
});
assert.lengthOf(messages, 2);
// They are not in DESC order because MessageCollection is sorting them
assert.strictEqual(messages.at(0).attributes.id, message2.id);
assert.strictEqual(messages.at(1).attributes.id, message3.id);
});
});
describe('getMessageMetricsForConversation', () => {
it('returns metrics properly for story and non-story timelines', async () => {
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const target = Date.now();
const conversationId = getUuid();
const storyId = getUuid();
const story: MessageAttributesType = {
id: getUuid(),
body: 'story',
type: 'story',
conversationId,
sent_at: target - 10,
received_at: target - 10,
timestamp: target - 10,
};
const oldestInStory: MessageAttributesType = {
id: getUuid(),
body: 'oldestInStory',
type: 'outgoing',
conversationId,
sent_at: target - 9,
received_at: target - 9,
timestamp: target - 9,
storyId,
};
const oldest: MessageAttributesType = {
id: getUuid(),
body: 'oldest',
type: 'outgoing',
conversationId,
sent_at: target - 8,
received_at: target - 8,
timestamp: target - 8,
};
const oldestUnread: MessageAttributesType = {
id: getUuid(),
body: 'oldestUnread',
type: 'incoming',
conversationId,
sent_at: target - 7,
received_at: target - 7,
timestamp: target - 7,
readStatus: ReadStatus.Unread,
};
const oldestStoryUnread: MessageAttributesType = {
id: getUuid(),
body: 'oldestStoryUnread',
type: 'incoming',
conversationId,
sent_at: target - 6,
received_at: target - 6,
timestamp: target - 6,
readStatus: ReadStatus.Unread,
storyId,
};
const anotherUnread: MessageAttributesType = {
id: getUuid(),
body: 'anotherUnread',
type: 'incoming',
conversationId,
sent_at: target - 5,
received_at: target - 5,
timestamp: target - 5,
readStatus: ReadStatus.Unread,
};
const newestInStory: MessageAttributesType = {
id: getUuid(),
body: 'newestStory',
type: 'outgoing',
conversationId,
sent_at: target - 4,
received_at: target - 4,
timestamp: target - 4,
storyId,
};
const newest: MessageAttributesType = {
id: getUuid(),
body: 'newest',
type: 'outgoing',
conversationId,
sent_at: target - 3,
received_at: target - 3,
timestamp: target - 3,
};
await saveMessages(
[
story,
oldestInStory,
oldest,
oldestUnread,
oldestStoryUnread,
anotherUnread,
newestInStory,
newest,
],
{ forceSave: true }
);
assert.lengthOf(
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
8
);
const metricsInTimeline = await getMessageMetricsForConversation(
conversationId
);
assert.strictEqual(metricsInTimeline?.oldest?.id, oldest.id, 'oldest');
assert.strictEqual(metricsInTimeline?.newest?.id, newest.id, 'newest');
assert.strictEqual(
metricsInTimeline?.oldestUnread?.id,
oldestUnread.id,
'oldestUnread'
);
assert.strictEqual(metricsInTimeline?.totalUnread, 2, 'totalUnread');
const metricsInStory = await getMessageMetricsForConversation(
conversationId,
storyId
);
assert.strictEqual(
metricsInStory?.oldest?.id,
oldestInStory.id,
'oldestInStory'
);
assert.strictEqual(
metricsInStory?.newest?.id,
newestInStory.id,
'newestInStory'
);
assert.strictEqual(
metricsInStory?.oldestUnread?.id,
oldestStoryUnread.id,
'oldestStoryUnread'
);
assert.strictEqual(metricsInStory?.totalUnread, 1, 'totalUnread');
});
});
});

View file

@ -802,4 +802,111 @@ describe('SQL migrations test', () => {
});
});
});
describe('updateToSchemaVersion45', () => {
it('creates new storyId field and delete trigger for storyReads', () => {
const AUTHOR_ID = generateGuid();
const STORY_ID_1 = generateGuid();
const STORY_ID_2 = generateGuid();
const MESSAGE_ID_1 = generateGuid();
const MESSAGE_ID_2 = generateGuid();
const MESSAGE_ID_3 = generateGuid();
const MESSAGE_ID_4 = generateGuid();
const MESSAGE_ID_5 = generateGuid();
const CONVERSATION_ID = generateGuid();
updateToVersion(45);
db.exec(
`
INSERT INTO messages
(id, storyId, conversationId, type, body)
VALUES
('${MESSAGE_ID_1}', '${STORY_ID_1}', '${CONVERSATION_ID}', 'story', 'story 1'),
('${MESSAGE_ID_2}', '${STORY_ID_2}', '${CONVERSATION_ID}', 'story', 'story 2'),
('${MESSAGE_ID_3}', '${STORY_ID_1}', '${CONVERSATION_ID}', 'outgoing', 'reply to story 1'),
('${MESSAGE_ID_4}', '${STORY_ID_1}', '${CONVERSATION_ID}', 'incoming', 'reply to story 1'),
('${MESSAGE_ID_5}', '${STORY_ID_2}', '${CONVERSATION_ID}', 'outgoing', 'reply to story 2');
INSERT INTO storyReads (authorId, conversationId, storyId, storyReadDate) VALUES
('${AUTHOR_ID}', '${CONVERSATION_ID}', '${STORY_ID_1}', ${Date.now()}),
('${AUTHOR_ID}', '${CONVERSATION_ID}', '${STORY_ID_2}', ${Date.now()}); `
);
const storyReadCount = db
.prepare('SELECT COUNT(*) FROM storyReads;')
.pluck();
const messageCount = db.prepare('SELECT COUNT(*) FROM messages;').pluck();
assert.strictEqual(storyReadCount.get(), 2);
assert.strictEqual(messageCount.get(), 5);
db.exec(`DELETE FROM messages WHERE id = '${MESSAGE_ID_1}';`);
assert.strictEqual(storyReadCount.get(), 1);
assert.strictEqual(messageCount.get(), 4);
db.exec(`DELETE FROM messages WHERE storyId = '${STORY_ID_1}';`);
assert.strictEqual(storyReadCount.get(), 1);
assert.strictEqual(messageCount.get(), 2);
const storyReadIds = db
.prepare('SELECT storyId FROM storyReads;')
.pluck()
.all();
assert.sameDeepMembers(storyReadIds, [STORY_ID_2]);
});
it('creates new storyDistributions/Members with cascade delete', () => {
const LIST_ID_1 = generateGuid();
const LIST_ID_2 = generateGuid();
const UUID_1 = generateGuid();
const UUID_2 = generateGuid();
const UUID_3 = generateGuid();
const UUID_4 = generateGuid();
updateToVersion(45);
db.exec(
`
INSERT INTO storyDistributions
(id, name)
VALUES
('${LIST_ID_1}', 'distribution list 1'),
('${LIST_ID_2}', 'distrubution list 2');
INSERT INTO storyDistributionMembers (listId, uuid) VALUES
('${LIST_ID_1}', '${UUID_1}'),
('${LIST_ID_1}', '${UUID_2}'),
('${LIST_ID_1}', '${UUID_3}'),
('${LIST_ID_1}', '${UUID_4}'),
('${LIST_ID_2}', '${UUID_1}'),
('${LIST_ID_2}', '${UUID_2}');
`
);
const listCount = db
.prepare('SELECT COUNT(*) FROM storyDistributions;')
.pluck();
const memberCount = db
.prepare('SELECT COUNT(*) FROM storyDistributionMembers;')
.pluck();
assert.strictEqual(listCount.get(), 2);
assert.strictEqual(memberCount.get(), 6);
db.exec(`DELETE FROM storyDistributions WHERE id = '${LIST_ID_1}';`);
assert.strictEqual(listCount.get(), 1);
assert.strictEqual(memberCount.get(), 2);
const members = db
.prepare('SELECT uuid FROM storyDistributionMembers;')
.pluck()
.all();
assert.sameDeepMembers(members, [UUID_1, UUID_2]);
});
});
});

View file

@ -5,7 +5,7 @@ export type ReactionType = Readonly<{
conversationId: string;
emoji: string;
fromId: string;
messageId: string | undefined;
messageId: string;
messageReceivedAt: number;
targetAuthorUuid: string;
targetTimestamp: number;

View file

@ -10,7 +10,7 @@ import * as log from '../logging/log';
export async function markConversationRead(
conversationAttrs: ConversationAttributesType,
newestUnreadId: number,
newestUnreadAt: number,
options: { readAt?: number; sendReadReceipts: boolean } = {
sendReadReceipts: true,
}
@ -18,20 +18,20 @@ export async function markConversationRead(
const { id: conversationId } = conversationAttrs;
const [unreadMessages, unreadReactions] = await Promise.all([
window.Signal.Data.getUnreadByConversationAndMarkRead(
window.Signal.Data.getUnreadByConversationAndMarkRead({
conversationId,
newestUnreadId,
options.readAt
),
window.Signal.Data.getUnreadReactionsAndMarkRead(
newestUnreadAt,
readAt: options.readAt,
}),
window.Signal.Data.getUnreadReactionsAndMarkRead({
conversationId,
newestUnreadId
),
newestUnreadAt,
}),
]);
log.info('markConversationRead', {
conversationId,
newestUnreadId,
newestUnreadAt,
unreadMessages: unreadMessages.length,
unreadReactions: unreadReactions.length,
});