On send, pull data from target edit if sending edit
This commit is contained in:
parent
146b562c91
commit
48245eeea6
12 changed files with 529 additions and 135 deletions
|
@ -15,7 +15,6 @@ import { SignalService as Proto } from '../../protobuf';
|
||||||
import { handleMessageSend } from '../../util/handleMessageSend';
|
import { handleMessageSend } from '../../util/handleMessageSend';
|
||||||
import { findAndFormatContact } from '../../util/findAndFormatContact';
|
import { findAndFormatContact } from '../../util/findAndFormatContact';
|
||||||
import { uploadAttachment } from '../../util/uploadAttachment';
|
import { uploadAttachment } from '../../util/uploadAttachment';
|
||||||
import { getMessageSentTimestamp } from '../../util/getMessageSentTimestamp';
|
|
||||||
import type { CallbackResultType } from '../../textsecure/Types.d';
|
import type { CallbackResultType } from '../../textsecure/Types.d';
|
||||||
import { isSent } from '../../messages/MessageSendState';
|
import { isSent } from '../../messages/MessageSendState';
|
||||||
import { isOutgoing, canReact } from '../../state/selectors/message';
|
import { isOutgoing, canReact } from '../../state/selectors/message';
|
||||||
|
@ -54,6 +53,12 @@ import type { DurationInSeconds } from '../../util/durations';
|
||||||
import type { ServiceIdString } from '../../types/ServiceId';
|
import type { ServiceIdString } from '../../types/ServiceId';
|
||||||
import { normalizeAci } from '../../util/normalizeAci';
|
import { normalizeAci } from '../../util/normalizeAci';
|
||||||
import * as Bytes from '../../Bytes';
|
import * as Bytes from '../../Bytes';
|
||||||
|
import {
|
||||||
|
getPropForTimestamp,
|
||||||
|
getTargetOfThisEditTimestamp,
|
||||||
|
setPropForTimestamp,
|
||||||
|
} from '../../util/editHelpers';
|
||||||
|
import { getMessageSentTimestamp } from '../../util/getMessageSentTimestamp';
|
||||||
|
|
||||||
const LONG_ATTACHMENT_LIMIT = 2048;
|
const LONG_ATTACHMENT_LIMIT = 2048;
|
||||||
const MAX_CONCURRENT_ATTACHMENT_UPLOADS = 5;
|
const MAX_CONCURRENT_ATTACHMENT_UPLOADS = 5;
|
||||||
|
@ -100,6 +105,20 @@ export async function sendNormalMessage(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The original timestamp for this message
|
||||||
|
const messageTimestamp = getMessageSentTimestamp(message.attributes, {
|
||||||
|
includeEdits: false,
|
||||||
|
log,
|
||||||
|
});
|
||||||
|
// The timestamp for the thing we're sending now, whether a first send or an edit
|
||||||
|
const targetTimestamp = editedMessageTimestamp || messageTimestamp;
|
||||||
|
// The timestamp identifying the target of this edit; could be the original timestamp
|
||||||
|
// or the most recent edit prior to this one
|
||||||
|
const targetOfThisEditTimestamp = getTargetOfThisEditTimestamp({
|
||||||
|
message,
|
||||||
|
targetTimestamp,
|
||||||
|
});
|
||||||
|
|
||||||
let messageSendErrors: Array<Error> = [];
|
let messageSendErrors: Array<Error> = [];
|
||||||
|
|
||||||
// We don't want to save errors on messages unless we're giving up. If it's our
|
// We don't want to save errors on messages unless we're giving up. If it's our
|
||||||
|
@ -118,9 +137,11 @@ export async function sendNormalMessage(
|
||||||
|
|
||||||
if (!shouldContinue) {
|
if (!shouldContinue) {
|
||||||
log.info(`message ${messageId} ran out of time. Giving up on sending it`);
|
log.info(`message ${messageId} ran out of time. Giving up on sending it`);
|
||||||
await markMessageFailed(message, [
|
await markMessageFailed({
|
||||||
new Error('Message send ran out of time'),
|
message,
|
||||||
]);
|
errors: [new Error('Message send ran out of time')],
|
||||||
|
targetTimestamp,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,6 +162,7 @@ export async function sendNormalMessage(
|
||||||
log,
|
log,
|
||||||
message,
|
message,
|
||||||
conversation,
|
conversation,
|
||||||
|
targetTimestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (untrustedServiceIds.length) {
|
if (untrustedServiceIds.length) {
|
||||||
|
@ -169,14 +191,13 @@ export async function sendNormalMessage(
|
||||||
deletedForEveryoneTimestamp,
|
deletedForEveryoneTimestamp,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
bodyRanges,
|
bodyRanges,
|
||||||
messageTimestamp,
|
|
||||||
preview,
|
preview,
|
||||||
quote,
|
quote,
|
||||||
reaction,
|
reaction,
|
||||||
sticker,
|
sticker,
|
||||||
storyMessage,
|
storyMessage,
|
||||||
storyContext,
|
storyContext,
|
||||||
} = await getMessageSendData({ log, message });
|
} = await getMessageSendData({ log, message, targetTimestamp });
|
||||||
|
|
||||||
if (reaction) {
|
if (reaction) {
|
||||||
strictAssert(
|
strictAssert(
|
||||||
|
@ -197,13 +218,21 @@ export async function sendNormalMessage(
|
||||||
log.info(
|
log.info(
|
||||||
`could not react to ${messageId}. Removing this pending reaction`
|
`could not react to ${messageId}. Removing this pending reaction`
|
||||||
);
|
);
|
||||||
await markMessageFailed(message, [
|
await markMessageFailed({
|
||||||
new Error('Could not react to story'),
|
message,
|
||||||
]);
|
errors: [new Error('Could not react to story')],
|
||||||
|
targetTimestamp,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
'Sending normal message;',
|
||||||
|
`editedMessageTimestamp=${editedMessageTimestamp},`,
|
||||||
|
`storyMessage=${Boolean(storyMessage)}`
|
||||||
|
);
|
||||||
|
|
||||||
let messageSendPromise: Promise<CallbackResultType | void>;
|
let messageSendPromise: Promise<CallbackResultType | void>;
|
||||||
|
|
||||||
if (recipientServiceIdsWithoutMe.length === 0) {
|
if (recipientServiceIdsWithoutMe.length === 0) {
|
||||||
|
@ -215,7 +244,11 @@ export async function sendNormalMessage(
|
||||||
log.info(
|
log.info(
|
||||||
'No recipients; not sending to ourselves or to group, and no successful sends. Failing job.'
|
'No recipients; not sending to ourselves or to group, and no successful sends. Failing job.'
|
||||||
);
|
);
|
||||||
void markMessageFailed(message, [new Error('No valid recipients')]);
|
void markMessageFailed({
|
||||||
|
message,
|
||||||
|
errors: [new Error('No valid recipients')],
|
||||||
|
targetTimestamp,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,7 +262,6 @@ export async function sendNormalMessage(
|
||||||
bodyRanges,
|
bodyRanges,
|
||||||
contact,
|
contact,
|
||||||
deletedForEveryoneTimestamp,
|
deletedForEveryoneTimestamp,
|
||||||
editedMessageTimestamp,
|
|
||||||
expireTimer,
|
expireTimer,
|
||||||
groupV2: conversation.getGroupV2Info({
|
groupV2: conversation.getGroupV2Info({
|
||||||
members: recipientServiceIdsWithoutMe,
|
members: recipientServiceIdsWithoutMe,
|
||||||
|
@ -240,10 +272,17 @@ export async function sendNormalMessage(
|
||||||
recipients: allRecipientServiceIds,
|
recipients: allRecipientServiceIds,
|
||||||
sticker,
|
sticker,
|
||||||
storyContext,
|
storyContext,
|
||||||
timestamp: messageTimestamp,
|
targetTimestampForEdit: editedMessageTimestamp
|
||||||
|
? targetOfThisEditTimestamp
|
||||||
|
: undefined,
|
||||||
|
timestamp: targetTimestamp,
|
||||||
reaction,
|
reaction,
|
||||||
});
|
});
|
||||||
messageSendPromise = message.sendSyncMessageOnly(dataMessage, saveErrors);
|
messageSendPromise = message.sendSyncMessageOnly({
|
||||||
|
dataMessage,
|
||||||
|
saveErrors,
|
||||||
|
targetTimestamp,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const conversationType = conversation.get('type');
|
const conversationType = conversation.get('type');
|
||||||
const sendOptions = await getSendOptions(conversation.attributes);
|
const sendOptions = await getSendOptions(conversation.attributes);
|
||||||
|
@ -275,7 +314,6 @@ export async function sendNormalMessage(
|
||||||
bodyRanges,
|
bodyRanges,
|
||||||
contact,
|
contact,
|
||||||
deletedForEveryoneTimestamp,
|
deletedForEveryoneTimestamp,
|
||||||
editedMessageTimestamp,
|
|
||||||
expireTimer,
|
expireTimer,
|
||||||
groupV2: groupV2Info,
|
groupV2: groupV2Info,
|
||||||
messageText: body,
|
messageText: body,
|
||||||
|
@ -285,7 +323,10 @@ export async function sendNormalMessage(
|
||||||
sticker,
|
sticker,
|
||||||
storyContext,
|
storyContext,
|
||||||
reaction,
|
reaction,
|
||||||
timestamp: messageTimestamp,
|
targetTimestampForEdit: editedMessageTimestamp
|
||||||
|
? targetOfThisEditTimestamp
|
||||||
|
: undefined,
|
||||||
|
timestamp: targetTimestamp,
|
||||||
},
|
},
|
||||||
messageId,
|
messageId,
|
||||||
sendOptions,
|
sendOptions,
|
||||||
|
@ -300,25 +341,33 @@ export async function sendNormalMessage(
|
||||||
log.info(
|
log.info(
|
||||||
`conversation ${conversation.idForLogging()} is not accepted; refusing to send`
|
`conversation ${conversation.idForLogging()} is not accepted; refusing to send`
|
||||||
);
|
);
|
||||||
void markMessageFailed(message, [
|
void markMessageFailed({
|
||||||
new Error('Message request was not accepted'),
|
message,
|
||||||
]);
|
errors: [new Error('Message request was not accepted')],
|
||||||
|
targetTimestamp,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isConversationUnregistered(conversation.attributes)) {
|
if (isConversationUnregistered(conversation.attributes)) {
|
||||||
log.info(
|
log.info(
|
||||||
`conversation ${conversation.idForLogging()} is unregistered; refusing to send`
|
`conversation ${conversation.idForLogging()} is unregistered; refusing to send`
|
||||||
);
|
);
|
||||||
void markMessageFailed(message, [
|
void markMessageFailed({
|
||||||
new Error('Contact no longer has a Signal account'),
|
message,
|
||||||
]);
|
errors: [new Error('Contact no longer has a Signal account')],
|
||||||
|
targetTimestamp,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (conversation.isBlocked()) {
|
if (conversation.isBlocked()) {
|
||||||
log.info(
|
log.info(
|
||||||
`conversation ${conversation.idForLogging()} is blocked; refusing to send`
|
`conversation ${conversation.idForLogging()} is blocked; refusing to send`
|
||||||
);
|
);
|
||||||
void markMessageFailed(message, [new Error('Contact is blocked')]);
|
void markMessageFailed({
|
||||||
|
message,
|
||||||
|
errors: [new Error('Contact is blocked')],
|
||||||
|
targetTimestamp,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -329,7 +378,6 @@ export async function sendNormalMessage(
|
||||||
contact,
|
contact,
|
||||||
contentHint: ContentHint.RESENDABLE,
|
contentHint: ContentHint.RESENDABLE,
|
||||||
deletedForEveryoneTimestamp,
|
deletedForEveryoneTimestamp,
|
||||||
editedMessageTimestamp,
|
|
||||||
expireTimer,
|
expireTimer,
|
||||||
groupId: undefined,
|
groupId: undefined,
|
||||||
serviceId: recipientServiceIdsWithoutMe[0],
|
serviceId: recipientServiceIdsWithoutMe[0],
|
||||||
|
@ -341,20 +389,24 @@ export async function sendNormalMessage(
|
||||||
sticker,
|
sticker,
|
||||||
storyContext,
|
storyContext,
|
||||||
reaction,
|
reaction,
|
||||||
timestamp: messageTimestamp,
|
targetTimestampForEdit: editedMessageTimestamp
|
||||||
|
? targetOfThisEditTimestamp
|
||||||
|
: undefined,
|
||||||
|
timestamp: targetTimestamp,
|
||||||
// Note: 1:1 story replies should not set story=true - they aren't group sends
|
// Note: 1:1 story replies should not set story=true - they aren't group sends
|
||||||
urgent: true,
|
urgent: true,
|
||||||
includePniSignatureMessage: true,
|
includePniSignatureMessage: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
messageSendPromise = message.send(
|
messageSendPromise = message.send({
|
||||||
handleMessageSend(innerPromise, {
|
promise: handleMessageSend(innerPromise, {
|
||||||
messageIds: [messageId],
|
messageIds: [messageId],
|
||||||
sendType: 'message',
|
sendType: 'message',
|
||||||
}),
|
}),
|
||||||
saveErrors
|
saveErrors,
|
||||||
);
|
targetTimestamp,
|
||||||
|
});
|
||||||
|
|
||||||
// Because message.send swallows and processes errors, we'll await the inner promise
|
// Because message.send swallows and processes errors, we'll await the inner promise
|
||||||
// to get the SendMessageProtoError, which gives us information upstream
|
// to get the SendMessageProtoError, which gives us information upstream
|
||||||
|
@ -377,7 +429,12 @@ export async function sendNormalMessage(
|
||||||
await messageSendPromise;
|
await messageSendPromise;
|
||||||
|
|
||||||
const didFullySend =
|
const didFullySend =
|
||||||
!messageSendErrors.length || didSendToEveryone(message);
|
!messageSendErrors.length ||
|
||||||
|
didSendToEveryone({
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
targetTimestamp: editedMessageTimestamp || messageTimestamp,
|
||||||
|
});
|
||||||
if (!didFullySend) {
|
if (!didFullySend) {
|
||||||
throw new Error('message did not fully send');
|
throw new Error('message did not fully send');
|
||||||
}
|
}
|
||||||
|
@ -387,7 +444,12 @@ export async function sendNormalMessage(
|
||||||
errors,
|
errors,
|
||||||
isFinalAttempt,
|
isFinalAttempt,
|
||||||
log,
|
log,
|
||||||
markFailed: () => markMessageFailed(message, messageSendErrors),
|
markFailed: () =>
|
||||||
|
markMessageFailed({
|
||||||
|
message,
|
||||||
|
errors: messageSendErrors,
|
||||||
|
targetTimestamp,
|
||||||
|
}),
|
||||||
timeRemaining,
|
timeRemaining,
|
||||||
// In the case of a failed group send thrownError will not be SentMessageProtoError,
|
// In the case of a failed group send thrownError will not be SentMessageProtoError,
|
||||||
// but we should have been able to harvest the original error. In the Note to Self
|
// but we should have been able to harvest the original error. In the Note to Self
|
||||||
|
@ -402,10 +464,12 @@ function getMessageRecipients({
|
||||||
log,
|
log,
|
||||||
conversation,
|
conversation,
|
||||||
message,
|
message,
|
||||||
|
targetTimestamp,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
log: LoggerType;
|
log: LoggerType;
|
||||||
conversation: ConversationModel;
|
conversation: ConversationModel;
|
||||||
message: MessageModel;
|
message: MessageModel;
|
||||||
|
targetTimestamp: number;
|
||||||
}>): {
|
}>): {
|
||||||
allRecipientServiceIds: Array<ServiceIdString>;
|
allRecipientServiceIds: Array<ServiceIdString>;
|
||||||
recipientServiceIdsWithoutMe: Array<ServiceIdString>;
|
recipientServiceIdsWithoutMe: Array<ServiceIdString>;
|
||||||
|
@ -419,7 +483,15 @@ function getMessageRecipients({
|
||||||
|
|
||||||
const currentConversationRecipients = conversation.getMemberConversationIds();
|
const currentConversationRecipients = conversation.getMemberConversationIds();
|
||||||
|
|
||||||
Object.entries(message.get('sendStateByConversationId') || {}).forEach(
|
const sendStateByConversationId =
|
||||||
|
getPropForTimestamp({
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
prop: 'sendStateByConversationId',
|
||||||
|
targetTimestamp,
|
||||||
|
}) || {};
|
||||||
|
|
||||||
|
Object.entries(sendStateByConversationId).forEach(
|
||||||
([recipientConversationId, sendState]) => {
|
([recipientConversationId, sendState]) => {
|
||||||
const recipient = window.ConversationController.get(
|
const recipient = window.ConversationController.get(
|
||||||
recipientConversationId
|
recipientConversationId
|
||||||
|
@ -483,9 +555,11 @@ function getMessageRecipients({
|
||||||
async function getMessageSendData({
|
async function getMessageSendData({
|
||||||
log,
|
log,
|
||||||
message,
|
message,
|
||||||
|
targetTimestamp,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
log: LoggerType;
|
log: LoggerType;
|
||||||
message: MessageModel;
|
message: MessageModel;
|
||||||
|
targetTimestamp: number;
|
||||||
}>): Promise<{
|
}>): Promise<{
|
||||||
attachments: Array<UploadedAttachmentType>;
|
attachments: Array<UploadedAttachmentType>;
|
||||||
body: undefined | string;
|
body: undefined | string;
|
||||||
|
@ -493,7 +567,6 @@ async function getMessageSendData({
|
||||||
deletedForEveryoneTimestamp: undefined | number;
|
deletedForEveryoneTimestamp: undefined | number;
|
||||||
expireTimer: undefined | DurationInSeconds;
|
expireTimer: undefined | DurationInSeconds;
|
||||||
bodyRanges: undefined | ReadonlyArray<RawBodyRange>;
|
bodyRanges: undefined | ReadonlyArray<RawBodyRange>;
|
||||||
messageTimestamp: number;
|
|
||||||
preview: Array<OutgoingLinkPreviewType> | undefined;
|
preview: Array<OutgoingLinkPreviewType> | undefined;
|
||||||
quote: OutgoingQuoteType | undefined;
|
quote: OutgoingQuoteType | undefined;
|
||||||
sticker: OutgoingStickerType | undefined;
|
sticker: OutgoingStickerType | undefined;
|
||||||
|
@ -501,25 +574,22 @@ async function getMessageSendData({
|
||||||
storyMessage?: MessageModel;
|
storyMessage?: MessageModel;
|
||||||
storyContext?: StoryContextType;
|
storyContext?: StoryContextType;
|
||||||
}> {
|
}> {
|
||||||
const editMessageTimestamp = message.get('editMessageTimestamp');
|
|
||||||
|
|
||||||
const mainMessageTimestamp = getMessageSentTimestamp(message.attributes, {
|
|
||||||
includeEdits: false,
|
|
||||||
log,
|
|
||||||
});
|
|
||||||
const messageTimestamp = editMessageTimestamp || mainMessageTimestamp;
|
|
||||||
|
|
||||||
const storyId = message.get('storyId');
|
const storyId = message.get('storyId');
|
||||||
|
|
||||||
// Figure out if we need to upload message body as an attachment.
|
// Figure out if we need to upload message body as an attachment.
|
||||||
let body = message.get('body');
|
let body = getPropForTimestamp({
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
prop: 'body',
|
||||||
|
targetTimestamp,
|
||||||
|
});
|
||||||
let maybeLongAttachment: AttachmentWithHydratedData | undefined;
|
let maybeLongAttachment: AttachmentWithHydratedData | undefined;
|
||||||
if (body && body.length > LONG_ATTACHMENT_LIMIT) {
|
if (body && body.length > LONG_ATTACHMENT_LIMIT) {
|
||||||
const data = Bytes.fromString(body);
|
const data = Bytes.fromString(body);
|
||||||
|
|
||||||
maybeLongAttachment = {
|
maybeLongAttachment = {
|
||||||
contentType: LONG_MESSAGE,
|
contentType: LONG_MESSAGE,
|
||||||
fileName: `long-message-${messageTimestamp}.txt`,
|
fileName: `long-message-${targetTimestamp}.txt`,
|
||||||
data,
|
data,
|
||||||
size: data.byteLength,
|
size: data.byteLength,
|
||||||
};
|
};
|
||||||
|
@ -530,6 +600,13 @@ async function getMessageSendData({
|
||||||
concurrency: MAX_CONCURRENT_ATTACHMENT_UPLOADS,
|
concurrency: MAX_CONCURRENT_ATTACHMENT_UPLOADS,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const preUploadAttachments =
|
||||||
|
getPropForTimestamp({
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
prop: 'attachments',
|
||||||
|
targetTimestamp,
|
||||||
|
}) || [];
|
||||||
const [
|
const [
|
||||||
uploadedAttachments,
|
uploadedAttachments,
|
||||||
maybeUploadedLongAttachment,
|
maybeUploadedLongAttachment,
|
||||||
|
@ -540,16 +617,32 @@ async function getMessageSendData({
|
||||||
storyMessage,
|
storyMessage,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
uploadQueue.addAll(
|
uploadQueue.addAll(
|
||||||
(message.get('attachments') ?? []).map(
|
preUploadAttachments.map(
|
||||||
attachment => () => uploadSingleAttachment(message, attachment)
|
attachment => () =>
|
||||||
|
uploadSingleAttachment({
|
||||||
|
attachment,
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
targetTimestamp,
|
||||||
|
})
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
uploadQueue.add(async () =>
|
uploadQueue.add(async () =>
|
||||||
maybeLongAttachment ? uploadAttachment(maybeLongAttachment) : undefined
|
maybeLongAttachment ? uploadAttachment(maybeLongAttachment) : undefined
|
||||||
),
|
),
|
||||||
uploadMessageContacts(message, uploadQueue),
|
uploadMessageContacts(message, uploadQueue),
|
||||||
uploadMessagePreviews(message, uploadQueue),
|
uploadMessagePreviews({
|
||||||
uploadMessageQuote(message, uploadQueue),
|
log,
|
||||||
|
message,
|
||||||
|
targetTimestamp,
|
||||||
|
uploadQueue,
|
||||||
|
}),
|
||||||
|
uploadMessageQuote({
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
targetTimestamp,
|
||||||
|
uploadQueue,
|
||||||
|
}),
|
||||||
uploadMessageSticker(message, uploadQueue),
|
uploadMessageSticker(message, uploadQueue),
|
||||||
storyId ? __DEPRECATED$getMessageById(storyId) : undefined,
|
storyId ? __DEPRECATED$getMessageById(storyId) : undefined,
|
||||||
]);
|
]);
|
||||||
|
@ -582,9 +675,12 @@ async function getMessageSendData({
|
||||||
contact,
|
contact,
|
||||||
deletedForEveryoneTimestamp: message.get('deletedForEveryoneTimestamp'),
|
deletedForEveryoneTimestamp: message.get('deletedForEveryoneTimestamp'),
|
||||||
expireTimer: message.get('expireTimer'),
|
expireTimer: message.get('expireTimer'),
|
||||||
// TODO: we want filtration here if feature flag doesn't allow format/spoiler sends
|
bodyRanges: getPropForTimestamp({
|
||||||
bodyRanges: message.get('bodyRanges'),
|
log,
|
||||||
messageTimestamp,
|
message,
|
||||||
|
prop: 'bodyRanges',
|
||||||
|
targetTimestamp,
|
||||||
|
}),
|
||||||
preview,
|
preview,
|
||||||
quote,
|
quote,
|
||||||
reaction: reactionForSend,
|
reaction: reactionForSend,
|
||||||
|
@ -604,10 +700,17 @@ async function getMessageSendData({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadSingleAttachment(
|
async function uploadSingleAttachment({
|
||||||
message: MessageModel,
|
attachment,
|
||||||
attachment: AttachmentType
|
log,
|
||||||
): Promise<UploadedAttachmentType> {
|
message,
|
||||||
|
targetTimestamp,
|
||||||
|
}: {
|
||||||
|
attachment: AttachmentType;
|
||||||
|
log: LoggerType;
|
||||||
|
message: MessageModel;
|
||||||
|
targetTimestamp: number;
|
||||||
|
}): Promise<UploadedAttachmentType> {
|
||||||
const { loadAttachmentData } = window.Signal.Migrations;
|
const { loadAttachmentData } = window.Signal.Migrations;
|
||||||
|
|
||||||
const withData = await loadAttachmentData(attachment);
|
const withData = await loadAttachmentData(attachment);
|
||||||
|
@ -615,7 +718,12 @@ async function uploadSingleAttachment(
|
||||||
|
|
||||||
// Add digest to the attachment
|
// Add digest to the attachment
|
||||||
const logId = `uploadSingleAttachment(${message.idForLogging()}`;
|
const logId = `uploadSingleAttachment(${message.idForLogging()}`;
|
||||||
const oldAttachments = message.get('attachments');
|
const oldAttachments = getPropForTimestamp({
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
prop: 'attachments',
|
||||||
|
targetTimestamp,
|
||||||
|
});
|
||||||
strictAssert(
|
strictAssert(
|
||||||
oldAttachments !== undefined,
|
oldAttachments !== undefined,
|
||||||
`${logId}: Attachment was uploaded, but message doesn't ` +
|
`${logId}: Attachment was uploaded, but message doesn't ` +
|
||||||
|
@ -634,24 +742,42 @@ async function uploadSingleAttachment(
|
||||||
...copyCdnFields(uploaded),
|
...copyCdnFields(uploaded),
|
||||||
};
|
};
|
||||||
|
|
||||||
message.set('attachments', newAttachments);
|
setPropForTimestamp({
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
prop: 'attachments',
|
||||||
|
targetTimestamp,
|
||||||
|
value: newAttachments,
|
||||||
|
});
|
||||||
|
|
||||||
return uploaded;
|
return uploaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadMessageQuote(
|
async function uploadMessageQuote({
|
||||||
message: MessageModel,
|
log,
|
||||||
uploadQueue: PQueue
|
message,
|
||||||
): Promise<OutgoingQuoteType | undefined> {
|
targetTimestamp,
|
||||||
|
uploadQueue,
|
||||||
|
}: {
|
||||||
|
log: LoggerType;
|
||||||
|
message: MessageModel;
|
||||||
|
targetTimestamp: number;
|
||||||
|
uploadQueue: PQueue;
|
||||||
|
}): Promise<OutgoingQuoteType | undefined> {
|
||||||
const { loadQuoteData } = window.Signal.Migrations;
|
const { loadQuoteData } = window.Signal.Migrations;
|
||||||
|
|
||||||
// We don't update the caches here because (1) we expect the caches to be populated
|
// We don't update the caches here because (1) we expect the caches to be populated
|
||||||
// on initial send, so they should be there in the 99% case (2) if you're retrying
|
// on initial send, so they should be there in the 99% case (2) if you're retrying
|
||||||
// a failed message across restarts, we don't touch the cache for simplicity. If
|
// a failed message across restarts, we don't touch the cache for simplicity. If
|
||||||
// sends are failing, let's not add the complication of a cache.
|
// sends are failing, let's not add the complication of a cache.
|
||||||
|
const startingQuote = getPropForTimestamp({
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
prop: 'quote',
|
||||||
|
targetTimestamp,
|
||||||
|
});
|
||||||
const loadedQuote =
|
const loadedQuote =
|
||||||
message.cachedOutgoingQuoteData ||
|
message.cachedOutgoingQuoteData || (await loadQuoteData(startingQuote));
|
||||||
(await loadQuoteData(message.get('quote')));
|
|
||||||
|
|
||||||
if (!loadedQuote) {
|
if (!loadedQuote) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -680,7 +806,12 @@ async function uploadMessageQuote(
|
||||||
|
|
||||||
// Update message with attachment digests
|
// Update message with attachment digests
|
||||||
const logId = `uploadMessageQuote(${message.idForLogging()}`;
|
const logId = `uploadMessageQuote(${message.idForLogging()}`;
|
||||||
const oldQuote = message.get('quote');
|
const oldQuote = getPropForTimestamp({
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
prop: 'quote',
|
||||||
|
targetTimestamp,
|
||||||
|
});
|
||||||
strictAssert(oldQuote, `${logId}: Quote is gone after upload`);
|
strictAssert(oldQuote, `${logId}: Quote is gone after upload`);
|
||||||
|
|
||||||
const newQuote = {
|
const newQuote = {
|
||||||
|
@ -711,7 +842,13 @@ async function uploadMessageQuote(
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
message.set('quote', newQuote);
|
setPropForTimestamp({
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
prop: 'quote',
|
||||||
|
targetTimestamp,
|
||||||
|
value: newQuote,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isGiftBadge: loadedQuote.isGiftBadge,
|
isGiftBadge: loadedQuote.isGiftBadge,
|
||||||
|
@ -725,17 +862,31 @@ async function uploadMessageQuote(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadMessagePreviews(
|
async function uploadMessagePreviews({
|
||||||
message: MessageModel,
|
log,
|
||||||
uploadQueue: PQueue
|
message,
|
||||||
): Promise<Array<OutgoingLinkPreviewType> | undefined> {
|
targetTimestamp,
|
||||||
|
uploadQueue,
|
||||||
|
}: {
|
||||||
|
log: LoggerType;
|
||||||
|
message: MessageModel;
|
||||||
|
targetTimestamp: number;
|
||||||
|
uploadQueue: PQueue;
|
||||||
|
}): Promise<Array<OutgoingLinkPreviewType> | undefined> {
|
||||||
const { loadPreviewData } = window.Signal.Migrations;
|
const { loadPreviewData } = window.Signal.Migrations;
|
||||||
|
|
||||||
// See uploadMessageQuote for comment on how we do caching for these
|
// See uploadMessageQuote for comment on how we do caching for these
|
||||||
// attachments.
|
// attachments.
|
||||||
|
const startingPreview = getPropForTimestamp({
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
prop: 'preview',
|
||||||
|
targetTimestamp,
|
||||||
|
});
|
||||||
|
|
||||||
const loadedPreviews =
|
const loadedPreviews =
|
||||||
message.cachedOutgoingPreviewData ||
|
message.cachedOutgoingPreviewData ||
|
||||||
(await loadPreviewData(message.get('preview')));
|
(await loadPreviewData(startingPreview));
|
||||||
|
|
||||||
if (!loadedPreviews) {
|
if (!loadedPreviews) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -766,7 +917,12 @@ async function uploadMessagePreviews(
|
||||||
|
|
||||||
// Update message with attachment digests
|
// Update message with attachment digests
|
||||||
const logId = `uploadMessagePreviews(${message.idForLogging()}`;
|
const logId = `uploadMessagePreviews(${message.idForLogging()}`;
|
||||||
const oldPreview = message.get('preview');
|
const oldPreview = getPropForTimestamp({
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
prop: 'preview',
|
||||||
|
targetTimestamp,
|
||||||
|
});
|
||||||
strictAssert(oldPreview, `${logId}: Link preview is gone after upload`);
|
strictAssert(oldPreview, `${logId}: Link preview is gone after upload`);
|
||||||
|
|
||||||
const newPreview = oldPreview.map((preview, index) => {
|
const newPreview = oldPreview.map((preview, index) => {
|
||||||
|
@ -788,7 +944,14 @@ async function uploadMessagePreviews(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
message.set('preview', newPreview);
|
|
||||||
|
setPropForTimestamp({
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
prop: 'preview',
|
||||||
|
targetTimestamp,
|
||||||
|
value: newPreview,
|
||||||
|
});
|
||||||
|
|
||||||
return uploadedPreviews;
|
return uploadedPreviews;
|
||||||
}
|
}
|
||||||
|
@ -928,20 +1091,38 @@ async function uploadMessageContacts(
|
||||||
return uploadedContacts;
|
return uploadedContacts;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function markMessageFailed(
|
async function markMessageFailed({
|
||||||
message: MessageModel,
|
errors,
|
||||||
errors: Array<Error>
|
message,
|
||||||
): Promise<void> {
|
targetTimestamp,
|
||||||
message.markFailed();
|
}: {
|
||||||
|
errors: Array<Error>;
|
||||||
|
message: MessageModel;
|
||||||
|
targetTimestamp: number;
|
||||||
|
}): Promise<void> {
|
||||||
|
message.markFailed(targetTimestamp);
|
||||||
void message.saveErrors(errors, { skipSave: true });
|
void message.saveErrors(errors, { skipSave: true });
|
||||||
await window.Signal.Data.saveMessage(message.attributes, {
|
await window.Signal.Data.saveMessage(message.attributes, {
|
||||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function didSendToEveryone(message: Readonly<MessageModel>): boolean {
|
function didSendToEveryone({
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
targetTimestamp,
|
||||||
|
}: {
|
||||||
|
log: LoggerType;
|
||||||
|
message: MessageModel;
|
||||||
|
targetTimestamp: number;
|
||||||
|
}): boolean {
|
||||||
const sendStateByConversationId =
|
const sendStateByConversationId =
|
||||||
message.get('sendStateByConversationId') || {};
|
getPropForTimestamp({
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
prop: 'sendStateByConversationId',
|
||||||
|
targetTimestamp,
|
||||||
|
}) || {};
|
||||||
return Object.values(sendStateByConversationId).every(sendState =>
|
return Object.values(sendStateByConversationId).every(sendState =>
|
||||||
isSent(sendState.status)
|
isSent(sendState.status)
|
||||||
);
|
);
|
||||||
|
|
|
@ -198,10 +198,11 @@ export async function sendReaction(
|
||||||
recipients: allRecipientServiceIds,
|
recipients: allRecipientServiceIds,
|
||||||
timestamp: pendingReaction.timestamp,
|
timestamp: pendingReaction.timestamp,
|
||||||
});
|
});
|
||||||
await ephemeralMessageForReactionSend.sendSyncMessageOnly(
|
await ephemeralMessageForReactionSend.sendSyncMessageOnly({
|
||||||
dataMessage,
|
dataMessage,
|
||||||
saveErrors
|
saveErrors,
|
||||||
);
|
targetTimestamp: pendingReaction.timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
didFullySend = true;
|
didFullySend = true;
|
||||||
successfulConversationIds.add(ourConversationId);
|
successfulConversationIds.add(ourConversationId);
|
||||||
|
@ -289,13 +290,14 @@ export async function sendReaction(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ephemeralMessageForReactionSend.send(
|
await ephemeralMessageForReactionSend.send({
|
||||||
handleMessageSend(promise, {
|
promise: handleMessageSend(promise, {
|
||||||
messageIds: [messageId],
|
messageIds: [messageId],
|
||||||
sendType: 'reaction',
|
sendType: 'reaction',
|
||||||
}),
|
}),
|
||||||
saveErrors
|
saveErrors,
|
||||||
);
|
targetTimestamp: pendingReaction.timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
// Because message.send swallows and processes errors, we'll await the inner promise
|
// Because message.send swallows and processes errors, we'll await the inner promise
|
||||||
// to get the SendMessageProtoError, which gives us information upstream
|
// to get the SendMessageProtoError, which gives us information upstream
|
||||||
|
|
|
@ -365,13 +365,14 @@ export async function sendStory(
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
message.doNotSendSyncMessage = true;
|
message.doNotSendSyncMessage = true;
|
||||||
|
|
||||||
const messageSendPromise = message.send(
|
const messageSendPromise = message.send({
|
||||||
handleMessageSend(innerPromise, {
|
promise: handleMessageSend(innerPromise, {
|
||||||
messageIds: [message.id],
|
messageIds: [message.id],
|
||||||
sendType: 'story',
|
sendType: 'story',
|
||||||
}),
|
}),
|
||||||
saveErrors
|
saveErrors,
|
||||||
);
|
targetTimestamp: message.get('timestamp'),
|
||||||
|
});
|
||||||
|
|
||||||
// Because message.send swallows and processes errors, we'll await the
|
// Because message.send swallows and processes errors, we'll await the
|
||||||
// inner promise to get the SendMessageProtoError, which gives us
|
// inner promise to get the SendMessageProtoError, which gives us
|
||||||
|
|
2
ts/model-types.d.ts
vendored
2
ts/model-types.d.ts
vendored
|
@ -113,6 +113,8 @@ export type MessageReactionType = {
|
||||||
isSentByConversationId?: Record<string, boolean>;
|
isSentByConversationId?: Record<string, boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Note: when adding to the set of things that can change via edits, sendNormalMessage.ts
|
||||||
|
// needs more usage of get/setPropForTimestamp.
|
||||||
export type EditHistoryType = {
|
export type EditHistoryType = {
|
||||||
attachments?: Array<AttachmentType>;
|
attachments?: Array<AttachmentType>;
|
||||||
body?: string;
|
body?: string;
|
||||||
|
|
|
@ -39,7 +39,6 @@ import type {
|
||||||
} from '../textsecure/Types.d';
|
} from '../textsecure/Types.d';
|
||||||
import { SendMessageProtoError } from '../textsecure/Errors';
|
import { SendMessageProtoError } from '../textsecure/Errors';
|
||||||
import { getUserLanguages } from '../util/userLanguages';
|
import { getUserLanguages } from '../util/userLanguages';
|
||||||
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
|
|
||||||
import { copyCdnFields } from '../util/attachments';
|
import { copyCdnFields } from '../util/attachments';
|
||||||
|
|
||||||
import type { ReactionType } from '../types/Reactions';
|
import type { ReactionType } from '../types/Reactions';
|
||||||
|
@ -156,6 +155,11 @@ import { getSenderIdentifier } from '../util/getSenderIdentifier';
|
||||||
import { getNotificationDataForMessage } from '../util/getNotificationDataForMessage';
|
import { getNotificationDataForMessage } from '../util/getNotificationDataForMessage';
|
||||||
import { getNotificationTextForMessage } from '../util/getNotificationTextForMessage';
|
import { getNotificationTextForMessage } from '../util/getNotificationTextForMessage';
|
||||||
import { getMessageAuthorText } from '../util/getMessageAuthorText';
|
import { getMessageAuthorText } from '../util/getMessageAuthorText';
|
||||||
|
import {
|
||||||
|
getPropForTimestamp,
|
||||||
|
setPropForTimestamp,
|
||||||
|
hasEditBeenSent,
|
||||||
|
} from '../util/editHelpers';
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
|
|
||||||
|
@ -831,18 +835,40 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
* Change any Pending send state to Failed. Note that this will not mark successful
|
* Change any Pending send state to Failed. Note that this will not mark successful
|
||||||
* sends failed.
|
* sends failed.
|
||||||
*/
|
*/
|
||||||
public markFailed(): void {
|
public markFailed(editMessageTimestamp?: number): void {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
this.set(
|
|
||||||
'sendStateByConversationId',
|
const targetTimestamp = editMessageTimestamp || this.get('timestamp');
|
||||||
mapValues(this.get('sendStateByConversationId') || {}, sendState =>
|
const sendStateByConversationId = getPropForTimestamp({
|
||||||
|
log,
|
||||||
|
message: this,
|
||||||
|
prop: 'sendStateByConversationId',
|
||||||
|
targetTimestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newSendStateByConversationId = mapValues(
|
||||||
|
sendStateByConversationId || {},
|
||||||
|
sendState =>
|
||||||
sendStateReducer(sendState, {
|
sendStateReducer(sendState, {
|
||||||
type: SendActionType.Failed,
|
type: SendActionType.Failed,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setPropForTimestamp({
|
||||||
|
log,
|
||||||
|
message: this,
|
||||||
|
prop: 'sendStateByConversationId',
|
||||||
|
targetTimestamp,
|
||||||
|
value: newSendStateByConversationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// We aren't trying to send this message anymore, so we'll delete these caches
|
||||||
|
delete this.cachedOutgoingContactData;
|
||||||
|
delete this.cachedOutgoingPreviewData;
|
||||||
|
delete this.cachedOutgoingQuoteData;
|
||||||
|
delete this.cachedOutgoingStickerData;
|
||||||
|
|
||||||
this.notifyStorySendFailed();
|
this.notifyStorySendFailed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -886,10 +912,15 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
return errors[0][0];
|
return errors[0][0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(
|
async send({
|
||||||
promise: Promise<CallbackResultType | void | null>,
|
promise,
|
||||||
saveErrors?: (errors: Array<Error>) => void
|
saveErrors,
|
||||||
): Promise<void> {
|
targetTimestamp,
|
||||||
|
}: {
|
||||||
|
promise: Promise<CallbackResultType | void | null>;
|
||||||
|
saveErrors?: (errors: Array<Error>) => void;
|
||||||
|
targetTimestamp: number;
|
||||||
|
}): Promise<void> {
|
||||||
const updateLeftPane =
|
const updateLeftPane =
|
||||||
this.getConversation()?.debouncedUpdateLastMessage ?? noop;
|
this.getConversation()?.debouncedUpdateLastMessage ?? noop;
|
||||||
|
|
||||||
|
@ -926,7 +957,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendStateByConversationId = {
|
const sendStateByConversationId = {
|
||||||
...(this.get('sendStateByConversationId') || {}),
|
...(getPropForTimestamp({
|
||||||
|
log,
|
||||||
|
message: this,
|
||||||
|
prop: 'sendStateByConversationId',
|
||||||
|
targetTimestamp,
|
||||||
|
}) || {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendIsNotFinal =
|
const sendIsNotFinal =
|
||||||
|
@ -970,9 +1006,13 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Integrate sends via sealed sender
|
// Integrate sends via sealed sender
|
||||||
|
const latestEditTimestamp = this.get('editMessageTimestamp');
|
||||||
|
const sendIsLatest =
|
||||||
|
!latestEditTimestamp || targetTimestamp === latestEditTimestamp;
|
||||||
const previousUnidentifiedDeliveries =
|
const previousUnidentifiedDeliveries =
|
||||||
this.get('unidentifiedDeliveries') || [];
|
this.get('unidentifiedDeliveries') || [];
|
||||||
const newUnidentifiedDeliveries =
|
const newUnidentifiedDeliveries =
|
||||||
|
sendIsLatest &&
|
||||||
sendIsFinal &&
|
sendIsFinal &&
|
||||||
'unidentifiedDeliveries' in result.value &&
|
'unidentifiedDeliveries' in result.value &&
|
||||||
Array.isArray(result.value.unidentifiedDeliveries)
|
Array.isArray(result.value.unidentifiedDeliveries)
|
||||||
|
@ -1051,7 +1091,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
attributesToUpdate.sendStateByConversationId = sendStateByConversationId;
|
|
||||||
// Only update the expirationStartTimestamp if we don't already have one set
|
// Only update the expirationStartTimestamp if we don't already have one set
|
||||||
if (!this.get('expirationStartTimestamp')) {
|
if (!this.get('expirationStartTimestamp')) {
|
||||||
attributesToUpdate.expirationStartTimestamp = sentToAtLeastOneRecipient
|
attributesToUpdate.expirationStartTimestamp = sentToAtLeastOneRecipient
|
||||||
|
@ -1073,6 +1112,14 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
void this.saveErrors(errorsToSave, { skipSave: true });
|
void this.saveErrors(errorsToSave, { skipSave: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPropForTimestamp({
|
||||||
|
log,
|
||||||
|
message: this,
|
||||||
|
prop: 'sendStateByConversationId',
|
||||||
|
targetTimestamp,
|
||||||
|
value: sendStateByConversationId,
|
||||||
|
});
|
||||||
|
|
||||||
if (!this.doNotSave) {
|
if (!this.doNotSave) {
|
||||||
await window.Signal.Data.saveMessage(this.attributes, {
|
await window.Signal.Data.saveMessage(this.attributes, {
|
||||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||||
|
@ -1082,13 +1129,14 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
updateLeftPane();
|
updateLeftPane();
|
||||||
|
|
||||||
if (sentToAtLeastOneRecipient && !this.doNotSendSyncMessage) {
|
if (sentToAtLeastOneRecipient && !this.doNotSendSyncMessage) {
|
||||||
promises.push(this.sendSyncMessage());
|
promises.push(this.sendSyncMessage(targetTimestamp));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
const isTotalSuccess: boolean =
|
const isTotalSuccess: boolean =
|
||||||
result.success && !this.get('errors')?.length;
|
result.success && !this.get('errors')?.length;
|
||||||
|
|
||||||
if (isTotalSuccess) {
|
if (isTotalSuccess) {
|
||||||
delete this.cachedOutgoingContactData;
|
delete this.cachedOutgoingContactData;
|
||||||
delete this.cachedOutgoingPreviewData;
|
delete this.cachedOutgoingPreviewData;
|
||||||
|
@ -1099,10 +1147,15 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
updateLeftPane();
|
updateLeftPane();
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendSyncMessageOnly(
|
async sendSyncMessageOnly({
|
||||||
dataMessage: Uint8Array,
|
targetTimestamp,
|
||||||
saveErrors?: (errors: Array<Error>) => void
|
dataMessage,
|
||||||
): Promise<CallbackResultType | void> {
|
saveErrors,
|
||||||
|
}: {
|
||||||
|
targetTimestamp: number;
|
||||||
|
dataMessage: Uint8Array;
|
||||||
|
saveErrors?: (errors: Array<Error>) => void;
|
||||||
|
}): Promise<CallbackResultType | void> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const conv = this.getConversation()!;
|
const conv = this.getConversation()!;
|
||||||
this.set({ dataMessage });
|
this.set({ dataMessage });
|
||||||
|
@ -1115,7 +1168,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
expirationStartTimestamp: Date.now(),
|
expirationStartTimestamp: Date.now(),
|
||||||
errors: [],
|
errors: [],
|
||||||
});
|
});
|
||||||
const result = await this.sendSyncMessage();
|
const result = await this.sendSyncMessage(targetTimestamp);
|
||||||
this.set({
|
this.set({
|
||||||
// We have to do this afterward, since we didn't have a previous send!
|
// We have to do this afterward, since we didn't have a previous send!
|
||||||
unidentifiedDeliveries:
|
unidentifiedDeliveries:
|
||||||
|
@ -1147,7 +1200,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendSyncMessage(): Promise<CallbackResultType | void> {
|
async sendSyncMessage(
|
||||||
|
targetTimestamp: number
|
||||||
|
): Promise<CallbackResultType | void> {
|
||||||
const ourConversation =
|
const ourConversation =
|
||||||
window.ConversationController.getOurConversationOrThrow();
|
window.ConversationController.getOurConversationOrThrow();
|
||||||
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
const sendOptions = await getSendOptions(ourConversation.attributes, {
|
||||||
|
@ -1173,8 +1228,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
if (!dataMessage) {
|
if (!dataMessage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isEditedMessage = Boolean(this.get('editHistory'));
|
const wasEditSent = hasEditBeenSent(this);
|
||||||
const isUpdate = Boolean(this.get('synced')) && !isEditedMessage;
|
const isUpdate = Boolean(this.get('synced')) && !wasEditSent;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const conv = this.getConversation()!;
|
const conv = this.getConversation()!;
|
||||||
|
|
||||||
|
@ -1206,9 +1261,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
map(conversationsWithSealedSender, c => c.id)
|
map(conversationsWithSealedSender, c => c.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
const timestamp = getMessageSentTimestamp(this.attributes, { log });
|
const encodedContent = wasEditSent
|
||||||
|
|
||||||
const encodedContent = isEditedMessage
|
|
||||||
? {
|
? {
|
||||||
encodedEditMessage: dataMessage,
|
encodedEditMessage: dataMessage,
|
||||||
}
|
}
|
||||||
|
@ -1219,7 +1272,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
return handleMessageSend(
|
return handleMessageSend(
|
||||||
messaging.sendSyncMessage({
|
messaging.sendSyncMessage({
|
||||||
...encodedContent,
|
...encodedContent,
|
||||||
timestamp,
|
timestamp: targetTimestamp,
|
||||||
destination: conv.get('e164'),
|
destination: conv.get('e164'),
|
||||||
destinationServiceId: conv.getServiceId(),
|
destinationServiceId: conv.getServiceId(),
|
||||||
expirationStartTimestamp:
|
expirationStartTimestamp:
|
||||||
|
|
|
@ -183,7 +183,10 @@ describe('Message', () => {
|
||||||
editMessage: undefined,
|
editMessage: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
await message.send(promise);
|
await message.send({
|
||||||
|
promise,
|
||||||
|
targetTimestamp: message.get('timestamp'),
|
||||||
|
});
|
||||||
|
|
||||||
const result = message.get('sendStateByConversationId') || {};
|
const result = message.get('sendStateByConversationId') || {};
|
||||||
assert.hasAllKeys(result, [
|
assert.hasAllKeys(result, [
|
||||||
|
@ -203,7 +206,10 @@ describe('Message', () => {
|
||||||
const message = createMessage({ type: 'outgoing', source });
|
const message = createMessage({ type: 'outgoing', source });
|
||||||
|
|
||||||
const promise = Promise.reject(new Error('foo bar'));
|
const promise = Promise.reject(new Error('foo bar'));
|
||||||
await message.send(promise);
|
await message.send({
|
||||||
|
promise,
|
||||||
|
targetTimestamp: message.get('timestamp'),
|
||||||
|
});
|
||||||
|
|
||||||
const errors = message.get('errors') || [];
|
const errors = message.get('errors') || [];
|
||||||
assert.lengthOf(errors, 1);
|
assert.lengthOf(errors, 1);
|
||||||
|
@ -217,7 +223,10 @@ describe('Message', () => {
|
||||||
errors: [new Error('baz qux')],
|
errors: [new Error('baz qux')],
|
||||||
};
|
};
|
||||||
const promise = Promise.reject(result);
|
const promise = Promise.reject(result);
|
||||||
await message.send(promise);
|
await message.send({
|
||||||
|
promise,
|
||||||
|
targetTimestamp: message.get('timestamp'),
|
||||||
|
});
|
||||||
|
|
||||||
const errors = message.get('errors') || [];
|
const errors = message.get('errors') || [];
|
||||||
assert.lengthOf(errors, 1);
|
assert.lengthOf(errors, 1);
|
||||||
|
|
|
@ -766,8 +766,8 @@ describe('editing', function (this: Mocha.Suite) {
|
||||||
strictAssert(v2.sendStateByConversationId, 'v2 has send state');
|
strictAssert(v2.sendStateByConversationId, 'v2 has send state');
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
v2.sendStateByConversationId[conversationId].status,
|
v2.sendStateByConversationId[conversationId].status,
|
||||||
SendStatus.Pending, // TODO (DESKTOP-6176) - this should be Sent!
|
SendStatus.Sent,
|
||||||
'send state for v2 message is pending'
|
'send state for v2 message is sent'
|
||||||
);
|
);
|
||||||
|
|
||||||
strictAssert(v3.sendStateByConversationId, 'v3 has send state');
|
strictAssert(v3.sendStateByConversationId, 'v3 has send state');
|
||||||
|
@ -780,8 +780,8 @@ describe('editing', function (this: Mocha.Suite) {
|
||||||
strictAssert(v4.sendStateByConversationId, 'v4 has send state');
|
strictAssert(v4.sendStateByConversationId, 'v4 has send state');
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
v4.sendStateByConversationId[conversationId].status,
|
v4.sendStateByConversationId[conversationId].status,
|
||||||
SendStatus.Pending, // TODO (DESKTOP-6176) - this should be Sent!
|
SendStatus.Sent,
|
||||||
'send state for v4 message is pending'
|
'send state for v4 message is sent'
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
|
|
|
@ -164,7 +164,6 @@ export type MessageOptionsType = {
|
||||||
body?: string;
|
body?: string;
|
||||||
bodyRanges?: ReadonlyArray<RawBodyRange>;
|
bodyRanges?: ReadonlyArray<RawBodyRange>;
|
||||||
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
|
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
|
||||||
editedMessageTimestamp?: number;
|
|
||||||
expireTimer?: DurationInSeconds;
|
expireTimer?: DurationInSeconds;
|
||||||
flags?: number;
|
flags?: number;
|
||||||
group?: {
|
group?: {
|
||||||
|
@ -180,6 +179,7 @@ export type MessageOptionsType = {
|
||||||
sticker?: OutgoingStickerType;
|
sticker?: OutgoingStickerType;
|
||||||
reaction?: ReactionType;
|
reaction?: ReactionType;
|
||||||
deletedForEveryoneTimestamp?: number;
|
deletedForEveryoneTimestamp?: number;
|
||||||
|
targetTimestampForEdit?: number;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
groupCallUpdate?: GroupCallUpdateType;
|
groupCallUpdate?: GroupCallUpdateType;
|
||||||
storyContext?: StoryContextType;
|
storyContext?: StoryContextType;
|
||||||
|
@ -189,7 +189,7 @@ export type GroupSendOptionsType = {
|
||||||
bodyRanges?: ReadonlyArray<RawBodyRange>;
|
bodyRanges?: ReadonlyArray<RawBodyRange>;
|
||||||
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
|
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
|
||||||
deletedForEveryoneTimestamp?: number;
|
deletedForEveryoneTimestamp?: number;
|
||||||
editedMessageTimestamp?: number;
|
targetTimestampForEdit?: number;
|
||||||
expireTimer?: DurationInSeconds;
|
expireTimer?: DurationInSeconds;
|
||||||
flags?: number;
|
flags?: number;
|
||||||
groupCallUpdate?: GroupCallUpdateType;
|
groupCallUpdate?: GroupCallUpdateType;
|
||||||
|
@ -692,11 +692,11 @@ export default class MessageSender {
|
||||||
const message = await this.getHydratedMessage(options);
|
const message = await this.getHydratedMessage(options);
|
||||||
const dataMessage = message.toProto();
|
const dataMessage = message.toProto();
|
||||||
|
|
||||||
if (options.editedMessageTimestamp) {
|
if (options.targetTimestampForEdit) {
|
||||||
const editMessage = new Proto.EditMessage();
|
const editMessage = new Proto.EditMessage();
|
||||||
editMessage.dataMessage = dataMessage;
|
editMessage.dataMessage = dataMessage;
|
||||||
editMessage.targetSentTimestamp = Long.fromNumber(
|
editMessage.targetSentTimestamp = Long.fromNumber(
|
||||||
options.editedMessageTimestamp
|
options.targetTimestampForEdit
|
||||||
);
|
);
|
||||||
return Proto.EditMessage.encode(editMessage).finish();
|
return Proto.EditMessage.encode(editMessage).finish();
|
||||||
}
|
}
|
||||||
|
@ -768,11 +768,11 @@ export default class MessageSender {
|
||||||
const dataMessage = message.toProto();
|
const dataMessage = message.toProto();
|
||||||
|
|
||||||
const contentMessage = new Proto.Content();
|
const contentMessage = new Proto.Content();
|
||||||
if (options.editedMessageTimestamp) {
|
if (options.targetTimestampForEdit) {
|
||||||
const editMessage = new Proto.EditMessage();
|
const editMessage = new Proto.EditMessage();
|
||||||
editMessage.dataMessage = dataMessage;
|
editMessage.dataMessage = dataMessage;
|
||||||
editMessage.targetSentTimestamp = Long.fromNumber(
|
editMessage.targetSentTimestamp = Long.fromNumber(
|
||||||
options.editedMessageTimestamp
|
options.targetTimestampForEdit
|
||||||
);
|
);
|
||||||
contentMessage.editMessage = editMessage;
|
contentMessage.editMessage = editMessage;
|
||||||
} else {
|
} else {
|
||||||
|
@ -858,7 +858,6 @@ export default class MessageSender {
|
||||||
bodyRanges,
|
bodyRanges,
|
||||||
contact,
|
contact,
|
||||||
deletedForEveryoneTimestamp,
|
deletedForEveryoneTimestamp,
|
||||||
editedMessageTimestamp,
|
|
||||||
expireTimer,
|
expireTimer,
|
||||||
flags,
|
flags,
|
||||||
groupCallUpdate,
|
groupCallUpdate,
|
||||||
|
@ -870,6 +869,7 @@ export default class MessageSender {
|
||||||
reaction,
|
reaction,
|
||||||
sticker,
|
sticker,
|
||||||
storyContext,
|
storyContext,
|
||||||
|
targetTimestampForEdit,
|
||||||
timestamp,
|
timestamp,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
@ -900,7 +900,6 @@ export default class MessageSender {
|
||||||
body: messageText,
|
body: messageText,
|
||||||
contact,
|
contact,
|
||||||
deletedForEveryoneTimestamp,
|
deletedForEveryoneTimestamp,
|
||||||
editedMessageTimestamp,
|
|
||||||
expireTimer,
|
expireTimer,
|
||||||
flags,
|
flags,
|
||||||
groupCallUpdate,
|
groupCallUpdate,
|
||||||
|
@ -912,6 +911,7 @@ export default class MessageSender {
|
||||||
recipients,
|
recipients,
|
||||||
sticker,
|
sticker,
|
||||||
storyContext,
|
storyContext,
|
||||||
|
targetTimestampForEdit,
|
||||||
timestamp,
|
timestamp,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1133,7 +1133,6 @@ export default class MessageSender {
|
||||||
contact,
|
contact,
|
||||||
contentHint,
|
contentHint,
|
||||||
deletedForEveryoneTimestamp,
|
deletedForEveryoneTimestamp,
|
||||||
editedMessageTimestamp,
|
|
||||||
expireTimer,
|
expireTimer,
|
||||||
groupId,
|
groupId,
|
||||||
serviceId,
|
serviceId,
|
||||||
|
@ -1146,6 +1145,7 @@ export default class MessageSender {
|
||||||
sticker,
|
sticker,
|
||||||
storyContext,
|
storyContext,
|
||||||
story,
|
story,
|
||||||
|
targetTimestampForEdit,
|
||||||
timestamp,
|
timestamp,
|
||||||
urgent,
|
urgent,
|
||||||
includePniSignatureMessage,
|
includePniSignatureMessage,
|
||||||
|
@ -1155,7 +1155,6 @@ export default class MessageSender {
|
||||||
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
|
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
|
||||||
contentHint: number;
|
contentHint: number;
|
||||||
deletedForEveryoneTimestamp: number | undefined;
|
deletedForEveryoneTimestamp: number | undefined;
|
||||||
editedMessageTimestamp?: number;
|
|
||||||
expireTimer: DurationInSeconds | undefined;
|
expireTimer: DurationInSeconds | undefined;
|
||||||
groupId: string | undefined;
|
groupId: string | undefined;
|
||||||
serviceId: ServiceIdString;
|
serviceId: ServiceIdString;
|
||||||
|
@ -1168,6 +1167,7 @@ export default class MessageSender {
|
||||||
sticker?: OutgoingStickerType;
|
sticker?: OutgoingStickerType;
|
||||||
storyContext?: StoryContextType;
|
storyContext?: StoryContextType;
|
||||||
story?: boolean;
|
story?: boolean;
|
||||||
|
targetTimestampForEdit?: number;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
urgent: boolean;
|
urgent: boolean;
|
||||||
includePniSignatureMessage?: boolean;
|
includePniSignatureMessage?: boolean;
|
||||||
|
@ -1179,7 +1179,6 @@ export default class MessageSender {
|
||||||
body: messageText,
|
body: messageText,
|
||||||
contact,
|
contact,
|
||||||
deletedForEveryoneTimestamp,
|
deletedForEveryoneTimestamp,
|
||||||
editedMessageTimestamp,
|
|
||||||
expireTimer,
|
expireTimer,
|
||||||
preview,
|
preview,
|
||||||
profileKey,
|
profileKey,
|
||||||
|
@ -1188,6 +1187,7 @@ export default class MessageSender {
|
||||||
recipients: [serviceId],
|
recipients: [serviceId],
|
||||||
sticker,
|
sticker,
|
||||||
storyContext,
|
storyContext,
|
||||||
|
targetTimestampForEdit,
|
||||||
timestamp,
|
timestamp,
|
||||||
},
|
},
|
||||||
contentHint,
|
contentHint,
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { DAY } from './durations';
|
||||||
import { canEditMessages } from './canEditMessages';
|
import { canEditMessages } from './canEditMessages';
|
||||||
import { isMoreRecentThan } from './timestamp';
|
import { isMoreRecentThan } from './timestamp';
|
||||||
import { isOutgoing } from '../messages/helpers';
|
import { isOutgoing } from '../messages/helpers';
|
||||||
import { isSent, someSendStatus } from '../messages/MessageSendState';
|
|
||||||
|
|
||||||
export const MESSAGE_MAX_EDIT_COUNT = 10;
|
export const MESSAGE_MAX_EDIT_COUNT = 10;
|
||||||
|
|
||||||
|
@ -16,7 +15,6 @@ export function canEditMessage(message: MessageAttributesType): boolean {
|
||||||
!message.deletedForEveryone &&
|
!message.deletedForEveryone &&
|
||||||
isOutgoing(message) &&
|
isOutgoing(message) &&
|
||||||
isMoreRecentThan(message.sent_at, DAY) &&
|
isMoreRecentThan(message.sent_at, DAY) &&
|
||||||
someSendStatus(message.sendStateByConversationId, isSent) &&
|
|
||||||
Boolean(message.body);
|
Boolean(message.body);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
|
|
143
ts/util/editHelpers.ts
Normal file
143
ts/util/editHelpers.ts
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { isNumber, sortBy } from 'lodash';
|
||||||
|
|
||||||
|
import { strictAssert } from './assert';
|
||||||
|
import { isSent } from '../messages/MessageSendState';
|
||||||
|
|
||||||
|
import type { EditHistoryType } from '../model-types';
|
||||||
|
import type { MessageModel } from '../models/messages';
|
||||||
|
import type { LoggerType } from '../types/Logging';
|
||||||
|
|
||||||
|
export function hasEditBeenSent(message: MessageModel): boolean {
|
||||||
|
const originalTimestamp = message.get('timestamp');
|
||||||
|
const editHistory = message.get('editHistory') || [];
|
||||||
|
|
||||||
|
return Boolean(
|
||||||
|
editHistory.find(item => {
|
||||||
|
if (item.timestamp === originalTimestamp) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Object.values(item.sendStateByConversationId || {}).some(
|
||||||
|
sendState => isSent(sendState.status)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tricky bit for this function is if we are on our second+ attempt to send a given
|
||||||
|
// edit, we're still sending that edit.
|
||||||
|
export function getTargetOfThisEditTimestamp({
|
||||||
|
message,
|
||||||
|
targetTimestamp,
|
||||||
|
}: {
|
||||||
|
message: MessageModel;
|
||||||
|
targetTimestamp: number;
|
||||||
|
}): number {
|
||||||
|
const originalTimestamp = message.get('timestamp');
|
||||||
|
const editHistory = message.get('editHistory') || [];
|
||||||
|
|
||||||
|
const sentItems = editHistory.filter(item => {
|
||||||
|
return item.timestamp <= targetTimestamp;
|
||||||
|
});
|
||||||
|
const mostRecent = sortBy(
|
||||||
|
sentItems,
|
||||||
|
(item: EditHistoryType) => item.timestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
const { length } = mostRecent;
|
||||||
|
|
||||||
|
// We want the second-to-last item, because we may have partially sent targetTimestamp
|
||||||
|
if (length > 1) {
|
||||||
|
return mostRecent[length - 2].timestamp;
|
||||||
|
}
|
||||||
|
// If there's only one item, we'll use it
|
||||||
|
if (length > 0) {
|
||||||
|
return mostRecent[length - 1].timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a good failover in case we ever stop duplicating data in editHistory
|
||||||
|
return originalTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPropForTimestamp<T extends keyof EditHistoryType>({
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
prop,
|
||||||
|
targetTimestamp,
|
||||||
|
}: {
|
||||||
|
log: LoggerType;
|
||||||
|
message: MessageModel;
|
||||||
|
prop: T;
|
||||||
|
targetTimestamp: number;
|
||||||
|
}): EditHistoryType[T] {
|
||||||
|
const logId = `getPropForTimestamp(${message.idForLogging()}, target=${targetTimestamp}})`;
|
||||||
|
|
||||||
|
const editHistory = message.get('editHistory');
|
||||||
|
const targetEdit = editHistory?.find(
|
||||||
|
item => item.timestamp === targetTimestamp
|
||||||
|
);
|
||||||
|
if (!targetEdit) {
|
||||||
|
if (editHistory) {
|
||||||
|
log.warn(`${logId}: No edit found, using top-level data`);
|
||||||
|
}
|
||||||
|
return message.get(prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetEdit[prop];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setPropForTimestamp<T extends keyof EditHistoryType>({
|
||||||
|
log,
|
||||||
|
message,
|
||||||
|
prop,
|
||||||
|
targetTimestamp,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
log: LoggerType;
|
||||||
|
message: MessageModel;
|
||||||
|
prop: T;
|
||||||
|
targetTimestamp: number;
|
||||||
|
value: EditHistoryType[T];
|
||||||
|
}): void {
|
||||||
|
const logId = `setPropForTimestamp(${message.idForLogging()}, target=${targetTimestamp}})`;
|
||||||
|
|
||||||
|
const editHistory = message.get('editHistory');
|
||||||
|
const targetEditIndex = editHistory?.findIndex(
|
||||||
|
item => item.timestamp === targetTimestamp
|
||||||
|
);
|
||||||
|
const targetEdit =
|
||||||
|
editHistory && isNumber(targetEditIndex)
|
||||||
|
? editHistory[targetEditIndex]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (!targetEdit) {
|
||||||
|
if (editHistory) {
|
||||||
|
log.warn(`${logId}: No edit found, updating top-level data`);
|
||||||
|
}
|
||||||
|
message.set({
|
||||||
|
[prop]: value,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
strictAssert(editHistory, 'Got targetEdit, but no editHistory');
|
||||||
|
strictAssert(
|
||||||
|
isNumber(targetEditIndex),
|
||||||
|
'Got targetEdit, but no targetEditIndex'
|
||||||
|
);
|
||||||
|
|
||||||
|
const newEditHistory = [...editHistory];
|
||||||
|
newEditHistory[targetEditIndex] = { ...targetEdit, [prop]: value };
|
||||||
|
|
||||||
|
message.set('editHistory', newEditHistory);
|
||||||
|
|
||||||
|
// We always edit the top-level attribute if this is the most recent send
|
||||||
|
const editMessageTimestamp = message.get('editMessageTimestamp');
|
||||||
|
if (!editMessageTimestamp || editMessageTimestamp === targetTimestamp) {
|
||||||
|
message.set({
|
||||||
|
[prop]: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -218,7 +218,7 @@ export async function sendEditedMessage(
|
||||||
conversationId,
|
conversationId,
|
||||||
messageId: targetMessageId,
|
messageId: targetMessageId,
|
||||||
revision: conversation.get('revision'),
|
revision: conversation.get('revision'),
|
||||||
editedMessageTimestamp: targetSentTimestamp,
|
editedMessageTimestamp: timestamp,
|
||||||
},
|
},
|
||||||
async jobToInsert => {
|
async jobToInsert => {
|
||||||
log.info(
|
log.info(
|
||||||
|
@ -246,7 +246,7 @@ export async function sendEditedMessage(
|
||||||
now: timestamp,
|
now: timestamp,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
duration => `${idLog}: batchDisptach took ${duration}ms`
|
duration => `${idLog}: batchDispatch took ${duration}ms`
|
||||||
);
|
);
|
||||||
|
|
||||||
window.Signal.Data.updateConversation(conversation.attributes);
|
window.Signal.Data.updateConversation(conversation.attributes);
|
||||||
|
|
|
@ -819,6 +819,11 @@ export function _shouldFailSend(error: unknown, logId: string): boolean {
|
||||||
// SendMessageChallengeError
|
// SendMessageChallengeError
|
||||||
// MessageError
|
// MessageError
|
||||||
if (isRecord(error) && typeof error.code === 'number') {
|
if (isRecord(error) && typeof error.code === 'number') {
|
||||||
|
if (error.code === -1) {
|
||||||
|
logError("We don't have connectivity. Failing.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (error.code === 400) {
|
if (error.code === 400) {
|
||||||
logError('Invalid request, failing.');
|
logError('Invalid request, failing.');
|
||||||
return true;
|
return true;
|
||||||
|
|
Loading…
Reference in a new issue