Send edited messages support

Co-authored-by: Fedor Indutnyy <indutny@signal.org>
This commit is contained in:
Josh Perez 2023-04-20 12:31:59 -04:00 committed by GitHub
parent d380817a44
commit 1f2cde6d04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
79 changed files with 2507 additions and 1175 deletions

View file

@ -13,7 +13,6 @@ import {
SenderKeyDistributionMessage,
} from '@signalapp/libsignal-client';
import type { QuotedMessageType } from '../model-types.d';
import type { ConversationModel } from '../models/conversations';
import { GLOBAL_ZONE } from '../SignalProtocolStore';
import { assertDev, strictAssert } from '../util/assert';
@ -21,9 +20,10 @@ import { parseIntOrThrow } from '../util/parseIntOrThrow';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { SenderKeys } from '../LibSignalStores';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import { MIMETypeToString } from '../types/MIME';
import type * as Attachment from '../types/Attachment';
import type {
TextAttachmentType,
UploadedAttachmentType,
} from '../types/Attachment';
import type { UUID } from '../types/UUID';
import type {
ChallengeType,
@ -49,7 +49,7 @@ import type {
} from './OutgoingMessage';
import OutgoingMessage from './OutgoingMessage';
import * as Bytes from '../Bytes';
import { getRandomBytes, getZeroes, encryptAttachment } from '../Crypto';
import { getRandomBytes } from '../Crypto';
import {
MessageError,
SignedPreKeyRotationError,
@ -57,8 +57,8 @@ import {
HTTPError,
NoSenderKeyError,
} from './Errors';
import type { RawBodyRange } from '../types/BodyRange';
import { BodyRange } from '../types/BodyRange';
import type { RawBodyRange } from '../types/BodyRange';
import type { StoryContextType } from '../types/Util';
import type {
LinkPreviewImage,
@ -71,13 +71,12 @@ import { uuidToBytes } from '../util/uuidToBytes';
import type { DurationInSeconds } from '../util/durations';
import { SignalService as Proto } from '../protobuf';
import * as log from '../logging/log';
import type { Avatar, EmbeddedContactType } from '../types/EmbeddedContact';
import type { EmbeddedContactWithUploadedAvatar } from '../types/EmbeddedContact';
import {
numberToPhoneType,
numberToEmailType,
numberToAddressType,
} from '../types/EmbeddedContact';
import type { StickerWithHydratedData } from '../types/Stickers';
import { missingCaseError } from '../util/missingCaseError';
export type SendMetadataType = {
@ -92,9 +91,33 @@ export type SendOptionsType = {
online?: boolean;
};
type QuoteAttachmentType = {
thumbnail?: AttachmentType;
attachmentPointer?: Proto.IAttachmentPointer;
export type OutgoingQuoteAttachmentType = Readonly<{
contentType: string;
fileName?: string;
thumbnail: UploadedAttachmentType;
}>;
export type OutgoingQuoteType = Readonly<{
isGiftBadge?: boolean;
id?: number;
authorUuid?: string;
text?: string;
attachments: ReadonlyArray<OutgoingQuoteAttachmentType>;
bodyRanges?: ReadonlyArray<RawBodyRange>;
}>;
export type OutgoingLinkPreviewType = Readonly<{
title?: string;
description?: string;
domain?: string;
url: string;
isStickerPack?: boolean;
image?: Readonly<UploadedAttachmentType>;
date?: number;
}>;
export type OutgoingTextAttachmentType = Omit<TextAttachmentType, 'preview'> & {
preview?: OutgoingLinkPreviewType;
};
export type GroupV2InfoType = {
@ -108,9 +131,13 @@ type GroupCallUpdateType = {
eraId: string;
};
export type StickerType = StickerWithHydratedData & {
attachmentPointer?: Proto.IAttachmentPointer;
};
export type OutgoingStickerType = Readonly<{
packId: string;
packKey: string;
stickerId: number;
emoji?: string;
data: Readonly<UploadedAttachmentType>;
}>;
export type ReactionType = {
emoji?: string;
@ -119,22 +146,6 @@ export type ReactionType = {
targetTimestamp?: number;
};
export type AttachmentType = {
size: number;
data: Uint8Array;
contentType: string;
fileName?: string;
flags?: number;
width?: number;
height?: number;
caption?: string;
attachmentPointer?: Proto.IAttachmentPointer;
blurHash?: string;
};
export const singleProtoJobDataSchema = z.object({
contentHint: z.number(),
identifier: z.string(),
@ -147,35 +158,12 @@ export const singleProtoJobDataSchema = z.object({
export type SingleProtoJobData = z.infer<typeof singleProtoJobDataSchema>;
function makeAttachmentSendReady(
attachment: Attachment.AttachmentType
): AttachmentType | undefined {
const { data } = attachment;
if (!data) {
throw new Error(
'makeAttachmentSendReady: Missing data, returning undefined'
);
}
return {
...attachment,
contentType: MIMETypeToString(attachment.contentType),
data,
};
}
export type ContactWithHydratedAvatar = EmbeddedContactType & {
avatar?: Avatar & {
attachmentPointer?: Proto.IAttachmentPointer;
};
};
export type MessageOptionsType = {
attachments?: ReadonlyArray<AttachmentType> | null;
attachments?: ReadonlyArray<UploadedAttachmentType>;
body?: string;
bodyRanges?: ReadonlyArray<RawBodyRange>;
contact?: Array<ContactWithHydratedAvatar>;
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
editedMessageTimestamp?: number;
expireTimer?: DurationInSeconds;
flags?: number;
group?: {
@ -184,11 +172,11 @@ export type MessageOptionsType = {
};
groupV2?: GroupV2InfoType;
needsSync?: boolean;
preview?: ReadonlyArray<LinkPreviewType>;
preview?: ReadonlyArray<OutgoingLinkPreviewType>;
profileKey?: Uint8Array;
quote?: QuotedMessageType | null;
quote?: OutgoingQuoteType;
recipients: ReadonlyArray<string>;
sticker?: StickerWithHydratedData;
sticker?: OutgoingStickerType;
reaction?: ReactionType;
deletedForEveryoneTimestamp?: number;
timestamp: number;
@ -196,32 +184,33 @@ export type MessageOptionsType = {
storyContext?: StoryContextType;
};
export type GroupSendOptionsType = {
attachments?: Array<AttachmentType>;
attachments?: ReadonlyArray<UploadedAttachmentType>;
bodyRanges?: ReadonlyArray<RawBodyRange>;
contact?: Array<ContactWithHydratedAvatar>;
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
deletedForEveryoneTimestamp?: number;
editedMessageTimestamp?: number;
expireTimer?: DurationInSeconds;
flags?: number;
groupCallUpdate?: GroupCallUpdateType;
groupV2?: GroupV2InfoType;
messageText?: string;
preview?: ReadonlyArray<LinkPreviewType>;
preview?: ReadonlyArray<OutgoingLinkPreviewType>;
profileKey?: Uint8Array;
quote?: QuotedMessageType | null;
quote?: OutgoingQuoteType;
reaction?: ReactionType;
sticker?: StickerWithHydratedData;
sticker?: OutgoingStickerType;
storyContext?: StoryContextType;
timestamp: number;
};
class Message {
attachments: ReadonlyArray<AttachmentType>;
attachments: ReadonlyArray<UploadedAttachmentType>;
body?: string;
bodyRanges?: ReadonlyArray<RawBodyRange>;
contact?: Array<ContactWithHydratedAvatar>;
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
expireTimer?: DurationInSeconds;
@ -236,15 +225,15 @@ class Message {
needsSync?: boolean;
preview?: ReadonlyArray<LinkPreviewType>;
preview?: ReadonlyArray<OutgoingLinkPreviewType>;
profileKey?: Uint8Array;
quote?: QuotedMessageType | null;
quote?: OutgoingQuoteType;
recipients: ReadonlyArray<string>;
sticker?: StickerType;
sticker?: OutgoingStickerType;
reaction?: ReactionType;
@ -252,8 +241,6 @@ class Message {
dataMessage?: Proto.DataMessage;
attachmentPointers: Array<Proto.IAttachmentPointer> = [];
deletedForEveryoneTimestamp?: number;
groupCallUpdate?: GroupCallUpdateType;
@ -346,7 +333,7 @@ class Message {
const proto = new Proto.DataMessage();
proto.timestamp = Long.fromNumber(this.timestamp);
proto.attachments = this.attachmentPointers;
proto.attachments = this.attachments.slice();
if (this.body) {
proto.body = this.body;
@ -383,10 +370,7 @@ class Message {
proto.sticker.packKey = Bytes.fromBase64(this.sticker.packKey);
proto.sticker.stickerId = this.sticker.stickerId;
proto.sticker.emoji = this.sticker.emoji;
if (this.sticker.attachmentPointer) {
proto.sticker.data = this.sticker.attachmentPointer;
}
proto.sticker.data = this.sticker.data;
}
if (this.reaction) {
proto.reaction = new Proto.DataMessage.Reaction();
@ -406,82 +390,83 @@ class Message {
item.url = preview.url;
item.description = preview.description || null;
item.date = preview.date || null;
if (preview.attachmentPointer) {
item.image = preview.attachmentPointer;
if (preview.image) {
item.image = preview.image;
}
return item;
});
}
if (Array.isArray(this.contact)) {
proto.contact = this.contact.map(contact => {
const contactProto = new Proto.DataMessage.Contact();
if (contact.name) {
const nameProto: Proto.DataMessage.Contact.IName = {
givenName: contact.name.givenName,
familyName: contact.name.familyName,
prefix: contact.name.prefix,
suffix: contact.name.suffix,
middleName: contact.name.middleName,
displayName: contact.name.displayName,
};
contactProto.name = new Proto.DataMessage.Contact.Name(nameProto);
}
if (Array.isArray(contact.number)) {
contactProto.number = contact.number.map(number => {
const numberProto: Proto.DataMessage.Contact.IPhone = {
value: number.value,
type: numberToPhoneType(number.type),
label: number.label,
proto.contact = this.contact.map(
(contact: EmbeddedContactWithUploadedAvatar) => {
const contactProto = new Proto.DataMessage.Contact();
if (contact.name) {
const nameProto: Proto.DataMessage.Contact.IName = {
givenName: contact.name.givenName,
familyName: contact.name.familyName,
prefix: contact.name.prefix,
suffix: contact.name.suffix,
middleName: contact.name.middleName,
displayName: contact.name.displayName,
};
contactProto.name = new Proto.DataMessage.Contact.Name(nameProto);
}
if (Array.isArray(contact.number)) {
contactProto.number = contact.number.map(number => {
const numberProto: Proto.DataMessage.Contact.IPhone = {
value: number.value,
type: numberToPhoneType(number.type),
label: number.label,
};
return new Proto.DataMessage.Contact.Phone(numberProto);
});
}
if (Array.isArray(contact.email)) {
contactProto.email = contact.email.map(email => {
const emailProto: Proto.DataMessage.Contact.IEmail = {
value: email.value,
type: numberToEmailType(email.type),
label: email.label,
};
return new Proto.DataMessage.Contact.Phone(numberProto);
});
}
if (Array.isArray(contact.email)) {
contactProto.email = contact.email.map(email => {
const emailProto: Proto.DataMessage.Contact.IEmail = {
value: email.value,
type: numberToEmailType(email.type),
label: email.label,
};
return new Proto.DataMessage.Contact.Email(emailProto);
});
}
if (Array.isArray(contact.address)) {
contactProto.address = contact.address.map(address => {
const addressProto: Proto.DataMessage.Contact.IPostalAddress = {
type: numberToAddressType(address.type),
label: address.label,
street: address.street,
pobox: address.pobox,
neighborhood: address.neighborhood,
city: address.city,
region: address.region,
postcode: address.postcode,
country: address.country,
};
return new Proto.DataMessage.Contact.Email(emailProto);
});
}
if (Array.isArray(contact.address)) {
contactProto.address = contact.address.map(address => {
const addressProto: Proto.DataMessage.Contact.IPostalAddress = {
type: numberToAddressType(address.type),
label: address.label,
street: address.street,
pobox: address.pobox,
neighborhood: address.neighborhood,
city: address.city,
region: address.region,
postcode: address.postcode,
country: address.country,
};
return new Proto.DataMessage.Contact.PostalAddress(addressProto);
});
}
if (contact.avatar && contact.avatar.attachmentPointer) {
const avatarProto = new Proto.DataMessage.Contact.Avatar();
avatarProto.avatar = contact.avatar.attachmentPointer;
avatarProto.isProfile = Boolean(contact.avatar.isProfile);
contactProto.avatar = avatarProto;
}
return new Proto.DataMessage.Contact.PostalAddress(addressProto);
});
}
if (contact.avatar?.avatar) {
const avatarProto = new Proto.DataMessage.Contact.Avatar();
avatarProto.avatar = contact.avatar.avatar;
avatarProto.isProfile = Boolean(contact.avatar.isProfile);
contactProto.avatar = avatarProto;
}
if (contact.organization) {
contactProto.organization = contact.organization;
}
if (contact.organization) {
contactProto.organization = contact.organization;
}
return contactProto;
});
return contactProto;
}
);
}
if (this.quote) {
const { QuotedAttachment } = Proto.DataMessage.Quote;
const { BodyRange: ProtoBodyRange, Quote } = Proto.DataMessage;
proto.quote = new Quote();
@ -497,21 +482,7 @@ class Message {
this.quote.id === undefined ? null : Long.fromNumber(this.quote.id);
quote.authorUuid = this.quote.authorUuid || null;
quote.text = this.quote.text || null;
quote.attachments = (this.quote.attachments || []).map(
(attachment: AttachmentType) => {
const quotedAttachment = new QuotedAttachment();
quotedAttachment.contentType = attachment.contentType;
if (attachment.fileName) {
quotedAttachment.fileName = attachment.fileName;
}
if (attachment.attachmentPointer) {
quotedAttachment.thumbnail = attachment.attachmentPointer;
}
return quotedAttachment;
}
);
quote.attachments = this.quote.attachments.slice() || [];
const bodyRanges = this.quote.bodyRanges || [];
quote.bodyRanges = bodyRanges.map(range => {
const bodyRange = new ProtoBodyRange();
@ -665,13 +636,6 @@ export default class MessageSender {
// Attachment upload functions
_getAttachmentSizeBucket(size: number): number {
return Math.max(
541,
Math.floor(1.05 ** Math.ceil(Math.log(size) / Math.log(1.05)))
);
}
static getRandomPadding(): Uint8Array {
// Generate a random int from 1 and 512
const buffer = getRandomBytes(2);
@ -681,216 +645,11 @@ export default class MessageSender {
return getRandomBytes(paddingLength);
}
getPaddedAttachment(data: Readonly<Uint8Array>): Uint8Array {
const size = data.byteLength;
const paddedSize = this._getAttachmentSizeBucket(size);
const padding = getZeroes(paddedSize - size);
return Bytes.concatenate([data, padding]);
}
async makeAttachmentPointer(
attachment: Readonly<
Partial<AttachmentType> &
Pick<AttachmentType, 'data' | 'size' | 'contentType'>
>
): Promise<Proto.IAttachmentPointer> {
assertDev(
typeof attachment === 'object' && attachment != null,
'Got null attachment in `makeAttachmentPointer`'
);
const { data, size, contentType } = attachment;
if (!(data instanceof Uint8Array)) {
throw new Error(
`makeAttachmentPointer: data was a '${typeof data}' instead of Uint8Array`
);
}
if (data.byteLength !== size) {
throw new Error(
`makeAttachmentPointer: Size ${size} did not match data.byteLength ${data.byteLength}`
);
}
if (typeof contentType !== 'string') {
throw new Error(
`makeAttachmentPointer: contentType ${contentType} was not a string`
);
}
const padded = this.getPaddedAttachment(data);
const key = getRandomBytes(64);
const result = encryptAttachment(padded, key);
const id = await this.server.putAttachment(result.ciphertext);
const proto = new Proto.AttachmentPointer();
proto.cdnId = Long.fromString(id);
proto.contentType = attachment.contentType;
proto.key = key;
proto.size = data.byteLength;
proto.digest = result.digest;
if (attachment.fileName) {
proto.fileName = attachment.fileName;
}
if (attachment.flags) {
proto.flags = attachment.flags;
}
if (attachment.width) {
proto.width = attachment.width;
}
if (attachment.height) {
proto.height = attachment.height;
}
if (attachment.caption) {
proto.caption = attachment.caption;
}
if (attachment.blurHash) {
proto.blurHash = attachment.blurHash;
}
return proto;
}
async uploadAttachments(message: Message): Promise<void> {
try {
// eslint-disable-next-line no-param-reassign
message.attachmentPointers = await Promise.all(
message.attachments.map(attachment =>
this.makeAttachmentPointer(attachment)
)
);
} catch (error) {
if (error instanceof HTTPError) {
throw new MessageError(message, error);
} else {
throw error;
}
}
}
async uploadLinkPreviews(message: Message): Promise<void> {
try {
const preview = await Promise.all(
(message.preview || []).map(async (item: Readonly<LinkPreviewType>) => {
if (!item.image) {
return item;
}
const attachment = makeAttachmentSendReady(item.image);
if (!attachment) {
return item;
}
return {
...item,
attachmentPointer: await this.makeAttachmentPointer(attachment),
};
})
);
// eslint-disable-next-line no-param-reassign
message.preview = preview;
} catch (error) {
if (error instanceof HTTPError) {
throw new MessageError(message, error);
} else {
throw error;
}
}
}
async uploadSticker(message: Message): Promise<void> {
try {
const { sticker } = message;
if (!sticker) {
return;
}
if (!sticker.data) {
throw new Error('uploadSticker: No sticker data to upload!');
}
// eslint-disable-next-line no-param-reassign
message.sticker = {
...sticker,
attachmentPointer: await this.makeAttachmentPointer(sticker.data),
};
} catch (error) {
if (error instanceof HTTPError) {
throw new MessageError(message, error);
} else {
throw error;
}
}
}
async uploadContactAvatar(message: Message): Promise<void> {
const { contact } = message;
if (!contact || contact.length === 0) {
return;
}
try {
await Promise.all(
contact.map(async (item: ContactWithHydratedAvatar) => {
const itemAvatar = item?.avatar;
const avatar = itemAvatar?.avatar;
if (!itemAvatar || !avatar || !avatar.data) {
return;
}
const attachment = makeAttachmentSendReady(avatar);
if (!attachment) {
return;
}
itemAvatar.attachmentPointer = await this.makeAttachmentPointer(
attachment
);
})
);
} catch (error) {
if (error instanceof HTTPError) {
throw new MessageError(message, error);
} else {
throw error;
}
}
}
async uploadThumbnails(message: Message): Promise<void> {
const { quote } = message;
if (!quote || !quote.attachments || quote.attachments.length === 0) {
return;
}
try {
await Promise.all(
quote.attachments.map(async (attachment: QuoteAttachmentType) => {
if (!attachment.thumbnail) {
return;
}
// eslint-disable-next-line no-param-reassign
attachment.attachmentPointer = await this.makeAttachmentPointer(
attachment.thumbnail
);
})
);
} catch (error) {
if (error instanceof HTTPError) {
throw new MessageError(message, error);
} else {
throw error;
}
}
}
// Proto assembly
async getTextAttachmentProto(
attachmentAttrs: Attachment.TextAttachmentType
): Promise<Proto.TextAttachment> {
getTextAttachmentProto(
attachmentAttrs: OutgoingTextAttachmentType
): Proto.TextAttachment {
const textAttachment = new Proto.TextAttachment();
if (attachmentAttrs.text) {
@ -910,15 +669,8 @@ export default class MessageSender {
}
if (attachmentAttrs.preview) {
const previewImage = attachmentAttrs.preview.image;
// This cast is OK because we're ensuring that previewImage.data is truthy
const image =
previewImage && previewImage.data
? await this.makeAttachmentPointer(previewImage as AttachmentType)
: undefined;
textAttachment.preview = {
image,
image: attachmentAttrs.preview.image,
title: attachmentAttrs.preview.title,
url: attachmentAttrs.preview.url,
};
@ -950,20 +702,17 @@ export default class MessageSender {
textAttachment,
}: {
allowsReplies?: boolean;
fileAttachment?: AttachmentType;
fileAttachment?: UploadedAttachmentType;
groupV2?: GroupV2InfoType;
profileKey: Uint8Array;
textAttachment?: Attachment.TextAttachmentType;
textAttachment?: OutgoingTextAttachmentType;
}): Promise<Proto.StoryMessage> {
const storyMessage = new Proto.StoryMessage();
storyMessage.profileKey = profileKey;
if (fileAttachment) {
try {
const attachmentPointer = await this.makeAttachmentPointer(
fileAttachment
);
storyMessage.fileAttachment = attachmentPointer;
storyMessage.fileAttachment = fileAttachment;
} catch (error) {
if (error instanceof HTTPError) {
throw new MessageError(message, error);
@ -974,9 +723,7 @@ export default class MessageSender {
}
if (textAttachment) {
storyMessage.textAttachment = await this.getTextAttachmentProto(
textAttachment
);
storyMessage.textAttachment = this.getTextAttachmentProto(textAttachment);
}
if (groupV2) {
@ -1006,7 +753,16 @@ export default class MessageSender {
const dataMessage = message.toProto();
const contentMessage = new Proto.Content();
contentMessage.dataMessage = dataMessage;
if (options.editedMessageTimestamp) {
const editMessage = new Proto.EditMessage();
editMessage.dataMessage = dataMessage;
editMessage.targetSentTimestamp = Long.fromNumber(
options.editedMessageTimestamp
);
contentMessage.editMessage = editMessage;
} else {
contentMessage.dataMessage = dataMessage;
}
const { includePniSignatureMessage } = options;
if (includePniSignatureMessage) {
@ -1033,13 +789,6 @@ export default class MessageSender {
attributes: Readonly<MessageOptionsType>
): Promise<Message> {
const message = new Message(attributes);
await Promise.all([
this.uploadAttachments(message),
this.uploadContactAvatar(message),
this.uploadThumbnails(message),
this.uploadLinkPreviews(message),
this.uploadSticker(message),
]);
return message;
}
@ -1094,6 +843,7 @@ export default class MessageSender {
bodyRanges,
contact,
deletedForEveryoneTimestamp,
editedMessageTimestamp,
expireTimer,
flags,
groupCallUpdate,
@ -1144,6 +894,7 @@ export default class MessageSender {
body: messageText,
contact,
deletedForEveryoneTimestamp,
editedMessageTimestamp,
expireTimer,
flags,
groupCallUpdate,
@ -1353,6 +1104,7 @@ export default class MessageSender {
contact,
contentHint,
deletedForEveryoneTimestamp,
editedMessageTimestamp,
expireTimer,
groupId,
identifier,
@ -1369,21 +1121,22 @@ export default class MessageSender {
urgent,
includePniSignatureMessage,
}: Readonly<{
attachments: ReadonlyArray<AttachmentType> | undefined;
attachments: ReadonlyArray<UploadedAttachmentType> | undefined;
bodyRanges?: ReadonlyArray<RawBodyRange>;
contact?: Array<ContactWithHydratedAvatar>;
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
contentHint: number;
deletedForEveryoneTimestamp: number | undefined;
editedMessageTimestamp?: number;
expireTimer: DurationInSeconds | undefined;
groupId: string | undefined;
identifier: string;
messageText: string | undefined;
options?: SendOptionsType;
preview?: ReadonlyArray<LinkPreviewType> | undefined;
preview?: ReadonlyArray<OutgoingLinkPreviewType> | undefined;
profileKey?: Uint8Array;
quote?: QuotedMessageType | null;
quote?: OutgoingQuoteType;
reaction?: ReactionType;
sticker?: StickerWithHydratedData;
sticker?: OutgoingStickerType;
storyContext?: StoryContextType;
story?: boolean;
timestamp: number;
@ -1397,6 +1150,7 @@ export default class MessageSender {
body: messageText,
contact,
deletedForEveryoneTimestamp,
editedMessageTimestamp,
expireTimer,
preview,
profileKey,
@ -1421,6 +1175,7 @@ export default class MessageSender {
// Note: this is used for sending real messages to your other devices after sending a
// message to others.
async sendSyncMessage({
editedMessageTimestamp,
encodedDataMessage,
timestamp,
destination,
@ -1434,6 +1189,7 @@ export default class MessageSender {
storyMessage,
storyMessageRecipients,
}: Readonly<{
editedMessageTimestamp?: number;
encodedDataMessage?: Uint8Array;
timestamp: number;
destination: string | undefined;
@ -1452,7 +1208,13 @@ export default class MessageSender {
const sentMessage = new Proto.SyncMessage.Sent();
sentMessage.timestamp = Long.fromNumber(timestamp);
if (encodedDataMessage) {
if (editedMessageTimestamp && encodedDataMessage) {
const dataMessage = Proto.DataMessage.decode(encodedDataMessage);
const editMessage = new Proto.EditMessage();
editMessage.dataMessage = dataMessage;
editMessage.targetSentTimestamp = Long.fromNumber(editedMessageTimestamp);
sentMessage.editMessage = editMessage;
} else if (encodedDataMessage) {
const dataMessage = Proto.DataMessage.decode(encodedDataMessage);
sentMessage.message = dataMessage;
}