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;
|
type TaskResultType = any;
|
||||||
|
|
||||||
|
export type SenderKeyInfoType = {
|
||||||
|
createdAtDate: number;
|
||||||
|
distributionId: string;
|
||||||
|
memberDevices: Array<DeviceType>;
|
||||||
|
};
|
||||||
|
|
||||||
export type CustomError = Error & {
|
export type CustomError = Error & {
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
number?: string;
|
number?: string;
|
||||||
|
@ -131,6 +137,7 @@ export type MessageAttributesType = {
|
||||||
requiredProtocolVersion?: number;
|
requiredProtocolVersion?: number;
|
||||||
retryOptions?: RetryOptions;
|
retryOptions?: RetryOptions;
|
||||||
sourceDevice?: number;
|
sourceDevice?: number;
|
||||||
|
storyId?: string;
|
||||||
supportedVersionAtReceive?: unknown;
|
supportedVersionAtReceive?: unknown;
|
||||||
synced?: boolean;
|
synced?: boolean;
|
||||||
unidentifiedDeliveryReceived?: boolean;
|
unidentifiedDeliveryReceived?: boolean;
|
||||||
|
@ -150,6 +157,7 @@ export type MessageAttributesType = {
|
||||||
| 'message-history-unsynced'
|
| 'message-history-unsynced'
|
||||||
| 'outgoing'
|
| 'outgoing'
|
||||||
| 'profile-change'
|
| 'profile-change'
|
||||||
|
| 'story'
|
||||||
| 'timer-notification'
|
| 'timer-notification'
|
||||||
| 'universal-timer-notification'
|
| 'universal-timer-notification'
|
||||||
| 'change-number-notification'
|
| 'change-number-notification'
|
||||||
|
@ -301,11 +309,7 @@ export type ConversationAttributesType = {
|
||||||
secretParams?: string;
|
secretParams?: string;
|
||||||
publicParams?: string;
|
publicParams?: string;
|
||||||
revision?: number;
|
revision?: number;
|
||||||
senderKeyInfo?: {
|
senderKeyInfo?: SenderKeyInfoType;
|
||||||
createdAtDate: number;
|
|
||||||
distributionId: string;
|
|
||||||
memberDevices: Array<DeviceType>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// GroupV2 other fields
|
// GroupV2 other fields
|
||||||
accessControl?: {
|
accessControl?: {
|
||||||
|
|
|
@ -4554,14 +4554,14 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
async markRead(
|
async markRead(
|
||||||
newestUnreadId: number,
|
newestUnreadAt: number,
|
||||||
options: { readAt?: number; sendReadReceipts: boolean } = {
|
options: { readAt?: number; sendReadReceipts: boolean } = {
|
||||||
sendReadReceipts: true,
|
sendReadReceipts: true,
|
||||||
}
|
}
|
||||||
): Promise<void> {
|
): 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
|
this.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
156
ts/sql/Client.ts
156
ts/sql/Client.ts
|
@ -80,6 +80,10 @@ import type {
|
||||||
StickerPackStatusType,
|
StickerPackStatusType,
|
||||||
StickerPackType,
|
StickerPackType,
|
||||||
StickerType,
|
StickerType,
|
||||||
|
StoryDistributionType,
|
||||||
|
StoryDistributionMemberType,
|
||||||
|
StoryDistributionWithMembersType,
|
||||||
|
StoryReadType,
|
||||||
UnprocessedType,
|
UnprocessedType,
|
||||||
UnprocessedUpdateType,
|
UnprocessedUpdateType,
|
||||||
} from './Interface';
|
} from './Interface';
|
||||||
|
@ -217,13 +221,14 @@ const dataInterface: ClientInterface = {
|
||||||
saveMessages,
|
saveMessages,
|
||||||
removeMessage,
|
removeMessage,
|
||||||
removeMessages,
|
removeMessages,
|
||||||
getUnreadCountForConversation,
|
getTotalUnreadForConversation,
|
||||||
getUnreadByConversationAndMarkRead,
|
getUnreadByConversationAndMarkRead,
|
||||||
getUnreadReactionsAndMarkRead,
|
getUnreadReactionsAndMarkRead,
|
||||||
markReactionAsRead,
|
markReactionAsRead,
|
||||||
removeReactionFromConversation,
|
removeReactionFromConversation,
|
||||||
addReaction,
|
addReaction,
|
||||||
_getAllReactions,
|
_getAllReactions,
|
||||||
|
_removeAllReactions,
|
||||||
|
|
||||||
getMessageBySender,
|
getMessageBySender,
|
||||||
getMessageById,
|
getMessageById,
|
||||||
|
@ -236,6 +241,7 @@ const dataInterface: ClientInterface = {
|
||||||
getNextTapToViewMessageTimestampToAgeOut,
|
getNextTapToViewMessageTimestampToAgeOut,
|
||||||
getTapToViewMessagesNeedingErase,
|
getTapToViewMessagesNeedingErase,
|
||||||
getOlderMessagesByConversation,
|
getOlderMessagesByConversation,
|
||||||
|
getOlderStories,
|
||||||
getNewerMessagesByConversation,
|
getNewerMessagesByConversation,
|
||||||
getLastConversationMessages,
|
getLastConversationMessages,
|
||||||
getMessageMetricsForConversation,
|
getMessageMetricsForConversation,
|
||||||
|
@ -277,6 +283,19 @@ const dataInterface: ClientInterface = {
|
||||||
updateOrCreateBadges,
|
updateOrCreateBadges,
|
||||||
badgeImageFileDownloaded,
|
badgeImageFileDownloaded,
|
||||||
|
|
||||||
|
_getAllStoryDistributions,
|
||||||
|
_getAllStoryDistributionMembers,
|
||||||
|
_deleteAllStoryDistributions,
|
||||||
|
createNewStoryDistribution,
|
||||||
|
getAllStoryDistributionsWithMembers,
|
||||||
|
modifyStoryDistributionMembers,
|
||||||
|
deleteStoryDistribution,
|
||||||
|
|
||||||
|
_getAllStoryReads,
|
||||||
|
_deleteAllStoryReads,
|
||||||
|
addNewStoryRead,
|
||||||
|
getLastStoryReadsForAuthor,
|
||||||
|
|
||||||
removeAll,
|
removeAll,
|
||||||
removeAllConfiguration,
|
removeAllConfiguration,
|
||||||
|
|
||||||
|
@ -300,6 +319,7 @@ const dataInterface: ClientInterface = {
|
||||||
// Test-only
|
// Test-only
|
||||||
|
|
||||||
_getAllMessages,
|
_getAllMessages,
|
||||||
|
_removeAllMessages,
|
||||||
|
|
||||||
// Client-side only
|
// Client-side only
|
||||||
|
|
||||||
|
@ -1190,6 +1210,9 @@ async function _getAllMessages({
|
||||||
|
|
||||||
return new MessageCollection(messages);
|
return new MessageCollection(messages);
|
||||||
}
|
}
|
||||||
|
async function _removeAllMessages() {
|
||||||
|
await channels._removeAllMessages();
|
||||||
|
}
|
||||||
|
|
||||||
async function getAllMessageIds() {
|
async function getAllMessageIds() {
|
||||||
const ids = await channels.getAllMessageIds();
|
const ids = await channels.getAllMessageIds();
|
||||||
|
@ -1224,27 +1247,28 @@ async function getMessageBySender(
|
||||||
return new Message(messages[0]);
|
return new Message(messages[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUnreadCountForConversation(conversationId: string) {
|
async function getTotalUnreadForConversation(
|
||||||
return channels.getUnreadCountForConversation(conversationId);
|
conversationId: string,
|
||||||
|
storyId?: UUIDStringType
|
||||||
|
) {
|
||||||
|
return channels.getTotalUnreadForConversation(conversationId, storyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUnreadByConversationAndMarkRead(
|
async function getUnreadByConversationAndMarkRead(options: {
|
||||||
conversationId: string,
|
conversationId: string;
|
||||||
newestUnreadId: number,
|
newestUnreadAt: number;
|
||||||
readAt?: number
|
readAt?: number;
|
||||||
) {
|
storyId?: UUIDStringType;
|
||||||
return channels.getUnreadByConversationAndMarkRead(
|
}) {
|
||||||
conversationId,
|
return channels.getUnreadByConversationAndMarkRead(options);
|
||||||
newestUnreadId,
|
|
||||||
readAt
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUnreadReactionsAndMarkRead(
|
async function getUnreadReactionsAndMarkRead(options: {
|
||||||
conversationId: string,
|
conversationId: string;
|
||||||
newestUnreadId: number
|
newestUnreadAt: number;
|
||||||
) {
|
storyId?: UUIDStringType;
|
||||||
return channels.getUnreadReactionsAndMarkRead(conversationId, newestUnreadId);
|
}) {
|
||||||
|
return channels.getUnreadReactionsAndMarkRead(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function markReactionAsRead(
|
async function markReactionAsRead(
|
||||||
|
@ -1270,6 +1294,9 @@ async function addReaction(reactionObj: ReactionType) {
|
||||||
async function _getAllReactions() {
|
async function _getAllReactions() {
|
||||||
return channels._getAllReactions();
|
return channels._getAllReactions();
|
||||||
}
|
}
|
||||||
|
async function _removeAllReactions() {
|
||||||
|
await channels._removeAllReactions();
|
||||||
|
}
|
||||||
|
|
||||||
function handleMessageJSON(messages: Array<MessageTypeUnhydrated>) {
|
function handleMessageJSON(messages: Array<MessageTypeUnhydrated>) {
|
||||||
return messages.map(message => JSON.parse(message.json));
|
return messages.map(message => JSON.parse(message.json));
|
||||||
|
@ -1279,16 +1306,18 @@ async function getOlderMessagesByConversation(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
{
|
{
|
||||||
limit = 100,
|
limit = 100,
|
||||||
|
MessageCollection,
|
||||||
|
messageId,
|
||||||
receivedAt = Number.MAX_VALUE,
|
receivedAt = Number.MAX_VALUE,
|
||||||
sentAt = Number.MAX_VALUE,
|
sentAt = Number.MAX_VALUE,
|
||||||
messageId,
|
storyId,
|
||||||
MessageCollection,
|
|
||||||
}: {
|
}: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
|
messageId?: string;
|
||||||
receivedAt?: number;
|
receivedAt?: number;
|
||||||
sentAt?: number;
|
sentAt?: number;
|
||||||
messageId?: string;
|
storyId?: UUIDStringType;
|
||||||
MessageCollection: typeof MessageModelCollectionType;
|
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const messages = await channels.getOlderMessagesByConversation(
|
const messages = await channels.getOlderMessagesByConversation(
|
||||||
|
@ -1298,23 +1327,36 @@ async function getOlderMessagesByConversation(
|
||||||
receivedAt,
|
receivedAt,
|
||||||
sentAt,
|
sentAt,
|
||||||
messageId,
|
messageId,
|
||||||
|
storyId,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return new MessageCollection(handleMessageJSON(messages));
|
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(
|
async function getNewerMessagesByConversation(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
{
|
{
|
||||||
limit = 100,
|
limit = 100,
|
||||||
|
MessageCollection,
|
||||||
receivedAt = 0,
|
receivedAt = 0,
|
||||||
sentAt = 0,
|
sentAt = 0,
|
||||||
MessageCollection,
|
storyId,
|
||||||
}: {
|
}: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
receivedAt?: number;
|
receivedAt?: number;
|
||||||
sentAt?: number;
|
sentAt?: number;
|
||||||
MessageCollection: typeof MessageModelCollectionType;
|
storyId?: UUIDStringType;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const messages = await channels.getNewerMessagesByConversation(
|
const messages = await channels.getNewerMessagesByConversation(
|
||||||
|
@ -1323,6 +1365,7 @@ async function getNewerMessagesByConversation(
|
||||||
limit,
|
limit,
|
||||||
receivedAt,
|
receivedAt,
|
||||||
sentAt,
|
sentAt,
|
||||||
|
storyId,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1349,9 +1392,13 @@ async function getLastConversationMessages({
|
||||||
hasUserInitiatedMessages,
|
hasUserInitiatedMessages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
async function getMessageMetricsForConversation(conversationId: string) {
|
async function getMessageMetricsForConversation(
|
||||||
|
conversationId: string,
|
||||||
|
storyId?: UUIDStringType
|
||||||
|
) {
|
||||||
const result = await channels.getMessageMetricsForConversation(
|
const result = await channels.getMessageMetricsForConversation(
|
||||||
conversationId
|
conversationId,
|
||||||
|
storyId
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -1597,6 +1644,63 @@ function badgeImageFileDownloaded(
|
||||||
return channels.badgeImageFileDownloaded(url, localPath);
|
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
|
// Other
|
||||||
|
|
||||||
async function removeAll() {
|
async function removeAll() {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import type {
|
||||||
ConversationModelCollectionType,
|
ConversationModelCollectionType,
|
||||||
MessageAttributesType,
|
MessageAttributesType,
|
||||||
MessageModelCollectionType,
|
MessageModelCollectionType,
|
||||||
|
SenderKeyInfoType,
|
||||||
} from '../model-types.d';
|
} from '../model-types.d';
|
||||||
import type { MessageModel } from '../models/messages';
|
import type { MessageModel } from '../models/messages';
|
||||||
import type { ConversationModel } from '../models/conversations';
|
import type { ConversationModel } from '../models/conversations';
|
||||||
|
@ -239,6 +240,31 @@ export type DeleteSentProtoRecipientOptionsType = Readonly<{
|
||||||
deviceId: number;
|
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 = {
|
export type DataInterface = {
|
||||||
close: () => Promise<void>;
|
close: () => Promise<void>;
|
||||||
removeDB: () => Promise<void>;
|
removeDB: () => Promise<void>;
|
||||||
|
@ -349,8 +375,16 @@ export type DataInterface = {
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
getMessageCount: (conversationId?: string) => Promise<number>;
|
getMessageCount: (conversationId?: string) => Promise<number>;
|
||||||
getAllMessageIds: () => Promise<Array<string>>;
|
getAllMessageIds: () => Promise<Array<string>>;
|
||||||
|
getOlderStories: (options: {
|
||||||
|
conversationId?: string;
|
||||||
|
limit?: number;
|
||||||
|
receivedAt?: number;
|
||||||
|
sentAt?: number;
|
||||||
|
sourceUuid?: string;
|
||||||
|
}) => Promise<Array<MessageType>>;
|
||||||
getMessageMetricsForConversation: (
|
getMessageMetricsForConversation: (
|
||||||
conversationId: string
|
conversationId: string,
|
||||||
|
storyId?: UUIDStringType
|
||||||
) => Promise<ConversationMetricsType>;
|
) => Promise<ConversationMetricsType>;
|
||||||
hasGroupCallHistoryMessage: (
|
hasGroupCallHistoryMessage: (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
|
@ -361,21 +395,27 @@ export type DataInterface = {
|
||||||
currentId: string
|
currentId: string
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
getNextTapToViewMessageTimestampToAgeOut: () => Promise<undefined | number>;
|
getNextTapToViewMessageTimestampToAgeOut: () => Promise<undefined | number>;
|
||||||
|
_removeAllMessages: () => Promise<void>;
|
||||||
|
|
||||||
getUnreadCountForConversation: (conversationId: string) => Promise<number>;
|
getTotalUnreadForConversation: (
|
||||||
getUnreadByConversationAndMarkRead: (
|
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
newestUnreadId: number,
|
storyId?: UUIDStringType
|
||||||
readAt?: number
|
) => Promise<number>;
|
||||||
) => Promise<
|
getUnreadByConversationAndMarkRead: (options: {
|
||||||
|
conversationId: string;
|
||||||
|
newestUnreadAt: number;
|
||||||
|
readAt?: number;
|
||||||
|
storyId?: UUIDStringType;
|
||||||
|
}) => Promise<
|
||||||
Array<
|
Array<
|
||||||
Pick<MessageType, 'id' | 'source' | 'sourceUuid' | 'sent_at' | 'type'>
|
Pick<MessageType, 'id' | 'source' | 'sourceUuid' | 'sent_at' | 'type'>
|
||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
getUnreadReactionsAndMarkRead: (
|
getUnreadReactionsAndMarkRead: (options: {
|
||||||
conversationId: string,
|
conversationId: string;
|
||||||
newestUnreadId: number
|
newestUnreadAt: number;
|
||||||
) => Promise<
|
storyId?: UUIDStringType;
|
||||||
|
}) => Promise<
|
||||||
Array<
|
Array<
|
||||||
Pick<ReactionType, 'targetAuthorUuid' | 'targetTimestamp' | 'messageId'>
|
Pick<ReactionType, 'targetAuthorUuid' | 'targetTimestamp' | 'messageId'>
|
||||||
>
|
>
|
||||||
|
@ -392,6 +432,7 @@ export type DataInterface = {
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
addReaction: (reactionObj: ReactionType) => Promise<void>;
|
addReaction: (reactionObj: ReactionType) => Promise<void>;
|
||||||
_getAllReactions: () => Promise<Array<ReactionType>>;
|
_getAllReactions: () => Promise<Array<ReactionType>>;
|
||||||
|
_removeAllReactions: () => Promise<void>;
|
||||||
|
|
||||||
getUnprocessedCount: () => Promise<number>;
|
getUnprocessedCount: () => Promise<number>;
|
||||||
getAllUnprocessed: () => Promise<Array<UnprocessedType>>;
|
getAllUnprocessed: () => Promise<Array<UnprocessedType>>;
|
||||||
|
@ -452,6 +493,35 @@ export type DataInterface = {
|
||||||
updateOrCreateBadges(badges: ReadonlyArray<BadgeType>): Promise<void>;
|
updateOrCreateBadges(badges: ReadonlyArray<BadgeType>): Promise<void>;
|
||||||
badgeImageFileDownloaded(url: string, localPath: string): 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>;
|
removeAll: () => Promise<void>;
|
||||||
removeAllConfiguration: (type?: RemoveAllConfiguration) => Promise<void>;
|
removeAllConfiguration: (type?: RemoveAllConfiguration) => Promise<void>;
|
||||||
|
|
||||||
|
@ -525,14 +595,20 @@ export type ServerInterface = DataInterface & {
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
options?: {
|
options?: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
messageId?: string;
|
||||||
receivedAt?: number;
|
receivedAt?: number;
|
||||||
sentAt?: number;
|
sentAt?: number;
|
||||||
messageId?: string;
|
storyId?: UUIDStringType;
|
||||||
}
|
}
|
||||||
) => Promise<Array<MessageTypeUnhydrated>>;
|
) => Promise<Array<MessageTypeUnhydrated>>;
|
||||||
getNewerMessagesByConversation: (
|
getNewerMessagesByConversation: (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
options?: { limit?: number; receivedAt?: number; sentAt?: number }
|
options?: {
|
||||||
|
limit?: number;
|
||||||
|
receivedAt?: number;
|
||||||
|
sentAt?: number;
|
||||||
|
storyId?: UUIDStringType;
|
||||||
|
}
|
||||||
) => Promise<Array<MessageTypeUnhydrated>>;
|
) => Promise<Array<MessageTypeUnhydrated>>;
|
||||||
getLastConversationMessages: (options: {
|
getLastConversationMessages: (options: {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
@ -622,19 +698,21 @@ export type ClientInterface = DataInterface & {
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
options: {
|
options: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
receivedAt?: number;
|
receivedAt?: number;
|
||||||
sentAt?: number;
|
sentAt?: number;
|
||||||
MessageCollection: typeof MessageModelCollectionType;
|
storyId?: UUIDStringType;
|
||||||
}
|
}
|
||||||
) => Promise<MessageModelCollectionType>;
|
) => Promise<MessageModelCollectionType>;
|
||||||
getNewerMessagesByConversation: (
|
getNewerMessagesByConversation: (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
options: {
|
options: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
receivedAt?: number;
|
receivedAt?: number;
|
||||||
sentAt?: number;
|
sentAt?: number;
|
||||||
MessageCollection: typeof MessageModelCollectionType;
|
storyId?: UUIDStringType;
|
||||||
}
|
}
|
||||||
) => Promise<MessageModelCollectionType>;
|
) => Promise<MessageModelCollectionType>;
|
||||||
getLastConversationMessages: (options: {
|
getLastConversationMessages: (options: {
|
||||||
|
|
580
ts/sql/Server.ts
580
ts/sql/Server.ts
|
@ -14,6 +14,7 @@ import type { Dictionary } from 'lodash';
|
||||||
import {
|
import {
|
||||||
forEach,
|
forEach,
|
||||||
fromPairs,
|
fromPairs,
|
||||||
|
groupBy,
|
||||||
isNil,
|
isNil,
|
||||||
isNumber,
|
isNumber,
|
||||||
isString,
|
isString,
|
||||||
|
@ -75,19 +76,19 @@ import type {
|
||||||
ConversationType,
|
ConversationType,
|
||||||
DeleteSentProtoRecipientOptionsType,
|
DeleteSentProtoRecipientOptionsType,
|
||||||
EmojiType,
|
EmojiType,
|
||||||
IdentityKeyType,
|
|
||||||
IdentityKeyIdType,
|
IdentityKeyIdType,
|
||||||
|
IdentityKeyType,
|
||||||
ItemKeyType,
|
ItemKeyType,
|
||||||
ItemType,
|
ItemType,
|
||||||
LastConversationMessagesServerType,
|
LastConversationMessagesServerType,
|
||||||
MessageMetricsType,
|
MessageMetricsType,
|
||||||
MessageType,
|
MessageType,
|
||||||
MessageTypeUnhydrated,
|
MessageTypeUnhydrated,
|
||||||
PreKeyType,
|
|
||||||
PreKeyIdType,
|
PreKeyIdType,
|
||||||
|
PreKeyType,
|
||||||
SearchResultMessageType,
|
SearchResultMessageType,
|
||||||
SenderKeyType,
|
|
||||||
SenderKeyIdType,
|
SenderKeyIdType,
|
||||||
|
SenderKeyType,
|
||||||
SentMessageDBType,
|
SentMessageDBType,
|
||||||
SentMessagesType,
|
SentMessagesType,
|
||||||
SentProtoType,
|
SentProtoType,
|
||||||
|
@ -95,13 +96,17 @@ import type {
|
||||||
SentRecipientsDBType,
|
SentRecipientsDBType,
|
||||||
SentRecipientsType,
|
SentRecipientsType,
|
||||||
ServerInterface,
|
ServerInterface,
|
||||||
SessionType,
|
|
||||||
SessionIdType,
|
SessionIdType,
|
||||||
SignedPreKeyType,
|
SessionType,
|
||||||
SignedPreKeyIdType,
|
SignedPreKeyIdType,
|
||||||
|
SignedPreKeyType,
|
||||||
StickerPackStatusType,
|
StickerPackStatusType,
|
||||||
StickerPackType,
|
StickerPackType,
|
||||||
StickerType,
|
StickerType,
|
||||||
|
StoryDistributionMemberType,
|
||||||
|
StoryDistributionType,
|
||||||
|
StoryDistributionWithMembersType,
|
||||||
|
StoryReadType,
|
||||||
UnprocessedType,
|
UnprocessedType,
|
||||||
UnprocessedUpdateType,
|
UnprocessedUpdateType,
|
||||||
} from './Interface';
|
} from './Interface';
|
||||||
|
@ -207,17 +212,18 @@ const dataInterface: ServerInterface = {
|
||||||
saveMessages,
|
saveMessages,
|
||||||
removeMessage,
|
removeMessage,
|
||||||
removeMessages,
|
removeMessages,
|
||||||
getUnreadCountForConversation,
|
|
||||||
getUnreadByConversationAndMarkRead,
|
getUnreadByConversationAndMarkRead,
|
||||||
getUnreadReactionsAndMarkRead,
|
getUnreadReactionsAndMarkRead,
|
||||||
markReactionAsRead,
|
markReactionAsRead,
|
||||||
addReaction,
|
addReaction,
|
||||||
removeReactionFromConversation,
|
removeReactionFromConversation,
|
||||||
_getAllReactions,
|
_getAllReactions,
|
||||||
|
_removeAllReactions,
|
||||||
getMessageBySender,
|
getMessageBySender,
|
||||||
getMessageById,
|
getMessageById,
|
||||||
getMessagesById,
|
getMessagesById,
|
||||||
_getAllMessages,
|
_getAllMessages,
|
||||||
|
_removeAllMessages,
|
||||||
getAllMessageIds,
|
getAllMessageIds,
|
||||||
getMessagesBySentAt,
|
getMessagesBySentAt,
|
||||||
getExpiredMessages,
|
getExpiredMessages,
|
||||||
|
@ -226,7 +232,9 @@ const dataInterface: ServerInterface = {
|
||||||
getNextTapToViewMessageTimestampToAgeOut,
|
getNextTapToViewMessageTimestampToAgeOut,
|
||||||
getTapToViewMessagesNeedingErase,
|
getTapToViewMessagesNeedingErase,
|
||||||
getOlderMessagesByConversation,
|
getOlderMessagesByConversation,
|
||||||
|
getOlderStories,
|
||||||
getNewerMessagesByConversation,
|
getNewerMessagesByConversation,
|
||||||
|
getTotalUnreadForConversation,
|
||||||
getMessageMetricsForConversation,
|
getMessageMetricsForConversation,
|
||||||
getLastConversationMessages,
|
getLastConversationMessages,
|
||||||
hasGroupCallHistoryMessage,
|
hasGroupCallHistoryMessage,
|
||||||
|
@ -267,6 +275,19 @@ const dataInterface: ServerInterface = {
|
||||||
updateOrCreateBadges,
|
updateOrCreateBadges,
|
||||||
badgeImageFileDownloaded,
|
badgeImageFileDownloaded,
|
||||||
|
|
||||||
|
_getAllStoryDistributions,
|
||||||
|
_getAllStoryDistributionMembers,
|
||||||
|
_deleteAllStoryDistributions,
|
||||||
|
createNewStoryDistribution,
|
||||||
|
getAllStoryDistributionsWithMembers,
|
||||||
|
modifyStoryDistributionMembers,
|
||||||
|
deleteStoryDistribution,
|
||||||
|
|
||||||
|
_getAllStoryReads,
|
||||||
|
_deleteAllStoryReads,
|
||||||
|
addNewStoryRead,
|
||||||
|
getLastStoryReadsForAuthor,
|
||||||
|
|
||||||
removeAll,
|
removeAll,
|
||||||
removeAllConfiguration,
|
removeAllConfiguration,
|
||||||
|
|
||||||
|
@ -1674,8 +1695,6 @@ async function getMessageCount(conversationId?: string): Promise<number> {
|
||||||
function hasUserInitiatedMessages(conversationId: string): boolean {
|
function hasUserInitiatedMessages(conversationId: string): boolean {
|
||||||
const db = getInstance();
|
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
|
const row: { count: number } = db
|
||||||
.prepare<Query>(
|
.prepare<Query>(
|
||||||
`
|
`
|
||||||
|
@ -1687,14 +1706,15 @@ function hasUserInitiatedMessages(conversationId: string): boolean {
|
||||||
(type IS NULL
|
(type IS NULL
|
||||||
OR
|
OR
|
||||||
type NOT IN (
|
type NOT IN (
|
||||||
'profile-change',
|
|
||||||
'verified-change',
|
|
||||||
'message-history-unsynced',
|
|
||||||
'keychange',
|
|
||||||
'group-v1-migration',
|
|
||||||
'universal-timer-notification',
|
|
||||||
'change-number-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
|
LIMIT 1
|
||||||
|
@ -1749,6 +1769,7 @@ function saveMessageSync(
|
||||||
source,
|
source,
|
||||||
sourceUuid,
|
sourceUuid,
|
||||||
sourceDevice,
|
sourceDevice,
|
||||||
|
storyId,
|
||||||
type,
|
type,
|
||||||
readStatus,
|
readStatus,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
|
@ -1775,6 +1796,7 @@ function saveMessageSync(
|
||||||
source: source || null,
|
source: source || null,
|
||||||
sourceUuid: sourceUuid || null,
|
sourceUuid: sourceUuid || null,
|
||||||
sourceDevice: sourceDevice || null,
|
sourceDevice: sourceDevice || null,
|
||||||
|
storyId: storyId || null,
|
||||||
type: type || null,
|
type: type || null,
|
||||||
readStatus: readStatus ?? null,
|
readStatus: readStatus ?? null,
|
||||||
};
|
};
|
||||||
|
@ -1803,6 +1825,7 @@ function saveMessageSync(
|
||||||
source = $source,
|
source = $source,
|
||||||
sourceUuid = $sourceUuid,
|
sourceUuid = $sourceUuid,
|
||||||
sourceDevice = $sourceDevice,
|
sourceDevice = $sourceDevice,
|
||||||
|
storyId = $storyId,
|
||||||
type = $type,
|
type = $type,
|
||||||
readStatus = $readStatus
|
readStatus = $readStatus
|
||||||
WHERE id = $id;
|
WHERE id = $id;
|
||||||
|
@ -1844,6 +1867,7 @@ function saveMessageSync(
|
||||||
source,
|
source,
|
||||||
sourceUuid,
|
sourceUuid,
|
||||||
sourceDevice,
|
sourceDevice,
|
||||||
|
storyId,
|
||||||
type,
|
type,
|
||||||
readStatus
|
readStatus
|
||||||
) values (
|
) values (
|
||||||
|
@ -1866,6 +1890,7 @@ function saveMessageSync(
|
||||||
$source,
|
$source,
|
||||||
$sourceUuid,
|
$sourceUuid,
|
||||||
$sourceDevice,
|
$sourceDevice,
|
||||||
|
$storyId,
|
||||||
$type,
|
$type,
|
||||||
$readStatus
|
$readStatus
|
||||||
);
|
);
|
||||||
|
@ -1974,6 +1999,10 @@ async function _getAllMessages(): Promise<Array<MessageType>> {
|
||||||
|
|
||||||
return rows.map(row => jsonToObject(row.json));
|
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>> {
|
async function getAllMessageIds(): Promise<Array<string>> {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
|
@ -2014,30 +2043,17 @@ async function getMessageBySender({
|
||||||
return rows.map(row => jsonToObject(row.json));
|
return rows.map(row => jsonToObject(row.json));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUnreadCountForConversation(
|
async function getUnreadByConversationAndMarkRead({
|
||||||
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,
|
conversationId,
|
||||||
});
|
newestUnreadAt,
|
||||||
return row.unreadCount;
|
storyId,
|
||||||
}
|
readAt,
|
||||||
|
}: {
|
||||||
async function getUnreadByConversationAndMarkRead(
|
conversationId: string;
|
||||||
conversationId: string,
|
newestUnreadAt: number;
|
||||||
newestUnreadId: number,
|
storyId?: UUIDStringType;
|
||||||
readAt?: number
|
readAt?: number;
|
||||||
): Promise<
|
}): Promise<
|
||||||
Array<Pick<MessageType, 'id' | 'source' | 'sourceUuid' | 'sent_at' | 'type'>>
|
Array<Pick<MessageType, 'id' | 'source' | 'sourceUuid' | 'sent_at' | 'type'>>
|
||||||
> {
|
> {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
|
@ -2057,13 +2073,15 @@ async function getUnreadByConversationAndMarkRead(
|
||||||
) AND
|
) AND
|
||||||
expireTimer IS NOT NULL AND
|
expireTimer IS NOT NULL AND
|
||||||
conversationId = $conversationId AND
|
conversationId = $conversationId AND
|
||||||
received_at <= $newestUnreadId;
|
storyId IS $storyId AND
|
||||||
|
received_at <= $newestUnreadAt;
|
||||||
`
|
`
|
||||||
).run({
|
).run({
|
||||||
conversationId,
|
conversationId,
|
||||||
expirationStartTimestamp,
|
expirationStartTimestamp,
|
||||||
jsonPatch: JSON.stringify({ expirationStartTimestamp }),
|
jsonPatch: JSON.stringify({ expirationStartTimestamp }),
|
||||||
newestUnreadId,
|
newestUnreadAt,
|
||||||
|
storyId: storyId || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rows = db
|
const rows = db
|
||||||
|
@ -2074,13 +2092,15 @@ async function getUnreadByConversationAndMarkRead(
|
||||||
WHERE
|
WHERE
|
||||||
readStatus = ${ReadStatus.Unread} AND
|
readStatus = ${ReadStatus.Unread} AND
|
||||||
conversationId = $conversationId AND
|
conversationId = $conversationId AND
|
||||||
received_at <= $newestUnreadId
|
storyId IS $storyId AND
|
||||||
|
received_at <= $newestUnreadAt
|
||||||
ORDER BY received_at DESC, sent_at DESC;
|
ORDER BY received_at DESC, sent_at DESC;
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.all({
|
.all({
|
||||||
conversationId,
|
conversationId,
|
||||||
newestUnreadId,
|
newestUnreadAt,
|
||||||
|
storyId: storyId || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
db.prepare<Query>(
|
db.prepare<Query>(
|
||||||
|
@ -2092,12 +2112,14 @@ async function getUnreadByConversationAndMarkRead(
|
||||||
WHERE
|
WHERE
|
||||||
readStatus = ${ReadStatus.Unread} AND
|
readStatus = ${ReadStatus.Unread} AND
|
||||||
conversationId = $conversationId AND
|
conversationId = $conversationId AND
|
||||||
received_at <= $newestUnreadId;
|
storyId IS $storyId AND
|
||||||
|
received_at <= $newestUnreadAt;
|
||||||
`
|
`
|
||||||
).run({
|
).run({
|
||||||
conversationId,
|
conversationId,
|
||||||
jsonPatch: JSON.stringify({ readStatus: ReadStatus.Read }),
|
jsonPatch: JSON.stringify({ readStatus: ReadStatus.Read }),
|
||||||
newestUnreadId,
|
newestUnreadAt,
|
||||||
|
storyId: storyId || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return rows.map(row => {
|
return rows.map(row => {
|
||||||
|
@ -2117,42 +2139,50 @@ async function getUnreadByConversationAndMarkRead(
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUnreadReactionsAndMarkRead(
|
type ReactionResultType = Pick<
|
||||||
conversationId: string,
|
ReactionType,
|
||||||
newestUnreadId: number
|
'targetAuthorUuid' | 'targetTimestamp' | 'messageId'
|
||||||
): Promise<
|
> & { rowid: number };
|
||||||
Array<
|
async function getUnreadReactionsAndMarkRead({
|
||||||
Pick<ReactionType, 'targetAuthorUuid' | 'targetTimestamp' | 'messageId'>
|
conversationId,
|
||||||
>
|
newestUnreadAt,
|
||||||
> {
|
storyId,
|
||||||
|
}: {
|
||||||
|
conversationId: string;
|
||||||
|
newestUnreadAt: number;
|
||||||
|
storyId?: UUIDStringType;
|
||||||
|
}): Promise<Array<ReactionResultType>> {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
|
|
||||||
return db.transaction(() => {
|
return db.transaction(() => {
|
||||||
const unreadMessages = db
|
const unreadMessages: Array<ReactionResultType> = db
|
||||||
.prepare<Query>(
|
.prepare<Query>(
|
||||||
`
|
`
|
||||||
SELECT targetAuthorUuid, targetTimestamp, messageId
|
SELECT rowid, targetAuthorUuid, targetTimestamp, messageId
|
||||||
FROM reactions WHERE
|
FROM reactions
|
||||||
unread = 1 AND
|
JOIN messages on messages.id IS reactions.messageId
|
||||||
conversationId = $conversationId AND
|
WHERE
|
||||||
messageReceivedAt <= $newestUnreadId;
|
unread IS NOT 0 AND
|
||||||
|
messages.conversationId IS $conversationId AND
|
||||||
|
messages.received_at <= $newestUnreadAt AND
|
||||||
|
messages.storyId IS $storyId
|
||||||
|
ORDER BY messageReceivedAt DESC;
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.all({
|
.all({
|
||||||
conversationId,
|
conversationId,
|
||||||
newestUnreadId,
|
newestUnreadAt,
|
||||||
|
storyId: storyId || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
db.prepare(
|
const idsToUpdate = unreadMessages.map(item => item.rowid);
|
||||||
|
batchMultiVarQuery(db, idsToUpdate, (ids: Array<number>): void => {
|
||||||
|
db.prepare<ArrayQuery>(
|
||||||
`
|
`
|
||||||
UPDATE reactions SET
|
UPDATE reactions SET
|
||||||
unread = 0 WHERE
|
unread = 0 WHERE rowid IN ( ${ids.map(() => '?').join(', ')} );
|
||||||
conversationId = $conversationId AND
|
|
||||||
messageReceivedAt <= $newestUnreadId;
|
|
||||||
`
|
`
|
||||||
).run({
|
).run(idsToUpdate);
|
||||||
conversationId,
|
|
||||||
newestUnreadId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return unreadMessages;
|
return unreadMessages;
|
||||||
|
@ -2275,31 +2305,37 @@ async function _getAllReactions(): Promise<Array<ReactionType>> {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
return db.prepare<EmptyQuery>('SELECT * from reactions;').all();
|
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(
|
async function getOlderMessagesByConversation(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
{
|
{
|
||||||
limit = 100,
|
limit = 100,
|
||||||
|
messageId,
|
||||||
receivedAt = Number.MAX_VALUE,
|
receivedAt = Number.MAX_VALUE,
|
||||||
sentAt = Number.MAX_VALUE,
|
sentAt = Number.MAX_VALUE,
|
||||||
messageId,
|
storyId,
|
||||||
}: {
|
}: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
messageId?: string;
|
||||||
receivedAt?: number;
|
receivedAt?: number;
|
||||||
sentAt?: number;
|
sentAt?: number;
|
||||||
messageId?: string;
|
storyId?: UUIDStringType;
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<Array<MessageTypeUnhydrated>> {
|
): Promise<Array<MessageTypeUnhydrated>> {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
let rows: JSONRows;
|
|
||||||
|
|
||||||
if (messageId) {
|
return db
|
||||||
rows = db
|
|
||||||
.prepare<Query>(
|
.prepare<Query>(
|
||||||
`
|
`
|
||||||
SELECT json FROM messages WHERE
|
SELECT json FROM messages WHERE
|
||||||
conversationId = $conversationId AND
|
conversationId = $conversationId AND
|
||||||
id != $messageId 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 AND sent_at < $sent_at) OR
|
||||||
received_at < $received_at
|
received_at < $received_at
|
||||||
|
@ -2310,34 +2346,54 @@ async function getOlderMessagesByConversation(
|
||||||
)
|
)
|
||||||
.all({
|
.all({
|
||||||
conversationId,
|
conversationId,
|
||||||
|
limit,
|
||||||
|
messageId: messageId || null,
|
||||||
received_at: receivedAt,
|
received_at: receivedAt,
|
||||||
sent_at: sentAt,
|
sent_at: sentAt,
|
||||||
limit,
|
storyId: storyId || null,
|
||||||
messageId,
|
})
|
||||||
});
|
.reverse();
|
||||||
} else {
|
|
||||||
rows = db
|
|
||||||
.prepare<Query>(
|
|
||||||
`
|
|
||||||
SELECT json FROM messages WHERE
|
|
||||||
conversationId = $conversationId 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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
async function getNewerMessagesByConversation(
|
||||||
|
@ -2346,7 +2402,13 @@ async function getNewerMessagesByConversation(
|
||||||
limit = 100,
|
limit = 100,
|
||||||
receivedAt = 0,
|
receivedAt = 0,
|
||||||
sentAt = 0,
|
sentAt = 0,
|
||||||
}: { limit?: number; receivedAt?: number; sentAt?: number } = {}
|
storyId,
|
||||||
|
}: {
|
||||||
|
limit?: number;
|
||||||
|
receivedAt?: number;
|
||||||
|
sentAt?: number;
|
||||||
|
storyId?: UUIDStringType;
|
||||||
|
} = {}
|
||||||
): Promise<Array<MessageTypeUnhydrated>> {
|
): Promise<Array<MessageTypeUnhydrated>> {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
const rows: JSONRows = db
|
const rows: JSONRows = db
|
||||||
|
@ -2354,6 +2416,8 @@ async function getNewerMessagesByConversation(
|
||||||
`
|
`
|
||||||
SELECT json FROM messages WHERE
|
SELECT json FROM messages WHERE
|
||||||
conversationId = $conversationId AND
|
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 AND sent_at > $sent_at) OR
|
||||||
received_at > $received_at
|
received_at > $received_at
|
||||||
|
@ -2364,28 +2428,33 @@ async function getNewerMessagesByConversation(
|
||||||
)
|
)
|
||||||
.all({
|
.all({
|
||||||
conversationId,
|
conversationId,
|
||||||
|
limit,
|
||||||
received_at: receivedAt,
|
received_at: receivedAt,
|
||||||
sent_at: sentAt,
|
sent_at: sentAt,
|
||||||
limit,
|
storyId: storyId || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
function getOldestMessageForConversation(
|
function getOldestMessageForConversation(
|
||||||
conversationId: string
|
conversationId: string,
|
||||||
|
storyId?: UUIDStringType
|
||||||
): MessageMetricsType | undefined {
|
): MessageMetricsType | undefined {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
const row = db
|
const row = db
|
||||||
.prepare<Query>(
|
.prepare<Query>(
|
||||||
`
|
`
|
||||||
SELECT * FROM messages WHERE
|
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
|
ORDER BY received_at ASC, sent_at ASC
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.get({
|
.get({
|
||||||
conversationId,
|
conversationId,
|
||||||
|
storyId: storyId || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
|
@ -2395,20 +2464,24 @@ function getOldestMessageForConversation(
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
function getNewestMessageForConversation(
|
function getNewestMessageForConversation(
|
||||||
conversationId: string
|
conversationId: string,
|
||||||
|
storyId?: UUIDStringType
|
||||||
): MessageMetricsType | undefined {
|
): MessageMetricsType | undefined {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
const row = db
|
const row = db
|
||||||
.prepare<Query>(
|
.prepare<Query>(
|
||||||
`
|
`
|
||||||
SELECT * FROM messages WHERE
|
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
|
ORDER BY received_at DESC, sent_at DESC
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.get({
|
.get({
|
||||||
conversationId,
|
conversationId,
|
||||||
|
storyId: storyId || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
|
@ -2435,13 +2508,14 @@ function getLastConversationActivity({
|
||||||
(type IS NULL
|
(type IS NULL
|
||||||
OR
|
OR
|
||||||
type NOT IN (
|
type NOT IN (
|
||||||
'profile-change',
|
'change-number-notification',
|
||||||
'verified-change',
|
|
||||||
'message-history-unsynced',
|
|
||||||
'keychange',
|
|
||||||
'group-v1-migration',
|
'group-v1-migration',
|
||||||
|
'keychange',
|
||||||
|
'message-history-unsynced',
|
||||||
|
'profile-change',
|
||||||
|
'story',
|
||||||
'universal-timer-notification',
|
'universal-timer-notification',
|
||||||
'change-number-notification'
|
'verified-change'
|
||||||
)
|
)
|
||||||
) AND
|
) AND
|
||||||
(
|
(
|
||||||
|
@ -2492,12 +2566,13 @@ function getLastConversationPreview({
|
||||||
type IS NULL
|
type IS NULL
|
||||||
OR
|
OR
|
||||||
type NOT IN (
|
type NOT IN (
|
||||||
'profile-change',
|
'change-number-notification',
|
||||||
'verified-change',
|
|
||||||
'message-history-unsynced',
|
|
||||||
'group-v1-migration',
|
'group-v1-migration',
|
||||||
|
'message-history-unsynced',
|
||||||
|
'profile-change',
|
||||||
|
'story',
|
||||||
'universal-timer-notification',
|
'universal-timer-notification',
|
||||||
'change-number-notification'
|
'verified-change'
|
||||||
)
|
)
|
||||||
) AND NOT
|
) AND NOT
|
||||||
(
|
(
|
||||||
|
@ -2548,7 +2623,8 @@ async function getLastConversationMessages({
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOldestUnreadMessageForConversation(
|
function getOldestUnreadMessageForConversation(
|
||||||
conversationId: string
|
conversationId: string,
|
||||||
|
storyId?: UUIDStringType
|
||||||
): MessageMetricsType | undefined {
|
): MessageMetricsType | undefined {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
const row = db
|
const row = db
|
||||||
|
@ -2556,13 +2632,16 @@ function getOldestUnreadMessageForConversation(
|
||||||
`
|
`
|
||||||
SELECT * FROM messages WHERE
|
SELECT * FROM messages WHERE
|
||||||
conversationId = $conversationId AND
|
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
|
ORDER BY received_at ASC, sent_at ASC
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.get({
|
.get({
|
||||||
conversationId,
|
conversationId,
|
||||||
|
storyId: storyId || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
|
@ -2572,7 +2651,10 @@ function getOldestUnreadMessageForConversation(
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTotalUnreadForConversation(conversationId: string): number {
|
async function getTotalUnreadForConversation(
|
||||||
|
conversationId: string,
|
||||||
|
storyId?: UUIDStringType
|
||||||
|
): Promise<number> {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
const row = db
|
const row = db
|
||||||
.prepare<Query>(
|
.prepare<Query>(
|
||||||
|
@ -2581,11 +2663,14 @@ function getTotalUnreadForConversation(conversationId: string): number {
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE
|
WHERE
|
||||||
conversationId = $conversationId AND
|
conversationId = $conversationId AND
|
||||||
readStatus = ${ReadStatus.Unread};
|
readStatus = ${ReadStatus.Unread} AND
|
||||||
|
type IS NOT 'story' AND
|
||||||
|
storyId IS $storyId;
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.get({
|
.get({
|
||||||
conversationId,
|
conversationId,
|
||||||
|
storyId: storyId || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
|
@ -2596,12 +2681,19 @@ function getTotalUnreadForConversation(conversationId: string): number {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMessageMetricsForConversation(
|
async function getMessageMetricsForConversation(
|
||||||
conversationId: string
|
conversationId: string,
|
||||||
|
storyId?: UUIDStringType
|
||||||
): Promise<ConversationMetricsType> {
|
): Promise<ConversationMetricsType> {
|
||||||
const oldest = getOldestMessageForConversation(conversationId);
|
const oldest = getOldestMessageForConversation(conversationId, storyId);
|
||||||
const newest = getNewestMessageForConversation(conversationId);
|
const newest = getNewestMessageForConversation(conversationId, storyId);
|
||||||
const oldestUnread = getOldestUnreadMessageForConversation(conversationId);
|
const oldestUnread = getOldestUnreadMessageForConversation(
|
||||||
const totalUnread = getTotalUnreadForConversation(conversationId);
|
conversationId,
|
||||||
|
storyId
|
||||||
|
);
|
||||||
|
const totalUnread = await getTotalUnreadForConversation(
|
||||||
|
conversationId,
|
||||||
|
storyId
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
oldest: oldest ? pick(oldest, ['received_at', 'sent_at', 'id']) : undefined,
|
oldest: oldest ? pick(oldest, ['received_at', 'sent_at', 'id']) : undefined,
|
||||||
|
@ -3706,29 +3798,256 @@ async function getAllBadgeImageFileLocalPaths(): Promise<Set<string>> {
|
||||||
return new Set(localPaths);
|
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
|
// All data in database
|
||||||
async function removeAll(): Promise<void> {
|
async function removeAll(): Promise<void> {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
|
|
||||||
db.transaction(() => {
|
db.transaction(() => {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
DELETE FROM badges;
|
DELETE FROM attachment_downloads;
|
||||||
DELETE FROM badgeImageFiles;
|
DELETE FROM badgeImageFiles;
|
||||||
|
DELETE FROM badges;
|
||||||
DELETE FROM conversations;
|
DELETE FROM conversations;
|
||||||
|
DELETE FROM emojis;
|
||||||
|
DELETE FROM groupCallRings;
|
||||||
DELETE FROM identityKeys;
|
DELETE FROM identityKeys;
|
||||||
DELETE FROM items;
|
DELETE FROM items;
|
||||||
|
DELETE FROM jobs;
|
||||||
|
DELETE FROM jobs;
|
||||||
|
DELETE FROM messages_fts;
|
||||||
DELETE FROM messages;
|
DELETE FROM messages;
|
||||||
DELETE FROM preKeys;
|
DELETE FROM preKeys;
|
||||||
|
DELETE FROM reactions;
|
||||||
DELETE FROM senderKeys;
|
DELETE FROM senderKeys;
|
||||||
|
DELETE FROM sendLogMessageIds;
|
||||||
|
DELETE FROM sendLogPayloads;
|
||||||
|
DELETE FROM sendLogRecipients;
|
||||||
DELETE FROM sessions;
|
DELETE FROM sessions;
|
||||||
DELETE FROM signedPreKeys;
|
DELETE FROM signedPreKeys;
|
||||||
DELETE FROM unprocessed;
|
|
||||||
DELETE FROM attachment_downloads;
|
|
||||||
DELETE FROM messages_fts;
|
|
||||||
DELETE FROM stickers;
|
|
||||||
DELETE FROM sticker_packs;
|
DELETE FROM sticker_packs;
|
||||||
DELETE FROM sticker_references;
|
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(
|
db.exec(
|
||||||
`
|
`
|
||||||
DELETE FROM identityKeys;
|
DELETE FROM identityKeys;
|
||||||
|
DELETE FROM jobs;
|
||||||
DELETE FROM preKeys;
|
DELETE FROM preKeys;
|
||||||
DELETE FROM senderKeys;
|
DELETE FROM senderKeys;
|
||||||
|
DELETE FROM sendLogMessageIds;
|
||||||
|
DELETE FROM sendLogPayloads;
|
||||||
|
DELETE FROM sendLogRecipients;
|
||||||
DELETE FROM sessions;
|
DELETE FROM sessions;
|
||||||
DELETE FROM signedPreKeys;
|
DELETE FROM signedPreKeys;
|
||||||
DELETE FROM unprocessed;
|
DELETE FROM unprocessed;
|
||||||
DELETE FROM jobs;
|
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -3811,6 +4133,8 @@ async function getMessagesWithVisualMediaAttachments(
|
||||||
.prepare<Query>(
|
.prepare<Query>(
|
||||||
`
|
`
|
||||||
SELECT json FROM messages WHERE
|
SELECT json FROM messages WHERE
|
||||||
|
type IS NOT 'story' AND
|
||||||
|
storyId IS NULL AND
|
||||||
conversationId = $conversationId AND
|
conversationId = $conversationId AND
|
||||||
hasVisualMediaAttachments = 1
|
hasVisualMediaAttachments = 1
|
||||||
ORDER BY received_at DESC, sent_at DESC
|
ORDER BY received_at DESC, sent_at DESC
|
||||||
|
@ -3834,6 +4158,8 @@ async function getMessagesWithFileAttachments(
|
||||||
.prepare<Query>(
|
.prepare<Query>(
|
||||||
`
|
`
|
||||||
SELECT json FROM messages WHERE
|
SELECT json FROM messages WHERE
|
||||||
|
type IS NOT 'story' AND
|
||||||
|
storyId IS NULL AND
|
||||||
conversationId = $conversationId AND
|
conversationId = $conversationId AND
|
||||||
hasFileAttachments = 1
|
hasFileAttachments = 1
|
||||||
ORDER BY received_at DESC, sent_at DESC
|
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 updateToSchemaVersion42 from './42-stale-reactions';
|
||||||
import updateToSchemaVersion43 from './43-gv2-uuid';
|
import updateToSchemaVersion43 from './43-gv2-uuid';
|
||||||
import updateToSchemaVersion44 from './44-badges';
|
import updateToSchemaVersion44 from './44-badges';
|
||||||
|
import updateToSchemaVersion45 from './45-stories';
|
||||||
|
|
||||||
function updateToSchemaVersion1(
|
function updateToSchemaVersion1(
|
||||||
currentVersion: number,
|
currentVersion: number,
|
||||||
|
@ -1903,6 +1904,7 @@ export const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion42,
|
updateToSchemaVersion42,
|
||||||
updateToSchemaVersion43,
|
updateToSchemaVersion43,
|
||||||
updateToSchemaVersion44,
|
updateToSchemaVersion44,
|
||||||
|
updateToSchemaVersion45,
|
||||||
];
|
];
|
||||||
|
|
||||||
export function updateSchema(db: Database, logger: LoggerType): void {
|
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,
|
saveMessage,
|
||||||
} = dataInterface;
|
} = dataInterface;
|
||||||
|
|
||||||
describe('sendLog', () => {
|
describe('sql/sendLog', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await removeAllSentProtos();
|
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;
|
conversationId: string;
|
||||||
emoji: string;
|
emoji: string;
|
||||||
fromId: string;
|
fromId: string;
|
||||||
messageId: string | undefined;
|
messageId: string;
|
||||||
messageReceivedAt: number;
|
messageReceivedAt: number;
|
||||||
targetAuthorUuid: string;
|
targetAuthorUuid: string;
|
||||||
targetTimestamp: number;
|
targetTimestamp: number;
|
||||||
|
|
|
@ -10,7 +10,7 @@ import * as log from '../logging/log';
|
||||||
|
|
||||||
export async function markConversationRead(
|
export async function markConversationRead(
|
||||||
conversationAttrs: ConversationAttributesType,
|
conversationAttrs: ConversationAttributesType,
|
||||||
newestUnreadId: number,
|
newestUnreadAt: number,
|
||||||
options: { readAt?: number; sendReadReceipts: boolean } = {
|
options: { readAt?: number; sendReadReceipts: boolean } = {
|
||||||
sendReadReceipts: true,
|
sendReadReceipts: true,
|
||||||
}
|
}
|
||||||
|
@ -18,20 +18,20 @@ export async function markConversationRead(
|
||||||
const { id: conversationId } = conversationAttrs;
|
const { id: conversationId } = conversationAttrs;
|
||||||
|
|
||||||
const [unreadMessages, unreadReactions] = await Promise.all([
|
const [unreadMessages, unreadReactions] = await Promise.all([
|
||||||
window.Signal.Data.getUnreadByConversationAndMarkRead(
|
window.Signal.Data.getUnreadByConversationAndMarkRead({
|
||||||
conversationId,
|
conversationId,
|
||||||
newestUnreadId,
|
newestUnreadAt,
|
||||||
options.readAt
|
readAt: options.readAt,
|
||||||
),
|
}),
|
||||||
window.Signal.Data.getUnreadReactionsAndMarkRead(
|
window.Signal.Data.getUnreadReactionsAndMarkRead({
|
||||||
conversationId,
|
conversationId,
|
||||||
newestUnreadId
|
newestUnreadAt,
|
||||||
),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
log.info('markConversationRead', {
|
log.info('markConversationRead', {
|
||||||
conversationId,
|
conversationId,
|
||||||
newestUnreadId,
|
newestUnreadAt,
|
||||||
unreadMessages: unreadMessages.length,
|
unreadMessages: unreadMessages.length,
|
||||||
unreadReactions: unreadReactions.length,
|
unreadReactions: unreadReactions.length,
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue