Use minimal replacement class for MessageModel
This commit is contained in:
parent
6b00cf756e
commit
f846678b90
95 changed files with 3919 additions and 4457 deletions
8
ts/CI.ts
8
ts/CI.ts
|
@ -15,6 +15,7 @@ import { migrateAllMessages } from './messages/migrateMessageData';
|
|||
import { SECOND } from './util/durations';
|
||||
import { isSignalRoute } from './util/signalRoutes';
|
||||
import { strictAssert } from './util/assert';
|
||||
import { MessageModel } from './models/messages';
|
||||
|
||||
type ResolveType = (data: unknown) => void;
|
||||
|
||||
|
@ -142,12 +143,7 @@ export function getCI({ deviceName }: GetCIOptionsType): CIType {
|
|||
[sentAt]
|
||||
);
|
||||
return messages.map(
|
||||
m =>
|
||||
window.MessageCache.__DEPRECATED$register(
|
||||
m.id,
|
||||
m,
|
||||
'CI.getMessagesBySentAt'
|
||||
).attributes
|
||||
m => window.MessageCache.register(new MessageModel(m)).attributes
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import { stats } from '../util/benchmark/stats';
|
|||
import type { StatsType } from '../util/benchmark/stats';
|
||||
import type { MessageAttributesType } from '../model-types.d';
|
||||
import * as log from '../logging/log';
|
||||
import { postSaveUpdates } from '../util/cleanup';
|
||||
|
||||
const BUFFER_DELAY_MS = 50;
|
||||
|
||||
|
@ -90,6 +91,7 @@ export async function populateConversationWithMessages({
|
|||
await DataWriter.saveMessages(messages, {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
conversation.set('active_at', Date.now());
|
||||
|
|
|
@ -203,6 +203,9 @@ import {
|
|||
maybeQueueDeviceNameFetch,
|
||||
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 {
|
||||
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
||||
|
@ -1421,7 +1424,7 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
void badgeImageFileDownloader.checkForFilesToDownload();
|
||||
|
||||
initializeExpiringMessageService(singleProtoJobQueue);
|
||||
initializeExpiringMessageService();
|
||||
|
||||
log.info('Blocked uuids cleanup: starting...');
|
||||
const blockedUuids = window.storage.get(BLOCKED_UUIDS_ID, []);
|
||||
|
@ -1473,6 +1476,7 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
await DataWriter.saveMessages(newMessageAttributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
}
|
||||
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
|
||||
drop(message.handleDataMessage(data.message, event.confirm));
|
||||
drop(handleDataMessage(message, data.message, event.confirm));
|
||||
}
|
||||
|
||||
async function onProfileKey({
|
||||
|
@ -2803,7 +2807,7 @@ export async function startApp(): Promise<void> {
|
|||
unidentifiedDeliveries,
|
||||
};
|
||||
|
||||
return new window.Whisper.Message(partialMessage);
|
||||
return new MessageModel(partialMessage);
|
||||
}
|
||||
|
||||
// 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
|
||||
drop(
|
||||
message.handleDataMessage(data.message, event.confirm, {
|
||||
handleDataMessage(message, data.message, event.confirm, {
|
||||
data,
|
||||
})
|
||||
);
|
||||
|
@ -3060,7 +3064,7 @@ export async function startApp(): Promise<void> {
|
|||
type: data.message.isStory ? 'story' : 'incoming',
|
||||
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
|
||||
};
|
||||
return new window.Whisper.Message(partialMessage);
|
||||
return new MessageModel(partialMessage);
|
||||
}
|
||||
|
||||
// Returns `false` if this message isn't a group call message.
|
||||
|
|
37
ts/groups.ts
37
ts/groups.ts
|
@ -102,6 +102,8 @@ import {
|
|||
} from './util/groupSendEndorsements';
|
||||
import { getProfile } from './util/getProfile';
|
||||
import { generateMessageId } from './util/generateMessageId';
|
||||
import { postSaveUpdates } from './util/cleanup';
|
||||
import { MessageModel } from './models/messages';
|
||||
|
||||
type AccessRequiredEnum = Proto.AccessControl.AccessRequired;
|
||||
|
||||
|
@ -253,7 +255,7 @@ export type GroupV2ChangeDetailType =
|
|||
|
||||
export type GroupV2ChangeType = {
|
||||
from?: ServiceIdString;
|
||||
details: Array<GroupV2ChangeDetailType>;
|
||||
details: ReadonlyArray<GroupV2ChangeDetailType>;
|
||||
};
|
||||
|
||||
export type GroupFields = {
|
||||
|
@ -2016,7 +2018,7 @@ export async function createGroupV2(
|
|||
revision: groupV2Info.revision,
|
||||
});
|
||||
|
||||
const createdTheGroupMessage: MessageAttributesType = {
|
||||
const createdTheGroupMessage = new MessageModel({
|
||||
...generateMessageId(incrementMessageCounter()),
|
||||
|
||||
schemaVersion: MAX_MESSAGE_SCHEMA,
|
||||
|
@ -2032,17 +2034,12 @@ export async function createGroupV2(
|
|||
from: ourAci,
|
||||
details: [{ type: 'create' }],
|
||||
},
|
||||
};
|
||||
await DataWriter.saveMessages([createdTheGroupMessage], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
});
|
||||
window.MessageCache.__DEPRECATED$register(
|
||||
createdTheGroupMessage.id,
|
||||
new window.Whisper.Message(createdTheGroupMessage),
|
||||
'createGroupV2'
|
||||
);
|
||||
conversation.trigger('newmessage', createdTheGroupMessage);
|
||||
await window.MessageCache.saveMessage(createdTheGroupMessage, {
|
||||
forceSave: true,
|
||||
});
|
||||
window.MessageCache.register(createdTheGroupMessage);
|
||||
drop(conversation.onNewMessage(createdTheGroupMessage));
|
||||
|
||||
if (expireTimer) {
|
||||
await conversation.updateExpirationTimer(expireTimer, {
|
||||
|
@ -3442,6 +3439,7 @@ async function appendChangeMessages(
|
|||
log.info(`appendChangeMessages/${logId}: updating ${first.id}`);
|
||||
await DataWriter.saveMessage(first, {
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
|
||||
// We don't use forceSave here because this is an update of existing
|
||||
// message.
|
||||
|
@ -3453,6 +3451,7 @@ async function appendChangeMessages(
|
|||
await DataWriter.saveMessages(rest, {
|
||||
ourAci,
|
||||
forceSave: true,
|
||||
postSaveUpdates,
|
||||
});
|
||||
} else {
|
||||
log.info(
|
||||
|
@ -3461,15 +3460,13 @@ async function appendChangeMessages(
|
|||
await DataWriter.saveMessages(mergedMessages, {
|
||||
ourAci,
|
||||
forceSave: true,
|
||||
postSaveUpdates,
|
||||
});
|
||||
}
|
||||
|
||||
let newMessages = 0;
|
||||
for (const changeMessage of mergedMessages) {
|
||||
const existing = window.MessageCache.__DEPRECATED$getById(
|
||||
changeMessage.id,
|
||||
'appendChangeMessages'
|
||||
);
|
||||
const existing = window.MessageCache.getById(changeMessage.id);
|
||||
|
||||
// Update existing message
|
||||
if (existing) {
|
||||
|
@ -3481,12 +3478,8 @@ async function appendChangeMessages(
|
|||
continue;
|
||||
}
|
||||
|
||||
window.MessageCache.__DEPRECATED$register(
|
||||
changeMessage.id,
|
||||
new window.Whisper.Message(changeMessage),
|
||||
'appendChangeMessages'
|
||||
);
|
||||
conversation.trigger('newmessage', changeMessage);
|
||||
const model = window.MessageCache.register(new MessageModel(changeMessage));
|
||||
drop(conversation.onNewMessage(model));
|
||||
newMessages += 1;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
AttachmentVariant,
|
||||
mightBeOnBackupTier,
|
||||
} from '../types/Attachment';
|
||||
import { __DEPRECATED$getMessageById } from '../messages/getMessageById';
|
||||
import { getMessageById } from '../messages/getMessageById';
|
||||
import {
|
||||
KIBIBYTE,
|
||||
getMaximumIncomingAttachmentSizeInKb,
|
||||
|
@ -52,6 +52,7 @@ import {
|
|||
} from '../AttachmentCrypto';
|
||||
import { safeParsePartial } from '../util/schemas';
|
||||
import { createBatcher } from '../util/batcher';
|
||||
import { postSaveUpdates } from '../util/cleanup';
|
||||
|
||||
export enum AttachmentDownloadUrgency {
|
||||
IMMEDIATE = 'immediate',
|
||||
|
@ -327,10 +328,7 @@ async function runDownloadAttachmentJob({
|
|||
const jobIdForLogging = getJobIdForLogging(job);
|
||||
const logId = `AttachmentDownloadManager/runDownloadAttachmentJob/${jobIdForLogging}`;
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
job.messageId,
|
||||
'runDownloadAttachmentJob'
|
||||
);
|
||||
const message = await getMessageById(job.messageId);
|
||||
|
||||
if (!message) {
|
||||
log.error(`${logId} message not found`);
|
||||
|
@ -430,6 +428,7 @@ async function runDownloadAttachmentJob({
|
|||
// is good
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import { getUntrustedConversationServiceIds } from './getUntrustedConversationSe
|
|||
import { handleMessageSend } from '../../util/handleMessageSend';
|
||||
import { isConversationAccepted } from '../../util/isConversationAccepted';
|
||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
||||
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
|
||||
import { getMessageById } from '../../messages/getMessageById';
|
||||
import { isNotNil } from '../../util/isNotNil';
|
||||
import type { CallbackResultType } from '../../textsecure/Types.d';
|
||||
import type { MessageModel } from '../../models/messages';
|
||||
|
@ -38,6 +38,7 @@ import type { LoggerType } from '../../types/Logging';
|
|||
import type { ServiceIdString } from '../../types/ServiceId';
|
||||
import { isStory } from '../../messages/helpers';
|
||||
import { sendToGroup } from '../../util/sendToGroup';
|
||||
import { postSaveUpdates } from '../../util/cleanup';
|
||||
|
||||
export async function sendDeleteForEveryone(
|
||||
conversation: ConversationModel,
|
||||
|
@ -60,7 +61,7 @@ export async function sendDeleteForEveryone(
|
|||
|
||||
const logId = `sendDeleteForEveryone(${conversation.idForLogging()}, ${messageId})`;
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(messageId, logId);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
log.error(`${logId}: Failed to fetch message. Failing job.`);
|
||||
return;
|
||||
|
@ -307,6 +308,7 @@ async function updateMessageWithSuccessfulSends(
|
|||
});
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
return;
|
||||
|
@ -330,6 +332,7 @@ async function updateMessageWithSuccessfulSends(
|
|||
});
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -346,5 +349,6 @@ async function updateMessageWithFailure(
|
|||
message.set({ deletedForEveryoneFailed: true });
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import { getUntrustedConversationServiceIds } from './getUntrustedConversationSe
|
|||
import { handleMessageSend } from '../../util/handleMessageSend';
|
||||
import { isConversationAccepted } from '../../util/isConversationAccepted';
|
||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
||||
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
|
||||
import { getMessageById } from '../../messages/getMessageById';
|
||||
import { isNotNil } from '../../util/isNotNil';
|
||||
import type { CallbackResultType } from '../../textsecure/Types.d';
|
||||
import type { MessageModel } from '../../models/messages';
|
||||
|
@ -29,6 +29,7 @@ import { SendMessageProtoError } from '../../textsecure/Errors';
|
|||
import { strictAssert } from '../../util/assert';
|
||||
import type { LoggerType } from '../../types/Logging';
|
||||
import { isStory } from '../../messages/helpers';
|
||||
import { postSaveUpdates } from '../../util/cleanup';
|
||||
|
||||
export async function sendDeleteStoryForEveryone(
|
||||
ourConversation: ConversationModel,
|
||||
|
@ -46,10 +47,7 @@ export async function sendDeleteStoryForEveryone(
|
|||
|
||||
const logId = `sendDeleteStoryForEveryone(${storyId})`;
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
storyId,
|
||||
'sendDeleteStoryForEveryone'
|
||||
);
|
||||
const message = await getMessageById(storyId);
|
||||
if (!message) {
|
||||
log.error(`${logId}: Failed to fetch message. Failing job.`);
|
||||
return;
|
||||
|
@ -284,6 +282,7 @@ async function updateMessageWithSuccessfulSends(
|
|||
});
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
return;
|
||||
|
@ -307,6 +306,7 @@ async function updateMessageWithSuccessfulSends(
|
|||
});
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -323,5 +323,6 @@ async function updateMessageWithFailure(
|
|||
message.set({ deletedForEveryoneFailed: true });
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import { DataWriter } from '../../sql/Client';
|
|||
import * as Errors from '../../types/errors';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import type { MessageModel } from '../../models/messages';
|
||||
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
|
||||
import { getMessageById } from '../../messages/getMessageById';
|
||||
import type { ConversationModel } from '../../models/conversations';
|
||||
import { isGroup, isGroupV2, isMe } from '../../util/whatTypeOfConversation';
|
||||
import { getSendOptions } from '../../util/getSendOptions';
|
||||
|
@ -56,6 +56,13 @@ import {
|
|||
import { getMessageSentTimestamp } from '../../util/getMessageSentTimestamp';
|
||||
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||
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;
|
||||
|
||||
|
@ -73,10 +80,7 @@ export async function sendNormalMessage(
|
|||
const { Message } = window.Signal.Types;
|
||||
|
||||
const { messageId, revision, editedMessageTimestamp } = data;
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'sendNormalMessage'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
log.info(
|
||||
`message ${messageId} was not found, maybe because it was deleted. Giving up on sending it`
|
||||
|
@ -84,7 +88,9 @@ export async function sendNormalMessage(
|
|||
return;
|
||||
}
|
||||
|
||||
const messageConversation = message.getConversation();
|
||||
const messageConversation = window.ConversationController.get(
|
||||
message.get('conversationId')
|
||||
);
|
||||
if (messageConversation !== conversation) {
|
||||
log.error(
|
||||
`Message conversation '${messageConversation?.idForLogging()}' does not match job conversation ${conversation.idForLogging()}`
|
||||
|
@ -106,7 +112,7 @@ export async function sendNormalMessage(
|
|||
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`);
|
||||
return;
|
||||
}
|
||||
|
@ -285,7 +291,7 @@ export async function sendNormalMessage(
|
|||
timestamp: targetTimestamp,
|
||||
reaction,
|
||||
});
|
||||
messageSendPromise = message.sendSyncMessageOnly({
|
||||
messageSendPromise = sendSyncMessageOnly(message, {
|
||||
dataMessage,
|
||||
saveErrors,
|
||||
targetTimestamp,
|
||||
|
@ -407,7 +413,7 @@ export async function sendNormalMessage(
|
|||
});
|
||||
}
|
||||
|
||||
messageSendPromise = message.send({
|
||||
messageSendPromise = send(message, {
|
||||
promise: handleMessageSend(innerPromise, {
|
||||
messageIds: [messageId],
|
||||
sendType: 'message',
|
||||
|
@ -657,14 +663,13 @@ async function getMessageSendData({
|
|||
uploadQueue,
|
||||
}),
|
||||
uploadMessageSticker(message, uploadQueue),
|
||||
storyId
|
||||
? __DEPRECATED$getMessageById(storyId, 'sendNormalMessage')
|
||||
: undefined,
|
||||
storyId ? getMessageById(storyId) : undefined,
|
||||
]);
|
||||
|
||||
// Save message after uploading attachments
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
const storyReaction = message.get('storyReaction');
|
||||
|
@ -732,7 +737,7 @@ async function uploadSingleAttachment({
|
|||
const uploaded = await uploadAttachment(withData);
|
||||
|
||||
// Add digest to the attachment
|
||||
const logId = `uploadSingleAttachment(${message.idForLogging()}`;
|
||||
const logId = `uploadSingleAttachment(${getMessageIdForLogging(message.attributes)}`;
|
||||
const oldAttachments = getPropForTimestamp({
|
||||
log,
|
||||
message: message.attributes,
|
||||
|
@ -788,7 +793,7 @@ async function uploadLongMessageAttachment({
|
|||
const uploaded = await uploadAttachment(withData);
|
||||
|
||||
// Add digest to the attachment
|
||||
const logId = `uploadLongMessageAttachment(${message.idForLogging()}`;
|
||||
const logId = `uploadLongMessageAttachment(${getMessageIdForLogging(message.attributes)}`;
|
||||
const oldAttachment = getPropForTimestamp({
|
||||
log,
|
||||
message: message.attributes,
|
||||
|
@ -872,7 +877,7 @@ async function uploadMessageQuote({
|
|||
);
|
||||
|
||||
// Update message with attachment digests
|
||||
const logId = `uploadMessageQuote(${message.idForLogging()}`;
|
||||
const logId = `uploadMessageQuote(${getMessageIdForLogging(message.attributes)}`;
|
||||
const oldQuote = getPropForTimestamp({
|
||||
log,
|
||||
message: message.attributes,
|
||||
|
@ -980,7 +985,7 @@ async function uploadMessagePreviews({
|
|||
);
|
||||
|
||||
// Update message with attachment digests
|
||||
const logId = `uploadMessagePreviews(${message.idForLogging()}`;
|
||||
const logId = `uploadMessagePreviews(${getMessageIdForLogging(message.attributes)}`;
|
||||
const oldPreview = getPropForTimestamp({
|
||||
log,
|
||||
message: message.attributes,
|
||||
|
@ -1043,7 +1048,7 @@ async function uploadMessageSticker(
|
|||
);
|
||||
|
||||
// Add digest to the attachment
|
||||
const logId = `uploadMessageSticker(${message.idForLogging()}`;
|
||||
const logId = `uploadMessageSticker(${getMessageIdForLogging(message.attributes)}`;
|
||||
const existingSticker = message.get('sticker');
|
||||
strictAssert(
|
||||
existingSticker?.data !== undefined,
|
||||
|
@ -1054,12 +1059,14 @@ async function uploadMessageSticker(
|
|||
existingSticker.data.path === startingSticker?.data?.path,
|
||||
`${logId}: Sticker was uploaded, but message has a different sticker`
|
||||
);
|
||||
message.set('sticker', {
|
||||
message.set({
|
||||
sticker: {
|
||||
...existingSticker,
|
||||
data: {
|
||||
...existingSticker.data,
|
||||
...copyCdnFields(uploaded),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -1111,7 +1118,7 @@ async function uploadMessageContacts(
|
|||
);
|
||||
|
||||
// Add digest to the attachment
|
||||
const logId = `uploadMessageContacts(${message.idForLogging()}`;
|
||||
const logId = `uploadMessageContacts(${getMessageIdForLogging(message.attributes)}`;
|
||||
const oldContact = message.get('contact');
|
||||
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;
|
||||
}
|
||||
|
@ -1162,10 +1169,9 @@ async function markMessageFailed({
|
|||
message: MessageModel;
|
||||
targetTimestamp: number;
|
||||
}): Promise<void> {
|
||||
message.markFailed(targetTimestamp);
|
||||
void message.saveErrors(errors, { skipSave: true });
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
markFailed(message, targetTimestamp);
|
||||
await saveErrorsOnMessage(message, errors, {
|
||||
skipSave: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -7,14 +7,14 @@ import * as Errors from '../../types/errors';
|
|||
import { strictAssert } from '../../util/assert';
|
||||
import { repeat, zipObject } from '../../util/iterables';
|
||||
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 { ConversationModel } from '../../models/conversations';
|
||||
import { DataWriter } from '../../sql/Client';
|
||||
|
||||
import * as reactionUtil from '../../reactions/util';
|
||||
import { isSent, SendStatus } from '../../messages/MessageSendState';
|
||||
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
|
||||
import { getMessageById } from '../../messages/getMessageById';
|
||||
import { isIncoming } from '../../messages/helpers';
|
||||
import {
|
||||
isMe,
|
||||
|
@ -41,6 +41,9 @@ import { isConversationAccepted } from '../../util/isConversationAccepted';
|
|||
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
||||
import type { LoggerType } from '../../types/Logging';
|
||||
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(
|
||||
conversation: ConversationModel,
|
||||
|
@ -61,7 +64,7 @@ export async function sendReaction(
|
|||
const ourConversationId =
|
||||
window.ConversationController.getOurConversationIdOrThrow();
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(messageId, 'sendReaction');
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
log.info(
|
||||
`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)) {
|
||||
log.info(`could not react to ${messageId}. Removing this pending reaction`);
|
||||
markReactionFailed(message, pendingReaction);
|
||||
await DataWriter.saveMessage(message.attributes, { ourAci });
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -96,7 +102,10 @@ export async function sendReaction(
|
|||
`reacting to message ${messageId} ran out of time. Giving up on sending it`
|
||||
);
|
||||
markReactionFailed(message, pendingReaction);
|
||||
await DataWriter.saveMessage(message.attributes, { ourAci });
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -108,7 +117,9 @@ export async function sendReaction(
|
|||
let originalError: Error | undefined;
|
||||
|
||||
try {
|
||||
const messageConversation = message.getConversation();
|
||||
const messageConversation = window.ConversationController.get(
|
||||
message.get('conversationId')
|
||||
);
|
||||
if (messageConversation !== conversation) {
|
||||
log.error(
|
||||
`message conversation '${messageConversation?.idForLogging()}' does not match job conversation ${conversation.idForLogging()}`
|
||||
|
@ -158,7 +169,7 @@ export async function sendReaction(
|
|||
targetAuthorAci,
|
||||
remove: !emoji,
|
||||
};
|
||||
const ephemeralMessageForReactionSend = new window.Whisper.Message({
|
||||
const ephemeralMessageForReactionSend = new MessageModel({
|
||||
...generateMessageId(incrementMessageCounter()),
|
||||
type: 'outgoing',
|
||||
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;
|
||||
|
||||
// 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;
|
||||
const successfulConversationIds = new Set<string>();
|
||||
|
||||
|
@ -199,7 +209,7 @@ export async function sendReaction(
|
|||
recipients: allRecipientServiceIds,
|
||||
timestamp: pendingReaction.timestamp,
|
||||
});
|
||||
await ephemeralMessageForReactionSend.sendSyncMessageOnly({
|
||||
await sendSyncMessageOnly(ephemeralMessageForReactionSend, {
|
||||
dataMessage,
|
||||
saveErrors,
|
||||
targetTimestamp: pendingReaction.timestamp,
|
||||
|
@ -292,7 +302,7 @@ export async function sendReaction(
|
|||
);
|
||||
}
|
||||
|
||||
await ephemeralMessageForReactionSend.send({
|
||||
await send(ephemeralMessageForReactionSend, {
|
||||
promise: handleMessageSend(promise, {
|
||||
messageIds: [messageId],
|
||||
sendType: 'reaction',
|
||||
|
@ -334,19 +344,16 @@ export async function sendReaction(
|
|||
if (!ephemeralMessageForReactionSend.doNotSave) {
|
||||
const reactionMessage = ephemeralMessageForReactionSend;
|
||||
|
||||
await reactionMessage.hydrateStoryContext(message.attributes, {
|
||||
await hydrateStoryContext(reactionMessage.id, message.attributes, {
|
||||
shouldSave: false,
|
||||
});
|
||||
await DataWriter.saveMessage(reactionMessage.attributes, {
|
||||
ourAci,
|
||||
forceSave: true,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
window.MessageCache.__DEPRECATED$register(
|
||||
reactionMessage.id,
|
||||
reactionMessage,
|
||||
'sendReaction'
|
||||
);
|
||||
window.MessageCache.register(reactionMessage);
|
||||
void conversation.addSingleMessage(reactionMessage.attributes);
|
||||
}
|
||||
}
|
||||
|
@ -375,7 +382,10 @@ export async function sendReaction(
|
|||
toThrow: originalError || thrownError,
|
||||
});
|
||||
} finally {
|
||||
await DataWriter.saveMessage(message.attributes, { ourAci });
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -388,9 +398,9 @@ const setReactions = (
|
|||
reactions: Array<MessageReactionType>
|
||||
): void => {
|
||||
if (reactions.length) {
|
||||
message.set('reactions', reactions);
|
||||
message.set({ reactions });
|
||||
} else {
|
||||
message.set('reactions', undefined);
|
||||
message.set({ reactions: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -39,6 +39,13 @@ import { distributionListToSendTarget } from '../../util/distributionListToSendT
|
|||
import { uploadAttachment } from '../../util/uploadAttachment';
|
||||
import { SendMessageChallengeError } from '../../textsecure/Errors';
|
||||
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(
|
||||
conversation: ConversationModel,
|
||||
|
@ -71,14 +78,15 @@ export async function sendStory(
|
|||
}
|
||||
|
||||
const notFound = new Set(messageIds);
|
||||
const messages = (await getMessagesById(messageIds, 'sendStory')).filter(
|
||||
message => {
|
||||
const messages = (await getMessagesById(messageIds)).filter(message => {
|
||||
notFound.delete(message.id);
|
||||
|
||||
const distributionId = message.get('storyDistributionListId');
|
||||
const logId = `stories.sendStory(${timestamp}/${distributionId})`;
|
||||
|
||||
const messageConversation = message.getConversation();
|
||||
const messageConversation = window.ConversationController.get(
|
||||
message.get('conversationId')
|
||||
);
|
||||
if (messageConversation !== conversation) {
|
||||
log.error(
|
||||
`${logId}: Message conversation ` +
|
||||
|
@ -97,14 +105,13 @@ export async function sendStory(
|
|||
return false;
|
||||
}
|
||||
|
||||
if (message.isErased() || message.get('deletedForEveryone')) {
|
||||
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) {
|
||||
log.info(
|
||||
|
@ -367,7 +374,7 @@ export async function sendStory(
|
|||
// eslint-disable-next-line no-param-reassign
|
||||
message.doNotSendSyncMessage = true;
|
||||
|
||||
const messageSendPromise = message.send({
|
||||
const messageSendPromise = send(message, {
|
||||
promise: handleMessageSend(innerPromise, {
|
||||
messageIds: [message.id],
|
||||
sendType: 'story',
|
||||
|
@ -535,16 +542,17 @@ export async function sendStory(
|
|||
}, {} as SendStateByConversationId);
|
||||
|
||||
if (hasFailedSends) {
|
||||
message.notifyStorySendFailed();
|
||||
notifyStorySendFailed(message);
|
||||
}
|
||||
|
||||
if (isEqual(oldSendStateByConversationId, newSendStateByConversationId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
message.set('sendStateByConversationId', newSendStateByConversationId);
|
||||
message.set({ sendStateByConversationId: newSendStateByConversationId });
|
||||
return DataWriter.saveMessage(message.attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
@ -688,10 +696,9 @@ async function markMessageFailed(
|
|||
message: MessageModel,
|
||||
errors: Array<Error>
|
||||
): Promise<void> {
|
||||
message.markFailed();
|
||||
void message.saveErrors(errors, { skipSave: true });
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
markFailed(message);
|
||||
await saveErrorsOnMessage(message, errors, {
|
||||
skipSave: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,55 @@ import type { AttachmentDownloadJobTypeType } from '../types/AttachmentDownload'
|
|||
|
||||
import type { AttachmentType } 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(
|
||||
messageId: string,
|
||||
|
@ -15,7 +63,7 @@ export async function addAttachmentToMessage(
|
|||
{ type }: { type: AttachmentDownloadJobTypeType }
|
||||
): Promise<void> {
|
||||
const logPrefix = `${jobLogId}/addAttachmentToMessage`;
|
||||
const message = await __DEPRECATED$getMessageById(messageId, logPrefix);
|
||||
const message = await getMessageById(messageId);
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
|
|
|
@ -9,6 +9,7 @@ import * as Errors from '../types/errors';
|
|||
import { deleteForEveryone } from '../util/deleteForEveryone';
|
||||
import { drop } from '../util/drop';
|
||||
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
export type DeleteAttributesType = {
|
||||
envelopeId: string;
|
||||
|
@ -86,10 +87,8 @@ export async function onDelete(del: DeleteAttributesType): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$register(
|
||||
targetMessage.id,
|
||||
targetMessage,
|
||||
'Deletes.onDelete'
|
||||
const message = window.MessageCache.register(
|
||||
new MessageModel(targetMessage)
|
||||
);
|
||||
|
||||
await deleteForEveryone(message, del);
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
isAttachmentDownloadQueueEmpty,
|
||||
registerQueueEmptyCallback,
|
||||
} from '../util/attachmentDownloadQueue';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
export type EditAttributesType = {
|
||||
conversationId: string;
|
||||
|
@ -134,10 +135,8 @@ export async function onEdit(edit: EditAttributesType): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$register(
|
||||
targetMessage.id,
|
||||
targetMessage,
|
||||
'Edits.onEdit'
|
||||
const message = window.MessageCache.register(
|
||||
new MessageModel(targetMessage)
|
||||
);
|
||||
|
||||
await handleEditMessage(message.attributes, edit);
|
||||
|
|
|
@ -32,6 +32,9 @@ import {
|
|||
RECEIPT_BATCHER_WAIT_MS,
|
||||
} from '../types/Receipt';
|
||||
import { drop } from '../util/drop';
|
||||
import { getMessageById } from '../messages/getMessageById';
|
||||
import { postSaveUpdates } from '../util/cleanup';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
const { deleteSentProtoRecipient, removeSyncTaskById } = DataWriter;
|
||||
|
||||
|
@ -78,12 +81,11 @@ const processReceiptBatcher = createWaitBatcher({
|
|||
> = new Map();
|
||||
|
||||
function addReceiptAndTargetMessage(
|
||||
message: MessageAttributesType,
|
||||
message: MessageModel,
|
||||
receipt: MessageReceiptAttributesType
|
||||
): void {
|
||||
const existing = receiptsByMessageId.get(message.id);
|
||||
if (!existing) {
|
||||
window.MessageCache.toMessageAttributes(message);
|
||||
receiptsByMessageId.set(message.id, [receipt]);
|
||||
} else {
|
||||
existing.push(receipt);
|
||||
|
@ -151,9 +153,10 @@ const processReceiptBatcher = createWaitBatcher({
|
|||
);
|
||||
|
||||
if (targetMessages.length) {
|
||||
targetMessages.forEach(msg =>
|
||||
addReceiptAndTargetMessage(msg, receipt)
|
||||
);
|
||||
targetMessages.forEach(msg => {
|
||||
const model = window.MessageCache.register(new MessageModel(msg));
|
||||
addReceiptAndTargetMessage(model, receipt);
|
||||
});
|
||||
} else {
|
||||
// Nope, no target message was found
|
||||
const { receiptSync } = receipt;
|
||||
|
@ -188,53 +191,43 @@ async function processReceiptsForMessage(
|
|||
}
|
||||
|
||||
// Get message from cache or DB
|
||||
const message = await window.MessageCache.resolveAttributes(
|
||||
'processReceiptsForMessage',
|
||||
messageId
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
`processReceiptsForMessage: Failed to find message ${messageId}`
|
||||
);
|
||||
}
|
||||
|
||||
// Note: it is important to have no `await` in between `resolveAttributes` and
|
||||
// `setAttributes` since it might overwrite other updates otherwise.
|
||||
const { updatedMessage, validReceipts, droppedReceipts } =
|
||||
updateMessageWithReceipts(message, receipts);
|
||||
const { validReceipts } = await updateMessageWithReceipts(message, receipts);
|
||||
|
||||
// Save it to cache & to DB, and remove dropped receipts
|
||||
await Promise.all([
|
||||
window.MessageCache.setAttributes({
|
||||
messageId,
|
||||
messageAttributes: updatedMessage,
|
||||
skipSaveToDatabase: false,
|
||||
}),
|
||||
Promise.all(droppedReceipts.map(remove)),
|
||||
]);
|
||||
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||
await DataWriter.saveMessage(message.attributes, { ourAci, postSaveUpdates });
|
||||
|
||||
// Confirm/remove receipts, and delete sent protos
|
||||
for (const receipt of validReceipts) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await remove(receipt);
|
||||
drop(addToDeleteSentProtoBatcher(receipt, updatedMessage));
|
||||
drop(addToDeleteSentProtoBatcher(receipt, message.attributes));
|
||||
}
|
||||
|
||||
// notify frontend listeners
|
||||
const conversation = window.ConversationController.get(
|
||||
message.conversationId
|
||||
message.get('conversationId')
|
||||
);
|
||||
conversation?.debouncedUpdateLastMessage?.();
|
||||
}
|
||||
|
||||
function updateMessageWithReceipts(
|
||||
message: MessageAttributesType,
|
||||
async function updateMessageWithReceipts(
|
||||
message: MessageModel,
|
||||
receipts: Array<MessageReceiptAttributesType>
|
||||
): {
|
||||
updatedMessage: MessageAttributesType;
|
||||
): Promise<{
|
||||
validReceipts: Array<MessageReceiptAttributesType>;
|
||||
droppedReceipts: Array<MessageReceiptAttributesType>;
|
||||
} {
|
||||
const logId = `updateMessageWithReceipts(timestamp=${message.timestamp})`;
|
||||
}> {
|
||||
const logId = `updateMessageWithReceipts(timestamp=${message.get('timestamp')})`;
|
||||
|
||||
const droppedReceipts: Array<MessageReceiptAttributesType> = [];
|
||||
const receiptsToProcess = receipts.filter(receipt => {
|
||||
if (shouldDropReceipt(receipt, message)) {
|
||||
if (shouldDropReceipt(receipt, message.attributes)) {
|
||||
const { receiptSync } = receipt;
|
||||
log.info(
|
||||
`${logId}: Dropping a receipt ${receiptSync.type} for sentAt=${receiptSync.messageSentAt}`
|
||||
|
@ -257,14 +250,16 @@ function updateMessageWithReceipts(
|
|||
);
|
||||
|
||||
// Generate the updated message synchronously
|
||||
let updatedMessage: MessageAttributesType = { ...message };
|
||||
let { attributes } = message;
|
||||
for (const receipt of receiptsToProcess) {
|
||||
updatedMessage = {
|
||||
...updatedMessage,
|
||||
...updateMessageSendStateWithReceipt(updatedMessage, receipt),
|
||||
attributes = {
|
||||
...attributes,
|
||||
...updateMessageSendStateWithReceipt(attributes, receipt),
|
||||
};
|
||||
}
|
||||
return { updatedMessage, validReceipts: receiptsToProcess, droppedReceipts };
|
||||
message.set(attributes);
|
||||
|
||||
return { validReceipts: receiptsToProcess };
|
||||
}
|
||||
|
||||
const deleteSentProtoBatcher = createWaitBatcher({
|
||||
|
@ -310,7 +305,7 @@ function getTargetMessage({
|
|||
sourceConversationId: string;
|
||||
messagesMatchingTimestamp: ReadonlyArray<MessageAttributesType>;
|
||||
targetTimestamp: number;
|
||||
}): MessageAttributesType | null {
|
||||
}): MessageModel | null {
|
||||
if (messagesMatchingTimestamp.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
@ -366,7 +361,7 @@ function getTargetMessage({
|
|||
}
|
||||
|
||||
const message = matchingMessages[0];
|
||||
return window.MessageCache.toMessageAttributes(message);
|
||||
return window.MessageCache.register(new MessageModel(message));
|
||||
}
|
||||
const wasDeliveredWithSealedSender = (
|
||||
conversationId: string,
|
||||
|
|
|
@ -1,23 +1,45 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { maxBy } from 'lodash';
|
||||
|
||||
import type { AciString } from '../types/ServiceId';
|
||||
import type {
|
||||
MessageAttributesType,
|
||||
MessageReactionType,
|
||||
ReadonlyMessageAttributesType,
|
||||
} from '../model-types.d';
|
||||
import type { MessageModel } from '../models/messages';
|
||||
import type { ReactionSource } from '../reactions/ReactionSource';
|
||||
import { DataReader } from '../sql/Client';
|
||||
import { MessageModel } from '../models/messages';
|
||||
import { ReactionSource } from '../reactions/ReactionSource';
|
||||
import { DataReader, DataWriter } from '../sql/Client';
|
||||
import * as Errors from '../types/errors';
|
||||
import * as log from '../logging/log';
|
||||
import { getAuthor } from '../messages/helpers';
|
||||
import { getAuthor, isIncoming, isOutgoing } from '../messages/helpers';
|
||||
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
|
||||
import { isMe } from '../util/whatTypeOfConversation';
|
||||
import { isStory } from '../state/selectors/message';
|
||||
import { isDirectConversation, isMe } from '../util/whatTypeOfConversation';
|
||||
import {
|
||||
getMessagePropStatus,
|
||||
hasErrors,
|
||||
isStory,
|
||||
} from '../state/selectors/message';
|
||||
import { getPropForTimestamp } from '../util/editHelpers';
|
||||
import { isSent } from '../messages/MessageSendState';
|
||||
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 = {
|
||||
emoji: string;
|
||||
|
@ -36,24 +58,26 @@ export type ReactionAttributesType = {
|
|||
receivedAtDate: number;
|
||||
};
|
||||
|
||||
const reactions = new Map<string, ReactionAttributesType>();
|
||||
const reactionCache = new Map<string, ReactionAttributesType>();
|
||||
|
||||
function remove(reaction: ReactionAttributesType): void {
|
||||
reactions.delete(reaction.envelopeId);
|
||||
reactionCache.delete(reaction.envelopeId);
|
||||
reaction.removeFromMessageReceiverCache();
|
||||
}
|
||||
|
||||
export function findReactionsForMessage(
|
||||
message: ReadonlyMessageAttributesType
|
||||
): Array<ReactionAttributesType> {
|
||||
const matchingReactions = Array.from(reactions.values()).filter(reaction => {
|
||||
const matchingReactions = Array.from(reactionCache.values()).filter(
|
||||
reaction => {
|
||||
return isMessageAMatchForReaction({
|
||||
message,
|
||||
targetTimestamp: reaction.targetTimestamp,
|
||||
targetAuthorAci: reaction.targetAuthorAci,
|
||||
reactionSenderConversationId: reaction.fromId,
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
matchingReactions.forEach(reaction => remove(reaction));
|
||||
return matchingReactions;
|
||||
|
@ -173,7 +197,7 @@ function isMessageAMatchForReaction({
|
|||
export async function onReaction(
|
||||
reaction: ReactionAttributesType
|
||||
): Promise<void> {
|
||||
reactions.set(reaction.envelopeId, reaction);
|
||||
reactionCache.set(reaction.envelopeId, reaction);
|
||||
|
||||
const logId = `Reactions.onReaction(timestamp=${reaction.timestamp};target=${reaction.targetTimestamp})`;
|
||||
|
||||
|
@ -231,23 +255,21 @@ export async function onReaction(
|
|||
return;
|
||||
}
|
||||
|
||||
const targetMessageModel = window.MessageCache.__DEPRECATED$register(
|
||||
targetMessage.id,
|
||||
targetMessage,
|
||||
'Reactions.onReaction'
|
||||
const targetMessageModel = window.MessageCache.register(
|
||||
new MessageModel(targetMessage)
|
||||
);
|
||||
|
||||
// Use the generated message in ts/background.ts to create a message
|
||||
// if the reaction is targeted at a story.
|
||||
if (!isStory(targetMessage)) {
|
||||
await targetMessageModel.handleReaction(reaction);
|
||||
await handleReaction(targetMessageModel, reaction);
|
||||
} else {
|
||||
const generatedMessage = reaction.generatedMessageForStoryReaction;
|
||||
strictAssert(
|
||||
generatedMessage,
|
||||
'Generated message must exist for story reaction'
|
||||
);
|
||||
await generatedMessage.handleReaction(reaction, {
|
||||
await handleReaction(generatedMessage, reaction, {
|
||||
storyMessage: targetMessage,
|
||||
});
|
||||
}
|
||||
|
@ -260,3 +282,324 @@ export async function onReaction(
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ import { queueUpdateMessage } from '../util/messageBatcher';
|
|||
import { strictAssert } from '../util/assert';
|
||||
import { isAciString } from '../util/isAciString';
|
||||
import { DataReader, DataWriter } from '../sql/Client';
|
||||
import { markRead } from '../services/MessageUpdater';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
const { removeSyncTaskById } = DataWriter;
|
||||
|
||||
|
@ -146,11 +148,7 @@ export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
|
|||
|
||||
notificationService.removeBy({ messageId: found.id });
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$register(
|
||||
found.id,
|
||||
found,
|
||||
'ReadSyncs.onSync'
|
||||
);
|
||||
const message = window.MessageCache.register(new MessageModel(found));
|
||||
const readAt = Math.min(readSync.readAt, Date.now());
|
||||
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
|
||||
// the previous read time.
|
||||
if (isMessageUnread(message.attributes)) {
|
||||
// TODO DESKTOP-1509: use MessageUpdater.markRead once this is TS
|
||||
message.markRead(readAt, { skipSave: true });
|
||||
message.set(markRead(message.attributes, readAt, { skipSave: true }));
|
||||
|
||||
const updateConversation = async () => {
|
||||
const conversation = message.getConversation();
|
||||
const conversation = window.ConversationController.get(
|
||||
message.get('conversationId')
|
||||
);
|
||||
strictAssert(conversation, `${logId}: conversation not found`);
|
||||
// onReadMessage may result in messages older than this one being
|
||||
// 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
|
||||
if (StartupQueue.isAvailable()) {
|
||||
const conversation = message.getConversation();
|
||||
const conversation = window.ConversationController.get(
|
||||
message.get('conversationId')
|
||||
);
|
||||
strictAssert(
|
||||
conversation,
|
||||
`${logId}: conversation not found (StartupQueue)`
|
||||
|
|
|
@ -7,6 +7,8 @@ import { DataReader } from '../sql/Client';
|
|||
import * as Errors from '../types/errors';
|
||||
import * as log from '../logging/log';
|
||||
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||
import { markViewOnceMessageViewed } from '../services/MessageUpdater';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
export type ViewOnceOpenSyncAttributesType = {
|
||||
removeFromMessageReceiverCache: () => unknown;
|
||||
|
@ -93,12 +95,8 @@ export async function onSync(
|
|||
return;
|
||||
}
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$register(
|
||||
found.id,
|
||||
found,
|
||||
'ViewOnceOpenSyncs.onSync'
|
||||
);
|
||||
await message.markViewOnceMessageViewed({ fromSync: true });
|
||||
const message = window.MessageCache.register(new MessageModel(found));
|
||||
await markViewOnceMessageViewed(message, { fromSync: true });
|
||||
|
||||
viewOnceSyncs.delete(sync.timestamp);
|
||||
sync.removeFromMessageReceiverCache();
|
||||
|
|
|
@ -19,6 +19,7 @@ import { queueUpdateMessage } from '../util/messageBatcher';
|
|||
import { AttachmentDownloadUrgency } from '../jobs/AttachmentDownloadManager';
|
||||
import { isAciString } from '../util/isAciString';
|
||||
import { DataReader, DataWriter } from '../sql/Client';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
export const viewSyncTaskSchema = z.object({
|
||||
type: z.literal('ViewSync').readonly(),
|
||||
|
@ -114,11 +115,7 @@ export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
|
|||
|
||||
notificationService.removeBy({ messageId: found.id });
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$register(
|
||||
found.id,
|
||||
found,
|
||||
'ViewSyncs.onSync'
|
||||
);
|
||||
const message = window.MessageCache.register(new MessageModel(found));
|
||||
let didChangeMessage = false;
|
||||
|
||||
if (message.get('readStatus') !== ReadStatus.Viewed) {
|
||||
|
|
|
@ -5,10 +5,6 @@ import { omit } from 'lodash';
|
|||
|
||||
import * as log from '../logging/log';
|
||||
import type { QuotedMessageType } from '../model-types';
|
||||
import type {
|
||||
MessageAttributesType,
|
||||
ReadonlyMessageAttributesType,
|
||||
} from '../model-types.d';
|
||||
import { SignalService } from '../protobuf';
|
||||
import { isGiftBadge, isTapToView } from '../state/selectors/message';
|
||||
import type { ProcessedQuote } from '../textsecure/Types';
|
||||
|
@ -18,16 +14,15 @@ import { getQuoteBodyText } from '../util/getQuoteBodyText';
|
|||
import { isQuoteAMatch, messageHasPaymentEvent } from './helpers';
|
||||
import * as Errors from '../types/errors';
|
||||
import { isDownloadable } from '../types/Attachment';
|
||||
import type { MessageModel } from '../models/messages';
|
||||
|
||||
export type MinimalMessageCache = Readonly<{
|
||||
findBySentAt(
|
||||
sentAt: number,
|
||||
predicate: (attributes: ReadonlyMessageAttributesType) => boolean
|
||||
): Promise<MessageAttributesType | undefined>;
|
||||
upgradeSchema(
|
||||
attributes: MessageAttributesType,
|
||||
minSchemaVersion: number
|
||||
): Promise<MessageAttributesType>;
|
||||
predicate: (attributes: MessageModel) => boolean
|
||||
): Promise<MessageModel | undefined>;
|
||||
upgradeSchema(message: MessageModel, minSchemaVersion: number): Promise<void>;
|
||||
register(message: MessageModel): MessageModel;
|
||||
}>;
|
||||
|
||||
export type CopyQuoteOptionsType = Readonly<{
|
||||
|
@ -57,8 +52,11 @@ export const copyFromQuotedMessage = async (
|
|||
isViewOnce: false,
|
||||
};
|
||||
|
||||
const queryMessage = await messageCache.findBySentAt(id, attributes =>
|
||||
isQuoteAMatch(attributes, conversationId, result)
|
||||
const queryMessage = await messageCache.findBySentAt(
|
||||
id,
|
||||
(message: MessageModel) => {
|
||||
return isQuoteAMatch(message.attributes, conversationId, result);
|
||||
}
|
||||
);
|
||||
|
||||
if (queryMessage == null) {
|
||||
|
@ -74,21 +72,19 @@ export const copyFromQuotedMessage = async (
|
|||
};
|
||||
|
||||
export const copyQuoteContentFromOriginal = async (
|
||||
providedOriginalMessage: MessageAttributesType,
|
||||
message: MessageModel,
|
||||
quote: QuotedMessageType,
|
||||
{ messageCache = window.MessageCache }: CopyQuoteOptionsType = {}
|
||||
): Promise<void> => {
|
||||
let originalMessage = providedOriginalMessage;
|
||||
|
||||
const { attachments } = quote;
|
||||
const firstAttachment = attachments ? attachments[0] : undefined;
|
||||
|
||||
if (messageHasPaymentEvent(originalMessage)) {
|
||||
if (messageHasPaymentEvent(message.attributes)) {
|
||||
// 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
|
||||
quote.text = undefined;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
|
@ -103,7 +99,7 @@ export const copyQuoteContentFromOriginal = async (
|
|||
return;
|
||||
}
|
||||
|
||||
const isMessageAGiftBadge = isGiftBadge(originalMessage);
|
||||
const isMessageAGiftBadge = isGiftBadge(message.attributes);
|
||||
if (isMessageAGiftBadge !== quote.isGiftBadge) {
|
||||
log.warn(
|
||||
`copyQuoteContentFromOriginal: Quote.isGiftBadge: ${quote.isGiftBadge}, isGiftBadge(message): ${isMessageAGiftBadge}`
|
||||
|
@ -124,18 +120,18 @@ export const copyQuoteContentFromOriginal = async (
|
|||
quote.isViewOnce = false;
|
||||
|
||||
// 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
|
||||
quote.bodyRanges = originalMessage.bodyRanges;
|
||||
quote.bodyRanges = message.attributes.bodyRanges;
|
||||
|
||||
if (!firstAttachment || !firstAttachment.contentType) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
originalMessage = await messageCache.upgradeSchema(
|
||||
originalMessage,
|
||||
await messageCache.upgradeSchema(
|
||||
message,
|
||||
window.Signal.Types.Message.VERSION_NEEDED_FOR_DISPLAY
|
||||
);
|
||||
} catch (error) {
|
||||
|
@ -150,7 +146,7 @@ export const copyQuoteContentFromOriginal = async (
|
|||
attachments: queryAttachments = [],
|
||||
preview: queryPreview = [],
|
||||
sticker,
|
||||
} = originalMessage;
|
||||
} = message.attributes;
|
||||
|
||||
if (queryAttachments.length > 0) {
|
||||
const queryFirst = queryAttachments[0];
|
||||
|
|
|
@ -2,20 +2,15 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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 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(
|
||||
messageId: string,
|
||||
location: string
|
||||
export async function getMessageById(
|
||||
messageId: string
|
||||
): Promise<MessageModel | undefined> {
|
||||
const innerLocation = `__DEPRECATED$getMessageById/${location}`;
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
messageId,
|
||||
innerLocation
|
||||
);
|
||||
const message = window.MessageCache.getById(messageId);
|
||||
if (message) {
|
||||
return message;
|
||||
}
|
||||
|
@ -34,9 +29,5 @@ export async function __DEPRECATED$getMessageById(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
return window.MessageCache.__DEPRECATED$register(
|
||||
found.id,
|
||||
found,
|
||||
innerLocation
|
||||
);
|
||||
return window.MessageCache.register(new MessageModel(found));
|
||||
}
|
||||
|
|
|
@ -3,23 +3,18 @@
|
|||
|
||||
import * as log from '../logging/log';
|
||||
import { DataReader } from '../sql/Client';
|
||||
import type { MessageModel } from '../models/messages';
|
||||
import { MessageModel } from '../models/messages';
|
||||
import type { MessageAttributesType } from '../model-types.d';
|
||||
import * as Errors from '../types/errors';
|
||||
|
||||
export async function getMessagesById(
|
||||
messageIds: Iterable<string>,
|
||||
location: string
|
||||
messageIds: Iterable<string>
|
||||
): Promise<Array<MessageModel>> {
|
||||
const innerLocation = `getMessagesById/${location}`;
|
||||
const messagesFromMemory: Array<MessageModel> = [];
|
||||
const messageIdsToLookUpInDatabase: Array<string> = [];
|
||||
|
||||
for (const messageId of messageIds) {
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
messageId,
|
||||
innerLocation
|
||||
);
|
||||
const message = window.MessageCache.getById(messageId);
|
||||
if (message) {
|
||||
messagesFromMemory.push(message);
|
||||
} else {
|
||||
|
@ -41,15 +36,8 @@ export async function getMessagesById(
|
|||
return [];
|
||||
}
|
||||
|
||||
const messagesFromDatabase = rawMessagesFromDatabase.map(rawMessage => {
|
||||
// We use `window.Whisper.Message` instead of `MessageModel` here to avoid a circular
|
||||
// import.
|
||||
const message = new window.Whisper.Message(rawMessage);
|
||||
return window.MessageCache.__DEPRECATED$register(
|
||||
message.id,
|
||||
message,
|
||||
innerLocation
|
||||
);
|
||||
const messagesFromDatabase = rawMessagesFromDatabase.map(message => {
|
||||
return window.MessageCache.register(new MessageModel(message));
|
||||
});
|
||||
|
||||
return [...messagesFromMemory, ...messagesFromDatabase];
|
||||
|
|
817
ts/messages/handleDataMessage.ts
Normal file
817
ts/messages/handleDataMessage.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -12,6 +12,7 @@ import type { MessageAttributesType } from '../model-types.d';
|
|||
import type { AciString } from '../types/ServiceId';
|
||||
import * as Errors from '../types/errors';
|
||||
import { DataReader, DataWriter } from '../sql/Client';
|
||||
import { postSaveUpdates } from '../util/cleanup';
|
||||
|
||||
const MAX_CONCURRENCY = 5;
|
||||
|
||||
|
@ -57,7 +58,7 @@ export async function _migrateMessageData({
|
|||
) => Promise<Array<MessageAttributesType>>;
|
||||
saveMessagesIndividually: (
|
||||
data: ReadonlyArray<MessageAttributesType>,
|
||||
options: { ourAci: AciString }
|
||||
options: { ourAci: AciString; postSaveUpdates: () => Promise<void> }
|
||||
) => Promise<{ failedIndices: Array<number> }>;
|
||||
incrementMessagesMigrationAttempts: (
|
||||
messageIds: ReadonlyArray<string>
|
||||
|
@ -122,6 +123,7 @@ export async function _migrateMessageData({
|
|||
upgradedMessages,
|
||||
{
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
72
ts/messages/saveAndNotify.ts
Normal file
72
ts/messages/saveAndNotify.ts
Normal 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
491
ts/messages/send.ts
Normal 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
26
ts/model-types.d.ts
vendored
|
@ -71,8 +71,8 @@ export type CustomError = Error & {
|
|||
|
||||
export type GroupMigrationType = {
|
||||
areWeInvited: boolean;
|
||||
droppedMemberIds?: Array<string>;
|
||||
invitedMembers?: Array<LegacyMigrationPendingMemberType>;
|
||||
droppedMemberIds?: ReadonlyArray<string>;
|
||||
invitedMembers?: ReadonlyArray<LegacyMigrationPendingMemberType>;
|
||||
|
||||
// We don't generate data like this; these were added to support import/export
|
||||
droppedMemberCount?: number;
|
||||
|
@ -113,7 +113,7 @@ type StoryReplyContextType = {
|
|||
|
||||
export type GroupV1Update = {
|
||||
avatarUpdated?: boolean;
|
||||
joined?: Array<string>;
|
||||
joined?: ReadonlyArray<string>;
|
||||
left?: string | 'You';
|
||||
name?: string;
|
||||
};
|
||||
|
@ -130,11 +130,11 @@ export type MessageReactionType = {
|
|||
// needs more usage of get/setPropForTimestamp. Also, these fields must match the fields
|
||||
// in MessageAttributesType.
|
||||
export type EditHistoryType = {
|
||||
attachments?: Array<AttachmentType>;
|
||||
attachments?: ReadonlyArray<AttachmentType>;
|
||||
body?: string;
|
||||
bodyAttachment?: AttachmentType;
|
||||
bodyRanges?: ReadonlyArray<RawBodyRange>;
|
||||
preview?: Array<LinkPreviewType>;
|
||||
preview?: ReadonlyArray<LinkPreviewType>;
|
||||
quote?: QuotedMessageType;
|
||||
sendStateByConversationId?: SendStateByConversationId;
|
||||
timestamp: number;
|
||||
|
@ -178,7 +178,7 @@ export type MessageAttributesType = {
|
|||
decrypted_at?: number;
|
||||
deletedForEveryone?: boolean;
|
||||
deletedForEveryoneTimestamp?: number;
|
||||
errors?: Array<CustomError>;
|
||||
errors?: ReadonlyArray<CustomError>;
|
||||
expirationStartTimestamp?: number | null;
|
||||
expireTimer?: DurationInSeconds;
|
||||
groupMigration?: GroupMigrationType;
|
||||
|
@ -190,7 +190,7 @@ export type MessageAttributesType = {
|
|||
isErased?: boolean;
|
||||
isTapToViewInvalid?: boolean;
|
||||
isViewOnce?: boolean;
|
||||
editHistory?: Array<EditHistoryType>;
|
||||
editHistory?: ReadonlyArray<EditHistoryType>;
|
||||
editMessageTimestamp?: number;
|
||||
editMessageReceivedAt?: number;
|
||||
editMessageReceivedAtMs?: number;
|
||||
|
@ -220,12 +220,12 @@ export type MessageAttributesType = {
|
|||
id: string;
|
||||
type: MessageType;
|
||||
body?: string;
|
||||
attachments?: Array<AttachmentType>;
|
||||
preview?: Array<LinkPreviewType>;
|
||||
attachments?: ReadonlyArray<AttachmentType>;
|
||||
preview?: ReadonlyArray<LinkPreviewType>;
|
||||
sticker?: StickerType;
|
||||
sent_at: number;
|
||||
unidentifiedDeliveries?: Array<string>;
|
||||
contact?: Array<EmbeddedContactType>;
|
||||
unidentifiedDeliveries?: ReadonlyArray<string>;
|
||||
contact?: ReadonlyArray<EmbeddedContactType>;
|
||||
conversationId: string;
|
||||
storyReaction?: {
|
||||
emoji: string;
|
||||
|
@ -286,8 +286,8 @@ export type MessageAttributesType = {
|
|||
timestamp: number;
|
||||
|
||||
// Backwards-compatibility with prerelease data schema
|
||||
invitedGV2Members?: Array<LegacyMigrationPendingMemberType>;
|
||||
droppedGV2MemberIds?: Array<string>;
|
||||
invitedGV2Members?: ReadonlyArray<LegacyMigrationPendingMemberType>;
|
||||
droppedGV2MemberIds?: ReadonlyArray<string>;
|
||||
|
||||
sendHQImages?: boolean;
|
||||
|
||||
|
|
|
@ -189,6 +189,9 @@ import { getCallHistorySelector } from '../state/selectors/callHistory';
|
|||
import { migrateLegacyReadStatus } from '../messages/migrateLegacyReadStatus';
|
||||
import { migrateLegacySendAttributes } from '../messages/migrateLegacySendAttributes';
|
||||
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 */
|
||||
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:name change:profileName change:profileFamilyName change:e164 ' +
|
||||
|
@ -464,8 +466,6 @@ export class ConversationModel extends window.Backbone
|
|||
SECOND
|
||||
);
|
||||
|
||||
this.on('newmessage', this.throttledUpdateVerified);
|
||||
|
||||
const migratedColor = this.getColor();
|
||||
if (this.get('color') !== migratedColor) {
|
||||
this.set('color', migratedColor);
|
||||
|
@ -1442,8 +1442,14 @@ export class ConversationModel extends window.Backbone
|
|||
});
|
||||
}
|
||||
|
||||
async onNewMessage(message: MessageAttributesType): Promise<void> {
|
||||
const { sourceServiceId: serviceId, source: e164, sourceDevice } = message;
|
||||
async onNewMessage(message: MessageModel): Promise<void> {
|
||||
const {
|
||||
sourceServiceId: serviceId,
|
||||
source: e164,
|
||||
sourceDevice,
|
||||
storyId,
|
||||
} = message.attributes;
|
||||
this.throttledUpdateVerified?.();
|
||||
|
||||
const source = window.ConversationController.lookupOrCreate({
|
||||
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
|
||||
// the last message or add new messages to redux.
|
||||
const isGroupStoryReply = isGroup(this.attributes) && message.storyId;
|
||||
if (isGroupStoryReply || isStory(message)) {
|
||||
const isGroupStoryReply = isGroup(this.attributes) && storyId;
|
||||
if (isGroupStoryReply || isStory(message.attributes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Change to message request state if contact was removed and sent message.
|
||||
if (
|
||||
this.get('removalStage') === 'justNotification' &&
|
||||
isIncoming(message)
|
||||
isIncoming(message.attributes)
|
||||
) {
|
||||
this.set({
|
||||
removalStage: 'messageRequest',
|
||||
|
@ -1476,7 +1482,7 @@ export class ConversationModel extends window.Backbone
|
|||
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
|
||||
|
@ -1966,65 +1972,64 @@ export class ConversationModel extends window.Backbone
|
|||
|
||||
const hydrated = await Promise.all(
|
||||
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) {
|
||||
migratedMessage = {
|
||||
...migratedMessage,
|
||||
updated = true;
|
||||
model.set({
|
||||
readStatus,
|
||||
seenStatus:
|
||||
readStatus === ReadStatus.Unread
|
||||
? SeenStatus.Unseen
|
||||
: SeenStatus.Seen,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (ourConversationId) {
|
||||
const sendStateByConversationId = migrateLegacySendAttributes(
|
||||
migratedMessage,
|
||||
model.attributes,
|
||||
window.ConversationController.get.bind(
|
||||
window.ConversationController
|
||||
),
|
||||
ourConversationId
|
||||
);
|
||||
if (sendStateByConversationId) {
|
||||
migratedMessage = {
|
||||
...migratedMessage,
|
||||
updated = true;
|
||||
model.set({
|
||||
sendStateByConversationId,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const upgradedMessage = await window.MessageCache.upgradeSchema(
|
||||
migratedMessage,
|
||||
const startingAttributes = model.attributes;
|
||||
await window.MessageCache.upgradeSchema(
|
||||
model,
|
||||
Message.VERSION_NEEDED_FOR_DISPLAY
|
||||
);
|
||||
if (startingAttributes !== model.attributes) {
|
||||
updated = true;
|
||||
}
|
||||
|
||||
const patch = await hydrateStoryContext(message.id, undefined, {
|
||||
shouldSave: true,
|
||||
});
|
||||
|
||||
const didMigrate = migratedMessage !== message;
|
||||
const didUpgrade = upgradedMessage !== migratedMessage;
|
||||
const didPatch = Boolean(patch);
|
||||
|
||||
if (didMigrate || didUpgrade || didPatch) {
|
||||
upgraded += 1;
|
||||
if (patch) {
|
||||
updated = true;
|
||||
model.set(patch);
|
||||
}
|
||||
if (didMigrate && !didUpgrade && !didPatch) {
|
||||
await window.MessageCache.setAttributes({
|
||||
messageId: message.id,
|
||||
messageAttributes: migratedMessage,
|
||||
skipSaveToDatabase: false,
|
||||
|
||||
if (updated) {
|
||||
upgraded += 1;
|
||||
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||
await DataWriter.saveMessage(model.attributes, {
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
}
|
||||
|
||||
if (patch) {
|
||||
return { ...upgradedMessage, ...patch };
|
||||
}
|
||||
|
||||
return upgradedMessage;
|
||||
return model.attributes;
|
||||
})
|
||||
);
|
||||
if (upgraded > 0) {
|
||||
|
@ -2322,15 +2327,13 @@ export class ConversationModel extends window.Backbone
|
|||
// eslint-disable-next-line no-await-in-loop
|
||||
await Promise.all(
|
||||
readMessages.map(async m => {
|
||||
const registered = window.MessageCache.__DEPRECATED$register(
|
||||
m.id,
|
||||
m,
|
||||
'handleReadAndDownloadAttachments'
|
||||
);
|
||||
const shouldSave = await registered.queueAttachmentDownloads();
|
||||
const registered = window.MessageCache.register(new MessageModel(m));
|
||||
const shouldSave =
|
||||
await queueAttachmentDownloadsForMessage(registered);
|
||||
if (shouldSave) {
|
||||
await DataWriter.saveMessage(registered.attributes, {
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
}
|
||||
})
|
||||
|
@ -2354,7 +2357,7 @@ export class ConversationModel extends window.Backbone
|
|||
? timestamp
|
||||
: lastMessageTimestamp;
|
||||
|
||||
const message: MessageAttributesType = {
|
||||
const message = new MessageModel({
|
||||
...generateMessageId(incrementMessageCounter()),
|
||||
conversationId: this.id,
|
||||
type: 'message-request-response-event',
|
||||
|
@ -2364,18 +2367,17 @@ export class ConversationModel extends window.Backbone
|
|||
seenStatus: SeenStatus.NotApplicable,
|
||||
timestamp,
|
||||
messageRequestResponseEvent: event,
|
||||
};
|
||||
});
|
||||
|
||||
await DataWriter.saveMessage(message, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
await window.MessageCache.saveMessage(message, {
|
||||
forceSave: true,
|
||||
});
|
||||
if (!getIsInitialSync() && !this.get('active_at')) {
|
||||
this.set({ active_at: Date.now() });
|
||||
await DataWriter.updateConversation(this.attributes);
|
||||
}
|
||||
window.MessageCache.toMessageAttributes(message);
|
||||
this.trigger('newmessage', message);
|
||||
window.MessageCache.register(message);
|
||||
drop(this.onNewMessage(message));
|
||||
drop(this.updateLastMessage());
|
||||
}
|
||||
|
||||
|
@ -3120,7 +3122,7 @@ export class ConversationModel extends window.Backbone
|
|||
receivedAt,
|
||||
});
|
||||
|
||||
const message: MessageAttributesType = {
|
||||
const message = new MessageModel({
|
||||
...generateMessageId(receivedAtCounter),
|
||||
conversationId: this.id,
|
||||
type: 'chat-session-refreshed',
|
||||
|
@ -3129,19 +3131,15 @@ export class ConversationModel extends window.Backbone
|
|||
received_at_ms: receivedAt,
|
||||
readStatus: ReadStatus.Unread,
|
||||
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);
|
||||
void this.updateUnread();
|
||||
await window.MessageCache.saveMessage(message, {
|
||||
forceSave: true,
|
||||
});
|
||||
window.MessageCache.register(message);
|
||||
|
||||
drop(this.onNewMessage(message));
|
||||
drop(this.updateUnread());
|
||||
}
|
||||
|
||||
async addDeliveryIssue({
|
||||
|
@ -3167,7 +3165,7 @@ export class ConversationModel extends window.Backbone
|
|||
return;
|
||||
}
|
||||
|
||||
const message: MessageAttributesType = {
|
||||
const message = new MessageModel({
|
||||
...generateMessageId(receivedAtCounter),
|
||||
conversationId: this.id,
|
||||
type: 'delivery-issue',
|
||||
|
@ -3177,21 +3175,17 @@ export class ConversationModel extends window.Backbone
|
|||
timestamp: receivedAt,
|
||||
readStatus: ReadStatus.Unread,
|
||||
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);
|
||||
void this.updateUnread();
|
||||
drop(this.onNewMessage(message));
|
||||
drop(this.updateUnread());
|
||||
|
||||
await this.notify(message.attributes);
|
||||
}
|
||||
|
||||
async addKeyChange(
|
||||
|
@ -3216,7 +3210,7 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
|
||||
const timestamp = Date.now();
|
||||
const message: MessageAttributesType = {
|
||||
const message = new MessageModel({
|
||||
...generateMessageId(incrementMessageCounter()),
|
||||
conversationId: this.id,
|
||||
type: 'keychange',
|
||||
|
@ -3227,19 +3221,14 @@ export class ConversationModel extends window.Backbone
|
|||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Unseen,
|
||||
schemaVersion: Message.VERSION_NEEDED_FOR_DISPLAY,
|
||||
};
|
||||
});
|
||||
|
||||
await DataWriter.saveMessage(message, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
await window.MessageCache.saveMessage(message, {
|
||||
forceSave: true,
|
||||
});
|
||||
window.MessageCache.__DEPRECATED$register(
|
||||
message.id,
|
||||
message,
|
||||
'addKeyChange'
|
||||
);
|
||||
window.MessageCache.register(message);
|
||||
|
||||
this.trigger('newmessage', message);
|
||||
drop(this.onNewMessage(message));
|
||||
|
||||
const serviceId = this.getServiceId();
|
||||
|
||||
|
@ -3277,7 +3266,7 @@ export class ConversationModel extends window.Backbone
|
|||
);
|
||||
|
||||
const timestamp = Date.now();
|
||||
const message: MessageAttributesType = {
|
||||
const message = new MessageModel({
|
||||
...generateMessageId(incrementMessageCounter()),
|
||||
conversationId: this.id,
|
||||
type: 'conversation-merge',
|
||||
|
@ -3290,19 +3279,12 @@ export class ConversationModel extends window.Backbone
|
|||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Unseen,
|
||||
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> {
|
||||
|
@ -3325,7 +3307,7 @@ export class ConversationModel extends window.Backbone
|
|||
|
||||
log.info(`${logId}: adding notification`);
|
||||
const timestamp = Date.now();
|
||||
const message: MessageAttributesType = {
|
||||
const message = new MessageModel({
|
||||
...generateMessageId(incrementMessageCounter()),
|
||||
conversationId: this.id,
|
||||
type: 'phone-number-discovery',
|
||||
|
@ -3338,19 +3320,12 @@ export class ConversationModel extends window.Backbone
|
|||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Unseen,
|
||||
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(
|
||||
|
@ -3373,7 +3348,7 @@ export class ConversationModel extends window.Backbone
|
|||
);
|
||||
|
||||
const timestamp = Date.now();
|
||||
const message: MessageAttributesType = {
|
||||
const message = new MessageModel({
|
||||
...generateMessageId(incrementMessageCounter()),
|
||||
conversationId: this.id,
|
||||
local: Boolean(options.local),
|
||||
|
@ -3385,19 +3360,12 @@ export class ConversationModel extends window.Backbone
|
|||
type: 'verified-change',
|
||||
verified,
|
||||
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());
|
||||
|
||||
const serviceId = this.getServiceId();
|
||||
|
@ -3417,7 +3385,7 @@ export class ConversationModel extends window.Backbone
|
|||
conversationId?: string
|
||||
): Promise<void> {
|
||||
const now = Date.now();
|
||||
const message: MessageAttributesType = {
|
||||
const message = new MessageModel({
|
||||
...generateMessageId(incrementMessageCounter()),
|
||||
conversationId: this.id,
|
||||
type: 'profile-change',
|
||||
|
@ -3428,18 +3396,12 @@ export class ConversationModel extends window.Backbone
|
|||
timestamp: now,
|
||||
changedId: conversationId || this.id,
|
||||
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();
|
||||
if (isDirectConversation(this.attributes) && serviceId) {
|
||||
|
@ -3460,7 +3422,7 @@ export class ConversationModel extends window.Backbone
|
|||
extra: Partial<MessageAttributesType> = {}
|
||||
): Promise<string> {
|
||||
const now = Date.now();
|
||||
const message: MessageAttributesType = {
|
||||
const message = new MessageModel({
|
||||
...generateMessageId(incrementMessageCounter()),
|
||||
conversationId: this.id,
|
||||
type,
|
||||
|
@ -3472,18 +3434,12 @@ export class ConversationModel extends window.Backbone
|
|||
seenStatus: SeenStatus.NotApplicable,
|
||||
|
||||
...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;
|
||||
}
|
||||
|
@ -3561,13 +3517,10 @@ export class ConversationModel extends window.Backbone
|
|||
`maybeRemoveUniversalTimer(${this.idForLogging()}): removed notification`
|
||||
);
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
notificationId,
|
||||
'maybeRemoveUniversalTimer'
|
||||
);
|
||||
const message = window.MessageCache.getById(notificationId);
|
||||
if (message) {
|
||||
await DataWriter.removeMessage(message.id, {
|
||||
singleProtoJobQueue,
|
||||
cleanupMessages,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
|
@ -3607,13 +3560,10 @@ export class ConversationModel extends window.Backbone
|
|||
`maybeClearContactRemoved(${this.idForLogging()}): removed notification`
|
||||
);
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
notificationId,
|
||||
'maybeClearContactRemoved'
|
||||
);
|
||||
const message = window.MessageCache.getById(notificationId);
|
||||
if (message) {
|
||||
await DataWriter.removeMessage(message.id, {
|
||||
singleProtoJobQueue,
|
||||
cleanupMessages,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -4164,16 +4114,12 @@ export class ConversationModel extends window.Backbone
|
|||
storyId,
|
||||
});
|
||||
|
||||
window.MessageCache.__DEPRECATED$register(
|
||||
attributes.id,
|
||||
attributes,
|
||||
'enqueueMessageForSend'
|
||||
);
|
||||
const model = window.MessageCache.register(new MessageModel(attributes));
|
||||
|
||||
const dbStart = Date.now();
|
||||
|
||||
strictAssert(
|
||||
typeof attributes.timestamp === 'number',
|
||||
typeof model.get('timestamp') === 'number',
|
||||
'Expected a timestamp'
|
||||
);
|
||||
|
||||
|
@ -4186,17 +4132,16 @@ export class ConversationModel extends window.Backbone
|
|||
{
|
||||
type: conversationQueueJobEnum.enum.NormalMessage,
|
||||
conversationId: this.id,
|
||||
messageId: attributes.id,
|
||||
messageId: model.id,
|
||||
revision: this.get('revision'),
|
||||
},
|
||||
async jobToInsert => {
|
||||
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,
|
||||
forceSave: true,
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -4212,14 +4157,14 @@ export class ConversationModel extends window.Backbone
|
|||
const renderStart = Date.now();
|
||||
|
||||
// Perform asynchronous tasks before entering the batching mode
|
||||
await this.beforeAddSingleMessage(attributes);
|
||||
await this.beforeAddSingleMessage(model.attributes);
|
||||
|
||||
if (sticker) {
|
||||
await addStickerPackReference(attributes.id, sticker.packId);
|
||||
await addStickerPackReference(model.id, sticker.packId);
|
||||
}
|
||||
|
||||
this.beforeMessageSend({
|
||||
message: attributes,
|
||||
message: model.attributes,
|
||||
dontClearDraft,
|
||||
dontAddMessage: false,
|
||||
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
|
||||
// in memory we use that data instead of the data from the db which may
|
||||
// be out of date.
|
||||
if (preview) {
|
||||
const inMemory = window.MessageCache.accessAttributes(preview.id);
|
||||
preview = inMemory || preview;
|
||||
preview = (await this.cleanAttributes([preview]))?.[0] || preview;
|
||||
if (previewAttributes) {
|
||||
preview = window.MessageCache.register(
|
||||
new MessageModel(previewAttributes)
|
||||
);
|
||||
const updates = (await this.cleanAttributes([preview.attributes]))?.[0];
|
||||
preview.set(updates);
|
||||
}
|
||||
|
||||
if (activity) {
|
||||
const inMemory = window.MessageCache.accessAttributes(activity.id);
|
||||
activity = inMemory || activity;
|
||||
activity = (await this.cleanAttributes([activity]))?.[0] || activity;
|
||||
if (activityAttributes) {
|
||||
activity = window.MessageCache.register(
|
||||
new MessageModel(activityAttributes)
|
||||
);
|
||||
const updates = (await this.cleanAttributes([activity.attributes]))?.[0];
|
||||
activity.set(updates);
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -4386,7 +4337,7 @@ export class ConversationModel extends window.Backbone
|
|||
this.get('draftTimestamp') &&
|
||||
(!preview ||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
preview.sent_at < this.get('draftTimestamp')!)
|
||||
preview.get('sent_at') < this.get('draftTimestamp')!)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
@ -4395,36 +4346,43 @@ export class ConversationModel extends window.Backbone
|
|||
let lastMessageReceivedAt = this.get('lastMessageReceivedAt');
|
||||
let lastMessageReceivedAtMs = this.get('lastMessageReceivedAtMs');
|
||||
if (activity) {
|
||||
const { callId } = activity;
|
||||
const { callId } = activity.attributes;
|
||||
const callHistory = callId
|
||||
? getCallHistorySelector(window.reduxStore.getState())(callId)
|
||||
: undefined;
|
||||
|
||||
timestamp = callHistory?.timestamp || activity.sent_at || timestamp;
|
||||
lastMessageReceivedAt = activity.received_at || lastMessageReceivedAt;
|
||||
timestamp =
|
||||
callHistory?.timestamp || activity.get('sent_at') || timestamp;
|
||||
lastMessageReceivedAt =
|
||||
activity.get('received_at') || lastMessageReceivedAt;
|
||||
lastMessageReceivedAtMs =
|
||||
activity.received_at_ms || lastMessageReceivedAtMs;
|
||||
activity.get('received_at_ms') || lastMessageReceivedAtMs;
|
||||
}
|
||||
|
||||
const notificationData = preview
|
||||
? getNotificationDataForMessage(preview)
|
||||
? getNotificationDataForMessage(preview.attributes)
|
||||
: undefined;
|
||||
|
||||
this.set({
|
||||
lastMessage:
|
||||
notificationData?.text ||
|
||||
(preview ? getNotificationTextForMessage(preview) : undefined) ||
|
||||
(preview
|
||||
? getNotificationTextForMessage(preview.attributes)
|
||||
: undefined) ||
|
||||
'',
|
||||
lastMessageBodyRanges: notificationData?.bodyRanges,
|
||||
lastMessagePrefix: notificationData?.emoji,
|
||||
lastMessageAuthor: getMessageAuthorText(preview),
|
||||
lastMessageStatus:
|
||||
(preview ? getMessagePropStatus(preview, ourConversationId) : null) ||
|
||||
null,
|
||||
lastMessageAuthor: preview
|
||||
? getMessageAuthorText(preview.attributes)
|
||||
: undefined,
|
||||
lastMessageStatus: preview
|
||||
? getMessagePropStatus(preview.attributes, ourConversationId)
|
||||
: undefined,
|
||||
lastMessageReceivedAt,
|
||||
lastMessageReceivedAtMs,
|
||||
timestamp,
|
||||
lastMessageDeletedForEveryone: preview?.deletedForEveryone || false,
|
||||
lastMessageDeletedForEveryone:
|
||||
preview?.get('deletedForEveryone') || false,
|
||||
});
|
||||
|
||||
await DataWriter.updateConversation(this.attributes);
|
||||
|
@ -4785,7 +4743,7 @@ export class ConversationModel extends window.Backbone
|
|||
(isInitialSync && isFromSyncOperation) || isFromMe || isNoteToSelf;
|
||||
|
||||
const counter = receivedAt ?? incrementMessageCounter();
|
||||
const attributes = {
|
||||
const message = new MessageModel({
|
||||
...generateMessageId(counter),
|
||||
conversationId: this.id,
|
||||
expirationTimerUpdate: {
|
||||
|
@ -4801,24 +4759,18 @@ export class ConversationModel extends window.Backbone
|
|||
sent_at: sentAt,
|
||||
timestamp: sentAt,
|
||||
type: 'timer-notification' as const,
|
||||
};
|
||||
|
||||
await DataWriter.saveMessage(attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
forceSave: true,
|
||||
});
|
||||
|
||||
window.MessageCache.__DEPRECATED$register(
|
||||
attributes.id,
|
||||
attributes,
|
||||
'updateExpirationTimer'
|
||||
);
|
||||
await window.MessageCache.saveMessage(message, {
|
||||
forceSave: true,
|
||||
});
|
||||
window.MessageCache.register(message);
|
||||
|
||||
void this.addSingleMessage(attributes);
|
||||
void this.addSingleMessage(message.attributes);
|
||||
void this.updateUnread();
|
||||
|
||||
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`);
|
||||
await DataWriter.removeMessagesInConversation(this.id, {
|
||||
cleanupMessages,
|
||||
fromSync: source !== 'local-delete-sync',
|
||||
logId: this.idForLogging(),
|
||||
singleProtoJobQueue,
|
||||
});
|
||||
log.info(`${logId}: Delete complete`);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,10 +5,13 @@ import noop from 'lodash/noop';
|
|||
import { v7 as generateUuid } from 'uuid';
|
||||
|
||||
import { DataWriter } from '../sql/Client';
|
||||
import type { MessageModel } from '../models/messages';
|
||||
import type { ReactionAttributesType } from '../messageModifiers/Reactions';
|
||||
import { MessageModel } from '../models/messages';
|
||||
import {
|
||||
handleReaction,
|
||||
type ReactionAttributesType,
|
||||
} from '../messageModifiers/Reactions';
|
||||
import { ReactionSource } from './ReactionSource';
|
||||
import { __DEPRECATED$getMessageById } from '../messages/getMessageById';
|
||||
import { getMessageById } from '../messages/getMessageById';
|
||||
import { getSourceServiceId, isStory } from '../messages/helpers';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import { isDirectConversation } from '../util/whatTypeOfConversation';
|
||||
|
@ -19,6 +22,7 @@ import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
|
|||
import { isAciString } from '../util/isAciString';
|
||||
import { SendStatus } from '../messages/MessageSendState';
|
||||
import * as log from '../logging/log';
|
||||
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||
|
||||
export async function enqueueReactionForSend({
|
||||
emoji,
|
||||
|
@ -29,20 +33,17 @@ export async function enqueueReactionForSend({
|
|||
messageId: string;
|
||||
remove: boolean;
|
||||
}>): Promise<void> {
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'enqueueReactionForSend'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
strictAssert(message, 'enqueueReactionForSend: no message found');
|
||||
|
||||
const targetAuthorAci = getSourceServiceId(message.attributes);
|
||||
strictAssert(
|
||||
targetAuthorAci,
|
||||
`enqueueReactionForSend: message ${message.idForLogging()} had no source UUID`
|
||||
`enqueueReactionForSend: message ${getMessageIdForLogging(message.attributes)} had no source UUID`
|
||||
);
|
||||
strictAssert(
|
||||
isAciString(targetAuthorAci),
|
||||
`enqueueReactionForSend: message ${message.idForLogging()} had no source ACI`
|
||||
`enqueueReactionForSend: message ${getMessageIdForLogging(message.attributes)} had no source ACI`
|
||||
);
|
||||
|
||||
const targetTimestamp = getMessageSentTimestamp(message.attributes, {
|
||||
|
@ -50,11 +51,13 @@ export async function enqueueReactionForSend({
|
|||
});
|
||||
strictAssert(
|
||||
targetTimestamp,
|
||||
`enqueueReactionForSend: message ${message.idForLogging()} had no timestamp`
|
||||
`enqueueReactionForSend: message ${getMessageIdForLogging(message.attributes)} had no timestamp`
|
||||
);
|
||||
|
||||
const timestamp = Date.now();
|
||||
const messageConversation = message.getConversation();
|
||||
const messageConversation = window.ConversationController.get(
|
||||
message.get('conversationId')
|
||||
);
|
||||
strictAssert(
|
||||
messageConversation,
|
||||
'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
|
||||
let storyReactionMessage: MessageModel | undefined;
|
||||
if (storyMessage) {
|
||||
storyReactionMessage = new window.Whisper.Message({
|
||||
storyReactionMessage = new MessageModel({
|
||||
...generateMessageId(incrementMessageCounter()),
|
||||
type: 'outgoing',
|
||||
conversationId: targetConversation.id,
|
||||
|
@ -132,5 +135,5 @@ export async function enqueueReactionForSend({
|
|||
timestamp,
|
||||
};
|
||||
|
||||
await message.handleReaction(reaction, { storyMessage });
|
||||
await handleReaction(message, reaction, { storyMessage });
|
||||
}
|
||||
|
|
|
@ -1,63 +1,124 @@
|
|||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { throttle } from 'lodash';
|
||||
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 { getEnvironment, Environment } from '../environment';
|
||||
import { MessageModel } from '../models/messages';
|
||||
import { DataReader, DataWriter } from '../sql/Client';
|
||||
import { getMessageConversation } from '../util/getMessageConversation';
|
||||
import { getMessageModelLogger } from '../util/MessageModelLogger';
|
||||
import { getSenderIdentifier } from '../util/getSenderIdentifier';
|
||||
import { isNotNil } from '../util/isNotNil';
|
||||
import { softAssert, strictAssert } from '../util/assert';
|
||||
import { isStory } from '../messages/helpers';
|
||||
import type { SendStateByConversationId } from '../messages/MessageSendState';
|
||||
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;
|
||||
export class MessageCache {
|
||||
static install(): MessageCache {
|
||||
const instance = new MessageCache();
|
||||
window.MessageCache = instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
private state = {
|
||||
messages: new Map<string, MessageAttributesType>(),
|
||||
messages: new Map<string, MessageModel>(),
|
||||
messageIdsBySender: new Map<string, string>(),
|
||||
messageIdsBySentAt: new Map<number, Array<string>>(),
|
||||
lastAccessedAt: new Map<string, number>(),
|
||||
};
|
||||
|
||||
// Stores the models so that __DEPRECATED$register always returns the existing
|
||||
// copy instead of a new model.
|
||||
private modelCache = new Map<string, MessageModel>();
|
||||
public saveMessage(
|
||||
message: MessageAttributesType | 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 undefined if the message does not exist in memory.
|
||||
public accessAttributes(
|
||||
messageId: string
|
||||
): Readonly<MessageAttributesType> | undefined {
|
||||
const messageAttributes = this.state.messages.get(messageId);
|
||||
return messageAttributes
|
||||
? this.freezeAttributes(messageAttributes)
|
||||
: undefined;
|
||||
return DataWriter.saveMessage(attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
// Synchronously access a message's attributes from internal cache. Throws
|
||||
// if the message does not exist in memory.
|
||||
public accessAttributesOrThrow(
|
||||
source: string,
|
||||
messageId: string
|
||||
): Readonly<MessageAttributesType> {
|
||||
const messageAttributes = this.accessAttributes(messageId);
|
||||
strictAssert(
|
||||
messageAttributes,
|
||||
`MessageCache.accessAttributesOrThrow/${source}: no message for id ${messageId}`
|
||||
);
|
||||
return messageAttributes;
|
||||
public register(message: MessageModel): MessageModel {
|
||||
if (!message || !message.id) {
|
||||
throw new Error('MessageCache.register: Got falsey id or message');
|
||||
}
|
||||
|
||||
const existing = this.getById(message.id);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
this.addMessageToCache(message);
|
||||
|
||||
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
|
||||
|
@ -65,9 +126,9 @@ export class MessageCache {
|
|||
public deleteExpiredMessages(expiryTime: number): void {
|
||||
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 conversation = getMessageConversation(messageAttributes);
|
||||
const conversation = getMessageConversation(message.attributes);
|
||||
|
||||
const state = window.reduxStore.getState();
|
||||
const selectedId = state?.conversations?.selectedConversationId;
|
||||
|
@ -75,21 +136,25 @@ export class MessageCache {
|
|||
conversation && selectedId && conversation.id === selectedId;
|
||||
|
||||
if (now - timeLastAccessed > expiryTime && !inActiveConversation) {
|
||||
this.__DEPRECATED$unregister(messageId);
|
||||
this.unregister(messageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finds a message in the cache by sender identifier
|
||||
public findBySender(
|
||||
senderIdentifier: string
|
||||
): Readonly<MessageAttributesType> | undefined {
|
||||
const id = this.state.messageIdsBySender.get(senderIdentifier);
|
||||
if (!id) {
|
||||
return undefined;
|
||||
public async upgradeSchema(
|
||||
message: MessageModel,
|
||||
minSchemaVersion: number
|
||||
): Promise<void> {
|
||||
const { schemaVersion } = message.attributes;
|
||||
if (!schemaVersion || schemaVersion >= minSchemaVersion) {
|
||||
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({
|
||||
|
@ -112,12 +177,12 @@ export class MessageCache {
|
|||
};
|
||||
};
|
||||
|
||||
for (const [messageId, messageAttributes] of this.state.messages) {
|
||||
if (messageAttributes.conversationId !== obsoleteId) {
|
||||
for (const [, message] of this.state.messages) {
|
||||
if (message.get('conversationId') !== obsoleteId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const editHistory = messageAttributes.editHistory?.map(history => {
|
||||
const editHistory = message.get('editHistory')?.map(history => {
|
||||
return {
|
||||
...history,
|
||||
sendStateByConversationId: updateSendState(
|
||||
|
@ -126,117 +191,33 @@ export class MessageCache {
|
|||
};
|
||||
});
|
||||
|
||||
this.setAttributes({
|
||||
messageId,
|
||||
messageAttributes: {
|
||||
message.set({
|
||||
conversationId,
|
||||
sendStateByConversationId: updateSendState(
|
||||
messageAttributes.sendStateByConversationId
|
||||
message.get('sendStateByConversationId')
|
||||
),
|
||||
editHistory,
|
||||
},
|
||||
skipSaveToDatabase: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Find the message's attributes whether in memory or in the database.
|
||||
// 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);
|
||||
// Semi-public API
|
||||
|
||||
if (inMemoryMessageAttributes) {
|
||||
return inMemoryMessageAttributes;
|
||||
}
|
||||
// Should only be called by MessageModel's set() function
|
||||
public _updateCaches(message: MessageModel): undefined {
|
||||
const existing = this.getById(message.id);
|
||||
|
||||
let messageAttributesFromDatabase: MessageAttributesType | undefined;
|
||||
try {
|
||||
messageAttributesFromDatabase =
|
||||
await DataReader.getMessageById(messageId);
|
||||
} 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;
|
||||
// If this model hasn't been registered yet, we can't add to cache because we don't
|
||||
// want to force `message` to be the primary MessageModel for this message.
|
||||
if (!existing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.messageIdsBySender.delete(
|
||||
getSenderIdentifier(messageAttributes)
|
||||
getSenderIdentifier(message.attributes)
|
||||
);
|
||||
|
||||
const nextMessageAttributes = {
|
||||
...messageAttributes,
|
||||
...partialMessageAttributes,
|
||||
};
|
||||
|
||||
const { id, sent_at: sentAt } = nextMessageAttributes;
|
||||
const { id, sent_at: sentAt } = message.attributes;
|
||||
const previousIdsBySentAt = this.state.messageIdsBySentAt.get(sentAt);
|
||||
|
||||
let nextIdsBySentAtSet: Set<string>;
|
||||
|
@ -247,44 +228,70 @@ export class MessageCache {
|
|||
nextIdsBySentAtSet = new Set([id]);
|
||||
}
|
||||
|
||||
this.state.messages.set(id, nextMessageAttributes);
|
||||
this.state.lastAccessedAt.set(id, Date.now());
|
||||
this.state.messageIdsBySender.set(
|
||||
getSenderIdentifier(messageAttributes),
|
||||
getSenderIdentifier(message.attributes),
|
||||
id
|
||||
);
|
||||
|
||||
this.markModelStale(nextMessageAttributes);
|
||||
this.throttledUpdateRedux(message.attributes);
|
||||
}
|
||||
|
||||
this.throttledUpdateRedux(nextMessageAttributes);
|
||||
// Helpers
|
||||
|
||||
if (skipSaveToDatabase) {
|
||||
private addMessageToCache(message: MessageModel): void {
|
||||
if (!message.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
return DataWriter.saveMessage(nextMessageAttributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
});
|
||||
if (this.state.messages.has(message.id)) {
|
||||
this.state.lastAccessedAt.set(message.id, Date.now());
|
||||
return;
|
||||
}
|
||||
|
||||
private throttledReduxUpdaters = new LRUCache<
|
||||
string,
|
||||
typeof this.updateRedux
|
||||
>({
|
||||
max: MAX_THROTTLED_REDUX_UPDATERS,
|
||||
});
|
||||
const { id, sent_at: sentAt } = message.attributes;
|
||||
const previousIdsBySentAt = this.state.messageIdsBySentAt.get(sentAt);
|
||||
|
||||
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);
|
||||
let nextIdsBySentAtSet: Set<string>;
|
||||
if (previousIdsBySentAt) {
|
||||
nextIdsBySentAtSet = new Set(previousIdsBySentAt);
|
||||
nextIdsBySentAtSet.add(id);
|
||||
} else {
|
||||
nextIdsBySentAtSet = new Set([id]);
|
||||
}
|
||||
|
||||
updater(attributes);
|
||||
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) {
|
||||
|
@ -313,238 +320,23 @@ export class MessageCache {
|
|||
);
|
||||
}
|
||||
|
||||
// When you already have the message attributes from the db and want to
|
||||
// ensure that they're added to the cache. The latest attributes from cache
|
||||
// are returned if they exist, if not the attributes passed in are returned.
|
||||
public toMessageAttributes(
|
||||
messageAttributes: MessageAttributesType
|
||||
): Readonly<MessageAttributesType> {
|
||||
this.addMessageToCache(messageAttributes);
|
||||
|
||||
const nextMessageAttributes = this.state.messages.get(messageAttributes.id);
|
||||
strictAssert(
|
||||
nextMessageAttributes,
|
||||
`MessageCache.toMessageAttributes: no message for id ${messageAttributes.id}`
|
||||
);
|
||||
|
||||
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,
|
||||
private throttledReduxUpdaters = new LRUCache<
|
||||
string,
|
||||
typeof this.updateRedux
|
||||
>({
|
||||
max: MAX_THROTTLED_REDUX_UPDATERS,
|
||||
});
|
||||
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(', '),
|
||||
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);
|
||||
}
|
||||
|
||||
// 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 =
|
||||
'attributes' in messageAttributes
|
||||
? messageAttributes
|
||||
: new window.Whisper.Message(messageAttributes);
|
||||
|
||||
const proxy = getMessageModelLogger(model);
|
||||
|
||||
this.modelCache.set(messageAttributes.id, proxy);
|
||||
|
||||
return proxy;
|
||||
updater(attributes);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,19 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { MessageAttributesType } from '../model-types.d';
|
||||
import type { MessageModel } from '../models/messages';
|
||||
import { ReadStatus, maxReadStatus } from '../messages/MessageReadStatus';
|
||||
import { notificationService } from './notifications';
|
||||
import { SeenStatus } from '../MessageSeenStatus';
|
||||
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(
|
||||
messageAttrs: Readonly<MessageAttributesType>,
|
||||
|
@ -54,3 +63,65 @@ export const markViewed = (
|
|||
{ skipSave = false } = {}
|
||||
): MessageAttributesType =>
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,6 +128,7 @@ import { isAdhoc, isNightly } from '../../util/version';
|
|||
import { ToastType } from '../../types/Toast';
|
||||
import { isConversationAccepted } from '../../util/isConversationAccepted';
|
||||
import { saveBackupsSubscriberData } from '../../util/backupSubscriptionData';
|
||||
import { postSaveUpdates } from '../../util/cleanup';
|
||||
|
||||
const MAX_CONCURRENCY = 10;
|
||||
|
||||
|
@ -609,6 +610,7 @@ export class BackupImportStream extends Writable {
|
|||
await DataWriter.saveMessages(batch, {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
const attachmentDownloadJobPromises: Array<Promise<unknown>> = [];
|
||||
|
|
|
@ -4,22 +4,21 @@
|
|||
import { batch } from 'react-redux';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import * as Errors from '../types/errors';
|
||||
import * as log from '../logging/log';
|
||||
import { DataReader, DataWriter } from '../sql/Client';
|
||||
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
||||
import { sleep } from '../util/sleep';
|
||||
import { SECOND } from '../util/durations';
|
||||
import * as Errors from '../types/errors';
|
||||
import * as log from '../logging/log';
|
||||
|
||||
import type { MessageModel } from '../models/messages';
|
||||
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||
import { MessageModel } from '../models/messages';
|
||||
import { cleanupMessages } from '../util/cleanup';
|
||||
|
||||
class ExpiringMessagesDeletionService {
|
||||
public update: typeof this.checkExpiringMessages;
|
||||
|
||||
private timeout?: ReturnType<typeof setTimeout>;
|
||||
|
||||
constructor(private readonly singleProtoJobQueue: SingleProtoJobQueue) {
|
||||
constructor() {
|
||||
this.update = debounce(this.checkExpiringMessages, 1000);
|
||||
}
|
||||
|
||||
|
@ -37,17 +36,15 @@ class ExpiringMessagesDeletionService {
|
|||
const inMemoryMessages: Array<MessageModel> = [];
|
||||
|
||||
messages.forEach(dbMessage => {
|
||||
const message = window.MessageCache.__DEPRECATED$register(
|
||||
dbMessage.id,
|
||||
dbMessage,
|
||||
'destroyExpiredMessages'
|
||||
const message = window.MessageCache.register(
|
||||
new MessageModel(dbMessage)
|
||||
);
|
||||
messageIds.push(message.id);
|
||||
inMemoryMessages.push(message);
|
||||
});
|
||||
|
||||
await DataWriter.removeMessages(messageIds, {
|
||||
singleProtoJobQueue: this.singleProtoJobQueue,
|
||||
cleanupMessages,
|
||||
});
|
||||
|
||||
batch(() => {
|
||||
|
@ -57,7 +54,6 @@ class ExpiringMessagesDeletionService {
|
|||
});
|
||||
|
||||
// We do this to update the UI, if this message is being displayed somewhere
|
||||
message.trigger('expired');
|
||||
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
|
||||
// SingleProtoJobQueue. Instead of direct access, it is provided once on startup.
|
||||
export function initialize(singleProtoJobQueue: SingleProtoJobQueue): void {
|
||||
export function initialize(): void {
|
||||
if (instance) {
|
||||
log.warn('Expiring Messages Deletion service is already initialized!');
|
||||
return;
|
||||
}
|
||||
instance = new ExpiringMessagesDeletionService(singleProtoJobQueue);
|
||||
instance = new ExpiringMessagesDeletionService();
|
||||
}
|
||||
|
||||
export async function update(): Promise<void> {
|
||||
|
|
|
@ -26,6 +26,7 @@ import type {
|
|||
ReleaseNoteResponseType,
|
||||
} from '../textsecure/WebAPI';
|
||||
import type { WithRequiredProperties } from '../types/Util';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
const FETCH_INTERVAL = 3 * durations.DAY;
|
||||
const ERROR_RETRY_DELAY = 3 * durations.HOUR;
|
||||
|
@ -187,7 +188,7 @@ export class ReleaseNotesFetcher {
|
|||
];
|
||||
const timestamp = Date.now() + index;
|
||||
|
||||
const message: MessageAttributesType = {
|
||||
const message = new MessageModel({
|
||||
...generateMessageId(incrementMessageCounter()),
|
||||
body: messageBody,
|
||||
bodyRanges,
|
||||
|
@ -201,12 +202,12 @@ export class ReleaseNotesFetcher {
|
|||
sourceServiceId: signalConversation.getServiceId(),
|
||||
timestamp,
|
||||
type: 'incoming',
|
||||
};
|
||||
});
|
||||
|
||||
window.MessageCache.toMessageAttributes(message);
|
||||
signalConversation.trigger('newmessage', message);
|
||||
window.MessageCache.register(message);
|
||||
drop(signalConversation.onNewMessage(message));
|
||||
|
||||
messages.push(message);
|
||||
messages.push(message.attributes);
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
|
|
|
@ -18,6 +18,7 @@ import { strictAssert } from '../util/assert';
|
|||
import { dropNull } from '../util/dropNull';
|
||||
import { DurationInSeconds } from '../util/durations';
|
||||
import { SIGNAL_ACI } from '../types/SignalConversation';
|
||||
import { postSaveUpdates } from '../util/cleanup';
|
||||
|
||||
let storyData: GetAllStoriesResultType | undefined;
|
||||
|
||||
|
@ -174,6 +175,7 @@ async function repairUnexpiredStories(): Promise<void> {
|
|||
storiesWithExpiry.map(messageAttributes => {
|
||||
return DataWriter.saveMessage(messageAttributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
|
|
@ -8,6 +8,9 @@ import { getMessageQueueTime } from '../util/getMessageQueueTime';
|
|||
import * as Errors from '../types/errors';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import { toBoundedDate } from '../util/timestamp';
|
||||
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||
import { eraseMessageContents } from '../util/cleanup';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
async function eraseTapToViewMessages() {
|
||||
try {
|
||||
|
@ -26,22 +29,17 @@ async function eraseTapToViewMessages() {
|
|||
'Must be older than maxTimestamp'
|
||||
);
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$register(
|
||||
fromDB.id,
|
||||
fromDB,
|
||||
'eraseTapToViewMessages'
|
||||
);
|
||||
const message = window.MessageCache.register(new MessageModel(fromDB));
|
||||
|
||||
window.SignalContext.log.info(
|
||||
'eraseTapToViewMessages: erasing message contents',
|
||||
message.idForLogging()
|
||||
getMessageIdForLogging(message.attributes)
|
||||
);
|
||||
|
||||
// We do this to update the UI, if this message is being displayed somewhere
|
||||
message.trigger('expired');
|
||||
window.reduxActions.conversations.messageExpired(message.id);
|
||||
|
||||
await message.eraseContents();
|
||||
await eraseMessageContents(message);
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
|
|
139
ts/sql/Client.ts
139
ts/sql/Client.ts
|
@ -2,30 +2,34 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { ipcRenderer as ipc } from 'electron';
|
||||
|
||||
import { groupBy, isTypedArray, last, map, omit } from 'lodash';
|
||||
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
|
||||
import { deleteExternalFiles } from '../types/Conversation';
|
||||
import { update as updateExpiringMessagesService } from '../services/expiringMessagesDeletion';
|
||||
import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService';
|
||||
// Note: nothing imported here can come back and require Client.ts, and that includes
|
||||
// their imports too. That circularity causes problems. Anything that would do that needs
|
||||
// to be passed in, like cleanupMessages below.
|
||||
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 { assertDev, softAssert } from '../util/assert';
|
||||
import { mapObjectWithSpec } from '../util/mapObjectWithSpec';
|
||||
import type { ObjectMappingSpecType } from '../util/mapObjectWithSpec';
|
||||
import { cleanDataForIpc } from './cleanDataForIpc';
|
||||
import type { AciString, ServiceIdString } from '../types/ServiceId';
|
||||
import createTaskWithTimeout from '../textsecure/TaskWithTimeout';
|
||||
import * as log from '../logging/log';
|
||||
import { isValidUuid, isValidUuidV7 } from '../util/isValidUuid';
|
||||
import * as Errors from '../types/errors';
|
||||
|
||||
import type { StoredJob } from '../jobs/types';
|
||||
import { formatJobForInsert } from '../jobs/formatJobForInsert';
|
||||
import { cleanupMessages } from '../util/cleanup';
|
||||
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 {
|
||||
ClientInterfaceWrap,
|
||||
AdjacentMessagesByConversationOptionsType,
|
||||
|
@ -58,12 +62,8 @@ import type {
|
|||
ClientOnlyReadableInterface,
|
||||
ClientOnlyWritableInterface,
|
||||
} from './Interface';
|
||||
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||
import type { MessageAttributesType } from '../model-types';
|
||||
import { incrementMessageCounter } from '../util/incrementMessageCounter';
|
||||
import { generateSnippetAroundMention } from '../util/search';
|
||||
import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
|
||||
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||
|
||||
const ERASE_SQL_KEY = 'erase-sql-key';
|
||||
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
|
||||
|
@ -121,6 +121,10 @@ const clientOnlyWritable: ClientOnlyWritableInterface = {
|
|||
removeMessage,
|
||||
removeMessages,
|
||||
|
||||
saveMessage,
|
||||
saveMessages,
|
||||
saveMessagesIndividually,
|
||||
|
||||
// Client-side only
|
||||
|
||||
flushUpdateConversationBatcher,
|
||||
|
@ -137,17 +141,12 @@ const clientOnlyWritable: ClientOnlyWritableInterface = {
|
|||
type ClientOverridesType = ClientOnlyWritableInterface &
|
||||
Pick<
|
||||
ClientInterfaceWrap<ServerWritableDirectInterface>,
|
||||
| 'saveAttachmentDownloadJob'
|
||||
| 'saveMessage'
|
||||
| 'saveMessages'
|
||||
| 'updateConversations'
|
||||
'saveAttachmentDownloadJob' | 'updateConversations'
|
||||
>;
|
||||
|
||||
const clientOnlyWritableOverrides: ClientOverridesType = {
|
||||
...clientOnlyWritable,
|
||||
saveAttachmentDownloadJob,
|
||||
saveMessage,
|
||||
saveMessages,
|
||||
updateConversations,
|
||||
};
|
||||
|
||||
|
@ -595,41 +594,76 @@ async function searchMessages({
|
|||
|
||||
async function saveMessage(
|
||||
data: ReadonlyDeep<MessageType>,
|
||||
options: {
|
||||
jobToInsert?: Readonly<StoredJob>;
|
||||
{
|
||||
forceSave,
|
||||
jobToInsert,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
}: {
|
||||
forceSave?: boolean;
|
||||
jobToInsert?: Readonly<StoredJob>;
|
||||
ourAci: AciString;
|
||||
postSaveUpdates: () => Promise<void>;
|
||||
}
|
||||
): Promise<string> {
|
||||
const id = await writableChannel.saveMessage(_cleanMessageData(data), {
|
||||
...options,
|
||||
jobToInsert: options.jobToInsert && formatJobForInsert(options.jobToInsert),
|
||||
forceSave,
|
||||
jobToInsert: jobToInsert && formatJobForInsert(jobToInsert),
|
||||
ourAci,
|
||||
});
|
||||
|
||||
softAssert(
|
||||
// Older messages still have `UUIDv4` so don't log errors when encountering
|
||||
// it.
|
||||
(!options.forceSave && isValidUuid(id)) || isValidUuidV7(id),
|
||||
(!forceSave && isValidUuid(id)) || isValidUuidV7(id),
|
||||
'saveMessage: messageId is not a UUID'
|
||||
);
|
||||
|
||||
void updateExpiringMessagesService();
|
||||
void tapToViewMessagesDeletionService.update();
|
||||
drop(postSaveUpdates?.());
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
async function saveMessages(
|
||||
arrayOfMessages: ReadonlyArray<ReadonlyDeep<MessageType>>,
|
||||
options: { forceSave?: boolean; ourAci: AciString }
|
||||
{
|
||||
forceSave,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
}: {
|
||||
forceSave?: boolean;
|
||||
ourAci: AciString;
|
||||
postSaveUpdates: () => Promise<void>;
|
||||
}
|
||||
): Promise<Array<string>> {
|
||||
const result = await writableChannel.saveMessages(
|
||||
arrayOfMessages.map(message => _cleanMessageData(message)),
|
||||
options
|
||||
{ forceSave, ourAci }
|
||||
);
|
||||
|
||||
void updateExpiringMessagesService();
|
||||
void tapToViewMessagesDeletionService.update();
|
||||
drop(postSaveUpdates?.());
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -637,7 +671,10 @@ async function saveMessages(
|
|||
async function removeMessage(
|
||||
id: string,
|
||||
options: {
|
||||
singleProtoJobQueue: SingleProtoJobQueue;
|
||||
cleanupMessages: (
|
||||
messages: ReadonlyArray<MessageAttributesType>,
|
||||
options: { fromSync?: boolean }
|
||||
) => Promise<void>;
|
||||
fromSync?: boolean;
|
||||
}
|
||||
): Promise<void> {
|
||||
|
@ -647,9 +684,8 @@ async function removeMessage(
|
|||
// it needs to delete all associated on-disk files along with the database delete.
|
||||
if (message) {
|
||||
await writableChannel.removeMessage(id);
|
||||
await cleanupMessages([message], {
|
||||
...options,
|
||||
markCallHistoryDeleted: DataWriter.markCallHistoryDeleted,
|
||||
await options.cleanupMessages([message], {
|
||||
fromSync: options.fromSync,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -659,7 +695,10 @@ export async function deleteAndCleanup(
|
|||
logId: string,
|
||||
options: {
|
||||
fromSync?: boolean;
|
||||
singleProtoJobQueue: SingleProtoJobQueue;
|
||||
cleanupMessages: (
|
||||
messages: ReadonlyArray<MessageAttributesType>,
|
||||
options: { fromSync?: boolean }
|
||||
) => Promise<void>;
|
||||
}
|
||||
): Promise<void> {
|
||||
const ids = messages.map(message => message.id);
|
||||
|
@ -668,9 +707,8 @@ export async function deleteAndCleanup(
|
|||
await writableChannel.removeMessages(ids);
|
||||
|
||||
log.info(`deleteAndCleanup/${logId}: Cleanup for ${ids.length} messages...`);
|
||||
await cleanupMessages(messages, {
|
||||
...options,
|
||||
markCallHistoryDeleted: DataWriter.markCallHistoryDeleted,
|
||||
await options.cleanupMessages(messages, {
|
||||
fromSync: Boolean(options.fromSync),
|
||||
});
|
||||
|
||||
log.info(`deleteAndCleanup/${logId}: Complete`);
|
||||
|
@ -680,13 +718,15 @@ async function removeMessages(
|
|||
messageIds: ReadonlyArray<string>,
|
||||
options: {
|
||||
fromSync?: boolean;
|
||||
singleProtoJobQueue: SingleProtoJobQueue;
|
||||
cleanupMessages: (
|
||||
messages: ReadonlyArray<MessageAttributesType>,
|
||||
options: { fromSync?: boolean }
|
||||
) => Promise<void>;
|
||||
}
|
||||
): Promise<void> {
|
||||
const messages = await readableChannel.getMessagesById(messageIds);
|
||||
await cleanupMessages(messages, {
|
||||
...options,
|
||||
markCallHistoryDeleted: DataWriter.markCallHistoryDeleted,
|
||||
await options.cleanupMessages(messages, {
|
||||
fromSync: Boolean(options.fromSync),
|
||||
});
|
||||
await writableChannel.removeMessages(messageIds);
|
||||
}
|
||||
|
@ -743,15 +783,18 @@ async function getConversationRangeCenteredOnMessage(
|
|||
async function removeMessagesInConversation(
|
||||
conversationId: string,
|
||||
{
|
||||
cleanupMessages,
|
||||
fromSync,
|
||||
logId,
|
||||
receivedAt,
|
||||
singleProtoJobQueue,
|
||||
fromSync,
|
||||
}: {
|
||||
cleanupMessages: (
|
||||
messages: ReadonlyArray<MessageAttributesType>,
|
||||
options: { fromSync?: boolean | undefined }
|
||||
) => Promise<void>;
|
||||
fromSync?: boolean;
|
||||
logId: string;
|
||||
receivedAt?: number;
|
||||
singleProtoJobQueue: SingleProtoJobQueue;
|
||||
}
|
||||
): Promise<void> {
|
||||
let messages;
|
||||
|
@ -776,7 +819,7 @@ async function removeMessagesInConversation(
|
|||
}
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await deleteAndCleanup(messages, logId, { fromSync, singleProtoJobQueue });
|
||||
await deleteAndCleanup(messages, logId, { fromSync, cleanupMessages });
|
||||
} while (messages.length > 0);
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,6 @@ import type {
|
|||
} from '../types/GroupSendEndorsements';
|
||||
import type { SyncTaskType } from '../util/syncTasks';
|
||||
import type { AttachmentBackupJobType } from '../types/AttachmentBackup';
|
||||
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||
|
||||
export type ReadableDB = Database & { __readable_db: never };
|
||||
export type WritableDB = ReadableDB & { __writable_db: never };
|
||||
|
@ -749,23 +748,6 @@ type WritableInterface = {
|
|||
replaceAllEndorsementsForGroup: (data: GroupSendEndorsementsData) => 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: {
|
||||
conversationId: string;
|
||||
includeStoryReplies: boolean;
|
||||
|
@ -1047,6 +1029,22 @@ export type ServerWritableDirectInterface = WritableInterface & {
|
|||
updateConversation: (data: ConversationType) => 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;
|
||||
removeMessages: (ids: ReadonlyArray<string>) => void;
|
||||
|
||||
|
@ -1134,18 +1132,49 @@ export type ClientOnlyWritableInterface = ClientInterfaceWrap<{
|
|||
removeConversation: (id: string) => 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: (
|
||||
id: string,
|
||||
options: {
|
||||
fromSync?: boolean;
|
||||
singleProtoJobQueue: SingleProtoJobQueue;
|
||||
cleanupMessages: (
|
||||
messages: ReadonlyArray<MessageAttributesType>,
|
||||
options: { fromSync?: boolean | undefined }
|
||||
) => Promise<void>;
|
||||
}
|
||||
) => void;
|
||||
removeMessages: (
|
||||
ids: ReadonlyArray<string>,
|
||||
options: {
|
||||
fromSync?: boolean;
|
||||
singleProtoJobQueue: SingleProtoJobQueue;
|
||||
cleanupMessages: (
|
||||
messages: ReadonlyArray<MessageAttributesType>,
|
||||
options: { fromSync?: boolean | undefined }
|
||||
) => Promise<void>;
|
||||
}
|
||||
) => void;
|
||||
|
||||
|
@ -1170,10 +1199,13 @@ export type ClientOnlyWritableInterface = ClientInterfaceWrap<{
|
|||
removeMessagesInConversation: (
|
||||
conversationId: string,
|
||||
options: {
|
||||
cleanupMessages: (
|
||||
messages: ReadonlyArray<MessageAttributesType>,
|
||||
options: { fromSync?: boolean | undefined }
|
||||
) => Promise<void>;
|
||||
fromSync?: boolean;
|
||||
logId: string;
|
||||
receivedAt?: number;
|
||||
singleProtoJobQueue: SingleProtoJobQueue;
|
||||
}
|
||||
) => void;
|
||||
removeOtherData: () => void;
|
||||
|
|
|
@ -69,7 +69,7 @@ import { resolveAttachmentDraftData } from '../../util/resolveAttachmentDraftDat
|
|||
import { resolveDraftAttachmentOnDisk } from '../../util/resolveDraftAttachmentOnDisk';
|
||||
import { shouldShowInvalidMessageToast } from '../../util/shouldShowInvalidMessageToast';
|
||||
import { writeDraftAttachment } from '../../util/writeDraftAttachment';
|
||||
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
|
||||
import { getMessageById } from '../../messages/getMessageById';
|
||||
import { canReply, isNormalBubble } from '../selectors/message';
|
||||
import { getAuthorId } from '../../messages/helpers';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
|
@ -730,9 +730,7 @@ export function setQuoteByMessageId(
|
|||
return;
|
||||
}
|
||||
|
||||
const message = messageId
|
||||
? await __DEPRECATED$getMessageById(messageId, 'setQuoteByMessageId')
|
||||
: undefined;
|
||||
const message = messageId ? await getMessageById(messageId) : undefined;
|
||||
const state = getState();
|
||||
|
||||
if (
|
||||
|
|
|
@ -126,11 +126,12 @@ import {
|
|||
isDirectConversation,
|
||||
isGroup,
|
||||
isGroupV2,
|
||||
isMe,
|
||||
} from '../../util/whatTypeOfConversation';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
|
||||
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 { sendDeleteForEveryoneMessage } from '../../util/sendDeleteForEveryoneMessage';
|
||||
import type { ShowToastActionType } from './toast';
|
||||
|
@ -149,7 +150,7 @@ import {
|
|||
buildUpdateAttributesChange,
|
||||
initiateMigrationToGroupV2 as doInitiateMigrationToGroupV2,
|
||||
} from '../../groups';
|
||||
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
|
||||
import { getMessageById } from '../../messages/getMessageById';
|
||||
import type { PanelRenderType, PanelRequestType } from '../../types/Panels';
|
||||
import type { ConversationQueueJobData } from '../../jobs/conversationJobQueue';
|
||||
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 { sortByMessageOrder } from '../../types/ForwardDraft';
|
||||
import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation';
|
||||
import { getConversationIdForLogging } from '../../util/idForLogging';
|
||||
import {
|
||||
getConversationIdForLogging,
|
||||
getMessageIdForLogging,
|
||||
} from '../../util/idForLogging';
|
||||
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
|
||||
import MessageSender from '../../textsecure/SendMessage';
|
||||
import {
|
||||
|
@ -204,6 +208,18 @@ import { markCallHistoryReadInConversation } from './callHistory';
|
|||
import type { CapabilitiesType } from '../../textsecure/WebAPI';
|
||||
import { actions as searchActions } 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
|
||||
|
||||
export type DBConversationType = ReadonlyDeep<{
|
||||
|
@ -1410,10 +1426,7 @@ function markMessageRead(
|
|||
return;
|
||||
}
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'markMessageRead'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(`markMessageRead: failed to load message ${messageId}`);
|
||||
}
|
||||
|
@ -1767,10 +1780,7 @@ function deleteMessages({
|
|||
await Promise.all(
|
||||
messageIds.map(
|
||||
async (messageId): Promise<MessageToDelete | undefined> => {
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'deleteMessages'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(`deleteMessages: Message ${messageId} missing!`);
|
||||
}
|
||||
|
@ -1805,7 +1815,7 @@ function deleteMessages({
|
|||
}
|
||||
|
||||
await DataWriter.removeMessages(messageIds, {
|
||||
singleProtoJobQueue,
|
||||
cleanupMessages,
|
||||
});
|
||||
|
||||
popPanelForConversation()(dispatch, getState, undefined);
|
||||
|
@ -1930,9 +1940,7 @@ function setMessageToEdit(
|
|||
return;
|
||||
}
|
||||
|
||||
const message = (
|
||||
await __DEPRECATED$getMessageById(messageId, 'setMessageToEdit')
|
||||
)?.attributes;
|
||||
const message = (await getMessageById(messageId))?.attributes;
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
@ -2025,10 +2033,7 @@ function generateNewGroupLink(
|
|||
* replace it with an actual action that fits in with the redux approach.
|
||||
*/
|
||||
export const markViewed = (messageId: string): void => {
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
messageId,
|
||||
'markViewed'
|
||||
);
|
||||
const message = window.MessageCache.getById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(`markViewed: Message ${messageId} missing!`);
|
||||
}
|
||||
|
@ -2051,7 +2056,9 @@ export const markViewed = (messageId: string): void => {
|
|||
);
|
||||
senderAci = sourceServiceId;
|
||||
|
||||
const convoAttributes = message.getConversation()?.attributes;
|
||||
const convo = window.ConversationController.get(
|
||||
message.get('conversationId')
|
||||
);
|
||||
const conversationId = message.get('conversationId');
|
||||
drop(
|
||||
conversationJobQueue.add({
|
||||
|
@ -2065,8 +2072,8 @@ export const markViewed = (messageId: string): void => {
|
|||
senderE164,
|
||||
senderAci,
|
||||
timestamp,
|
||||
isDirectConversation: convoAttributes
|
||||
? isDirectConversation(convoAttributes)
|
||||
isDirectConversation: convo
|
||||
? isDirectConversation(convo.attributes)
|
||||
: true,
|
||||
},
|
||||
],
|
||||
|
@ -2292,16 +2299,14 @@ function kickOffAttachmentDownload(
|
|||
options: Readonly<{ messageId: string }>
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return async dispatch => {
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
options.messageId,
|
||||
'kickOffAttachmentDownload'
|
||||
);
|
||||
const message = await getMessageById(options.messageId);
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
`kickOffAttachmentDownload: Message ${options.messageId} missing!`
|
||||
);
|
||||
}
|
||||
const didUpdateValues = await message.queueAttachmentDownloads(
|
||||
const didUpdateValues = await queueAttachmentDownloadsForMessage(
|
||||
message,
|
||||
AttachmentDownloadUrgency.IMMEDIATE
|
||||
);
|
||||
|
||||
|
@ -2309,6 +2314,7 @@ function kickOffAttachmentDownload(
|
|||
drop(
|
||||
DataWriter.saveMessage(message.attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -2329,10 +2335,7 @@ function cancelAttachmentDownload({
|
|||
NoopActionType
|
||||
> {
|
||||
return async dispatch => {
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'cancelAttachmentDownload'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
log.warn(`cancelAttachmentDownload: Message ${messageId} missing!`);
|
||||
} else {
|
||||
|
@ -2344,7 +2347,10 @@ function cancelAttachmentDownload({
|
|||
});
|
||||
|
||||
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
|
||||
|
@ -2370,16 +2376,7 @@ function markAttachmentAsCorrupted(
|
|||
options: AttachmentOptions
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return async dispatch => {
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
options.messageId,
|
||||
'markAttachmentAsCorrupted'
|
||||
);
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
`markAttachmentAsCorrupted: Message ${options.messageId} missing!`
|
||||
);
|
||||
}
|
||||
message.markAttachmentAsCorrupted(options.attachment);
|
||||
await doMarkAttachmentAsCorrupted(options.messageId, options.attachment);
|
||||
|
||||
dispatch({
|
||||
type: 'NOOP',
|
||||
|
@ -2392,10 +2389,7 @@ function openGiftBadge(
|
|||
messageId: string
|
||||
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
|
||||
return async dispatch => {
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'openGiftBadge'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(`openGiftBadge: Message ${messageId} missing!`);
|
||||
}
|
||||
|
@ -2415,14 +2409,106 @@ function retryMessageSend(
|
|||
messageId: string
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return async dispatch => {
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'retryMessageSend'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
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({
|
||||
type: 'NOOP',
|
||||
|
@ -2435,15 +2521,12 @@ export function copyMessageText(
|
|||
messageId: string
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return async dispatch => {
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'copyMessageText'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(`copy: Message ${messageId} missing!`);
|
||||
}
|
||||
|
||||
const body = message.getNotificationText();
|
||||
const body = getNotificationTextForMessage(message.attributes);
|
||||
clipboard.writeText(body);
|
||||
|
||||
dispatch({
|
||||
|
@ -2457,10 +2540,7 @@ export function retryDeleteForEveryone(
|
|||
messageId: string
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return async dispatch => {
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'retryDeleteForEveryone'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(`retryDeleteForEveryone: Message ${messageId} missing!`);
|
||||
}
|
||||
|
@ -2472,7 +2552,9 @@ export function retryDeleteForEveryone(
|
|||
}
|
||||
|
||||
try {
|
||||
const conversation = message.getConversation();
|
||||
const conversation = window.ConversationController.get(
|
||||
message.get('conversationId')
|
||||
);
|
||||
if (!conversation) {
|
||||
throw new Error(
|
||||
`retryDeleteForEveryone: Conversation for ${messageId} missing!`
|
||||
|
@ -2489,7 +2571,7 @@ export function retryDeleteForEveryone(
|
|||
};
|
||||
|
||||
log.info(
|
||||
`retryDeleteForEveryone: Adding job for message ${message.idForLogging()}!`
|
||||
`retryDeleteForEveryone: Adding job for message ${getMessageIdForLogging(message.attributes)}!`
|
||||
);
|
||||
await conversationJobQueue.add(jobData);
|
||||
|
||||
|
@ -3247,12 +3329,7 @@ function pushPanelForConversation(
|
|||
|
||||
const message =
|
||||
conversations.messagesLookup[messageId] ||
|
||||
(
|
||||
await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'pushPanelForConversation'
|
||||
)
|
||||
)?.attributes;
|
||||
(await getMessageById(messageId))?.attributes;
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
'pushPanelForConversation: could not find message for MessageDetails'
|
||||
|
@ -3328,17 +3405,16 @@ function deleteMessagesForEveryone(
|
|||
await Promise.all(
|
||||
messageIds.map(async messageId => {
|
||||
try {
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
messageId,
|
||||
'deleteMessagesForEveryone'
|
||||
);
|
||||
const message = window.MessageCache.getById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
`deleteMessageForEveryone: Message ${messageId} missing!`
|
||||
);
|
||||
}
|
||||
|
||||
const conversation = message.getConversation();
|
||||
const conversation = window.ConversationController.get(
|
||||
message.get('conversationId')
|
||||
);
|
||||
if (!conversation) {
|
||||
throw new Error('deleteMessageForEveryone: no conversation');
|
||||
}
|
||||
|
@ -3834,11 +3910,7 @@ function loadRecentMediaItems(
|
|||
|
||||
// Cache these messages in memory to ensure Lightbox can find them
|
||||
messages.forEach(message => {
|
||||
window.MessageCache.__DEPRECATED$register(
|
||||
message.id,
|
||||
message,
|
||||
'loadRecentMediaItems'
|
||||
);
|
||||
window.MessageCache.register(new MessageModel(message));
|
||||
});
|
||||
|
||||
let index = 0;
|
||||
|
@ -4042,10 +4114,7 @@ export function saveAttachmentFromMessage(
|
|||
providedAttachment?: AttachmentType
|
||||
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
|
||||
return async (dispatch, getState) => {
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'saveAttachmentFromMessage'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
`saveAttachmentFromMessage: Message ${messageId} missing!`
|
||||
|
@ -4138,10 +4207,7 @@ export function scrollToMessage(
|
|||
throw new Error('scrollToMessage: No conversation found');
|
||||
}
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'scrollToMessage'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(`scrollToMessage: failed to load message ${messageId}`);
|
||||
}
|
||||
|
@ -4155,12 +4221,7 @@ export function scrollToMessage(
|
|||
|
||||
let isInMemory = true;
|
||||
|
||||
if (
|
||||
!window.MessageCache.__DEPRECATED$getById(
|
||||
messageId,
|
||||
'scrollToMessage/notInMemory'
|
||||
)
|
||||
) {
|
||||
if (!window.MessageCache.getById(messageId)) {
|
||||
isInMemory = false;
|
||||
}
|
||||
|
||||
|
@ -4591,10 +4652,7 @@ function onConversationOpened(
|
|||
log.info(`${logId}: Updating newly opened conversation state`);
|
||||
|
||||
if (messageId) {
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'onConversationOpened'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
|
||||
if (message) {
|
||||
drop(conversation.loadAndScroll(messageId));
|
||||
|
@ -4733,12 +4791,9 @@ function showArchivedConversations(): ShowArchivedConversationsActionType {
|
|||
}
|
||||
|
||||
function doubleCheckMissingQuoteReference(messageId: string): NoopActionType {
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
messageId,
|
||||
'doubleCheckMissingQuoteReference'
|
||||
);
|
||||
const message = window.MessageCache.getById(messageId);
|
||||
if (message) {
|
||||
void message.doubleCheckMissingQuoteReference();
|
||||
drop(doDoubleCheckMissingQuoteReference(message));
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -49,6 +49,7 @@ import type { CallLinkType } from '../../types/CallLink';
|
|||
import type { LocalizerType } from '../../types/I18N';
|
||||
import { linkCallRoute } from '../../util/signalRoutes';
|
||||
import type { StartCallData } from '../../components/ConfirmLeaveCallModal';
|
||||
import { getMessageById } from '../../messages/getMessageById';
|
||||
|
||||
// State
|
||||
|
||||
|
@ -624,12 +625,13 @@ function toggleForwardMessagesModal(
|
|||
if (payload.type === ForwardMessagesModalType.Forward) {
|
||||
messageDrafts = await Promise.all(
|
||||
payload.messageIds.map(async messageId => {
|
||||
const messageAttributes = await window.MessageCache.resolveAttributes(
|
||||
'toggleForwardMessagesModal',
|
||||
messageId
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
'toggleForwardMessagesModal: failed to find target message'
|
||||
);
|
||||
|
||||
const { attachments = [] } = messageAttributes;
|
||||
}
|
||||
const { attachments = [] } = message.attributes;
|
||||
|
||||
if (!attachments.every(isDownloaded)) {
|
||||
dispatch(
|
||||
|
@ -641,7 +643,7 @@ function toggleForwardMessagesModal(
|
|||
const messagePropsSelector = getMessagePropsSelector(state);
|
||||
const conversationSelector = getConversationSelector(state);
|
||||
|
||||
const messageProps = messagePropsSelector(messageAttributes);
|
||||
const messageProps = messagePropsSelector(message.attributes);
|
||||
const messageDraft = toMessageForwardDraft(
|
||||
messageProps,
|
||||
conversationSelector
|
||||
|
@ -944,12 +946,14 @@ function showEditHistoryModal(
|
|||
messageId: string
|
||||
): ThunkAction<void, RootStateType, unknown, ShowEditHistoryModalActionType> {
|
||||
return async dispatch => {
|
||||
const messageAttributes = await window.MessageCache.resolveAttributes(
|
||||
'showEditHistoryModal',
|
||||
messageId
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
throw new Error('showEditHistoryModal: failed to find target message');
|
||||
}
|
||||
|
||||
const nextEditHistoryMessages = copyOverMessageAttributesIntoEditHistory(
|
||||
message.attributes
|
||||
);
|
||||
const nextEditHistoryMessages =
|
||||
copyOverMessageAttributesIntoEditHistory(messageAttributes);
|
||||
|
||||
if (!nextEditHistoryMessages) {
|
||||
log.warn('showEditHistoryModal: no edit history for message');
|
||||
|
|
|
@ -17,7 +17,7 @@ import type { ShowToastActionType } from './toast';
|
|||
import type { StateType as RootStateType } from '../reducer';
|
||||
|
||||
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 { isGIF } from '../../types/Attachment';
|
||||
import {
|
||||
|
@ -40,6 +40,8 @@ import {
|
|||
import { showStickerPackPreview } from './globalModals';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||
import { DataReader } from '../../sql/Client';
|
||||
import { getMessageIdForLogging } from '../../util/idForLogging';
|
||||
import { markViewOnceMessageViewed } from '../../services/MessageUpdater';
|
||||
|
||||
// eslint-disable-next-line local-rules/type-alias-readonlydeep
|
||||
export type LightboxStateType =
|
||||
|
@ -156,10 +158,7 @@ function showLightboxForViewOnceMedia(
|
|||
return async dispatch => {
|
||||
log.info('showLightboxForViewOnceMedia: attempting to display message');
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'showLightboxForViewOnceMedia'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
`showLightboxForViewOnceMedia: Message ${messageId} missing!`
|
||||
|
@ -168,20 +167,20 @@ function showLightboxForViewOnceMedia(
|
|||
|
||||
if (!isTapToView(message.attributes)) {
|
||||
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(
|
||||
`showLightboxForViewOnceMedia: Message ${message.idForLogging()} is already erased`
|
||||
`showLightboxForViewOnceMedia: Message ${getMessageIdForLogging(message.attributes)} is already erased`
|
||||
);
|
||||
}
|
||||
|
||||
const firstAttachment = (message.get('attachments') || [])[0];
|
||||
if (!firstAttachment || !firstAttachment.path) {
|
||||
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,
|
||||
};
|
||||
|
||||
await message.markViewOnceMessageViewed();
|
||||
await markViewOnceMessageViewed(message);
|
||||
|
||||
const { contentType } = tempAttachment;
|
||||
|
||||
|
@ -253,10 +252,7 @@ function showLightbox(opts: {
|
|||
return async (dispatch, getState) => {
|
||||
const { attachment, messageId } = opts;
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'showLightbox'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(`showLightbox: Message ${messageId} missing!`);
|
||||
}
|
||||
|
@ -393,10 +389,7 @@ function showLightboxForAdjacentMessage(
|
|||
const [media] = lightbox.media;
|
||||
const { id: messageId, receivedAt, sentAt } = media.message;
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'showLightboxForAdjacentMessage'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
log.warn('showLightboxForAdjacentMessage: original message is gone');
|
||||
dispatch({
|
||||
|
|
|
@ -33,6 +33,7 @@ import type { MIMEType } from '../../types/MIME';
|
|||
import type { MediaItemType } from '../../types/MediaItem';
|
||||
import type { StateType as RootStateType } from '../reducer';
|
||||
import type { MessageAttributesType } from '../../model-types';
|
||||
import { MessageModel } from '../../models/messages';
|
||||
|
||||
type MediaItemMessage = ReadonlyDeep<{
|
||||
attachments: Array<AttachmentType>;
|
||||
|
@ -141,13 +142,13 @@ function _getMediaItemMessage(
|
|||
}
|
||||
|
||||
function _cleanVisualAttachments(
|
||||
rawMedia: ReadonlyDeep<ReadonlyArray<MessageAttributesType>>
|
||||
rawMedia: ReadonlyDeep<ReadonlyArray<MessageModel>>
|
||||
): ReadonlyArray<MediaType> {
|
||||
return rawMedia
|
||||
.flatMap(message => {
|
||||
let index = 0;
|
||||
|
||||
return (message.attachments || []).map(
|
||||
return (message.get('attachments') || []).map(
|
||||
(attachment: AttachmentType): MediaType | undefined => {
|
||||
if (
|
||||
!attachment.path ||
|
||||
|
@ -168,7 +169,7 @@ function _cleanVisualAttachments(
|
|||
contentType: attachment.contentType,
|
||||
index,
|
||||
attachment,
|
||||
message: _getMediaItemMessage(message),
|
||||
message: _getMediaItemMessage(message.attributes),
|
||||
};
|
||||
|
||||
index += 1;
|
||||
|
@ -181,11 +182,11 @@ function _cleanVisualAttachments(
|
|||
}
|
||||
|
||||
function _cleanFileAttachments(
|
||||
rawDocuments: ReadonlyDeep<ReadonlyArray<MessageAttributesType>>
|
||||
rawDocuments: ReadonlyDeep<ReadonlyArray<MessageModel>>
|
||||
): ReadonlyArray<MediaItemType> {
|
||||
return rawDocuments
|
||||
.map(message => {
|
||||
const attachments = message.attachments || [];
|
||||
const attachments = message.get('attachments') || [];
|
||||
const attachment = attachments[0];
|
||||
if (!attachment) {
|
||||
return;
|
||||
|
@ -196,7 +197,7 @@ function _cleanFileAttachments(
|
|||
index: 0,
|
||||
attachment,
|
||||
message: {
|
||||
..._getMediaItemMessage(message),
|
||||
..._getMediaItemMessage(message.attributes),
|
||||
attachments: [attachment],
|
||||
},
|
||||
};
|
||||
|
@ -205,27 +206,25 @@ function _cleanFileAttachments(
|
|||
}
|
||||
|
||||
async function _upgradeMessages(
|
||||
messages: ReadonlyArray<MessageAttributesType>
|
||||
): Promise<ReadonlyArray<MessageAttributesType>> {
|
||||
messages: ReadonlyArray<MessageModel>
|
||||
): Promise<void> {
|
||||
// We upgrade these messages so they are sure to have thumbnails
|
||||
const upgraded = await Promise.all(
|
||||
await Promise.all(
|
||||
messages.map(async message => {
|
||||
try {
|
||||
return await window.MessageCache.upgradeSchema(
|
||||
await window.MessageCache.upgradeSchema(
|
||||
message,
|
||||
VERSION_NEEDED_FOR_DISPLAY
|
||||
);
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
'_upgradeMessages: Failed to upgrade message ' +
|
||||
`${getMessageIdForLogging(message)}: ${Errors.toLogFormat(error)}`
|
||||
`${getMessageIdForLogging(message.attributes)}: ${Errors.toLogFormat(error)}`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return upgraded.filter(isNotNil);
|
||||
}
|
||||
|
||||
function initialLoad(
|
||||
|
@ -242,24 +241,28 @@ function initialLoad(
|
|||
payload: { loading: true },
|
||||
});
|
||||
|
||||
const rawMedia = await DataReader.getOlderMessagesByConversation({
|
||||
const rawMedia = (
|
||||
await DataReader.getOlderMessagesByConversation({
|
||||
conversationId,
|
||||
includeStoryReplies: false,
|
||||
limit: FETCH_CHUNK_COUNT,
|
||||
requireVisualMediaAttachments: true,
|
||||
storyId: undefined,
|
||||
});
|
||||
const rawDocuments = await DataReader.getOlderMessagesByConversation({
|
||||
})
|
||||
).map(item => window.MessageCache.register(new MessageModel(item)));
|
||||
const rawDocuments = (
|
||||
await DataReader.getOlderMessagesByConversation({
|
||||
conversationId,
|
||||
includeStoryReplies: false,
|
||||
limit: FETCH_CHUNK_COUNT,
|
||||
requireFileAttachments: true,
|
||||
storyId: undefined,
|
||||
});
|
||||
})
|
||||
).map(item => window.MessageCache.register(new MessageModel(item)));
|
||||
|
||||
const upgraded = await _upgradeMessages(rawMedia);
|
||||
const media = _cleanVisualAttachments(upgraded);
|
||||
await _upgradeMessages(rawMedia);
|
||||
|
||||
const media = _cleanVisualAttachments(rawMedia);
|
||||
const documents = _cleanFileAttachments(rawDocuments);
|
||||
|
||||
dispatch({
|
||||
|
@ -305,7 +308,8 @@ function loadMoreMedia(
|
|||
|
||||
const { sentAt, receivedAt, id: messageId } = oldestLoadedMedia.message;
|
||||
|
||||
const rawMedia = await DataReader.getOlderMessagesByConversation({
|
||||
const rawMedia = (
|
||||
await DataReader.getOlderMessagesByConversation({
|
||||
conversationId,
|
||||
includeStoryReplies: false,
|
||||
limit: FETCH_CHUNK_COUNT,
|
||||
|
@ -314,10 +318,12 @@ function loadMoreMedia(
|
|||
requireVisualMediaAttachments: true,
|
||||
sentAt,
|
||||
storyId: undefined,
|
||||
});
|
||||
})
|
||||
).map(item => window.MessageCache.register(new MessageModel(item)));
|
||||
|
||||
const upgraded = await _upgradeMessages(rawMedia);
|
||||
const media = _cleanVisualAttachments(upgraded);
|
||||
await _upgradeMessages(rawMedia);
|
||||
|
||||
const media = _cleanVisualAttachments(rawMedia);
|
||||
|
||||
dispatch({
|
||||
type: LOAD_MORE_MEDIA,
|
||||
|
@ -367,7 +373,8 @@ function loadMoreDocuments(
|
|||
|
||||
const { sentAt, receivedAt, id: messageId } = oldestLoadedDocument.message;
|
||||
|
||||
const rawDocuments = await DataReader.getOlderMessagesByConversation({
|
||||
const rawDocuments = (
|
||||
await DataReader.getOlderMessagesByConversation({
|
||||
conversationId,
|
||||
includeStoryReplies: false,
|
||||
limit: FETCH_CHUNK_COUNT,
|
||||
|
@ -376,7 +383,8 @@ function loadMoreDocuments(
|
|||
requireFileAttachments: true,
|
||||
sentAt,
|
||||
storyId: undefined,
|
||||
});
|
||||
})
|
||||
).map(item => window.MessageCache.register(new MessageModel(item)));
|
||||
|
||||
const documents = _cleanFileAttachments(rawDocuments);
|
||||
|
||||
|
@ -500,8 +508,12 @@ export function reducer(
|
|||
const oldestLoadedMedia = state.media[0];
|
||||
const oldestLoadedDocument = state.documents[0];
|
||||
|
||||
const newMedia = _cleanVisualAttachments([message]);
|
||||
const newDocuments = _cleanFileAttachments([message]);
|
||||
const newMedia = _cleanVisualAttachments([
|
||||
window.MessageCache.register(new MessageModel(message)),
|
||||
]);
|
||||
const newDocuments = _cleanFileAttachments([
|
||||
window.MessageCache.register(new MessageModel(message)),
|
||||
]);
|
||||
|
||||
let { documents, haveOldestDocument, haveOldestMedia, media } = state;
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ import { blockSendUntilConversationsAreVerified } from '../../util/blockSendUnti
|
|||
import { deleteStoryForEveryone as doDeleteStoryForEveryone } from '../../util/deleteStoryForEveryone';
|
||||
import { deleteGroupStoryReplyForEveryone as doDeleteGroupStoryReplyForEveryone } from '../../util/deleteGroupStoryReplyForEveryone';
|
||||
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
|
||||
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
|
||||
import { getMessageById } from '../../messages/getMessageById';
|
||||
import { markOnboardingStoryAsRead } from '../../util/markOnboardingStoryAsRead';
|
||||
import { markViewed } from '../../services/MessageUpdater';
|
||||
import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads';
|
||||
|
@ -69,7 +69,7 @@ import {
|
|||
conversationQueueJobEnum,
|
||||
} from '../../jobs/conversationJobQueue';
|
||||
import { ReceiptType } from '../../types/Receipt';
|
||||
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
|
||||
import { cleanupMessages, postSaveUpdates } from '../../util/cleanup';
|
||||
|
||||
export type StoryDataType = ReadonlyDeep<
|
||||
{
|
||||
|
@ -286,7 +286,7 @@ function deleteGroupStoryReply(
|
|||
messageId: string
|
||||
): ThunkAction<void, RootStateType, unknown, StoryReplyDeletedActionType> {
|
||||
return async dispatch => {
|
||||
await DataWriter.removeMessage(messageId, { singleProtoJobQueue });
|
||||
await DataWriter.removeMessage(messageId, { cleanupMessages });
|
||||
dispatch({
|
||||
type: STORY_REPLY_DELETED,
|
||||
payload: messageId,
|
||||
|
@ -382,10 +382,7 @@ function markStoryRead(
|
|||
return;
|
||||
}
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'markStoryRead'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
|
||||
if (!message) {
|
||||
log.warn(`markStoryRead: no message found ${messageId}`);
|
||||
|
@ -427,6 +424,7 @@ function markStoryRead(
|
|||
drop(
|
||||
DataWriter.saveMessage(message.attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -524,10 +522,7 @@ function queueStoryDownload(
|
|||
return;
|
||||
}
|
||||
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
storyId,
|
||||
'queueStoryDownload'
|
||||
);
|
||||
const message = await getMessageById(storyId);
|
||||
|
||||
if (message) {
|
||||
// We want to ensure that we re-hydrate the story reply context with the
|
||||
|
@ -1402,10 +1397,7 @@ function removeAllContactStories(
|
|||
const messages = (
|
||||
await Promise.all(
|
||||
messageIds.map(async messageId => {
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'removeAllContactStories'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
|
||||
if (!message) {
|
||||
log.warn(`${logId}: no message found ${messageId}`);
|
||||
|
@ -1419,7 +1411,7 @@ function removeAllContactStories(
|
|||
|
||||
log.info(`${logId}: removing ${messages.length} stories`);
|
||||
|
||||
await DataWriter.removeMessages(messageIds, { singleProtoJobQueue });
|
||||
await DataWriter.removeMessages(messageIds, { cleanupMessages });
|
||||
|
||||
dispatch({
|
||||
type: 'NOOP',
|
||||
|
|
|
@ -23,7 +23,7 @@ import { useLinkPreviewActions } from '../ducks/linkPreviews';
|
|||
import { SmartCompositionTextArea } from './CompositionTextArea';
|
||||
import { useToastActions } from '../ducks/toast';
|
||||
import { isDownloaded } from '../../types/Attachment';
|
||||
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
|
||||
import { getMessageById } from '../../messages/getMessageById';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import type {
|
||||
ForwardMessageData,
|
||||
|
@ -117,10 +117,7 @@ function SmartForwardMessagesModalInner({
|
|||
if (draft.originalMessageId == null) {
|
||||
return { draft, originalMessage: null };
|
||||
}
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
draft.originalMessageId,
|
||||
'doForwardMessages'
|
||||
);
|
||||
const message = await getMessageById(draft.originalMessageId);
|
||||
strictAssert(message, 'no message found');
|
||||
return {
|
||||
draft,
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
messageReceiptTypeSchema,
|
||||
} from '../messageModifiers/MessageReceipts';
|
||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||
import { postSaveUpdates } from '../util/cleanup';
|
||||
|
||||
describe('MessageReceipts', () => {
|
||||
let ourAci: AciString;
|
||||
|
@ -81,6 +82,7 @@ describe('MessageReceipts', () => {
|
|||
await DataWriter.saveMessage(messageAttributes, {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
|
@ -158,6 +160,7 @@ describe('MessageReceipts', () => {
|
|||
await DataWriter.saveMessage(messageAttributes, {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
await DataWriter.saveEditedMessage(messageAttributes, ourAci, {
|
||||
conversationId: messageAttributes.conversationId,
|
||||
|
|
|
@ -27,6 +27,7 @@ import { generateAci, generatePni } from '../../types/ServiceId';
|
|||
import { DataReader, DataWriter } from '../../sql/Client';
|
||||
import { getRandomBytes } from '../../Crypto';
|
||||
import * as Bytes from '../../Bytes';
|
||||
import { postSaveUpdates } from '../../util/cleanup';
|
||||
|
||||
export const OUR_ACI = generateAci();
|
||||
export const OUR_PNI = generatePni();
|
||||
|
@ -213,7 +214,11 @@ export async function asymmetricRoundtripHarness(
|
|||
try {
|
||||
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);
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ import { clearData } from './helpers';
|
|||
import { loadAllAndReinitializeRedux } from '../../services/allLoaders';
|
||||
import { backupsService, BackupType } from '../../services/backups';
|
||||
import { initialize as initializeExpiringMessageService } from '../../services/expiringMessagesDeletion';
|
||||
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
|
||||
import { DataWriter } from '../../sql/Client';
|
||||
|
||||
const { BACKUP_INTEGRATION_DIR } = process.env;
|
||||
|
@ -42,7 +41,7 @@ class MemoryStream extends InputStream {
|
|||
|
||||
describe('backup/integration', () => {
|
||||
before(async () => {
|
||||
await initializeExpiringMessageService(singleProtoJobQueue);
|
||||
await initializeExpiringMessageService();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
|
|
@ -2,12 +2,14 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { v4 as generateUuid } from 'uuid';
|
||||
import { v7 as generateUuid } from 'uuid';
|
||||
|
||||
import { DataWriter } from '../../sql/Client';
|
||||
import { SendStatus } from '../../messages/MessageSendState';
|
||||
import { IMAGE_PNG } from '../../types/MIME';
|
||||
import { generateAci, generatePni } from '../../types/ServiceId';
|
||||
import { postSaveUpdates } from '../../util/cleanup';
|
||||
import { MessageModel } from '../../models/messages';
|
||||
|
||||
describe('Conversations', () => {
|
||||
async function resetConversationController(): Promise<void> {
|
||||
|
@ -40,6 +42,7 @@ describe('Conversations', () => {
|
|||
profileSharing: true,
|
||||
version: 0,
|
||||
expireTimerVersion: 1,
|
||||
lastMessage: 'starting value',
|
||||
});
|
||||
|
||||
await window.textsecure.storage.user.setCredentials({
|
||||
|
@ -59,7 +62,7 @@ describe('Conversations', () => {
|
|||
|
||||
// Creating a fake message
|
||||
const now = Date.now();
|
||||
let message = new window.Whisper.Message({
|
||||
let message = new MessageModel({
|
||||
attachments: [],
|
||||
body: 'bananas',
|
||||
conversationId: conversation.id,
|
||||
|
@ -84,12 +87,9 @@ describe('Conversations', () => {
|
|||
await DataWriter.saveMessage(message.attributes, {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
message = window.MessageCache.__DEPRECATED$register(
|
||||
message.id,
|
||||
message,
|
||||
'test'
|
||||
);
|
||||
message = window.MessageCache.register(message);
|
||||
await DataWriter.updateConversation(conversation.attributes);
|
||||
await conversation.updateLastMessage();
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import type { AttachmentType } from '../../types/Attachment';
|
|||
import type { CallbackResultType } from '../../textsecure/Types.d';
|
||||
import type { ConversationModel } from '../../models/conversations';
|
||||
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 { StorageAccessType } from '../../types/Storage.d';
|
||||
import type { WebAPIType } from '../../textsecure/WebAPI';
|
||||
|
@ -30,6 +30,9 @@ import {
|
|||
TEXT_ATTACHMENT,
|
||||
VIDEO_MP4,
|
||||
} from '../../types/MIME';
|
||||
import { getNotificationDataForMessage } from '../../util/getNotificationDataForMessage';
|
||||
import { getNotificationTextForMessage } from '../../util/getNotificationTextForMessage';
|
||||
import { send } from '../../messages/send';
|
||||
|
||||
describe('Message', () => {
|
||||
const STORAGE_KEYS_TO_RESTORE: Array<keyof StorageAccessType> = [
|
||||
|
@ -54,17 +57,22 @@ describe('Message', () => {
|
|||
const ourServiceId = generateAci();
|
||||
|
||||
function createMessage(attrs: Partial<MessageAttributesType>): MessageModel {
|
||||
return new window.Whisper.Message({
|
||||
id: generateUuid(),
|
||||
const id = generateUuid();
|
||||
return window.MessageCache.register(
|
||||
new MessageModel({
|
||||
id,
|
||||
...attrs,
|
||||
sent_at: Date.now(),
|
||||
received_at: Date.now(),
|
||||
} as MessageAttributesType);
|
||||
} as MessageAttributesType)
|
||||
);
|
||||
}
|
||||
|
||||
function createMessageAndGetNotificationData(attrs: {
|
||||
[key: string]: unknown;
|
||||
}) {
|
||||
return createMessage(attrs).getNotificationData();
|
||||
const message = createMessage(attrs);
|
||||
return getNotificationDataForMessage(message.attributes);
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
|
@ -184,7 +192,7 @@ describe('Message', () => {
|
|||
editMessage: undefined,
|
||||
});
|
||||
|
||||
await message.send({
|
||||
await send(message, {
|
||||
promise,
|
||||
targetTimestamp: message.get('timestamp'),
|
||||
});
|
||||
|
@ -207,7 +215,7 @@ describe('Message', () => {
|
|||
const message = createMessage({ type: 'outgoing', source });
|
||||
|
||||
const promise = Promise.reject(new Error('foo bar'));
|
||||
await message.send({
|
||||
await send(message, {
|
||||
promise,
|
||||
targetTimestamp: message.get('timestamp'),
|
||||
});
|
||||
|
@ -224,7 +232,7 @@ describe('Message', () => {
|
|||
errors: [new Error('baz qux')],
|
||||
};
|
||||
const promise = Promise.reject(result);
|
||||
await message.send({
|
||||
await send(message, {
|
||||
promise,
|
||||
targetTimestamp: message.get('timestamp'),
|
||||
});
|
||||
|
@ -675,8 +683,7 @@ describe('Message', () => {
|
|||
|
||||
describe('getNotificationText', () => {
|
||||
it("returns a notification's text", async () => {
|
||||
assert.strictEqual(
|
||||
createMessage({
|
||||
const message = createMessage({
|
||||
conversationId: (
|
||||
await window.ConversationController.getOrCreateAndWait(
|
||||
generateUuid(),
|
||||
|
@ -686,7 +693,10 @@ describe('Message', () => {
|
|||
type: 'incoming',
|
||||
source,
|
||||
body: 'hello world',
|
||||
}).getNotificationText(),
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
getNotificationTextForMessage(message.attributes),
|
||||
'hello world'
|
||||
);
|
||||
});
|
||||
|
@ -698,9 +708,7 @@ describe('Message', () => {
|
|||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
createMessage({
|
||||
const message = createMessage({
|
||||
conversationId: (
|
||||
await window.ConversationController.getOrCreateAndWait(
|
||||
generateUuid(),
|
||||
|
@ -715,7 +723,9 @@ describe('Message', () => {
|
|||
size: 0,
|
||||
},
|
||||
],
|
||||
}).getNotificationText(),
|
||||
});
|
||||
assert.strictEqual(
|
||||
getNotificationTextForMessage(message.attributes),
|
||||
'📷 Photo'
|
||||
);
|
||||
});
|
||||
|
@ -728,8 +738,7 @@ describe('Message', () => {
|
|||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
createMessage({
|
||||
const message = createMessage({
|
||||
conversationId: (
|
||||
await window.ConversationController.getOrCreateAndWait(
|
||||
generateUuid(),
|
||||
|
@ -744,7 +753,10 @@ describe('Message', () => {
|
|||
size: 0,
|
||||
},
|
||||
],
|
||||
}).getNotificationText(),
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
getNotificationTextForMessage(message.attributes),
|
||||
'Photo'
|
||||
);
|
||||
});
|
||||
|
|
|
@ -22,6 +22,7 @@ import { type AttachmentType, AttachmentVariant } from '../../types/Attachment';
|
|||
import { strictAssert } from '../../util/assert';
|
||||
import { AttachmentDownloadSource } from '../../sql/Interface';
|
||||
import { getAttachmentCiphertextLength } from '../../AttachmentCrypto';
|
||||
import { postSaveUpdates } from '../../util/cleanup';
|
||||
|
||||
function composeJob({
|
||||
messageId,
|
||||
|
@ -119,6 +120,7 @@ describe('AttachmentDownloadManager/JobManager', () => {
|
|||
{
|
||||
ourAci: 'ourAci' as AciString,
|
||||
forceSave: true,
|
||||
postSaveUpdates,
|
||||
}
|
||||
);
|
||||
await downloadManager?.addJob({
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
|
||||
import { assert } from 'chai';
|
||||
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 { strictAssert } from '../../util/assert';
|
||||
|
||||
|
@ -56,25 +54,33 @@ describe('MessageCache', () => {
|
|||
type: 'outgoing',
|
||||
});
|
||||
|
||||
message1 = mc.__DEPRECATED$register(message1.id, message1, 'test');
|
||||
message2 = mc.__DEPRECATED$register(message2.id, message2, 'test');
|
||||
message1 = mc.register(message1);
|
||||
message2 = mc.register(message2);
|
||||
// We deliberately register this message twice for testing.
|
||||
message2 = mc.__DEPRECATED$register(message2.id, message2, 'test');
|
||||
mc.__DEPRECATED$register(message3.id, message3, 'test');
|
||||
message2 = mc.register(message2);
|
||||
mc.register(message3);
|
||||
|
||||
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);
|
||||
|
||||
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', () => {
|
||||
const message1 = new MessageModel({
|
||||
conversationId: 'xyz',
|
||||
|
@ -85,65 +91,33 @@ describe('MessageCache', () => {
|
|||
timestamp: Date.now(),
|
||||
type: 'outgoing',
|
||||
});
|
||||
const messageFromController = window.MessageCache.__DEPRECATED$register(
|
||||
message1.id,
|
||||
message1,
|
||||
'test'
|
||||
);
|
||||
const messageFromController = window.MessageCache.register(message1);
|
||||
|
||||
assert.strictEqual(
|
||||
message1,
|
||||
messageFromController,
|
||||
'same objects from mc.__DEPRECATED$register'
|
||||
'same objects from mc.register'
|
||||
);
|
||||
|
||||
const messageById = window.MessageCache.__DEPRECATED$getById(
|
||||
message1.id,
|
||||
'test'
|
||||
const messageInCache = window.MessageCache.getById(message1.id);
|
||||
assert.strictEqual(
|
||||
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(
|
||||
message1.attributes,
|
||||
messageInCache,
|
||||
messageInCache?.attributes,
|
||||
'same attributes as in cache'
|
||||
);
|
||||
|
||||
message1.set({ body: 'test2' });
|
||||
assert.equal(message1.attributes.body, 'test2', 'message model updated');
|
||||
assert.equal(
|
||||
messageById?.attributes.body,
|
||||
messageInCache?.attributes.body,
|
||||
'test2',
|
||||
'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)', () => {
|
||||
|
@ -157,271 +131,28 @@ describe('MessageCache', () => {
|
|||
type: 'outgoing',
|
||||
});
|
||||
|
||||
window.MessageCache.toMessageAttributes(message.attributes);
|
||||
const messageFromController = window.MessageCache.register(message);
|
||||
|
||||
const messageFromController = window.MessageCache.__DEPRECATED$register(
|
||||
message.id,
|
||||
message,
|
||||
'test'
|
||||
);
|
||||
|
||||
assert.notStrictEqual(
|
||||
assert.strictEqual(
|
||||
message,
|
||||
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(
|
||||
message.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(
|
||||
message.get('body'),
|
||||
messageFromController.get('body'),
|
||||
'new model is not equal to old model'
|
||||
);
|
||||
|
||||
const messageInCache = window.MessageCache.accessAttributes(message.id);
|
||||
const messageInCache = window.MessageCache.getById(message.id);
|
||||
strictAssert(messageInCache, 'no message found');
|
||||
assert.equal(
|
||||
messageFromController.get('body'),
|
||||
messageInCache.body,
|
||||
messageInCache.get('body'),
|
||||
'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());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import { generateAci } from '../../types/ServiceId';
|
|||
import { DurationInSeconds } from '../../util/durations';
|
||||
|
||||
import type { MessageAttributesType } from '../../model-types.d';
|
||||
import { postSaveUpdates } from '../../util/cleanup';
|
||||
|
||||
const { _getAllMessages, getConversationMessageStats } = DataReader;
|
||||
const { removeAll, saveMessages } = DataWriter;
|
||||
|
@ -56,6 +57,7 @@ describe('sql/conversationSummary', () => {
|
|||
await saveMessages([message1, message2, message3], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 3);
|
||||
|
@ -109,6 +111,7 @@ describe('sql/conversationSummary', () => {
|
|||
await saveMessages([message1, message2, message3], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 3);
|
||||
|
@ -199,6 +202,7 @@ describe('sql/conversationSummary', () => {
|
|||
{
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -306,6 +310,7 @@ describe('sql/conversationSummary', () => {
|
|||
{
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -355,6 +360,7 @@ describe('sql/conversationSummary', () => {
|
|||
await saveMessages([message1, message2], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 2);
|
||||
|
@ -404,6 +410,7 @@ describe('sql/conversationSummary', () => {
|
|||
await saveMessages([message1, message2], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 2);
|
||||
|
@ -446,6 +453,7 @@ describe('sql/conversationSummary', () => {
|
|||
await saveMessages([message1, message2], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 2);
|
||||
|
@ -490,6 +498,7 @@ describe('sql/conversationSummary', () => {
|
|||
await saveMessages([message1, message2], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 2);
|
||||
|
@ -549,6 +558,7 @@ describe('sql/conversationSummary', () => {
|
|||
await saveMessages([message1, message2], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 2);
|
||||
|
|
|
@ -8,6 +8,7 @@ import { DataReader, DataWriter } from '../../sql/Client';
|
|||
import { generateAci } from '../../types/ServiceId';
|
||||
|
||||
import type { MessageAttributesType } from '../../model-types.d';
|
||||
import { postSaveUpdates } from '../../util/cleanup';
|
||||
|
||||
const { _getAllMessages, searchMessages } = DataReader;
|
||||
const { removeAll, saveMessages, saveMessage } = DataWriter;
|
||||
|
@ -54,6 +55,7 @@ describe('sql/searchMessages', () => {
|
|||
await saveMessages([message1, message2, message3], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 3);
|
||||
|
@ -63,7 +65,7 @@ describe('sql/searchMessages', () => {
|
|||
assert.strictEqual(searchResults[0].id, message2.id);
|
||||
|
||||
message3.body = 'message 3 - unique string';
|
||||
await saveMessage(message3, { ourAci });
|
||||
await saveMessage(message3, { ourAci, postSaveUpdates });
|
||||
|
||||
const searchResults2 = await searchMessages({ query: 'unique' });
|
||||
assert.lengthOf(searchResults2, 2);
|
||||
|
@ -110,6 +112,7 @@ describe('sql/searchMessages', () => {
|
|||
await saveMessages([message1, message2, message3], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 3);
|
||||
|
@ -119,7 +122,7 @@ describe('sql/searchMessages', () => {
|
|||
assert.strictEqual(searchResults[0].id, message1.id);
|
||||
|
||||
message1.body = 'message 3 - unique string';
|
||||
await saveMessage(message3, { ourAci });
|
||||
await saveMessage(message3, { ourAci, postSaveUpdates });
|
||||
|
||||
const searchResults2 = await searchMessages({ query: 'unique' });
|
||||
assert.lengthOf(searchResults2, 1);
|
||||
|
@ -165,6 +168,7 @@ describe('sql/searchMessages', () => {
|
|||
await saveMessages([message1, message2, message3], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 3);
|
||||
|
@ -174,7 +178,7 @@ describe('sql/searchMessages', () => {
|
|||
assert.strictEqual(searchResults[0].id, message1.id);
|
||||
|
||||
message1.body = 'message 3 - unique string';
|
||||
await saveMessage(message3, { ourAci });
|
||||
await saveMessage(message3, { ourAci, postSaveUpdates });
|
||||
|
||||
const searchResults2 = await searchMessages({ query: 'unique' });
|
||||
assert.lengthOf(searchResults2, 1);
|
||||
|
@ -211,6 +215,7 @@ describe('sql/searchMessages', () => {
|
|||
await saveMessages([message1, message2], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 2);
|
||||
|
@ -251,6 +256,7 @@ describe('sql/searchMessages/withMentions', () => {
|
|||
await saveMessages(messages, {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
return messages;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { DataReader, DataWriter } from '../../sql/Client';
|
|||
import { generateAci } from '../../types/ServiceId';
|
||||
|
||||
import type { MessageAttributesType } from '../../model-types.d';
|
||||
import { postSaveUpdates } from '../../util/cleanup';
|
||||
|
||||
const { _getAllMessages, getCallHistoryMessageByCallId } = DataReader;
|
||||
const { removeAll, saveMessages } = DataWriter;
|
||||
|
@ -37,6 +38,7 @@ describe('sql/getCallHistoryMessageByCallId', () => {
|
|||
await saveMessages([callHistoryMessage], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
const allMessages = await _getAllMessages();
|
||||
|
|
|
@ -8,6 +8,7 @@ import { generateAci } from '../../types/ServiceId';
|
|||
import { DataReader, DataWriter } from '../../sql/Client';
|
||||
|
||||
import type { MessageAttributesType } from '../../model-types';
|
||||
import { postSaveUpdates } from '../../util/cleanup';
|
||||
|
||||
const { _getAllMessages, getMessagesBetween } = DataReader;
|
||||
const { saveMessages, _removeAllMessages } = DataWriter;
|
||||
|
@ -45,6 +46,7 @@ describe('sql/getMessagesBetween', () => {
|
|||
await saveMessages([message1, message2, message3, message4, message5], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 5);
|
||||
|
@ -93,6 +95,7 @@ describe('sql/getMessagesBetween', () => {
|
|||
await saveMessages([message1, message2, message3, message5], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 4);
|
||||
|
|
|
@ -8,6 +8,7 @@ import { DataReader, DataWriter } from '../../sql/Client';
|
|||
import { generateAci } from '../../types/ServiceId';
|
||||
|
||||
import type { MessageAttributesType } from '../../model-types';
|
||||
import { postSaveUpdates } from '../../util/cleanup';
|
||||
|
||||
const { _getAllMessages, getNearbyMessageFromDeletedSet } = DataReader;
|
||||
const { saveMessages, _removeAllMessages } = DataWriter;
|
||||
|
@ -45,6 +46,7 @@ describe('sql/getNearbyMessageFromDeletedSet', () => {
|
|||
await saveMessages([message1, message2, message3, message4, message5], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 5);
|
||||
|
|
|
@ -8,6 +8,7 @@ import { DataReader, DataWriter } from '../../sql/Client';
|
|||
import { generateAci } from '../../types/ServiceId';
|
||||
|
||||
import type { MessageAttributesType } from '../../model-types.d';
|
||||
import { postSaveUpdates } from '../../util/cleanup';
|
||||
|
||||
const { _getAllMessages, getRecentStoryReplies } = DataReader;
|
||||
const { removeAll, saveMessages } = DataWriter;
|
||||
|
@ -91,6 +92,7 @@ describe('sql/getRecentStoryReplies', () => {
|
|||
{
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import { ReactionReadStatus } from '../../types/Reactions';
|
|||
import { DurationInSeconds } from '../../util/durations';
|
||||
import type { MessageAttributesType } from '../../model-types.d';
|
||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||
import { postSaveUpdates } from '../../util/cleanup';
|
||||
|
||||
const { _getAllReactions, _getAllMessages, getTotalUnreadForConversation } =
|
||||
DataReader;
|
||||
|
@ -126,6 +127,7 @@ describe('sql/markRead', () => {
|
|||
{
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -290,6 +292,7 @@ describe('sql/markRead', () => {
|
|||
{
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -392,6 +395,7 @@ describe('sql/markRead', () => {
|
|||
await saveMessages([message1, message2, message3, message4, message5], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
|
@ -518,6 +522,7 @@ describe('sql/markRead', () => {
|
|||
{
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
}
|
||||
);
|
||||
assert.lengthOf(await _getAllMessages(), pad.length + 5);
|
||||
|
@ -673,6 +678,7 @@ describe('sql/markRead', () => {
|
|||
await saveMessages([message1, message2, message3, message4, message5], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
assert.lengthOf(await _getAllMessages(), 5);
|
||||
|
||||
|
@ -823,6 +829,7 @@ describe('sql/markRead', () => {
|
|||
await saveMessages([message1, message2, message3, message4], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 4);
|
||||
|
|
|
@ -7,7 +7,7 @@ import { v4 as generateUuid } from 'uuid';
|
|||
import { DataReader, DataWriter } from '../../sql/Client';
|
||||
import { generateAci } from '../../types/ServiceId';
|
||||
import { constantTimeEqual, getRandomBytes } from '../../Crypto';
|
||||
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
|
||||
import { cleanupMessages, postSaveUpdates } from '../../util/cleanup';
|
||||
|
||||
const {
|
||||
_getAllSentProtoMessageIds,
|
||||
|
@ -128,7 +128,7 @@ describe('sql/sendLog', () => {
|
|||
timestamp,
|
||||
type: 'outgoing',
|
||||
},
|
||||
{ forceSave: true, ourAci }
|
||||
{ forceSave: true, ourAci, postSaveUpdates }
|
||||
);
|
||||
|
||||
const bytes = getRandomBytes(128);
|
||||
|
@ -152,7 +152,7 @@ describe('sql/sendLog', () => {
|
|||
|
||||
assert.strictEqual(actual.timestamp, proto.timestamp);
|
||||
|
||||
await removeMessage(id, { singleProtoJobQueue });
|
||||
await removeMessage(id, { cleanupMessages });
|
||||
|
||||
assert.lengthOf(await getAllSentProtos(), 0);
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@ import { DataReader, DataWriter } from '../../sql/Client';
|
|||
import { generateAci } from '../../types/ServiceId';
|
||||
|
||||
import type { MessageAttributesType } from '../../model-types.d';
|
||||
import { postSaveUpdates } from '../../util/cleanup';
|
||||
|
||||
const { _getAllMessages, getAllStories } = DataReader;
|
||||
const { removeAll, saveMessages } = DataWriter;
|
||||
|
@ -80,6 +81,7 @@ describe('sql/stories', () => {
|
|||
await saveMessages([story1, story2, story3, story4, story5], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 5);
|
||||
|
@ -217,6 +219,7 @@ describe('sql/stories', () => {
|
|||
{
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { generateAci } from '../../types/ServiceId';
|
|||
|
||||
import type { MessageAttributesType } from '../../model-types.d';
|
||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||
import { postSaveUpdates } from '../../util/cleanup';
|
||||
|
||||
const {
|
||||
_getAllMessages,
|
||||
|
@ -86,6 +87,7 @@ describe('sql/timelineFetches', () => {
|
|||
await saveMessages([message1, message2, message3, message4, message5], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 5);
|
||||
|
@ -144,6 +146,7 @@ describe('sql/timelineFetches', () => {
|
|||
await saveMessages([message1, message2, message3], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 3);
|
||||
|
@ -199,6 +202,7 @@ describe('sql/timelineFetches', () => {
|
|||
await saveMessages([message1, message2, message3], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 3);
|
||||
|
@ -251,6 +255,7 @@ describe('sql/timelineFetches', () => {
|
|||
await saveMessages([message1, message2, message3], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 3);
|
||||
|
@ -305,6 +310,7 @@ describe('sql/timelineFetches', () => {
|
|||
await saveMessages([message1, message2, message3], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 3);
|
||||
|
@ -363,6 +369,7 @@ describe('sql/timelineFetches', () => {
|
|||
await saveMessages([message1, message2, message3], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 3);
|
||||
|
@ -442,6 +449,7 @@ describe('sql/timelineFetches', () => {
|
|||
await saveMessages([message1, message2, message3, message4, message5], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 5);
|
||||
|
@ -499,6 +507,7 @@ describe('sql/timelineFetches', () => {
|
|||
await saveMessages([message1, message2, message3], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 3);
|
||||
|
@ -552,6 +561,7 @@ describe('sql/timelineFetches', () => {
|
|||
await saveMessages([message1, message2, message3], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 3);
|
||||
|
@ -608,6 +618,7 @@ describe('sql/timelineFetches', () => {
|
|||
await saveMessages([message1, message2, message3], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 3);
|
||||
|
@ -662,6 +673,7 @@ describe('sql/timelineFetches', () => {
|
|||
await saveMessages([message1, message2, message3], {
|
||||
forceSave: true,
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
assert.lengthOf(await _getAllMessages(), 3);
|
||||
|
@ -781,7 +793,7 @@ describe('sql/timelineFetches', () => {
|
|||
newestInStory,
|
||||
newest,
|
||||
],
|
||||
{ forceSave: true, ourAci }
|
||||
{ forceSave: true, ourAci, postSaveUpdates }
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
|
|
|
@ -27,6 +27,8 @@ import { actions, getEmptyState } from '../../../state/ducks/stories';
|
|||
import { noopAction } from '../../../state/ducks/noop';
|
||||
import { reducer as rootReducer } from '../../../state/reducer';
|
||||
import { dropNull } from '../../../util/dropNull';
|
||||
import { postSaveUpdates } from '../../../util/cleanup';
|
||||
import { MessageModel } from '../../../models/messages';
|
||||
|
||||
describe('both/state/ducks/stories', () => {
|
||||
const getEmptyRootState = () => ({
|
||||
|
@ -862,11 +864,7 @@ describe('both/state/ducks/stories', () => {
|
|||
const storyId = generateUuid();
|
||||
const messageAttributes = getStoryMessage(storyId);
|
||||
|
||||
window.MessageCache.__DEPRECATED$register(
|
||||
storyId,
|
||||
messageAttributes,
|
||||
'test'
|
||||
);
|
||||
window.MessageCache.register(new MessageModel(messageAttributes));
|
||||
|
||||
const dispatch = sinon.spy();
|
||||
await queueStoryDownload(storyId)(dispatch, getEmptyRootState, null);
|
||||
|
@ -888,11 +886,7 @@ describe('both/state/ducks/stories', () => {
|
|||
],
|
||||
};
|
||||
|
||||
window.MessageCache.__DEPRECATED$register(
|
||||
storyId,
|
||||
messageAttributes,
|
||||
'test'
|
||||
);
|
||||
window.MessageCache.register(new MessageModel(messageAttributes));
|
||||
|
||||
const dispatch = sinon.spy();
|
||||
await queueStoryDownload(storyId)(dispatch, getEmptyRootState, null);
|
||||
|
@ -914,11 +908,7 @@ describe('both/state/ducks/stories', () => {
|
|||
],
|
||||
};
|
||||
|
||||
window.MessageCache.__DEPRECATED$register(
|
||||
storyId,
|
||||
messageAttributes,
|
||||
'test'
|
||||
);
|
||||
window.MessageCache.register(new MessageModel(messageAttributes));
|
||||
|
||||
const dispatch = sinon.spy();
|
||||
await queueStoryDownload(storyId)(dispatch, getEmptyRootState, null);
|
||||
|
@ -941,6 +931,7 @@ describe('both/state/ducks/stories', () => {
|
|||
await DataWriter.saveMessage(messageAttributes, {
|
||||
forceSave: true,
|
||||
ourAci: generateAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
const rootState = getEmptyRootState();
|
||||
|
||||
|
@ -963,11 +954,7 @@ describe('both/state/ducks/stories', () => {
|
|||
},
|
||||
});
|
||||
|
||||
window.MessageCache.__DEPRECATED$register(
|
||||
storyId,
|
||||
messageAttributes,
|
||||
'test'
|
||||
);
|
||||
window.MessageCache.register(new MessageModel(messageAttributes));
|
||||
|
||||
const dispatch = sinon.spy();
|
||||
await queueStoryDownload(storyId)(dispatch, getState, null);
|
||||
|
@ -1007,6 +994,7 @@ describe('both/state/ducks/stories', () => {
|
|||
await DataWriter.saveMessage(messageAttributes, {
|
||||
forceSave: true,
|
||||
ourAci: generateAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
const rootState = getEmptyRootState();
|
||||
|
||||
|
@ -1029,11 +1017,7 @@ describe('both/state/ducks/stories', () => {
|
|||
},
|
||||
});
|
||||
|
||||
window.MessageCache.__DEPRECATED$register(
|
||||
storyId,
|
||||
messageAttributes,
|
||||
'test'
|
||||
);
|
||||
window.MessageCache.register(new MessageModel(messageAttributes));
|
||||
|
||||
const dispatch = sinon.spy();
|
||||
await queueStoryDownload(storyId)(dispatch, getState, null);
|
||||
|
|
|
@ -12,7 +12,7 @@ import { SignalProtocolStore } from '../../SignalProtocolStore';
|
|||
import type { ConversationModel } from '../../models/conversations';
|
||||
import * as KeyChangeListener from '../../textsecure/KeyChangeListener';
|
||||
import * as Bytes from '../../Bytes';
|
||||
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
|
||||
import { cleanupMessages } from '../../util/cleanup';
|
||||
|
||||
describe('KeyChangeListener', () => {
|
||||
let oldNumberId: string | undefined;
|
||||
|
@ -71,7 +71,7 @@ describe('KeyChangeListener', () => {
|
|||
afterEach(async () => {
|
||||
await DataWriter.removeMessagesInConversation(convo.id, {
|
||||
logId: ourServiceIdWithKeyChange,
|
||||
singleProtoJobQueue,
|
||||
cleanupMessages,
|
||||
});
|
||||
await DataWriter.removeConversation(convo.id);
|
||||
|
||||
|
@ -109,7 +109,7 @@ describe('KeyChangeListener', () => {
|
|||
afterEach(async () => {
|
||||
await DataWriter.removeMessagesInConversation(groupConvo.id, {
|
||||
logId: ourServiceIdWithKeyChange,
|
||||
singleProtoJobQueue,
|
||||
cleanupMessages,
|
||||
});
|
||||
await DataWriter.removeConversation(groupConvo.id);
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@ import { _migrateMessageData as migrateMessageData } from '../../messages/migrat
|
|||
import type { MessageAttributesType } from '../../model-types';
|
||||
import { DataReader, DataWriter } from '../../sql/Client';
|
||||
import { generateAci } from '../../types/ServiceId';
|
||||
import { postSaveUpdates } from '../../util/cleanup';
|
||||
|
||||
function composeMessage(timestamp: number): MessageAttributesType {
|
||||
return {
|
||||
|
@ -39,6 +40,7 @@ describe('utils/migrateMessageData', async () => {
|
|||
await DataWriter.saveMessages(messages, {
|
||||
forceSave: true,
|
||||
ourAci: generateAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
const result = await migrateMessageData({
|
||||
|
|
|
@ -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 window = await app.getWindow();
|
||||
|
|
159
ts/test-node/util/messageFailures.ts
Normal file
159
ts/test-node/util/messageFailures.ts
Normal 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
|
||||
);
|
||||
}
|
|
@ -26,9 +26,9 @@ import { getLocalAttachmentUrl } from '../util/getLocalAttachmentUrl';
|
|||
|
||||
type GenericEmbeddedContactType<AvatarType> = {
|
||||
name?: Name;
|
||||
number?: Array<Phone>;
|
||||
email?: Array<Email>;
|
||||
address?: Array<PostalAddress>;
|
||||
number?: ReadonlyArray<Phone>;
|
||||
email?: ReadonlyArray<Email>;
|
||||
address?: ReadonlyArray<PostalAddress>;
|
||||
avatar?: AvatarType;
|
||||
organization?: string;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -2,10 +2,17 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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 MIME from '../types/MIME';
|
||||
|
||||
import { DataWriter } from '../sql/Client';
|
||||
import { isMoreRecentThan } from './timestamp';
|
||||
import { isNotNil } from './isNotNil';
|
||||
import { queueAttachmentDownloadsForMessage } from './queueAttachmentDownloads';
|
||||
import { postSaveUpdates } from './cleanup';
|
||||
|
||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||
const MAX_ATTACHMENT_MSGS_TO_DOWNLOAD = 250;
|
||||
|
@ -66,10 +73,7 @@ export async function flushAttachmentDownloadQueue(): Promise<void> {
|
|||
let numMessagesQueued = 0;
|
||||
await Promise.all(
|
||||
messageIdsToDownload.map(async messageId => {
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
messageId,
|
||||
'flushAttachmentDownloadQueue'
|
||||
);
|
||||
const message = window.MessageCache.getById(messageId);
|
||||
if (!message) {
|
||||
log.warn(
|
||||
'attachmentDownloadQueue: message not found in messageCache, maybe it was deleted?'
|
||||
|
@ -79,14 +83,14 @@ export async function flushAttachmentDownloadQueue(): Promise<void> {
|
|||
|
||||
if (
|
||||
isMoreRecentThan(
|
||||
message.getReceivedAt(),
|
||||
message.get('received_at_ms') || message.get('received_at'),
|
||||
MAX_ATTACHMENT_DOWNLOAD_AGE
|
||||
) ||
|
||||
// Stickers and long text attachments has to be downloaded for UI
|
||||
// to display the message properly.
|
||||
message.hasRequiredAttachmentDownloads()
|
||||
hasRequiredAttachmentDownloads(message.attributes)
|
||||
) {
|
||||
const shouldSave = await message.queueAttachmentDownloads();
|
||||
const shouldSave = await queueAttachmentDownloadsForMessage(message);
|
||||
if (shouldSave) {
|
||||
messageIdsToSave.push(messageId);
|
||||
}
|
||||
|
@ -101,13 +105,35 @@ export async function flushAttachmentDownloadQueue(): Promise<void> {
|
|||
);
|
||||
|
||||
const messagesToSave = messageIdsToSave
|
||||
.map(messageId => window.MessageCache.accessAttributes(messageId))
|
||||
.map(messageId => window.MessageCache.getById(messageId)?.attributes)
|
||||
.filter(isNotNil);
|
||||
|
||||
await DataWriter.saveMessages(messagesToSave, {
|
||||
ourAci: window.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
attachmentDownloadQueue = undefined;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -46,10 +46,7 @@ import { incrementMessageCounter } from './incrementMessageCounter';
|
|||
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||
import { SeenStatus, maxSeenStatus } from '../MessageSeenStatus';
|
||||
import { canConversationBeUnarchived } from './canConversationBeUnarchived';
|
||||
import type {
|
||||
ConversationAttributesType,
|
||||
MessageAttributesType,
|
||||
} from '../model-types';
|
||||
import type { ConversationAttributesType } from '../model-types';
|
||||
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||
import MessageSender from '../textsecure/SendMessage';
|
||||
import * as Bytes from '../Bytes';
|
||||
|
@ -71,6 +68,8 @@ import { storageServiceUploadJob } from '../services/storage';
|
|||
import { CallLinkFinalizeDeleteManager } from '../jobs/CallLinkFinalizeDeleteManager';
|
||||
import { parsePartial, parseStrict } from './schemas';
|
||||
import { calling } from '../services/calling';
|
||||
import { cleanupMessages } from './cleanup';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
// utils
|
||||
// -----
|
||||
|
@ -1192,7 +1191,7 @@ async function saveCallHistory({
|
|||
if (prevMessage != null) {
|
||||
await DataWriter.removeMessage(prevMessage.id, {
|
||||
fromSync: true,
|
||||
singleProtoJobQueue,
|
||||
cleanupMessages,
|
||||
});
|
||||
}
|
||||
return callHistory;
|
||||
|
@ -1222,7 +1221,7 @@ async function saveCallHistory({
|
|||
|
||||
const { id: newId } = generateMessageId(counter);
|
||||
|
||||
const message: MessageAttributesType = {
|
||||
const message = new MessageModel({
|
||||
id: prevMessage?.id ?? newId,
|
||||
conversationId: conversation.id,
|
||||
type: 'call-history',
|
||||
|
@ -1234,20 +1233,15 @@ async function saveCallHistory({
|
|||
readStatus: ReadStatus.Read,
|
||||
seenStatus,
|
||||
callId: callHistory.callId,
|
||||
};
|
||||
});
|
||||
|
||||
message.id = await DataWriter.saveMessage(message, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
// We don't want to force save if we're updating an existing message
|
||||
const id = await window.MessageCache.saveMessage(message, {
|
||||
forceSave: prevMessage == null,
|
||||
});
|
||||
message.set({ id });
|
||||
log.info('saveCallHistory: Saved call history message:', message.id);
|
||||
|
||||
window.MessageCache.__DEPRECATED$register(
|
||||
message.id,
|
||||
message,
|
||||
'callDisposition'
|
||||
);
|
||||
const model = window.MessageCache.register(message);
|
||||
|
||||
if (prevMessage == null) {
|
||||
if (callHistory.direction === CallDirection.Outgoing) {
|
||||
|
@ -1255,7 +1249,7 @@ async function saveCallHistory({
|
|||
} else {
|
||||
conversation.incrementMessageCount();
|
||||
}
|
||||
conversation.trigger('newmessage', message);
|
||||
drop(conversation.onNewMessage(model));
|
||||
}
|
||||
|
||||
await conversation.updateLastMessage().catch(error => {
|
||||
|
@ -1356,11 +1350,10 @@ export async function updateCallHistoryFromLocalEvent(
|
|||
|
||||
export function updateDeletedMessages(messageIds: ReadonlyArray<string>): void {
|
||||
messageIds.forEach(messageId => {
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
messageId,
|
||||
'updateDeletedMessages'
|
||||
const message = window.MessageCache.getById(messageId);
|
||||
const conversation = window.ConversationController.get(
|
||||
message?.get('conversationId')
|
||||
);
|
||||
const conversation = message?.getConversation();
|
||||
if (message == null || conversation == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -1369,7 +1362,7 @@ export function updateDeletedMessages(messageIds: ReadonlyArray<string>): void {
|
|||
message.get('conversationId')
|
||||
);
|
||||
conversation.debouncedUpdateLastMessage();
|
||||
window.MessageCache.__DEPRECATED$unregister(messageId);
|
||||
window.MessageCache.unregister(messageId);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -5,11 +5,15 @@ import PQueue from 'p-queue';
|
|||
import { batch } from 'react-redux';
|
||||
|
||||
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 { isStory } from '../messages/helpers';
|
||||
import { isDirectConversation } from './whatTypeOfConversation';
|
||||
import * as log from '../logging/log';
|
||||
import { getCallHistorySelector } from '../state/selectors/callHistory';
|
||||
import {
|
||||
DirectCallStatus,
|
||||
|
@ -17,20 +21,71 @@ import {
|
|||
AdhocCallStatus,
|
||||
} from '../types/CallDisposition';
|
||||
import { getMessageIdForLogging } from './idForLogging';
|
||||
import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||
import { MINUTE } from './durations';
|
||||
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(
|
||||
messages: ReadonlyArray<MessageAttributesType>,
|
||||
{
|
||||
fromSync,
|
||||
markCallHistoryDeleted,
|
||||
singleProtoJobQueue,
|
||||
}: {
|
||||
fromSync?: boolean;
|
||||
markCallHistoryDeleted: (callId: string) => Promise<void>;
|
||||
singleProtoJobQueue: SingleProtoJobQueue;
|
||||
}
|
||||
): Promise<void> {
|
||||
// First, handle any calls that need to be deleted
|
||||
|
@ -40,8 +95,6 @@ export async function cleanupMessages(
|
|||
messages.map((message: MessageAttributesType) => async () => {
|
||||
await maybeDeleteCall(message, {
|
||||
fromSync,
|
||||
markCallHistoryDeleted,
|
||||
singleProtoJobQueue,
|
||||
});
|
||||
})
|
||||
)
|
||||
|
@ -76,7 +129,7 @@ export function cleanupMessageFromMemory(message: MessageAttributesType): void {
|
|||
const parentConversation = window.ConversationController.get(conversationId);
|
||||
parentConversation?.debouncedUpdateLastMessage();
|
||||
|
||||
window.MessageCache.__DEPRECATED$unregister(id);
|
||||
window.MessageCache.unregister(id);
|
||||
}
|
||||
|
||||
async function cleanupStoryReplies(
|
||||
|
@ -120,24 +173,18 @@ async function cleanupStoryReplies(
|
|||
// Cleanup all group replies
|
||||
await Promise.all(
|
||||
replies.map(reply => {
|
||||
const replyMessageModel = window.MessageCache.__DEPRECATED$register(
|
||||
reply.id,
|
||||
reply,
|
||||
'cleanupStoryReplies/group'
|
||||
const replyMessageModel = window.MessageCache.register(
|
||||
new MessageModel(reply)
|
||||
);
|
||||
return replyMessageModel.eraseContents();
|
||||
return eraseMessageContents(replyMessageModel);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Refresh the storyReplyContext data for 1:1 conversations
|
||||
await Promise.all(
|
||||
replies.map(async reply => {
|
||||
const model = window.MessageCache.__DEPRECATED$register(
|
||||
reply.id,
|
||||
reply,
|
||||
'cleanupStoryReplies/1:1'
|
||||
);
|
||||
await model.hydrateStoryContext(story, {
|
||||
const model = window.MessageCache.register(new MessageModel(reply));
|
||||
await hydrateStoryContext(model.id, story, {
|
||||
shouldSave: true,
|
||||
isStoryErased: true,
|
||||
});
|
||||
|
@ -175,12 +222,8 @@ export async function maybeDeleteCall(
|
|||
message: MessageAttributesType,
|
||||
{
|
||||
fromSync,
|
||||
markCallHistoryDeleted,
|
||||
singleProtoJobQueue,
|
||||
}: {
|
||||
fromSync?: boolean;
|
||||
markCallHistoryDeleted: (callId: string) => Promise<void>;
|
||||
singleProtoJobQueue: SingleProtoJobQueue;
|
||||
}
|
||||
): Promise<void> {
|
||||
const { callId } = message;
|
||||
|
@ -214,6 +257,6 @@ export async function maybeDeleteCall(
|
|||
window.textsecure.MessageSender.getDeleteCallEvent(callHistory)
|
||||
);
|
||||
}
|
||||
await markCallHistoryDeleted(callId);
|
||||
await DataWriter.markCallHistoryDeleted(callId);
|
||||
window.reduxActions.callHistory.removeCallHistory(callId);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,9 @@ import { isMe } from './whatTypeOfConversation';
|
|||
import { getAuthorId } from '../messages/helpers';
|
||||
import { isStory } from '../state/selectors/message';
|
||||
import { isTooOldToModifyMessage } from './isTooOldToModifyMessage';
|
||||
import { drop } from './drop';
|
||||
import { eraseMessageContents } from './cleanup';
|
||||
import { notificationService } from '../services/notifications';
|
||||
|
||||
export async function deleteForEveryone(
|
||||
message: MessageModel,
|
||||
|
@ -18,7 +21,9 @@ export async function deleteForEveryone(
|
|||
shouldPersist = true
|
||||
): Promise<void> {
|
||||
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
|
||||
if (
|
||||
|
@ -29,7 +34,7 @@ export async function deleteForEveryone(
|
|||
return;
|
||||
}
|
||||
|
||||
await message.handleDeleteForEveryone(doe, shouldPersist);
|
||||
await handleDeleteForEveryone(message, doe, shouldPersist);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -44,7 +49,7 @@ export async function deleteForEveryone(
|
|||
return;
|
||||
}
|
||||
|
||||
await message.handleDeleteForEveryone(doe, shouldPersist);
|
||||
await handleDeleteForEveryone(message, doe, shouldPersist);
|
||||
}
|
||||
|
||||
function isDeletionByMe(
|
||||
|
@ -58,3 +63,49 @@ function isDeletionByMe(
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import { missingCaseError } from './missingCaseError';
|
|||
import { getMessageSentTimestampSet } from './getMessageSentTimestampSet';
|
||||
import { getAuthor } from '../messages/helpers';
|
||||
import { isPniString } from '../types/ServiceId';
|
||||
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||
import { DataReader, DataWriter, deleteAndCleanup } from '../sql/Client';
|
||||
import { deleteData } from '../types/Attachment';
|
||||
|
||||
|
@ -29,7 +28,8 @@ import type {
|
|||
} from '../textsecure/messageReceiverEvents';
|
||||
import type { AciString, PniString } from '../types/ServiceId';
|
||||
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;
|
||||
|
||||
|
@ -98,8 +98,8 @@ export async function deleteMessage(
|
|||
return false;
|
||||
}
|
||||
|
||||
const message = window.MessageCache.toMessageAttributes(found);
|
||||
await applyDeleteMessage(message, logId);
|
||||
const message = window.MessageCache.register(new MessageModel(found));
|
||||
await applyDeleteMessage(message.attributes, logId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ export async function applyDeleteMessage(
|
|||
): Promise<void> {
|
||||
await deleteAndCleanup([message], logId, {
|
||||
fromSync: true,
|
||||
singleProtoJobQueue,
|
||||
cleanupMessages,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -141,11 +141,7 @@ export async function deleteAttachmentFromMessage(
|
|||
return false;
|
||||
}
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$register(
|
||||
found.id,
|
||||
found,
|
||||
'ReadSyncs.onSync'
|
||||
);
|
||||
const message = window.MessageCache.register(new MessageModel(found));
|
||||
|
||||
return applyDeleteAttachmentFromMessage(message, deleteAttachmentData, {
|
||||
deleteOnDisk,
|
||||
|
@ -209,7 +205,7 @@ export async function applyDeleteAttachmentFromMessage(
|
|||
attachments: attachments?.filter(item => item !== attachment),
|
||||
});
|
||||
if (shouldSave) {
|
||||
await saveMessage(message.attributes, { ourAci });
|
||||
await saveMessage(message.attributes, { ourAci, postSaveUpdates });
|
||||
}
|
||||
await deleteData({ deleteOnDisk, deleteDownloadOnDisk })(attachment);
|
||||
|
||||
|
@ -291,10 +287,10 @@ export async function deleteConversation(
|
|||
const { received_at: receivedAt } = newestMessage;
|
||||
|
||||
await removeMessagesInConversation(conversation.id, {
|
||||
cleanupMessages,
|
||||
fromSync: true,
|
||||
receivedAt,
|
||||
logId: `${logId}(receivedAt=${receivedAt})`,
|
||||
singleProtoJobQueue,
|
||||
receivedAt,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -315,10 +311,10 @@ export async function deleteConversation(
|
|||
const { received_at: receivedAt } = newestNondisappearingMessage;
|
||||
|
||||
await removeMessagesInConversation(conversation.id, {
|
||||
cleanupMessages,
|
||||
fromSync: true,
|
||||
receivedAt,
|
||||
logId: `${logId}(receivedAt=${receivedAt})`,
|
||||
singleProtoJobQueue,
|
||||
receivedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,16 +3,13 @@
|
|||
|
||||
import { DAY } from './durations';
|
||||
import { sendDeleteForEveryoneMessage } from './sendDeleteForEveryoneMessage';
|
||||
import { __DEPRECATED$getMessageById } from '../messages/getMessageById';
|
||||
import { getMessageById } from '../messages/getMessageById';
|
||||
import * as log from '../logging/log';
|
||||
|
||||
export async function deleteGroupStoryReplyForEveryone(
|
||||
replyMessageId: string
|
||||
): Promise<void> {
|
||||
const messageModel = await __DEPRECATED$getMessageById(
|
||||
replyMessageId,
|
||||
'deleteGroupStoryReplyForEveryone'
|
||||
);
|
||||
const messageModel = await getMessageById(replyMessageId);
|
||||
|
||||
if (!messageModel) {
|
||||
log.warn(
|
||||
|
@ -23,7 +20,9 @@ export async function deleteGroupStoryReplyForEveryone(
|
|||
|
||||
const timestamp = messageModel.get('timestamp');
|
||||
|
||||
const group = messageModel.getConversation();
|
||||
const group = window.ConversationController.get(
|
||||
messageModel.get('conversationId')
|
||||
);
|
||||
|
||||
if (!group) {
|
||||
log.warn(
|
||||
|
|
|
@ -20,10 +20,11 @@ import {
|
|||
import { onStoryRecipientUpdate } from './onStoryRecipientUpdate';
|
||||
import { sendDeleteForEveryoneMessage } from './sendDeleteForEveryoneMessage';
|
||||
import { isGroupV2 } from './whatTypeOfConversation';
|
||||
import { __DEPRECATED$getMessageById } from '../messages/getMessageById';
|
||||
import { getMessageById } from '../messages/getMessageById';
|
||||
import { strictAssert } from './assert';
|
||||
import { repeat, zipObject } from './iterables';
|
||||
import { isOlderThan } from './timestamp';
|
||||
import { postSaveUpdates } from './cleanup';
|
||||
|
||||
export async function deleteStoryForEveryone(
|
||||
stories: ReadonlyArray<StoryDataType>,
|
||||
|
@ -47,10 +48,7 @@ export async function deleteStoryForEveryone(
|
|||
}
|
||||
|
||||
const logId = `deleteStoryForEveryone(${story.messageId})`;
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
story.messageId,
|
||||
'deleteStoryForEveryone'
|
||||
);
|
||||
const message = await getMessageById(story.messageId);
|
||||
if (!message) {
|
||||
throw new Error('Story not found');
|
||||
}
|
||||
|
@ -197,6 +195,7 @@ export async function deleteStoryForEveryone(
|
|||
await DataWriter.saveMessage(message.attributes, {
|
||||
jobToInsert,
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
94
ts/util/doubleCheckMissingQuoteReference.ts
Normal file
94
ts/util/doubleCheckMissingQuoteReference.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -4,8 +4,7 @@
|
|||
import { v4 as generateUuid } from 'uuid';
|
||||
|
||||
import type { AttachmentType } from '../types/Attachment';
|
||||
import type { MessageAttributesType } from '../model-types.d';
|
||||
import type { MessageModel } from '../models/messages';
|
||||
import { MessageModel } from '../models/messages';
|
||||
import * as log from '../logging/log';
|
||||
import { IMAGE_JPEG } from '../types/MIME';
|
||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||
|
@ -84,7 +83,7 @@ export async function downloadOnboardingStory(): Promise<void> {
|
|||
(attachment, index) => {
|
||||
const timestamp = Date.now() + index;
|
||||
|
||||
const partialMessage: MessageAttributesType = {
|
||||
const message = new MessageModel({
|
||||
attachments: [attachment],
|
||||
canReplyToStory: false,
|
||||
conversationId: signalConversation.id,
|
||||
|
@ -99,12 +98,8 @@ export async function downloadOnboardingStory(): Promise<void> {
|
|||
sourceServiceId: signalConversation.getServiceId(),
|
||||
timestamp,
|
||||
type: 'story',
|
||||
};
|
||||
return window.MessageCache.__DEPRECATED$register(
|
||||
partialMessage.id,
|
||||
partialMessage,
|
||||
'downloadOnboardingStory'
|
||||
);
|
||||
});
|
||||
return window.MessageCache.register(message);
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -112,11 +107,6 @@ export async function downloadOnboardingStory(): Promise<void> {
|
|||
storyMessages.map(message => saveNewMessageBatcher.add(message.attributes))
|
||||
);
|
||||
|
||||
// Sync to redux
|
||||
storyMessages.forEach(message => {
|
||||
message.trigger('change');
|
||||
});
|
||||
|
||||
await window.storage.put(
|
||||
'existingOnboardingStoryMessageIds',
|
||||
storyMessages.map(message => message.id)
|
||||
|
|
|
@ -5,7 +5,8 @@ import * as log from '../logging/log';
|
|||
import { DataWriter } from '../sql/Client';
|
||||
import { calculateExpirationTimestamp } from './expirationTimer';
|
||||
import { DAY } from './durations';
|
||||
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||
import { cleanupMessages } from './cleanup';
|
||||
import { getMessageById } from '../messages/getMessageById';
|
||||
|
||||
export async function findAndDeleteOnboardingStoryIfExists(): Promise<void> {
|
||||
const existingOnboardingStoryMessageIds = window.storage.get(
|
||||
|
@ -19,12 +20,14 @@ export async function findAndDeleteOnboardingStoryIfExists(): Promise<void> {
|
|||
const hasExpired = await (async () => {
|
||||
const [storyId] = existingOnboardingStoryMessageIds;
|
||||
try {
|
||||
const messageAttributes = await window.MessageCache.resolveAttributes(
|
||||
'findAndDeleteOnboardingStoryIfExists',
|
||||
storyId
|
||||
const message = await getMessageById(storyId);
|
||||
if (!message) {
|
||||
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 isExpired = expires < now;
|
||||
|
@ -46,7 +49,7 @@ export async function findAndDeleteOnboardingStoryIfExists(): Promise<void> {
|
|||
log.info('findAndDeleteOnboardingStoryIfExists: removing onboarding stories');
|
||||
|
||||
await DataWriter.removeMessages(existingOnboardingStoryMessageIds, {
|
||||
singleProtoJobQueue,
|
||||
cleanupMessages,
|
||||
});
|
||||
|
||||
await window.storage.put('existingOnboardingStoryMessageIds', undefined);
|
||||
|
|
|
@ -25,6 +25,7 @@ import { isTooOldToModifyMessage } from './isTooOldToModifyMessage';
|
|||
import { queueAttachmentDownloads } from './queueAttachmentDownloads';
|
||||
import { modifyTargetMessage } from './modifyTargetMessage';
|
||||
import { isMessageNoteToSelf } from './isMessageNoteToSelf';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
const RECURSION_LIMIT = 15;
|
||||
|
||||
|
@ -103,15 +104,13 @@ export async function handleEditMessage(
|
|||
return;
|
||||
}
|
||||
|
||||
const mainMessageModel = window.MessageCache.__DEPRECATED$register(
|
||||
mainMessage.id,
|
||||
mainMessage,
|
||||
'handleEditMessage'
|
||||
const mainMessageModel = window.MessageCache.register(
|
||||
new MessageModel(mainMessage)
|
||||
);
|
||||
|
||||
// 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.
|
||||
let editHistory: Array<EditHistoryType> = mainMessage.editHistory || [
|
||||
let editHistory: ReadonlyArray<EditHistoryType> = mainMessage.editHistory || [
|
||||
{
|
||||
attachments: mainMessage.attachments,
|
||||
body: mainMessage.body,
|
||||
|
@ -215,8 +214,10 @@ export async function handleEditMessage(
|
|||
const { quote: upgradedQuote } = upgradedEditedMessageData;
|
||||
let nextEditedMessageQuote: QuotedMessageType | undefined;
|
||||
if (!upgradedQuote) {
|
||||
if (mainMessage.quote) {
|
||||
// Quote dropped
|
||||
log.info(`${idLog}: dropping quote`);
|
||||
}
|
||||
} else if (!upgradedQuote.id || upgradedQuote.id === mainMessage.quote?.id) {
|
||||
// Quote preserved
|
||||
nextEditedMessageQuote = mainMessage.quote;
|
||||
|
@ -370,7 +371,9 @@ export async function handleEditMessage(
|
|||
conversation.clearContactTypingTimer(typingToken);
|
||||
}
|
||||
|
||||
const mainMessageConversation = mainMessageModel.getConversation();
|
||||
const mainMessageConversation = window.ConversationController.get(
|
||||
mainMessageModel.get('conversationId')
|
||||
);
|
||||
if (mainMessageConversation) {
|
||||
drop(mainMessageConversation.updateLastMessage());
|
||||
// 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
|
||||
const edits = Edits.forMessage({
|
||||
...mainMessage,
|
||||
...mainMessageModel.attributes,
|
||||
sent_at: editedMessage.timestamp,
|
||||
timestamp: editedMessage.timestamp,
|
||||
});
|
||||
|
|
|
@ -12,6 +12,10 @@ import { softAssert, strictAssert } from './assert';
|
|||
import { getMessageSentTimestamp } from './getMessageSentTimestamp';
|
||||
import { isOlderThan } from './timestamp';
|
||||
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(
|
||||
messageId: string,
|
||||
|
@ -24,23 +28,18 @@ export async function hydrateStoryContext(
|
|||
isStoryErased?: boolean;
|
||||
} = {}
|
||||
): Promise<Partial<MessageAttributesType> | undefined> {
|
||||
let messageAttributes: MessageAttributesType;
|
||||
try {
|
||||
messageAttributes = await window.MessageCache.resolveAttributes(
|
||||
'hydrateStoryContext',
|
||||
messageId
|
||||
);
|
||||
} catch {
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
log.warn(`hydrateStoryContext: Message ${messageId} not found`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { storyId } = messageAttributes;
|
||||
const { storyId, storyReplyContext: context } = message.attributes;
|
||||
if (!storyId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { storyReplyContext: context } = messageAttributes;
|
||||
const sentTimestamp = getMessageSentTimestamp(messageAttributes, {
|
||||
const sentTimestamp = getMessageSentTimestamp(message.attributes, {
|
||||
includeEdits: false,
|
||||
log,
|
||||
});
|
||||
|
@ -55,22 +54,19 @@ export async function hydrateStoryContext(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
let storyMessage: MessageAttributesType | undefined;
|
||||
let storyMessage: MessageModel | undefined;
|
||||
try {
|
||||
storyMessage =
|
||||
storyMessageParam === undefined
|
||||
? await window.MessageCache.resolveAttributes(
|
||||
'hydrateStoryContext/story',
|
||||
storyId
|
||||
)
|
||||
: window.MessageCache.toMessageAttributes(storyMessageParam);
|
||||
? await getMessageById(storyId)
|
||||
: window.MessageCache.register(new MessageModel(storyMessageParam));
|
||||
} catch {
|
||||
storyMessage = undefined;
|
||||
}
|
||||
|
||||
if (!storyMessage || isStoryErased) {
|
||||
const conversation = window.ConversationController.get(
|
||||
messageAttributes.conversationId
|
||||
message.attributes.conversationId
|
||||
);
|
||||
softAssert(
|
||||
conversation && isDirectConversation(conversation.attributes),
|
||||
|
@ -84,30 +80,25 @@ export async function hydrateStoryContext(
|
|||
messageId: '',
|
||||
},
|
||||
};
|
||||
message.set(newMessageAttributes);
|
||||
if (shouldSave) {
|
||||
await window.MessageCache.setAttributes({
|
||||
messageId,
|
||||
messageAttributes: newMessageAttributes,
|
||||
skipSaveToDatabase: false,
|
||||
});
|
||||
} else {
|
||||
window.MessageCache.setAttributes({
|
||||
messageId,
|
||||
messageAttributes: newMessageAttributes,
|
||||
skipSaveToDatabase: true,
|
||||
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
}
|
||||
|
||||
return newMessageAttributes;
|
||||
}
|
||||
|
||||
const attachments = getAttachmentsForMessage({ ...storyMessage });
|
||||
const attachments = getAttachmentsForMessage({ ...storyMessage.attributes });
|
||||
let attachment: AttachmentType | undefined = attachments?.[0];
|
||||
if (attachment && !attachment.url && !attachment.textAttachment) {
|
||||
attachment = undefined;
|
||||
}
|
||||
|
||||
const { sourceServiceId: authorAci } = storyMessage;
|
||||
const { sourceServiceId: authorAci } = storyMessage.attributes;
|
||||
strictAssert(isAciString(authorAci), 'Story message from pni');
|
||||
const newMessageAttributes: Partial<MessageAttributesType> = {
|
||||
storyReplyContext: {
|
||||
|
@ -116,18 +107,14 @@ export async function hydrateStoryContext(
|
|||
messageId: storyMessage.id,
|
||||
},
|
||||
};
|
||||
message.set(newMessageAttributes);
|
||||
if (shouldSave) {
|
||||
await window.MessageCache.setAttributes({
|
||||
messageId,
|
||||
messageAttributes: newMessageAttributes,
|
||||
skipSaveToDatabase: false,
|
||||
});
|
||||
} else {
|
||||
window.MessageCache.setAttributes({
|
||||
messageId,
|
||||
messageAttributes: newMessageAttributes,
|
||||
skipSaveToDatabase: true,
|
||||
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
}
|
||||
|
||||
return newMessageAttributes;
|
||||
}
|
||||
|
|
97
ts/util/isMessageEmpty.ts
Normal file
97
ts/util/isMessageEmpty.ts
Normal 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;
|
||||
}
|
38
ts/util/isValidTapToView.ts
Normal file
38
ts/util/isValidTapToView.ts
Normal 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;
|
||||
}
|
|
@ -55,8 +55,8 @@ export async function makeQuote(
|
|||
}
|
||||
|
||||
export async function getQuoteAttachment(
|
||||
attachments?: Array<AttachmentType>,
|
||||
preview?: Array<LinkPreviewType>,
|
||||
attachments?: ReadonlyArray<AttachmentType>,
|
||||
preview?: ReadonlyArray<LinkPreviewType>,
|
||||
sticker?: StickerType
|
||||
): Promise<Array<QuotedAttachmentType>> {
|
||||
const { loadAttachmentData } = window.Signal.Migrations;
|
||||
|
|
|
@ -103,10 +103,7 @@ export async function markConversationRead(
|
|||
|
||||
const allReadMessagesSync = allUnreadMessages
|
||||
.map(messageSyncData => {
|
||||
const message = window.MessageCache.__DEPRECATED$getById(
|
||||
messageSyncData.id,
|
||||
'markConversationRead'
|
||||
);
|
||||
const message = window.MessageCache.getById(messageSyncData.id);
|
||||
// we update the in-memory MessageModel with fresh read/seen status
|
||||
if (message) {
|
||||
message.set(
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
|
||||
import * as log from '../logging/log';
|
||||
import { DataWriter } from '../sql/Client';
|
||||
import { __DEPRECATED$getMessageById } from '../messages/getMessageById';
|
||||
import { getMessageById } from '../messages/getMessageById';
|
||||
import { isNotNil } from './isNotNil';
|
||||
import { DurationInSeconds } from './durations';
|
||||
import { markViewed } from '../services/MessageUpdater';
|
||||
import { storageServiceUploadJob } from '../services/storage';
|
||||
import { postSaveUpdates } from './cleanup';
|
||||
|
||||
export async function markOnboardingStoryAsRead(): Promise<boolean> {
|
||||
const existingOnboardingStoryMessageIds = window.storage.get(
|
||||
|
@ -20,9 +21,7 @@ export async function markOnboardingStoryAsRead(): Promise<boolean> {
|
|||
}
|
||||
|
||||
const messages = await Promise.all(
|
||||
existingOnboardingStoryMessageIds.map(id =>
|
||||
__DEPRECATED$getMessageById(id, 'markOnboardingStoryAsRead')
|
||||
)
|
||||
existingOnboardingStoryMessageIds.map(id => getMessageById(id))
|
||||
);
|
||||
|
||||
const storyReadDate = Date.now();
|
||||
|
@ -49,6 +48,7 @@ export async function markOnboardingStoryAsRead(): Promise<boolean> {
|
|||
|
||||
await DataWriter.saveMessages(messageAttributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
await window.storage.put('hasViewedOnboardingStory', true);
|
||||
|
|
|
@ -6,6 +6,8 @@ import { createBatcher } from './batcher';
|
|||
import { createWaitBatcher } from './waitBatcher';
|
||||
import { DataWriter } from '../sql/Client';
|
||||
import * as log from '../logging/log';
|
||||
import { postSaveUpdates } from './cleanup';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
const updateMessageBatcher = createBatcher<ReadonlyMessageAttributesType>({
|
||||
name: 'messageBatcher.updateMessageBatcher',
|
||||
|
@ -16,11 +18,12 @@ const updateMessageBatcher = createBatcher<ReadonlyMessageAttributesType>({
|
|||
|
||||
// Grab the latest from the cache in case they've changed
|
||||
const messagesToSave = messageAttrs.map(
|
||||
message => window.MessageCache.accessAttributes(message.id) ?? message
|
||||
message => window.MessageCache.getById(message.id)?.attributes ?? message
|
||||
);
|
||||
|
||||
await DataWriter.saveMessages(messagesToSave, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -35,6 +38,7 @@ export function queueUpdateMessage(
|
|||
} else {
|
||||
void DataWriter.saveMessage(messageAttr, {
|
||||
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
|
||||
const messagesToSave = messageAttrs.map(
|
||||
message => window.MessageCache.accessAttributes(message.id) ?? message
|
||||
message =>
|
||||
window.MessageCache.register(new MessageModel(message))?.attributes ??
|
||||
message
|
||||
);
|
||||
|
||||
await DataWriter.saveMessages(messagesToSave, {
|
||||
forceSave: true,
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -35,6 +35,10 @@ import {
|
|||
applyDeleteAttachmentFromMessage,
|
||||
applyDeleteMessage,
|
||||
} from './deleteForMe';
|
||||
import { getMessageIdForLogging } from './idForLogging';
|
||||
import { markViewOnceMessageViewed } from '../services/MessageUpdater';
|
||||
import { handleReaction } from '../messageModifiers/Reactions';
|
||||
import { postSaveUpdates } from './cleanup';
|
||||
|
||||
export enum ModifyTargetMessageResult {
|
||||
Modified = 'Modified',
|
||||
|
@ -52,7 +56,7 @@ export async function modifyTargetMessage(
|
|||
): Promise<ModifyTargetMessageResult> {
|
||||
const { isFirstRun = false, skipEdits = false } = options ?? {};
|
||||
|
||||
const logId = `modifyTargetMessage/${message.idForLogging()}`;
|
||||
const logId = `modifyTargetMessage/${getMessageIdForLogging(message.attributes)}`;
|
||||
const type = message.get('type');
|
||||
let changed = false;
|
||||
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||
|
@ -157,7 +161,7 @@ export async function modifyTargetMessage(
|
|||
);
|
||||
|
||||
if (!isEqual(oldSendStateByConversationId, newSendStateByConversationId)) {
|
||||
message.set('sendStateByConversationId', newSendStateByConversationId);
|
||||
message.set({ sendStateByConversationId: newSendStateByConversationId });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
@ -184,10 +188,12 @@ export async function modifyTargetMessage(
|
|||
const existingExpirationStartTimestamp = message.get(
|
||||
'expirationStartTimestamp'
|
||||
);
|
||||
message.set(
|
||||
'expirationStartTimestamp',
|
||||
Math.min(existingExpirationStartTimestamp ?? Date.now(), markReadAt)
|
||||
);
|
||||
message.set({
|
||||
expirationStartTimestamp: Math.min(
|
||||
existingExpirationStartTimestamp ?? Date.now(),
|
||||
markReadAt
|
||||
),
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
@ -208,8 +214,10 @@ export async function modifyTargetMessage(
|
|||
});
|
||||
changed = true;
|
||||
|
||||
message.setPendingMarkRead(
|
||||
Math.min(message.getPendingMarkRead() ?? Date.now(), markReadAt)
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message.pendingMarkRead = Math.min(
|
||||
message.pendingMarkRead ?? Date.now(),
|
||||
markReadAt
|
||||
);
|
||||
} else if (
|
||||
isFirstRun &&
|
||||
|
@ -219,9 +227,10 @@ export async function modifyTargetMessage(
|
|||
conversation.setArchived(false);
|
||||
}
|
||||
|
||||
if (!isFirstRun && message.getPendingMarkRead()) {
|
||||
const markReadAt = message.getPendingMarkRead();
|
||||
message.setPendingMarkRead(undefined);
|
||||
if (!isFirstRun && message.pendingMarkRead) {
|
||||
const markReadAt = message.pendingMarkRead;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message.pendingMarkRead = undefined;
|
||||
const newestSentAt = maybeSingleReadSync?.readSync.timestamp;
|
||||
|
||||
// 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
|
||||
// the database.
|
||||
drop(
|
||||
message
|
||||
.getConversation()
|
||||
?.onReadMessage(message.attributes, markReadAt, newestSentAt)
|
||||
window.ConversationController.get(
|
||||
message.get('conversationId')
|
||||
)?.onReadMessage(message.attributes, markReadAt, newestSentAt)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -242,7 +251,7 @@ export async function modifyTargetMessage(
|
|||
if (isTapToView(message.attributes)) {
|
||||
const viewOnceOpenSync = ViewOnceOpenSyncs.forMessage(message.attributes);
|
||||
if (viewOnceOpenSync) {
|
||||
await message.markViewOnceMessageViewed({ fromSync: true });
|
||||
await markViewOnceMessageViewed(message, { fromSync: true });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
@ -262,8 +271,10 @@ export async function modifyTargetMessage(
|
|||
Date.now(),
|
||||
...viewSyncs.map(({ viewSync }) => viewSync.viewedAt)
|
||||
);
|
||||
message.setPendingMarkRead(
|
||||
Math.min(message.getPendingMarkRead() ?? Date.now(), markReadAt)
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message.pendingMarkRead = Math.min(
|
||||
message.pendingMarkRead ?? Date.now(),
|
||||
markReadAt
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -272,7 +283,7 @@ export async function modifyTargetMessage(
|
|||
expirationStartTimestamp: message.get('timestamp'),
|
||||
expireTimer: message.get('expireTimer'),
|
||||
});
|
||||
message.set('expirationStartTimestamp', message.get('timestamp'));
|
||||
message.set({ expirationStartTimestamp: message.get('timestamp') });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
@ -292,12 +303,12 @@ export async function modifyTargetMessage(
|
|||
generatedMessage,
|
||||
'Story reactions must provide storyReactionMessage'
|
||||
);
|
||||
await generatedMessage.handleReaction(reaction, {
|
||||
await handleReaction(generatedMessage, reaction, {
|
||||
storyMessage: message.attributes,
|
||||
});
|
||||
} else {
|
||||
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.`);
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
ourAci,
|
||||
postSaveUpdates,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@ import { isStory } from '../state/selectors/message';
|
|||
import { queueUpdateMessage } from './messageBatcher';
|
||||
import { isMe } from './whatTypeOfConversation';
|
||||
import { drop } from './drop';
|
||||
import { handleDeleteForEveryone } from './deleteForEveryone';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
export async function onStoryRecipientUpdate(
|
||||
event: StoryRecipientUpdateEvent
|
||||
|
@ -162,11 +164,7 @@ export async function onStoryRecipientUpdate(
|
|||
return true;
|
||||
}
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$register(
|
||||
item.id,
|
||||
item,
|
||||
'onStoryRecipientUpdate'
|
||||
);
|
||||
const message = window.MessageCache.register(new MessageModel(item));
|
||||
|
||||
const sendStateConversationIds = new Set(
|
||||
Object.keys(nextSendStateByConversationId)
|
||||
|
@ -190,7 +188,7 @@ export async function onStoryRecipientUpdate(
|
|||
// sent timestamp doesn't happen (it would return all copies of the
|
||||
// story, not just the one we want to delete).
|
||||
drop(
|
||||
message.handleDeleteForEveryone({
|
||||
handleDeleteForEveryone(message, {
|
||||
fromId: ourConversationId,
|
||||
serverTimestamp: Number(item.serverTimestamp),
|
||||
targetSentTimestamp: item.timestamp,
|
||||
|
|
|
@ -33,13 +33,23 @@ import {
|
|||
AttachmentDownloadUrgency,
|
||||
} from '../jobs/AttachmentDownloadManager';
|
||||
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 = {
|
||||
bodyAttachment?: AttachmentType;
|
||||
attachments: Array<AttachmentType>;
|
||||
editHistory?: Array<EditHistoryType>;
|
||||
preview: Array<LinkPreviewType>;
|
||||
contact: Array<EmbeddedContactType>;
|
||||
attachments: ReadonlyArray<AttachmentType>;
|
||||
editHistory?: ReadonlyArray<EditHistoryType>;
|
||||
preview: ReadonlyArray<LinkPreviewType>;
|
||||
contact: ReadonlyArray<EmbeddedContactType>;
|
||||
quote?: QuotedMessageType;
|
||||
sticker?: StickerType;
|
||||
};
|
||||
|
@ -49,6 +59,50 @@ function getLogger(source: AttachmentDownloadSource) {
|
|||
const log = verbose ? logger : { ...logger, info: () => null };
|
||||
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
|
||||
// NOTE: If you're changing any logic in this function that deals with the
|
||||
// count then you'll also have to modify ./hasAttachmentsDownloads
|
||||
|
|
|
@ -16,11 +16,12 @@ import {
|
|||
getConversationIdForLogging,
|
||||
getMessageIdForLogging,
|
||||
} from './idForLogging';
|
||||
import { __DEPRECATED$getMessageById } from '../messages/getMessageById';
|
||||
import { getMessageById } from '../messages/getMessageById';
|
||||
import { getRecipientConversationIds } from './getRecipientConversationIds';
|
||||
import { getRecipients } from './getRecipients';
|
||||
import { repeat, zipObject } from './iterables';
|
||||
import { isMe } from './whatTypeOfConversation';
|
||||
import { postSaveUpdates } from './cleanup';
|
||||
|
||||
export async function sendDeleteForEveryoneMessage(
|
||||
conversationAttributes: ConversationAttributesType,
|
||||
|
@ -35,10 +36,7 @@ export async function sendDeleteForEveryoneMessage(
|
|||
timestamp: targetTimestamp,
|
||||
id: messageId,
|
||||
} = options;
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
messageId,
|
||||
'sendDeleteForEveryoneMessage'
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
throw new Error('sendDeleteForEveryoneMessage: Cannot find message!');
|
||||
}
|
||||
|
@ -88,6 +86,7 @@ export async function sendDeleteForEveryoneMessage(
|
|||
await DataWriter.saveMessage(message.attributes, {
|
||||
jobToInsert,
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
import { concat, filter, map, repeat, zipObject, find } from './iterables';
|
||||
import { getConversationIdForLogging } from './idForLogging';
|
||||
import { isQuoteAMatch } from '../messages/helpers';
|
||||
import { __DEPRECATED$getMessageById } from '../messages/getMessageById';
|
||||
import { getMessageById } from '../messages/getMessageById';
|
||||
import { handleEditMessage } from './handleEditMessage';
|
||||
import { incrementMessageCounter } from './incrementMessageCounter';
|
||||
import { isGroupV1 } from './whatTypeOfConversation';
|
||||
|
@ -34,6 +34,7 @@ import { strictAssert } from './assert';
|
|||
import { timeAndLogIfTooLong } from './timeAndLogIfTooLong';
|
||||
import { makeQuote } from './makeQuote';
|
||||
import { getMessageSentTimestamp } from './getMessageSentTimestamp';
|
||||
import { postSaveUpdates } from './cleanup';
|
||||
|
||||
const SEND_REPORT_THRESHOLD_MS = 25;
|
||||
|
||||
|
@ -65,10 +66,7 @@ export async function sendEditedMessage(
|
|||
conversation.attributes
|
||||
)})`;
|
||||
|
||||
const targetMessage = await __DEPRECATED$getMessageById(
|
||||
targetMessageId,
|
||||
'sendEditedMessage'
|
||||
);
|
||||
const targetMessage = await getMessageById(targetMessageId);
|
||||
strictAssert(targetMessage, 'could not find message to edit');
|
||||
|
||||
if (isGroupV1(conversation.attributes)) {
|
||||
|
@ -229,6 +227,7 @@ export async function sendEditedMessage(
|
|||
await DataWriter.saveMessage(targetMessage.attributes, {
|
||||
jobToInsert,
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
}
|
||||
),
|
||||
|
|
|
@ -31,6 +31,8 @@ import { collect } from './iterables';
|
|||
import { DurationInSeconds } from './durations';
|
||||
import { sanitizeLinkPreview } from '../services/LinkPreview';
|
||||
import type { DraftBodyRanges } from '../types/BodyRange';
|
||||
import { postSaveUpdates } from './cleanup';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
export async function sendStoryMessage(
|
||||
listIds: Array<string>,
|
||||
|
@ -308,11 +310,7 @@ export async function sendStoryMessage(
|
|||
// * Add the message to the conversation
|
||||
await Promise.all(
|
||||
distributionListMessages.map(message => {
|
||||
window.MessageCache.__DEPRECATED$register(
|
||||
message.id,
|
||||
new window.Whisper.Message(message),
|
||||
'sendStoryMessage'
|
||||
);
|
||||
window.MessageCache.register(new MessageModel(message));
|
||||
|
||||
void ourConversation.addSingleMessage(message, { isJustSent: true });
|
||||
|
||||
|
@ -320,6 +318,7 @@ export async function sendStoryMessage(
|
|||
return DataWriter.saveMessage(message, {
|
||||
forceSave: true,
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
@ -359,11 +358,7 @@ export async function sendStoryMessage(
|
|||
timestamp: messageAttributes.timestamp,
|
||||
},
|
||||
async jobToInsert => {
|
||||
window.MessageCache.__DEPRECATED$register(
|
||||
messageAttributes.id,
|
||||
new window.Whisper.Message(messageAttributes),
|
||||
'sendStoryMessage'
|
||||
);
|
||||
window.MessageCache.register(new MessageModel(messageAttributes));
|
||||
const conversation =
|
||||
window.ConversationController.get(conversationId);
|
||||
void conversation?.addSingleMessage(messageAttributes, {
|
||||
|
@ -377,6 +372,7 @@ export async function sendStoryMessage(
|
|||
forceSave: true,
|
||||
jobToInsert,
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
2
ts/window.d.ts
vendored
2
ts/window.d.ts
vendored
|
@ -32,7 +32,6 @@ import type { Receipt } from './types/Receipt';
|
|||
import type { ConversationController } from './ConversationController';
|
||||
import type { ReduxActions } from './state/types';
|
||||
import type { createApp } from './state/roots/createApp';
|
||||
import type { MessageModel } from './models/messages';
|
||||
import type { ConversationModel } from './models/conversations';
|
||||
import type { BatcherType } from './util/batcher';
|
||||
import type { ConfirmationDialog } from './components/ConfirmationDialog';
|
||||
|
@ -319,7 +318,6 @@ declare global {
|
|||
export type WhisperType = {
|
||||
Conversation: typeof ConversationModel;
|
||||
ConversationCollection: typeof ConversationModelCollectionType;
|
||||
Message: typeof MessageModel;
|
||||
|
||||
deliveryReceiptQueue: PQueue;
|
||||
deliveryReceiptBatcher: BatcherType<Receipt>;
|
||||
|
|
|
@ -65,8 +65,7 @@ if (
|
|||
)?.attributes;
|
||||
},
|
||||
getConversation: (id: string) => window.ConversationController.get(id),
|
||||
getMessageById: (id: string) =>
|
||||
window.MessageCache.__DEPRECATED$getById(id, 'SignalDebug'),
|
||||
getMessageById: (id: string) => window.MessageCache.getById(id),
|
||||
getMessageBySentAt: (timestamp: number) =>
|
||||
window.MessageCache.findBySentAt(timestamp, () => true),
|
||||
getReduxState: () => window.reduxStore.getState(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue