Migration and data access functions for stories
This commit is contained in:
parent
9f4a01c535
commit
fdc9885baa
18 changed files with 3428 additions and 202 deletions
14
ts/model-types.d.ts
vendored
14
ts/model-types.d.ts
vendored
|
@ -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?: {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
156
ts/sql/Client.ts
156
ts/sql/Client.ts
|
@ -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() {
|
||||
|
|
|
@ -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: {
|
||||
|
|
612
ts/sql/Server.ts
612
ts/sql/Server.ts
|
@ -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
|
||||
|
|
133
ts/sql/migrations/45-stories.ts
Normal file
133
ts/sql/migrations/45-stories.ts
Normal 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!');
|
||||
}
|
|
@ -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 {
|
||||
|
|
266
ts/test-electron/sql/allMedia_test.ts
Normal file
266
ts/test-electron/sql/allMedia_test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
210
ts/test-electron/sql/fullTextSearch_test.ts
Normal file
210
ts/test-electron/sql/fullTextSearch_test.ts
Normal 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);
|
||||
});
|
||||
});
|
738
ts/test-electron/sql/markRead_test.ts
Normal file
738
ts/test-electron/sql/markRead_test.ts
Normal 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'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -23,7 +23,7 @@ const {
|
|||
saveMessage,
|
||||
} = dataInterface;
|
||||
|
||||
describe('sendLog', () => {
|
||||
describe('sql/sendLog', () => {
|
||||
beforeEach(async () => {
|
||||
await removeAllSentProtos();
|
||||
});
|
||||
|
|
244
ts/test-electron/sql/stories_test.ts
Normal file
244
ts/test-electron/sql/stories_test.ts
Normal 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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
126
ts/test-electron/sql/storyDistribution_test.ts
Normal file
126
ts/test-electron/sql/storyDistribution_test.ts
Normal 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]);
|
||||
});
|
||||
});
|
134
ts/test-electron/sql/storyReads_test.ts
Normal file
134
ts/test-electron/sql/storyReads_test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
754
ts/test-electron/sql/timelineFetches_test.ts
Normal file
754
ts/test-electron/sql/timelineFetches_test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue