Use minimal replacement class for MessageModel

This commit is contained in:
Scott Nonnenberg 2025-01-10 08:18:32 +10:00 committed by GitHub
parent 6b00cf756e
commit f846678b90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
95 changed files with 3919 additions and 4457 deletions

View file

@ -15,6 +15,7 @@ import { migrateAllMessages } from './messages/migrateMessageData';
import { SECOND } from './util/durations'; import { SECOND } from './util/durations';
import { isSignalRoute } from './util/signalRoutes'; import { isSignalRoute } from './util/signalRoutes';
import { strictAssert } from './util/assert'; import { strictAssert } from './util/assert';
import { MessageModel } from './models/messages';
type ResolveType = (data: unknown) => void; type ResolveType = (data: unknown) => void;
@ -142,12 +143,7 @@ export function getCI({ deviceName }: GetCIOptionsType): CIType {
[sentAt] [sentAt]
); );
return messages.map( return messages.map(
m => m => window.MessageCache.register(new MessageModel(m)).attributes
window.MessageCache.__DEPRECATED$register(
m.id,
m,
'CI.getMessagesBySentAt'
).attributes
); );
} }

View file

@ -16,6 +16,7 @@ import { stats } from '../util/benchmark/stats';
import type { StatsType } from '../util/benchmark/stats'; import type { StatsType } from '../util/benchmark/stats';
import type { MessageAttributesType } from '../model-types.d'; import type { MessageAttributesType } from '../model-types.d';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { postSaveUpdates } from '../util/cleanup';
const BUFFER_DELAY_MS = 50; const BUFFER_DELAY_MS = 50;
@ -90,6 +91,7 @@ export async function populateConversationWithMessages({
await DataWriter.saveMessages(messages, { await DataWriter.saveMessages(messages, {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
conversation.set('active_at', Date.now()); conversation.set('active_at', Date.now());

View file

@ -203,6 +203,9 @@ import {
maybeQueueDeviceNameFetch, maybeQueueDeviceNameFetch,
onDeviceNameChangeSync, onDeviceNameChangeSync,
} from './util/onDeviceNameChangeSync'; } from './util/onDeviceNameChangeSync';
import { postSaveUpdates } from './util/cleanup';
import { handleDataMessage } from './messages/handleDataMessage';
import { MessageModel } from './models/messages';
export function isOverHourIntoPast(timestamp: number): boolean { export function isOverHourIntoPast(timestamp: number): boolean {
return isNumber(timestamp) && isOlderThan(timestamp, HOUR); return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
@ -1421,7 +1424,7 @@ export async function startApp(): Promise<void> {
void badgeImageFileDownloader.checkForFilesToDownload(); void badgeImageFileDownloader.checkForFilesToDownload();
initializeExpiringMessageService(singleProtoJobQueue); initializeExpiringMessageService();
log.info('Blocked uuids cleanup: starting...'); log.info('Blocked uuids cleanup: starting...');
const blockedUuids = window.storage.get(BLOCKED_UUIDS_ID, []); const blockedUuids = window.storage.get(BLOCKED_UUIDS_ID, []);
@ -1473,6 +1476,7 @@ export async function startApp(): Promise<void> {
await DataWriter.saveMessages(newMessageAttributes, { await DataWriter.saveMessages(newMessageAttributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
} }
log.info('Expiration start timestamp cleanup: complete'); log.info('Expiration start timestamp cleanup: complete');
@ -2627,7 +2631,7 @@ export async function startApp(): Promise<void> {
} }
// Don't wait for handleDataMessage, as it has its own per-conversation queueing // Don't wait for handleDataMessage, as it has its own per-conversation queueing
drop(message.handleDataMessage(data.message, event.confirm)); drop(handleDataMessage(message, data.message, event.confirm));
} }
async function onProfileKey({ async function onProfileKey({
@ -2803,7 +2807,7 @@ export async function startApp(): Promise<void> {
unidentifiedDeliveries, unidentifiedDeliveries,
}; };
return new window.Whisper.Message(partialMessage); return new MessageModel(partialMessage);
} }
// Works with 'sent' and 'message' data sent from MessageReceiver // Works with 'sent' and 'message' data sent from MessageReceiver
@ -3021,7 +3025,7 @@ export async function startApp(): Promise<void> {
// Don't wait for handleDataMessage, as it has its own per-conversation queueing // Don't wait for handleDataMessage, as it has its own per-conversation queueing
drop( drop(
message.handleDataMessage(data.message, event.confirm, { handleDataMessage(message, data.message, event.confirm, {
data, data,
}) })
); );
@ -3060,7 +3064,7 @@ export async function startApp(): Promise<void> {
type: data.message.isStory ? 'story' : 'incoming', type: data.message.isStory ? 'story' : 'incoming',
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived, unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
}; };
return new window.Whisper.Message(partialMessage); return new MessageModel(partialMessage);
} }
// Returns `false` if this message isn't a group call message. // Returns `false` if this message isn't a group call message.

View file

@ -102,6 +102,8 @@ import {
} from './util/groupSendEndorsements'; } from './util/groupSendEndorsements';
import { getProfile } from './util/getProfile'; import { getProfile } from './util/getProfile';
import { generateMessageId } from './util/generateMessageId'; import { generateMessageId } from './util/generateMessageId';
import { postSaveUpdates } from './util/cleanup';
import { MessageModel } from './models/messages';
type AccessRequiredEnum = Proto.AccessControl.AccessRequired; type AccessRequiredEnum = Proto.AccessControl.AccessRequired;
@ -253,7 +255,7 @@ export type GroupV2ChangeDetailType =
export type GroupV2ChangeType = { export type GroupV2ChangeType = {
from?: ServiceIdString; from?: ServiceIdString;
details: Array<GroupV2ChangeDetailType>; details: ReadonlyArray<GroupV2ChangeDetailType>;
}; };
export type GroupFields = { export type GroupFields = {
@ -2016,7 +2018,7 @@ export async function createGroupV2(
revision: groupV2Info.revision, revision: groupV2Info.revision,
}); });
const createdTheGroupMessage: MessageAttributesType = { const createdTheGroupMessage = new MessageModel({
...generateMessageId(incrementMessageCounter()), ...generateMessageId(incrementMessageCounter()),
schemaVersion: MAX_MESSAGE_SCHEMA, schemaVersion: MAX_MESSAGE_SCHEMA,
@ -2032,17 +2034,12 @@ export async function createGroupV2(
from: ourAci, from: ourAci,
details: [{ type: 'create' }], details: [{ type: 'create' }],
}, },
};
await DataWriter.saveMessages([createdTheGroupMessage], {
forceSave: true,
ourAci,
}); });
window.MessageCache.__DEPRECATED$register( await window.MessageCache.saveMessage(createdTheGroupMessage, {
createdTheGroupMessage.id, forceSave: true,
new window.Whisper.Message(createdTheGroupMessage), });
'createGroupV2' window.MessageCache.register(createdTheGroupMessage);
); drop(conversation.onNewMessage(createdTheGroupMessage));
conversation.trigger('newmessage', createdTheGroupMessage);
if (expireTimer) { if (expireTimer) {
await conversation.updateExpirationTimer(expireTimer, { await conversation.updateExpirationTimer(expireTimer, {
@ -3442,6 +3439,7 @@ async function appendChangeMessages(
log.info(`appendChangeMessages/${logId}: updating ${first.id}`); log.info(`appendChangeMessages/${logId}: updating ${first.id}`);
await DataWriter.saveMessage(first, { await DataWriter.saveMessage(first, {
ourAci, ourAci,
postSaveUpdates,
// We don't use forceSave here because this is an update of existing // We don't use forceSave here because this is an update of existing
// message. // message.
@ -3453,6 +3451,7 @@ async function appendChangeMessages(
await DataWriter.saveMessages(rest, { await DataWriter.saveMessages(rest, {
ourAci, ourAci,
forceSave: true, forceSave: true,
postSaveUpdates,
}); });
} else { } else {
log.info( log.info(
@ -3461,15 +3460,13 @@ async function appendChangeMessages(
await DataWriter.saveMessages(mergedMessages, { await DataWriter.saveMessages(mergedMessages, {
ourAci, ourAci,
forceSave: true, forceSave: true,
postSaveUpdates,
}); });
} }
let newMessages = 0; let newMessages = 0;
for (const changeMessage of mergedMessages) { for (const changeMessage of mergedMessages) {
const existing = window.MessageCache.__DEPRECATED$getById( const existing = window.MessageCache.getById(changeMessage.id);
changeMessage.id,
'appendChangeMessages'
);
// Update existing message // Update existing message
if (existing) { if (existing) {
@ -3481,12 +3478,8 @@ async function appendChangeMessages(
continue; continue;
} }
window.MessageCache.__DEPRECATED$register( const model = window.MessageCache.register(new MessageModel(changeMessage));
changeMessage.id, drop(conversation.onNewMessage(model));
new window.Whisper.Message(changeMessage),
'appendChangeMessages'
);
conversation.trigger('newmessage', changeMessage);
newMessages += 1; newMessages += 1;
} }

View file

@ -24,7 +24,7 @@ import {
AttachmentVariant, AttachmentVariant,
mightBeOnBackupTier, mightBeOnBackupTier,
} from '../types/Attachment'; } from '../types/Attachment';
import { __DEPRECATED$getMessageById } from '../messages/getMessageById'; import { getMessageById } from '../messages/getMessageById';
import { import {
KIBIBYTE, KIBIBYTE,
getMaximumIncomingAttachmentSizeInKb, getMaximumIncomingAttachmentSizeInKb,
@ -52,6 +52,7 @@ import {
} from '../AttachmentCrypto'; } from '../AttachmentCrypto';
import { safeParsePartial } from '../util/schemas'; import { safeParsePartial } from '../util/schemas';
import { createBatcher } from '../util/batcher'; import { createBatcher } from '../util/batcher';
import { postSaveUpdates } from '../util/cleanup';
export enum AttachmentDownloadUrgency { export enum AttachmentDownloadUrgency {
IMMEDIATE = 'immediate', IMMEDIATE = 'immediate',
@ -327,10 +328,7 @@ async function runDownloadAttachmentJob({
const jobIdForLogging = getJobIdForLogging(job); const jobIdForLogging = getJobIdForLogging(job);
const logId = `AttachmentDownloadManager/runDownloadAttachmentJob/${jobIdForLogging}`; const logId = `AttachmentDownloadManager/runDownloadAttachmentJob/${jobIdForLogging}`;
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(job.messageId);
job.messageId,
'runDownloadAttachmentJob'
);
if (!message) { if (!message) {
log.error(`${logId} message not found`); log.error(`${logId} message not found`);
@ -430,6 +428,7 @@ async function runDownloadAttachmentJob({
// is good // is good
await DataWriter.saveMessage(message.attributes, { await DataWriter.saveMessage(message.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
} }
} }

View file

@ -28,7 +28,7 @@ import { getUntrustedConversationServiceIds } from './getUntrustedConversationSe
import { handleMessageSend } from '../../util/handleMessageSend'; import { handleMessageSend } from '../../util/handleMessageSend';
import { isConversationAccepted } from '../../util/isConversationAccepted'; import { isConversationAccepted } from '../../util/isConversationAccepted';
import { isConversationUnregistered } from '../../util/isConversationUnregistered'; import { isConversationUnregistered } from '../../util/isConversationUnregistered';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; import { getMessageById } from '../../messages/getMessageById';
import { isNotNil } from '../../util/isNotNil'; import { isNotNil } from '../../util/isNotNil';
import type { CallbackResultType } from '../../textsecure/Types.d'; import type { CallbackResultType } from '../../textsecure/Types.d';
import type { MessageModel } from '../../models/messages'; import type { MessageModel } from '../../models/messages';
@ -38,6 +38,7 @@ import type { LoggerType } from '../../types/Logging';
import type { ServiceIdString } from '../../types/ServiceId'; import type { ServiceIdString } from '../../types/ServiceId';
import { isStory } from '../../messages/helpers'; import { isStory } from '../../messages/helpers';
import { sendToGroup } from '../../util/sendToGroup'; import { sendToGroup } from '../../util/sendToGroup';
import { postSaveUpdates } from '../../util/cleanup';
export async function sendDeleteForEveryone( export async function sendDeleteForEveryone(
conversation: ConversationModel, conversation: ConversationModel,
@ -60,7 +61,7 @@ export async function sendDeleteForEveryone(
const logId = `sendDeleteForEveryone(${conversation.idForLogging()}, ${messageId})`; const logId = `sendDeleteForEveryone(${conversation.idForLogging()}, ${messageId})`;
const message = await __DEPRECATED$getMessageById(messageId, logId); const message = await getMessageById(messageId);
if (!message) { if (!message) {
log.error(`${logId}: Failed to fetch message. Failing job.`); log.error(`${logId}: Failed to fetch message. Failing job.`);
return; return;
@ -307,6 +308,7 @@ async function updateMessageWithSuccessfulSends(
}); });
await DataWriter.saveMessage(message.attributes, { await DataWriter.saveMessage(message.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
return; return;
@ -330,6 +332,7 @@ async function updateMessageWithSuccessfulSends(
}); });
await DataWriter.saveMessage(message.attributes, { await DataWriter.saveMessage(message.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
} }
@ -346,5 +349,6 @@ async function updateMessageWithFailure(
message.set({ deletedForEveryoneFailed: true }); message.set({ deletedForEveryoneFailed: true });
await DataWriter.saveMessage(message.attributes, { await DataWriter.saveMessage(message.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
} }

View file

@ -21,7 +21,7 @@ import { getUntrustedConversationServiceIds } from './getUntrustedConversationSe
import { handleMessageSend } from '../../util/handleMessageSend'; import { handleMessageSend } from '../../util/handleMessageSend';
import { isConversationAccepted } from '../../util/isConversationAccepted'; import { isConversationAccepted } from '../../util/isConversationAccepted';
import { isConversationUnregistered } from '../../util/isConversationUnregistered'; import { isConversationUnregistered } from '../../util/isConversationUnregistered';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; import { getMessageById } from '../../messages/getMessageById';
import { isNotNil } from '../../util/isNotNil'; import { isNotNil } from '../../util/isNotNil';
import type { CallbackResultType } from '../../textsecure/Types.d'; import type { CallbackResultType } from '../../textsecure/Types.d';
import type { MessageModel } from '../../models/messages'; import type { MessageModel } from '../../models/messages';
@ -29,6 +29,7 @@ import { SendMessageProtoError } from '../../textsecure/Errors';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import type { LoggerType } from '../../types/Logging'; import type { LoggerType } from '../../types/Logging';
import { isStory } from '../../messages/helpers'; import { isStory } from '../../messages/helpers';
import { postSaveUpdates } from '../../util/cleanup';
export async function sendDeleteStoryForEveryone( export async function sendDeleteStoryForEveryone(
ourConversation: ConversationModel, ourConversation: ConversationModel,
@ -46,10 +47,7 @@ export async function sendDeleteStoryForEveryone(
const logId = `sendDeleteStoryForEveryone(${storyId})`; const logId = `sendDeleteStoryForEveryone(${storyId})`;
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(storyId);
storyId,
'sendDeleteStoryForEveryone'
);
if (!message) { if (!message) {
log.error(`${logId}: Failed to fetch message. Failing job.`); log.error(`${logId}: Failed to fetch message. Failing job.`);
return; return;
@ -284,6 +282,7 @@ async function updateMessageWithSuccessfulSends(
}); });
await DataWriter.saveMessage(message.attributes, { await DataWriter.saveMessage(message.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
return; return;
@ -307,6 +306,7 @@ async function updateMessageWithSuccessfulSends(
}); });
await DataWriter.saveMessage(message.attributes, { await DataWriter.saveMessage(message.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
} }
@ -323,5 +323,6 @@ async function updateMessageWithFailure(
message.set({ deletedForEveryoneFailed: true }); message.set({ deletedForEveryoneFailed: true });
await DataWriter.saveMessage(message.attributes, { await DataWriter.saveMessage(message.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
} }

View file

@ -8,7 +8,7 @@ import { DataWriter } from '../../sql/Client';
import * as Errors from '../../types/errors'; import * as Errors from '../../types/errors';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import type { MessageModel } from '../../models/messages'; import type { MessageModel } from '../../models/messages';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; import { getMessageById } from '../../messages/getMessageById';
import type { ConversationModel } from '../../models/conversations'; import type { ConversationModel } from '../../models/conversations';
import { isGroup, isGroupV2, isMe } from '../../util/whatTypeOfConversation'; import { isGroup, isGroupV2, isMe } from '../../util/whatTypeOfConversation';
import { getSendOptions } from '../../util/getSendOptions'; import { getSendOptions } from '../../util/getSendOptions';
@ -56,6 +56,13 @@ import {
import { getMessageSentTimestamp } from '../../util/getMessageSentTimestamp'; import { getMessageSentTimestamp } from '../../util/getMessageSentTimestamp';
import { isSignalConversation } from '../../util/isSignalConversation'; import { isSignalConversation } from '../../util/isSignalConversation';
import { isBodyTooLong, trimBody } from '../../util/longAttachment'; import { isBodyTooLong, trimBody } from '../../util/longAttachment';
import {
markFailed,
saveErrorsOnMessage,
} from '../../test-node/util/messageFailures';
import { getMessageIdForLogging } from '../../util/idForLogging';
import { postSaveUpdates } from '../../util/cleanup';
import { send, sendSyncMessageOnly } from '../../messages/send';
const MAX_CONCURRENT_ATTACHMENT_UPLOADS = 5; const MAX_CONCURRENT_ATTACHMENT_UPLOADS = 5;
@ -73,10 +80,7 @@ export async function sendNormalMessage(
const { Message } = window.Signal.Types; const { Message } = window.Signal.Types;
const { messageId, revision, editedMessageTimestamp } = data; const { messageId, revision, editedMessageTimestamp } = data;
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'sendNormalMessage'
);
if (!message) { if (!message) {
log.info( log.info(
`message ${messageId} was not found, maybe because it was deleted. Giving up on sending it` `message ${messageId} was not found, maybe because it was deleted. Giving up on sending it`
@ -84,7 +88,9 @@ export async function sendNormalMessage(
return; return;
} }
const messageConversation = message.getConversation(); const messageConversation = window.ConversationController.get(
message.get('conversationId')
);
if (messageConversation !== conversation) { if (messageConversation !== conversation) {
log.error( log.error(
`Message conversation '${messageConversation?.idForLogging()}' does not match job conversation ${conversation.idForLogging()}` `Message conversation '${messageConversation?.idForLogging()}' does not match job conversation ${conversation.idForLogging()}`
@ -106,7 +112,7 @@ export async function sendNormalMessage(
return; return;
} }
if (message.isErased() || message.get('deletedForEveryone')) { if (message.get('isErased') || message.get('deletedForEveryone')) {
log.info(`message ${messageId} was erased. Giving up on sending it`); log.info(`message ${messageId} was erased. Giving up on sending it`);
return; return;
} }
@ -285,7 +291,7 @@ export async function sendNormalMessage(
timestamp: targetTimestamp, timestamp: targetTimestamp,
reaction, reaction,
}); });
messageSendPromise = message.sendSyncMessageOnly({ messageSendPromise = sendSyncMessageOnly(message, {
dataMessage, dataMessage,
saveErrors, saveErrors,
targetTimestamp, targetTimestamp,
@ -407,7 +413,7 @@ export async function sendNormalMessage(
}); });
} }
messageSendPromise = message.send({ messageSendPromise = send(message, {
promise: handleMessageSend(innerPromise, { promise: handleMessageSend(innerPromise, {
messageIds: [messageId], messageIds: [messageId],
sendType: 'message', sendType: 'message',
@ -657,14 +663,13 @@ async function getMessageSendData({
uploadQueue, uploadQueue,
}), }),
uploadMessageSticker(message, uploadQueue), uploadMessageSticker(message, uploadQueue),
storyId storyId ? getMessageById(storyId) : undefined,
? __DEPRECATED$getMessageById(storyId, 'sendNormalMessage')
: undefined,
]); ]);
// Save message after uploading attachments // Save message after uploading attachments
await DataWriter.saveMessage(message.attributes, { await DataWriter.saveMessage(message.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
const storyReaction = message.get('storyReaction'); const storyReaction = message.get('storyReaction');
@ -732,7 +737,7 @@ async function uploadSingleAttachment({
const uploaded = await uploadAttachment(withData); const uploaded = await uploadAttachment(withData);
// Add digest to the attachment // Add digest to the attachment
const logId = `uploadSingleAttachment(${message.idForLogging()}`; const logId = `uploadSingleAttachment(${getMessageIdForLogging(message.attributes)}`;
const oldAttachments = getPropForTimestamp({ const oldAttachments = getPropForTimestamp({
log, log,
message: message.attributes, message: message.attributes,
@ -788,7 +793,7 @@ async function uploadLongMessageAttachment({
const uploaded = await uploadAttachment(withData); const uploaded = await uploadAttachment(withData);
// Add digest to the attachment // Add digest to the attachment
const logId = `uploadLongMessageAttachment(${message.idForLogging()}`; const logId = `uploadLongMessageAttachment(${getMessageIdForLogging(message.attributes)}`;
const oldAttachment = getPropForTimestamp({ const oldAttachment = getPropForTimestamp({
log, log,
message: message.attributes, message: message.attributes,
@ -872,7 +877,7 @@ async function uploadMessageQuote({
); );
// Update message with attachment digests // Update message with attachment digests
const logId = `uploadMessageQuote(${message.idForLogging()}`; const logId = `uploadMessageQuote(${getMessageIdForLogging(message.attributes)}`;
const oldQuote = getPropForTimestamp({ const oldQuote = getPropForTimestamp({
log, log,
message: message.attributes, message: message.attributes,
@ -980,7 +985,7 @@ async function uploadMessagePreviews({
); );
// Update message with attachment digests // Update message with attachment digests
const logId = `uploadMessagePreviews(${message.idForLogging()}`; const logId = `uploadMessagePreviews(${getMessageIdForLogging(message.attributes)}`;
const oldPreview = getPropForTimestamp({ const oldPreview = getPropForTimestamp({
log, log,
message: message.attributes, message: message.attributes,
@ -1043,7 +1048,7 @@ async function uploadMessageSticker(
); );
// Add digest to the attachment // Add digest to the attachment
const logId = `uploadMessageSticker(${message.idForLogging()}`; const logId = `uploadMessageSticker(${getMessageIdForLogging(message.attributes)}`;
const existingSticker = message.get('sticker'); const existingSticker = message.get('sticker');
strictAssert( strictAssert(
existingSticker?.data !== undefined, existingSticker?.data !== undefined,
@ -1054,11 +1059,13 @@ async function uploadMessageSticker(
existingSticker.data.path === startingSticker?.data?.path, existingSticker.data.path === startingSticker?.data?.path,
`${logId}: Sticker was uploaded, but message has a different sticker` `${logId}: Sticker was uploaded, but message has a different sticker`
); );
message.set('sticker', { message.set({
...existingSticker, sticker: {
data: { ...existingSticker,
...existingSticker.data, data: {
...copyCdnFields(uploaded), ...existingSticker.data,
...copyCdnFields(uploaded),
},
}, },
}); });
@ -1111,7 +1118,7 @@ async function uploadMessageContacts(
); );
// Add digest to the attachment // Add digest to the attachment
const logId = `uploadMessageContacts(${message.idForLogging()}`; const logId = `uploadMessageContacts(${getMessageIdForLogging(message.attributes)}`;
const oldContact = message.get('contact'); const oldContact = message.get('contact');
strictAssert(oldContact, `${logId}: Contacts are gone after upload`); strictAssert(oldContact, `${logId}: Contacts are gone after upload`);
@ -1148,7 +1155,7 @@ async function uploadMessageContacts(
}, },
}; };
}); });
message.set('contact', newContact); message.set({ contact: newContact });
return uploadedContacts; return uploadedContacts;
} }
@ -1162,10 +1169,9 @@ async function markMessageFailed({
message: MessageModel; message: MessageModel;
targetTimestamp: number; targetTimestamp: number;
}): Promise<void> { }): Promise<void> {
message.markFailed(targetTimestamp); markFailed(message, targetTimestamp);
void message.saveErrors(errors, { skipSave: true }); await saveErrorsOnMessage(message, errors, {
await DataWriter.saveMessage(message.attributes, { skipSave: false,
ourAci: window.textsecure.storage.user.getCheckedAci(),
}); });
} }

View file

@ -7,14 +7,14 @@ import * as Errors from '../../types/errors';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { repeat, zipObject } from '../../util/iterables'; import { repeat, zipObject } from '../../util/iterables';
import type { CallbackResultType } from '../../textsecure/Types.d'; import type { CallbackResultType } from '../../textsecure/Types.d';
import type { MessageModel } from '../../models/messages'; import { MessageModel } from '../../models/messages';
import type { MessageReactionType } from '../../model-types.d'; import type { MessageReactionType } from '../../model-types.d';
import type { ConversationModel } from '../../models/conversations'; import type { ConversationModel } from '../../models/conversations';
import { DataWriter } from '../../sql/Client'; import { DataWriter } from '../../sql/Client';
import * as reactionUtil from '../../reactions/util'; import * as reactionUtil from '../../reactions/util';
import { isSent, SendStatus } from '../../messages/MessageSendState'; import { isSent, SendStatus } from '../../messages/MessageSendState';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; import { getMessageById } from '../../messages/getMessageById';
import { isIncoming } from '../../messages/helpers'; import { isIncoming } from '../../messages/helpers';
import { import {
isMe, isMe,
@ -41,6 +41,9 @@ import { isConversationAccepted } from '../../util/isConversationAccepted';
import { isConversationUnregistered } from '../../util/isConversationUnregistered'; import { isConversationUnregistered } from '../../util/isConversationUnregistered';
import type { LoggerType } from '../../types/Logging'; import type { LoggerType } from '../../types/Logging';
import { sendToGroup } from '../../util/sendToGroup'; import { sendToGroup } from '../../util/sendToGroup';
import { hydrateStoryContext } from '../../util/hydrateStoryContext';
import { postSaveUpdates } from '../../util/cleanup';
import { send, sendSyncMessageOnly } from '../../messages/send';
export async function sendReaction( export async function sendReaction(
conversation: ConversationModel, conversation: ConversationModel,
@ -61,7 +64,7 @@ export async function sendReaction(
const ourConversationId = const ourConversationId =
window.ConversationController.getOurConversationIdOrThrow(); window.ConversationController.getOurConversationIdOrThrow();
const message = await __DEPRECATED$getMessageById(messageId, 'sendReaction'); const message = await getMessageById(messageId);
if (!message) { if (!message) {
log.info( log.info(
`message ${messageId} was not found, maybe because it was deleted. Giving up on sending its reactions` `message ${messageId} was not found, maybe because it was deleted. Giving up on sending its reactions`
@ -87,7 +90,10 @@ export async function sendReaction(
if (!canReact(message.attributes, ourConversationId, findAndFormatContact)) { if (!canReact(message.attributes, ourConversationId, findAndFormatContact)) {
log.info(`could not react to ${messageId}. Removing this pending reaction`); log.info(`could not react to ${messageId}. Removing this pending reaction`);
markReactionFailed(message, pendingReaction); markReactionFailed(message, pendingReaction);
await DataWriter.saveMessage(message.attributes, { ourAci }); await DataWriter.saveMessage(message.attributes, {
ourAci,
postSaveUpdates,
});
return; return;
} }
@ -96,7 +102,10 @@ export async function sendReaction(
`reacting to message ${messageId} ran out of time. Giving up on sending it` `reacting to message ${messageId} ran out of time. Giving up on sending it`
); );
markReactionFailed(message, pendingReaction); markReactionFailed(message, pendingReaction);
await DataWriter.saveMessage(message.attributes, { ourAci }); await DataWriter.saveMessage(message.attributes, {
ourAci,
postSaveUpdates,
});
return; return;
} }
@ -108,7 +117,9 @@ export async function sendReaction(
let originalError: Error | undefined; let originalError: Error | undefined;
try { try {
const messageConversation = message.getConversation(); const messageConversation = window.ConversationController.get(
message.get('conversationId')
);
if (messageConversation !== conversation) { if (messageConversation !== conversation) {
log.error( log.error(
`message conversation '${messageConversation?.idForLogging()}' does not match job conversation ${conversation.idForLogging()}` `message conversation '${messageConversation?.idForLogging()}' does not match job conversation ${conversation.idForLogging()}`
@ -158,7 +169,7 @@ export async function sendReaction(
targetAuthorAci, targetAuthorAci,
remove: !emoji, remove: !emoji,
}; };
const ephemeralMessageForReactionSend = new window.Whisper.Message({ const ephemeralMessageForReactionSend = new MessageModel({
...generateMessageId(incrementMessageCounter()), ...generateMessageId(incrementMessageCounter()),
type: 'outgoing', type: 'outgoing',
conversationId: conversation.get('id'), conversationId: conversation.get('id'),
@ -173,14 +184,13 @@ export async function sendReaction(
}) })
), ),
}); });
// Adds the reaction's attributes to the message cache so that we can
// safely `set` on it later.
window.MessageCache.toMessageAttributes(
ephemeralMessageForReactionSend.attributes
);
ephemeralMessageForReactionSend.doNotSave = true; ephemeralMessageForReactionSend.doNotSave = true;
// Adds the reaction's attributes to the message cache so that we can
// safely `set` on it later.
window.MessageCache.register(ephemeralMessageForReactionSend);
let didFullySend: boolean; let didFullySend: boolean;
const successfulConversationIds = new Set<string>(); const successfulConversationIds = new Set<string>();
@ -199,7 +209,7 @@ export async function sendReaction(
recipients: allRecipientServiceIds, recipients: allRecipientServiceIds,
timestamp: pendingReaction.timestamp, timestamp: pendingReaction.timestamp,
}); });
await ephemeralMessageForReactionSend.sendSyncMessageOnly({ await sendSyncMessageOnly(ephemeralMessageForReactionSend, {
dataMessage, dataMessage,
saveErrors, saveErrors,
targetTimestamp: pendingReaction.timestamp, targetTimestamp: pendingReaction.timestamp,
@ -292,7 +302,7 @@ export async function sendReaction(
); );
} }
await ephemeralMessageForReactionSend.send({ await send(ephemeralMessageForReactionSend, {
promise: handleMessageSend(promise, { promise: handleMessageSend(promise, {
messageIds: [messageId], messageIds: [messageId],
sendType: 'reaction', sendType: 'reaction',
@ -334,19 +344,16 @@ export async function sendReaction(
if (!ephemeralMessageForReactionSend.doNotSave) { if (!ephemeralMessageForReactionSend.doNotSave) {
const reactionMessage = ephemeralMessageForReactionSend; const reactionMessage = ephemeralMessageForReactionSend;
await reactionMessage.hydrateStoryContext(message.attributes, { await hydrateStoryContext(reactionMessage.id, message.attributes, {
shouldSave: false, shouldSave: false,
}); });
await DataWriter.saveMessage(reactionMessage.attributes, { await DataWriter.saveMessage(reactionMessage.attributes, {
ourAci, ourAci,
forceSave: true, forceSave: true,
postSaveUpdates,
}); });
window.MessageCache.__DEPRECATED$register( window.MessageCache.register(reactionMessage);
reactionMessage.id,
reactionMessage,
'sendReaction'
);
void conversation.addSingleMessage(reactionMessage.attributes); void conversation.addSingleMessage(reactionMessage.attributes);
} }
} }
@ -375,7 +382,10 @@ export async function sendReaction(
toThrow: originalError || thrownError, toThrow: originalError || thrownError,
}); });
} finally { } finally {
await DataWriter.saveMessage(message.attributes, { ourAci }); await DataWriter.saveMessage(message.attributes, {
ourAci,
postSaveUpdates,
});
} }
} }
@ -388,9 +398,9 @@ const setReactions = (
reactions: Array<MessageReactionType> reactions: Array<MessageReactionType>
): void => { ): void => {
if (reactions.length) { if (reactions.length) {
message.set('reactions', reactions); message.set({ reactions });
} else { } else {
message.set('reactions', undefined); message.set({ reactions: undefined });
} }
}; };

View file

@ -39,6 +39,13 @@ import { distributionListToSendTarget } from '../../util/distributionListToSendT
import { uploadAttachment } from '../../util/uploadAttachment'; import { uploadAttachment } from '../../util/uploadAttachment';
import { SendMessageChallengeError } from '../../textsecure/Errors'; import { SendMessageChallengeError } from '../../textsecure/Errors';
import type { OutgoingTextAttachmentType } from '../../textsecure/SendMessage'; import type { OutgoingTextAttachmentType } from '../../textsecure/SendMessage';
import {
markFailed,
notifyStorySendFailed,
saveErrorsOnMessage,
} from '../../test-node/util/messageFailures';
import { postSaveUpdates } from '../../util/cleanup';
import { send } from '../../messages/send';
export async function sendStory( export async function sendStory(
conversation: ConversationModel, conversation: ConversationModel,
@ -71,40 +78,40 @@ export async function sendStory(
} }
const notFound = new Set(messageIds); const notFound = new Set(messageIds);
const messages = (await getMessagesById(messageIds, 'sendStory')).filter( const messages = (await getMessagesById(messageIds)).filter(message => {
message => { notFound.delete(message.id);
notFound.delete(message.id);
const distributionId = message.get('storyDistributionListId'); const distributionId = message.get('storyDistributionListId');
const logId = `stories.sendStory(${timestamp}/${distributionId})`; const logId = `stories.sendStory(${timestamp}/${distributionId})`;
const messageConversation = message.getConversation(); const messageConversation = window.ConversationController.get(
if (messageConversation !== conversation) { message.get('conversationId')
log.error( );
`${logId}: Message conversation ` + if (messageConversation !== conversation) {
`'${messageConversation?.idForLogging()}' does not match job ` + log.error(
`conversation ${conversation.idForLogging()}` `${logId}: Message conversation ` +
); `'${messageConversation?.idForLogging()}' does not match job ` +
return false; `conversation ${conversation.idForLogging()}`
} );
return false;
if (message.get('timestamp') !== timestamp) {
log.error(
`${logId}: Message timestamp ${message.get(
'timestamp'
)} does not match job timestamp`
);
return false;
}
if (message.isErased() || message.get('deletedForEveryone')) {
log.info(`${logId}: message was erased. Giving up on sending it`);
return false;
}
return true;
} }
);
if (message.get('timestamp') !== timestamp) {
log.error(
`${logId}: Message timestamp ${message.get(
'timestamp'
)} does not match job timestamp`
);
return false;
}
if (message.get('isErased') || message.get('deletedForEveryone')) {
log.info(`${logId}: message was erased. Giving up on sending it`);
return false;
}
return true;
});
for (const messageId of notFound) { for (const messageId of notFound) {
log.info( log.info(
@ -367,7 +374,7 @@ export async function sendStory(
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
message.doNotSendSyncMessage = true; message.doNotSendSyncMessage = true;
const messageSendPromise = message.send({ const messageSendPromise = send(message, {
promise: handleMessageSend(innerPromise, { promise: handleMessageSend(innerPromise, {
messageIds: [message.id], messageIds: [message.id],
sendType: 'story', sendType: 'story',
@ -535,16 +542,17 @@ export async function sendStory(
}, {} as SendStateByConversationId); }, {} as SendStateByConversationId);
if (hasFailedSends) { if (hasFailedSends) {
message.notifyStorySendFailed(); notifyStorySendFailed(message);
} }
if (isEqual(oldSendStateByConversationId, newSendStateByConversationId)) { if (isEqual(oldSendStateByConversationId, newSendStateByConversationId)) {
return; return;
} }
message.set('sendStateByConversationId', newSendStateByConversationId); message.set({ sendStateByConversationId: newSendStateByConversationId });
return DataWriter.saveMessage(message.attributes, { return DataWriter.saveMessage(message.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
}) })
); );
@ -688,10 +696,9 @@ async function markMessageFailed(
message: MessageModel, message: MessageModel,
errors: Array<Error> errors: Array<Error>
): Promise<void> { ): Promise<void> {
message.markFailed(); markFailed(message);
void message.saveErrors(errors, { skipSave: true }); await saveErrorsOnMessage(message, errors, {
await DataWriter.saveMessage(message.attributes, { skipSave: false,
ourAci: window.textsecure.storage.user.getCheckedAci(),
}); });
} }

View file

@ -6,7 +6,55 @@ import type { AttachmentDownloadJobTypeType } from '../types/AttachmentDownload'
import type { AttachmentType } from '../types/Attachment'; import type { AttachmentType } from '../types/Attachment';
import { getAttachmentSignatureSafe, isDownloaded } from '../types/Attachment'; import { getAttachmentSignatureSafe, isDownloaded } from '../types/Attachment';
import { __DEPRECATED$getMessageById } from '../messages/getMessageById'; import { getMessageById } from '../messages/getMessageById';
export async function markAttachmentAsCorrupted(
messageId: string,
attachment: AttachmentType
): Promise<void> {
const message = await getMessageById(messageId);
if (!message) {
return;
}
if (!attachment.path) {
throw new Error(
"Attachment can't be marked as corrupted because it wasn't loaded"
);
}
// We intentionally don't check in quotes/stickers/contacts/... here,
// because this function should be called only for something that can
// be displayed as a generic attachment.
const attachments: ReadonlyArray<AttachmentType> =
message.get('attachments') || [];
let changed = false;
const newAttachments = attachments.map(existing => {
if (existing.path !== attachment.path) {
return existing;
}
changed = true;
return {
...existing,
isCorrupted: true,
};
});
if (!changed) {
throw new Error(
"Attachment can't be marked as corrupted because it wasn't found"
);
}
log.info('markAttachmentAsCorrupted: marking an attachment as corrupted');
message.set({
attachments: newAttachments,
});
}
export async function addAttachmentToMessage( export async function addAttachmentToMessage(
messageId: string, messageId: string,
@ -15,7 +63,7 @@ export async function addAttachmentToMessage(
{ type }: { type: AttachmentDownloadJobTypeType } { type }: { type: AttachmentDownloadJobTypeType }
): Promise<void> { ): Promise<void> {
const logPrefix = `${jobLogId}/addAttachmentToMessage`; const logPrefix = `${jobLogId}/addAttachmentToMessage`;
const message = await __DEPRECATED$getMessageById(messageId, logPrefix); const message = await getMessageById(messageId);
if (!message) { if (!message) {
return; return;

View file

@ -9,6 +9,7 @@ import * as Errors from '../types/errors';
import { deleteForEveryone } from '../util/deleteForEveryone'; import { deleteForEveryone } from '../util/deleteForEveryone';
import { drop } from '../util/drop'; import { drop } from '../util/drop';
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet'; import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
import { MessageModel } from '../models/messages';
export type DeleteAttributesType = { export type DeleteAttributesType = {
envelopeId: string; envelopeId: string;
@ -86,10 +87,8 @@ export async function onDelete(del: DeleteAttributesType): Promise<void> {
return; return;
} }
const message = window.MessageCache.__DEPRECATED$register( const message = window.MessageCache.register(
targetMessage.id, new MessageModel(targetMessage)
targetMessage,
'Deletes.onDelete'
); );
await deleteForEveryone(message, del); await deleteForEveryone(message, del);

View file

@ -13,6 +13,7 @@ import {
isAttachmentDownloadQueueEmpty, isAttachmentDownloadQueueEmpty,
registerQueueEmptyCallback, registerQueueEmptyCallback,
} from '../util/attachmentDownloadQueue'; } from '../util/attachmentDownloadQueue';
import { MessageModel } from '../models/messages';
export type EditAttributesType = { export type EditAttributesType = {
conversationId: string; conversationId: string;
@ -134,10 +135,8 @@ export async function onEdit(edit: EditAttributesType): Promise<void> {
return; return;
} }
const message = window.MessageCache.__DEPRECATED$register( const message = window.MessageCache.register(
targetMessage.id, new MessageModel(targetMessage)
targetMessage,
'Edits.onEdit'
); );
await handleEditMessage(message.attributes, edit); await handleEditMessage(message.attributes, edit);

View file

@ -32,6 +32,9 @@ import {
RECEIPT_BATCHER_WAIT_MS, RECEIPT_BATCHER_WAIT_MS,
} from '../types/Receipt'; } from '../types/Receipt';
import { drop } from '../util/drop'; import { drop } from '../util/drop';
import { getMessageById } from '../messages/getMessageById';
import { postSaveUpdates } from '../util/cleanup';
import { MessageModel } from '../models/messages';
const { deleteSentProtoRecipient, removeSyncTaskById } = DataWriter; const { deleteSentProtoRecipient, removeSyncTaskById } = DataWriter;
@ -78,12 +81,11 @@ const processReceiptBatcher = createWaitBatcher({
> = new Map(); > = new Map();
function addReceiptAndTargetMessage( function addReceiptAndTargetMessage(
message: MessageAttributesType, message: MessageModel,
receipt: MessageReceiptAttributesType receipt: MessageReceiptAttributesType
): void { ): void {
const existing = receiptsByMessageId.get(message.id); const existing = receiptsByMessageId.get(message.id);
if (!existing) { if (!existing) {
window.MessageCache.toMessageAttributes(message);
receiptsByMessageId.set(message.id, [receipt]); receiptsByMessageId.set(message.id, [receipt]);
} else { } else {
existing.push(receipt); existing.push(receipt);
@ -151,9 +153,10 @@ const processReceiptBatcher = createWaitBatcher({
); );
if (targetMessages.length) { if (targetMessages.length) {
targetMessages.forEach(msg => targetMessages.forEach(msg => {
addReceiptAndTargetMessage(msg, receipt) const model = window.MessageCache.register(new MessageModel(msg));
); addReceiptAndTargetMessage(model, receipt);
});
} else { } else {
// Nope, no target message was found // Nope, no target message was found
const { receiptSync } = receipt; const { receiptSync } = receipt;
@ -188,53 +191,43 @@ async function processReceiptsForMessage(
} }
// Get message from cache or DB // Get message from cache or DB
const message = await window.MessageCache.resolveAttributes( const message = await getMessageById(messageId);
'processReceiptsForMessage', if (!message) {
messageId throw new Error(
); `processReceiptsForMessage: Failed to find message ${messageId}`
);
}
// Note: it is important to have no `await` in between `resolveAttributes` and const { validReceipts } = await updateMessageWithReceipts(message, receipts);
// `setAttributes` since it might overwrite other updates otherwise.
const { updatedMessage, validReceipts, droppedReceipts } =
updateMessageWithReceipts(message, receipts);
// Save it to cache & to DB, and remove dropped receipts const ourAci = window.textsecure.storage.user.getCheckedAci();
await Promise.all([ await DataWriter.saveMessage(message.attributes, { ourAci, postSaveUpdates });
window.MessageCache.setAttributes({
messageId,
messageAttributes: updatedMessage,
skipSaveToDatabase: false,
}),
Promise.all(droppedReceipts.map(remove)),
]);
// Confirm/remove receipts, and delete sent protos // Confirm/remove receipts, and delete sent protos
for (const receipt of validReceipts) { for (const receipt of validReceipts) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await remove(receipt); await remove(receipt);
drop(addToDeleteSentProtoBatcher(receipt, updatedMessage)); drop(addToDeleteSentProtoBatcher(receipt, message.attributes));
} }
// notify frontend listeners // notify frontend listeners
const conversation = window.ConversationController.get( const conversation = window.ConversationController.get(
message.conversationId message.get('conversationId')
); );
conversation?.debouncedUpdateLastMessage?.(); conversation?.debouncedUpdateLastMessage?.();
} }
function updateMessageWithReceipts( async function updateMessageWithReceipts(
message: MessageAttributesType, message: MessageModel,
receipts: Array<MessageReceiptAttributesType> receipts: Array<MessageReceiptAttributesType>
): { ): Promise<{
updatedMessage: MessageAttributesType;
validReceipts: Array<MessageReceiptAttributesType>; validReceipts: Array<MessageReceiptAttributesType>;
droppedReceipts: Array<MessageReceiptAttributesType>; }> {
} { const logId = `updateMessageWithReceipts(timestamp=${message.get('timestamp')})`;
const logId = `updateMessageWithReceipts(timestamp=${message.timestamp})`;
const droppedReceipts: Array<MessageReceiptAttributesType> = []; const droppedReceipts: Array<MessageReceiptAttributesType> = [];
const receiptsToProcess = receipts.filter(receipt => { const receiptsToProcess = receipts.filter(receipt => {
if (shouldDropReceipt(receipt, message)) { if (shouldDropReceipt(receipt, message.attributes)) {
const { receiptSync } = receipt; const { receiptSync } = receipt;
log.info( log.info(
`${logId}: Dropping a receipt ${receiptSync.type} for sentAt=${receiptSync.messageSentAt}` `${logId}: Dropping a receipt ${receiptSync.type} for sentAt=${receiptSync.messageSentAt}`
@ -257,14 +250,16 @@ function updateMessageWithReceipts(
); );
// Generate the updated message synchronously // Generate the updated message synchronously
let updatedMessage: MessageAttributesType = { ...message }; let { attributes } = message;
for (const receipt of receiptsToProcess) { for (const receipt of receiptsToProcess) {
updatedMessage = { attributes = {
...updatedMessage, ...attributes,
...updateMessageSendStateWithReceipt(updatedMessage, receipt), ...updateMessageSendStateWithReceipt(attributes, receipt),
}; };
} }
return { updatedMessage, validReceipts: receiptsToProcess, droppedReceipts }; message.set(attributes);
return { validReceipts: receiptsToProcess };
} }
const deleteSentProtoBatcher = createWaitBatcher({ const deleteSentProtoBatcher = createWaitBatcher({
@ -310,7 +305,7 @@ function getTargetMessage({
sourceConversationId: string; sourceConversationId: string;
messagesMatchingTimestamp: ReadonlyArray<MessageAttributesType>; messagesMatchingTimestamp: ReadonlyArray<MessageAttributesType>;
targetTimestamp: number; targetTimestamp: number;
}): MessageAttributesType | null { }): MessageModel | null {
if (messagesMatchingTimestamp.length === 0) { if (messagesMatchingTimestamp.length === 0) {
return null; return null;
} }
@ -366,7 +361,7 @@ function getTargetMessage({
} }
const message = matchingMessages[0]; const message = matchingMessages[0];
return window.MessageCache.toMessageAttributes(message); return window.MessageCache.register(new MessageModel(message));
} }
const wasDeliveredWithSealedSender = ( const wasDeliveredWithSealedSender = (
conversationId: string, conversationId: string,

View file

@ -1,23 +1,45 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { maxBy } from 'lodash';
import type { AciString } from '../types/ServiceId'; import type { AciString } from '../types/ServiceId';
import type { import type {
MessageAttributesType, MessageAttributesType,
MessageReactionType,
ReadonlyMessageAttributesType, ReadonlyMessageAttributesType,
} from '../model-types.d'; } from '../model-types.d';
import type { MessageModel } from '../models/messages'; import { MessageModel } from '../models/messages';
import type { ReactionSource } from '../reactions/ReactionSource'; import { ReactionSource } from '../reactions/ReactionSource';
import { DataReader } from '../sql/Client'; import { DataReader, DataWriter } from '../sql/Client';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { getAuthor } from '../messages/helpers'; import { getAuthor, isIncoming, isOutgoing } from '../messages/helpers';
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet'; import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
import { isMe } from '../util/whatTypeOfConversation'; import { isDirectConversation, isMe } from '../util/whatTypeOfConversation';
import { isStory } from '../state/selectors/message'; import {
getMessagePropStatus,
hasErrors,
isStory,
} from '../state/selectors/message';
import { getPropForTimestamp } from '../util/editHelpers'; import { getPropForTimestamp } from '../util/editHelpers';
import { isSent } from '../messages/MessageSendState'; import { isSent } from '../messages/MessageSendState';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
import { repeat, zipObject } from '../util/iterables';
import { getMessageIdForLogging } from '../util/idForLogging';
import { hydrateStoryContext } from '../util/hydrateStoryContext';
import { shouldReplyNotifyUser } from '../util/shouldReplyNotifyUser';
import { drop } from '../util/drop';
import * as reactionUtil from '../reactions/util';
import { isNewReactionReplacingPrevious } from '../reactions/util';
import { notificationService } from '../services/notifications';
import { ReactionReadStatus } from '../types/Reactions';
import type { ConversationQueueJobData } from '../jobs/conversationJobQueue';
import {
conversationJobQueue,
conversationQueueJobEnum,
} from '../jobs/conversationJobQueue';
import { postSaveUpdates } from '../util/cleanup';
export type ReactionAttributesType = { export type ReactionAttributesType = {
emoji: string; emoji: string;
@ -36,24 +58,26 @@ export type ReactionAttributesType = {
receivedAtDate: number; receivedAtDate: number;
}; };
const reactions = new Map<string, ReactionAttributesType>(); const reactionCache = new Map<string, ReactionAttributesType>();
function remove(reaction: ReactionAttributesType): void { function remove(reaction: ReactionAttributesType): void {
reactions.delete(reaction.envelopeId); reactionCache.delete(reaction.envelopeId);
reaction.removeFromMessageReceiverCache(); reaction.removeFromMessageReceiverCache();
} }
export function findReactionsForMessage( export function findReactionsForMessage(
message: ReadonlyMessageAttributesType message: ReadonlyMessageAttributesType
): Array<ReactionAttributesType> { ): Array<ReactionAttributesType> {
const matchingReactions = Array.from(reactions.values()).filter(reaction => { const matchingReactions = Array.from(reactionCache.values()).filter(
return isMessageAMatchForReaction({ reaction => {
message, return isMessageAMatchForReaction({
targetTimestamp: reaction.targetTimestamp, message,
targetAuthorAci: reaction.targetAuthorAci, targetTimestamp: reaction.targetTimestamp,
reactionSenderConversationId: reaction.fromId, targetAuthorAci: reaction.targetAuthorAci,
}); reactionSenderConversationId: reaction.fromId,
}); });
}
);
matchingReactions.forEach(reaction => remove(reaction)); matchingReactions.forEach(reaction => remove(reaction));
return matchingReactions; return matchingReactions;
@ -173,7 +197,7 @@ function isMessageAMatchForReaction({
export async function onReaction( export async function onReaction(
reaction: ReactionAttributesType reaction: ReactionAttributesType
): Promise<void> { ): Promise<void> {
reactions.set(reaction.envelopeId, reaction); reactionCache.set(reaction.envelopeId, reaction);
const logId = `Reactions.onReaction(timestamp=${reaction.timestamp};target=${reaction.targetTimestamp})`; const logId = `Reactions.onReaction(timestamp=${reaction.timestamp};target=${reaction.targetTimestamp})`;
@ -231,23 +255,21 @@ export async function onReaction(
return; return;
} }
const targetMessageModel = window.MessageCache.__DEPRECATED$register( const targetMessageModel = window.MessageCache.register(
targetMessage.id, new MessageModel(targetMessage)
targetMessage,
'Reactions.onReaction'
); );
// Use the generated message in ts/background.ts to create a message // Use the generated message in ts/background.ts to create a message
// if the reaction is targeted at a story. // if the reaction is targeted at a story.
if (!isStory(targetMessage)) { if (!isStory(targetMessage)) {
await targetMessageModel.handleReaction(reaction); await handleReaction(targetMessageModel, reaction);
} else { } else {
const generatedMessage = reaction.generatedMessageForStoryReaction; const generatedMessage = reaction.generatedMessageForStoryReaction;
strictAssert( strictAssert(
generatedMessage, generatedMessage,
'Generated message must exist for story reaction' 'Generated message must exist for story reaction'
); );
await generatedMessage.handleReaction(reaction, { await handleReaction(generatedMessage, reaction, {
storyMessage: targetMessage, storyMessage: targetMessage,
}); });
} }
@ -260,3 +282,324 @@ export async function onReaction(
log.error(`${logId} error:`, Errors.toLogFormat(error)); log.error(`${logId} error:`, Errors.toLogFormat(error));
} }
} }
export async function handleReaction(
message: MessageModel,
reaction: ReactionAttributesType,
{
storyMessage,
shouldPersist = true,
}: {
storyMessage?: MessageAttributesType;
shouldPersist?: boolean;
} = {}
): Promise<void> {
const { attributes } = message;
if (message.get('deletedForEveryone')) {
return;
}
// We allow you to react to messages with outgoing errors only if it has sent
// successfully to at least one person.
if (
hasErrors(attributes) &&
(isIncoming(attributes) ||
getMessagePropStatus(
attributes,
window.ConversationController.getOurConversationIdOrThrow()
) !== 'partial-sent')
) {
return;
}
const conversation = window.ConversationController.get(
message.attributes.conversationId
);
if (!conversation) {
return;
}
const isFromThisDevice = reaction.source === ReactionSource.FromThisDevice;
const isFromSync = reaction.source === ReactionSource.FromSync;
const isFromSomeoneElse = reaction.source === ReactionSource.FromSomeoneElse;
strictAssert(
isFromThisDevice || isFromSync || isFromSomeoneElse,
'Reaction can only be from this device, from sync, or from someone else'
);
const newReaction: MessageReactionType = {
emoji: reaction.remove ? undefined : reaction.emoji,
fromId: reaction.fromId,
targetTimestamp: reaction.targetTimestamp,
timestamp: reaction.timestamp,
isSentByConversationId: isFromThisDevice
? zipObject(conversation.getMemberConversationIds(), repeat(false))
: undefined,
};
// Reactions to stories are saved as separate messages, and so require a totally
// different codepath.
if (storyMessage) {
if (isFromThisDevice) {
log.info(
'handleReaction: sending story reaction to ' +
`${getMessageIdForLogging(storyMessage)} from this device`
);
} else {
if (isFromSomeoneElse) {
log.info(
'handleReaction: receiving story reaction to ' +
`${getMessageIdForLogging(storyMessage)} from someone else`
);
} else if (isFromSync) {
log.info(
'handleReaction: receiving story reaction to ' +
`${getMessageIdForLogging(storyMessage)} from another device`
);
}
const generatedMessage = reaction.generatedMessageForStoryReaction;
strictAssert(
generatedMessage,
'Story reactions must provide storyReactionMessage'
);
const targetConversation = window.ConversationController.get(
generatedMessage.get('conversationId')
);
strictAssert(
targetConversation,
'handleReaction: targetConversation not found'
);
window.MessageCache.register(generatedMessage);
generatedMessage.set({
expireTimer: isDirectConversation(targetConversation.attributes)
? targetConversation.get('expireTimer')
: undefined,
storyId: storyMessage.id,
storyReaction: {
emoji: reaction.emoji,
targetAuthorAci: reaction.targetAuthorAci,
targetTimestamp: reaction.targetTimestamp,
},
});
await hydrateStoryContext(generatedMessage.id, storyMessage, {
shouldSave: false,
});
// Note: generatedMessage comes with an id, so we have to force this save
await DataWriter.saveMessage(generatedMessage.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
forceSave: true,
postSaveUpdates,
});
log.info('Reactions.onReaction adding reaction to story', {
reactionMessageId: getMessageIdForLogging(generatedMessage.attributes),
storyId: getMessageIdForLogging(storyMessage),
targetTimestamp: reaction.targetTimestamp,
timestamp: reaction.timestamp,
});
window.MessageCache.register(generatedMessage);
if (isDirectConversation(targetConversation.attributes)) {
await targetConversation.addSingleMessage(generatedMessage.attributes);
if (!targetConversation.get('active_at')) {
targetConversation.set({
active_at: generatedMessage.attributes.timestamp,
});
await DataWriter.updateConversation(targetConversation.attributes);
}
}
if (isFromSomeoneElse) {
log.info(
'handleReaction: notifying for story reaction to ' +
`${getMessageIdForLogging(storyMessage)} from someone else`
);
if (
await shouldReplyNotifyUser(
generatedMessage.attributes,
targetConversation
)
) {
drop(targetConversation.notify(generatedMessage.attributes));
}
}
}
} else {
// Reactions to all messages other than stories will update the target message
const previousLength = (message.get('reactions') || []).length;
if (isFromThisDevice) {
log.info(
`handleReaction: sending reaction to ${getMessageIdForLogging(message.attributes)} ` +
'from this device'
);
const reactions = reactionUtil.addOutgoingReaction(
message.get('reactions') || [],
newReaction
);
message.set({ reactions });
} else {
const oldReactions = message.get('reactions') || [];
let reactions: Array<MessageReactionType>;
const oldReaction = oldReactions.find(re =>
isNewReactionReplacingPrevious(re, newReaction)
);
if (oldReaction) {
notificationService.removeBy({
...oldReaction,
messageId: message.id,
});
}
if (reaction.remove) {
log.info(
'handleReaction: removing reaction for message',
getMessageIdForLogging(message.attributes)
);
if (isFromSync) {
reactions = oldReactions.filter(
re =>
!isNewReactionReplacingPrevious(re, newReaction) ||
re.timestamp > reaction.timestamp
);
} else {
reactions = oldReactions.filter(
re => !isNewReactionReplacingPrevious(re, newReaction)
);
}
message.set({ reactions });
} else {
log.info(
'handleReaction: adding reaction for message',
getMessageIdForLogging(message.attributes)
);
let reactionToAdd: MessageReactionType;
if (isFromSync) {
const ourReactions = [
newReaction,
...oldReactions.filter(re => re.fromId === reaction.fromId),
];
reactionToAdd = maxBy(ourReactions, 'timestamp') || newReaction;
} else {
reactionToAdd = newReaction;
}
reactions = oldReactions.filter(
re => !isNewReactionReplacingPrevious(re, reaction)
);
reactions.push(reactionToAdd);
message.set({ reactions });
if (isOutgoing(message.attributes) && isFromSomeoneElse) {
void conversation.notify(message.attributes, reaction);
}
}
}
if (reaction.remove) {
await DataWriter.removeReactionFromConversation({
emoji: reaction.emoji,
fromId: reaction.fromId,
targetAuthorServiceId: reaction.targetAuthorAci,
targetTimestamp: reaction.targetTimestamp,
});
} else {
await DataWriter.addReaction(
{
conversationId: message.get('conversationId'),
emoji: reaction.emoji,
fromId: reaction.fromId,
messageId: message.id,
messageReceivedAt: message.get('received_at'),
targetAuthorAci: reaction.targetAuthorAci,
targetTimestamp: reaction.targetTimestamp,
timestamp: reaction.timestamp,
},
{
readStatus: isFromThisDevice
? ReactionReadStatus.Read
: ReactionReadStatus.Unread,
}
);
}
const currentLength = (message.get('reactions') || []).length;
log.info(
'handleReaction:',
`Done processing reaction for message ${getMessageIdForLogging(message.attributes)}.`,
`Went from ${previousLength} to ${currentLength} reactions.`
);
}
if (isFromThisDevice) {
let jobData: ConversationQueueJobData;
if (storyMessage) {
strictAssert(
newReaction.emoji !== undefined,
'New story reaction must have an emoji'
);
const generatedMessage = reaction.generatedMessageForStoryReaction;
strictAssert(
generatedMessage,
'Story reactions must provide storyReactionmessage'
);
await hydrateStoryContext(generatedMessage.id, message.attributes, {
shouldSave: false,
});
await DataWriter.saveMessage(generatedMessage.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
forceSave: true,
postSaveUpdates,
});
window.MessageCache.register(generatedMessage);
void conversation.addSingleMessage(generatedMessage.attributes);
jobData = {
type: conversationQueueJobEnum.enum.NormalMessage,
conversationId: conversation.id,
messageId: generatedMessage.id,
revision: conversation.get('revision'),
};
} else {
jobData = {
type: conversationQueueJobEnum.enum.Reaction,
conversationId: conversation.id,
messageId: message.id,
revision: conversation.get('revision'),
};
}
if (shouldPersist) {
await conversationJobQueue.add(jobData, async jobToInsert => {
log.info(
`enqueueReactionForSend: saving message ${getMessageIdForLogging(message.attributes)} and job ${
jobToInsert.id
}`
);
await DataWriter.saveMessage(message.attributes, {
jobToInsert,
ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
});
});
} else {
await conversationJobQueue.add(jobData);
}
} else if (shouldPersist && !isStory(message.attributes)) {
await DataWriter.saveMessage(message.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
});
window.reduxActions.conversations.markOpenConversationRead(conversation.id);
}
}

View file

@ -17,6 +17,8 @@ import { queueUpdateMessage } from '../util/messageBatcher';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
import { isAciString } from '../util/isAciString'; import { isAciString } from '../util/isAciString';
import { DataReader, DataWriter } from '../sql/Client'; import { DataReader, DataWriter } from '../sql/Client';
import { markRead } from '../services/MessageUpdater';
import { MessageModel } from '../models/messages';
const { removeSyncTaskById } = DataWriter; const { removeSyncTaskById } = DataWriter;
@ -146,11 +148,7 @@ export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
notificationService.removeBy({ messageId: found.id }); notificationService.removeBy({ messageId: found.id });
const message = window.MessageCache.__DEPRECATED$register( const message = window.MessageCache.register(new MessageModel(found));
found.id,
found,
'ReadSyncs.onSync'
);
const readAt = Math.min(readSync.readAt, Date.now()); const readAt = Math.min(readSync.readAt, Date.now());
const newestSentAt = readSync.timestamp; const newestSentAt = readSync.timestamp;
@ -158,11 +156,12 @@ export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
// timer to the time specified by the read sync if it's earlier than // timer to the time specified by the read sync if it's earlier than
// the previous read time. // the previous read time.
if (isMessageUnread(message.attributes)) { if (isMessageUnread(message.attributes)) {
// TODO DESKTOP-1509: use MessageUpdater.markRead once this is TS message.set(markRead(message.attributes, readAt, { skipSave: true }));
message.markRead(readAt, { skipSave: true });
const updateConversation = async () => { const updateConversation = async () => {
const conversation = message.getConversation(); const conversation = window.ConversationController.get(
message.get('conversationId')
);
strictAssert(conversation, `${logId}: conversation not found`); strictAssert(conversation, `${logId}: conversation not found`);
// onReadMessage may result in messages older than this one being // onReadMessage may result in messages older than this one being
// marked read. We want those messages to have the same expire timer // marked read. We want those messages to have the same expire timer
@ -174,7 +173,9 @@ export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
// only available during initialization // only available during initialization
if (StartupQueue.isAvailable()) { if (StartupQueue.isAvailable()) {
const conversation = message.getConversation(); const conversation = window.ConversationController.get(
message.get('conversationId')
);
strictAssert( strictAssert(
conversation, conversation,
`${logId}: conversation not found (StartupQueue)` `${logId}: conversation not found (StartupQueue)`

View file

@ -7,6 +7,8 @@ import { DataReader } from '../sql/Client';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { getMessageIdForLogging } from '../util/idForLogging'; import { getMessageIdForLogging } from '../util/idForLogging';
import { markViewOnceMessageViewed } from '../services/MessageUpdater';
import { MessageModel } from '../models/messages';
export type ViewOnceOpenSyncAttributesType = { export type ViewOnceOpenSyncAttributesType = {
removeFromMessageReceiverCache: () => unknown; removeFromMessageReceiverCache: () => unknown;
@ -93,12 +95,8 @@ export async function onSync(
return; return;
} }
const message = window.MessageCache.__DEPRECATED$register( const message = window.MessageCache.register(new MessageModel(found));
found.id, await markViewOnceMessageViewed(message, { fromSync: true });
found,
'ViewOnceOpenSyncs.onSync'
);
await message.markViewOnceMessageViewed({ fromSync: true });
viewOnceSyncs.delete(sync.timestamp); viewOnceSyncs.delete(sync.timestamp);
sync.removeFromMessageReceiverCache(); sync.removeFromMessageReceiverCache();

View file

@ -19,6 +19,7 @@ import { queueUpdateMessage } from '../util/messageBatcher';
import { AttachmentDownloadUrgency } from '../jobs/AttachmentDownloadManager'; import { AttachmentDownloadUrgency } from '../jobs/AttachmentDownloadManager';
import { isAciString } from '../util/isAciString'; import { isAciString } from '../util/isAciString';
import { DataReader, DataWriter } from '../sql/Client'; import { DataReader, DataWriter } from '../sql/Client';
import { MessageModel } from '../models/messages';
export const viewSyncTaskSchema = z.object({ export const viewSyncTaskSchema = z.object({
type: z.literal('ViewSync').readonly(), type: z.literal('ViewSync').readonly(),
@ -114,11 +115,7 @@ export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
notificationService.removeBy({ messageId: found.id }); notificationService.removeBy({ messageId: found.id });
const message = window.MessageCache.__DEPRECATED$register( const message = window.MessageCache.register(new MessageModel(found));
found.id,
found,
'ViewSyncs.onSync'
);
let didChangeMessage = false; let didChangeMessage = false;
if (message.get('readStatus') !== ReadStatus.Viewed) { if (message.get('readStatus') !== ReadStatus.Viewed) {

View file

@ -5,10 +5,6 @@ import { omit } from 'lodash';
import * as log from '../logging/log'; import * as log from '../logging/log';
import type { QuotedMessageType } from '../model-types'; import type { QuotedMessageType } from '../model-types';
import type {
MessageAttributesType,
ReadonlyMessageAttributesType,
} from '../model-types.d';
import { SignalService } from '../protobuf'; import { SignalService } from '../protobuf';
import { isGiftBadge, isTapToView } from '../state/selectors/message'; import { isGiftBadge, isTapToView } from '../state/selectors/message';
import type { ProcessedQuote } from '../textsecure/Types'; import type { ProcessedQuote } from '../textsecure/Types';
@ -18,16 +14,15 @@ import { getQuoteBodyText } from '../util/getQuoteBodyText';
import { isQuoteAMatch, messageHasPaymentEvent } from './helpers'; import { isQuoteAMatch, messageHasPaymentEvent } from './helpers';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
import { isDownloadable } from '../types/Attachment'; import { isDownloadable } from '../types/Attachment';
import type { MessageModel } from '../models/messages';
export type MinimalMessageCache = Readonly<{ export type MinimalMessageCache = Readonly<{
findBySentAt( findBySentAt(
sentAt: number, sentAt: number,
predicate: (attributes: ReadonlyMessageAttributesType) => boolean predicate: (attributes: MessageModel) => boolean
): Promise<MessageAttributesType | undefined>; ): Promise<MessageModel | undefined>;
upgradeSchema( upgradeSchema(message: MessageModel, minSchemaVersion: number): Promise<void>;
attributes: MessageAttributesType, register(message: MessageModel): MessageModel;
minSchemaVersion: number
): Promise<MessageAttributesType>;
}>; }>;
export type CopyQuoteOptionsType = Readonly<{ export type CopyQuoteOptionsType = Readonly<{
@ -57,8 +52,11 @@ export const copyFromQuotedMessage = async (
isViewOnce: false, isViewOnce: false,
}; };
const queryMessage = await messageCache.findBySentAt(id, attributes => const queryMessage = await messageCache.findBySentAt(
isQuoteAMatch(attributes, conversationId, result) id,
(message: MessageModel) => {
return isQuoteAMatch(message.attributes, conversationId, result);
}
); );
if (queryMessage == null) { if (queryMessage == null) {
@ -74,21 +72,19 @@ export const copyFromQuotedMessage = async (
}; };
export const copyQuoteContentFromOriginal = async ( export const copyQuoteContentFromOriginal = async (
providedOriginalMessage: MessageAttributesType, message: MessageModel,
quote: QuotedMessageType, quote: QuotedMessageType,
{ messageCache = window.MessageCache }: CopyQuoteOptionsType = {} { messageCache = window.MessageCache }: CopyQuoteOptionsType = {}
): Promise<void> => { ): Promise<void> => {
let originalMessage = providedOriginalMessage;
const { attachments } = quote; const { attachments } = quote;
const firstAttachment = attachments ? attachments[0] : undefined; const firstAttachment = attachments ? attachments[0] : undefined;
if (messageHasPaymentEvent(originalMessage)) { if (messageHasPaymentEvent(message.attributes)) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
quote.payment = originalMessage.payment; quote.payment = message.get('payment');
} }
if (isTapToView(originalMessage)) { if (isTapToView(message.attributes)) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
quote.text = undefined; quote.text = undefined;
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
@ -103,7 +99,7 @@ export const copyQuoteContentFromOriginal = async (
return; return;
} }
const isMessageAGiftBadge = isGiftBadge(originalMessage); const isMessageAGiftBadge = isGiftBadge(message.attributes);
if (isMessageAGiftBadge !== quote.isGiftBadge) { if (isMessageAGiftBadge !== quote.isGiftBadge) {
log.warn( log.warn(
`copyQuoteContentFromOriginal: Quote.isGiftBadge: ${quote.isGiftBadge}, isGiftBadge(message): ${isMessageAGiftBadge}` `copyQuoteContentFromOriginal: Quote.isGiftBadge: ${quote.isGiftBadge}, isGiftBadge(message): ${isMessageAGiftBadge}`
@ -124,18 +120,18 @@ export const copyQuoteContentFromOriginal = async (
quote.isViewOnce = false; quote.isViewOnce = false;
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
quote.text = getQuoteBodyText(originalMessage, quote.id); quote.text = getQuoteBodyText(message.attributes, quote.id);
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
quote.bodyRanges = originalMessage.bodyRanges; quote.bodyRanges = message.attributes.bodyRanges;
if (!firstAttachment || !firstAttachment.contentType) { if (!firstAttachment || !firstAttachment.contentType) {
return; return;
} }
try { try {
originalMessage = await messageCache.upgradeSchema( await messageCache.upgradeSchema(
originalMessage, message,
window.Signal.Types.Message.VERSION_NEEDED_FOR_DISPLAY window.Signal.Types.Message.VERSION_NEEDED_FOR_DISPLAY
); );
} catch (error) { } catch (error) {
@ -150,7 +146,7 @@ export const copyQuoteContentFromOriginal = async (
attachments: queryAttachments = [], attachments: queryAttachments = [],
preview: queryPreview = [], preview: queryPreview = [],
sticker, sticker,
} = originalMessage; } = message.attributes;
if (queryAttachments.length > 0) { if (queryAttachments.length > 0) {
const queryFirst = queryAttachments[0]; const queryFirst = queryAttachments[0];

View file

@ -2,20 +2,15 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as log from '../logging/log'; import * as log from '../logging/log';
import { DataReader } from '../sql/Client';
import type { MessageAttributesType } from '../model-types.d';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
import type { MessageModel } from '../models/messages'; import { DataReader } from '../sql/Client';
import { MessageModel } from '../models/messages';
import type { MessageAttributesType } from '../model-types.d';
export async function __DEPRECATED$getMessageById( export async function getMessageById(
messageId: string, messageId: string
location: string
): Promise<MessageModel | undefined> { ): Promise<MessageModel | undefined> {
const innerLocation = `__DEPRECATED$getMessageById/${location}`; const message = window.MessageCache.getById(messageId);
const message = window.MessageCache.__DEPRECATED$getById(
messageId,
innerLocation
);
if (message) { if (message) {
return message; return message;
} }
@ -34,9 +29,5 @@ export async function __DEPRECATED$getMessageById(
return undefined; return undefined;
} }
return window.MessageCache.__DEPRECATED$register( return window.MessageCache.register(new MessageModel(found));
found.id,
found,
innerLocation
);
} }

View file

@ -3,23 +3,18 @@
import * as log from '../logging/log'; import * as log from '../logging/log';
import { DataReader } from '../sql/Client'; import { DataReader } from '../sql/Client';
import type { MessageModel } from '../models/messages'; import { MessageModel } from '../models/messages';
import type { MessageAttributesType } from '../model-types.d'; import type { MessageAttributesType } from '../model-types.d';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
export async function getMessagesById( export async function getMessagesById(
messageIds: Iterable<string>, messageIds: Iterable<string>
location: string
): Promise<Array<MessageModel>> { ): Promise<Array<MessageModel>> {
const innerLocation = `getMessagesById/${location}`;
const messagesFromMemory: Array<MessageModel> = []; const messagesFromMemory: Array<MessageModel> = [];
const messageIdsToLookUpInDatabase: Array<string> = []; const messageIdsToLookUpInDatabase: Array<string> = [];
for (const messageId of messageIds) { for (const messageId of messageIds) {
const message = window.MessageCache.__DEPRECATED$getById( const message = window.MessageCache.getById(messageId);
messageId,
innerLocation
);
if (message) { if (message) {
messagesFromMemory.push(message); messagesFromMemory.push(message);
} else { } else {
@ -41,15 +36,8 @@ export async function getMessagesById(
return []; return [];
} }
const messagesFromDatabase = rawMessagesFromDatabase.map(rawMessage => { const messagesFromDatabase = rawMessagesFromDatabase.map(message => {
// We use `window.Whisper.Message` instead of `MessageModel` here to avoid a circular return window.MessageCache.register(new MessageModel(message));
// import.
const message = new window.Whisper.Message(rawMessage);
return window.MessageCache.__DEPRECATED$register(
message.id,
message,
innerLocation
);
}); });
return [...messagesFromMemory, ...messagesFromDatabase]; return [...messagesFromMemory, ...messagesFromDatabase];

View file

@ -0,0 +1,817 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { isNumber, partition } from 'lodash';
import * as log from '../logging/log';
import * as Errors from '../types/errors';
import * as MIME from '../types/MIME';
import * as LinkPreview from '../types/LinkPreview';
import { getAuthor, isStory, messageHasPaymentEvent } from './helpers';
import { getMessageIdForLogging } from '../util/idForLogging';
import { getSenderIdentifier } from '../util/getSenderIdentifier';
import { isNormalNumber } from '../util/isNormalNumber';
import { getOwn } from '../util/getOwn';
import {
SendActionType,
sendStateReducer,
SendStatus,
} from './MessageSendState';
import { DataReader, DataWriter } from '../sql/Client';
import { eraseMessageContents, postSaveUpdates } from '../util/cleanup';
import {
isDirectConversation,
isGroup,
isGroupV1,
} from '../util/whatTypeOfConversation';
import { generateMessageId } from '../util/generateMessageId';
import {
hasErrors,
isEndSession,
isExpirationTimerUpdate,
isGroupUpdate,
isTapToView,
isUnsupportedMessage,
} from '../state/selectors/message';
import { drop } from '../util/drop';
import { strictAssert } from '../util/assert';
import { isAciString } from '../util/isAciString';
import { copyFromQuotedMessage } from './copyQuote';
import { findStoryMessages } from '../util/findStoryMessage';
import { getRoomIdFromCallLink } from '../util/callLinksRingrtc';
import { isNotNil } from '../util/isNotNil';
import { normalizeServiceId } from '../types/ServiceId';
import { BodyRange } from '../types/BodyRange';
import { hydrateStoryContext } from '../util/hydrateStoryContext';
import { isMessageEmpty } from '../util/isMessageEmpty';
import { isValidTapToView } from '../util/isValidTapToView';
import { getNotificationTextForMessage } from '../util/getNotificationTextForMessage';
import { getMessageAuthorText } from '../util/getMessageAuthorText';
import { GiftBadgeStates } from '../components/conversation/Message';
import { getUserLanguages } from '../util/userLanguages';
import { parseBoostBadgeListFromServer } from '../badges/parseBadgesFromServer';
import { SignalService as Proto } from '../protobuf';
import {
modifyTargetMessage,
ModifyTargetMessageResult,
} from '../util/modifyTargetMessage';
import { saveAndNotify } from './saveAndNotify';
import { MessageModel } from '../models/messages';
import type { SentEventData } from '../textsecure/messageReceiverEvents';
import type {
ProcessedDataMessage,
ProcessedUnidentifiedDeliveryStatus,
} from '../textsecure/Types';
import type { ServiceIdString } from '../types/ServiceId';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
const CURRENT_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.CURRENT;
const INITIAL_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.INITIAL;
export async function handleDataMessage(
message: MessageModel,
initialMessage: ProcessedDataMessage,
confirm: () => void,
options: { data?: SentEventData } = {}
): Promise<void> {
const { data } = options;
const { upgradeMessageSchema } = window.Signal.Migrations;
// This function is called from the background script in a few scenarios:
// 1. on an incoming message
// 2. on a sent message sync'd from another device
// 3. in rare cases, an incoming message can be retried, though it will
// still go through one of the previous two codepaths
const source = message.get('source');
const sourceServiceId = message.get('sourceServiceId');
const type = message.get('type');
const conversationId = message.get('conversationId');
const fromContact = getAuthor(message.attributes);
if (fromContact) {
fromContact.setRegistered();
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const conversation = window.ConversationController.get(conversationId)!;
const idLog = `handleDataMessage/${conversation.idForLogging()} ${getMessageIdForLogging(message.attributes)}`;
await conversation.queueJob(idLog, async () => {
log.info(`${idLog}: starting processing in queue`);
// First, check for duplicates. If we find one, stop processing here.
const senderIdentifier = getSenderIdentifier(message.attributes);
const inMemoryMessage = window.MessageCache.findBySender(senderIdentifier);
if (inMemoryMessage) {
log.info(`${idLog}: cache hit`, senderIdentifier);
} else {
log.info(`${idLog}: duplicate check db lookup needed`, senderIdentifier);
}
let existingMessage = inMemoryMessage;
if (!existingMessage) {
const fromDb = await DataReader.getMessageBySender(message.attributes);
existingMessage = fromDb
? window.MessageCache.register(new MessageModel(fromDb))
: undefined;
}
const isUpdate = Boolean(data && data.isRecipientUpdate);
const isDuplicateMessage =
existingMessage &&
(type === 'incoming' ||
(type === 'story' &&
existingMessage.get('storyDistributionListId') ===
message.attributes.storyDistributionListId));
if (isDuplicateMessage) {
log.warn(
`${idLog}: Received duplicate message`,
getMessageIdForLogging(message.attributes)
);
confirm();
return;
}
if (type === 'outgoing') {
if (isUpdate && existingMessage) {
log.info(
`${idLog}: Updating message ${getMessageIdForLogging(message.attributes)} with received transcript`
);
const toUpdate = window.MessageCache.register(existingMessage);
const unidentifiedDeliveriesSet = new Set<string>(
toUpdate.get('unidentifiedDeliveries') ?? []
);
const sendStateByConversationId = {
...(toUpdate.get('sendStateByConversationId') || {}),
};
const unidentifiedStatus: Array<ProcessedUnidentifiedDeliveryStatus> =
data && Array.isArray(data.unidentifiedStatus)
? data.unidentifiedStatus
: [];
unidentifiedStatus.forEach(
({ destinationServiceId, destination, unidentified }) => {
const identifier = destinationServiceId || destination;
if (!identifier) {
return;
}
const destinationConversation =
window.ConversationController.lookupOrCreate({
serviceId: destinationServiceId,
e164: destination || undefined,
reason: `handleDataMessage(${initialMessage.timestamp})`,
});
if (!destinationConversation) {
return;
}
const updatedAt: number =
data && isNormalNumber(data.timestamp)
? data.timestamp
: Date.now();
const previousSendState = getOwn(
sendStateByConversationId,
destinationConversation.id
);
sendStateByConversationId[destinationConversation.id] =
previousSendState
? sendStateReducer(previousSendState, {
type: SendActionType.Sent,
updatedAt,
})
: {
status: SendStatus.Sent,
updatedAt,
};
if (unidentified) {
unidentifiedDeliveriesSet.add(identifier);
}
}
);
toUpdate.set({
sendStateByConversationId,
unidentifiedDeliveries: [...unidentifiedDeliveriesSet],
});
await DataWriter.saveMessage(toUpdate.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
});
confirm();
return;
}
if (isUpdate) {
log.warn(
`${idLog}: Received update transcript, but no existing entry for message ${getMessageIdForLogging(message.attributes)}. Dropping.`
);
confirm();
return;
}
if (existingMessage) {
// TODO: (DESKTOP-7301): improve this check in case previous message is not yet
// registered in memory
log.warn(
`${idLog}: Received duplicate transcript for message ${getMessageIdForLogging(message.attributes)}, but it was not an update transcript. Dropping.`
);
confirm();
return;
}
}
// GroupV2
if (initialMessage.groupV2) {
if (isGroupV1(conversation.attributes)) {
// If we received a GroupV2 message in a GroupV1 group, we migrate!
const { revision, groupChange } = initialMessage.groupV2;
await window.Signal.Groups.respondToGroupV2Migration({
conversation,
groupChange: groupChange
? {
base64: groupChange,
isTrusted: false,
}
: undefined,
newRevision: revision,
receivedAt: message.get('received_at'),
sentAt: message.get('sent_at'),
});
} else if (
initialMessage.groupV2.masterKey &&
initialMessage.groupV2.secretParams &&
initialMessage.groupV2.publicParams
) {
// Repair core GroupV2 data if needed
await conversation.maybeRepairGroupV2({
masterKey: initialMessage.groupV2.masterKey,
secretParams: initialMessage.groupV2.secretParams,
publicParams: initialMessage.groupV2.publicParams,
});
const existingRevision = conversation.get('revision');
const isFirstUpdate = !isNumber(existingRevision);
// Standard GroupV2 modification codepath
const isV2GroupUpdate =
initialMessage.groupV2 &&
isNumber(initialMessage.groupV2.revision) &&
(isFirstUpdate || initialMessage.groupV2.revision > existingRevision);
if (isV2GroupUpdate && initialMessage.groupV2) {
const { revision, groupChange } = initialMessage.groupV2;
try {
await window.Signal.Groups.maybeUpdateGroup({
conversation,
groupChange: groupChange
? {
base64: groupChange,
isTrusted: false,
}
: undefined,
newRevision: revision,
receivedAt: message.get('received_at'),
sentAt: message.get('sent_at'),
});
} catch (error) {
const errorText = Errors.toLogFormat(error);
log.error(
`${idLog}: Failed to process group update as part of message ${getMessageIdForLogging(message.attributes)}: ${errorText}`
);
throw error;
}
}
}
}
const ourAci = window.textsecure.storage.user.getCheckedAci();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const sender = window.ConversationController.lookupOrCreate({
e164: source,
serviceId: sourceServiceId,
reason: 'handleDataMessage',
})!;
const hasGroupV2Prop = Boolean(initialMessage.groupV2);
// Drop if from blocked user. Only GroupV2 messages should need to be dropped here.
const isBlocked =
(source && window.storage.blocked.isBlocked(source)) ||
(sourceServiceId &&
window.storage.blocked.isServiceIdBlocked(sourceServiceId));
if (isBlocked) {
log.info(
`${idLog}: Dropping message from blocked sender. hasGroupV2Prop: ${hasGroupV2Prop}`
);
confirm();
return;
}
const areWeMember =
!conversation.get('left') && conversation.hasMember(ourAci);
// Drop an incoming GroupV2 message if we or the sender are not part of the group
// after applying the message's associated group changes.
if (
type === 'incoming' &&
!isDirectConversation(conversation.attributes) &&
hasGroupV2Prop &&
(!areWeMember ||
(sourceServiceId && !conversation.hasMember(sourceServiceId)))
) {
log.warn(
`${idLog}: Received message destined for group, which we or the sender are not a part of. Dropping.`
);
confirm();
return;
}
// We drop incoming messages for v1 groups we already know about, which we're not
// a part of, except for group updates. Because group v1 updates haven't been
// applied by this point.
// Note: if we have no information about a group at all, we will accept those
// messages. We detect that via a missing 'members' field.
if (
type === 'incoming' &&
!isDirectConversation(conversation.attributes) &&
!hasGroupV2Prop &&
conversation.get('members') &&
!areWeMember
) {
log.warn(
`Received message destined for group ${conversation.idForLogging()}, which we're not a part of. Dropping.`
);
confirm();
return;
}
// Drop incoming messages to announcement only groups where sender is not admin
if (conversation.get('announcementsOnly')) {
const senderServiceId = sender.getServiceId();
if (!senderServiceId || !conversation.isAdmin(senderServiceId)) {
confirm();
return;
}
}
const messageId =
message.get('id') || generateMessageId(message.get('received_at')).id;
// Send delivery receipts, but only for non-story sealed sender messages
// and not for messages from unaccepted conversations
if (
type === 'incoming' &&
message.get('unidentifiedDeliveryReceived') &&
!hasErrors(message.attributes) &&
conversation.getAccepted()
) {
// Note: We both queue and batch because we want to wait until we are done
// processing incoming messages to start sending outgoing delivery receipts.
// The queue can be paused easily.
drop(
window.Whisper.deliveryReceiptQueue.add(() => {
strictAssert(
isAciString(sourceServiceId),
'Incoming message must be from ACI'
);
window.Whisper.deliveryReceiptBatcher.add({
messageId,
conversationId,
senderE164: source,
senderAci: sourceServiceId,
timestamp: message.get('sent_at'),
isDirectConversation: isDirectConversation(conversation.attributes),
});
})
);
}
const { storyContext } = initialMessage;
let storyContextLogId = 'no storyContext';
if (storyContext) {
storyContextLogId =
`storyContext(${storyContext.sentTimestamp}, ` +
`${storyContext.authorAci})`;
}
// Ensure that quote author's conversation exist
if (initialMessage.quote) {
window.ConversationController.lookupOrCreate({
serviceId: initialMessage.quote.authorAci,
reason: 'handleDataMessage.quote.author',
});
}
const [quote, storyQuotes] = await Promise.all([
initialMessage.quote
? copyFromQuotedMessage(initialMessage.quote, conversation.id)
: undefined,
findStoryMessages(conversation.id, storyContext),
]);
const storyQuote = storyQuotes.find(candidateQuote => {
const sendStateByConversationId =
candidateQuote.sendStateByConversationId || {};
const sendState = sendStateByConversationId[sender.id];
const storyQuoteIsFromSelf =
candidateQuote.sourceServiceId === window.storage.user.getCheckedAci();
if (!storyQuoteIsFromSelf) {
return true;
}
// The sender is not a recipient for this story
if (sendState === undefined) {
return false;
}
// Group replies are always allowed
if (!isDirectConversation(conversation.attributes)) {
return true;
}
// For 1:1 stories, we need to check if they can be replied to
return sendState.isAllowedToReplyToStory !== false;
});
if (
storyContext &&
!storyQuote &&
!isDirectConversation(conversation.attributes)
) {
log.warn(
`${idLog}: Received ${storyContextLogId} message in group but no matching story. Dropping.`
);
confirm();
return;
}
if (storyQuote) {
const { storyDistributionListId } = storyQuote;
if (storyDistributionListId) {
const storyDistribution =
await DataReader.getStoryDistributionWithMembers(
storyDistributionListId
);
if (!storyDistribution) {
log.warn(
`${idLog}: Received ${storyContextLogId} message for story with no associated distribution list. Dropping.`
);
confirm();
return;
}
if (!storyDistribution.allowsReplies) {
log.warn(
`${idLog}: Received ${storyContextLogId} message but distribution list does not allow replies. Dropping.`
);
confirm();
return;
}
}
}
const withQuoteReference = {
...message.attributes,
...initialMessage,
quote,
storyId: storyQuote?.id,
};
// There are type conflicts between ModelAttributesType and protos passed in here
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dataMessage = await upgradeMessageSchema(withQuoteReference as any);
const isGroupStoryReply =
isGroup(conversation.attributes) && dataMessage.storyId;
try {
const now = new Date().getTime();
const urls = LinkPreview.findLinks(dataMessage.body || '');
const incomingPreview = dataMessage.preview || [];
const preview = incomingPreview
.map((item: LinkPreviewType) => {
if (LinkPreview.isCallLink(item.url)) {
return {
...item,
isCallLink: true,
callLinkRoomId: getRoomIdFromCallLink(item.url),
};
}
if (!item.image && !item.title) {
return null;
}
// Story link previews don't have to correspond to links in the
// message body.
if (isStory(message.attributes)) {
return item;
}
if (
!urls.includes(item.url) ||
!LinkPreview.shouldPreviewHref(item.url)
) {
return undefined;
}
return item;
})
.filter(isNotNil);
if (preview.length < incomingPreview.length) {
log.info(
`${getMessageIdForLogging(message.attributes)}: Eliminated ${
incomingPreview.length - preview.length
} previews with invalid urls'`
);
}
const ourPni = window.textsecure.storage.user.getCheckedPni();
const ourServiceIds: Set<ServiceIdString> = new Set([ourAci, ourPni]);
const [longMessageAttachments, normalAttachments] = partition(
dataMessage.attachments ?? [],
attachment => MIME.isLongMessage(attachment.contentType)
);
// eslint-disable-next-line no-param-reassign
message = window.MessageCache.register(message);
message.set({
id: messageId,
attachments: normalAttachments,
body: dataMessage.body,
bodyAttachment: longMessageAttachments[0],
bodyRanges: dataMessage.bodyRanges,
contact: dataMessage.contact,
conversationId: conversation.id,
decrypted_at: now,
errors: [],
flags: dataMessage.flags,
giftBadge: initialMessage.giftBadge,
hasAttachments: dataMessage.hasAttachments,
hasFileAttachments: dataMessage.hasFileAttachments,
hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments,
isViewOnce: Boolean(dataMessage.isViewOnce),
mentionsMe: (dataMessage.bodyRanges ?? []).some(bodyRange => {
if (!BodyRange.isMention(bodyRange)) {
return false;
}
return ourServiceIds.has(
normalizeServiceId(
bodyRange.mentionAci,
'handleDataMessage: mentionsMe check'
)
);
}),
preview,
requiredProtocolVersion:
dataMessage.requiredProtocolVersion || INITIAL_PROTOCOL_VERSION,
supportedVersionAtReceive: CURRENT_PROTOCOL_VERSION,
payment: dataMessage.payment,
quote: dataMessage.quote,
schemaVersion: dataMessage.schemaVersion,
sticker: dataMessage.sticker,
storyId: dataMessage.storyId,
});
if (storyQuote) {
await hydrateStoryContext(message.id, storyQuote, {
shouldSave: true,
});
}
const isSupported = !isUnsupportedMessage(message.attributes);
if (!isSupported) {
await eraseMessageContents(message);
}
if (isSupported) {
const attributes = {
...conversation.attributes,
};
// Drop empty messages after. This needs to happen after the initial
// message.set call and after GroupV1 processing to make sure all possible
// properties are set before we determine that a message is empty.
if (isMessageEmpty(message.attributes)) {
log.info(`${idLog}: Dropping empty message`);
confirm();
return;
}
if (isStory(message.attributes)) {
attributes.hasPostedStory = true;
} else {
attributes.active_at = now;
}
conversation.set(attributes);
// Sync group story reply expiration timers with the parent story's
// expiration timer
if (isGroupStoryReply && storyQuote) {
message.set({
expireTimer: storyQuote.expireTimer,
expirationStartTimestamp: storyQuote.expirationStartTimestamp,
});
}
if (dataMessage.expireTimer && !isExpirationTimerUpdate(dataMessage)) {
message.set({ expireTimer: dataMessage.expireTimer });
if (isStory(message.attributes)) {
log.info(`${idLog}: Starting story expiration`);
message.set({
expirationStartTimestamp: dataMessage.timestamp,
});
}
}
if (!hasGroupV2Prop && !isStory(message.attributes)) {
if (isExpirationTimerUpdate(message.attributes)) {
message.set({
expirationTimerUpdate: {
source,
sourceServiceId,
expireTimer: initialMessage.expireTimer,
},
});
if (conversation.get('expireTimer') !== dataMessage.expireTimer) {
log.info('Incoming expirationTimerUpdate changed timer', {
id: conversation.idForLogging(),
expireTimer: dataMessage.expireTimer || 'disabled',
source: idLog,
});
conversation.set({
expireTimer: dataMessage.expireTimer,
});
}
}
// Note: For incoming expire timer updates (not normal messages that come
// along with an expireTimer), the conversation will be updated by this
// point and these calls will return early.
if (dataMessage.expireTimer) {
void conversation.updateExpirationTimer(dataMessage.expireTimer, {
source: sourceServiceId || source,
receivedAt: message.get('received_at'),
receivedAtMS: message.get('received_at_ms'),
sentAt: message.get('sent_at'),
reason: idLog,
version: initialMessage.expireTimerVersion,
});
} else if (
// We won't turn off timers for these kinds of messages:
!isGroupUpdate(message.attributes) &&
!isEndSession(message.attributes)
) {
void conversation.updateExpirationTimer(undefined, {
source: sourceServiceId || source,
receivedAt: message.get('received_at'),
receivedAtMS: message.get('received_at_ms'),
sentAt: message.get('sent_at'),
reason: idLog,
version: initialMessage.expireTimerVersion,
});
}
}
if (initialMessage.profileKey) {
const { profileKey } = initialMessage;
if (
source === window.textsecure.storage.user.getNumber() ||
sourceServiceId === window.textsecure.storage.user.getAci()
) {
conversation.set({ profileSharing: true });
} else if (isDirectConversation(conversation.attributes)) {
drop(
conversation.setProfileKey(profileKey, {
reason: 'handleDataMessage',
})
);
} else {
const local = window.ConversationController.lookupOrCreate({
e164: source,
serviceId: sourceServiceId,
reason: 'handleDataMessage:setProfileKey',
});
drop(
local?.setProfileKey(profileKey, {
reason: 'handleDataMessage',
})
);
}
}
if (isTapToView(message.attributes) && type === 'outgoing') {
await eraseMessageContents(message);
}
if (
type === 'incoming' &&
isTapToView(message.attributes) &&
!isValidTapToView(message.attributes)
) {
log.warn(
`${idLog}: Received tap to view message with invalid data. Erasing contents.`
);
message.set({
isTapToViewInvalid: true,
});
await eraseMessageContents(message);
}
}
const conversationTimestamp = conversation.get('timestamp');
if (
!isStory(message.attributes) &&
!isGroupStoryReply &&
(!conversationTimestamp ||
message.get('sent_at') > conversationTimestamp) &&
messageHasPaymentEvent(message.attributes)
) {
conversation.set({
lastMessage: getNotificationTextForMessage(message.attributes),
lastMessageAuthor: getMessageAuthorText(message.attributes),
timestamp: message.get('sent_at'),
});
}
// eslint-disable-next-line no-param-reassign
message = window.MessageCache.register(message);
conversation.incrementMessageCount();
// If we sent a message in a given conversation, unarchive it!
if (type === 'outgoing') {
conversation.setArchived(false);
}
await DataWriter.updateConversation(conversation.attributes);
const giftBadge = message.get('giftBadge');
if (giftBadge && giftBadge.state !== GiftBadgeStates.Failed) {
const { level } = giftBadge;
const { updatesUrl } = window.SignalContext.config;
strictAssert(
typeof updatesUrl === 'string',
'getProfile: expected updatesUrl to be a defined string'
);
const userLanguages = getUserLanguages(
window.SignalContext.getPreferredSystemLocales(),
window.SignalContext.getResolvedMessagesLocale()
);
const { messaging } = window.textsecure;
if (!messaging) {
throw new Error(`${idLog}: messaging is not available`);
}
const response =
await messaging.server.getSubscriptionConfiguration(userLanguages);
const boostBadgesByLevel = parseBoostBadgeListFromServer(
response,
updatesUrl
);
const badge = boostBadgesByLevel[level];
if (!badge) {
log.error(
`${idLog}: gift badge with level ${level} not found on server`
);
} else {
await window.reduxActions.badges.updateOrCreate([badge]);
giftBadge.id = badge.id;
}
}
const result = await modifyTargetMessage(message, conversation, {
isFirstRun: true,
skipEdits: false,
});
if (result === ModifyTargetMessageResult.Deleted) {
confirm();
return;
}
log.info(`${idLog}: Batching save`);
drop(saveAndNotify(message, conversation, confirm));
} catch (error) {
const errorForLog = Errors.toLogFormat(error);
log.error(`${idLog}: error:`, errorForLog);
throw error;
}
});
}

View file

@ -12,6 +12,7 @@ import type { MessageAttributesType } from '../model-types.d';
import type { AciString } from '../types/ServiceId'; import type { AciString } from '../types/ServiceId';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
import { DataReader, DataWriter } from '../sql/Client'; import { DataReader, DataWriter } from '../sql/Client';
import { postSaveUpdates } from '../util/cleanup';
const MAX_CONCURRENCY = 5; const MAX_CONCURRENCY = 5;
@ -57,7 +58,7 @@ export async function _migrateMessageData({
) => Promise<Array<MessageAttributesType>>; ) => Promise<Array<MessageAttributesType>>;
saveMessagesIndividually: ( saveMessagesIndividually: (
data: ReadonlyArray<MessageAttributesType>, data: ReadonlyArray<MessageAttributesType>,
options: { ourAci: AciString } options: { ourAci: AciString; postSaveUpdates: () => Promise<void> }
) => Promise<{ failedIndices: Array<number> }>; ) => Promise<{ failedIndices: Array<number> }>;
incrementMessagesMigrationAttempts: ( incrementMessagesMigrationAttempts: (
messageIds: ReadonlyArray<string> messageIds: ReadonlyArray<string>
@ -122,6 +123,7 @@ export async function _migrateMessageData({
upgradedMessages, upgradedMessages,
{ {
ourAci, ourAci,
postSaveUpdates,
} }
); );

View file

@ -0,0 +1,72 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as log from '../logging/log';
import { explodePromise } from '../util/explodePromise';
import { saveNewMessageBatcher } from '../util/messageBatcher';
import { handleAttachmentDownloadsForNewMessage } from '../util/queueAttachmentDownloads';
import {
modifyTargetMessage,
ModifyTargetMessageResult,
} from '../util/modifyTargetMessage';
import { shouldReplyNotifyUser } from '../util/shouldReplyNotifyUser';
import { isStory } from './helpers';
import { drop } from '../util/drop';
import type { ConversationModel } from '../models/conversations';
import type { MessageModel } from '../models/messages';
export async function saveAndNotify(
message: MessageModel,
conversation: ConversationModel,
confirm: () => void
): Promise<void> {
const { resolve, promise } = explodePromise<void>();
try {
conversation.addSavePromise(promise);
await saveNewMessageBatcher.add(message.attributes);
log.info('Message saved', message.get('sent_at'));
// Once the message is saved to DB, we queue attachment downloads
await handleAttachmentDownloadsForNewMessage(message, conversation);
// We'd like to check for deletions before scheduling downloads, but if an edit
// comes in, we want to have kicked off attachment downloads for the original
// message.
const result = await modifyTargetMessage(message, conversation, {
isFirstRun: false,
skipEdits: false,
});
if (result === ModifyTargetMessageResult.Deleted) {
confirm();
return;
}
drop(conversation.onNewMessage(message));
if (await shouldReplyNotifyUser(message.attributes, conversation)) {
await conversation.notify(message.attributes);
}
// Increment the sent message count if this is an outgoing message
if (message.get('type') === 'outgoing') {
conversation.incrementSentMessageCount();
}
window.Whisper.events.trigger('incrementProgress');
confirm();
if (!isStory(message.attributes)) {
drop(
conversation.queueJob('updateUnread', () => conversation.updateUnread())
);
}
} finally {
resolve();
conversation.removeSavePromise(promise);
}
}

491
ts/messages/send.ts Normal file
View file

@ -0,0 +1,491 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { noop, union } from 'lodash';
import { filter, map } from '../util/iterables';
import { isNotNil } from '../util/isNotNil';
import { SendMessageProtoError } from '../textsecure/Errors';
import { getOwn } from '../util/getOwn';
import { isGroup } from '../util/whatTypeOfConversation';
import { handleMessageSend } from '../util/handleMessageSend';
import { getSendOptions } from '../util/getSendOptions';
import * as log from '../logging/log';
import { DataWriter } from '../sql/Client';
import {
getPropForTimestamp,
getChangesForPropAtTimestamp,
} from '../util/editHelpers';
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
import {
notifyStorySendFailed,
saveErrorsOnMessage,
} from '../test-node/util/messageFailures';
import { postSaveUpdates } from '../util/cleanup';
import { isCustomError } from './helpers';
import { SendActionType, isSent, sendStateReducer } from './MessageSendState';
import type { CustomError, MessageAttributesType } from '../model-types.d';
import type { CallbackResultType } from '../textsecure/Types.d';
import type { MessageModel } from '../models/messages';
import type { ServiceIdString } from '../types/ServiceId';
import type { SendStateByConversationId } from './MessageSendState';
/* eslint-disable more/no-then */
export async function send(
message: MessageModel,
{
promise,
saveErrors,
targetTimestamp,
}: {
promise: Promise<CallbackResultType | void | null>;
saveErrors?: (errors: Array<Error>) => void;
targetTimestamp: number;
}
): Promise<void> {
const conversation = window.ConversationController.get(
message.attributes.conversationId
);
const updateLeftPane = conversation?.debouncedUpdateLastMessage ?? noop;
updateLeftPane();
let result:
| { success: true; value: CallbackResultType }
| {
success: false;
value: CustomError | SendMessageProtoError;
};
try {
const value = await (promise as Promise<CallbackResultType>);
result = { success: true, value };
} catch (err) {
result = { success: false, value: err };
}
updateLeftPane();
const attributesToUpdate: Partial<MessageAttributesType> = {};
// This is used by sendSyncMessage, then set to null
if ('dataMessage' in result.value && result.value.dataMessage) {
attributesToUpdate.dataMessage = result.value.dataMessage;
} else if ('editMessage' in result.value && result.value.editMessage) {
attributesToUpdate.dataMessage = result.value.editMessage;
}
if (!message.doNotSave) {
await DataWriter.saveMessage(message.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
});
}
const sendStateByConversationId = {
...(getPropForTimestamp({
log,
message: message.attributes,
prop: 'sendStateByConversationId',
targetTimestamp,
}) || {}),
};
const sendIsNotFinal =
'sendIsNotFinal' in result.value && result.value.sendIsNotFinal;
const sendIsFinal = !sendIsNotFinal;
// Capture successful sends
const successfulServiceIds: Array<ServiceIdString> =
sendIsFinal &&
'successfulServiceIds' in result.value &&
Array.isArray(result.value.successfulServiceIds)
? result.value.successfulServiceIds
: [];
const sentToAtLeastOneRecipient =
result.success || Boolean(successfulServiceIds.length);
successfulServiceIds.forEach(serviceId => {
const targetConversation = window.ConversationController.get(serviceId);
if (!targetConversation) {
return;
}
// If we successfully sent to a user, we can remove our unregistered flag.
if (targetConversation.isEverUnregistered()) {
targetConversation.setRegistered();
}
const previousSendState = getOwn(
sendStateByConversationId,
targetConversation.id
);
if (previousSendState) {
sendStateByConversationId[targetConversation.id] = sendStateReducer(
previousSendState,
{
type: SendActionType.Sent,
updatedAt: Date.now(),
}
);
}
});
// Integrate sends via sealed sender
const latestEditTimestamp = message.get('editMessageTimestamp');
const sendIsLatest =
!latestEditTimestamp || targetTimestamp === latestEditTimestamp;
const previousUnidentifiedDeliveries =
message.get('unidentifiedDeliveries') || [];
const newUnidentifiedDeliveries =
sendIsLatest &&
sendIsFinal &&
'unidentifiedDeliveries' in result.value &&
Array.isArray(result.value.unidentifiedDeliveries)
? result.value.unidentifiedDeliveries
: [];
const promises: Array<Promise<unknown>> = [];
// Process errors
let errors: Array<CustomError>;
if (result.value instanceof SendMessageProtoError && result.value.errors) {
({ errors } = result.value);
} else if (isCustomError(result.value)) {
errors = [result.value];
} else if (Array.isArray(result.value.errors)) {
({ errors } = result.value);
} else {
errors = [];
}
// In groups, we don't treat unregistered users as a user-visible
// error. The message will look successful, but the details
// screen will show that we didn't send to these unregistered users.
const errorsToSave: Array<CustomError> = [];
errors.forEach(error => {
const errorConversation =
window.ConversationController.get(error.serviceId) ||
window.ConversationController.get(error.number);
if (errorConversation && !saveErrors && sendIsFinal) {
const previousSendState = getOwn(
sendStateByConversationId,
errorConversation.id
);
if (previousSendState) {
sendStateByConversationId[errorConversation.id] = sendStateReducer(
previousSendState,
{
type: SendActionType.Failed,
updatedAt: Date.now(),
}
);
notifyStorySendFailed(message);
}
}
let shouldSaveError = true;
switch (error.name) {
case 'OutgoingIdentityKeyError': {
if (conversation) {
promises.push(
conversation.getProfiles().catch(() => {
/* nothing to do here; logging already happened */
})
);
}
break;
}
case 'UnregisteredUserError':
if (conversation && isGroup(conversation.attributes)) {
shouldSaveError = false;
}
// If we just found out that we couldn't send to a user because they are no
// longer registered, we will update our unregistered flag. In groups we
// will not event try to send to them for 6 hours. And we will never try
// to fetch them on startup again.
//
// The way to discover registration once more is:
// 1) any attempt to send to them in 1:1 conversation
// 2) the six-hour time period has passed and we send in a group again
conversation?.setUnregistered();
break;
default:
break;
}
if (shouldSaveError) {
errorsToSave.push(error);
}
});
// Only update the expirationStartTimestamp if we don't already have one set
if (!message.get('expirationStartTimestamp')) {
attributesToUpdate.expirationStartTimestamp = sentToAtLeastOneRecipient
? Date.now()
: undefined;
}
attributesToUpdate.unidentifiedDeliveries = union(
previousUnidentifiedDeliveries,
newUnidentifiedDeliveries
);
// We may overwrite this in the `saveErrors` call below.
attributesToUpdate.errors = [];
const additionalProps = getChangesForPropAtTimestamp({
log,
message: message.attributes,
prop: 'sendStateByConversationId',
targetTimestamp,
value: sendStateByConversationId,
});
message.set({ ...attributesToUpdate, ...additionalProps });
if (saveErrors) {
saveErrors(errorsToSave);
} else {
// We skip save because we'll save in the next step.
await saveErrorsOnMessage(message, errorsToSave, {
skipSave: true,
});
}
if (!message.doNotSave) {
await window.MessageCache.saveMessage(message);
}
updateLeftPane();
if (sentToAtLeastOneRecipient && !message.doNotSendSyncMessage) {
promises.push(sendSyncMessage(message, targetTimestamp));
}
await Promise.all(promises);
updateLeftPane();
}
export async function sendSyncMessageOnly(
message: MessageModel,
{
targetTimestamp,
dataMessage,
saveErrors,
}: {
targetTimestamp: number;
dataMessage: Uint8Array;
saveErrors?: (errors: Array<Error>) => void;
}
): Promise<CallbackResultType | void> {
const conv = window.ConversationController.get(
message.attributes.conversationId
);
message.set({ dataMessage });
const updateLeftPane = conv?.debouncedUpdateLastMessage;
try {
message.set({
// This is the same as a normal send()
expirationStartTimestamp: Date.now(),
errors: [],
});
const result = await sendSyncMessage(message, targetTimestamp);
message.set({
// We have to do this afterward, since we didn't have a previous send!
unidentifiedDeliveries:
result && result.unidentifiedDeliveries
? result.unidentifiedDeliveries
: undefined,
});
return result;
} catch (error) {
const resultErrors = error?.errors;
const errors = Array.isArray(resultErrors)
? resultErrors
: [new Error('Unknown error')];
if (saveErrors) {
saveErrors(errors);
} else {
// We don't save because we're about to save below.
await saveErrorsOnMessage(message, errors, {
skipSave: true,
});
}
throw error;
} finally {
await DataWriter.saveMessage(message.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
});
if (updateLeftPane) {
updateLeftPane();
}
}
}
export async function sendSyncMessage(
message: MessageModel,
targetTimestamp: number
): Promise<CallbackResultType | void> {
const ourConversation =
window.ConversationController.getOurConversationOrThrow();
const sendOptions = await getSendOptions(ourConversation.attributes, {
syncMessage: true,
});
if (window.ConversationController.areWePrimaryDevice()) {
log.warn(
'sendSyncMessage: We are primary device; not sending sync message'
);
message.set({ dataMessage: undefined });
return;
}
const { messaging } = window.textsecure;
if (!messaging) {
throw new Error('sendSyncMessage: messaging not available!');
}
// eslint-disable-next-line no-param-reassign
message.syncPromise = message.syncPromise || Promise.resolve();
const next = async () => {
const dataMessage = message.get('dataMessage');
if (!dataMessage) {
return;
}
const originalTimestamp = getMessageSentTimestamp(message.attributes, {
includeEdits: false,
log,
});
const isSendingEdit = targetTimestamp !== originalTimestamp;
const isUpdate = Boolean(message.get('synced')) && !isSendingEdit;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const conv = window.ConversationController.get(
message.attributes.conversationId
)!;
const sendEntries = Object.entries(
getPropForTimestamp({
log,
message: message.attributes,
prop: 'sendStateByConversationId',
targetTimestamp,
}) || {}
);
const sentEntries = filter(sendEntries, ([_conversationId, { status }]) =>
isSent(status)
);
const allConversationIdsSentTo = map(
sentEntries,
([conversationId]) => conversationId
);
const conversationIdsSentTo = filter(
allConversationIdsSentTo,
conversationId => conversationId !== ourConversation.id
);
const unidentifiedDeliveries = message.get('unidentifiedDeliveries') || [];
const maybeConversationsWithSealedSender = map(
unidentifiedDeliveries,
identifier => window.ConversationController.get(identifier)
);
const conversationsWithSealedSender = filter(
maybeConversationsWithSealedSender,
isNotNil
);
const conversationIdsWithSealedSender = new Set(
map(conversationsWithSealedSender, c => c.id)
);
const encodedContent = isSendingEdit
? {
encodedEditMessage: dataMessage,
}
: {
encodedDataMessage: dataMessage,
};
return handleMessageSend(
messaging.sendSyncMessage({
...encodedContent,
timestamp: targetTimestamp,
destination: conv.get('e164'),
destinationServiceId: conv.getServiceId(),
expirationStartTimestamp:
message.get('expirationStartTimestamp') || null,
conversationIdsSentTo,
conversationIdsWithSealedSender,
isUpdate,
options: sendOptions,
urgent: false,
}),
// Note: in some situations, for doNotSave messages, the message has no
// id, so we provide an empty array here.
{ messageIds: message.id ? [message.id] : [], sendType: 'sentSync' }
).then(async result => {
let newSendStateByConversationId: undefined | SendStateByConversationId;
const sendStateByConversationId =
getPropForTimestamp({
log,
message: message.attributes,
prop: 'sendStateByConversationId',
targetTimestamp,
}) || {};
const ourOldSendState = getOwn(
sendStateByConversationId,
ourConversation.id
);
if (ourOldSendState) {
const ourNewSendState = sendStateReducer(ourOldSendState, {
type: SendActionType.Sent,
updatedAt: Date.now(),
});
if (ourNewSendState !== ourOldSendState) {
newSendStateByConversationId = {
...sendStateByConversationId,
[ourConversation.id]: ourNewSendState,
};
}
}
const attributesForUpdate = newSendStateByConversationId
? getChangesForPropAtTimestamp({
log,
message: message.attributes,
prop: 'sendStateByConversationId',
value: newSendStateByConversationId,
targetTimestamp,
})
: null;
message.set({
synced: true,
dataMessage: null,
...attributesForUpdate,
});
// Return early, skip the save
if (message.doNotSave) {
return result;
}
await DataWriter.saveMessage(message.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
});
return result;
});
};
// eslint-disable-next-line no-param-reassign
message.syncPromise = message.syncPromise.then(next, next);
return message.syncPromise;
}

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

@ -71,8 +71,8 @@ export type CustomError = Error & {
export type GroupMigrationType = { export type GroupMigrationType = {
areWeInvited: boolean; areWeInvited: boolean;
droppedMemberIds?: Array<string>; droppedMemberIds?: ReadonlyArray<string>;
invitedMembers?: Array<LegacyMigrationPendingMemberType>; invitedMembers?: ReadonlyArray<LegacyMigrationPendingMemberType>;
// We don't generate data like this; these were added to support import/export // We don't generate data like this; these were added to support import/export
droppedMemberCount?: number; droppedMemberCount?: number;
@ -113,7 +113,7 @@ type StoryReplyContextType = {
export type GroupV1Update = { export type GroupV1Update = {
avatarUpdated?: boolean; avatarUpdated?: boolean;
joined?: Array<string>; joined?: ReadonlyArray<string>;
left?: string | 'You'; left?: string | 'You';
name?: string; name?: string;
}; };
@ -130,11 +130,11 @@ export type MessageReactionType = {
// needs more usage of get/setPropForTimestamp. Also, these fields must match the fields // needs more usage of get/setPropForTimestamp. Also, these fields must match the fields
// in MessageAttributesType. // in MessageAttributesType.
export type EditHistoryType = { export type EditHistoryType = {
attachments?: Array<AttachmentType>; attachments?: ReadonlyArray<AttachmentType>;
body?: string; body?: string;
bodyAttachment?: AttachmentType; bodyAttachment?: AttachmentType;
bodyRanges?: ReadonlyArray<RawBodyRange>; bodyRanges?: ReadonlyArray<RawBodyRange>;
preview?: Array<LinkPreviewType>; preview?: ReadonlyArray<LinkPreviewType>;
quote?: QuotedMessageType; quote?: QuotedMessageType;
sendStateByConversationId?: SendStateByConversationId; sendStateByConversationId?: SendStateByConversationId;
timestamp: number; timestamp: number;
@ -178,7 +178,7 @@ export type MessageAttributesType = {
decrypted_at?: number; decrypted_at?: number;
deletedForEveryone?: boolean; deletedForEveryone?: boolean;
deletedForEveryoneTimestamp?: number; deletedForEveryoneTimestamp?: number;
errors?: Array<CustomError>; errors?: ReadonlyArray<CustomError>;
expirationStartTimestamp?: number | null; expirationStartTimestamp?: number | null;
expireTimer?: DurationInSeconds; expireTimer?: DurationInSeconds;
groupMigration?: GroupMigrationType; groupMigration?: GroupMigrationType;
@ -190,7 +190,7 @@ export type MessageAttributesType = {
isErased?: boolean; isErased?: boolean;
isTapToViewInvalid?: boolean; isTapToViewInvalid?: boolean;
isViewOnce?: boolean; isViewOnce?: boolean;
editHistory?: Array<EditHistoryType>; editHistory?: ReadonlyArray<EditHistoryType>;
editMessageTimestamp?: number; editMessageTimestamp?: number;
editMessageReceivedAt?: number; editMessageReceivedAt?: number;
editMessageReceivedAtMs?: number; editMessageReceivedAtMs?: number;
@ -220,12 +220,12 @@ export type MessageAttributesType = {
id: string; id: string;
type: MessageType; type: MessageType;
body?: string; body?: string;
attachments?: Array<AttachmentType>; attachments?: ReadonlyArray<AttachmentType>;
preview?: Array<LinkPreviewType>; preview?: ReadonlyArray<LinkPreviewType>;
sticker?: StickerType; sticker?: StickerType;
sent_at: number; sent_at: number;
unidentifiedDeliveries?: Array<string>; unidentifiedDeliveries?: ReadonlyArray<string>;
contact?: Array<EmbeddedContactType>; contact?: ReadonlyArray<EmbeddedContactType>;
conversationId: string; conversationId: string;
storyReaction?: { storyReaction?: {
emoji: string; emoji: string;
@ -286,8 +286,8 @@ export type MessageAttributesType = {
timestamp: number; timestamp: number;
// Backwards-compatibility with prerelease data schema // Backwards-compatibility with prerelease data schema
invitedGV2Members?: Array<LegacyMigrationPendingMemberType>; invitedGV2Members?: ReadonlyArray<LegacyMigrationPendingMemberType>;
droppedGV2MemberIds?: Array<string>; droppedGV2MemberIds?: ReadonlyArray<string>;
sendHQImages?: boolean; sendHQImages?: boolean;

View file

@ -189,6 +189,9 @@ import { getCallHistorySelector } from '../state/selectors/callHistory';
import { migrateLegacyReadStatus } from '../messages/migrateLegacyReadStatus'; import { migrateLegacyReadStatus } from '../messages/migrateLegacyReadStatus';
import { migrateLegacySendAttributes } from '../messages/migrateLegacySendAttributes'; import { migrateLegacySendAttributes } from '../messages/migrateLegacySendAttributes';
import { getIsInitialSync } from '../services/contactSync'; import { getIsInitialSync } from '../services/contactSync';
import { queueAttachmentDownloadsForMessage } from '../util/queueAttachmentDownloads';
import { cleanupMessages, postSaveUpdates } from '../util/cleanup';
import { MessageModel } from './messages';
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -385,7 +388,6 @@ export class ConversationModel extends window.Backbone
); );
} }
this.on('newmessage', this.onNewMessage);
this.on('change:profileKey', this.onChangeProfileKey); this.on('change:profileKey', this.onChangeProfileKey);
this.on( this.on(
'change:name change:profileName change:profileFamilyName change:e164 ' + 'change:name change:profileName change:profileFamilyName change:e164 ' +
@ -464,8 +466,6 @@ export class ConversationModel extends window.Backbone
SECOND SECOND
); );
this.on('newmessage', this.throttledUpdateVerified);
const migratedColor = this.getColor(); const migratedColor = this.getColor();
if (this.get('color') !== migratedColor) { if (this.get('color') !== migratedColor) {
this.set('color', migratedColor); this.set('color', migratedColor);
@ -1442,8 +1442,14 @@ export class ConversationModel extends window.Backbone
}); });
} }
async onNewMessage(message: MessageAttributesType): Promise<void> { async onNewMessage(message: MessageModel): Promise<void> {
const { sourceServiceId: serviceId, source: e164, sourceDevice } = message; const {
sourceServiceId: serviceId,
source: e164,
sourceDevice,
storyId,
} = message.attributes;
this.throttledUpdateVerified?.();
const source = window.ConversationController.lookupOrCreate({ const source = window.ConversationController.lookupOrCreate({
serviceId, serviceId,
@ -1459,15 +1465,15 @@ export class ConversationModel extends window.Backbone
// If it's a group story reply or a story message, we don't want to update // If it's a group story reply or a story message, we don't want to update
// the last message or add new messages to redux. // the last message or add new messages to redux.
const isGroupStoryReply = isGroup(this.attributes) && message.storyId; const isGroupStoryReply = isGroup(this.attributes) && storyId;
if (isGroupStoryReply || isStory(message)) { if (isGroupStoryReply || isStory(message.attributes)) {
return; return;
} }
// Change to message request state if contact was removed and sent message. // Change to message request state if contact was removed and sent message.
if ( if (
this.get('removalStage') === 'justNotification' && this.get('removalStage') === 'justNotification' &&
isIncoming(message) isIncoming(message.attributes)
) { ) {
this.set({ this.set({
removalStage: 'messageRequest', removalStage: 'messageRequest',
@ -1476,7 +1482,7 @@ export class ConversationModel extends window.Backbone
await DataWriter.updateConversation(this.attributes); await DataWriter.updateConversation(this.attributes);
} }
void this.addSingleMessage(message); drop(this.addSingleMessage(message.attributes));
} }
// New messages might arrive while we're in the middle of a bulk fetch from the // New messages might arrive while we're in the middle of a bulk fetch from the
@ -1966,65 +1972,64 @@ export class ConversationModel extends window.Backbone
const hydrated = await Promise.all( const hydrated = await Promise.all(
present.map(async message => { present.map(async message => {
let migratedMessage = message; const model = window.MessageCache.register(new MessageModel(message));
let updated = false;
const readStatus = migrateLegacyReadStatus(migratedMessage); const readStatus = migrateLegacyReadStatus(model.attributes);
if (readStatus !== undefined) { if (readStatus !== undefined) {
migratedMessage = { updated = true;
...migratedMessage, model.set({
readStatus, readStatus,
seenStatus: seenStatus:
readStatus === ReadStatus.Unread readStatus === ReadStatus.Unread
? SeenStatus.Unseen ? SeenStatus.Unseen
: SeenStatus.Seen, : SeenStatus.Seen,
}; });
} }
if (ourConversationId) { if (ourConversationId) {
const sendStateByConversationId = migrateLegacySendAttributes( const sendStateByConversationId = migrateLegacySendAttributes(
migratedMessage, model.attributes,
window.ConversationController.get.bind( window.ConversationController.get.bind(
window.ConversationController window.ConversationController
), ),
ourConversationId ourConversationId
); );
if (sendStateByConversationId) { if (sendStateByConversationId) {
migratedMessage = { updated = true;
...migratedMessage, model.set({
sendStateByConversationId, sendStateByConversationId,
}; });
} }
} }
const upgradedMessage = await window.MessageCache.upgradeSchema( const startingAttributes = model.attributes;
migratedMessage, await window.MessageCache.upgradeSchema(
model,
Message.VERSION_NEEDED_FOR_DISPLAY Message.VERSION_NEEDED_FOR_DISPLAY
); );
if (startingAttributes !== model.attributes) {
updated = true;
}
const patch = await hydrateStoryContext(message.id, undefined, { const patch = await hydrateStoryContext(message.id, undefined, {
shouldSave: true, shouldSave: true,
}); });
if (patch) {
const didMigrate = migratedMessage !== message; updated = true;
const didUpgrade = upgradedMessage !== migratedMessage; model.set(patch);
const didPatch = Boolean(patch);
if (didMigrate || didUpgrade || didPatch) {
upgraded += 1;
} }
if (didMigrate && !didUpgrade && !didPatch) {
await window.MessageCache.setAttributes({ if (updated) {
messageId: message.id, upgraded += 1;
messageAttributes: migratedMessage, const ourAci = window.textsecure.storage.user.getCheckedAci();
skipSaveToDatabase: false, await DataWriter.saveMessage(model.attributes, {
ourAci,
postSaveUpdates,
}); });
} }
if (patch) { return model.attributes;
return { ...upgradedMessage, ...patch };
}
return upgradedMessage;
}) })
); );
if (upgraded > 0) { if (upgraded > 0) {
@ -2322,15 +2327,13 @@ export class ConversationModel extends window.Backbone
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await Promise.all( await Promise.all(
readMessages.map(async m => { readMessages.map(async m => {
const registered = window.MessageCache.__DEPRECATED$register( const registered = window.MessageCache.register(new MessageModel(m));
m.id, const shouldSave =
m, await queueAttachmentDownloadsForMessage(registered);
'handleReadAndDownloadAttachments'
);
const shouldSave = await registered.queueAttachmentDownloads();
if (shouldSave) { if (shouldSave) {
await DataWriter.saveMessage(registered.attributes, { await DataWriter.saveMessage(registered.attributes, {
ourAci, ourAci,
postSaveUpdates,
}); });
} }
}) })
@ -2354,7 +2357,7 @@ export class ConversationModel extends window.Backbone
? timestamp ? timestamp
: lastMessageTimestamp; : lastMessageTimestamp;
const message: MessageAttributesType = { const message = new MessageModel({
...generateMessageId(incrementMessageCounter()), ...generateMessageId(incrementMessageCounter()),
conversationId: this.id, conversationId: this.id,
type: 'message-request-response-event', type: 'message-request-response-event',
@ -2364,18 +2367,17 @@ export class ConversationModel extends window.Backbone
seenStatus: SeenStatus.NotApplicable, seenStatus: SeenStatus.NotApplicable,
timestamp, timestamp,
messageRequestResponseEvent: event, messageRequestResponseEvent: event,
}; });
await DataWriter.saveMessage(message, { await window.MessageCache.saveMessage(message, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
forceSave: true, forceSave: true,
}); });
if (!getIsInitialSync() && !this.get('active_at')) { if (!getIsInitialSync() && !this.get('active_at')) {
this.set({ active_at: Date.now() }); this.set({ active_at: Date.now() });
await DataWriter.updateConversation(this.attributes); await DataWriter.updateConversation(this.attributes);
} }
window.MessageCache.toMessageAttributes(message); window.MessageCache.register(message);
this.trigger('newmessage', message); drop(this.onNewMessage(message));
drop(this.updateLastMessage()); drop(this.updateLastMessage());
} }
@ -3120,7 +3122,7 @@ export class ConversationModel extends window.Backbone
receivedAt, receivedAt,
}); });
const message: MessageAttributesType = { const message = new MessageModel({
...generateMessageId(receivedAtCounter), ...generateMessageId(receivedAtCounter),
conversationId: this.id, conversationId: this.id,
type: 'chat-session-refreshed', type: 'chat-session-refreshed',
@ -3129,19 +3131,15 @@ export class ConversationModel extends window.Backbone
received_at_ms: receivedAt, received_at_ms: receivedAt,
readStatus: ReadStatus.Unread, readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen, seenStatus: SeenStatus.Unseen,
};
await DataWriter.saveMessage(message, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
}); });
window.MessageCache.__DEPRECATED$register(
message.id,
message,
'addChatSessionRefreshed'
);
this.trigger('newmessage', message); await window.MessageCache.saveMessage(message, {
void this.updateUnread(); forceSave: true,
});
window.MessageCache.register(message);
drop(this.onNewMessage(message));
drop(this.updateUnread());
} }
async addDeliveryIssue({ async addDeliveryIssue({
@ -3167,7 +3165,7 @@ export class ConversationModel extends window.Backbone
return; return;
} }
const message: MessageAttributesType = { const message = new MessageModel({
...generateMessageId(receivedAtCounter), ...generateMessageId(receivedAtCounter),
conversationId: this.id, conversationId: this.id,
type: 'delivery-issue', type: 'delivery-issue',
@ -3177,21 +3175,17 @@ export class ConversationModel extends window.Backbone
timestamp: receivedAt, timestamp: receivedAt,
readStatus: ReadStatus.Unread, readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen, seenStatus: SeenStatus.Unseen,
};
await DataWriter.saveMessage(message, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
}); });
window.MessageCache.__DEPRECATED$register(
message.id,
message,
'addDeliveryIssue'
);
this.trigger('newmessage', message); await window.MessageCache.saveMessage(message, {
forceSave: true,
});
window.MessageCache.register(message);
await this.notify(message); drop(this.onNewMessage(message));
void this.updateUnread(); drop(this.updateUnread());
await this.notify(message.attributes);
} }
async addKeyChange( async addKeyChange(
@ -3216,7 +3210,7 @@ export class ConversationModel extends window.Backbone
} }
const timestamp = Date.now(); const timestamp = Date.now();
const message: MessageAttributesType = { const message = new MessageModel({
...generateMessageId(incrementMessageCounter()), ...generateMessageId(incrementMessageCounter()),
conversationId: this.id, conversationId: this.id,
type: 'keychange', type: 'keychange',
@ -3227,19 +3221,14 @@ export class ConversationModel extends window.Backbone
readStatus: ReadStatus.Read, readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Unseen, seenStatus: SeenStatus.Unseen,
schemaVersion: Message.VERSION_NEEDED_FOR_DISPLAY, schemaVersion: Message.VERSION_NEEDED_FOR_DISPLAY,
}; });
await DataWriter.saveMessage(message, { await window.MessageCache.saveMessage(message, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
forceSave: true, forceSave: true,
}); });
window.MessageCache.__DEPRECATED$register( window.MessageCache.register(message);
message.id,
message,
'addKeyChange'
);
this.trigger('newmessage', message); drop(this.onNewMessage(message));
const serviceId = this.getServiceId(); const serviceId = this.getServiceId();
@ -3277,7 +3266,7 @@ export class ConversationModel extends window.Backbone
); );
const timestamp = Date.now(); const timestamp = Date.now();
const message: MessageAttributesType = { const message = new MessageModel({
...generateMessageId(incrementMessageCounter()), ...generateMessageId(incrementMessageCounter()),
conversationId: this.id, conversationId: this.id,
type: 'conversation-merge', type: 'conversation-merge',
@ -3290,19 +3279,12 @@ export class ConversationModel extends window.Backbone
readStatus: ReadStatus.Read, readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Unseen, seenStatus: SeenStatus.Unseen,
schemaVersion: Message.VERSION_NEEDED_FOR_DISPLAY, schemaVersion: Message.VERSION_NEEDED_FOR_DISPLAY,
};
await DataWriter.saveMessage(message, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
forceSave: true,
}); });
window.MessageCache.__DEPRECATED$register(
message.id,
message,
'addConversationMerge'
);
this.trigger('newmessage', message); await window.MessageCache.saveMessage(message, { forceSave: true });
window.MessageCache.register(message);
drop(this.onNewMessage(message));
} }
async addPhoneNumberDiscoveryIfNeeded(originalPni: PniString): Promise<void> { async addPhoneNumberDiscoveryIfNeeded(originalPni: PniString): Promise<void> {
@ -3325,7 +3307,7 @@ export class ConversationModel extends window.Backbone
log.info(`${logId}: adding notification`); log.info(`${logId}: adding notification`);
const timestamp = Date.now(); const timestamp = Date.now();
const message: MessageAttributesType = { const message = new MessageModel({
...generateMessageId(incrementMessageCounter()), ...generateMessageId(incrementMessageCounter()),
conversationId: this.id, conversationId: this.id,
type: 'phone-number-discovery', type: 'phone-number-discovery',
@ -3338,19 +3320,12 @@ export class ConversationModel extends window.Backbone
readStatus: ReadStatus.Read, readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Unseen, seenStatus: SeenStatus.Unseen,
schemaVersion: Message.VERSION_NEEDED_FOR_DISPLAY, schemaVersion: Message.VERSION_NEEDED_FOR_DISPLAY,
};
await DataWriter.saveMessage(message, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
forceSave: true,
}); });
window.MessageCache.__DEPRECATED$register(
message.id,
message,
'addPhoneNumberDiscoveryIfNeeded'
);
this.trigger('newmessage', message); await window.MessageCache.saveMessage(message, { forceSave: true });
window.MessageCache.register(message);
drop(this.onNewMessage(message));
} }
async addVerifiedChange( async addVerifiedChange(
@ -3373,7 +3348,7 @@ export class ConversationModel extends window.Backbone
); );
const timestamp = Date.now(); const timestamp = Date.now();
const message: MessageAttributesType = { const message = new MessageModel({
...generateMessageId(incrementMessageCounter()), ...generateMessageId(incrementMessageCounter()),
conversationId: this.id, conversationId: this.id,
local: Boolean(options.local), local: Boolean(options.local),
@ -3385,19 +3360,12 @@ export class ConversationModel extends window.Backbone
type: 'verified-change', type: 'verified-change',
verified, verified,
verifiedChanged: verifiedChangeId, verifiedChanged: verifiedChangeId,
};
await DataWriter.saveMessage(message, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
forceSave: true,
}); });
window.MessageCache.__DEPRECATED$register(
message.id,
message,
'addVerifiedChange'
);
this.trigger('newmessage', message); await window.MessageCache.saveMessage(message, { forceSave: true });
window.MessageCache.register(message);
drop(this.onNewMessage(message));
drop(this.updateUnread()); drop(this.updateUnread());
const serviceId = this.getServiceId(); const serviceId = this.getServiceId();
@ -3417,7 +3385,7 @@ export class ConversationModel extends window.Backbone
conversationId?: string conversationId?: string
): Promise<void> { ): Promise<void> {
const now = Date.now(); const now = Date.now();
const message: MessageAttributesType = { const message = new MessageModel({
...generateMessageId(incrementMessageCounter()), ...generateMessageId(incrementMessageCounter()),
conversationId: this.id, conversationId: this.id,
type: 'profile-change', type: 'profile-change',
@ -3428,18 +3396,12 @@ export class ConversationModel extends window.Backbone
timestamp: now, timestamp: now,
changedId: conversationId || this.id, changedId: conversationId || this.id,
profileChange, profileChange,
};
await DataWriter.saveMessage(message, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
}); });
window.MessageCache.__DEPRECATED$register(
message.id,
message,
'addProfileChange'
);
this.trigger('newmessage', message); await window.MessageCache.saveMessage(message, { forceSave: true });
window.MessageCache.register(message);
drop(this.onNewMessage(message));
const serviceId = this.getServiceId(); const serviceId = this.getServiceId();
if (isDirectConversation(this.attributes) && serviceId) { if (isDirectConversation(this.attributes) && serviceId) {
@ -3460,7 +3422,7 @@ export class ConversationModel extends window.Backbone
extra: Partial<MessageAttributesType> = {} extra: Partial<MessageAttributesType> = {}
): Promise<string> { ): Promise<string> {
const now = Date.now(); const now = Date.now();
const message: MessageAttributesType = { const message = new MessageModel({
...generateMessageId(incrementMessageCounter()), ...generateMessageId(incrementMessageCounter()),
conversationId: this.id, conversationId: this.id,
type, type,
@ -3472,18 +3434,12 @@ export class ConversationModel extends window.Backbone
seenStatus: SeenStatus.NotApplicable, seenStatus: SeenStatus.NotApplicable,
...extra, ...extra,
};
await DataWriter.saveMessage(message, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
}); });
window.MessageCache.__DEPRECATED$register(
message.id,
message as MessageAttributesType,
'addNotification'
);
this.trigger('newmessage', message); await window.MessageCache.saveMessage(message, { forceSave: true });
window.MessageCache.register(message);
drop(this.onNewMessage(message));
return message.id; return message.id;
} }
@ -3561,13 +3517,10 @@ export class ConversationModel extends window.Backbone
`maybeRemoveUniversalTimer(${this.idForLogging()}): removed notification` `maybeRemoveUniversalTimer(${this.idForLogging()}): removed notification`
); );
const message = window.MessageCache.__DEPRECATED$getById( const message = window.MessageCache.getById(notificationId);
notificationId,
'maybeRemoveUniversalTimer'
);
if (message) { if (message) {
await DataWriter.removeMessage(message.id, { await DataWriter.removeMessage(message.id, {
singleProtoJobQueue, cleanupMessages,
}); });
} }
return true; return true;
@ -3607,13 +3560,10 @@ export class ConversationModel extends window.Backbone
`maybeClearContactRemoved(${this.idForLogging()}): removed notification` `maybeClearContactRemoved(${this.idForLogging()}): removed notification`
); );
const message = window.MessageCache.__DEPRECATED$getById( const message = window.MessageCache.getById(notificationId);
notificationId,
'maybeClearContactRemoved'
);
if (message) { if (message) {
await DataWriter.removeMessage(message.id, { await DataWriter.removeMessage(message.id, {
singleProtoJobQueue, cleanupMessages,
}); });
} }
@ -4164,16 +4114,12 @@ export class ConversationModel extends window.Backbone
storyId, storyId,
}); });
window.MessageCache.__DEPRECATED$register( const model = window.MessageCache.register(new MessageModel(attributes));
attributes.id,
attributes,
'enqueueMessageForSend'
);
const dbStart = Date.now(); const dbStart = Date.now();
strictAssert( strictAssert(
typeof attributes.timestamp === 'number', typeof model.get('timestamp') === 'number',
'Expected a timestamp' 'Expected a timestamp'
); );
@ -4186,17 +4132,16 @@ export class ConversationModel extends window.Backbone
{ {
type: conversationQueueJobEnum.enum.NormalMessage, type: conversationQueueJobEnum.enum.NormalMessage,
conversationId: this.id, conversationId: this.id,
messageId: attributes.id, messageId: model.id,
revision: this.get('revision'), revision: this.get('revision'),
}, },
async jobToInsert => { async jobToInsert => {
log.info( log.info(
`enqueueMessageForSend: saving message ${attributes.id} and job ${jobToInsert.id}` `enqueueMessageForSend: saving message ${model.id} and job ${jobToInsert.id}`
); );
await DataWriter.saveMessage(attributes, { await window.MessageCache.saveMessage(model, {
jobToInsert, jobToInsert,
forceSave: true, forceSave: true,
ourAci: window.textsecure.storage.user.getCheckedAci(),
}); });
} }
); );
@ -4212,14 +4157,14 @@ export class ConversationModel extends window.Backbone
const renderStart = Date.now(); const renderStart = Date.now();
// Perform asynchronous tasks before entering the batching mode // Perform asynchronous tasks before entering the batching mode
await this.beforeAddSingleMessage(attributes); await this.beforeAddSingleMessage(model.attributes);
if (sticker) { if (sticker) {
await addStickerPackReference(attributes.id, sticker.packId); await addStickerPackReference(model.id, sticker.packId);
} }
this.beforeMessageSend({ this.beforeMessageSend({
message: attributes, message: model.attributes,
dontClearDraft, dontClearDraft,
dontAddMessage: false, dontAddMessage: false,
now, now,
@ -4364,21 +4309,27 @@ export class ConversationModel extends window.Backbone
) )
); );
let { preview, activity } = stats; const { preview: previewAttributes, activity: activityAttributes } = stats;
let preview: MessageModel | undefined;
let activity: MessageModel | undefined;
// Get the in-memory message from MessageCache so that if it already exists // Get the in-memory message from MessageCache so that if it already exists
// in memory we use that data instead of the data from the db which may // in memory we use that data instead of the data from the db which may
// be out of date. // be out of date.
if (preview) { if (previewAttributes) {
const inMemory = window.MessageCache.accessAttributes(preview.id); preview = window.MessageCache.register(
preview = inMemory || preview; new MessageModel(previewAttributes)
preview = (await this.cleanAttributes([preview]))?.[0] || preview; );
const updates = (await this.cleanAttributes([preview.attributes]))?.[0];
preview.set(updates);
} }
if (activity) { if (activityAttributes) {
const inMemory = window.MessageCache.accessAttributes(activity.id); activity = window.MessageCache.register(
activity = inMemory || activity; new MessageModel(activityAttributes)
activity = (await this.cleanAttributes([activity]))?.[0] || activity; );
const updates = (await this.cleanAttributes([activity.attributes]))?.[0];
activity.set(updates);
} }
if ( if (
@ -4386,7 +4337,7 @@ export class ConversationModel extends window.Backbone
this.get('draftTimestamp') && this.get('draftTimestamp') &&
(!preview || (!preview ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
preview.sent_at < this.get('draftTimestamp')!) preview.get('sent_at') < this.get('draftTimestamp')!)
) { ) {
return; return;
} }
@ -4395,36 +4346,43 @@ export class ConversationModel extends window.Backbone
let lastMessageReceivedAt = this.get('lastMessageReceivedAt'); let lastMessageReceivedAt = this.get('lastMessageReceivedAt');
let lastMessageReceivedAtMs = this.get('lastMessageReceivedAtMs'); let lastMessageReceivedAtMs = this.get('lastMessageReceivedAtMs');
if (activity) { if (activity) {
const { callId } = activity; const { callId } = activity.attributes;
const callHistory = callId const callHistory = callId
? getCallHistorySelector(window.reduxStore.getState())(callId) ? getCallHistorySelector(window.reduxStore.getState())(callId)
: undefined; : undefined;
timestamp = callHistory?.timestamp || activity.sent_at || timestamp; timestamp =
lastMessageReceivedAt = activity.received_at || lastMessageReceivedAt; callHistory?.timestamp || activity.get('sent_at') || timestamp;
lastMessageReceivedAt =
activity.get('received_at') || lastMessageReceivedAt;
lastMessageReceivedAtMs = lastMessageReceivedAtMs =
activity.received_at_ms || lastMessageReceivedAtMs; activity.get('received_at_ms') || lastMessageReceivedAtMs;
} }
const notificationData = preview const notificationData = preview
? getNotificationDataForMessage(preview) ? getNotificationDataForMessage(preview.attributes)
: undefined; : undefined;
this.set({ this.set({
lastMessage: lastMessage:
notificationData?.text || notificationData?.text ||
(preview ? getNotificationTextForMessage(preview) : undefined) || (preview
? getNotificationTextForMessage(preview.attributes)
: undefined) ||
'', '',
lastMessageBodyRanges: notificationData?.bodyRanges, lastMessageBodyRanges: notificationData?.bodyRanges,
lastMessagePrefix: notificationData?.emoji, lastMessagePrefix: notificationData?.emoji,
lastMessageAuthor: getMessageAuthorText(preview), lastMessageAuthor: preview
lastMessageStatus: ? getMessageAuthorText(preview.attributes)
(preview ? getMessagePropStatus(preview, ourConversationId) : null) || : undefined,
null, lastMessageStatus: preview
? getMessagePropStatus(preview.attributes, ourConversationId)
: undefined,
lastMessageReceivedAt, lastMessageReceivedAt,
lastMessageReceivedAtMs, lastMessageReceivedAtMs,
timestamp, timestamp,
lastMessageDeletedForEveryone: preview?.deletedForEveryone || false, lastMessageDeletedForEveryone:
preview?.get('deletedForEveryone') || false,
}); });
await DataWriter.updateConversation(this.attributes); await DataWriter.updateConversation(this.attributes);
@ -4785,7 +4743,7 @@ export class ConversationModel extends window.Backbone
(isInitialSync && isFromSyncOperation) || isFromMe || isNoteToSelf; (isInitialSync && isFromSyncOperation) || isFromMe || isNoteToSelf;
const counter = receivedAt ?? incrementMessageCounter(); const counter = receivedAt ?? incrementMessageCounter();
const attributes = { const message = new MessageModel({
...generateMessageId(counter), ...generateMessageId(counter),
conversationId: this.id, conversationId: this.id,
expirationTimerUpdate: { expirationTimerUpdate: {
@ -4801,24 +4759,18 @@ export class ConversationModel extends window.Backbone
sent_at: sentAt, sent_at: sentAt,
timestamp: sentAt, timestamp: sentAt,
type: 'timer-notification' as const, type: 'timer-notification' as const,
};
await DataWriter.saveMessage(attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
forceSave: true,
}); });
window.MessageCache.__DEPRECATED$register( await window.MessageCache.saveMessage(message, {
attributes.id, forceSave: true,
attributes, });
'updateExpirationTimer' window.MessageCache.register(message);
);
void this.addSingleMessage(attributes); void this.addSingleMessage(message.attributes);
void this.updateUnread(); void this.updateUnread();
log.info( log.info(
`${logId}: added a notification received_at=${attributes.received_at}` `${logId}: added a notification received_at=${message.get('received_at')}`
); );
} }
@ -5306,9 +5258,9 @@ export class ConversationModel extends window.Backbone
log.info(`${logId}: Starting delete`); log.info(`${logId}: Starting delete`);
await DataWriter.removeMessagesInConversation(this.id, { await DataWriter.removeMessagesInConversation(this.id, {
cleanupMessages,
fromSync: source !== 'local-delete-sync', fromSync: source !== 'local-delete-sync',
logId: this.idForLogging(), logId: this.idForLogging(),
singleProtoJobQueue,
}); });
log.info(`${logId}: Delete complete`); log.info(`${logId}: Delete complete`);
} }

File diff suppressed because it is too large Load diff

View file

@ -5,10 +5,13 @@ import noop from 'lodash/noop';
import { v7 as generateUuid } from 'uuid'; import { v7 as generateUuid } from 'uuid';
import { DataWriter } from '../sql/Client'; import { DataWriter } from '../sql/Client';
import type { MessageModel } from '../models/messages'; import { MessageModel } from '../models/messages';
import type { ReactionAttributesType } from '../messageModifiers/Reactions'; import {
handleReaction,
type ReactionAttributesType,
} from '../messageModifiers/Reactions';
import { ReactionSource } from './ReactionSource'; import { ReactionSource } from './ReactionSource';
import { __DEPRECATED$getMessageById } from '../messages/getMessageById'; import { getMessageById } from '../messages/getMessageById';
import { getSourceServiceId, isStory } from '../messages/helpers'; import { getSourceServiceId, isStory } from '../messages/helpers';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
import { isDirectConversation } from '../util/whatTypeOfConversation'; import { isDirectConversation } from '../util/whatTypeOfConversation';
@ -19,6 +22,7 @@ import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
import { isAciString } from '../util/isAciString'; import { isAciString } from '../util/isAciString';
import { SendStatus } from '../messages/MessageSendState'; import { SendStatus } from '../messages/MessageSendState';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { getMessageIdForLogging } from '../util/idForLogging';
export async function enqueueReactionForSend({ export async function enqueueReactionForSend({
emoji, emoji,
@ -29,20 +33,17 @@ export async function enqueueReactionForSend({
messageId: string; messageId: string;
remove: boolean; remove: boolean;
}>): Promise<void> { }>): Promise<void> {
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'enqueueReactionForSend'
);
strictAssert(message, 'enqueueReactionForSend: no message found'); strictAssert(message, 'enqueueReactionForSend: no message found');
const targetAuthorAci = getSourceServiceId(message.attributes); const targetAuthorAci = getSourceServiceId(message.attributes);
strictAssert( strictAssert(
targetAuthorAci, targetAuthorAci,
`enqueueReactionForSend: message ${message.idForLogging()} had no source UUID` `enqueueReactionForSend: message ${getMessageIdForLogging(message.attributes)} had no source UUID`
); );
strictAssert( strictAssert(
isAciString(targetAuthorAci), isAciString(targetAuthorAci),
`enqueueReactionForSend: message ${message.idForLogging()} had no source ACI` `enqueueReactionForSend: message ${getMessageIdForLogging(message.attributes)} had no source ACI`
); );
const targetTimestamp = getMessageSentTimestamp(message.attributes, { const targetTimestamp = getMessageSentTimestamp(message.attributes, {
@ -50,11 +51,13 @@ export async function enqueueReactionForSend({
}); });
strictAssert( strictAssert(
targetTimestamp, targetTimestamp,
`enqueueReactionForSend: message ${message.idForLogging()} had no timestamp` `enqueueReactionForSend: message ${getMessageIdForLogging(message.attributes)} had no timestamp`
); );
const timestamp = Date.now(); const timestamp = Date.now();
const messageConversation = message.getConversation(); const messageConversation = window.ConversationController.get(
message.get('conversationId')
);
strictAssert( strictAssert(
messageConversation, messageConversation,
'enqueueReactionForSend: No conversation extracted from target message' 'enqueueReactionForSend: No conversation extracted from target message'
@ -94,7 +97,7 @@ export async function enqueueReactionForSend({
// Only used in story scenarios, where we use a whole message to represent the reaction // Only used in story scenarios, where we use a whole message to represent the reaction
let storyReactionMessage: MessageModel | undefined; let storyReactionMessage: MessageModel | undefined;
if (storyMessage) { if (storyMessage) {
storyReactionMessage = new window.Whisper.Message({ storyReactionMessage = new MessageModel({
...generateMessageId(incrementMessageCounter()), ...generateMessageId(incrementMessageCounter()),
type: 'outgoing', type: 'outgoing',
conversationId: targetConversation.id, conversationId: targetConversation.id,
@ -132,5 +135,5 @@ export async function enqueueReactionForSend({
timestamp, timestamp,
}; };
await message.handleReaction(reaction, { storyMessage }); await handleReaction(message, reaction, { storyMessage });
} }

View file

@ -1,63 +1,124 @@
// Copyright 2019 Signal Messenger, LLC // Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import cloneDeep from 'lodash/cloneDeep';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { LRUCache } from 'lru-cache'; import { LRUCache } from 'lru-cache';
import type {
MessageAttributesType,
ReadonlyMessageAttributesType,
} from '../model-types.d';
import type { MessageModel } from '../models/messages';
import { DataReader, DataWriter } from '../sql/Client';
import * as Errors from '../types/errors';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { getEnvironment, Environment } from '../environment'; import { MessageModel } from '../models/messages';
import { DataReader, DataWriter } from '../sql/Client';
import { getMessageConversation } from '../util/getMessageConversation'; import { getMessageConversation } from '../util/getMessageConversation';
import { getMessageModelLogger } from '../util/MessageModelLogger';
import { getSenderIdentifier } from '../util/getSenderIdentifier'; import { getSenderIdentifier } from '../util/getSenderIdentifier';
import { isNotNil } from '../util/isNotNil'; import { isNotNil } from '../util/isNotNil';
import { softAssert, strictAssert } from '../util/assert';
import { isStory } from '../messages/helpers'; import { isStory } from '../messages/helpers';
import type { SendStateByConversationId } from '../messages/MessageSendState';
import { getStoryDataFromMessageAttributes } from './storyLoader'; import { getStoryDataFromMessageAttributes } from './storyLoader';
import { postSaveUpdates } from '../util/cleanup';
import type { MessageAttributesType } from '../model-types.d';
import type { SendStateByConversationId } from '../messages/MessageSendState';
import type { StoredJob } from '../jobs/types';
const MAX_THROTTLED_REDUX_UPDATERS = 200; const MAX_THROTTLED_REDUX_UPDATERS = 200;
export class MessageCache { export class MessageCache {
static install(): MessageCache {
const instance = new MessageCache();
window.MessageCache = instance;
return instance;
}
private state = { private state = {
messages: new Map<string, MessageAttributesType>(), messages: new Map<string, MessageModel>(),
messageIdsBySender: new Map<string, string>(), messageIdsBySender: new Map<string, string>(),
messageIdsBySentAt: new Map<number, Array<string>>(), messageIdsBySentAt: new Map<number, Array<string>>(),
lastAccessedAt: new Map<string, number>(), lastAccessedAt: new Map<string, number>(),
}; };
// Stores the models so that __DEPRECATED$register always returns the existing public saveMessage(
// copy instead of a new model. message: MessageAttributesType | MessageModel,
private modelCache = new Map<string, MessageModel>(); options?: {
forceSave?: boolean;
jobToInsert?: Readonly<StoredJob>;
}
): Promise<string> {
const attributes =
message instanceof MessageModel ? message.attributes : message;
// Synchronously access a message's attributes from internal cache. Will return DataWriter.saveMessage(attributes, {
// return undefined if the message does not exist in memory. ourAci: window.textsecure.storage.user.getCheckedAci(),
public accessAttributes( postSaveUpdates,
messageId: string ...options,
): Readonly<MessageAttributesType> | undefined { });
const messageAttributes = this.state.messages.get(messageId);
return messageAttributes
? this.freezeAttributes(messageAttributes)
: undefined;
} }
// Synchronously access a message's attributes from internal cache. Throws public register(message: MessageModel): MessageModel {
// if the message does not exist in memory. if (!message || !message.id) {
public accessAttributesOrThrow( throw new Error('MessageCache.register: Got falsey id or message');
source: string, }
messageId: string
): Readonly<MessageAttributesType> { const existing = this.getById(message.id);
const messageAttributes = this.accessAttributes(messageId); if (existing) {
strictAssert( return existing;
messageAttributes, }
`MessageCache.accessAttributesOrThrow/${source}: no message for id ${messageId}`
); this.addMessageToCache(message);
return messageAttributes;
return message;
}
// Finds a message in the cache by sender identifier
public findBySender(senderIdentifier: string): MessageModel | undefined {
const id = this.state.messageIdsBySender.get(senderIdentifier);
if (!id) {
return undefined;
}
return this.getById(id);
}
// Finds a message in the cache by Id
public getById(id: string): MessageModel | undefined {
const message = this.state.messages.get(id);
if (!message) {
return undefined;
}
this.state.lastAccessedAt.set(id, Date.now());
return message;
}
// Finds a message in the cache by sentAt/timestamp
public async findBySentAt(
sentAt: number,
predicate: (model: MessageModel) => boolean
): Promise<MessageModel | undefined> {
const items = this.state.messageIdsBySentAt.get(sentAt) ?? [];
const inMemory = items
.map(id => this.getById(id))
.filter(isNotNil)
.find(predicate);
if (inMemory != null) {
return inMemory;
}
log.info(`findBySentAt(${sentAt}): db lookup needed`);
const allOnDisk = await DataReader.getMessagesBySentAt(sentAt);
const onDisk = allOnDisk
.map(message => this.register(new MessageModel(message)))
.find(predicate);
return onDisk;
}
// Deletes the message from our cache
public unregister(id: string): void {
const message = this.state.messages.get(id);
if (!message) {
return;
}
this.removeMessage(id);
} }
// Evicts messages from the message cache if they have not been accessed past // Evicts messages from the message cache if they have not been accessed past
@ -65,9 +126,9 @@ export class MessageCache {
public deleteExpiredMessages(expiryTime: number): void { public deleteExpiredMessages(expiryTime: number): void {
const now = Date.now(); const now = Date.now();
for (const [messageId, messageAttributes] of this.state.messages) { for (const [messageId, message] of this.state.messages) {
const timeLastAccessed = this.state.lastAccessedAt.get(messageId) ?? 0; const timeLastAccessed = this.state.lastAccessedAt.get(messageId) ?? 0;
const conversation = getMessageConversation(messageAttributes); const conversation = getMessageConversation(message.attributes);
const state = window.reduxStore.getState(); const state = window.reduxStore.getState();
const selectedId = state?.conversations?.selectedConversationId; const selectedId = state?.conversations?.selectedConversationId;
@ -75,21 +136,25 @@ export class MessageCache {
conversation && selectedId && conversation.id === selectedId; conversation && selectedId && conversation.id === selectedId;
if (now - timeLastAccessed > expiryTime && !inActiveConversation) { if (now - timeLastAccessed > expiryTime && !inActiveConversation) {
this.__DEPRECATED$unregister(messageId); this.unregister(messageId);
} }
} }
} }
// Finds a message in the cache by sender identifier public async upgradeSchema(
public findBySender( message: MessageModel,
senderIdentifier: string minSchemaVersion: number
): Readonly<MessageAttributesType> | undefined { ): Promise<void> {
const id = this.state.messageIdsBySender.get(senderIdentifier); const { schemaVersion } = message.attributes;
if (!id) { if (!schemaVersion || schemaVersion >= minSchemaVersion) {
return undefined; return;
}
const startingAttributes = message.attributes;
const upgradedAttributes =
await window.Signal.Migrations.upgradeMessageSchema(startingAttributes);
if (startingAttributes !== upgradedAttributes) {
message.set(upgradedAttributes);
} }
return this.accessAttributes(id);
} }
public replaceAllObsoleteConversationIds({ public replaceAllObsoleteConversationIds({
@ -112,12 +177,12 @@ export class MessageCache {
}; };
}; };
for (const [messageId, messageAttributes] of this.state.messages) { for (const [, message] of this.state.messages) {
if (messageAttributes.conversationId !== obsoleteId) { if (message.get('conversationId') !== obsoleteId) {
continue; continue;
} }
const editHistory = messageAttributes.editHistory?.map(history => { const editHistory = message.get('editHistory')?.map(history => {
return { return {
...history, ...history,
sendStateByConversationId: updateSendState( sendStateByConversationId: updateSendState(
@ -126,117 +191,33 @@ export class MessageCache {
}; };
}); });
this.setAttributes({ message.set({
messageId, conversationId,
messageAttributes: { sendStateByConversationId: updateSendState(
conversationId, message.get('sendStateByConversationId')
sendStateByConversationId: updateSendState( ),
messageAttributes.sendStateByConversationId editHistory,
),
editHistory,
},
skipSaveToDatabase: true,
}); });
} }
} }
// Find the message's attributes whether in memory or in the database. // Semi-public API
// Refresh the attributes in the cache if they exist. Throw if we cannot find
// a matching message.
public async resolveAttributes(
source: string,
messageId: string
): Promise<Readonly<MessageAttributesType>> {
const inMemoryMessageAttributes = this.accessAttributes(messageId);
if (inMemoryMessageAttributes) { // Should only be called by MessageModel's set() function
return inMemoryMessageAttributes; public _updateCaches(message: MessageModel): undefined {
} const existing = this.getById(message.id);
let messageAttributesFromDatabase: MessageAttributesType | undefined; // If this model hasn't been registered yet, we can't add to cache because we don't
try { // want to force `message` to be the primary MessageModel for this message.
messageAttributesFromDatabase = if (!existing) {
await DataReader.getMessageById(messageId); return;
} catch (err: unknown) {
log.error(
`MessageCache.resolveAttributes(${messageId}): db error ${Errors.toLogFormat(
err
)}`
);
}
strictAssert(
messageAttributesFromDatabase,
`MessageCache.resolveAttributes/${source}: no message for id ${messageId}`
);
return this.freezeAttributes(messageAttributesFromDatabase);
}
// Updates a message's attributes and saves the message to cache and to the
// database. Option to skip the save to the database.
// Overload #1: if skipSaveToDatabase = true, returns void
public setAttributes({
messageId,
messageAttributes,
skipSaveToDatabase,
}: {
messageId: string;
messageAttributes: Partial<MessageAttributesType>;
skipSaveToDatabase: true;
}): void;
// Overload #2: if skipSaveToDatabase = false, returns DB save promise
public setAttributes({
messageId,
messageAttributes,
skipSaveToDatabase,
}: {
messageId: string;
messageAttributes: Partial<MessageAttributesType>;
skipSaveToDatabase: false;
}): Promise<string>;
// Implementation
public setAttributes({
messageId,
messageAttributes: partialMessageAttributes,
skipSaveToDatabase,
}: {
messageId: string;
messageAttributes: Partial<MessageAttributesType>;
skipSaveToDatabase: boolean;
}): Promise<string> | undefined {
let messageAttributes = this.accessAttributes(messageId);
softAssert(messageAttributes, 'could not find message attributes');
if (!messageAttributes) {
// We expect message attributes to be defined in cache if one is trying to
// set new attributes. In the case that the attributes are missing in cache
// we'll add whatever we currently have to cache as a defensive measure so
// that the code continues to work properly downstream. The softAssert above
// that logs/debugger should be addressed upstream immediately by ensuring
// that message is in cache.
const partiallyCachedMessage = {
id: messageId,
...partialMessageAttributes,
} as MessageAttributesType;
this.addMessageToCache(partiallyCachedMessage);
messageAttributes = partiallyCachedMessage;
} }
this.state.messageIdsBySender.delete( this.state.messageIdsBySender.delete(
getSenderIdentifier(messageAttributes) getSenderIdentifier(message.attributes)
); );
const nextMessageAttributes = { const { id, sent_at: sentAt } = message.attributes;
...messageAttributes,
...partialMessageAttributes,
};
const { id, sent_at: sentAt } = nextMessageAttributes;
const previousIdsBySentAt = this.state.messageIdsBySentAt.get(sentAt); const previousIdsBySentAt = this.state.messageIdsBySentAt.get(sentAt);
let nextIdsBySentAtSet: Set<string>; let nextIdsBySentAtSet: Set<string>;
@ -247,44 +228,70 @@ export class MessageCache {
nextIdsBySentAtSet = new Set([id]); nextIdsBySentAtSet = new Set([id]);
} }
this.state.messages.set(id, nextMessageAttributes);
this.state.lastAccessedAt.set(id, Date.now()); this.state.lastAccessedAt.set(id, Date.now());
this.state.messageIdsBySender.set( this.state.messageIdsBySender.set(
getSenderIdentifier(messageAttributes), getSenderIdentifier(message.attributes),
id id
); );
this.markModelStale(nextMessageAttributes); this.throttledUpdateRedux(message.attributes);
}
this.throttledUpdateRedux(nextMessageAttributes); // Helpers
if (skipSaveToDatabase) { private addMessageToCache(message: MessageModel): void {
if (!message.id) {
return; return;
} }
return DataWriter.saveMessage(nextMessageAttributes, { if (this.state.messages.has(message.id)) {
ourAci: window.textsecure.storage.user.getCheckedAci(), this.state.lastAccessedAt.set(message.id, Date.now());
}); return;
}
private throttledReduxUpdaters = new LRUCache<
string,
typeof this.updateRedux
>({
max: MAX_THROTTLED_REDUX_UPDATERS,
});
private throttledUpdateRedux(attributes: MessageAttributesType) {
let updater = this.throttledReduxUpdaters.get(attributes.id);
if (!updater) {
updater = throttle(this.updateRedux.bind(this), 200, {
leading: true,
trailing: true,
});
this.throttledReduxUpdaters.set(attributes.id, updater);
} }
updater(attributes); const { id, sent_at: sentAt } = message.attributes;
const previousIdsBySentAt = this.state.messageIdsBySentAt.get(sentAt);
let nextIdsBySentAtSet: Set<string>;
if (previousIdsBySentAt) {
nextIdsBySentAtSet = new Set(previousIdsBySentAt);
nextIdsBySentAtSet.add(id);
} else {
nextIdsBySentAtSet = new Set([id]);
}
this.state.messages.set(message.id, message);
this.state.lastAccessedAt.set(message.id, Date.now());
this.state.messageIdsBySentAt.set(sentAt, Array.from(nextIdsBySentAtSet));
this.state.messageIdsBySender.set(
getSenderIdentifier(message.attributes),
id
);
}
private removeMessage(messageId: string): void {
const message = this.state.messages.get(messageId);
if (!message) {
return;
}
const { id, sent_at: sentAt } = message.attributes;
const nextIdsBySentAtSet =
new Set(this.state.messageIdsBySentAt.get(sentAt)) || new Set();
nextIdsBySentAtSet.delete(id);
if (nextIdsBySentAtSet.size) {
this.state.messageIdsBySentAt.set(sentAt, Array.from(nextIdsBySentAtSet));
} else {
this.state.messageIdsBySentAt.delete(sentAt);
}
this.state.messages.delete(messageId);
this.state.lastAccessedAt.delete(messageId);
this.state.messageIdsBySender.delete(
getSenderIdentifier(message.attributes)
);
} }
private updateRedux(attributes: MessageAttributesType) { private updateRedux(attributes: MessageAttributesType) {
@ -313,238 +320,23 @@ export class MessageCache {
); );
} }
// When you already have the message attributes from the db and want to private throttledReduxUpdaters = new LRUCache<
// ensure that they're added to the cache. The latest attributes from cache string,
// are returned if they exist, if not the attributes passed in are returned. typeof this.updateRedux
public toMessageAttributes( >({
messageAttributes: MessageAttributesType max: MAX_THROTTLED_REDUX_UPDATERS,
): Readonly<MessageAttributesType> { });
this.addMessageToCache(messageAttributes);
const nextMessageAttributes = this.state.messages.get(messageAttributes.id); private throttledUpdateRedux(attributes: MessageAttributesType) {
strictAssert( let updater = this.throttledReduxUpdaters.get(attributes.id);
nextMessageAttributes, if (!updater) {
`MessageCache.toMessageAttributes: no message for id ${messageAttributes.id}` updater = throttle(this.updateRedux.bind(this), 200, {
); leading: true,
trailing: true,
if (getEnvironment() === Environment.Development) {
return Object.freeze(cloneDeep(nextMessageAttributes));
}
return nextMessageAttributes;
}
static install(): MessageCache {
const instance = new MessageCache();
window.MessageCache = instance;
return instance;
}
private addMessageToCache(messageAttributes: MessageAttributesType): void {
if (!messageAttributes.id) {
return;
}
if (this.state.messages.has(messageAttributes.id)) {
this.state.lastAccessedAt.set(messageAttributes.id, Date.now());
return;
}
const { id, sent_at: sentAt } = messageAttributes;
const previousIdsBySentAt = this.state.messageIdsBySentAt.get(sentAt);
let nextIdsBySentAtSet: Set<string>;
if (previousIdsBySentAt) {
nextIdsBySentAtSet = new Set(previousIdsBySentAt);
nextIdsBySentAtSet.add(id);
} else {
nextIdsBySentAtSet = new Set([id]);
}
this.state.messages.set(messageAttributes.id, { ...messageAttributes });
this.state.lastAccessedAt.set(messageAttributes.id, Date.now());
this.state.messageIdsBySentAt.set(sentAt, Array.from(nextIdsBySentAtSet));
this.state.messageIdsBySender.set(
getSenderIdentifier(messageAttributes),
id
);
}
private freezeAttributes(
messageAttributes: MessageAttributesType
): Readonly<MessageAttributesType> {
this.addMessageToCache(messageAttributes);
if (getEnvironment() === Environment.Development) {
return Object.freeze(cloneDeep(messageAttributes));
}
return messageAttributes;
}
private removeMessage(messageId: string): void {
const messageAttributes = this.state.messages.get(messageId);
if (!messageAttributes) {
return;
}
const { id, sent_at: sentAt } = messageAttributes;
const nextIdsBySentAtSet =
new Set(this.state.messageIdsBySentAt.get(sentAt)) || new Set();
nextIdsBySentAtSet.delete(id);
if (nextIdsBySentAtSet.size) {
this.state.messageIdsBySentAt.set(sentAt, Array.from(nextIdsBySentAtSet));
} else {
this.state.messageIdsBySentAt.delete(sentAt);
}
this.state.messages.delete(messageId);
this.state.lastAccessedAt.delete(messageId);
this.state.messageIdsBySender.delete(
getSenderIdentifier(messageAttributes)
);
}
// Deprecated methods below
// Adds the message into the cache and eturns a Proxy that resembles
// a MessageModel
public __DEPRECATED$register(
id: string,
data: MessageModel | MessageAttributesType,
location: string
): MessageModel {
if (!id || !data) {
throw new Error(
'MessageCache.__DEPRECATED$register: Got falsey id or message'
);
}
const existing = this.__DEPRECATED$getById(id, location);
if (existing) {
this.addMessageToCache(existing.attributes);
return existing;
}
const modelProxy = this.toModel(data);
const messageAttributes = 'attributes' in data ? data.attributes : data;
this.addMessageToCache(messageAttributes);
modelProxy.registerLocations.add(location);
return modelProxy;
}
// Deletes the message from our cache
public __DEPRECATED$unregister(id: string): void {
const model = this.modelCache.get(id);
if (!model) {
return;
}
this.removeMessage(id);
this.modelCache.delete(id);
}
// Finds a message in the cache by Id
public __DEPRECATED$getById(
id: string,
location: string
): MessageModel | undefined {
const data = this.state.messages.get(id);
if (!data) {
return undefined;
}
const model = this.toModel(data);
model.registerLocations.add(location);
return model;
}
public async upgradeSchema(
attributes: MessageAttributesType,
minSchemaVersion: number
): Promise<MessageAttributesType> {
const { schemaVersion } = attributes;
if (!schemaVersion || schemaVersion >= minSchemaVersion) {
return attributes;
}
const upgradedAttributes =
await window.Signal.Migrations.upgradeMessageSchema(attributes);
await this.setAttributes({
messageId: upgradedAttributes.id,
messageAttributes: upgradedAttributes,
skipSaveToDatabase: false,
});
return upgradedAttributes;
}
// Finds a message in the cache by sentAt/timestamp
public async findBySentAt(
sentAt: number,
predicate: (attributes: ReadonlyMessageAttributesType) => boolean
): Promise<MessageAttributesType | undefined> {
const items = this.state.messageIdsBySentAt.get(sentAt) ?? [];
const inMemory = items
.map(id => this.accessAttributes(id))
.filter(isNotNil)
.find(predicate);
if (inMemory != null) {
return inMemory;
}
log.info(`findBySentAt(${sentAt}): db lookup needed`);
const allOnDisk = await DataReader.getMessagesBySentAt(sentAt);
const onDisk = allOnDisk.find(predicate);
if (onDisk != null) {
this.addMessageToCache(onDisk);
}
return onDisk;
}
// Marks cached model as "should be stale" to discourage continued use.
// The model's attributes are directly updated so that the model is in sync
// with the in-memory attributes.
private markModelStale(messageAttributes: MessageAttributesType): void {
const { id } = messageAttributes;
const model = this.modelCache.get(id);
if (!model) {
return;
}
model.attributes = { ...messageAttributes };
if (getEnvironment() === Environment.Development) {
log.warn('MessageCache: updating cached backbone model', {
cid: model.cid,
locations: Array.from(model.registerLocations).join(', '),
}); });
} this.throttledReduxUpdaters.set(attributes.id, updater);
}
// Creates a proxy object for MessageModel which logs usage in development
// so that we're able to migrate off of models
private toModel(
messageAttributes: MessageAttributesType | MessageModel
): MessageModel {
const existingModel = this.modelCache.get(messageAttributes.id);
if (existingModel) {
return existingModel;
} }
const model = updater(attributes);
'attributes' in messageAttributes
? messageAttributes
: new window.Whisper.Message(messageAttributes);
const proxy = getMessageModelLogger(model);
this.modelCache.set(messageAttributes.id, proxy);
return proxy;
} }
} }

View file

@ -2,10 +2,19 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { MessageAttributesType } from '../model-types.d'; import type { MessageAttributesType } from '../model-types.d';
import type { MessageModel } from '../models/messages';
import { ReadStatus, maxReadStatus } from '../messages/MessageReadStatus'; import { ReadStatus, maxReadStatus } from '../messages/MessageReadStatus';
import { notificationService } from './notifications'; import { notificationService } from './notifications';
import { SeenStatus } from '../MessageSeenStatus'; import { SeenStatus } from '../MessageSeenStatus';
import { queueUpdateMessage } from '../util/messageBatcher'; import { queueUpdateMessage } from '../util/messageBatcher';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
import { isValidTapToView } from '../util/isValidTapToView';
import { getMessageIdForLogging } from '../util/idForLogging';
import { eraseMessageContents } from '../util/cleanup';
import { getSource, getSourceServiceId } from '../messages/helpers';
import { isAciString } from '../util/isAciString';
import { viewOnceOpenJobQueue } from '../jobs/viewOnceOpenJobQueue';
function markReadOrViewed( function markReadOrViewed(
messageAttrs: Readonly<MessageAttributesType>, messageAttrs: Readonly<MessageAttributesType>,
@ -54,3 +63,65 @@ export const markViewed = (
{ skipSave = false } = {} { skipSave = false } = {}
): MessageAttributesType => ): MessageAttributesType =>
markReadOrViewed(messageAttrs, ReadStatus.Viewed, viewedAt, skipSave); markReadOrViewed(messageAttrs, ReadStatus.Viewed, viewedAt, skipSave);
export async function markViewOnceMessageViewed(
message: MessageModel,
options?: {
fromSync?: boolean;
}
): Promise<void> {
const { fromSync } = options || {};
if (!isValidTapToView(message.attributes)) {
log.warn(
`markViewOnceMessageViewed: Message ${getMessageIdForLogging(message.attributes)} is not a valid tap to view message!`
);
return;
}
if (message.attributes.isErased) {
log.warn(
`markViewOnceMessageViewed: Message ${getMessageIdForLogging(message.attributes)} is already erased!`
);
return;
}
if (message.get('readStatus') !== ReadStatus.Viewed) {
message.set(markViewed(message.attributes));
}
await eraseMessageContents(message);
if (!fromSync) {
const senderE164 = getSource(message.attributes);
const senderAci = getSourceServiceId(message.attributes);
const timestamp = message.get('sent_at');
if (senderAci === undefined || !isAciString(senderAci)) {
throw new Error('markViewOnceMessageViewed: senderAci is undefined');
}
if (window.ConversationController.areWePrimaryDevice()) {
log.warn(
'markViewOnceMessageViewed: We are primary device; not sending view once open sync'
);
return;
}
try {
await viewOnceOpenJobQueue.add({
viewOnceOpens: [
{
senderE164,
senderAci,
timestamp,
},
],
});
} catch (error) {
log.error(
'markViewOnceMessageViewed: Failed to queue view once open sync',
Errors.toLogFormat(error)
);
}
}
}

View file

@ -128,6 +128,7 @@ import { isAdhoc, isNightly } from '../../util/version';
import { ToastType } from '../../types/Toast'; import { ToastType } from '../../types/Toast';
import { isConversationAccepted } from '../../util/isConversationAccepted'; import { isConversationAccepted } from '../../util/isConversationAccepted';
import { saveBackupsSubscriberData } from '../../util/backupSubscriptionData'; import { saveBackupsSubscriberData } from '../../util/backupSubscriptionData';
import { postSaveUpdates } from '../../util/cleanup';
const MAX_CONCURRENCY = 10; const MAX_CONCURRENCY = 10;
@ -609,6 +610,7 @@ export class BackupImportStream extends Writable {
await DataWriter.saveMessages(batch, { await DataWriter.saveMessages(batch, {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
const attachmentDownloadJobPromises: Array<Promise<unknown>> = []; const attachmentDownloadJobPromises: Array<Promise<unknown>> = [];

View file

@ -4,22 +4,21 @@
import { batch } from 'react-redux'; import { batch } from 'react-redux';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
import { DataReader, DataWriter } from '../sql/Client'; import { DataReader, DataWriter } from '../sql/Client';
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary'; import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
import { sleep } from '../util/sleep'; import { sleep } from '../util/sleep';
import { SECOND } from '../util/durations'; import { SECOND } from '../util/durations';
import * as Errors from '../types/errors'; import { MessageModel } from '../models/messages';
import * as log from '../logging/log'; import { cleanupMessages } from '../util/cleanup';
import type { MessageModel } from '../models/messages';
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
class ExpiringMessagesDeletionService { class ExpiringMessagesDeletionService {
public update: typeof this.checkExpiringMessages; public update: typeof this.checkExpiringMessages;
private timeout?: ReturnType<typeof setTimeout>; private timeout?: ReturnType<typeof setTimeout>;
constructor(private readonly singleProtoJobQueue: SingleProtoJobQueue) { constructor() {
this.update = debounce(this.checkExpiringMessages, 1000); this.update = debounce(this.checkExpiringMessages, 1000);
} }
@ -37,17 +36,15 @@ class ExpiringMessagesDeletionService {
const inMemoryMessages: Array<MessageModel> = []; const inMemoryMessages: Array<MessageModel> = [];
messages.forEach(dbMessage => { messages.forEach(dbMessage => {
const message = window.MessageCache.__DEPRECATED$register( const message = window.MessageCache.register(
dbMessage.id, new MessageModel(dbMessage)
dbMessage,
'destroyExpiredMessages'
); );
messageIds.push(message.id); messageIds.push(message.id);
inMemoryMessages.push(message); inMemoryMessages.push(message);
}); });
await DataWriter.removeMessages(messageIds, { await DataWriter.removeMessages(messageIds, {
singleProtoJobQueue: this.singleProtoJobQueue, cleanupMessages,
}); });
batch(() => { batch(() => {
@ -57,7 +54,6 @@ class ExpiringMessagesDeletionService {
}); });
// We do this to update the UI, if this message is being displayed somewhere // We do this to update the UI, if this message is being displayed somewhere
message.trigger('expired');
window.reduxActions.conversations.messageExpired(message.id); window.reduxActions.conversations.messageExpired(message.id);
}); });
}); });
@ -114,14 +110,12 @@ class ExpiringMessagesDeletionService {
} }
} }
// Because this service is used inside of Client.ts, it can't directly reference export function initialize(): void {
// SingleProtoJobQueue. Instead of direct access, it is provided once on startup.
export function initialize(singleProtoJobQueue: SingleProtoJobQueue): void {
if (instance) { if (instance) {
log.warn('Expiring Messages Deletion service is already initialized!'); log.warn('Expiring Messages Deletion service is already initialized!');
return; return;
} }
instance = new ExpiringMessagesDeletionService(singleProtoJobQueue); instance = new ExpiringMessagesDeletionService();
} }
export async function update(): Promise<void> { export async function update(): Promise<void> {

View file

@ -26,6 +26,7 @@ import type {
ReleaseNoteResponseType, ReleaseNoteResponseType,
} from '../textsecure/WebAPI'; } from '../textsecure/WebAPI';
import type { WithRequiredProperties } from '../types/Util'; import type { WithRequiredProperties } from '../types/Util';
import { MessageModel } from '../models/messages';
const FETCH_INTERVAL = 3 * durations.DAY; const FETCH_INTERVAL = 3 * durations.DAY;
const ERROR_RETRY_DELAY = 3 * durations.HOUR; const ERROR_RETRY_DELAY = 3 * durations.HOUR;
@ -187,7 +188,7 @@ export class ReleaseNotesFetcher {
]; ];
const timestamp = Date.now() + index; const timestamp = Date.now() + index;
const message: MessageAttributesType = { const message = new MessageModel({
...generateMessageId(incrementMessageCounter()), ...generateMessageId(incrementMessageCounter()),
body: messageBody, body: messageBody,
bodyRanges, bodyRanges,
@ -201,12 +202,12 @@ export class ReleaseNotesFetcher {
sourceServiceId: signalConversation.getServiceId(), sourceServiceId: signalConversation.getServiceId(),
timestamp, timestamp,
type: 'incoming', type: 'incoming',
}; });
window.MessageCache.toMessageAttributes(message); window.MessageCache.register(message);
signalConversation.trigger('newmessage', message); drop(signalConversation.onNewMessage(message));
messages.push(message); messages.push(message.attributes);
}); });
await Promise.all( await Promise.all(

View file

@ -18,6 +18,7 @@ import { strictAssert } from '../util/assert';
import { dropNull } from '../util/dropNull'; import { dropNull } from '../util/dropNull';
import { DurationInSeconds } from '../util/durations'; import { DurationInSeconds } from '../util/durations';
import { SIGNAL_ACI } from '../types/SignalConversation'; import { SIGNAL_ACI } from '../types/SignalConversation';
import { postSaveUpdates } from '../util/cleanup';
let storyData: GetAllStoriesResultType | undefined; let storyData: GetAllStoriesResultType | undefined;
@ -174,6 +175,7 @@ async function repairUnexpiredStories(): Promise<void> {
storiesWithExpiry.map(messageAttributes => { storiesWithExpiry.map(messageAttributes => {
return DataWriter.saveMessage(messageAttributes, { return DataWriter.saveMessage(messageAttributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
}) })
); );

View file

@ -8,6 +8,9 @@ import { getMessageQueueTime } from '../util/getMessageQueueTime';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
import { toBoundedDate } from '../util/timestamp'; import { toBoundedDate } from '../util/timestamp';
import { getMessageIdForLogging } from '../util/idForLogging';
import { eraseMessageContents } from '../util/cleanup';
import { MessageModel } from '../models/messages';
async function eraseTapToViewMessages() { async function eraseTapToViewMessages() {
try { try {
@ -26,22 +29,17 @@ async function eraseTapToViewMessages() {
'Must be older than maxTimestamp' 'Must be older than maxTimestamp'
); );
const message = window.MessageCache.__DEPRECATED$register( const message = window.MessageCache.register(new MessageModel(fromDB));
fromDB.id,
fromDB,
'eraseTapToViewMessages'
);
window.SignalContext.log.info( window.SignalContext.log.info(
'eraseTapToViewMessages: erasing message contents', 'eraseTapToViewMessages: erasing message contents',
message.idForLogging() getMessageIdForLogging(message.attributes)
); );
// We do this to update the UI, if this message is being displayed somewhere // We do this to update the UI, if this message is being displayed somewhere
message.trigger('expired');
window.reduxActions.conversations.messageExpired(message.id); window.reduxActions.conversations.messageExpired(message.id);
await message.eraseContents(); await eraseMessageContents(message);
}) })
); );
} catch (error) { } catch (error) {

View file

@ -2,30 +2,34 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { ipcRenderer as ipc } from 'electron'; import { ipcRenderer as ipc } from 'electron';
import { groupBy, isTypedArray, last, map, omit } from 'lodash'; import { groupBy, isTypedArray, last, map, omit } from 'lodash';
import type { ReadonlyDeep } from 'type-fest'; import type { ReadonlyDeep } from 'type-fest';
import { deleteExternalFiles } from '../types/Conversation'; // Note: nothing imported here can come back and require Client.ts, and that includes
import { update as updateExpiringMessagesService } from '../services/expiringMessagesDeletion'; // their imports too. That circularity causes problems. Anything that would do that needs
import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService'; // to be passed in, like cleanupMessages below.
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import * as log from '../logging/log';
import * as Errors from '../types/errors';
import { deleteExternalFiles } from '../types/Conversation';
import { createBatcher } from '../util/batcher'; import { createBatcher } from '../util/batcher';
import { assertDev, softAssert } from '../util/assert'; import { assertDev, softAssert } from '../util/assert';
import { mapObjectWithSpec } from '../util/mapObjectWithSpec'; import { mapObjectWithSpec } from '../util/mapObjectWithSpec';
import type { ObjectMappingSpecType } from '../util/mapObjectWithSpec';
import { cleanDataForIpc } from './cleanDataForIpc'; import { cleanDataForIpc } from './cleanDataForIpc';
import type { AciString, ServiceIdString } from '../types/ServiceId';
import createTaskWithTimeout from '../textsecure/TaskWithTimeout'; import createTaskWithTimeout from '../textsecure/TaskWithTimeout';
import * as log from '../logging/log';
import { isValidUuid, isValidUuidV7 } from '../util/isValidUuid'; import { isValidUuid, isValidUuidV7 } from '../util/isValidUuid';
import * as Errors from '../types/errors';
import type { StoredJob } from '../jobs/types';
import { formatJobForInsert } from '../jobs/formatJobForInsert'; import { formatJobForInsert } from '../jobs/formatJobForInsert';
import { cleanupMessages } from '../util/cleanup';
import { AccessType, ipcInvoke, doShutdown, removeDB } from './channels'; import { AccessType, ipcInvoke, doShutdown, removeDB } from './channels';
import { getMessageIdForLogging } from '../util/idForLogging';
import { incrementMessageCounter } from '../util/incrementMessageCounter';
import { generateSnippetAroundMention } from '../util/search';
import { drop } from '../util/drop';
import type { ObjectMappingSpecType } from '../util/mapObjectWithSpec';
import type { AciString, ServiceIdString } from '../types/ServiceId';
import type { StoredJob } from '../jobs/types';
import type { import type {
ClientInterfaceWrap, ClientInterfaceWrap,
AdjacentMessagesByConversationOptionsType, AdjacentMessagesByConversationOptionsType,
@ -58,12 +62,8 @@ import type {
ClientOnlyReadableInterface, ClientOnlyReadableInterface,
ClientOnlyWritableInterface, ClientOnlyWritableInterface,
} from './Interface'; } from './Interface';
import { getMessageIdForLogging } from '../util/idForLogging';
import type { MessageAttributesType } from '../model-types'; import type { MessageAttributesType } from '../model-types';
import { incrementMessageCounter } from '../util/incrementMessageCounter';
import { generateSnippetAroundMention } from '../util/search';
import type { AttachmentDownloadJobType } from '../types/AttachmentDownload'; import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
const ERASE_SQL_KEY = 'erase-sql-key'; const ERASE_SQL_KEY = 'erase-sql-key';
const ERASE_ATTACHMENTS_KEY = 'erase-attachments'; const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
@ -121,6 +121,10 @@ const clientOnlyWritable: ClientOnlyWritableInterface = {
removeMessage, removeMessage,
removeMessages, removeMessages,
saveMessage,
saveMessages,
saveMessagesIndividually,
// Client-side only // Client-side only
flushUpdateConversationBatcher, flushUpdateConversationBatcher,
@ -137,17 +141,12 @@ const clientOnlyWritable: ClientOnlyWritableInterface = {
type ClientOverridesType = ClientOnlyWritableInterface & type ClientOverridesType = ClientOnlyWritableInterface &
Pick< Pick<
ClientInterfaceWrap<ServerWritableDirectInterface>, ClientInterfaceWrap<ServerWritableDirectInterface>,
| 'saveAttachmentDownloadJob' 'saveAttachmentDownloadJob' | 'updateConversations'
| 'saveMessage'
| 'saveMessages'
| 'updateConversations'
>; >;
const clientOnlyWritableOverrides: ClientOverridesType = { const clientOnlyWritableOverrides: ClientOverridesType = {
...clientOnlyWritable, ...clientOnlyWritable,
saveAttachmentDownloadJob, saveAttachmentDownloadJob,
saveMessage,
saveMessages,
updateConversations, updateConversations,
}; };
@ -595,41 +594,76 @@ async function searchMessages({
async function saveMessage( async function saveMessage(
data: ReadonlyDeep<MessageType>, data: ReadonlyDeep<MessageType>,
options: { {
jobToInsert?: Readonly<StoredJob>; forceSave,
jobToInsert,
ourAci,
postSaveUpdates,
}: {
forceSave?: boolean; forceSave?: boolean;
jobToInsert?: Readonly<StoredJob>;
ourAci: AciString; ourAci: AciString;
postSaveUpdates: () => Promise<void>;
} }
): Promise<string> { ): Promise<string> {
const id = await writableChannel.saveMessage(_cleanMessageData(data), { const id = await writableChannel.saveMessage(_cleanMessageData(data), {
...options, forceSave,
jobToInsert: options.jobToInsert && formatJobForInsert(options.jobToInsert), jobToInsert: jobToInsert && formatJobForInsert(jobToInsert),
ourAci,
}); });
softAssert( softAssert(
// Older messages still have `UUIDv4` so don't log errors when encountering // Older messages still have `UUIDv4` so don't log errors when encountering
// it. // it.
(!options.forceSave && isValidUuid(id)) || isValidUuidV7(id), (!forceSave && isValidUuid(id)) || isValidUuidV7(id),
'saveMessage: messageId is not a UUID' 'saveMessage: messageId is not a UUID'
); );
void updateExpiringMessagesService(); drop(postSaveUpdates?.());
void tapToViewMessagesDeletionService.update();
return id; return id;
} }
async function saveMessages( async function saveMessages(
arrayOfMessages: ReadonlyArray<ReadonlyDeep<MessageType>>, arrayOfMessages: ReadonlyArray<ReadonlyDeep<MessageType>>,
options: { forceSave?: boolean; ourAci: AciString } {
forceSave,
ourAci,
postSaveUpdates,
}: {
forceSave?: boolean;
ourAci: AciString;
postSaveUpdates: () => Promise<void>;
}
): Promise<Array<string>> { ): Promise<Array<string>> {
const result = await writableChannel.saveMessages( const result = await writableChannel.saveMessages(
arrayOfMessages.map(message => _cleanMessageData(message)), arrayOfMessages.map(message => _cleanMessageData(message)),
options { forceSave, ourAci }
); );
void updateExpiringMessagesService(); drop(postSaveUpdates?.());
void tapToViewMessagesDeletionService.update();
return result;
}
async function saveMessagesIndividually(
arrayOfMessages: ReadonlyArray<ReadonlyDeep<MessageType>>,
{
forceSave,
ourAci,
postSaveUpdates,
}: {
forceSave?: boolean;
ourAci: AciString;
postSaveUpdates: () => Promise<void>;
}
): Promise<{ failedIndices: Array<number> }> {
const result = await writableChannel.saveMessagesIndividually(
arrayOfMessages,
{ forceSave, ourAci }
);
drop(postSaveUpdates?.());
return result; return result;
} }
@ -637,7 +671,10 @@ async function saveMessages(
async function removeMessage( async function removeMessage(
id: string, id: string,
options: { options: {
singleProtoJobQueue: SingleProtoJobQueue; cleanupMessages: (
messages: ReadonlyArray<MessageAttributesType>,
options: { fromSync?: boolean }
) => Promise<void>;
fromSync?: boolean; fromSync?: boolean;
} }
): Promise<void> { ): Promise<void> {
@ -647,9 +684,8 @@ async function removeMessage(
// it needs to delete all associated on-disk files along with the database delete. // it needs to delete all associated on-disk files along with the database delete.
if (message) { if (message) {
await writableChannel.removeMessage(id); await writableChannel.removeMessage(id);
await cleanupMessages([message], { await options.cleanupMessages([message], {
...options, fromSync: options.fromSync,
markCallHistoryDeleted: DataWriter.markCallHistoryDeleted,
}); });
} }
} }
@ -659,7 +695,10 @@ export async function deleteAndCleanup(
logId: string, logId: string,
options: { options: {
fromSync?: boolean; fromSync?: boolean;
singleProtoJobQueue: SingleProtoJobQueue; cleanupMessages: (
messages: ReadonlyArray<MessageAttributesType>,
options: { fromSync?: boolean }
) => Promise<void>;
} }
): Promise<void> { ): Promise<void> {
const ids = messages.map(message => message.id); const ids = messages.map(message => message.id);
@ -668,9 +707,8 @@ export async function deleteAndCleanup(
await writableChannel.removeMessages(ids); await writableChannel.removeMessages(ids);
log.info(`deleteAndCleanup/${logId}: Cleanup for ${ids.length} messages...`); log.info(`deleteAndCleanup/${logId}: Cleanup for ${ids.length} messages...`);
await cleanupMessages(messages, { await options.cleanupMessages(messages, {
...options, fromSync: Boolean(options.fromSync),
markCallHistoryDeleted: DataWriter.markCallHistoryDeleted,
}); });
log.info(`deleteAndCleanup/${logId}: Complete`); log.info(`deleteAndCleanup/${logId}: Complete`);
@ -680,13 +718,15 @@ async function removeMessages(
messageIds: ReadonlyArray<string>, messageIds: ReadonlyArray<string>,
options: { options: {
fromSync?: boolean; fromSync?: boolean;
singleProtoJobQueue: SingleProtoJobQueue; cleanupMessages: (
messages: ReadonlyArray<MessageAttributesType>,
options: { fromSync?: boolean }
) => Promise<void>;
} }
): Promise<void> { ): Promise<void> {
const messages = await readableChannel.getMessagesById(messageIds); const messages = await readableChannel.getMessagesById(messageIds);
await cleanupMessages(messages, { await options.cleanupMessages(messages, {
...options, fromSync: Boolean(options.fromSync),
markCallHistoryDeleted: DataWriter.markCallHistoryDeleted,
}); });
await writableChannel.removeMessages(messageIds); await writableChannel.removeMessages(messageIds);
} }
@ -743,15 +783,18 @@ async function getConversationRangeCenteredOnMessage(
async function removeMessagesInConversation( async function removeMessagesInConversation(
conversationId: string, conversationId: string,
{ {
cleanupMessages,
fromSync,
logId, logId,
receivedAt, receivedAt,
singleProtoJobQueue,
fromSync,
}: { }: {
cleanupMessages: (
messages: ReadonlyArray<MessageAttributesType>,
options: { fromSync?: boolean | undefined }
) => Promise<void>;
fromSync?: boolean; fromSync?: boolean;
logId: string; logId: string;
receivedAt?: number; receivedAt?: number;
singleProtoJobQueue: SingleProtoJobQueue;
} }
): Promise<void> { ): Promise<void> {
let messages; let messages;
@ -776,7 +819,7 @@ async function removeMessagesInConversation(
} }
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await deleteAndCleanup(messages, logId, { fromSync, singleProtoJobQueue }); await deleteAndCleanup(messages, logId, { fromSync, cleanupMessages });
} while (messages.length > 0); } while (messages.length > 0);
} }

View file

@ -44,7 +44,6 @@ import type {
} from '../types/GroupSendEndorsements'; } from '../types/GroupSendEndorsements';
import type { SyncTaskType } from '../util/syncTasks'; import type { SyncTaskType } from '../util/syncTasks';
import type { AttachmentBackupJobType } from '../types/AttachmentBackup'; import type { AttachmentBackupJobType } from '../types/AttachmentBackup';
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
export type ReadableDB = Database & { __readable_db: never }; export type ReadableDB = Database & { __readable_db: never };
export type WritableDB = ReadableDB & { __writable_db: never }; export type WritableDB = ReadableDB & { __writable_db: never };
@ -749,23 +748,6 @@ type WritableInterface = {
replaceAllEndorsementsForGroup: (data: GroupSendEndorsementsData) => void; replaceAllEndorsementsForGroup: (data: GroupSendEndorsementsData) => void;
deleteAllEndorsementsForGroup: (groupId: string) => void; deleteAllEndorsementsForGroup: (groupId: string) => void;
saveMessage: (
data: ReadonlyDeep<MessageType>,
options: {
jobToInsert?: StoredJob;
forceSave?: boolean;
ourAci: AciString;
}
) => string;
saveMessages: (
arrayOfMessages: ReadonlyArray<ReadonlyDeep<MessageType>>,
options: { forceSave?: boolean; ourAci: AciString }
) => Array<string>;
saveMessagesIndividually: (
arrayOfMessages: ReadonlyArray<ReadonlyDeep<MessageType>>,
options: { forceSave?: boolean; ourAci: AciString }
) => { failedIndices: Array<number> };
getUnreadByConversationAndMarkRead: (options: { getUnreadByConversationAndMarkRead: (options: {
conversationId: string; conversationId: string;
includeStoryReplies: boolean; includeStoryReplies: boolean;
@ -1047,6 +1029,22 @@ export type ServerWritableDirectInterface = WritableInterface & {
updateConversation: (data: ConversationType) => void; updateConversation: (data: ConversationType) => void;
removeConversation: (id: Array<string> | string) => void; removeConversation: (id: Array<string> | string) => void;
saveMessage: (
data: ReadonlyDeep<MessageType>,
options: {
jobToInsert?: StoredJob;
forceSave?: boolean;
ourAci: AciString;
}
) => string;
saveMessages: (
arrayOfMessages: ReadonlyArray<ReadonlyDeep<MessageType>>,
options: { forceSave?: boolean; ourAci: AciString }
) => Array<string>;
saveMessagesIndividually: (
arrayOfMessages: ReadonlyArray<ReadonlyDeep<MessageType>>,
options: { forceSave?: boolean; ourAci: AciString }
) => { failedIndices: Array<number> };
removeMessage: (id: string) => void; removeMessage: (id: string) => void;
removeMessages: (ids: ReadonlyArray<string>) => void; removeMessages: (ids: ReadonlyArray<string>) => void;
@ -1134,18 +1132,49 @@ export type ClientOnlyWritableInterface = ClientInterfaceWrap<{
removeConversation: (id: string) => void; removeConversation: (id: string) => void;
flushUpdateConversationBatcher: () => void; flushUpdateConversationBatcher: () => void;
saveMessage: (
data: ReadonlyDeep<MessageType>,
options: {
jobToInsert?: StoredJob;
forceSave?: boolean;
ourAci: AciString;
postSaveUpdates: () => Promise<void>;
}
) => string;
saveMessages: (
arrayOfMessages: ReadonlyArray<ReadonlyDeep<MessageType>>,
options: {
forceSave?: boolean;
ourAci: AciString;
postSaveUpdates: () => Promise<void>;
}
) => Array<string>;
saveMessagesIndividually: (
arrayOfMessages: ReadonlyArray<ReadonlyDeep<MessageType>>,
options: {
forceSave?: boolean;
ourAci: AciString;
postSaveUpdates: () => Promise<void>;
}
) => { failedIndices: Array<number> };
removeMessage: ( removeMessage: (
id: string, id: string,
options: { options: {
fromSync?: boolean; fromSync?: boolean;
singleProtoJobQueue: SingleProtoJobQueue; cleanupMessages: (
messages: ReadonlyArray<MessageAttributesType>,
options: { fromSync?: boolean | undefined }
) => Promise<void>;
} }
) => void; ) => void;
removeMessages: ( removeMessages: (
ids: ReadonlyArray<string>, ids: ReadonlyArray<string>,
options: { options: {
fromSync?: boolean; fromSync?: boolean;
singleProtoJobQueue: SingleProtoJobQueue; cleanupMessages: (
messages: ReadonlyArray<MessageAttributesType>,
options: { fromSync?: boolean | undefined }
) => Promise<void>;
} }
) => void; ) => void;
@ -1170,10 +1199,13 @@ export type ClientOnlyWritableInterface = ClientInterfaceWrap<{
removeMessagesInConversation: ( removeMessagesInConversation: (
conversationId: string, conversationId: string,
options: { options: {
cleanupMessages: (
messages: ReadonlyArray<MessageAttributesType>,
options: { fromSync?: boolean | undefined }
) => Promise<void>;
fromSync?: boolean; fromSync?: boolean;
logId: string; logId: string;
receivedAt?: number; receivedAt?: number;
singleProtoJobQueue: SingleProtoJobQueue;
} }
) => void; ) => void;
removeOtherData: () => void; removeOtherData: () => void;

View file

@ -69,7 +69,7 @@ import { resolveAttachmentDraftData } from '../../util/resolveAttachmentDraftDat
import { resolveDraftAttachmentOnDisk } from '../../util/resolveDraftAttachmentOnDisk'; import { resolveDraftAttachmentOnDisk } from '../../util/resolveDraftAttachmentOnDisk';
import { shouldShowInvalidMessageToast } from '../../util/shouldShowInvalidMessageToast'; import { shouldShowInvalidMessageToast } from '../../util/shouldShowInvalidMessageToast';
import { writeDraftAttachment } from '../../util/writeDraftAttachment'; import { writeDraftAttachment } from '../../util/writeDraftAttachment';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; import { getMessageById } from '../../messages/getMessageById';
import { canReply, isNormalBubble } from '../selectors/message'; import { canReply, isNormalBubble } from '../selectors/message';
import { getAuthorId } from '../../messages/helpers'; import { getAuthorId } from '../../messages/helpers';
import { getConversationSelector } from '../selectors/conversations'; import { getConversationSelector } from '../selectors/conversations';
@ -730,9 +730,7 @@ export function setQuoteByMessageId(
return; return;
} }
const message = messageId const message = messageId ? await getMessageById(messageId) : undefined;
? await __DEPRECATED$getMessageById(messageId, 'setQuoteByMessageId')
: undefined;
const state = getState(); const state = getState();
if ( if (

View file

@ -126,11 +126,12 @@ import {
isDirectConversation, isDirectConversation,
isGroup, isGroup,
isGroupV2, isGroupV2,
isMe,
} from '../../util/whatTypeOfConversation'; } from '../../util/whatTypeOfConversation';
import { missingCaseError } from '../../util/missingCaseError'; import { missingCaseError } from '../../util/missingCaseError';
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue'; import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
import { ReadStatus } from '../../messages/MessageReadStatus'; import { ReadStatus } from '../../messages/MessageReadStatus';
import { isIncoming, processBodyRanges } from '../selectors/message'; import { isIncoming, isStory, processBodyRanges } from '../selectors/message';
import { getActiveCall, getActiveCallState } from '../selectors/calling'; import { getActiveCall, getActiveCallState } from '../selectors/calling';
import { sendDeleteForEveryoneMessage } from '../../util/sendDeleteForEveryoneMessage'; import { sendDeleteForEveryoneMessage } from '../../util/sendDeleteForEveryoneMessage';
import type { ShowToastActionType } from './toast'; import type { ShowToastActionType } from './toast';
@ -149,7 +150,7 @@ import {
buildUpdateAttributesChange, buildUpdateAttributesChange,
initiateMigrationToGroupV2 as doInitiateMigrationToGroupV2, initiateMigrationToGroupV2 as doInitiateMigrationToGroupV2,
} from '../../groups'; } from '../../groups';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; import { getMessageById } from '../../messages/getMessageById';
import type { PanelRenderType, PanelRequestType } from '../../types/Panels'; import type { PanelRenderType, PanelRequestType } from '../../types/Panels';
import type { ConversationQueueJobData } from '../../jobs/conversationJobQueue'; import type { ConversationQueueJobData } from '../../jobs/conversationJobQueue';
import { isOlderThan } from '../../util/timestamp'; import { isOlderThan } from '../../util/timestamp';
@ -184,7 +185,10 @@ import type { ChangeNavTabActionType } from './nav';
import { CHANGE_NAV_TAB, NavTab, actions as navActions } from './nav'; import { CHANGE_NAV_TAB, NavTab, actions as navActions } from './nav';
import { sortByMessageOrder } from '../../types/ForwardDraft'; import { sortByMessageOrder } from '../../types/ForwardDraft';
import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation'; import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation';
import { getConversationIdForLogging } from '../../util/idForLogging'; import {
getConversationIdForLogging,
getMessageIdForLogging,
} from '../../util/idForLogging';
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue'; import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
import MessageSender from '../../textsecure/SendMessage'; import MessageSender from '../../textsecure/SendMessage';
import { import {
@ -204,6 +208,18 @@ import { markCallHistoryReadInConversation } from './callHistory';
import type { CapabilitiesType } from '../../textsecure/WebAPI'; import type { CapabilitiesType } from '../../textsecure/WebAPI';
import { actions as searchActions } from './search'; import { actions as searchActions } from './search';
import type { SearchActionType } from './search'; import type { SearchActionType } from './search';
import { getNotificationTextForMessage } from '../../util/getNotificationTextForMessage';
import { doubleCheckMissingQuoteReference as doDoubleCheckMissingQuoteReference } from '../../util/doubleCheckMissingQuoteReference';
import { queueAttachmentDownloadsForMessage } from '../../util/queueAttachmentDownloads';
import { markAttachmentAsCorrupted as doMarkAttachmentAsCorrupted } from '../../messageModifiers/AttachmentDownloads';
import {
isSent,
SendActionType,
sendStateReducer,
} from '../../messages/MessageSendState';
import { markFailed } from '../../test-node/util/messageFailures';
import { cleanupMessages, postSaveUpdates } from '../../util/cleanup';
import { MessageModel } from '../../models/messages';
// State // State
export type DBConversationType = ReadonlyDeep<{ export type DBConversationType = ReadonlyDeep<{
@ -1410,10 +1426,7 @@ function markMessageRead(
return; return;
} }
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'markMessageRead'
);
if (!message) { if (!message) {
throw new Error(`markMessageRead: failed to load message ${messageId}`); throw new Error(`markMessageRead: failed to load message ${messageId}`);
} }
@ -1767,10 +1780,7 @@ function deleteMessages({
await Promise.all( await Promise.all(
messageIds.map( messageIds.map(
async (messageId): Promise<MessageToDelete | undefined> => { async (messageId): Promise<MessageToDelete | undefined> => {
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'deleteMessages'
);
if (!message) { if (!message) {
throw new Error(`deleteMessages: Message ${messageId} missing!`); throw new Error(`deleteMessages: Message ${messageId} missing!`);
} }
@ -1805,7 +1815,7 @@ function deleteMessages({
} }
await DataWriter.removeMessages(messageIds, { await DataWriter.removeMessages(messageIds, {
singleProtoJobQueue, cleanupMessages,
}); });
popPanelForConversation()(dispatch, getState, undefined); popPanelForConversation()(dispatch, getState, undefined);
@ -1930,9 +1940,7 @@ function setMessageToEdit(
return; return;
} }
const message = ( const message = (await getMessageById(messageId))?.attributes;
await __DEPRECATED$getMessageById(messageId, 'setMessageToEdit')
)?.attributes;
if (!message) { if (!message) {
return; return;
} }
@ -2025,10 +2033,7 @@ function generateNewGroupLink(
* replace it with an actual action that fits in with the redux approach. * replace it with an actual action that fits in with the redux approach.
*/ */
export const markViewed = (messageId: string): void => { export const markViewed = (messageId: string): void => {
const message = window.MessageCache.__DEPRECATED$getById( const message = window.MessageCache.getById(messageId);
messageId,
'markViewed'
);
if (!message) { if (!message) {
throw new Error(`markViewed: Message ${messageId} missing!`); throw new Error(`markViewed: Message ${messageId} missing!`);
} }
@ -2051,7 +2056,9 @@ export const markViewed = (messageId: string): void => {
); );
senderAci = sourceServiceId; senderAci = sourceServiceId;
const convoAttributes = message.getConversation()?.attributes; const convo = window.ConversationController.get(
message.get('conversationId')
);
const conversationId = message.get('conversationId'); const conversationId = message.get('conversationId');
drop( drop(
conversationJobQueue.add({ conversationJobQueue.add({
@ -2065,8 +2072,8 @@ export const markViewed = (messageId: string): void => {
senderE164, senderE164,
senderAci, senderAci,
timestamp, timestamp,
isDirectConversation: convoAttributes isDirectConversation: convo
? isDirectConversation(convoAttributes) ? isDirectConversation(convo.attributes)
: true, : true,
}, },
], ],
@ -2292,16 +2299,14 @@ function kickOffAttachmentDownload(
options: Readonly<{ messageId: string }> options: Readonly<{ messageId: string }>
): ThunkAction<void, RootStateType, unknown, NoopActionType> { ): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => { return async dispatch => {
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(options.messageId);
options.messageId,
'kickOffAttachmentDownload'
);
if (!message) { if (!message) {
throw new Error( throw new Error(
`kickOffAttachmentDownload: Message ${options.messageId} missing!` `kickOffAttachmentDownload: Message ${options.messageId} missing!`
); );
} }
const didUpdateValues = await message.queueAttachmentDownloads( const didUpdateValues = await queueAttachmentDownloadsForMessage(
message,
AttachmentDownloadUrgency.IMMEDIATE AttachmentDownloadUrgency.IMMEDIATE
); );
@ -2309,6 +2314,7 @@ function kickOffAttachmentDownload(
drop( drop(
DataWriter.saveMessage(message.attributes, { DataWriter.saveMessage(message.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}) })
); );
} }
@ -2329,10 +2335,7 @@ function cancelAttachmentDownload({
NoopActionType NoopActionType
> { > {
return async dispatch => { return async dispatch => {
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'cancelAttachmentDownload'
);
if (!message) { if (!message) {
log.warn(`cancelAttachmentDownload: Message ${messageId} missing!`); log.warn(`cancelAttachmentDownload: Message ${messageId} missing!`);
} else { } else {
@ -2344,7 +2347,10 @@ function cancelAttachmentDownload({
}); });
const ourAci = window.textsecure.storage.user.getCheckedAci(); const ourAci = window.textsecure.storage.user.getCheckedAci();
await DataWriter.saveMessage(message.attributes, { ourAci }); await DataWriter.saveMessage(message.attributes, {
ourAci,
postSaveUpdates,
});
} }
// A click kicks off downloads for every attachment in a message, so cancel does too // A click kicks off downloads for every attachment in a message, so cancel does too
@ -2370,16 +2376,7 @@ function markAttachmentAsCorrupted(
options: AttachmentOptions options: AttachmentOptions
): ThunkAction<void, RootStateType, unknown, NoopActionType> { ): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => { return async dispatch => {
const message = await __DEPRECATED$getMessageById( await doMarkAttachmentAsCorrupted(options.messageId, options.attachment);
options.messageId,
'markAttachmentAsCorrupted'
);
if (!message) {
throw new Error(
`markAttachmentAsCorrupted: Message ${options.messageId} missing!`
);
}
message.markAttachmentAsCorrupted(options.attachment);
dispatch({ dispatch({
type: 'NOOP', type: 'NOOP',
@ -2392,10 +2389,7 @@ function openGiftBadge(
messageId: string messageId: string
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> { ): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
return async dispatch => { return async dispatch => {
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'openGiftBadge'
);
if (!message) { if (!message) {
throw new Error(`openGiftBadge: Message ${messageId} missing!`); throw new Error(`openGiftBadge: Message ${messageId} missing!`);
} }
@ -2415,14 +2409,106 @@ function retryMessageSend(
messageId: string messageId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> { ): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => { return async dispatch => {
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'retryMessageSend'
);
if (!message) { if (!message) {
throw new Error(`retryMessageSend: Message ${messageId} missing!`); throw new Error(`retryMessageSend: Message ${messageId} missing!`);
} }
await message.retrySend();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const conversation = window.ConversationController.get(
message.attributes.conversationId
)!;
let currentConversationRecipients: Set<string> | undefined;
const { storyDistributionListId } = message.attributes;
if (storyDistributionListId) {
const storyDistribution =
await DataReader.getStoryDistributionWithMembers(
storyDistributionListId
);
if (!storyDistribution) {
markFailed(message);
return;
}
currentConversationRecipients = new Set(
storyDistribution.members
.map(serviceId => window.ConversationController.get(serviceId)?.id)
.filter(isNotNil)
);
} else {
currentConversationRecipients = conversation.getMemberConversationIds();
}
// Determine retry recipients and get their most up-to-date addressing information
const oldSendStateByConversationId =
message.get('sendStateByConversationId') || {};
const newSendStateByConversationId = { ...oldSendStateByConversationId };
for (const [conversationId, sendState] of Object.entries(
oldSendStateByConversationId
)) {
if (isSent(sendState.status)) {
continue;
}
const recipient = window.ConversationController.get(conversationId);
if (
!recipient ||
(!currentConversationRecipients.has(conversationId) &&
!isMe(recipient.attributes))
) {
continue;
}
newSendStateByConversationId[conversationId] = sendStateReducer(
sendState,
{
type: SendActionType.ManuallyRetried,
updatedAt: Date.now(),
}
);
}
message.set({ sendStateByConversationId: newSendStateByConversationId });
if (isStory(message.attributes)) {
await conversationJobQueue.add(
{
type: conversationQueueJobEnum.enum.Story,
conversationId: conversation.id,
messageIds: [message.id],
// using the group timestamp, which will differ from the 1:1 timestamp
timestamp: message.attributes.timestamp,
},
async jobToInsert => {
await DataWriter.saveMessage(message.attributes, {
jobToInsert,
ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
});
}
);
} else {
await conversationJobQueue.add(
{
type: conversationQueueJobEnum.enum.NormalMessage,
conversationId: conversation.id,
messageId: message.id,
revision: conversation.get('revision'),
},
async jobToInsert => {
await DataWriter.saveMessage(message.attributes, {
jobToInsert,
ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
});
}
);
}
dispatch({ dispatch({
type: 'NOOP', type: 'NOOP',
@ -2435,15 +2521,12 @@ export function copyMessageText(
messageId: string messageId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> { ): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => { return async dispatch => {
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'copyMessageText'
);
if (!message) { if (!message) {
throw new Error(`copy: Message ${messageId} missing!`); throw new Error(`copy: Message ${messageId} missing!`);
} }
const body = message.getNotificationText(); const body = getNotificationTextForMessage(message.attributes);
clipboard.writeText(body); clipboard.writeText(body);
dispatch({ dispatch({
@ -2457,10 +2540,7 @@ export function retryDeleteForEveryone(
messageId: string messageId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> { ): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => { return async dispatch => {
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'retryDeleteForEveryone'
);
if (!message) { if (!message) {
throw new Error(`retryDeleteForEveryone: Message ${messageId} missing!`); throw new Error(`retryDeleteForEveryone: Message ${messageId} missing!`);
} }
@ -2472,7 +2552,9 @@ export function retryDeleteForEveryone(
} }
try { try {
const conversation = message.getConversation(); const conversation = window.ConversationController.get(
message.get('conversationId')
);
if (!conversation) { if (!conversation) {
throw new Error( throw new Error(
`retryDeleteForEveryone: Conversation for ${messageId} missing!` `retryDeleteForEveryone: Conversation for ${messageId} missing!`
@ -2489,7 +2571,7 @@ export function retryDeleteForEveryone(
}; };
log.info( log.info(
`retryDeleteForEveryone: Adding job for message ${message.idForLogging()}!` `retryDeleteForEveryone: Adding job for message ${getMessageIdForLogging(message.attributes)}!`
); );
await conversationJobQueue.add(jobData); await conversationJobQueue.add(jobData);
@ -3247,12 +3329,7 @@ function pushPanelForConversation(
const message = const message =
conversations.messagesLookup[messageId] || conversations.messagesLookup[messageId] ||
( (await getMessageById(messageId))?.attributes;
await __DEPRECATED$getMessageById(
messageId,
'pushPanelForConversation'
)
)?.attributes;
if (!message) { if (!message) {
throw new Error( throw new Error(
'pushPanelForConversation: could not find message for MessageDetails' 'pushPanelForConversation: could not find message for MessageDetails'
@ -3328,17 +3405,16 @@ function deleteMessagesForEveryone(
await Promise.all( await Promise.all(
messageIds.map(async messageId => { messageIds.map(async messageId => {
try { try {
const message = window.MessageCache.__DEPRECATED$getById( const message = window.MessageCache.getById(messageId);
messageId,
'deleteMessagesForEveryone'
);
if (!message) { if (!message) {
throw new Error( throw new Error(
`deleteMessageForEveryone: Message ${messageId} missing!` `deleteMessageForEveryone: Message ${messageId} missing!`
); );
} }
const conversation = message.getConversation(); const conversation = window.ConversationController.get(
message.get('conversationId')
);
if (!conversation) { if (!conversation) {
throw new Error('deleteMessageForEveryone: no conversation'); throw new Error('deleteMessageForEveryone: no conversation');
} }
@ -3834,11 +3910,7 @@ function loadRecentMediaItems(
// Cache these messages in memory to ensure Lightbox can find them // Cache these messages in memory to ensure Lightbox can find them
messages.forEach(message => { messages.forEach(message => {
window.MessageCache.__DEPRECATED$register( window.MessageCache.register(new MessageModel(message));
message.id,
message,
'loadRecentMediaItems'
);
}); });
let index = 0; let index = 0;
@ -4042,10 +4114,7 @@ export function saveAttachmentFromMessage(
providedAttachment?: AttachmentType providedAttachment?: AttachmentType
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> { ): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'saveAttachmentFromMessage'
);
if (!message) { if (!message) {
throw new Error( throw new Error(
`saveAttachmentFromMessage: Message ${messageId} missing!` `saveAttachmentFromMessage: Message ${messageId} missing!`
@ -4138,10 +4207,7 @@ export function scrollToMessage(
throw new Error('scrollToMessage: No conversation found'); throw new Error('scrollToMessage: No conversation found');
} }
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'scrollToMessage'
);
if (!message) { if (!message) {
throw new Error(`scrollToMessage: failed to load message ${messageId}`); throw new Error(`scrollToMessage: failed to load message ${messageId}`);
} }
@ -4155,12 +4221,7 @@ export function scrollToMessage(
let isInMemory = true; let isInMemory = true;
if ( if (!window.MessageCache.getById(messageId)) {
!window.MessageCache.__DEPRECATED$getById(
messageId,
'scrollToMessage/notInMemory'
)
) {
isInMemory = false; isInMemory = false;
} }
@ -4591,10 +4652,7 @@ function onConversationOpened(
log.info(`${logId}: Updating newly opened conversation state`); log.info(`${logId}: Updating newly opened conversation state`);
if (messageId) { if (messageId) {
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'onConversationOpened'
);
if (message) { if (message) {
drop(conversation.loadAndScroll(messageId)); drop(conversation.loadAndScroll(messageId));
@ -4733,12 +4791,9 @@ function showArchivedConversations(): ShowArchivedConversationsActionType {
} }
function doubleCheckMissingQuoteReference(messageId: string): NoopActionType { function doubleCheckMissingQuoteReference(messageId: string): NoopActionType {
const message = window.MessageCache.__DEPRECATED$getById( const message = window.MessageCache.getById(messageId);
messageId,
'doubleCheckMissingQuoteReference'
);
if (message) { if (message) {
void message.doubleCheckMissingQuoteReference(); drop(doDoubleCheckMissingQuoteReference(message));
} }
return { return {

View file

@ -49,6 +49,7 @@ import type { CallLinkType } from '../../types/CallLink';
import type { LocalizerType } from '../../types/I18N'; import type { LocalizerType } from '../../types/I18N';
import { linkCallRoute } from '../../util/signalRoutes'; import { linkCallRoute } from '../../util/signalRoutes';
import type { StartCallData } from '../../components/ConfirmLeaveCallModal'; import type { StartCallData } from '../../components/ConfirmLeaveCallModal';
import { getMessageById } from '../../messages/getMessageById';
// State // State
@ -624,12 +625,13 @@ function toggleForwardMessagesModal(
if (payload.type === ForwardMessagesModalType.Forward) { if (payload.type === ForwardMessagesModalType.Forward) {
messageDrafts = await Promise.all( messageDrafts = await Promise.all(
payload.messageIds.map(async messageId => { payload.messageIds.map(async messageId => {
const messageAttributes = await window.MessageCache.resolveAttributes( const message = await getMessageById(messageId);
'toggleForwardMessagesModal', if (!message) {
messageId throw new Error(
); 'toggleForwardMessagesModal: failed to find target message'
);
const { attachments = [] } = messageAttributes; }
const { attachments = [] } = message.attributes;
if (!attachments.every(isDownloaded)) { if (!attachments.every(isDownloaded)) {
dispatch( dispatch(
@ -641,7 +643,7 @@ function toggleForwardMessagesModal(
const messagePropsSelector = getMessagePropsSelector(state); const messagePropsSelector = getMessagePropsSelector(state);
const conversationSelector = getConversationSelector(state); const conversationSelector = getConversationSelector(state);
const messageProps = messagePropsSelector(messageAttributes); const messageProps = messagePropsSelector(message.attributes);
const messageDraft = toMessageForwardDraft( const messageDraft = toMessageForwardDraft(
messageProps, messageProps,
conversationSelector conversationSelector
@ -944,12 +946,14 @@ function showEditHistoryModal(
messageId: string messageId: string
): ThunkAction<void, RootStateType, unknown, ShowEditHistoryModalActionType> { ): ThunkAction<void, RootStateType, unknown, ShowEditHistoryModalActionType> {
return async dispatch => { return async dispatch => {
const messageAttributes = await window.MessageCache.resolveAttributes( const message = await getMessageById(messageId);
'showEditHistoryModal', if (!message) {
messageId throw new Error('showEditHistoryModal: failed to find target message');
}
const nextEditHistoryMessages = copyOverMessageAttributesIntoEditHistory(
message.attributes
); );
const nextEditHistoryMessages =
copyOverMessageAttributesIntoEditHistory(messageAttributes);
if (!nextEditHistoryMessages) { if (!nextEditHistoryMessages) {
log.warn('showEditHistoryModal: no edit history for message'); log.warn('showEditHistoryModal: no edit history for message');

View file

@ -17,7 +17,7 @@ import type { ShowToastActionType } from './toast';
import type { StateType as RootStateType } from '../reducer'; import type { StateType as RootStateType } from '../reducer';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; import { getMessageById } from '../../messages/getMessageById';
import type { ReadonlyMessageAttributesType } from '../../model-types.d'; import type { ReadonlyMessageAttributesType } from '../../model-types.d';
import { isGIF } from '../../types/Attachment'; import { isGIF } from '../../types/Attachment';
import { import {
@ -40,6 +40,8 @@ import {
import { showStickerPackPreview } from './globalModals'; import { showStickerPackPreview } from './globalModals';
import { useBoundActions } from '../../hooks/useBoundActions'; import { useBoundActions } from '../../hooks/useBoundActions';
import { DataReader } from '../../sql/Client'; import { DataReader } from '../../sql/Client';
import { getMessageIdForLogging } from '../../util/idForLogging';
import { markViewOnceMessageViewed } from '../../services/MessageUpdater';
// eslint-disable-next-line local-rules/type-alias-readonlydeep // eslint-disable-next-line local-rules/type-alias-readonlydeep
export type LightboxStateType = export type LightboxStateType =
@ -156,10 +158,7 @@ function showLightboxForViewOnceMedia(
return async dispatch => { return async dispatch => {
log.info('showLightboxForViewOnceMedia: attempting to display message'); log.info('showLightboxForViewOnceMedia: attempting to display message');
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'showLightboxForViewOnceMedia'
);
if (!message) { if (!message) {
throw new Error( throw new Error(
`showLightboxForViewOnceMedia: Message ${messageId} missing!` `showLightboxForViewOnceMedia: Message ${messageId} missing!`
@ -168,20 +167,20 @@ function showLightboxForViewOnceMedia(
if (!isTapToView(message.attributes)) { if (!isTapToView(message.attributes)) {
throw new Error( throw new Error(
`showLightboxForViewOnceMedia: Message ${message.idForLogging()} is not a tap to view message` `showLightboxForViewOnceMedia: Message ${getMessageIdForLogging(message.attributes)} is not a tap to view message`
); );
} }
if (message.isErased()) { if (message.get('isErased')) {
throw new Error( throw new Error(
`showLightboxForViewOnceMedia: Message ${message.idForLogging()} is already erased` `showLightboxForViewOnceMedia: Message ${getMessageIdForLogging(message.attributes)} is already erased`
); );
} }
const firstAttachment = (message.get('attachments') || [])[0]; const firstAttachment = (message.get('attachments') || [])[0];
if (!firstAttachment || !firstAttachment.path) { if (!firstAttachment || !firstAttachment.path) {
throw new Error( throw new Error(
`showLightboxForViewOnceMedia: Message ${message.idForLogging()} had no first attachment with path` `showLightboxForViewOnceMedia: Message ${getMessageIdForLogging(message.attributes)} had no first attachment with path`
); );
} }
@ -195,7 +194,7 @@ function showLightboxForViewOnceMedia(
path: tempPath, path: tempPath,
}; };
await message.markViewOnceMessageViewed(); await markViewOnceMessageViewed(message);
const { contentType } = tempAttachment; const { contentType } = tempAttachment;
@ -253,10 +252,7 @@ function showLightbox(opts: {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const { attachment, messageId } = opts; const { attachment, messageId } = opts;
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'showLightbox'
);
if (!message) { if (!message) {
throw new Error(`showLightbox: Message ${messageId} missing!`); throw new Error(`showLightbox: Message ${messageId} missing!`);
} }
@ -393,10 +389,7 @@ function showLightboxForAdjacentMessage(
const [media] = lightbox.media; const [media] = lightbox.media;
const { id: messageId, receivedAt, sentAt } = media.message; const { id: messageId, receivedAt, sentAt } = media.message;
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'showLightboxForAdjacentMessage'
);
if (!message) { if (!message) {
log.warn('showLightboxForAdjacentMessage: original message is gone'); log.warn('showLightboxForAdjacentMessage: original message is gone');
dispatch({ dispatch({

View file

@ -33,6 +33,7 @@ import type { MIMEType } from '../../types/MIME';
import type { MediaItemType } from '../../types/MediaItem'; import type { MediaItemType } from '../../types/MediaItem';
import type { StateType as RootStateType } from '../reducer'; import type { StateType as RootStateType } from '../reducer';
import type { MessageAttributesType } from '../../model-types'; import type { MessageAttributesType } from '../../model-types';
import { MessageModel } from '../../models/messages';
type MediaItemMessage = ReadonlyDeep<{ type MediaItemMessage = ReadonlyDeep<{
attachments: Array<AttachmentType>; attachments: Array<AttachmentType>;
@ -141,13 +142,13 @@ function _getMediaItemMessage(
} }
function _cleanVisualAttachments( function _cleanVisualAttachments(
rawMedia: ReadonlyDeep<ReadonlyArray<MessageAttributesType>> rawMedia: ReadonlyDeep<ReadonlyArray<MessageModel>>
): ReadonlyArray<MediaType> { ): ReadonlyArray<MediaType> {
return rawMedia return rawMedia
.flatMap(message => { .flatMap(message => {
let index = 0; let index = 0;
return (message.attachments || []).map( return (message.get('attachments') || []).map(
(attachment: AttachmentType): MediaType | undefined => { (attachment: AttachmentType): MediaType | undefined => {
if ( if (
!attachment.path || !attachment.path ||
@ -168,7 +169,7 @@ function _cleanVisualAttachments(
contentType: attachment.contentType, contentType: attachment.contentType,
index, index,
attachment, attachment,
message: _getMediaItemMessage(message), message: _getMediaItemMessage(message.attributes),
}; };
index += 1; index += 1;
@ -181,11 +182,11 @@ function _cleanVisualAttachments(
} }
function _cleanFileAttachments( function _cleanFileAttachments(
rawDocuments: ReadonlyDeep<ReadonlyArray<MessageAttributesType>> rawDocuments: ReadonlyDeep<ReadonlyArray<MessageModel>>
): ReadonlyArray<MediaItemType> { ): ReadonlyArray<MediaItemType> {
return rawDocuments return rawDocuments
.map(message => { .map(message => {
const attachments = message.attachments || []; const attachments = message.get('attachments') || [];
const attachment = attachments[0]; const attachment = attachments[0];
if (!attachment) { if (!attachment) {
return; return;
@ -196,7 +197,7 @@ function _cleanFileAttachments(
index: 0, index: 0,
attachment, attachment,
message: { message: {
..._getMediaItemMessage(message), ..._getMediaItemMessage(message.attributes),
attachments: [attachment], attachments: [attachment],
}, },
}; };
@ -205,27 +206,25 @@ function _cleanFileAttachments(
} }
async function _upgradeMessages( async function _upgradeMessages(
messages: ReadonlyArray<MessageAttributesType> messages: ReadonlyArray<MessageModel>
): Promise<ReadonlyArray<MessageAttributesType>> { ): Promise<void> {
// We upgrade these messages so they are sure to have thumbnails // We upgrade these messages so they are sure to have thumbnails
const upgraded = await Promise.all( await Promise.all(
messages.map(async message => { messages.map(async message => {
try { try {
return await window.MessageCache.upgradeSchema( await window.MessageCache.upgradeSchema(
message, message,
VERSION_NEEDED_FOR_DISPLAY VERSION_NEEDED_FOR_DISPLAY
); );
} catch (error) { } catch (error) {
log.warn( log.warn(
'_upgradeMessages: Failed to upgrade message ' + '_upgradeMessages: Failed to upgrade message ' +
`${getMessageIdForLogging(message)}: ${Errors.toLogFormat(error)}` `${getMessageIdForLogging(message.attributes)}: ${Errors.toLogFormat(error)}`
); );
return undefined; return undefined;
} }
}) })
); );
return upgraded.filter(isNotNil);
} }
function initialLoad( function initialLoad(
@ -242,24 +241,28 @@ function initialLoad(
payload: { loading: true }, payload: { loading: true },
}); });
const rawMedia = await DataReader.getOlderMessagesByConversation({ const rawMedia = (
conversationId, await DataReader.getOlderMessagesByConversation({
includeStoryReplies: false, conversationId,
limit: FETCH_CHUNK_COUNT, includeStoryReplies: false,
requireVisualMediaAttachments: true, limit: FETCH_CHUNK_COUNT,
storyId: undefined, requireVisualMediaAttachments: true,
}); storyId: undefined,
const rawDocuments = await DataReader.getOlderMessagesByConversation({ })
conversationId, ).map(item => window.MessageCache.register(new MessageModel(item)));
includeStoryReplies: false, const rawDocuments = (
limit: FETCH_CHUNK_COUNT, await DataReader.getOlderMessagesByConversation({
requireFileAttachments: true, conversationId,
storyId: undefined, includeStoryReplies: false,
}); limit: FETCH_CHUNK_COUNT,
requireFileAttachments: true,
storyId: undefined,
})
).map(item => window.MessageCache.register(new MessageModel(item)));
const upgraded = await _upgradeMessages(rawMedia); await _upgradeMessages(rawMedia);
const media = _cleanVisualAttachments(upgraded);
const media = _cleanVisualAttachments(rawMedia);
const documents = _cleanFileAttachments(rawDocuments); const documents = _cleanFileAttachments(rawDocuments);
dispatch({ dispatch({
@ -305,19 +308,22 @@ function loadMoreMedia(
const { sentAt, receivedAt, id: messageId } = oldestLoadedMedia.message; const { sentAt, receivedAt, id: messageId } = oldestLoadedMedia.message;
const rawMedia = await DataReader.getOlderMessagesByConversation({ const rawMedia = (
conversationId, await DataReader.getOlderMessagesByConversation({
includeStoryReplies: false, conversationId,
limit: FETCH_CHUNK_COUNT, includeStoryReplies: false,
messageId, limit: FETCH_CHUNK_COUNT,
receivedAt, messageId,
requireVisualMediaAttachments: true, receivedAt,
sentAt, requireVisualMediaAttachments: true,
storyId: undefined, sentAt,
}); storyId: undefined,
})
).map(item => window.MessageCache.register(new MessageModel(item)));
const upgraded = await _upgradeMessages(rawMedia); await _upgradeMessages(rawMedia);
const media = _cleanVisualAttachments(upgraded);
const media = _cleanVisualAttachments(rawMedia);
dispatch({ dispatch({
type: LOAD_MORE_MEDIA, type: LOAD_MORE_MEDIA,
@ -367,16 +373,18 @@ function loadMoreDocuments(
const { sentAt, receivedAt, id: messageId } = oldestLoadedDocument.message; const { sentAt, receivedAt, id: messageId } = oldestLoadedDocument.message;
const rawDocuments = await DataReader.getOlderMessagesByConversation({ const rawDocuments = (
conversationId, await DataReader.getOlderMessagesByConversation({
includeStoryReplies: false, conversationId,
limit: FETCH_CHUNK_COUNT, includeStoryReplies: false,
messageId, limit: FETCH_CHUNK_COUNT,
receivedAt, messageId,
requireFileAttachments: true, receivedAt,
sentAt, requireFileAttachments: true,
storyId: undefined, sentAt,
}); storyId: undefined,
})
).map(item => window.MessageCache.register(new MessageModel(item)));
const documents = _cleanFileAttachments(rawDocuments); const documents = _cleanFileAttachments(rawDocuments);
@ -500,8 +508,12 @@ export function reducer(
const oldestLoadedMedia = state.media[0]; const oldestLoadedMedia = state.media[0];
const oldestLoadedDocument = state.documents[0]; const oldestLoadedDocument = state.documents[0];
const newMedia = _cleanVisualAttachments([message]); const newMedia = _cleanVisualAttachments([
const newDocuments = _cleanFileAttachments([message]); window.MessageCache.register(new MessageModel(message)),
]);
const newDocuments = _cleanFileAttachments([
window.MessageCache.register(new MessageModel(message)),
]);
let { documents, haveOldestDocument, haveOldestMedia, media } = state; let { documents, haveOldestDocument, haveOldestMedia, media } = state;

View file

@ -36,7 +36,7 @@ import { blockSendUntilConversationsAreVerified } from '../../util/blockSendUnti
import { deleteStoryForEveryone as doDeleteStoryForEveryone } from '../../util/deleteStoryForEveryone'; import { deleteStoryForEveryone as doDeleteStoryForEveryone } from '../../util/deleteStoryForEveryone';
import { deleteGroupStoryReplyForEveryone as doDeleteGroupStoryReplyForEveryone } from '../../util/deleteGroupStoryReplyForEveryone'; import { deleteGroupStoryReplyForEveryone as doDeleteGroupStoryReplyForEveryone } from '../../util/deleteGroupStoryReplyForEveryone';
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend'; import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; import { getMessageById } from '../../messages/getMessageById';
import { markOnboardingStoryAsRead } from '../../util/markOnboardingStoryAsRead'; import { markOnboardingStoryAsRead } from '../../util/markOnboardingStoryAsRead';
import { markViewed } from '../../services/MessageUpdater'; import { markViewed } from '../../services/MessageUpdater';
import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads'; import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads';
@ -69,7 +69,7 @@ import {
conversationQueueJobEnum, conversationQueueJobEnum,
} from '../../jobs/conversationJobQueue'; } from '../../jobs/conversationJobQueue';
import { ReceiptType } from '../../types/Receipt'; import { ReceiptType } from '../../types/Receipt';
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue'; import { cleanupMessages, postSaveUpdates } from '../../util/cleanup';
export type StoryDataType = ReadonlyDeep< export type StoryDataType = ReadonlyDeep<
{ {
@ -286,7 +286,7 @@ function deleteGroupStoryReply(
messageId: string messageId: string
): ThunkAction<void, RootStateType, unknown, StoryReplyDeletedActionType> { ): ThunkAction<void, RootStateType, unknown, StoryReplyDeletedActionType> {
return async dispatch => { return async dispatch => {
await DataWriter.removeMessage(messageId, { singleProtoJobQueue }); await DataWriter.removeMessage(messageId, { cleanupMessages });
dispatch({ dispatch({
type: STORY_REPLY_DELETED, type: STORY_REPLY_DELETED,
payload: messageId, payload: messageId,
@ -382,10 +382,7 @@ function markStoryRead(
return; return;
} }
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'markStoryRead'
);
if (!message) { if (!message) {
log.warn(`markStoryRead: no message found ${messageId}`); log.warn(`markStoryRead: no message found ${messageId}`);
@ -427,6 +424,7 @@ function markStoryRead(
drop( drop(
DataWriter.saveMessage(message.attributes, { DataWriter.saveMessage(message.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}) })
); );
@ -524,10 +522,7 @@ function queueStoryDownload(
return; return;
} }
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(storyId);
storyId,
'queueStoryDownload'
);
if (message) { if (message) {
// We want to ensure that we re-hydrate the story reply context with the // We want to ensure that we re-hydrate the story reply context with the
@ -1402,10 +1397,7 @@ function removeAllContactStories(
const messages = ( const messages = (
await Promise.all( await Promise.all(
messageIds.map(async messageId => { messageIds.map(async messageId => {
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'removeAllContactStories'
);
if (!message) { if (!message) {
log.warn(`${logId}: no message found ${messageId}`); log.warn(`${logId}: no message found ${messageId}`);
@ -1419,7 +1411,7 @@ function removeAllContactStories(
log.info(`${logId}: removing ${messages.length} stories`); log.info(`${logId}: removing ${messages.length} stories`);
await DataWriter.removeMessages(messageIds, { singleProtoJobQueue }); await DataWriter.removeMessages(messageIds, { cleanupMessages });
dispatch({ dispatch({
type: 'NOOP', type: 'NOOP',

View file

@ -23,7 +23,7 @@ import { useLinkPreviewActions } from '../ducks/linkPreviews';
import { SmartCompositionTextArea } from './CompositionTextArea'; import { SmartCompositionTextArea } from './CompositionTextArea';
import { useToastActions } from '../ducks/toast'; import { useToastActions } from '../ducks/toast';
import { isDownloaded } from '../../types/Attachment'; import { isDownloaded } from '../../types/Attachment';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; import { getMessageById } from '../../messages/getMessageById';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import type { import type {
ForwardMessageData, ForwardMessageData,
@ -117,10 +117,7 @@ function SmartForwardMessagesModalInner({
if (draft.originalMessageId == null) { if (draft.originalMessageId == null) {
return { draft, originalMessage: null }; return { draft, originalMessage: null };
} }
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(draft.originalMessageId);
draft.originalMessageId,
'doForwardMessages'
);
strictAssert(message, 'no message found'); strictAssert(message, 'no message found');
return { return {
draft, draft,

View file

@ -17,6 +17,7 @@ import {
messageReceiptTypeSchema, messageReceiptTypeSchema,
} from '../messageModifiers/MessageReceipts'; } from '../messageModifiers/MessageReceipts';
import { ReadStatus } from '../messages/MessageReadStatus'; import { ReadStatus } from '../messages/MessageReadStatus';
import { postSaveUpdates } from '../util/cleanup';
describe('MessageReceipts', () => { describe('MessageReceipts', () => {
let ourAci: AciString; let ourAci: AciString;
@ -81,6 +82,7 @@ describe('MessageReceipts', () => {
await DataWriter.saveMessage(messageAttributes, { await DataWriter.saveMessage(messageAttributes, {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
await Promise.all([ await Promise.all([
@ -158,6 +160,7 @@ describe('MessageReceipts', () => {
await DataWriter.saveMessage(messageAttributes, { await DataWriter.saveMessage(messageAttributes, {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
await DataWriter.saveEditedMessage(messageAttributes, ourAci, { await DataWriter.saveEditedMessage(messageAttributes, ourAci, {
conversationId: messageAttributes.conversationId, conversationId: messageAttributes.conversationId,

View file

@ -27,6 +27,7 @@ import { generateAci, generatePni } from '../../types/ServiceId';
import { DataReader, DataWriter } from '../../sql/Client'; import { DataReader, DataWriter } from '../../sql/Client';
import { getRandomBytes } from '../../Crypto'; import { getRandomBytes } from '../../Crypto';
import * as Bytes from '../../Bytes'; import * as Bytes from '../../Bytes';
import { postSaveUpdates } from '../../util/cleanup';
export const OUR_ACI = generateAci(); export const OUR_ACI = generateAci();
export const OUR_PNI = generatePni(); export const OUR_PNI = generatePni();
@ -213,7 +214,11 @@ export async function asymmetricRoundtripHarness(
try { try {
const targetOutputFile = path.join(outDir, 'backup.bin'); const targetOutputFile = path.join(outDir, 'backup.bin');
await DataWriter.saveMessages(before, { forceSave: true, ourAci: OUR_ACI }); await DataWriter.saveMessages(before, {
forceSave: true,
ourAci: OUR_ACI,
postSaveUpdates,
});
await backupsService.exportToDisk(targetOutputFile, options.backupLevel); await backupsService.exportToDisk(targetOutputFile, options.backupLevel);

View file

@ -17,7 +17,6 @@ import { clearData } from './helpers';
import { loadAllAndReinitializeRedux } from '../../services/allLoaders'; import { loadAllAndReinitializeRedux } from '../../services/allLoaders';
import { backupsService, BackupType } from '../../services/backups'; import { backupsService, BackupType } from '../../services/backups';
import { initialize as initializeExpiringMessageService } from '../../services/expiringMessagesDeletion'; import { initialize as initializeExpiringMessageService } from '../../services/expiringMessagesDeletion';
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
import { DataWriter } from '../../sql/Client'; import { DataWriter } from '../../sql/Client';
const { BACKUP_INTEGRATION_DIR } = process.env; const { BACKUP_INTEGRATION_DIR } = process.env;
@ -42,7 +41,7 @@ class MemoryStream extends InputStream {
describe('backup/integration', () => { describe('backup/integration', () => {
before(async () => { before(async () => {
await initializeExpiringMessageService(singleProtoJobQueue); await initializeExpiringMessageService();
}); });
beforeEach(async () => { beforeEach(async () => {

View file

@ -2,12 +2,14 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai'; import { assert } from 'chai';
import { v4 as generateUuid } from 'uuid'; import { v7 as generateUuid } from 'uuid';
import { DataWriter } from '../../sql/Client'; import { DataWriter } from '../../sql/Client';
import { SendStatus } from '../../messages/MessageSendState'; import { SendStatus } from '../../messages/MessageSendState';
import { IMAGE_PNG } from '../../types/MIME'; import { IMAGE_PNG } from '../../types/MIME';
import { generateAci, generatePni } from '../../types/ServiceId'; import { generateAci, generatePni } from '../../types/ServiceId';
import { postSaveUpdates } from '../../util/cleanup';
import { MessageModel } from '../../models/messages';
describe('Conversations', () => { describe('Conversations', () => {
async function resetConversationController(): Promise<void> { async function resetConversationController(): Promise<void> {
@ -40,6 +42,7 @@ describe('Conversations', () => {
profileSharing: true, profileSharing: true,
version: 0, version: 0,
expireTimerVersion: 1, expireTimerVersion: 1,
lastMessage: 'starting value',
}); });
await window.textsecure.storage.user.setCredentials({ await window.textsecure.storage.user.setCredentials({
@ -59,7 +62,7 @@ describe('Conversations', () => {
// Creating a fake message // Creating a fake message
const now = Date.now(); const now = Date.now();
let message = new window.Whisper.Message({ let message = new MessageModel({
attachments: [], attachments: [],
body: 'bananas', body: 'bananas',
conversationId: conversation.id, conversationId: conversation.id,
@ -84,12 +87,9 @@ describe('Conversations', () => {
await DataWriter.saveMessage(message.attributes, { await DataWriter.saveMessage(message.attributes, {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
message = window.MessageCache.__DEPRECATED$register( message = window.MessageCache.register(message);
message.id,
message,
'test'
);
await DataWriter.updateConversation(conversation.attributes); await DataWriter.updateConversation(conversation.attributes);
await conversation.updateLastMessage(); await conversation.updateLastMessage();

View file

@ -9,7 +9,7 @@ import type { AttachmentType } from '../../types/Attachment';
import type { CallbackResultType } from '../../textsecure/Types.d'; import type { CallbackResultType } from '../../textsecure/Types.d';
import type { ConversationModel } from '../../models/conversations'; import type { ConversationModel } from '../../models/conversations';
import type { MessageAttributesType } from '../../model-types.d'; import type { MessageAttributesType } from '../../model-types.d';
import type { MessageModel } from '../../models/messages'; import { MessageModel } from '../../models/messages';
import type { RawBodyRange } from '../../types/BodyRange'; import type { RawBodyRange } from '../../types/BodyRange';
import type { StorageAccessType } from '../../types/Storage.d'; import type { StorageAccessType } from '../../types/Storage.d';
import type { WebAPIType } from '../../textsecure/WebAPI'; import type { WebAPIType } from '../../textsecure/WebAPI';
@ -30,6 +30,9 @@ import {
TEXT_ATTACHMENT, TEXT_ATTACHMENT,
VIDEO_MP4, VIDEO_MP4,
} from '../../types/MIME'; } from '../../types/MIME';
import { getNotificationDataForMessage } from '../../util/getNotificationDataForMessage';
import { getNotificationTextForMessage } from '../../util/getNotificationTextForMessage';
import { send } from '../../messages/send';
describe('Message', () => { describe('Message', () => {
const STORAGE_KEYS_TO_RESTORE: Array<keyof StorageAccessType> = [ const STORAGE_KEYS_TO_RESTORE: Array<keyof StorageAccessType> = [
@ -54,17 +57,22 @@ describe('Message', () => {
const ourServiceId = generateAci(); const ourServiceId = generateAci();
function createMessage(attrs: Partial<MessageAttributesType>): MessageModel { function createMessage(attrs: Partial<MessageAttributesType>): MessageModel {
return new window.Whisper.Message({ const id = generateUuid();
id: generateUuid(), return window.MessageCache.register(
...attrs, new MessageModel({
received_at: Date.now(), id,
} as MessageAttributesType); ...attrs,
sent_at: Date.now(),
received_at: Date.now(),
} as MessageAttributesType)
);
} }
function createMessageAndGetNotificationData(attrs: { function createMessageAndGetNotificationData(attrs: {
[key: string]: unknown; [key: string]: unknown;
}) { }) {
return createMessage(attrs).getNotificationData(); const message = createMessage(attrs);
return getNotificationDataForMessage(message.attributes);
} }
before(async () => { before(async () => {
@ -184,7 +192,7 @@ describe('Message', () => {
editMessage: undefined, editMessage: undefined,
}); });
await message.send({ await send(message, {
promise, promise,
targetTimestamp: message.get('timestamp'), targetTimestamp: message.get('timestamp'),
}); });
@ -207,7 +215,7 @@ describe('Message', () => {
const message = createMessage({ type: 'outgoing', source }); const message = createMessage({ type: 'outgoing', source });
const promise = Promise.reject(new Error('foo bar')); const promise = Promise.reject(new Error('foo bar'));
await message.send({ await send(message, {
promise, promise,
targetTimestamp: message.get('timestamp'), targetTimestamp: message.get('timestamp'),
}); });
@ -224,7 +232,7 @@ describe('Message', () => {
errors: [new Error('baz qux')], errors: [new Error('baz qux')],
}; };
const promise = Promise.reject(result); const promise = Promise.reject(result);
await message.send({ await send(message, {
promise, promise,
targetTimestamp: message.get('timestamp'), targetTimestamp: message.get('timestamp'),
}); });
@ -675,18 +683,20 @@ describe('Message', () => {
describe('getNotificationText', () => { describe('getNotificationText', () => {
it("returns a notification's text", async () => { it("returns a notification's text", async () => {
const message = createMessage({
conversationId: (
await window.ConversationController.getOrCreateAndWait(
generateUuid(),
'private'
)
).id,
type: 'incoming',
source,
body: 'hello world',
});
assert.strictEqual( assert.strictEqual(
createMessage({ getNotificationTextForMessage(message.attributes),
conversationId: (
await window.ConversationController.getOrCreateAndWait(
generateUuid(),
'private'
)
).id,
type: 'incoming',
source,
body: 'hello world',
}).getNotificationText(),
'hello world' 'hello world'
); );
}); });
@ -698,24 +708,24 @@ describe('Message', () => {
return false; return false;
}, },
}); });
const message = createMessage({
conversationId: (
await window.ConversationController.getOrCreateAndWait(
generateUuid(),
'private'
)
).id,
type: 'incoming',
source,
attachments: [
{
contentType: IMAGE_PNG,
size: 0,
},
],
});
assert.strictEqual( assert.strictEqual(
createMessage({ getNotificationTextForMessage(message.attributes),
conversationId: (
await window.ConversationController.getOrCreateAndWait(
generateUuid(),
'private'
)
).id,
type: 'incoming',
source,
attachments: [
{
contentType: IMAGE_PNG,
size: 0,
},
],
}).getNotificationText(),
'📷 Photo' '📷 Photo'
); );
}); });
@ -728,23 +738,25 @@ describe('Message', () => {
}, },
}); });
const message = createMessage({
conversationId: (
await window.ConversationController.getOrCreateAndWait(
generateUuid(),
'private'
)
).id,
type: 'incoming',
source,
attachments: [
{
contentType: IMAGE_PNG,
size: 0,
},
],
});
assert.strictEqual( assert.strictEqual(
createMessage({ getNotificationTextForMessage(message.attributes),
conversationId: (
await window.ConversationController.getOrCreateAndWait(
generateUuid(),
'private'
)
).id,
type: 'incoming',
source,
attachments: [
{
contentType: IMAGE_PNG,
size: 0,
},
],
}).getNotificationText(),
'Photo' 'Photo'
); );
}); });

View file

@ -22,6 +22,7 @@ import { type AttachmentType, AttachmentVariant } from '../../types/Attachment';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { AttachmentDownloadSource } from '../../sql/Interface'; import { AttachmentDownloadSource } from '../../sql/Interface';
import { getAttachmentCiphertextLength } from '../../AttachmentCrypto'; import { getAttachmentCiphertextLength } from '../../AttachmentCrypto';
import { postSaveUpdates } from '../../util/cleanup';
function composeJob({ function composeJob({
messageId, messageId,
@ -119,6 +120,7 @@ describe('AttachmentDownloadManager/JobManager', () => {
{ {
ourAci: 'ourAci' as AciString, ourAci: 'ourAci' as AciString,
forceSave: true, forceSave: true,
postSaveUpdates,
} }
); );
await downloadManager?.addJob({ await downloadManager?.addJob({

View file

@ -3,8 +3,6 @@
import { assert } from 'chai'; import { assert } from 'chai';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import type { MessageAttributesType } from '../../model-types.d';
import { DataReader, DataWriter } from '../../sql/Client';
import { MessageModel } from '../../models/messages'; import { MessageModel } from '../../models/messages';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
@ -56,25 +54,33 @@ describe('MessageCache', () => {
type: 'outgoing', type: 'outgoing',
}); });
message1 = mc.__DEPRECATED$register(message1.id, message1, 'test'); message1 = mc.register(message1);
message2 = mc.__DEPRECATED$register(message2.id, message2, 'test'); message2 = mc.register(message2);
// We deliberately register this message twice for testing. // We deliberately register this message twice for testing.
message2 = mc.__DEPRECATED$register(message2.id, message2, 'test'); message2 = mc.register(message2);
mc.__DEPRECATED$register(message3.id, message3, 'test'); mc.register(message3);
const filteredMessage = await mc.findBySentAt(1234, () => true); const filteredMessage = await mc.findBySentAt(1234, () => true);
assert.deepEqual(filteredMessage, message1.attributes, 'first'); assert.deepEqual(
filteredMessage?.attributes,
message1.attributes,
'first'
);
mc.__DEPRECATED$unregister(message1.id); mc.unregister(message1.id);
const filteredMessage2 = await mc.findBySentAt(1234, () => true); const filteredMessage2 = await mc.findBySentAt(1234, () => true);
assert.deepEqual(filteredMessage2, message2.attributes, 'second'); assert.deepEqual(
filteredMessage2?.attributes,
message2.attributes,
'second'
);
}); });
}); });
describe('__DEPRECATED$register: syncing with backbone', () => { describe('register: syncing with backbone', () => {
it('backbone to redux', () => { it('backbone to redux', () => {
const message1 = new MessageModel({ const message1 = new MessageModel({
conversationId: 'xyz', conversationId: 'xyz',
@ -85,65 +91,33 @@ describe('MessageCache', () => {
timestamp: Date.now(), timestamp: Date.now(),
type: 'outgoing', type: 'outgoing',
}); });
const messageFromController = window.MessageCache.__DEPRECATED$register( const messageFromController = window.MessageCache.register(message1);
message1.id,
message1,
'test'
);
assert.strictEqual( assert.strictEqual(
message1, message1,
messageFromController, messageFromController,
'same objects from mc.__DEPRECATED$register' 'same objects from mc.register'
); );
const messageById = window.MessageCache.__DEPRECATED$getById( const messageInCache = window.MessageCache.getById(message1.id);
message1.id, assert.strictEqual(
'test' message1,
messageInCache,
'same objects from mc.getById'
); );
assert.strictEqual(message1, messageById, 'same objects from mc.getById');
const messageInCache = window.MessageCache.accessAttributes(message1.id);
strictAssert(messageInCache, 'no message found');
assert.deepEqual( assert.deepEqual(
message1.attributes, message1.attributes,
messageInCache, messageInCache?.attributes,
'same attributes as in cache' 'same attributes as in cache'
); );
message1.set({ body: 'test2' }); message1.set({ body: 'test2' });
assert.equal(message1.attributes.body, 'test2', 'message model updated'); assert.equal(message1.attributes.body, 'test2', 'message model updated');
assert.equal( assert.equal(
messageById?.attributes.body, messageInCache?.attributes.body,
'test2', 'test2',
'old reference from messageById was updated' 'old reference from messageById was updated'
); );
assert.equal(
messageInCache.body,
'test1',
'old cache reference not updated'
);
const newMessageById = window.MessageCache.__DEPRECATED$getById(
message1.id,
'test'
);
assert.deepEqual(
message1.attributes,
newMessageById?.attributes,
'same attributes from mc.getById (2)'
);
const newMessageInCache = window.MessageCache.accessAttributes(
message1.id
);
strictAssert(newMessageInCache, 'no message found');
assert.deepEqual(
message1.attributes,
newMessageInCache,
'same attributes as in cache (2)'
);
}); });
it('redux to backbone (working with models)', () => { it('redux to backbone (working with models)', () => {
@ -157,271 +131,28 @@ describe('MessageCache', () => {
type: 'outgoing', type: 'outgoing',
}); });
window.MessageCache.toMessageAttributes(message.attributes); const messageFromController = window.MessageCache.register(message);
const messageFromController = window.MessageCache.__DEPRECATED$register( assert.strictEqual(
message.id,
message,
'test'
);
assert.notStrictEqual(
message, message,
messageFromController, messageFromController,
'mc.__DEPRECATED$register returns existing but it is not the same reference' 'mc.register returns existing but it is not the same reference'
); );
assert.deepEqual( assert.deepEqual(
message.attributes, message.attributes,
messageFromController.attributes, messageFromController.attributes,
'mc.__DEPRECATED$register returns existing and is the same attributes' 'mc.register returns existing and is the same attributes'
); );
messageFromController.set({ body: 'test2' }); message.set({ body: 'test2' });
assert.notEqual( const messageInCache = window.MessageCache.getById(message.id);
message.get('body'),
messageFromController.get('body'),
'new model is not equal to old model'
);
const messageInCache = window.MessageCache.accessAttributes(message.id);
strictAssert(messageInCache, 'no message found'); strictAssert(messageInCache, 'no message found');
assert.equal( assert.equal(
messageFromController.get('body'), messageFromController.get('body'),
messageInCache.body, messageInCache.get('body'),
'new update is in cache' 'new update is in cache'
); );
assert.isUndefined(
messageFromController.get('storyReplyContext'),
'storyReplyContext is undefined'
);
window.MessageCache.setAttributes({
messageId: message.id,
messageAttributes: {
storyReplyContext: {
attachment: undefined,
authorAci: undefined,
messageId: 'test123',
},
},
skipSaveToDatabase: true,
});
// This works because we refresh the model whenever an attribute changes
// but this should log a warning.
assert.equal(
messageFromController.get('storyReplyContext')?.messageId,
'test123',
'storyReplyContext was updated (stale model)'
);
const newMessageFromController =
window.MessageCache.__DEPRECATED$register(message.id, message, 'test');
assert.equal(
newMessageFromController.get('storyReplyContext')?.messageId,
'test123',
'storyReplyContext was updated (not stale)'
);
});
it('redux to backbone (working with attributes)', () => {
it('sets the attributes and returns a fresh copy', () => {
const mc = new MessageCache();
const messageAttributes: MessageAttributesType = {
conversationId: uuid(),
id: uuid(),
received_at: 1,
sent_at: Date.now(),
timestamp: Date.now(),
type: 'incoming',
};
const messageModel = mc.__DEPRECATED$register(
messageAttributes.id,
messageAttributes,
'test/updateAttributes'
);
assert.deepEqual(
messageAttributes,
messageModel.attributes,
'initial attributes matches message model'
);
const proposedStoryReplyContext = {
attachment: undefined,
authorAci: undefined,
messageId: 'test123',
};
assert.notDeepEqual(
messageModel.attributes.storyReplyContext,
proposedStoryReplyContext,
'attributes were changed outside of the message model'
);
mc.setAttributes({
messageId: messageAttributes.id,
messageAttributes: {
storyReplyContext: proposedStoryReplyContext,
},
skipSaveToDatabase: true,
});
const nextMessageAttributes = mc.accessAttributesOrThrow(
'test',
messageAttributes.id
);
assert.notDeepEqual(
messageAttributes,
nextMessageAttributes,
'initial attributes are stale'
);
assert.notDeepEqual(
messageAttributes.storyReplyContext,
proposedStoryReplyContext,
'initial attributes are stale 2'
);
assert.deepEqual(
nextMessageAttributes.storyReplyContext,
proposedStoryReplyContext,
'fresh attributes match what was proposed'
);
assert.notStrictEqual(
nextMessageAttributes.storyReplyContext,
proposedStoryReplyContext,
'fresh attributes are not the same reference as proposed attributes'
);
assert.deepEqual(
messageModel.attributes,
nextMessageAttributes,
'model was updated'
);
assert.equal(
messageModel.get('storyReplyContext')?.messageId,
'test123',
'storyReplyContext in model is set correctly'
);
});
});
});
describe('accessAttributes', () => {
it('gets the attributes if they exist', () => {
const mc = new MessageCache();
const messageAttributes: MessageAttributesType = {
conversationId: uuid(),
id: uuid(),
received_at: 1,
sent_at: Date.now(),
timestamp: Date.now(),
type: 'incoming',
};
mc.toMessageAttributes(messageAttributes);
const accessAttributes = mc.accessAttributes(messageAttributes.id);
assert.deepEqual(
accessAttributes,
messageAttributes,
'attributes returned have the same values'
);
assert.notStrictEqual(
accessAttributes,
messageAttributes,
'attributes returned are not the same references'
);
const undefinedMessage = mc.accessAttributes(uuid());
assert.isUndefined(undefinedMessage, 'access did not find message');
});
});
describe('setAttributes', () => {
it('saves the new attributes to the database', async () => {
const mc = new MessageCache();
const ourAci = generateAci();
const id = uuid();
const messageAttributes: MessageAttributesType = {
conversationId: uuid(),
id,
received_at: 1,
sent_at: Date.now(),
timestamp: Date.now(),
type: 'incoming',
};
await DataWriter.saveMessage(messageAttributes, {
forceSave: true,
ourAci,
});
const changes = {
received_at: 2,
};
const newAttributes = {
...messageAttributes,
...changes,
};
mc.toMessageAttributes(messageAttributes);
await mc.setAttributes({
messageId: id,
messageAttributes: changes,
skipSaveToDatabase: false,
});
const messageFromDatabase = await DataReader.getMessageById(id);
assert.deepEqual(newAttributes, messageFromDatabase);
});
});
describe('accessAttributesOrThrow', () => {
it('accesses the attributes or throws if they do not exist', () => {
const mc = new MessageCache();
const messageAttributes: MessageAttributesType = {
conversationId: uuid(),
id: uuid(),
received_at: 1,
sent_at: Date.now(),
timestamp: Date.now(),
type: 'incoming',
};
mc.toMessageAttributes(messageAttributes);
const accessAttributes = mc.accessAttributesOrThrow(
'tests.1',
messageAttributes.id
);
assert.deepEqual(
accessAttributes,
messageAttributes,
'attributes returned have the same values'
);
assert.notStrictEqual(
accessAttributes,
messageAttributes,
'attributes returned are not the same references'
);
assert.throws(() => {
mc.accessAttributesOrThrow('tests.2', uuid());
});
}); });
}); });
}); });

View file

@ -9,6 +9,7 @@ import { generateAci } from '../../types/ServiceId';
import { DurationInSeconds } from '../../util/durations'; import { DurationInSeconds } from '../../util/durations';
import type { MessageAttributesType } from '../../model-types.d'; import type { MessageAttributesType } from '../../model-types.d';
import { postSaveUpdates } from '../../util/cleanup';
const { _getAllMessages, getConversationMessageStats } = DataReader; const { _getAllMessages, getConversationMessageStats } = DataReader;
const { removeAll, saveMessages } = DataWriter; const { removeAll, saveMessages } = DataWriter;
@ -56,6 +57,7 @@ describe('sql/conversationSummary', () => {
await saveMessages([message1, message2, message3], { await saveMessages([message1, message2, message3], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 3); assert.lengthOf(await _getAllMessages(), 3);
@ -109,6 +111,7 @@ describe('sql/conversationSummary', () => {
await saveMessages([message1, message2, message3], { await saveMessages([message1, message2, message3], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 3); assert.lengthOf(await _getAllMessages(), 3);
@ -199,6 +202,7 @@ describe('sql/conversationSummary', () => {
{ {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
} }
); );
@ -306,6 +310,7 @@ describe('sql/conversationSummary', () => {
{ {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
} }
); );
@ -355,6 +360,7 @@ describe('sql/conversationSummary', () => {
await saveMessages([message1, message2], { await saveMessages([message1, message2], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 2); assert.lengthOf(await _getAllMessages(), 2);
@ -404,6 +410,7 @@ describe('sql/conversationSummary', () => {
await saveMessages([message1, message2], { await saveMessages([message1, message2], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 2); assert.lengthOf(await _getAllMessages(), 2);
@ -446,6 +453,7 @@ describe('sql/conversationSummary', () => {
await saveMessages([message1, message2], { await saveMessages([message1, message2], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 2); assert.lengthOf(await _getAllMessages(), 2);
@ -490,6 +498,7 @@ describe('sql/conversationSummary', () => {
await saveMessages([message1, message2], { await saveMessages([message1, message2], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 2); assert.lengthOf(await _getAllMessages(), 2);
@ -549,6 +558,7 @@ describe('sql/conversationSummary', () => {
await saveMessages([message1, message2], { await saveMessages([message1, message2], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 2); assert.lengthOf(await _getAllMessages(), 2);

View file

@ -8,6 +8,7 @@ import { DataReader, DataWriter } from '../../sql/Client';
import { generateAci } from '../../types/ServiceId'; import { generateAci } from '../../types/ServiceId';
import type { MessageAttributesType } from '../../model-types.d'; import type { MessageAttributesType } from '../../model-types.d';
import { postSaveUpdates } from '../../util/cleanup';
const { _getAllMessages, searchMessages } = DataReader; const { _getAllMessages, searchMessages } = DataReader;
const { removeAll, saveMessages, saveMessage } = DataWriter; const { removeAll, saveMessages, saveMessage } = DataWriter;
@ -54,6 +55,7 @@ describe('sql/searchMessages', () => {
await saveMessages([message1, message2, message3], { await saveMessages([message1, message2, message3], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 3); assert.lengthOf(await _getAllMessages(), 3);
@ -63,7 +65,7 @@ describe('sql/searchMessages', () => {
assert.strictEqual(searchResults[0].id, message2.id); assert.strictEqual(searchResults[0].id, message2.id);
message3.body = 'message 3 - unique string'; message3.body = 'message 3 - unique string';
await saveMessage(message3, { ourAci }); await saveMessage(message3, { ourAci, postSaveUpdates });
const searchResults2 = await searchMessages({ query: 'unique' }); const searchResults2 = await searchMessages({ query: 'unique' });
assert.lengthOf(searchResults2, 2); assert.lengthOf(searchResults2, 2);
@ -110,6 +112,7 @@ describe('sql/searchMessages', () => {
await saveMessages([message1, message2, message3], { await saveMessages([message1, message2, message3], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 3); assert.lengthOf(await _getAllMessages(), 3);
@ -119,7 +122,7 @@ describe('sql/searchMessages', () => {
assert.strictEqual(searchResults[0].id, message1.id); assert.strictEqual(searchResults[0].id, message1.id);
message1.body = 'message 3 - unique string'; message1.body = 'message 3 - unique string';
await saveMessage(message3, { ourAci }); await saveMessage(message3, { ourAci, postSaveUpdates });
const searchResults2 = await searchMessages({ query: 'unique' }); const searchResults2 = await searchMessages({ query: 'unique' });
assert.lengthOf(searchResults2, 1); assert.lengthOf(searchResults2, 1);
@ -165,6 +168,7 @@ describe('sql/searchMessages', () => {
await saveMessages([message1, message2, message3], { await saveMessages([message1, message2, message3], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 3); assert.lengthOf(await _getAllMessages(), 3);
@ -174,7 +178,7 @@ describe('sql/searchMessages', () => {
assert.strictEqual(searchResults[0].id, message1.id); assert.strictEqual(searchResults[0].id, message1.id);
message1.body = 'message 3 - unique string'; message1.body = 'message 3 - unique string';
await saveMessage(message3, { ourAci }); await saveMessage(message3, { ourAci, postSaveUpdates });
const searchResults2 = await searchMessages({ query: 'unique' }); const searchResults2 = await searchMessages({ query: 'unique' });
assert.lengthOf(searchResults2, 1); assert.lengthOf(searchResults2, 1);
@ -211,6 +215,7 @@ describe('sql/searchMessages', () => {
await saveMessages([message1, message2], { await saveMessages([message1, message2], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 2); assert.lengthOf(await _getAllMessages(), 2);
@ -251,6 +256,7 @@ describe('sql/searchMessages/withMentions', () => {
await saveMessages(messages, { await saveMessages(messages, {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
return messages; return messages;
} }

View file

@ -8,6 +8,7 @@ import { DataReader, DataWriter } from '../../sql/Client';
import { generateAci } from '../../types/ServiceId'; import { generateAci } from '../../types/ServiceId';
import type { MessageAttributesType } from '../../model-types.d'; import type { MessageAttributesType } from '../../model-types.d';
import { postSaveUpdates } from '../../util/cleanup';
const { _getAllMessages, getCallHistoryMessageByCallId } = DataReader; const { _getAllMessages, getCallHistoryMessageByCallId } = DataReader;
const { removeAll, saveMessages } = DataWriter; const { removeAll, saveMessages } = DataWriter;
@ -37,6 +38,7 @@ describe('sql/getCallHistoryMessageByCallId', () => {
await saveMessages([callHistoryMessage], { await saveMessages([callHistoryMessage], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
const allMessages = await _getAllMessages(); const allMessages = await _getAllMessages();

View file

@ -8,6 +8,7 @@ import { generateAci } from '../../types/ServiceId';
import { DataReader, DataWriter } from '../../sql/Client'; import { DataReader, DataWriter } from '../../sql/Client';
import type { MessageAttributesType } from '../../model-types'; import type { MessageAttributesType } from '../../model-types';
import { postSaveUpdates } from '../../util/cleanup';
const { _getAllMessages, getMessagesBetween } = DataReader; const { _getAllMessages, getMessagesBetween } = DataReader;
const { saveMessages, _removeAllMessages } = DataWriter; const { saveMessages, _removeAllMessages } = DataWriter;
@ -45,6 +46,7 @@ describe('sql/getMessagesBetween', () => {
await saveMessages([message1, message2, message3, message4, message5], { await saveMessages([message1, message2, message3, message4, message5], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 5); assert.lengthOf(await _getAllMessages(), 5);
@ -93,6 +95,7 @@ describe('sql/getMessagesBetween', () => {
await saveMessages([message1, message2, message3, message5], { await saveMessages([message1, message2, message3, message5], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 4); assert.lengthOf(await _getAllMessages(), 4);

View file

@ -8,6 +8,7 @@ import { DataReader, DataWriter } from '../../sql/Client';
import { generateAci } from '../../types/ServiceId'; import { generateAci } from '../../types/ServiceId';
import type { MessageAttributesType } from '../../model-types'; import type { MessageAttributesType } from '../../model-types';
import { postSaveUpdates } from '../../util/cleanup';
const { _getAllMessages, getNearbyMessageFromDeletedSet } = DataReader; const { _getAllMessages, getNearbyMessageFromDeletedSet } = DataReader;
const { saveMessages, _removeAllMessages } = DataWriter; const { saveMessages, _removeAllMessages } = DataWriter;
@ -45,6 +46,7 @@ describe('sql/getNearbyMessageFromDeletedSet', () => {
await saveMessages([message1, message2, message3, message4, message5], { await saveMessages([message1, message2, message3, message4, message5], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 5); assert.lengthOf(await _getAllMessages(), 5);

View file

@ -8,6 +8,7 @@ import { DataReader, DataWriter } from '../../sql/Client';
import { generateAci } from '../../types/ServiceId'; import { generateAci } from '../../types/ServiceId';
import type { MessageAttributesType } from '../../model-types.d'; import type { MessageAttributesType } from '../../model-types.d';
import { postSaveUpdates } from '../../util/cleanup';
const { _getAllMessages, getRecentStoryReplies } = DataReader; const { _getAllMessages, getRecentStoryReplies } = DataReader;
const { removeAll, saveMessages } = DataWriter; const { removeAll, saveMessages } = DataWriter;
@ -91,6 +92,7 @@ describe('sql/getRecentStoryReplies', () => {
{ {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
} }
); );

View file

@ -12,6 +12,7 @@ import { ReactionReadStatus } from '../../types/Reactions';
import { DurationInSeconds } from '../../util/durations'; import { DurationInSeconds } from '../../util/durations';
import type { MessageAttributesType } from '../../model-types.d'; import type { MessageAttributesType } from '../../model-types.d';
import { ReadStatus } from '../../messages/MessageReadStatus'; import { ReadStatus } from '../../messages/MessageReadStatus';
import { postSaveUpdates } from '../../util/cleanup';
const { _getAllReactions, _getAllMessages, getTotalUnreadForConversation } = const { _getAllReactions, _getAllMessages, getTotalUnreadForConversation } =
DataReader; DataReader;
@ -126,6 +127,7 @@ describe('sql/markRead', () => {
{ {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
} }
); );
@ -290,6 +292,7 @@ describe('sql/markRead', () => {
{ {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
} }
); );
@ -392,6 +395,7 @@ describe('sql/markRead', () => {
await saveMessages([message1, message2, message3, message4, message5], { await saveMessages([message1, message2, message3, message4, message5], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.strictEqual( assert.strictEqual(
@ -518,6 +522,7 @@ describe('sql/markRead', () => {
{ {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
} }
); );
assert.lengthOf(await _getAllMessages(), pad.length + 5); assert.lengthOf(await _getAllMessages(), pad.length + 5);
@ -673,6 +678,7 @@ describe('sql/markRead', () => {
await saveMessages([message1, message2, message3, message4, message5], { await saveMessages([message1, message2, message3, message4, message5], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 5); assert.lengthOf(await _getAllMessages(), 5);
@ -823,6 +829,7 @@ describe('sql/markRead', () => {
await saveMessages([message1, message2, message3, message4], { await saveMessages([message1, message2, message3, message4], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 4); assert.lengthOf(await _getAllMessages(), 4);

View file

@ -7,7 +7,7 @@ import { v4 as generateUuid } from 'uuid';
import { DataReader, DataWriter } from '../../sql/Client'; import { DataReader, DataWriter } from '../../sql/Client';
import { generateAci } from '../../types/ServiceId'; import { generateAci } from '../../types/ServiceId';
import { constantTimeEqual, getRandomBytes } from '../../Crypto'; import { constantTimeEqual, getRandomBytes } from '../../Crypto';
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue'; import { cleanupMessages, postSaveUpdates } from '../../util/cleanup';
const { const {
_getAllSentProtoMessageIds, _getAllSentProtoMessageIds,
@ -128,7 +128,7 @@ describe('sql/sendLog', () => {
timestamp, timestamp,
type: 'outgoing', type: 'outgoing',
}, },
{ forceSave: true, ourAci } { forceSave: true, ourAci, postSaveUpdates }
); );
const bytes = getRandomBytes(128); const bytes = getRandomBytes(128);
@ -152,7 +152,7 @@ describe('sql/sendLog', () => {
assert.strictEqual(actual.timestamp, proto.timestamp); assert.strictEqual(actual.timestamp, proto.timestamp);
await removeMessage(id, { singleProtoJobQueue }); await removeMessage(id, { cleanupMessages });
assert.lengthOf(await getAllSentProtos(), 0); assert.lengthOf(await getAllSentProtos(), 0);
}); });

View file

@ -8,6 +8,7 @@ import { DataReader, DataWriter } from '../../sql/Client';
import { generateAci } from '../../types/ServiceId'; import { generateAci } from '../../types/ServiceId';
import type { MessageAttributesType } from '../../model-types.d'; import type { MessageAttributesType } from '../../model-types.d';
import { postSaveUpdates } from '../../util/cleanup';
const { _getAllMessages, getAllStories } = DataReader; const { _getAllMessages, getAllStories } = DataReader;
const { removeAll, saveMessages } = DataWriter; const { removeAll, saveMessages } = DataWriter;
@ -80,6 +81,7 @@ describe('sql/stories', () => {
await saveMessages([story1, story2, story3, story4, story5], { await saveMessages([story1, story2, story3, story4, story5], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 5); assert.lengthOf(await _getAllMessages(), 5);
@ -217,6 +219,7 @@ describe('sql/stories', () => {
{ {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
} }
); );

View file

@ -9,6 +9,7 @@ import { generateAci } from '../../types/ServiceId';
import type { MessageAttributesType } from '../../model-types.d'; import type { MessageAttributesType } from '../../model-types.d';
import { ReadStatus } from '../../messages/MessageReadStatus'; import { ReadStatus } from '../../messages/MessageReadStatus';
import { postSaveUpdates } from '../../util/cleanup';
const { const {
_getAllMessages, _getAllMessages,
@ -86,6 +87,7 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3, message4, message5], { await saveMessages([message1, message2, message3, message4, message5], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 5); assert.lengthOf(await _getAllMessages(), 5);
@ -144,6 +146,7 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3], { await saveMessages([message1, message2, message3], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 3); assert.lengthOf(await _getAllMessages(), 3);
@ -199,6 +202,7 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3], { await saveMessages([message1, message2, message3], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 3); assert.lengthOf(await _getAllMessages(), 3);
@ -251,6 +255,7 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3], { await saveMessages([message1, message2, message3], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 3); assert.lengthOf(await _getAllMessages(), 3);
@ -305,6 +310,7 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3], { await saveMessages([message1, message2, message3], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 3); assert.lengthOf(await _getAllMessages(), 3);
@ -363,6 +369,7 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3], { await saveMessages([message1, message2, message3], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 3); assert.lengthOf(await _getAllMessages(), 3);
@ -442,6 +449,7 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3, message4, message5], { await saveMessages([message1, message2, message3, message4, message5], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 5); assert.lengthOf(await _getAllMessages(), 5);
@ -499,6 +507,7 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3], { await saveMessages([message1, message2, message3], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 3); assert.lengthOf(await _getAllMessages(), 3);
@ -552,6 +561,7 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3], { await saveMessages([message1, message2, message3], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 3); assert.lengthOf(await _getAllMessages(), 3);
@ -608,6 +618,7 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3], { await saveMessages([message1, message2, message3], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 3); assert.lengthOf(await _getAllMessages(), 3);
@ -662,6 +673,7 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3], { await saveMessages([message1, message2, message3], {
forceSave: true, forceSave: true,
ourAci, ourAci,
postSaveUpdates,
}); });
assert.lengthOf(await _getAllMessages(), 3); assert.lengthOf(await _getAllMessages(), 3);
@ -781,7 +793,7 @@ describe('sql/timelineFetches', () => {
newestInStory, newestInStory,
newest, newest,
], ],
{ forceSave: true, ourAci } { forceSave: true, ourAci, postSaveUpdates }
); );
assert.lengthOf(await _getAllMessages(), 8); assert.lengthOf(await _getAllMessages(), 8);
@ -873,7 +885,11 @@ describe('sql/timelineFetches', () => {
} }
); );
await saveMessages(formattedMessages, { forceSave: true, ourAci }); await saveMessages(formattedMessages, {
forceSave: true,
ourAci,
postSaveUpdates,
});
assert.lengthOf(await _getAllMessages(), 4); assert.lengthOf(await _getAllMessages(), 4);

View file

@ -27,6 +27,8 @@ import { actions, getEmptyState } from '../../../state/ducks/stories';
import { noopAction } from '../../../state/ducks/noop'; import { noopAction } from '../../../state/ducks/noop';
import { reducer as rootReducer } from '../../../state/reducer'; import { reducer as rootReducer } from '../../../state/reducer';
import { dropNull } from '../../../util/dropNull'; import { dropNull } from '../../../util/dropNull';
import { postSaveUpdates } from '../../../util/cleanup';
import { MessageModel } from '../../../models/messages';
describe('both/state/ducks/stories', () => { describe('both/state/ducks/stories', () => {
const getEmptyRootState = () => ({ const getEmptyRootState = () => ({
@ -862,11 +864,7 @@ describe('both/state/ducks/stories', () => {
const storyId = generateUuid(); const storyId = generateUuid();
const messageAttributes = getStoryMessage(storyId); const messageAttributes = getStoryMessage(storyId);
window.MessageCache.__DEPRECATED$register( window.MessageCache.register(new MessageModel(messageAttributes));
storyId,
messageAttributes,
'test'
);
const dispatch = sinon.spy(); const dispatch = sinon.spy();
await queueStoryDownload(storyId)(dispatch, getEmptyRootState, null); await queueStoryDownload(storyId)(dispatch, getEmptyRootState, null);
@ -888,11 +886,7 @@ describe('both/state/ducks/stories', () => {
], ],
}; };
window.MessageCache.__DEPRECATED$register( window.MessageCache.register(new MessageModel(messageAttributes));
storyId,
messageAttributes,
'test'
);
const dispatch = sinon.spy(); const dispatch = sinon.spy();
await queueStoryDownload(storyId)(dispatch, getEmptyRootState, null); await queueStoryDownload(storyId)(dispatch, getEmptyRootState, null);
@ -914,11 +908,7 @@ describe('both/state/ducks/stories', () => {
], ],
}; };
window.MessageCache.__DEPRECATED$register( window.MessageCache.register(new MessageModel(messageAttributes));
storyId,
messageAttributes,
'test'
);
const dispatch = sinon.spy(); const dispatch = sinon.spy();
await queueStoryDownload(storyId)(dispatch, getEmptyRootState, null); await queueStoryDownload(storyId)(dispatch, getEmptyRootState, null);
@ -941,6 +931,7 @@ describe('both/state/ducks/stories', () => {
await DataWriter.saveMessage(messageAttributes, { await DataWriter.saveMessage(messageAttributes, {
forceSave: true, forceSave: true,
ourAci: generateAci(), ourAci: generateAci(),
postSaveUpdates,
}); });
const rootState = getEmptyRootState(); const rootState = getEmptyRootState();
@ -963,11 +954,7 @@ describe('both/state/ducks/stories', () => {
}, },
}); });
window.MessageCache.__DEPRECATED$register( window.MessageCache.register(new MessageModel(messageAttributes));
storyId,
messageAttributes,
'test'
);
const dispatch = sinon.spy(); const dispatch = sinon.spy();
await queueStoryDownload(storyId)(dispatch, getState, null); await queueStoryDownload(storyId)(dispatch, getState, null);
@ -1007,6 +994,7 @@ describe('both/state/ducks/stories', () => {
await DataWriter.saveMessage(messageAttributes, { await DataWriter.saveMessage(messageAttributes, {
forceSave: true, forceSave: true,
ourAci: generateAci(), ourAci: generateAci(),
postSaveUpdates,
}); });
const rootState = getEmptyRootState(); const rootState = getEmptyRootState();
@ -1029,11 +1017,7 @@ describe('both/state/ducks/stories', () => {
}, },
}); });
window.MessageCache.__DEPRECATED$register( window.MessageCache.register(new MessageModel(messageAttributes));
storyId,
messageAttributes,
'test'
);
const dispatch = sinon.spy(); const dispatch = sinon.spy();
await queueStoryDownload(storyId)(dispatch, getState, null); await queueStoryDownload(storyId)(dispatch, getState, null);

View file

@ -12,7 +12,7 @@ import { SignalProtocolStore } from '../../SignalProtocolStore';
import type { ConversationModel } from '../../models/conversations'; import type { ConversationModel } from '../../models/conversations';
import * as KeyChangeListener from '../../textsecure/KeyChangeListener'; import * as KeyChangeListener from '../../textsecure/KeyChangeListener';
import * as Bytes from '../../Bytes'; import * as Bytes from '../../Bytes';
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue'; import { cleanupMessages } from '../../util/cleanup';
describe('KeyChangeListener', () => { describe('KeyChangeListener', () => {
let oldNumberId: string | undefined; let oldNumberId: string | undefined;
@ -71,7 +71,7 @@ describe('KeyChangeListener', () => {
afterEach(async () => { afterEach(async () => {
await DataWriter.removeMessagesInConversation(convo.id, { await DataWriter.removeMessagesInConversation(convo.id, {
logId: ourServiceIdWithKeyChange, logId: ourServiceIdWithKeyChange,
singleProtoJobQueue, cleanupMessages,
}); });
await DataWriter.removeConversation(convo.id); await DataWriter.removeConversation(convo.id);
@ -109,7 +109,7 @@ describe('KeyChangeListener', () => {
afterEach(async () => { afterEach(async () => {
await DataWriter.removeMessagesInConversation(groupConvo.id, { await DataWriter.removeMessagesInConversation(groupConvo.id, {
logId: ourServiceIdWithKeyChange, logId: ourServiceIdWithKeyChange,
singleProtoJobQueue, cleanupMessages,
}); });
await DataWriter.removeConversation(groupConvo.id); await DataWriter.removeConversation(groupConvo.id);
}); });

View file

@ -7,6 +7,7 @@ import { _migrateMessageData as migrateMessageData } from '../../messages/migrat
import type { MessageAttributesType } from '../../model-types'; import type { MessageAttributesType } from '../../model-types';
import { DataReader, DataWriter } from '../../sql/Client'; import { DataReader, DataWriter } from '../../sql/Client';
import { generateAci } from '../../types/ServiceId'; import { generateAci } from '../../types/ServiceId';
import { postSaveUpdates } from '../../util/cleanup';
function composeMessage(timestamp: number): MessageAttributesType { function composeMessage(timestamp: number): MessageAttributesType {
return { return {
@ -39,6 +40,7 @@ describe('utils/migrateMessageData', async () => {
await DataWriter.saveMessages(messages, { await DataWriter.saveMessages(messages, {
forceSave: true, forceSave: true,
ourAci: generateAci(), ourAci: generateAci(),
postSaveUpdates,
}); });
const result = await migrateMessageData({ const result = await migrateMessageData({

View file

@ -274,7 +274,7 @@ describe('pnp/PNI Change', function (this: Mocha.Suite) {
} }
}); });
it('shows identity and phone number change on send to contact when e165 has changed owners', async () => { it('shows identity and phone number change on send to contact when e164 has changed owners', async () => {
const { desktop, phone } = bootstrap; const { desktop, phone } = bootstrap;
const window = await app.getWindow(); const window = await app.getWindow();

View file

@ -0,0 +1,159 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { mapValues, pick } from 'lodash';
import type { CustomError } from '../../textsecure/Types';
import type { MessageAttributesType } from '../../model-types';
import * as log from '../../logging/log';
import * as Errors from '../../types/errors';
import {
getChangesForPropAtTimestamp,
getPropForTimestamp,
} from '../../util/editHelpers';
import {
isSent,
SendActionType,
sendStateReducer,
someRecipientSendStatus,
} from '../../messages/MessageSendState';
import { isStory } from '../../messages/helpers';
import {
notificationService,
NotificationType,
} from '../../services/notifications';
import type { MessageModel } from '../../models/messages';
export async function saveErrorsOnMessage(
message: MessageModel,
providedErrors: Error | Array<Error>,
options: { skipSave?: boolean } = {}
): Promise<void> {
const { skipSave } = options;
let errors: Array<CustomError>;
if (!(providedErrors instanceof Array)) {
errors = [providedErrors];
} else {
errors = providedErrors;
}
errors.forEach(e => {
log.error('Message.saveErrors:', Errors.toLogFormat(e));
});
errors = errors.map(e => {
// Note: in our environment, instanceof can be scary, so we have a backup check
// (Node.js vs Browser context).
// We check instanceof second because typescript believes that anything that comes
// through here must be an instance of Error, so e is 'never' after that check.
if ((e.message && e.stack) || e instanceof Error) {
return pick(
e,
'name',
'message',
'code',
'number',
'identifier',
'retryAfter',
'data',
'reason'
) as Required<Error>;
}
return e;
});
message.set({
errors: errors.concat(message.get('errors') || []),
});
if (!skipSave) {
await window.MessageCache.saveMessage(message);
}
}
export function isReplayableError(e: Error): boolean {
return (
e.name === 'MessageError' ||
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SendMessageChallengeError' ||
e.name === 'OutgoingIdentityKeyError'
);
}
/**
* Change any Pending send state to Failed. Note that this will not mark successful
* sends failed.
*/
export function markFailed(
message: MessageModel,
editMessageTimestamp?: number
): void {
const now = Date.now();
const targetTimestamp = editMessageTimestamp || message.get('timestamp');
const sendStateByConversationId = getPropForTimestamp({
log,
message: message.attributes,
prop: 'sendStateByConversationId',
targetTimestamp,
});
const newSendStateByConversationId = mapValues(
sendStateByConversationId || {},
sendState =>
sendStateReducer(sendState, {
type: SendActionType.Failed,
updatedAt: now,
})
);
const updates = getChangesForPropAtTimestamp({
log,
message: message.attributes,
prop: 'sendStateByConversationId',
targetTimestamp,
value: newSendStateByConversationId,
});
if (updates) {
message.set(updates);
}
notifyStorySendFailed(message);
}
export function notifyStorySendFailed(message: MessageModel): void {
if (!isStory(message.attributes)) {
return;
}
const { conversationId, id, timestamp } = message.attributes;
const conversation = window.ConversationController.get(conversationId);
notificationService.add({
conversationId,
storyId: id,
messageId: id,
senderTitle: conversation?.getTitle() ?? window.i18n('icu:Stories__mine'),
message: hasSuccessfulDelivery(message.attributes)
? window.i18n('icu:Stories__failed-send--partial')
: window.i18n('icu:Stories__failed-send--full'),
isExpiringMessage: false,
sentAt: timestamp,
type: NotificationType.Message,
});
}
function hasSuccessfulDelivery(message: MessageAttributesType): boolean {
const { sendStateByConversationId } = message;
const ourConversationId =
window.ConversationController.getOurConversationIdOrThrow();
return someRecipientSendStatus(
sendStateByConversationId ?? {},
ourConversationId,
isSent
);
}

View file

@ -26,9 +26,9 @@ import { getLocalAttachmentUrl } from '../util/getLocalAttachmentUrl';
type GenericEmbeddedContactType<AvatarType> = { type GenericEmbeddedContactType<AvatarType> = {
name?: Name; name?: Name;
number?: Array<Phone>; number?: ReadonlyArray<Phone>;
email?: Array<Email>; email?: ReadonlyArray<Email>;
address?: Array<PostalAddress>; address?: ReadonlyArray<PostalAddress>;
avatar?: AvatarType; avatar?: AvatarType;
organization?: string; organization?: string;

View file

@ -1,52 +0,0 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { MessageModel } from '../models/messages';
import { getEnvironment, Environment } from '../environment';
export function getMessageModelLogger(model: MessageModel): MessageModel {
const { id } = model;
if (getEnvironment() !== Environment.Development) {
return model;
}
const proxyHandler: ProxyHandler<MessageModel> = {
get(_: MessageModel, property: keyof MessageModel) {
// Allowed set of attributes & methods
if (property === 'attributes') {
return model.attributes;
}
if (property === 'id') {
return id;
}
if (property === 'get') {
return model.get.bind(model);
}
if (property === 'set') {
return model.set.bind(model);
}
if (property === 'registerLocations') {
return model.registerLocations;
}
// Disallowed set of methods & attributes
if (typeof model[property] === 'function') {
return model[property].bind(model);
}
if (typeof model[property] !== 'undefined') {
return model[property];
}
return undefined;
},
};
return new Proxy(model, proxyHandler);
}

View file

@ -2,10 +2,17 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { MessageModel } from '../models/messages'; import type { MessageModel } from '../models/messages';
import type { MessageAttributesType } from '../model-types';
import type { AttachmentType } from '../types/Attachment';
import * as log from '../logging/log'; import * as log from '../logging/log';
import * as MIME from '../types/MIME';
import { DataWriter } from '../sql/Client'; import { DataWriter } from '../sql/Client';
import { isMoreRecentThan } from './timestamp'; import { isMoreRecentThan } from './timestamp';
import { isNotNil } from './isNotNil'; import { isNotNil } from './isNotNil';
import { queueAttachmentDownloadsForMessage } from './queueAttachmentDownloads';
import { postSaveUpdates } from './cleanup';
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000; const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
const MAX_ATTACHMENT_MSGS_TO_DOWNLOAD = 250; const MAX_ATTACHMENT_MSGS_TO_DOWNLOAD = 250;
@ -66,10 +73,7 @@ export async function flushAttachmentDownloadQueue(): Promise<void> {
let numMessagesQueued = 0; let numMessagesQueued = 0;
await Promise.all( await Promise.all(
messageIdsToDownload.map(async messageId => { messageIdsToDownload.map(async messageId => {
const message = window.MessageCache.__DEPRECATED$getById( const message = window.MessageCache.getById(messageId);
messageId,
'flushAttachmentDownloadQueue'
);
if (!message) { if (!message) {
log.warn( log.warn(
'attachmentDownloadQueue: message not found in messageCache, maybe it was deleted?' 'attachmentDownloadQueue: message not found in messageCache, maybe it was deleted?'
@ -79,14 +83,14 @@ export async function flushAttachmentDownloadQueue(): Promise<void> {
if ( if (
isMoreRecentThan( isMoreRecentThan(
message.getReceivedAt(), message.get('received_at_ms') || message.get('received_at'),
MAX_ATTACHMENT_DOWNLOAD_AGE MAX_ATTACHMENT_DOWNLOAD_AGE
) || ) ||
// Stickers and long text attachments has to be downloaded for UI // Stickers and long text attachments has to be downloaded for UI
// to display the message properly. // to display the message properly.
message.hasRequiredAttachmentDownloads() hasRequiredAttachmentDownloads(message.attributes)
) { ) {
const shouldSave = await message.queueAttachmentDownloads(); const shouldSave = await queueAttachmentDownloadsForMessage(message);
if (shouldSave) { if (shouldSave) {
messageIdsToSave.push(messageId); messageIdsToSave.push(messageId);
} }
@ -101,13 +105,35 @@ export async function flushAttachmentDownloadQueue(): Promise<void> {
); );
const messagesToSave = messageIdsToSave const messagesToSave = messageIdsToSave
.map(messageId => window.MessageCache.accessAttributes(messageId)) .map(messageId => window.MessageCache.getById(messageId)?.attributes)
.filter(isNotNil); .filter(isNotNil);
await DataWriter.saveMessages(messagesToSave, { await DataWriter.saveMessages(messagesToSave, {
ourAci: window.storage.user.getCheckedAci(), ourAci: window.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
attachmentDownloadQueue = undefined; attachmentDownloadQueue = undefined;
onQueueEmpty(); onQueueEmpty();
} }
function hasRequiredAttachmentDownloads(
message: MessageAttributesType
): boolean {
const attachments: ReadonlyArray<AttachmentType> = message.attachments || [];
const hasLongMessageAttachments = attachments.some(attachment => {
return MIME.isLongMessage(attachment.contentType);
});
if (hasLongMessageAttachments) {
return true;
}
const { sticker } = message;
if (sticker) {
return !sticker.data || !sticker.data.path;
}
return false;
}

View file

@ -46,10 +46,7 @@ import { incrementMessageCounter } from './incrementMessageCounter';
import { ReadStatus } from '../messages/MessageReadStatus'; import { ReadStatus } from '../messages/MessageReadStatus';
import { SeenStatus, maxSeenStatus } from '../MessageSeenStatus'; import { SeenStatus, maxSeenStatus } from '../MessageSeenStatus';
import { canConversationBeUnarchived } from './canConversationBeUnarchived'; import { canConversationBeUnarchived } from './canConversationBeUnarchived';
import type { import type { ConversationAttributesType } from '../model-types';
ConversationAttributesType,
MessageAttributesType,
} from '../model-types';
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue'; import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
import MessageSender from '../textsecure/SendMessage'; import MessageSender from '../textsecure/SendMessage';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
@ -71,6 +68,8 @@ import { storageServiceUploadJob } from '../services/storage';
import { CallLinkFinalizeDeleteManager } from '../jobs/CallLinkFinalizeDeleteManager'; import { CallLinkFinalizeDeleteManager } from '../jobs/CallLinkFinalizeDeleteManager';
import { parsePartial, parseStrict } from './schemas'; import { parsePartial, parseStrict } from './schemas';
import { calling } from '../services/calling'; import { calling } from '../services/calling';
import { cleanupMessages } from './cleanup';
import { MessageModel } from '../models/messages';
// utils // utils
// ----- // -----
@ -1192,7 +1191,7 @@ async function saveCallHistory({
if (prevMessage != null) { if (prevMessage != null) {
await DataWriter.removeMessage(prevMessage.id, { await DataWriter.removeMessage(prevMessage.id, {
fromSync: true, fromSync: true,
singleProtoJobQueue, cleanupMessages,
}); });
} }
return callHistory; return callHistory;
@ -1222,7 +1221,7 @@ async function saveCallHistory({
const { id: newId } = generateMessageId(counter); const { id: newId } = generateMessageId(counter);
const message: MessageAttributesType = { const message = new MessageModel({
id: prevMessage?.id ?? newId, id: prevMessage?.id ?? newId,
conversationId: conversation.id, conversationId: conversation.id,
type: 'call-history', type: 'call-history',
@ -1234,20 +1233,15 @@ async function saveCallHistory({
readStatus: ReadStatus.Read, readStatus: ReadStatus.Read,
seenStatus, seenStatus,
callId: callHistory.callId, callId: callHistory.callId,
}; });
message.id = await DataWriter.saveMessage(message, { const id = await window.MessageCache.saveMessage(message, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
// We don't want to force save if we're updating an existing message
forceSave: prevMessage == null, forceSave: prevMessage == null,
}); });
message.set({ id });
log.info('saveCallHistory: Saved call history message:', message.id); log.info('saveCallHistory: Saved call history message:', message.id);
window.MessageCache.__DEPRECATED$register( const model = window.MessageCache.register(message);
message.id,
message,
'callDisposition'
);
if (prevMessage == null) { if (prevMessage == null) {
if (callHistory.direction === CallDirection.Outgoing) { if (callHistory.direction === CallDirection.Outgoing) {
@ -1255,7 +1249,7 @@ async function saveCallHistory({
} else { } else {
conversation.incrementMessageCount(); conversation.incrementMessageCount();
} }
conversation.trigger('newmessage', message); drop(conversation.onNewMessage(model));
} }
await conversation.updateLastMessage().catch(error => { await conversation.updateLastMessage().catch(error => {
@ -1356,11 +1350,10 @@ export async function updateCallHistoryFromLocalEvent(
export function updateDeletedMessages(messageIds: ReadonlyArray<string>): void { export function updateDeletedMessages(messageIds: ReadonlyArray<string>): void {
messageIds.forEach(messageId => { messageIds.forEach(messageId => {
const message = window.MessageCache.__DEPRECATED$getById( const message = window.MessageCache.getById(messageId);
messageId, const conversation = window.ConversationController.get(
'updateDeletedMessages' message?.get('conversationId')
); );
const conversation = message?.getConversation();
if (message == null || conversation == null) { if (message == null || conversation == null) {
return; return;
} }
@ -1369,7 +1362,7 @@ export function updateDeletedMessages(messageIds: ReadonlyArray<string>): void {
message.get('conversationId') message.get('conversationId')
); );
conversation.debouncedUpdateLastMessage(); conversation.debouncedUpdateLastMessage();
window.MessageCache.__DEPRECATED$unregister(messageId); window.MessageCache.unregister(messageId);
}); });
} }

View file

@ -5,11 +5,15 @@ import PQueue from 'p-queue';
import { batch } from 'react-redux'; import { batch } from 'react-redux';
import type { MessageAttributesType } from '../model-types.d'; import type { MessageAttributesType } from '../model-types.d';
import { DataReader } from '../sql/Client'; import { MessageModel } from '../models/messages';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
import { DataReader, DataWriter } from '../sql/Client';
import { deletePackReference } from '../types/Stickers'; import { deletePackReference } from '../types/Stickers';
import { isStory } from '../messages/helpers'; import { isStory } from '../messages/helpers';
import { isDirectConversation } from './whatTypeOfConversation'; import { isDirectConversation } from './whatTypeOfConversation';
import * as log from '../logging/log';
import { getCallHistorySelector } from '../state/selectors/callHistory'; import { getCallHistorySelector } from '../state/selectors/callHistory';
import { import {
DirectCallStatus, DirectCallStatus,
@ -17,20 +21,71 @@ import {
AdhocCallStatus, AdhocCallStatus,
} from '../types/CallDisposition'; } from '../types/CallDisposition';
import { getMessageIdForLogging } from './idForLogging'; import { getMessageIdForLogging } from './idForLogging';
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue'; import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
import { MINUTE } from './durations'; import { MINUTE } from './durations';
import { drop } from './drop'; import { drop } from './drop';
import { hydrateStoryContext } from './hydrateStoryContext';
import { update as updateExpiringMessagesService } from '../services/expiringMessagesDeletion';
import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService';
export async function postSaveUpdates(): Promise<void> {
await updateExpiringMessagesService();
await tapToViewMessagesDeletionService.update();
}
export async function eraseMessageContents(
message: MessageModel,
additionalProperties = {},
shouldPersist = true
): Promise<void> {
log.info(
`Erasing data for message ${getMessageIdForLogging(message.attributes)}`
);
// Note: There are cases where we want to re-erase a given message. For example, when
// a viewed (or outgoing) View-Once message is deleted for everyone.
try {
await deleteMessageData(message.attributes);
} catch (error) {
log.error(
`Error erasing data for message ${getMessageIdForLogging(message.attributes)}:`,
Errors.toLogFormat(error)
);
}
message.set({
attachments: [],
body: '',
bodyRanges: undefined,
contact: [],
editHistory: undefined,
isErased: true,
preview: [],
quote: undefined,
sticker: undefined,
...additionalProperties,
});
window.ConversationController.get(
message.attributes.conversationId
)?.debouncedUpdateLastMessage();
if (shouldPersist) {
await DataWriter.saveMessage(message.attributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
});
}
await DataWriter.deleteSentProtoByMessageId(message.id);
}
export async function cleanupMessages( export async function cleanupMessages(
messages: ReadonlyArray<MessageAttributesType>, messages: ReadonlyArray<MessageAttributesType>,
{ {
fromSync, fromSync,
markCallHistoryDeleted,
singleProtoJobQueue,
}: { }: {
fromSync?: boolean; fromSync?: boolean;
markCallHistoryDeleted: (callId: string) => Promise<void>;
singleProtoJobQueue: SingleProtoJobQueue;
} }
): Promise<void> { ): Promise<void> {
// First, handle any calls that need to be deleted // First, handle any calls that need to be deleted
@ -40,8 +95,6 @@ export async function cleanupMessages(
messages.map((message: MessageAttributesType) => async () => { messages.map((message: MessageAttributesType) => async () => {
await maybeDeleteCall(message, { await maybeDeleteCall(message, {
fromSync, fromSync,
markCallHistoryDeleted,
singleProtoJobQueue,
}); });
}) })
) )
@ -76,7 +129,7 @@ export function cleanupMessageFromMemory(message: MessageAttributesType): void {
const parentConversation = window.ConversationController.get(conversationId); const parentConversation = window.ConversationController.get(conversationId);
parentConversation?.debouncedUpdateLastMessage(); parentConversation?.debouncedUpdateLastMessage();
window.MessageCache.__DEPRECATED$unregister(id); window.MessageCache.unregister(id);
} }
async function cleanupStoryReplies( async function cleanupStoryReplies(
@ -120,24 +173,18 @@ async function cleanupStoryReplies(
// Cleanup all group replies // Cleanup all group replies
await Promise.all( await Promise.all(
replies.map(reply => { replies.map(reply => {
const replyMessageModel = window.MessageCache.__DEPRECATED$register( const replyMessageModel = window.MessageCache.register(
reply.id, new MessageModel(reply)
reply,
'cleanupStoryReplies/group'
); );
return replyMessageModel.eraseContents(); return eraseMessageContents(replyMessageModel);
}) })
); );
} else { } else {
// Refresh the storyReplyContext data for 1:1 conversations // Refresh the storyReplyContext data for 1:1 conversations
await Promise.all( await Promise.all(
replies.map(async reply => { replies.map(async reply => {
const model = window.MessageCache.__DEPRECATED$register( const model = window.MessageCache.register(new MessageModel(reply));
reply.id, await hydrateStoryContext(model.id, story, {
reply,
'cleanupStoryReplies/1:1'
);
await model.hydrateStoryContext(story, {
shouldSave: true, shouldSave: true,
isStoryErased: true, isStoryErased: true,
}); });
@ -175,12 +222,8 @@ export async function maybeDeleteCall(
message: MessageAttributesType, message: MessageAttributesType,
{ {
fromSync, fromSync,
markCallHistoryDeleted,
singleProtoJobQueue,
}: { }: {
fromSync?: boolean; fromSync?: boolean;
markCallHistoryDeleted: (callId: string) => Promise<void>;
singleProtoJobQueue: SingleProtoJobQueue;
} }
): Promise<void> { ): Promise<void> {
const { callId } = message; const { callId } = message;
@ -214,6 +257,6 @@ export async function maybeDeleteCall(
window.textsecure.MessageSender.getDeleteCallEvent(callHistory) window.textsecure.MessageSender.getDeleteCallEvent(callHistory)
); );
} }
await markCallHistoryDeleted(callId); await DataWriter.markCallHistoryDeleted(callId);
window.reduxActions.callHistory.removeCallHistory(callId); window.reduxActions.callHistory.removeCallHistory(callId);
} }

View file

@ -8,6 +8,9 @@ import { isMe } from './whatTypeOfConversation';
import { getAuthorId } from '../messages/helpers'; import { getAuthorId } from '../messages/helpers';
import { isStory } from '../state/selectors/message'; import { isStory } from '../state/selectors/message';
import { isTooOldToModifyMessage } from './isTooOldToModifyMessage'; import { isTooOldToModifyMessage } from './isTooOldToModifyMessage';
import { drop } from './drop';
import { eraseMessageContents } from './cleanup';
import { notificationService } from '../services/notifications';
export async function deleteForEveryone( export async function deleteForEveryone(
message: MessageModel, message: MessageModel,
@ -18,7 +21,9 @@ export async function deleteForEveryone(
shouldPersist = true shouldPersist = true
): Promise<void> { ): Promise<void> {
if (isDeletionByMe(message, doe)) { if (isDeletionByMe(message, doe)) {
const conversation = message.getConversation(); const conversation = window.ConversationController.get(
message.get('conversationId')
);
// Our 1:1 stories are deleted through ts/util/onStoryRecipientUpdate.ts // Our 1:1 stories are deleted through ts/util/onStoryRecipientUpdate.ts
if ( if (
@ -29,7 +34,7 @@ export async function deleteForEveryone(
return; return;
} }
await message.handleDeleteForEveryone(doe, shouldPersist); await handleDeleteForEveryone(message, doe, shouldPersist);
return; return;
} }
@ -44,7 +49,7 @@ export async function deleteForEveryone(
return; return;
} }
await message.handleDeleteForEveryone(doe, shouldPersist); await handleDeleteForEveryone(message, doe, shouldPersist);
} }
function isDeletionByMe( function isDeletionByMe(
@ -58,3 +63,49 @@ function isDeletionByMe(
doe.fromId === ourConversationId doe.fromId === ourConversationId
); );
} }
export async function handleDeleteForEveryone(
message: MessageModel,
del: Pick<
DeleteAttributesType,
'fromId' | 'targetSentTimestamp' | 'serverTimestamp'
>,
shouldPersist = true
): Promise<void> {
if (message.deletingForEveryone || message.get('deletedForEveryone')) {
return;
}
log.info('Handling DOE.', {
messageId: message.id,
fromId: del.fromId,
targetSentTimestamp: del.targetSentTimestamp,
messageServerTimestamp: message.get('serverTimestamp'),
deleteServerTimestamp: del.serverTimestamp,
});
try {
// eslint-disable-next-line no-param-reassign
message.deletingForEveryone = true;
// Remove any notifications for this message
notificationService.removeBy({ messageId: message.get('id') });
// Erase the contents of this message
await eraseMessageContents(
message,
{ deletedForEveryone: true, reactions: [] },
shouldPersist
);
// Update the conversation's last message in case this was the last message
drop(
window.ConversationController.get(
message.attributes.conversationId
)?.updateLastMessage()
);
} finally {
// eslint-disable-next-line no-param-reassign
message.deletingForEveryone = undefined;
}
}

View file

@ -14,7 +14,6 @@ import { missingCaseError } from './missingCaseError';
import { getMessageSentTimestampSet } from './getMessageSentTimestampSet'; import { getMessageSentTimestampSet } from './getMessageSentTimestampSet';
import { getAuthor } from '../messages/helpers'; import { getAuthor } from '../messages/helpers';
import { isPniString } from '../types/ServiceId'; import { isPniString } from '../types/ServiceId';
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
import { DataReader, DataWriter, deleteAndCleanup } from '../sql/Client'; import { DataReader, DataWriter, deleteAndCleanup } from '../sql/Client';
import { deleteData } from '../types/Attachment'; import { deleteData } from '../types/Attachment';
@ -29,7 +28,8 @@ import type {
} from '../textsecure/messageReceiverEvents'; } from '../textsecure/messageReceiverEvents';
import type { AciString, PniString } from '../types/ServiceId'; import type { AciString, PniString } from '../types/ServiceId';
import type { AttachmentType } from '../types/Attachment'; import type { AttachmentType } from '../types/Attachment';
import type { MessageModel } from '../models/messages'; import { MessageModel } from '../models/messages';
import { cleanupMessages, postSaveUpdates } from './cleanup';
const { getMessagesBySentAt, getMostRecentAddressableMessages } = DataReader; const { getMessagesBySentAt, getMostRecentAddressableMessages } = DataReader;
@ -98,8 +98,8 @@ export async function deleteMessage(
return false; return false;
} }
const message = window.MessageCache.toMessageAttributes(found); const message = window.MessageCache.register(new MessageModel(found));
await applyDeleteMessage(message, logId); await applyDeleteMessage(message.attributes, logId);
return true; return true;
} }
@ -109,7 +109,7 @@ export async function applyDeleteMessage(
): Promise<void> { ): Promise<void> {
await deleteAndCleanup([message], logId, { await deleteAndCleanup([message], logId, {
fromSync: true, fromSync: true,
singleProtoJobQueue, cleanupMessages,
}); });
} }
@ -141,11 +141,7 @@ export async function deleteAttachmentFromMessage(
return false; return false;
} }
const message = window.MessageCache.__DEPRECATED$register( const message = window.MessageCache.register(new MessageModel(found));
found.id,
found,
'ReadSyncs.onSync'
);
return applyDeleteAttachmentFromMessage(message, deleteAttachmentData, { return applyDeleteAttachmentFromMessage(message, deleteAttachmentData, {
deleteOnDisk, deleteOnDisk,
@ -209,7 +205,7 @@ export async function applyDeleteAttachmentFromMessage(
attachments: attachments?.filter(item => item !== attachment), attachments: attachments?.filter(item => item !== attachment),
}); });
if (shouldSave) { if (shouldSave) {
await saveMessage(message.attributes, { ourAci }); await saveMessage(message.attributes, { ourAci, postSaveUpdates });
} }
await deleteData({ deleteOnDisk, deleteDownloadOnDisk })(attachment); await deleteData({ deleteOnDisk, deleteDownloadOnDisk })(attachment);
@ -291,10 +287,10 @@ export async function deleteConversation(
const { received_at: receivedAt } = newestMessage; const { received_at: receivedAt } = newestMessage;
await removeMessagesInConversation(conversation.id, { await removeMessagesInConversation(conversation.id, {
cleanupMessages,
fromSync: true, fromSync: true,
receivedAt,
logId: `${logId}(receivedAt=${receivedAt})`, logId: `${logId}(receivedAt=${receivedAt})`,
singleProtoJobQueue, receivedAt,
}); });
} }
@ -315,10 +311,10 @@ export async function deleteConversation(
const { received_at: receivedAt } = newestNondisappearingMessage; const { received_at: receivedAt } = newestNondisappearingMessage;
await removeMessagesInConversation(conversation.id, { await removeMessagesInConversation(conversation.id, {
cleanupMessages,
fromSync: true, fromSync: true,
receivedAt,
logId: `${logId}(receivedAt=${receivedAt})`, logId: `${logId}(receivedAt=${receivedAt})`,
singleProtoJobQueue, receivedAt,
}); });
} }
} }

View file

@ -3,16 +3,13 @@
import { DAY } from './durations'; import { DAY } from './durations';
import { sendDeleteForEveryoneMessage } from './sendDeleteForEveryoneMessage'; import { sendDeleteForEveryoneMessage } from './sendDeleteForEveryoneMessage';
import { __DEPRECATED$getMessageById } from '../messages/getMessageById'; import { getMessageById } from '../messages/getMessageById';
import * as log from '../logging/log'; import * as log from '../logging/log';
export async function deleteGroupStoryReplyForEveryone( export async function deleteGroupStoryReplyForEveryone(
replyMessageId: string replyMessageId: string
): Promise<void> { ): Promise<void> {
const messageModel = await __DEPRECATED$getMessageById( const messageModel = await getMessageById(replyMessageId);
replyMessageId,
'deleteGroupStoryReplyForEveryone'
);
if (!messageModel) { if (!messageModel) {
log.warn( log.warn(
@ -23,7 +20,9 @@ export async function deleteGroupStoryReplyForEveryone(
const timestamp = messageModel.get('timestamp'); const timestamp = messageModel.get('timestamp');
const group = messageModel.getConversation(); const group = window.ConversationController.get(
messageModel.get('conversationId')
);
if (!group) { if (!group) {
log.warn( log.warn(

View file

@ -20,10 +20,11 @@ import {
import { onStoryRecipientUpdate } from './onStoryRecipientUpdate'; import { onStoryRecipientUpdate } from './onStoryRecipientUpdate';
import { sendDeleteForEveryoneMessage } from './sendDeleteForEveryoneMessage'; import { sendDeleteForEveryoneMessage } from './sendDeleteForEveryoneMessage';
import { isGroupV2 } from './whatTypeOfConversation'; import { isGroupV2 } from './whatTypeOfConversation';
import { __DEPRECATED$getMessageById } from '../messages/getMessageById'; import { getMessageById } from '../messages/getMessageById';
import { strictAssert } from './assert'; import { strictAssert } from './assert';
import { repeat, zipObject } from './iterables'; import { repeat, zipObject } from './iterables';
import { isOlderThan } from './timestamp'; import { isOlderThan } from './timestamp';
import { postSaveUpdates } from './cleanup';
export async function deleteStoryForEveryone( export async function deleteStoryForEveryone(
stories: ReadonlyArray<StoryDataType>, stories: ReadonlyArray<StoryDataType>,
@ -47,10 +48,7 @@ export async function deleteStoryForEveryone(
} }
const logId = `deleteStoryForEveryone(${story.messageId})`; const logId = `deleteStoryForEveryone(${story.messageId})`;
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(story.messageId);
story.messageId,
'deleteStoryForEveryone'
);
if (!message) { if (!message) {
throw new Error('Story not found'); throw new Error('Story not found');
} }
@ -197,6 +195,7 @@ export async function deleteStoryForEveryone(
await DataWriter.saveMessage(message.attributes, { await DataWriter.saveMessage(message.attributes, {
jobToInsert, jobToInsert,
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
}); });
} catch (error) { } catch (error) {

View file

@ -0,0 +1,94 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { getMessageById } from '../messages/getMessageById';
import type { MessageModel } from '../models/messages';
import { hydrateStoryContext } from './hydrateStoryContext';
import { getMessageIdForLogging } from './idForLogging';
import * as log from '../logging/log';
import {
isQuoteAMatch,
shouldTryToCopyFromQuotedMessage,
} from '../messages/helpers';
import { copyQuoteContentFromOriginal } from '../messages/copyQuote';
import { queueUpdateMessage } from './messageBatcher';
export async function doubleCheckMissingQuoteReference(
message: MessageModel
): Promise<void> {
const logId = getMessageIdForLogging(message.attributes);
const storyId = message.get('storyId');
if (storyId) {
log.warn(
`doubleCheckMissingQuoteReference/${logId}: missing story reference`
);
const storyMessage = await getMessageById(storyId);
if (!storyMessage) {
return;
}
if (message.get('storyReplyContext')) {
message.set({ storyReplyContext: undefined });
}
await hydrateStoryContext(message.id, storyMessage.attributes, {
shouldSave: true,
});
return;
}
const quote = message.get('quote');
if (!quote) {
log.warn(`doubleCheckMissingQuoteReference/${logId}: Missing quote!`);
return;
}
const { authorAci, author, id: sentAt, referencedMessageNotFound } = quote;
const contact = window.ConversationController.get(authorAci || author);
// Is the quote really without a reference? Check with our in memory store
// first to make sure it's not there.
if (
contact &&
shouldTryToCopyFromQuotedMessage({
referencedMessageNotFound,
quoteAttachment: quote.attachments.at(0),
})
) {
const matchingMessage = await window.MessageCache.findBySentAt(
Number(sentAt),
model =>
isQuoteAMatch(model.attributes, message.get('conversationId'), quote)
);
if (!matchingMessage) {
log.info(
`doubleCheckMissingQuoteReference/${logId}: No match for ${sentAt}.`
);
return;
}
message.set({
quote: {
...quote,
referencedMessageNotFound: false,
},
});
log.info(
`doubleCheckMissingQuoteReference/${logId}: Found match for ${sentAt}, updating.`
);
await copyQuoteContentFromOriginal(matchingMessage, quote);
message.set({
quote: {
...quote,
referencedMessageNotFound: false,
},
});
queueUpdateMessage(message.attributes);
}
}

View file

@ -4,8 +4,7 @@
import { v4 as generateUuid } from 'uuid'; import { v4 as generateUuid } from 'uuid';
import type { AttachmentType } from '../types/Attachment'; import type { AttachmentType } from '../types/Attachment';
import type { MessageAttributesType } from '../model-types.d'; import { MessageModel } from '../models/messages';
import type { MessageModel } from '../models/messages';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { IMAGE_JPEG } from '../types/MIME'; import { IMAGE_JPEG } from '../types/MIME';
import { ReadStatus } from '../messages/MessageReadStatus'; import { ReadStatus } from '../messages/MessageReadStatus';
@ -84,7 +83,7 @@ export async function downloadOnboardingStory(): Promise<void> {
(attachment, index) => { (attachment, index) => {
const timestamp = Date.now() + index; const timestamp = Date.now() + index;
const partialMessage: MessageAttributesType = { const message = new MessageModel({
attachments: [attachment], attachments: [attachment],
canReplyToStory: false, canReplyToStory: false,
conversationId: signalConversation.id, conversationId: signalConversation.id,
@ -99,12 +98,8 @@ export async function downloadOnboardingStory(): Promise<void> {
sourceServiceId: signalConversation.getServiceId(), sourceServiceId: signalConversation.getServiceId(),
timestamp, timestamp,
type: 'story', type: 'story',
}; });
return window.MessageCache.__DEPRECATED$register( return window.MessageCache.register(message);
partialMessage.id,
partialMessage,
'downloadOnboardingStory'
);
} }
); );
@ -112,11 +107,6 @@ export async function downloadOnboardingStory(): Promise<void> {
storyMessages.map(message => saveNewMessageBatcher.add(message.attributes)) storyMessages.map(message => saveNewMessageBatcher.add(message.attributes))
); );
// Sync to redux
storyMessages.forEach(message => {
message.trigger('change');
});
await window.storage.put( await window.storage.put(
'existingOnboardingStoryMessageIds', 'existingOnboardingStoryMessageIds',
storyMessages.map(message => message.id) storyMessages.map(message => message.id)

View file

@ -5,7 +5,8 @@ import * as log from '../logging/log';
import { DataWriter } from '../sql/Client'; import { DataWriter } from '../sql/Client';
import { calculateExpirationTimestamp } from './expirationTimer'; import { calculateExpirationTimestamp } from './expirationTimer';
import { DAY } from './durations'; import { DAY } from './durations';
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue'; import { cleanupMessages } from './cleanup';
import { getMessageById } from '../messages/getMessageById';
export async function findAndDeleteOnboardingStoryIfExists(): Promise<void> { export async function findAndDeleteOnboardingStoryIfExists(): Promise<void> {
const existingOnboardingStoryMessageIds = window.storage.get( const existingOnboardingStoryMessageIds = window.storage.get(
@ -19,12 +20,14 @@ export async function findAndDeleteOnboardingStoryIfExists(): Promise<void> {
const hasExpired = await (async () => { const hasExpired = await (async () => {
const [storyId] = existingOnboardingStoryMessageIds; const [storyId] = existingOnboardingStoryMessageIds;
try { try {
const messageAttributes = await window.MessageCache.resolveAttributes( const message = await getMessageById(storyId);
'findAndDeleteOnboardingStoryIfExists', if (!message) {
storyId throw new Error(
); `findAndDeleteOnboardingStoryIfExists: Failed to find message ${storyId}`
);
}
const expires = calculateExpirationTimestamp(messageAttributes) ?? 0; const expires = calculateExpirationTimestamp(message.attributes) ?? 0;
const now = Date.now(); const now = Date.now();
const isExpired = expires < now; const isExpired = expires < now;
@ -46,7 +49,7 @@ export async function findAndDeleteOnboardingStoryIfExists(): Promise<void> {
log.info('findAndDeleteOnboardingStoryIfExists: removing onboarding stories'); log.info('findAndDeleteOnboardingStoryIfExists: removing onboarding stories');
await DataWriter.removeMessages(existingOnboardingStoryMessageIds, { await DataWriter.removeMessages(existingOnboardingStoryMessageIds, {
singleProtoJobQueue, cleanupMessages,
}); });
await window.storage.put('existingOnboardingStoryMessageIds', undefined); await window.storage.put('existingOnboardingStoryMessageIds', undefined);

View file

@ -25,6 +25,7 @@ import { isTooOldToModifyMessage } from './isTooOldToModifyMessage';
import { queueAttachmentDownloads } from './queueAttachmentDownloads'; import { queueAttachmentDownloads } from './queueAttachmentDownloads';
import { modifyTargetMessage } from './modifyTargetMessage'; import { modifyTargetMessage } from './modifyTargetMessage';
import { isMessageNoteToSelf } from './isMessageNoteToSelf'; import { isMessageNoteToSelf } from './isMessageNoteToSelf';
import { MessageModel } from '../models/messages';
const RECURSION_LIMIT = 15; const RECURSION_LIMIT = 15;
@ -103,15 +104,13 @@ export async function handleEditMessage(
return; return;
} }
const mainMessageModel = window.MessageCache.__DEPRECATED$register( const mainMessageModel = window.MessageCache.register(
mainMessage.id, new MessageModel(mainMessage)
mainMessage,
'handleEditMessage'
); );
// Pull out the edit history from the main message. If this is the first edit // Pull out the edit history from the main message. If this is the first edit
// then the original message becomes the first item in the edit history. // then the original message becomes the first item in the edit history.
let editHistory: Array<EditHistoryType> = mainMessage.editHistory || [ let editHistory: ReadonlyArray<EditHistoryType> = mainMessage.editHistory || [
{ {
attachments: mainMessage.attachments, attachments: mainMessage.attachments,
body: mainMessage.body, body: mainMessage.body,
@ -215,8 +214,10 @@ export async function handleEditMessage(
const { quote: upgradedQuote } = upgradedEditedMessageData; const { quote: upgradedQuote } = upgradedEditedMessageData;
let nextEditedMessageQuote: QuotedMessageType | undefined; let nextEditedMessageQuote: QuotedMessageType | undefined;
if (!upgradedQuote) { if (!upgradedQuote) {
// Quote dropped if (mainMessage.quote) {
log.info(`${idLog}: dropping quote`); // Quote dropped
log.info(`${idLog}: dropping quote`);
}
} else if (!upgradedQuote.id || upgradedQuote.id === mainMessage.quote?.id) { } else if (!upgradedQuote.id || upgradedQuote.id === mainMessage.quote?.id) {
// Quote preserved // Quote preserved
nextEditedMessageQuote = mainMessage.quote; nextEditedMessageQuote = mainMessage.quote;
@ -370,7 +371,9 @@ export async function handleEditMessage(
conversation.clearContactTypingTimer(typingToken); conversation.clearContactTypingTimer(typingToken);
} }
const mainMessageConversation = mainMessageModel.getConversation(); const mainMessageConversation = window.ConversationController.get(
mainMessageModel.get('conversationId')
);
if (mainMessageConversation) { if (mainMessageConversation) {
drop(mainMessageConversation.updateLastMessage()); drop(mainMessageConversation.updateLastMessage());
// Apply any other operations, excluding edits that target this message // Apply any other operations, excluding edits that target this message
@ -386,7 +389,7 @@ export async function handleEditMessage(
// Apply any other pending edits that target this message // Apply any other pending edits that target this message
const edits = Edits.forMessage({ const edits = Edits.forMessage({
...mainMessage, ...mainMessageModel.attributes,
sent_at: editedMessage.timestamp, sent_at: editedMessage.timestamp,
timestamp: editedMessage.timestamp, timestamp: editedMessage.timestamp,
}); });

View file

@ -12,6 +12,10 @@ import { softAssert, strictAssert } from './assert';
import { getMessageSentTimestamp } from './getMessageSentTimestamp'; import { getMessageSentTimestamp } from './getMessageSentTimestamp';
import { isOlderThan } from './timestamp'; import { isOlderThan } from './timestamp';
import { DAY } from './durations'; import { DAY } from './durations';
import { getMessageById } from '../messages/getMessageById';
import { MessageModel } from '../models/messages';
import { DataWriter } from '../sql/Client';
import { postSaveUpdates } from './cleanup';
export async function hydrateStoryContext( export async function hydrateStoryContext(
messageId: string, messageId: string,
@ -24,23 +28,18 @@ export async function hydrateStoryContext(
isStoryErased?: boolean; isStoryErased?: boolean;
} = {} } = {}
): Promise<Partial<MessageAttributesType> | undefined> { ): Promise<Partial<MessageAttributesType> | undefined> {
let messageAttributes: MessageAttributesType; const message = await getMessageById(messageId);
try { if (!message) {
messageAttributes = await window.MessageCache.resolveAttributes( log.warn(`hydrateStoryContext: Message ${messageId} not found`);
'hydrateStoryContext',
messageId
);
} catch {
return undefined; return undefined;
} }
const { storyId } = messageAttributes; const { storyId, storyReplyContext: context } = message.attributes;
if (!storyId) { if (!storyId) {
return undefined; return undefined;
} }
const { storyReplyContext: context } = messageAttributes; const sentTimestamp = getMessageSentTimestamp(message.attributes, {
const sentTimestamp = getMessageSentTimestamp(messageAttributes, {
includeEdits: false, includeEdits: false,
log, log,
}); });
@ -55,22 +54,19 @@ export async function hydrateStoryContext(
return undefined; return undefined;
} }
let storyMessage: MessageAttributesType | undefined; let storyMessage: MessageModel | undefined;
try { try {
storyMessage = storyMessage =
storyMessageParam === undefined storyMessageParam === undefined
? await window.MessageCache.resolveAttributes( ? await getMessageById(storyId)
'hydrateStoryContext/story', : window.MessageCache.register(new MessageModel(storyMessageParam));
storyId
)
: window.MessageCache.toMessageAttributes(storyMessageParam);
} catch { } catch {
storyMessage = undefined; storyMessage = undefined;
} }
if (!storyMessage || isStoryErased) { if (!storyMessage || isStoryErased) {
const conversation = window.ConversationController.get( const conversation = window.ConversationController.get(
messageAttributes.conversationId message.attributes.conversationId
); );
softAssert( softAssert(
conversation && isDirectConversation(conversation.attributes), conversation && isDirectConversation(conversation.attributes),
@ -84,30 +80,25 @@ export async function hydrateStoryContext(
messageId: '', messageId: '',
}, },
}; };
message.set(newMessageAttributes);
if (shouldSave) { if (shouldSave) {
await window.MessageCache.setAttributes({ const ourAci = window.textsecure.storage.user.getCheckedAci();
messageId, await DataWriter.saveMessage(message.attributes, {
messageAttributes: newMessageAttributes, ourAci,
skipSaveToDatabase: false, postSaveUpdates,
});
} else {
window.MessageCache.setAttributes({
messageId,
messageAttributes: newMessageAttributes,
skipSaveToDatabase: true,
}); });
} }
return newMessageAttributes; return newMessageAttributes;
} }
const attachments = getAttachmentsForMessage({ ...storyMessage }); const attachments = getAttachmentsForMessage({ ...storyMessage.attributes });
let attachment: AttachmentType | undefined = attachments?.[0]; let attachment: AttachmentType | undefined = attachments?.[0];
if (attachment && !attachment.url && !attachment.textAttachment) { if (attachment && !attachment.url && !attachment.textAttachment) {
attachment = undefined; attachment = undefined;
} }
const { sourceServiceId: authorAci } = storyMessage; const { sourceServiceId: authorAci } = storyMessage.attributes;
strictAssert(isAciString(authorAci), 'Story message from pni'); strictAssert(isAciString(authorAci), 'Story message from pni');
const newMessageAttributes: Partial<MessageAttributesType> = { const newMessageAttributes: Partial<MessageAttributesType> = {
storyReplyContext: { storyReplyContext: {
@ -116,18 +107,14 @@ export async function hydrateStoryContext(
messageId: storyMessage.id, messageId: storyMessage.id,
}, },
}; };
message.set(newMessageAttributes);
if (shouldSave) { if (shouldSave) {
await window.MessageCache.setAttributes({ const ourAci = window.textsecure.storage.user.getCheckedAci();
messageId, await DataWriter.saveMessage(message.attributes, {
messageAttributes: newMessageAttributes, ourAci,
skipSaveToDatabase: false, postSaveUpdates,
});
} else {
window.MessageCache.setAttributes({
messageId,
messageAttributes: newMessageAttributes,
skipSaveToDatabase: true,
}); });
} }
return newMessageAttributes; return newMessageAttributes;
} }

97
ts/util/isMessageEmpty.ts Normal file
View file

@ -0,0 +1,97 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { messageHasPaymentEvent } from '../messages/helpers';
import type { MessageAttributesType } from '../model-types';
import {
hasErrors,
isCallHistory,
isChatSessionRefreshed,
isConversationMerge,
isDeliveryIssue,
isEndSession,
isExpirationTimerUpdate,
isGiftBadge,
isGroupUpdate,
isGroupV2Change,
isKeyChange,
isPhoneNumberDiscovery,
isProfileChange,
isTapToView,
isTitleTransitionNotification,
isUniversalTimerNotification,
isUnsupportedMessage,
isVerifiedChange,
} from '../state/selectors/message';
export function isMessageEmpty(attributes: MessageAttributesType): boolean {
// Core message types - we check for all four because they can each stand alone
const hasBody = Boolean(attributes.body);
const hasAttachment = (attributes.attachments || []).length > 0;
const hasEmbeddedContact = (attributes.contact || []).length > 0;
const isSticker = Boolean(attributes.sticker);
// Rendered sync messages
const isCallHistoryValue = isCallHistory(attributes);
const isChatSessionRefreshedValue = isChatSessionRefreshed(attributes);
const isDeliveryIssueValue = isDeliveryIssue(attributes);
const isGiftBadgeValue = isGiftBadge(attributes);
const isGroupUpdateValue = isGroupUpdate(attributes);
const isGroupV2ChangeValue = isGroupV2Change(attributes);
const isEndSessionValue = isEndSession(attributes);
const isExpirationTimerUpdateValue = isExpirationTimerUpdate(attributes);
const isVerifiedChangeValue = isVerifiedChange(attributes);
// Placeholder messages
const isUnsupportedMessageValue = isUnsupportedMessage(attributes);
const isTapToViewValue = isTapToView(attributes);
// Errors
const hasErrorsValue = hasErrors(attributes);
// Locally-generated notifications
const isKeyChangeValue = isKeyChange(attributes);
const isProfileChangeValue = isProfileChange(attributes);
const isUniversalTimerNotificationValue =
isUniversalTimerNotification(attributes);
const isConversationMergeValue = isConversationMerge(attributes);
const isPhoneNumberDiscoveryValue = isPhoneNumberDiscovery(attributes);
const isTitleTransitionNotificationValue =
isTitleTransitionNotification(attributes);
const isPayment = messageHasPaymentEvent(attributes);
// Note: not all of these message types go through message.handleDataMessage
const hasSomethingToDisplay =
// Core message types
hasBody ||
hasAttachment ||
hasEmbeddedContact ||
isSticker ||
isPayment ||
// Rendered sync messages
isCallHistoryValue ||
isChatSessionRefreshedValue ||
isDeliveryIssueValue ||
isGiftBadgeValue ||
isGroupUpdateValue ||
isGroupV2ChangeValue ||
isEndSessionValue ||
isExpirationTimerUpdateValue ||
isVerifiedChangeValue ||
// Placeholder messages
isUnsupportedMessageValue ||
isTapToViewValue ||
// Errors
hasErrorsValue ||
// Locally-generated notifications
isKeyChangeValue ||
isProfileChangeValue ||
isUniversalTimerNotificationValue ||
isConversationMergeValue ||
isPhoneNumberDiscoveryValue ||
isTitleTransitionNotificationValue;
return !hasSomethingToDisplay;
}

View file

@ -0,0 +1,38 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { MessageAttributesType } from '../model-types';
import * as GoogleChrome from './GoogleChrome';
export function isValidTapToView(message: MessageAttributesType): boolean {
const { body } = message;
if (body) {
return false;
}
const { attachments } = message;
if (!attachments || attachments.length !== 1) {
return false;
}
const firstAttachment = attachments[0];
if (
!GoogleChrome.isImageTypeSupported(firstAttachment.contentType) &&
!GoogleChrome.isVideoTypeSupported(firstAttachment.contentType)
) {
return false;
}
const { quote, sticker, contact, preview } = message;
if (
quote ||
sticker ||
(contact && contact.length > 0) ||
(preview && preview.length > 0)
) {
return false;
}
return true;
}

View file

@ -55,8 +55,8 @@ export async function makeQuote(
} }
export async function getQuoteAttachment( export async function getQuoteAttachment(
attachments?: Array<AttachmentType>, attachments?: ReadonlyArray<AttachmentType>,
preview?: Array<LinkPreviewType>, preview?: ReadonlyArray<LinkPreviewType>,
sticker?: StickerType sticker?: StickerType
): Promise<Array<QuotedAttachmentType>> { ): Promise<Array<QuotedAttachmentType>> {
const { loadAttachmentData } = window.Signal.Migrations; const { loadAttachmentData } = window.Signal.Migrations;

View file

@ -103,10 +103,7 @@ export async function markConversationRead(
const allReadMessagesSync = allUnreadMessages const allReadMessagesSync = allUnreadMessages
.map(messageSyncData => { .map(messageSyncData => {
const message = window.MessageCache.__DEPRECATED$getById( const message = window.MessageCache.getById(messageSyncData.id);
messageSyncData.id,
'markConversationRead'
);
// we update the in-memory MessageModel with fresh read/seen status // we update the in-memory MessageModel with fresh read/seen status
if (message) { if (message) {
message.set( message.set(

View file

@ -3,11 +3,12 @@
import * as log from '../logging/log'; import * as log from '../logging/log';
import { DataWriter } from '../sql/Client'; import { DataWriter } from '../sql/Client';
import { __DEPRECATED$getMessageById } from '../messages/getMessageById'; import { getMessageById } from '../messages/getMessageById';
import { isNotNil } from './isNotNil'; import { isNotNil } from './isNotNil';
import { DurationInSeconds } from './durations'; import { DurationInSeconds } from './durations';
import { markViewed } from '../services/MessageUpdater'; import { markViewed } from '../services/MessageUpdater';
import { storageServiceUploadJob } from '../services/storage'; import { storageServiceUploadJob } from '../services/storage';
import { postSaveUpdates } from './cleanup';
export async function markOnboardingStoryAsRead(): Promise<boolean> { export async function markOnboardingStoryAsRead(): Promise<boolean> {
const existingOnboardingStoryMessageIds = window.storage.get( const existingOnboardingStoryMessageIds = window.storage.get(
@ -20,9 +21,7 @@ export async function markOnboardingStoryAsRead(): Promise<boolean> {
} }
const messages = await Promise.all( const messages = await Promise.all(
existingOnboardingStoryMessageIds.map(id => existingOnboardingStoryMessageIds.map(id => getMessageById(id))
__DEPRECATED$getMessageById(id, 'markOnboardingStoryAsRead')
)
); );
const storyReadDate = Date.now(); const storyReadDate = Date.now();
@ -49,6 +48,7 @@ export async function markOnboardingStoryAsRead(): Promise<boolean> {
await DataWriter.saveMessages(messageAttributes, { await DataWriter.saveMessages(messageAttributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
await window.storage.put('hasViewedOnboardingStory', true); await window.storage.put('hasViewedOnboardingStory', true);

View file

@ -6,6 +6,8 @@ import { createBatcher } from './batcher';
import { createWaitBatcher } from './waitBatcher'; import { createWaitBatcher } from './waitBatcher';
import { DataWriter } from '../sql/Client'; import { DataWriter } from '../sql/Client';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { postSaveUpdates } from './cleanup';
import { MessageModel } from '../models/messages';
const updateMessageBatcher = createBatcher<ReadonlyMessageAttributesType>({ const updateMessageBatcher = createBatcher<ReadonlyMessageAttributesType>({
name: 'messageBatcher.updateMessageBatcher', name: 'messageBatcher.updateMessageBatcher',
@ -16,11 +18,12 @@ const updateMessageBatcher = createBatcher<ReadonlyMessageAttributesType>({
// Grab the latest from the cache in case they've changed // Grab the latest from the cache in case they've changed
const messagesToSave = messageAttrs.map( const messagesToSave = messageAttrs.map(
message => window.MessageCache.accessAttributes(message.id) ?? message message => window.MessageCache.getById(message.id)?.attributes ?? message
); );
await DataWriter.saveMessages(messagesToSave, { await DataWriter.saveMessages(messagesToSave, {
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
}, },
}); });
@ -35,6 +38,7 @@ export function queueUpdateMessage(
} else { } else {
void DataWriter.saveMessage(messageAttr, { void DataWriter.saveMessage(messageAttr, {
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
} }
} }
@ -55,12 +59,15 @@ export const saveNewMessageBatcher =
// Grab the latest from the cache in case they've changed // Grab the latest from the cache in case they've changed
const messagesToSave = messageAttrs.map( const messagesToSave = messageAttrs.map(
message => window.MessageCache.accessAttributes(message.id) ?? message message =>
window.MessageCache.register(new MessageModel(message))?.attributes ??
message
); );
await DataWriter.saveMessages(messagesToSave, { await DataWriter.saveMessages(messagesToSave, {
forceSave: true, forceSave: true,
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
}, },
}); });

View file

@ -35,6 +35,10 @@ import {
applyDeleteAttachmentFromMessage, applyDeleteAttachmentFromMessage,
applyDeleteMessage, applyDeleteMessage,
} from './deleteForMe'; } from './deleteForMe';
import { getMessageIdForLogging } from './idForLogging';
import { markViewOnceMessageViewed } from '../services/MessageUpdater';
import { handleReaction } from '../messageModifiers/Reactions';
import { postSaveUpdates } from './cleanup';
export enum ModifyTargetMessageResult { export enum ModifyTargetMessageResult {
Modified = 'Modified', Modified = 'Modified',
@ -52,7 +56,7 @@ export async function modifyTargetMessage(
): Promise<ModifyTargetMessageResult> { ): Promise<ModifyTargetMessageResult> {
const { isFirstRun = false, skipEdits = false } = options ?? {}; const { isFirstRun = false, skipEdits = false } = options ?? {};
const logId = `modifyTargetMessage/${message.idForLogging()}`; const logId = `modifyTargetMessage/${getMessageIdForLogging(message.attributes)}`;
const type = message.get('type'); const type = message.get('type');
let changed = false; let changed = false;
const ourAci = window.textsecure.storage.user.getCheckedAci(); const ourAci = window.textsecure.storage.user.getCheckedAci();
@ -157,7 +161,7 @@ export async function modifyTargetMessage(
); );
if (!isEqual(oldSendStateByConversationId, newSendStateByConversationId)) { if (!isEqual(oldSendStateByConversationId, newSendStateByConversationId)) {
message.set('sendStateByConversationId', newSendStateByConversationId); message.set({ sendStateByConversationId: newSendStateByConversationId });
changed = true; changed = true;
} }
} }
@ -184,10 +188,12 @@ export async function modifyTargetMessage(
const existingExpirationStartTimestamp = message.get( const existingExpirationStartTimestamp = message.get(
'expirationStartTimestamp' 'expirationStartTimestamp'
); );
message.set( message.set({
'expirationStartTimestamp', expirationStartTimestamp: Math.min(
Math.min(existingExpirationStartTimestamp ?? Date.now(), markReadAt) existingExpirationStartTimestamp ?? Date.now(),
); markReadAt
),
});
changed = true; changed = true;
} }
@ -208,8 +214,10 @@ export async function modifyTargetMessage(
}); });
changed = true; changed = true;
message.setPendingMarkRead( // eslint-disable-next-line no-param-reassign
Math.min(message.getPendingMarkRead() ?? Date.now(), markReadAt) message.pendingMarkRead = Math.min(
message.pendingMarkRead ?? Date.now(),
markReadAt
); );
} else if ( } else if (
isFirstRun && isFirstRun &&
@ -219,9 +227,10 @@ export async function modifyTargetMessage(
conversation.setArchived(false); conversation.setArchived(false);
} }
if (!isFirstRun && message.getPendingMarkRead()) { if (!isFirstRun && message.pendingMarkRead) {
const markReadAt = message.getPendingMarkRead(); const markReadAt = message.pendingMarkRead;
message.setPendingMarkRead(undefined); // eslint-disable-next-line no-param-reassign
message.pendingMarkRead = undefined;
const newestSentAt = maybeSingleReadSync?.readSync.timestamp; const newestSentAt = maybeSingleReadSync?.readSync.timestamp;
// This is primarily to allow the conversation to mark all older // This is primarily to allow the conversation to mark all older
@ -232,9 +241,9 @@ export async function modifyTargetMessage(
// message and the other ones accompanying it in the batch are fully in // message and the other ones accompanying it in the batch are fully in
// the database. // the database.
drop( drop(
message window.ConversationController.get(
.getConversation() message.get('conversationId')
?.onReadMessage(message.attributes, markReadAt, newestSentAt) )?.onReadMessage(message.attributes, markReadAt, newestSentAt)
); );
} }
@ -242,7 +251,7 @@ export async function modifyTargetMessage(
if (isTapToView(message.attributes)) { if (isTapToView(message.attributes)) {
const viewOnceOpenSync = ViewOnceOpenSyncs.forMessage(message.attributes); const viewOnceOpenSync = ViewOnceOpenSyncs.forMessage(message.attributes);
if (viewOnceOpenSync) { if (viewOnceOpenSync) {
await message.markViewOnceMessageViewed({ fromSync: true }); await markViewOnceMessageViewed(message, { fromSync: true });
changed = true; changed = true;
} }
} }
@ -262,8 +271,10 @@ export async function modifyTargetMessage(
Date.now(), Date.now(),
...viewSyncs.map(({ viewSync }) => viewSync.viewedAt) ...viewSyncs.map(({ viewSync }) => viewSync.viewedAt)
); );
message.setPendingMarkRead( // eslint-disable-next-line no-param-reassign
Math.min(message.getPendingMarkRead() ?? Date.now(), markReadAt) message.pendingMarkRead = Math.min(
message.pendingMarkRead ?? Date.now(),
markReadAt
); );
} }
@ -272,7 +283,7 @@ export async function modifyTargetMessage(
expirationStartTimestamp: message.get('timestamp'), expirationStartTimestamp: message.get('timestamp'),
expireTimer: message.get('expireTimer'), expireTimer: message.get('expireTimer'),
}); });
message.set('expirationStartTimestamp', message.get('timestamp')); message.set({ expirationStartTimestamp: message.get('timestamp') });
changed = true; changed = true;
} }
} }
@ -292,12 +303,12 @@ export async function modifyTargetMessage(
generatedMessage, generatedMessage,
'Story reactions must provide storyReactionMessage' 'Story reactions must provide storyReactionMessage'
); );
await generatedMessage.handleReaction(reaction, { await handleReaction(generatedMessage, reaction, {
storyMessage: message.attributes, storyMessage: message.attributes,
}); });
} else { } else {
changed = true; changed = true;
await message.handleReaction(reaction, { shouldPersist: false }); await handleReaction(message, reaction, { shouldPersist: false });
} }
}) })
); );
@ -317,6 +328,7 @@ export async function modifyTargetMessage(
log.info(`${logId}: Changes in second run; saving.`); log.info(`${logId}: Changes in second run; saving.`);
await DataWriter.saveMessage(message.attributes, { await DataWriter.saveMessage(message.attributes, {
ourAci, ourAci,
postSaveUpdates,
}); });
} }

View file

@ -13,6 +13,8 @@ import { isStory } from '../state/selectors/message';
import { queueUpdateMessage } from './messageBatcher'; import { queueUpdateMessage } from './messageBatcher';
import { isMe } from './whatTypeOfConversation'; import { isMe } from './whatTypeOfConversation';
import { drop } from './drop'; import { drop } from './drop';
import { handleDeleteForEveryone } from './deleteForEveryone';
import { MessageModel } from '../models/messages';
export async function onStoryRecipientUpdate( export async function onStoryRecipientUpdate(
event: StoryRecipientUpdateEvent event: StoryRecipientUpdateEvent
@ -162,11 +164,7 @@ export async function onStoryRecipientUpdate(
return true; return true;
} }
const message = window.MessageCache.__DEPRECATED$register( const message = window.MessageCache.register(new MessageModel(item));
item.id,
item,
'onStoryRecipientUpdate'
);
const sendStateConversationIds = new Set( const sendStateConversationIds = new Set(
Object.keys(nextSendStateByConversationId) Object.keys(nextSendStateByConversationId)
@ -190,7 +188,7 @@ export async function onStoryRecipientUpdate(
// sent timestamp doesn't happen (it would return all copies of the // sent timestamp doesn't happen (it would return all copies of the
// story, not just the one we want to delete). // story, not just the one we want to delete).
drop( drop(
message.handleDeleteForEveryone({ handleDeleteForEveryone(message, {
fromId: ourConversationId, fromId: ourConversationId,
serverTimestamp: Number(item.serverTimestamp), serverTimestamp: Number(item.serverTimestamp),
targetSentTimestamp: item.timestamp, targetSentTimestamp: item.timestamp,

View file

@ -33,13 +33,23 @@ import {
AttachmentDownloadUrgency, AttachmentDownloadUrgency,
} from '../jobs/AttachmentDownloadManager'; } from '../jobs/AttachmentDownloadManager';
import { AttachmentDownloadSource } from '../sql/Interface'; import { AttachmentDownloadSource } from '../sql/Interface';
import type { MessageModel } from '../models/messages';
import type { ConversationModel } from '../models/conversations';
import { isOutgoing, isStory } from '../messages/helpers';
import { shouldDownloadStory } from './shouldDownloadStory';
import { hasAttachmentDownloads } from './hasAttachmentDownloads';
import {
addToAttachmentDownloadQueue,
shouldUseAttachmentDownloadQueue,
} from './attachmentDownloadQueue';
import { queueUpdateMessage } from './messageBatcher';
export type MessageAttachmentsDownloadedType = { export type MessageAttachmentsDownloadedType = {
bodyAttachment?: AttachmentType; bodyAttachment?: AttachmentType;
attachments: Array<AttachmentType>; attachments: ReadonlyArray<AttachmentType>;
editHistory?: Array<EditHistoryType>; editHistory?: ReadonlyArray<EditHistoryType>;
preview: Array<LinkPreviewType>; preview: ReadonlyArray<LinkPreviewType>;
contact: Array<EmbeddedContactType>; contact: ReadonlyArray<EmbeddedContactType>;
quote?: QuotedMessageType; quote?: QuotedMessageType;
sticker?: StickerType; sticker?: StickerType;
}; };
@ -49,6 +59,50 @@ function getLogger(source: AttachmentDownloadSource) {
const log = verbose ? logger : { ...logger, info: () => null }; const log = verbose ? logger : { ...logger, info: () => null };
return log; return log;
} }
export async function handleAttachmentDownloadsForNewMessage(
message: MessageModel,
conversation: ConversationModel
): Promise<void> {
const idLog = `handleAttachmentDownloadsForNewMessage/${conversation.idForLogging()} ${getMessageIdForLogging(message.attributes)}`;
// Only queue attachments for downloads if this is a story (with additional logic), or
// if it's either an outgoing message or we've accepted the conversation
let shouldQueueForDownload = false;
if (isStory(message.attributes)) {
shouldQueueForDownload = await shouldDownloadStory(conversation.attributes);
} else {
shouldQueueForDownload =
hasAttachmentDownloads(message.attributes) &&
(conversation.getAccepted() || isOutgoing(message.attributes));
}
if (shouldQueueForDownload) {
if (shouldUseAttachmentDownloadQueue()) {
addToAttachmentDownloadQueue(idLog, message);
} else {
await queueAttachmentDownloadsForMessage(message);
}
}
}
export async function queueAttachmentDownloadsForMessage(
message: MessageModel,
urgency?: AttachmentDownloadUrgency
): Promise<boolean> {
const updates = await queueAttachmentDownloads(message.attributes, {
urgency,
});
if (!updates) {
return false;
}
message.set(updates);
queueUpdateMessage(message.attributes);
return true;
}
// Receive logic // Receive logic
// NOTE: If you're changing any logic in this function that deals with the // NOTE: If you're changing any logic in this function that deals with the
// count then you'll also have to modify ./hasAttachmentsDownloads // count then you'll also have to modify ./hasAttachmentsDownloads

View file

@ -16,11 +16,12 @@ import {
getConversationIdForLogging, getConversationIdForLogging,
getMessageIdForLogging, getMessageIdForLogging,
} from './idForLogging'; } from './idForLogging';
import { __DEPRECATED$getMessageById } from '../messages/getMessageById'; import { getMessageById } from '../messages/getMessageById';
import { getRecipientConversationIds } from './getRecipientConversationIds'; import { getRecipientConversationIds } from './getRecipientConversationIds';
import { getRecipients } from './getRecipients'; import { getRecipients } from './getRecipients';
import { repeat, zipObject } from './iterables'; import { repeat, zipObject } from './iterables';
import { isMe } from './whatTypeOfConversation'; import { isMe } from './whatTypeOfConversation';
import { postSaveUpdates } from './cleanup';
export async function sendDeleteForEveryoneMessage( export async function sendDeleteForEveryoneMessage(
conversationAttributes: ConversationAttributesType, conversationAttributes: ConversationAttributesType,
@ -35,10 +36,7 @@ export async function sendDeleteForEveryoneMessage(
timestamp: targetTimestamp, timestamp: targetTimestamp,
id: messageId, id: messageId,
} = options; } = options;
const message = await __DEPRECATED$getMessageById( const message = await getMessageById(messageId);
messageId,
'sendDeleteForEveryoneMessage'
);
if (!message) { if (!message) {
throw new Error('sendDeleteForEveryoneMessage: Cannot find message!'); throw new Error('sendDeleteForEveryoneMessage: Cannot find message!');
} }
@ -88,6 +86,7 @@ export async function sendDeleteForEveryoneMessage(
await DataWriter.saveMessage(message.attributes, { await DataWriter.saveMessage(message.attributes, {
jobToInsert, jobToInsert,
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
}); });
} catch (error) { } catch (error) {

View file

@ -24,7 +24,7 @@ import {
import { concat, filter, map, repeat, zipObject, find } from './iterables'; import { concat, filter, map, repeat, zipObject, find } from './iterables';
import { getConversationIdForLogging } from './idForLogging'; import { getConversationIdForLogging } from './idForLogging';
import { isQuoteAMatch } from '../messages/helpers'; import { isQuoteAMatch } from '../messages/helpers';
import { __DEPRECATED$getMessageById } from '../messages/getMessageById'; import { getMessageById } from '../messages/getMessageById';
import { handleEditMessage } from './handleEditMessage'; import { handleEditMessage } from './handleEditMessage';
import { incrementMessageCounter } from './incrementMessageCounter'; import { incrementMessageCounter } from './incrementMessageCounter';
import { isGroupV1 } from './whatTypeOfConversation'; import { isGroupV1 } from './whatTypeOfConversation';
@ -34,6 +34,7 @@ import { strictAssert } from './assert';
import { timeAndLogIfTooLong } from './timeAndLogIfTooLong'; import { timeAndLogIfTooLong } from './timeAndLogIfTooLong';
import { makeQuote } from './makeQuote'; import { makeQuote } from './makeQuote';
import { getMessageSentTimestamp } from './getMessageSentTimestamp'; import { getMessageSentTimestamp } from './getMessageSentTimestamp';
import { postSaveUpdates } from './cleanup';
const SEND_REPORT_THRESHOLD_MS = 25; const SEND_REPORT_THRESHOLD_MS = 25;
@ -65,10 +66,7 @@ export async function sendEditedMessage(
conversation.attributes conversation.attributes
)})`; )})`;
const targetMessage = await __DEPRECATED$getMessageById( const targetMessage = await getMessageById(targetMessageId);
targetMessageId,
'sendEditedMessage'
);
strictAssert(targetMessage, 'could not find message to edit'); strictAssert(targetMessage, 'could not find message to edit');
if (isGroupV1(conversation.attributes)) { if (isGroupV1(conversation.attributes)) {
@ -229,6 +227,7 @@ export async function sendEditedMessage(
await DataWriter.saveMessage(targetMessage.attributes, { await DataWriter.saveMessage(targetMessage.attributes, {
jobToInsert, jobToInsert,
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
} }
), ),

View file

@ -31,6 +31,8 @@ import { collect } from './iterables';
import { DurationInSeconds } from './durations'; import { DurationInSeconds } from './durations';
import { sanitizeLinkPreview } from '../services/LinkPreview'; import { sanitizeLinkPreview } from '../services/LinkPreview';
import type { DraftBodyRanges } from '../types/BodyRange'; import type { DraftBodyRanges } from '../types/BodyRange';
import { postSaveUpdates } from './cleanup';
import { MessageModel } from '../models/messages';
export async function sendStoryMessage( export async function sendStoryMessage(
listIds: Array<string>, listIds: Array<string>,
@ -308,11 +310,7 @@ export async function sendStoryMessage(
// * Add the message to the conversation // * Add the message to the conversation
await Promise.all( await Promise.all(
distributionListMessages.map(message => { distributionListMessages.map(message => {
window.MessageCache.__DEPRECATED$register( window.MessageCache.register(new MessageModel(message));
message.id,
new window.Whisper.Message(message),
'sendStoryMessage'
);
void ourConversation.addSingleMessage(message, { isJustSent: true }); void ourConversation.addSingleMessage(message, { isJustSent: true });
@ -320,6 +318,7 @@ export async function sendStoryMessage(
return DataWriter.saveMessage(message, { return DataWriter.saveMessage(message, {
forceSave: true, forceSave: true,
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
}) })
); );
@ -359,11 +358,7 @@ export async function sendStoryMessage(
timestamp: messageAttributes.timestamp, timestamp: messageAttributes.timestamp,
}, },
async jobToInsert => { async jobToInsert => {
window.MessageCache.__DEPRECATED$register( window.MessageCache.register(new MessageModel(messageAttributes));
messageAttributes.id,
new window.Whisper.Message(messageAttributes),
'sendStoryMessage'
);
const conversation = const conversation =
window.ConversationController.get(conversationId); window.ConversationController.get(conversationId);
void conversation?.addSingleMessage(messageAttributes, { void conversation?.addSingleMessage(messageAttributes, {
@ -377,6 +372,7 @@ export async function sendStoryMessage(
forceSave: true, forceSave: true,
jobToInsert, jobToInsert,
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
postSaveUpdates,
}); });
} }
); );

2
ts/window.d.ts vendored
View file

@ -32,7 +32,6 @@ import type { Receipt } from './types/Receipt';
import type { ConversationController } from './ConversationController'; import type { ConversationController } from './ConversationController';
import type { ReduxActions } from './state/types'; import type { ReduxActions } from './state/types';
import type { createApp } from './state/roots/createApp'; import type { createApp } from './state/roots/createApp';
import type { MessageModel } from './models/messages';
import type { ConversationModel } from './models/conversations'; import type { ConversationModel } from './models/conversations';
import type { BatcherType } from './util/batcher'; import type { BatcherType } from './util/batcher';
import type { ConfirmationDialog } from './components/ConfirmationDialog'; import type { ConfirmationDialog } from './components/ConfirmationDialog';
@ -319,7 +318,6 @@ declare global {
export type WhisperType = { export type WhisperType = {
Conversation: typeof ConversationModel; Conversation: typeof ConversationModel;
ConversationCollection: typeof ConversationModelCollectionType; ConversationCollection: typeof ConversationModelCollectionType;
Message: typeof MessageModel;
deliveryReceiptQueue: PQueue; deliveryReceiptQueue: PQueue;
deliveryReceiptBatcher: BatcherType<Receipt>; deliveryReceiptBatcher: BatcherType<Receipt>;

View file

@ -65,8 +65,7 @@ if (
)?.attributes; )?.attributes;
}, },
getConversation: (id: string) => window.ConversationController.get(id), getConversation: (id: string) => window.ConversationController.get(id),
getMessageById: (id: string) => getMessageById: (id: string) => window.MessageCache.getById(id),
window.MessageCache.__DEPRECATED$getById(id, 'SignalDebug'),
getMessageBySentAt: (timestamp: number) => getMessageBySentAt: (timestamp: number) =>
window.MessageCache.findBySentAt(timestamp, () => true), window.MessageCache.findBySentAt(timestamp, () => true),
getReduxState: () => window.reduxStore.getState(), getReduxState: () => window.reduxStore.getState(),