Disappearing message cleanups
This commit is contained in:
parent
dfa6fb5d61
commit
03a187097f
16 changed files with 149 additions and 268 deletions
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016-2020 Signal Messenger, LLC
|
||||
// Copyright 2016-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global
|
||||
|
@ -96,11 +96,6 @@
|
|||
sent: true,
|
||||
});
|
||||
|
||||
if (message.isExpiring() && !expirationStartTimestamp) {
|
||||
// TODO DESKTOP-1509: use setToExpire once this is TS
|
||||
await message.setToExpire(false, { skipSave: true });
|
||||
}
|
||||
|
||||
window.Signal.Util.queueUpdateMessage(message.attributes);
|
||||
|
||||
// notify frontend listeners
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016-2020 Signal Messenger, LLC
|
||||
// Copyright 2016-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global
|
||||
|
@ -17,6 +17,9 @@
|
|||
const messages = await window.Signal.Data.getExpiredMessages({
|
||||
MessageCollection: Whisper.MessageCollection,
|
||||
});
|
||||
window.log.info(
|
||||
`destroyExpiredMessages: found ${messages.length} messages to expire`
|
||||
);
|
||||
|
||||
const messageIds = [];
|
||||
const inMemoryMessages = [];
|
||||
|
@ -59,20 +62,15 @@
|
|||
|
||||
let timeout;
|
||||
async function checkExpiringMessages() {
|
||||
// Look up the next expiring message and set a timer to destroy it
|
||||
const message = await window.Signal.Data.getNextExpiringMessage({
|
||||
Message: Whisper.Message,
|
||||
});
|
||||
window.log.info('checkExpiringMessages: checking for expiring messages');
|
||||
|
||||
if (!message) {
|
||||
const soonestExpiry = await window.Signal.Data.getSoonestMessageExpiry();
|
||||
if (!soonestExpiry) {
|
||||
window.log.info('checkExpiringMessages: found no messages to expire');
|
||||
return;
|
||||
}
|
||||
|
||||
const expiresAt = message.get('expires_at');
|
||||
Whisper.ExpiringMessagesListener.nextExpiration = expiresAt;
|
||||
window.log.info('next message expires', new Date(expiresAt).toISOString());
|
||||
|
||||
let wait = expiresAt - Date.now();
|
||||
let wait = soonestExpiry - Date.now();
|
||||
|
||||
// In the past
|
||||
if (wait < 0) {
|
||||
|
@ -84,6 +82,12 @@
|
|||
wait = 2147483647;
|
||||
}
|
||||
|
||||
window.log.info(
|
||||
`checkExpiringMessages: next message expires ${new Date(
|
||||
soonestExpiry
|
||||
).toISOString()}; waiting ${wait} ms before clearing`
|
||||
);
|
||||
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(destroyExpiredMessages, wait);
|
||||
}
|
||||
|
@ -93,7 +97,6 @@
|
|||
);
|
||||
|
||||
Whisper.ExpiringMessagesListener = {
|
||||
nextExpiration: null,
|
||||
init(events) {
|
||||
checkExpiringMessages();
|
||||
events.on('timetravel', debouncedCheckExpiringMessages);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016-2020 Signal Messenger, LLC
|
||||
// Copyright 2016-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global
|
||||
|
@ -97,11 +97,6 @@
|
|||
sent: true,
|
||||
});
|
||||
|
||||
if (message.isExpiring() && !expirationStartTimestamp) {
|
||||
// TODO DESKTOP-1509: use setToExpire once this is TS
|
||||
await message.setToExpire(false, { skipSave: true });
|
||||
}
|
||||
|
||||
window.Signal.Util.queueUpdateMessage(message.attributes);
|
||||
|
||||
// notify frontend listeners
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2017-2020 Signal Messenger, LLC
|
||||
// Copyright 2017-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global
|
||||
|
@ -120,10 +120,6 @@
|
|||
);
|
||||
message.set({ expirationStartTimestamp });
|
||||
|
||||
const force = true;
|
||||
// TODO DESKTOP-1509: use setToExpire once this is TS
|
||||
await message.setToExpire(force, { skipSave: true });
|
||||
|
||||
const conversation = message.getConversation();
|
||||
if (conversation) {
|
||||
conversation.trigger('expiration-change', message);
|
||||
|
|
|
@ -24,14 +24,13 @@ import { routineProfileRefresh } from './routineProfileRefresh';
|
|||
import { isMoreRecentThan, isOlderThan } from './util/timestamp';
|
||||
import { isValidReactionEmoji } from './reactions/isValidReactionEmoji';
|
||||
import { ConversationModel } from './models/conversations';
|
||||
import { getMessageById } from './models/messages';
|
||||
import { getMessageById, MessageModel } from './models/messages';
|
||||
import { createBatcher } from './util/batcher';
|
||||
import { updateConversationsWithUuidLookup } from './updateConversationsWithUuidLookup';
|
||||
import { initializeAllJobQueues } from './jobs/initializeAllJobQueues';
|
||||
import { removeStorageKeyJobQueue } from './jobs/removeStorageKeyJobQueue';
|
||||
import { ourProfileKeyService } from './services/ourProfileKey';
|
||||
import { shouldRespondWithProfileKey } from './util/shouldRespondWithProfileKey';
|
||||
import { setToExpire } from './services/MessageUpdater';
|
||||
import { LatestQueue } from './util/LatestQueue';
|
||||
import { parseIntOrThrow } from './util/parseIntOrThrow';
|
||||
import {
|
||||
|
@ -1642,7 +1641,7 @@ export async function startApp(): Promise<void> {
|
|||
window.dispatchEvent(new Event('storage_ready'));
|
||||
|
||||
window.log.info('Cleanup: starting...');
|
||||
const messagesForCleanup = await window.Signal.Data.getOutgoingWithoutExpiresAt(
|
||||
const messagesForCleanup = await window.Signal.Data.getOutgoingWithoutExpirationStartTimestamp(
|
||||
{
|
||||
MessageCollection: window.Whisper.MessageCollection,
|
||||
}
|
||||
|
@ -1652,11 +1651,13 @@ export async function startApp(): Promise<void> {
|
|||
);
|
||||
await Promise.all(
|
||||
messagesForCleanup.map(async message => {
|
||||
assert(
|
||||
!message.get('expirationStartTimestamp'),
|
||||
'Cleanup should not have messages with an expirationStartTimestamp'
|
||||
);
|
||||
|
||||
const delivered = message.get('delivered');
|
||||
const sentAt = message.get('sent_at');
|
||||
const expirationStartTimestamp = message.get(
|
||||
'expirationStartTimestamp'
|
||||
);
|
||||
|
||||
if (message.hasErrors()) {
|
||||
return;
|
||||
|
@ -1666,12 +1667,7 @@ export async function startApp(): Promise<void> {
|
|||
window.log.info(
|
||||
`Cleanup: Starting timer for delivered message ${sentAt}`
|
||||
);
|
||||
message.set(
|
||||
setToExpire({
|
||||
...message.attributes,
|
||||
expirationStartTimestamp: expirationStartTimestamp || sentAt,
|
||||
})
|
||||
);
|
||||
message.set('expirationStartTimestamp', sentAt);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1685,6 +1681,9 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
})
|
||||
);
|
||||
if (messagesForCleanup.length) {
|
||||
window.Whisper.ExpiringMessagesListener.update();
|
||||
}
|
||||
window.log.info('Cleanup: complete');
|
||||
|
||||
window.log.info('listening for registration events');
|
||||
|
@ -2389,7 +2388,9 @@ export async function startApp(): Promise<void> {
|
|||
messagesToSave.push(message.attributes);
|
||||
}
|
||||
});
|
||||
await window.Signal.Data.saveMessages(messagesToSave, {});
|
||||
await window.Signal.Data.saveMessages(messagesToSave, {
|
||||
Message: MessageModel,
|
||||
});
|
||||
}
|
||||
function onReconnect() {
|
||||
// We disable notifications on first connect, but the same applies to reconnect. In
|
||||
|
|
|
@ -152,7 +152,6 @@ export type PropsData = {
|
|||
isViewOnce: boolean;
|
||||
};
|
||||
previews: Array<LinkPreviewType>;
|
||||
isExpired?: boolean;
|
||||
|
||||
isTapToView?: boolean;
|
||||
isTapToViewExpired?: boolean;
|
||||
|
@ -497,7 +496,7 @@ export class Message extends React.Component<Props, State> {
|
|||
|
||||
public checkExpired(): void {
|
||||
const now = Date.now();
|
||||
const { isExpired, expirationTimestamp, expirationLength } = this.props;
|
||||
const { expirationTimestamp, expirationLength } = this.props;
|
||||
|
||||
if (!expirationTimestamp || !expirationLength) {
|
||||
return;
|
||||
|
@ -506,7 +505,7 @@ export class Message extends React.Component<Props, State> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (isExpired || now >= expirationTimestamp) {
|
||||
if (now >= expirationTimestamp) {
|
||||
this.setState({
|
||||
expiring: true,
|
||||
});
|
||||
|
|
|
@ -1717,6 +1717,7 @@ export async function createGroupV2({
|
|||
};
|
||||
await window.Signal.Data.saveMessages([createdTheGroupMessage], {
|
||||
forceSave: true,
|
||||
Message: window.Whisper.Message,
|
||||
});
|
||||
const model = new window.Whisper.Message(createdTheGroupMessage);
|
||||
window.MessageController.register(model.id, model);
|
||||
|
@ -2831,6 +2832,7 @@ async function updateGroup(
|
|||
if (changeMessagesToSave.length > 0) {
|
||||
await window.Signal.Data.saveMessages(changeMessagesToSave, {
|
||||
forceSave: true,
|
||||
Message: window.Whisper.Message,
|
||||
});
|
||||
changeMessagesToSave.forEach(changeMessage => {
|
||||
const model = new window.Whisper.Message(changeMessage);
|
||||
|
|
1
ts/model-types.d.ts
vendored
1
ts/model-types.d.ts
vendored
|
@ -90,7 +90,6 @@ export type MessageAttributesType = {
|
|||
errors?: Array<CustomError>;
|
||||
expirationStartTimestamp: number | null;
|
||||
expireTimer: number;
|
||||
expires_at: number;
|
||||
groupMigration?: GroupMigrationType;
|
||||
group_update: {
|
||||
avatarUpdated: boolean;
|
||||
|
|
|
@ -1234,9 +1234,6 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
|
||||
addSingleMessage(message: MessageModel): MessageModel {
|
||||
// TODO use MessageUpdater.setToExpire
|
||||
message.setToExpire();
|
||||
|
||||
const { messagesAdded } = window.reduxActions.conversations;
|
||||
const isNewMessage = true;
|
||||
messagesAdded(
|
||||
|
|
|
@ -56,7 +56,7 @@ import { AttachmentType, isImage, isVideo } from '../types/Attachment';
|
|||
import { MIMEType } from '../types/MIME';
|
||||
import { LinkPreviewType } from '../types/message/LinkPreviews';
|
||||
import { ourProfileKeyService } from '../services/ourProfileKey';
|
||||
import { markRead, setToExpire } from '../services/MessageUpdater';
|
||||
import { markRead } from '../services/MessageUpdater';
|
||||
import {
|
||||
isDirectConversation,
|
||||
isGroupV1,
|
||||
|
@ -256,8 +256,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
|
||||
isSelected?: boolean;
|
||||
|
||||
hasExpired?: boolean;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
quotedMessage: any;
|
||||
|
||||
|
@ -281,10 +279,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
this.OUR_UUID = window.textsecure.storage.user.getUuid();
|
||||
|
||||
this.on('destroy', this.onDestroy);
|
||||
this.on('change:expirationStartTimestamp', this.setToExpire);
|
||||
this.on('change:expireTimer', this.setToExpire);
|
||||
this.on('unload', this.unload);
|
||||
this.setToExpire();
|
||||
|
||||
this.on('change', this.notifyRedux);
|
||||
}
|
||||
|
@ -1035,7 +1030,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
attachments: this.getAttachmentsForMessage(),
|
||||
previews: this.getPropsForPreview(),
|
||||
quote: this.getPropsForQuote(),
|
||||
isExpired: this.hasExpired,
|
||||
expirationLength,
|
||||
expirationTimestamp,
|
||||
reactions,
|
||||
|
@ -2028,10 +2022,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
}
|
||||
}
|
||||
|
||||
onExpired(): void {
|
||||
this.hasExpired = true;
|
||||
}
|
||||
|
||||
isUnidentifiedDelivery(
|
||||
contactId: string,
|
||||
lookup: Record<string, unknown>
|
||||
|
@ -2163,29 +2153,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
return this.get('expireTimer') && this.get('expirationStartTimestamp');
|
||||
}
|
||||
|
||||
setToExpire(force = false, options = {}): void {
|
||||
this.set(setToExpire(this.attributes, { ...options, force }));
|
||||
}
|
||||
|
||||
isExpired(): boolean {
|
||||
return this.msTilExpire() <= 0;
|
||||
}
|
||||
|
||||
msTilExpire(): number {
|
||||
if (!this.isExpiring()) {
|
||||
return Infinity;
|
||||
}
|
||||
const now = Date.now();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const start = this.get('expirationStartTimestamp')!;
|
||||
const delta = this.get('expireTimer') * 1000;
|
||||
let msFromNow = start + delta - now;
|
||||
if (msFromNow < 0) {
|
||||
msFromNow = 0;
|
||||
}
|
||||
return msFromNow;
|
||||
}
|
||||
|
||||
getIncomingContact(): ConversationModel | undefined | null {
|
||||
if (!this.isIncoming()) {
|
||||
return null;
|
||||
|
|
|
@ -42,46 +42,3 @@ export function getExpiresAt(
|
|||
? messageAttrs.expirationStartTimestamp + expireTimerMs
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function setToExpire(
|
||||
messageAttrs: MessageAttributesType,
|
||||
{ force = false, skipSave = false } = {}
|
||||
): MessageAttributesType {
|
||||
if (!isExpiring(messageAttrs) || (!force && messageAttrs.expires_at)) {
|
||||
return messageAttrs;
|
||||
}
|
||||
|
||||
const expiresAt = getExpiresAt(messageAttrs);
|
||||
|
||||
if (!expiresAt) {
|
||||
return messageAttrs;
|
||||
}
|
||||
|
||||
const nextMessageAttributes = {
|
||||
...messageAttrs,
|
||||
expires_at: expiresAt,
|
||||
};
|
||||
|
||||
window.log.info('Set message expiration', {
|
||||
start: messageAttrs.expirationStartTimestamp,
|
||||
expiresAt,
|
||||
sentAt: messageAttrs.sent_at,
|
||||
});
|
||||
|
||||
if (messageAttrs.id && !skipSave) {
|
||||
window.Signal.Util.queueUpdateMessage(nextMessageAttributes);
|
||||
}
|
||||
|
||||
return nextMessageAttributes;
|
||||
}
|
||||
|
||||
function isExpiring(
|
||||
messageAttrs: Pick<
|
||||
MessageAttributesType,
|
||||
'expireTimer' | 'expirationStartTimestamp'
|
||||
>
|
||||
): boolean {
|
||||
return Boolean(
|
||||
messageAttrs.expireTimer && messageAttrs.expirationStartTimestamp
|
||||
);
|
||||
}
|
||||
|
|
|
@ -189,8 +189,8 @@ const dataInterface: ClientInterface = {
|
|||
getAllMessageIds,
|
||||
getMessagesBySentAt,
|
||||
getExpiredMessages,
|
||||
getOutgoingWithoutExpiresAt,
|
||||
getNextExpiringMessage,
|
||||
getOutgoingWithoutExpirationStartTimestamp,
|
||||
getSoonestMessageExpiry,
|
||||
getNextTapToViewMessageTimestampToAgeOut,
|
||||
getTapToViewMessagesNeedingErase,
|
||||
getOlderMessagesByConversation,
|
||||
|
@ -997,12 +997,13 @@ async function saveMessage(
|
|||
|
||||
async function saveMessages(
|
||||
arrayOfMessages: Array<MessageType>,
|
||||
{ forceSave }: { forceSave?: boolean } = {}
|
||||
{ forceSave, Message }: { forceSave?: boolean; Message: typeof MessageModel }
|
||||
) {
|
||||
await channels.saveMessages(
|
||||
arrayOfMessages.map(message => _cleanMessageData(message)),
|
||||
{ forceSave }
|
||||
);
|
||||
Message.updateTimers();
|
||||
}
|
||||
|
||||
async function removeMessage(
|
||||
|
@ -1300,28 +1301,18 @@ async function getExpiredMessages({
|
|||
return new MessageCollection(messages);
|
||||
}
|
||||
|
||||
async function getOutgoingWithoutExpiresAt({
|
||||
async function getOutgoingWithoutExpirationStartTimestamp({
|
||||
MessageCollection,
|
||||
}: {
|
||||
MessageCollection: typeof MessageModelCollectionType;
|
||||
}) {
|
||||
const messages = await channels.getOutgoingWithoutExpiresAt();
|
||||
const messages = await channels.getOutgoingWithoutExpirationStartTimestamp();
|
||||
|
||||
return new MessageCollection(messages);
|
||||
}
|
||||
|
||||
async function getNextExpiringMessage({
|
||||
Message,
|
||||
}: {
|
||||
Message: typeof MessageModel;
|
||||
}) {
|
||||
const message = await channels.getNextExpiringMessage();
|
||||
|
||||
if (message) {
|
||||
return new Message(message);
|
||||
}
|
||||
|
||||
return null;
|
||||
function getSoonestMessageExpiry() {
|
||||
return channels.getSoonestMessageExpiry();
|
||||
}
|
||||
|
||||
async function getNextTapToViewMessageTimestampToAgeOut() {
|
||||
|
|
|
@ -221,10 +221,6 @@ export type DataInterface = {
|
|||
|
||||
getMessageCount: (conversationId?: string) => Promise<number>;
|
||||
hasUserInitiatedMessages: (conversationId: string) => Promise<boolean>;
|
||||
saveMessages: (
|
||||
arrayOfMessages: Array<MessageType>,
|
||||
options: { forceSave?: boolean }
|
||||
) => Promise<void>;
|
||||
getAllMessageIds: () => Promise<Array<string>>;
|
||||
getMessageMetricsForConversation: (
|
||||
conversationId: string
|
||||
|
@ -313,6 +309,7 @@ export type DataInterface = {
|
|||
getMessageServerGuidsForSpam: (
|
||||
conversationId: string
|
||||
) => Promise<Array<string>>;
|
||||
getSoonestMessageExpiry: () => Promise<undefined | number>;
|
||||
|
||||
getJobsInQueue(queueType: string): Promise<Array<StoredJob>>;
|
||||
insertJob(job: Readonly<StoredJob>): Promise<void>;
|
||||
|
@ -370,8 +367,7 @@ export type ServerInterface = DataInterface & {
|
|||
conversationId: string;
|
||||
ourConversationId: string;
|
||||
}) => Promise<MessageType | undefined>;
|
||||
getNextExpiringMessage: () => Promise<MessageType | undefined>;
|
||||
getOutgoingWithoutExpiresAt: () => Promise<Array<MessageType>>;
|
||||
getOutgoingWithoutExpirationStartTimestamp: () => Promise<Array<MessageType>>;
|
||||
getTapToViewMessagesNeedingErase: () => Promise<Array<MessageType>>;
|
||||
getUnreadCountForConversation: (conversationId: string) => Promise<number>;
|
||||
getUnreadByConversationAndMarkRead: (
|
||||
|
@ -416,6 +412,10 @@ export type ServerInterface = DataInterface & {
|
|||
data: MessageType,
|
||||
options: { forceSave?: boolean }
|
||||
) => Promise<string>;
|
||||
saveMessages: (
|
||||
arrayOfMessages: Array<MessageType>,
|
||||
options: { forceSave?: boolean }
|
||||
) => Promise<void>;
|
||||
updateConversation: (data: ConversationType) => Promise<void>;
|
||||
|
||||
// For testing only
|
||||
|
@ -505,10 +505,7 @@ export type ClientInterface = DataInterface & {
|
|||
ourConversationId: string;
|
||||
Message: typeof MessageModel;
|
||||
}) => Promise<MessageModel | undefined>;
|
||||
getNextExpiringMessage: (options: {
|
||||
Message: typeof MessageModel;
|
||||
}) => Promise<MessageModel | null>;
|
||||
getOutgoingWithoutExpiresAt: (options: {
|
||||
getOutgoingWithoutExpirationStartTimestamp: (options: {
|
||||
MessageCollection: typeof MessageModelCollectionType;
|
||||
}) => Promise<MessageModelCollectionType>;
|
||||
getTapToViewMessagesNeedingErase: (options: {
|
||||
|
@ -557,6 +554,10 @@ export type ClientInterface = DataInterface & {
|
|||
data: MessageType,
|
||||
options: { forceSave?: boolean; Message: typeof MessageModel }
|
||||
) => Promise<string>;
|
||||
saveMessages: (
|
||||
arrayOfMessages: Array<MessageType>,
|
||||
options: { forceSave?: boolean; Message: typeof MessageModel }
|
||||
) => Promise<void>;
|
||||
searchMessages: (
|
||||
query: string,
|
||||
options?: { limit?: number }
|
||||
|
|
196
ts/sql/Server.ts
196
ts/sql/Server.ts
|
@ -33,10 +33,8 @@ import { ReactionType } from '../types/Reactions';
|
|||
import { StoredJob } from '../jobs/types';
|
||||
import { assert } from '../util/assert';
|
||||
import { combineNames } from '../util/combineNames';
|
||||
import { getExpiresAt } from '../services/MessageUpdater';
|
||||
import { isNormalNumber } from '../util/isNormalNumber';
|
||||
import { isNotNil } from '../util/isNotNil';
|
||||
import * as iterables from '../util/iterables';
|
||||
import { ConversationColorType, CustomColorType } from '../types/Colors';
|
||||
|
||||
import {
|
||||
|
@ -180,8 +178,8 @@ const dataInterface: ServerInterface = {
|
|||
getAllMessageIds,
|
||||
getMessagesBySentAt,
|
||||
getExpiredMessages,
|
||||
getOutgoingWithoutExpiresAt,
|
||||
getNextExpiringMessage,
|
||||
getOutgoingWithoutExpirationStartTimestamp,
|
||||
getSoonestMessageExpiry,
|
||||
getNextTapToViewMessageTimestampToAgeOut,
|
||||
getTapToViewMessagesNeedingErase,
|
||||
getOlderMessagesByConversation,
|
||||
|
@ -1859,6 +1857,38 @@ function updateToSchemaVersion32(currentVersion: number, db: Database) {
|
|||
console.log('updateToSchemaVersion32: success!');
|
||||
}
|
||||
|
||||
function updateToSchemaVersion33(currentVersion: number, db: Database) {
|
||||
if (currentVersion >= 33) {
|
||||
return;
|
||||
}
|
||||
|
||||
db.transaction(() => {
|
||||
db.exec(`
|
||||
-- These indexes should exist, but we add "IF EXISTS" for safety.
|
||||
DROP INDEX IF EXISTS messages_expires_at;
|
||||
DROP INDEX IF EXISTS messages_without_timer;
|
||||
|
||||
ALTER TABLE messages
|
||||
ADD COLUMN
|
||||
expiresAt INT
|
||||
GENERATED ALWAYS
|
||||
AS (expirationStartTimestamp + (expireTimer * 1000));
|
||||
|
||||
CREATE INDEX message_expires_at ON messages (
|
||||
expiresAt
|
||||
);
|
||||
|
||||
CREATE INDEX outgoing_messages_without_expiration_start_timestamp ON messages (
|
||||
expireTimer, expirationStartTimestamp, type
|
||||
)
|
||||
WHERE expireTimer IS NOT NULL AND expirationStartTimestamp IS NULL;
|
||||
`);
|
||||
|
||||
db.pragma('user_version = 33');
|
||||
})();
|
||||
console.log('updateToSchemaVersion33: success!');
|
||||
}
|
||||
|
||||
const SCHEMA_VERSIONS = [
|
||||
updateToSchemaVersion1,
|
||||
updateToSchemaVersion2,
|
||||
|
@ -1892,6 +1922,7 @@ const SCHEMA_VERSIONS = [
|
|||
updateToSchemaVersion30,
|
||||
updateToSchemaVersion31,
|
||||
updateToSchemaVersion32,
|
||||
updateToSchemaVersion33,
|
||||
];
|
||||
|
||||
function updateSchema(db: Database): void {
|
||||
|
@ -2997,7 +3028,6 @@ function saveMessageSync(
|
|||
const {
|
||||
body,
|
||||
conversationId,
|
||||
expires_at,
|
||||
hasAttachments,
|
||||
hasFileAttachments,
|
||||
hasVisualMediaAttachments,
|
||||
|
@ -3024,7 +3054,6 @@ function saveMessageSync(
|
|||
body: body || null,
|
||||
conversationId,
|
||||
expirationStartTimestamp: expirationStartTimestamp || null,
|
||||
expires_at: expires_at || null,
|
||||
expireTimer: expireTimer || null,
|
||||
hasAttachments: hasAttachments ? 1 : 0,
|
||||
hasFileAttachments: hasFileAttachments ? 1 : 0,
|
||||
|
@ -3053,7 +3082,6 @@ function saveMessageSync(
|
|||
body = $body,
|
||||
conversationId = $conversationId,
|
||||
expirationStartTimestamp = $expirationStartTimestamp,
|
||||
expires_at = $expires_at,
|
||||
expireTimer = $expireTimer,
|
||||
hasAttachments = $hasAttachments,
|
||||
hasFileAttachments = $hasFileAttachments,
|
||||
|
@ -3091,7 +3119,6 @@ function saveMessageSync(
|
|||
body,
|
||||
conversationId,
|
||||
expirationStartTimestamp,
|
||||
expires_at,
|
||||
expireTimer,
|
||||
hasAttachments,
|
||||
hasFileAttachments,
|
||||
|
@ -3114,7 +3141,6 @@ function saveMessageSync(
|
|||
$body,
|
||||
$conversationId,
|
||||
$expirationStartTimestamp,
|
||||
$expires_at,
|
||||
$expireTimer,
|
||||
$hasAttachments,
|
||||
$hasFileAttachments,
|
||||
|
@ -3243,31 +3269,6 @@ async function getMessageBySender({
|
|||
return rows.map(row => jsonToObject(row.json));
|
||||
}
|
||||
|
||||
function getExpireData(
|
||||
expireTimer: number,
|
||||
readAt?: number
|
||||
): {
|
||||
expirationStartTimestamp: number;
|
||||
expiresAt: number;
|
||||
} {
|
||||
const expirationStartTimestamp = Math.min(Date.now(), readAt || Date.now());
|
||||
const expiresAt = getExpiresAt({
|
||||
expireTimer,
|
||||
expirationStartTimestamp,
|
||||
});
|
||||
|
||||
// We are guaranteeing an expirationStartTimestamp above so this should
|
||||
// definitely return a number.
|
||||
if (!expiresAt || typeof expiresAt !== 'number') {
|
||||
assert(false, 'Expected expiresAt to be a number');
|
||||
}
|
||||
|
||||
return {
|
||||
expirationStartTimestamp,
|
||||
expiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
async function getUnreadCountForConversation(
|
||||
conversationId: string
|
||||
): Promise<number> {
|
||||
|
@ -3296,10 +3297,33 @@ async function getUnreadByConversationAndMarkRead(
|
|||
> {
|
||||
const db = getInstance();
|
||||
return db.transaction(() => {
|
||||
const expirationStartTimestamp = Math.min(Date.now(), readAt ?? Infinity);
|
||||
db.prepare<Query>(
|
||||
`
|
||||
UPDATE messages
|
||||
SET
|
||||
expirationStartTimestamp = $expirationStartTimestamp,
|
||||
json = json_patch(json, $jsonPatch)
|
||||
WHERE
|
||||
(
|
||||
expirationStartTimestamp IS NULL OR
|
||||
expirationStartTimestamp > $expirationStartTimestamp
|
||||
) AND
|
||||
expireTimer IS NOT NULL AND
|
||||
conversationId = $conversationId AND
|
||||
received_at <= $newestUnreadId;
|
||||
`
|
||||
).run({
|
||||
conversationId,
|
||||
expirationStartTimestamp,
|
||||
jsonPatch: JSON.stringify({ expirationStartTimestamp }),
|
||||
newestUnreadId,
|
||||
});
|
||||
|
||||
const rows = db
|
||||
.prepare<Query>(
|
||||
`
|
||||
SELECT id, expires_at, expireTimer, expirationStartTimestamp, json
|
||||
SELECT id, json
|
||||
FROM messages WHERE
|
||||
unread = $unread AND
|
||||
conversationId = $conversationId AND
|
||||
|
@ -3313,10 +3337,6 @@ async function getUnreadByConversationAndMarkRead(
|
|||
newestUnreadId,
|
||||
});
|
||||
|
||||
if (!rows.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
db.prepare<Query>(
|
||||
`
|
||||
UPDATE messages
|
||||
|
@ -3335,58 +3355,18 @@ async function getUnreadByConversationAndMarkRead(
|
|||
unread: 1,
|
||||
});
|
||||
|
||||
const rowsWithExpireTimers = iterables.filter(rows, row => row.expireTimer);
|
||||
const rowsNeedingExpirationUpdates = iterables.filter(
|
||||
rowsWithExpireTimers,
|
||||
row =>
|
||||
!row.expirationStartTimestamp ||
|
||||
!row.expires_at ||
|
||||
getExpireData(row.expireTimer, readAt).expirationStartTimestamp <
|
||||
row.expirationStartTimestamp
|
||||
);
|
||||
const expirationStartTimestampUpdates: Iterable<{
|
||||
id: string;
|
||||
expirationStartTimestamp: number;
|
||||
expiresAt: number;
|
||||
}> = iterables.map(rowsNeedingExpirationUpdates, row => ({
|
||||
id: row.id,
|
||||
...getExpireData(row.expireTimer, readAt),
|
||||
}));
|
||||
const stmt = db.prepare<Query>(
|
||||
`
|
||||
UPDATE messages
|
||||
SET
|
||||
expirationStartTimestamp = $expirationStartTimestamp,
|
||||
expires_at = $expiresAt
|
||||
WHERE
|
||||
id = $id;
|
||||
`
|
||||
);
|
||||
const updatedExpireDataByRowId = new Map<
|
||||
string,
|
||||
{
|
||||
expirationStartTimestamp: number;
|
||||
expiresAt: number;
|
||||
}
|
||||
>();
|
||||
for (const update of expirationStartTimestampUpdates) {
|
||||
stmt.run(update);
|
||||
updatedExpireDataByRowId.set(update.id, update);
|
||||
}
|
||||
|
||||
return rows.map(row => {
|
||||
const json = jsonToObject(row.json);
|
||||
const updatedExpireData = updatedExpireDataByRowId.get(row.id);
|
||||
return {
|
||||
unread: false,
|
||||
...pick(json, ['id', 'sent_at', 'source', 'sourceUuid', 'type']),
|
||||
...(updatedExpireData
|
||||
? {
|
||||
expirationStartTimestamp:
|
||||
updatedExpireData.expirationStartTimestamp,
|
||||
expires_at: updatedExpireData.expiresAt,
|
||||
}
|
||||
: {}),
|
||||
...pick(json, [
|
||||
'expirationStartTimestamp',
|
||||
'id',
|
||||
'sent_at',
|
||||
'source',
|
||||
'sourceUuid',
|
||||
'type',
|
||||
]),
|
||||
};
|
||||
});
|
||||
})();
|
||||
|
@ -3916,30 +3896,29 @@ async function getExpiredMessages(): Promise<Array<MessageType>> {
|
|||
.prepare<Query>(
|
||||
`
|
||||
SELECT json FROM messages WHERE
|
||||
expires_at IS NOT NULL AND
|
||||
expires_at <= $expires_at
|
||||
ORDER BY expires_at ASC;
|
||||
expiresAt IS NOT NULL AND
|
||||
expiresAt <= $now
|
||||
ORDER BY expiresAt ASC;
|
||||
`
|
||||
)
|
||||
.all({
|
||||
expires_at: now,
|
||||
});
|
||||
.all({ now });
|
||||
|
||||
return rows.map(row => jsonToObject(row.json));
|
||||
}
|
||||
|
||||
async function getOutgoingWithoutExpiresAt(): Promise<Array<MessageType>> {
|
||||
async function getOutgoingWithoutExpirationStartTimestamp(): Promise<
|
||||
Array<MessageType>
|
||||
> {
|
||||
const db = getInstance();
|
||||
const rows: JSONRows = db
|
||||
.prepare<EmptyQuery>(
|
||||
`
|
||||
SELECT json FROM messages
|
||||
INDEXED BY messages_without_timer
|
||||
INDEXED BY outgoing_messages_without_expiration_start_timestamp
|
||||
WHERE
|
||||
expireTimer > 0 AND
|
||||
expires_at IS NULL AND
|
||||
type IS 'outgoing'
|
||||
ORDER BY expires_at ASC;
|
||||
expirationStartTimestamp IS NULL AND
|
||||
type IS 'outgoing';
|
||||
`
|
||||
)
|
||||
.all();
|
||||
|
@ -3947,26 +3926,21 @@ async function getOutgoingWithoutExpiresAt(): Promise<Array<MessageType>> {
|
|||
return rows.map(row => jsonToObject(row.json));
|
||||
}
|
||||
|
||||
async function getNextExpiringMessage(): Promise<MessageType | undefined> {
|
||||
async function getSoonestMessageExpiry(): Promise<undefined | number> {
|
||||
const db = getInstance();
|
||||
|
||||
// Note: we avoid 'IS NOT NULL' here because it does seem to bypass our index
|
||||
const rows: JSONRows = db
|
||||
// Note: we use `pluck` to only get the first column.
|
||||
const result: null | number = db
|
||||
.prepare<EmptyQuery>(
|
||||
`
|
||||
SELECT json FROM messages
|
||||
WHERE expires_at > 0
|
||||
ORDER BY expires_at ASC
|
||||
LIMIT 1;
|
||||
SELECT MIN(expiresAt)
|
||||
FROM messages;
|
||||
`
|
||||
)
|
||||
.all();
|
||||
.pluck(true)
|
||||
.get();
|
||||
|
||||
if (!rows || rows.length < 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return jsonToObject(rows[0].json);
|
||||
return result || undefined;
|
||||
}
|
||||
|
||||
async function getNextTapToViewMessageTimestampToAgeOut(): Promise<
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
|
@ -56,7 +56,6 @@ export type OutgoingMessage = Readonly<
|
|||
|
||||
// Optional
|
||||
body?: string;
|
||||
expires_at?: number;
|
||||
expireTimer?: number;
|
||||
messageTimer?: number; // deprecated
|
||||
isViewOnce?: number;
|
||||
|
|
|
@ -11,7 +11,9 @@ const updateMessageBatcher = createBatcher<MessageAttributesType>({
|
|||
maxSize: 50,
|
||||
processBatch: async (messageAttrs: Array<MessageAttributesType>) => {
|
||||
window.log.info('updateMessageBatcher', messageAttrs.length);
|
||||
await window.Signal.Data.saveMessages(messageAttrs, {});
|
||||
await window.Signal.Data.saveMessages(messageAttrs, {
|
||||
Message: window.Whisper.Message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -37,6 +39,9 @@ export const saveNewMessageBatcher = createWaitBatcher<MessageAttributesType>({
|
|||
maxSize: 30,
|
||||
processBatch: async (messageAttrs: Array<MessageAttributesType>) => {
|
||||
window.log.info('saveNewMessageBatcher', messageAttrs.length);
|
||||
await window.Signal.Data.saveMessages(messageAttrs, { forceSave: true });
|
||||
await window.Signal.Data.saveMessages(messageAttrs, {
|
||||
forceSave: true,
|
||||
Message: window.Whisper.Message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue