Revert "Refactor outbound delivery state"

This reverts commit 9c48a95eb5.
This commit is contained in:
Fedor Indutny 2021-07-12 16:51:45 -07:00 committed by GitHub
parent 77668c3247
commit ad217c808d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 694 additions and 3197 deletions

View file

@ -1,7 +1,7 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { isEmpty, isEqual, noop, omit, union } from 'lodash';
import { isEmpty } from 'lodash';
import {
CustomError,
GroupV1Update,
@ -12,13 +12,12 @@ import {
QuotedMessageType,
WhatIsThis,
} from '../model-types.d';
import { concat, filter, find, map, reduce } from '../util/iterables';
import { isNotNil } from '../util/isNotNil';
import { isNormalNumber } from '../util/isNormalNumber';
import { SyncMessageClass } from '../textsecure.d';
import { strictAssert } from '../util/assert';
import { dropNull } from '../util/dropNull';
import { map, filter, find } from '../util/iterables';
import { isNotNil } from '../util/isNotNil';
import { ConversationModel } from './conversations';
import { MessageStatusType } from '../components/conversation/Message';
import {
OwnProps as SmartMessageDetailPropsType,
Contact as SmartMessageDetailContact,
@ -39,18 +38,6 @@ import * as Stickers from '../types/Stickers';
import { AttachmentType, isImage, isVideo } from '../types/Attachment';
import { MIMEType, IMAGE_WEBP } from '../types/MIME';
import { ourProfileKeyService } from '../services/ourProfileKey';
import {
SendAction,
SendActionType,
SendStateByConversationId,
SendStatus,
isMessageJustForMe,
isSent,
sendStateReducer,
someSendStatus,
} from '../messages/MessageSendState';
import { migrateLegacySendAttributes } from '../messages/migrateLegacySendAttributes';
import { getOwn } from '../util/getOwn';
import { markRead } from '../services/MessageUpdater';
import {
isDirectConversation,
@ -134,6 +121,9 @@ const { getTextWithMentions, GoogleChrome } = window.Signal.Util;
const { addStickerPackReference, getMessageBySender } = window.Signal.Data;
const { bytesFromString } = window.Signal.Crypto;
const includesAny = <T>(haystack: Array<T>, ...needles: Array<T>) =>
needles.some(needle => haystack.includes(needle));
export function isQuoteAMatch(
message: MessageModel | null | undefined,
conversationId: string,
@ -156,8 +146,6 @@ export function isQuoteAMatch(
);
}
const isCustomError = (e: unknown): e is CustomError => e instanceof Error;
export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
static updateTimers: () => void;
@ -191,17 +179,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
);
}
const sendStateByConversationId = migrateLegacySendAttributes(
this.attributes,
window.ConversationController.get.bind(window.ConversationController),
window.ConversationController.getOurConversationIdOrThrow()
);
if (sendStateByConversationId) {
this.set('sendStateByConversationId', sendStateByConversationId, {
silent: true,
});
}
this.CURRENT_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.CURRENT;
this.INITIAL_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.INITIAL;
this.OUR_NUMBER = window.textsecure.storage.user.getNumber();
@ -263,41 +240,35 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
);
}
getPropsForMessageDetail(ourConversationId: string): PropsForMessageDetail {
getPropsForMessageDetail(): PropsForMessageDetail {
const newIdentity = window.i18n('newIdentity');
const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError';
const sendStateByConversationId =
this.get('sendStateByConversationId') || {};
const unidentifiedLookup = (
this.get('unidentifiedDeliveries') || []
).reduce((accumulator: Record<string, boolean>, identifier: string) => {
accumulator[
window.ConversationController.getConversationId(identifier) as string
] = true;
return accumulator;
}, Object.create(null) as Record<string, boolean>);
const unidentifiedDeliveries = this.get('unidentifiedDeliveries') || [];
const unidentifiedDeliveriesSet = new Set(
map(
unidentifiedDeliveries,
identifier =>
window.ConversationController.getConversationId(identifier) as string
)
);
let conversationIds: Array<string>;
// We include numbers we didn't successfully send to so we can display errors.
// Older messages don't have the recipients included on the message, so we fall
// back to the conversation's current recipients
/* eslint-disable @typescript-eslint/no-non-null-assertion */
if (isIncoming(this.attributes)) {
conversationIds = [this.getContactId()!];
} else if (!isEmpty(sendStateByConversationId)) {
if (isMessageJustForMe(sendStateByConversationId, ourConversationId)) {
conversationIds = [ourConversationId];
} else {
conversationIds = Object.keys(sendStateByConversationId).filter(
id => id !== ourConversationId
const conversationIds = isIncoming(this.attributes)
? [this.getContactId()!]
: _.union(
(this.get('sent_to') || []).map(
(id: string) => window.ConversationController.getConversationId(id)!
),
(
this.get('recipients') || this.getConversation()!.getRecipients()
).map(
(id: string) => window.ConversationController.getConversationId(id)!
)
);
}
} else {
// Older messages don't have the recipients included on the message, so we fall back
// to the conversation's current recipients
conversationIds = (this.getConversation()?.getRecipients() || []).map(
(id: string) => window.ConversationController.getConversationId(id)!
);
}
/* eslint-enable @typescript-eslint/no-non-null-assertion */
// This will make the error message for outgoing key errors a bit nicer
@ -323,7 +294,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return window.ConversationController.getConversationId(identifier);
});
const finalContacts: Array<SmartMessageDetailContact> = conversationIds.map(
const finalContacts: Array<SmartMessageDetailContact> = (
conversationIds || []
).map(
(id: string): SmartMessageDetailContact => {
const errorsForContact = errorsGroupedById[id];
const isOutgoingKeyError = Boolean(
@ -331,19 +304,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
);
const isUnidentifiedDelivery =
window.storage.get('unidentifiedDeliveryIndicators', false) &&
this.isUnidentifiedDelivery(id, unidentifiedDeliveriesSet);
let status = getOwn(sendStateByConversationId, id)?.status || null;
// If a message was only sent to yourself (Note to Self or a lonely group), it
// is shown read.
if (id === ourConversationId && status && isSent(status)) {
status = SendStatus.Read;
}
this.isUnidentifiedDelivery(id, unidentifiedLookup);
return {
...findAndFormatContact(id),
status,
status: this.getStatus(id),
errors: errorsForContact,
isOutgoingKeyError,
isUnidentifiedDelivery,
@ -378,7 +344,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
message: getPropsForMessage(
this.attributes,
findAndFormatContact,
ourConversationId,
window.ConversationController.getOurConversationIdOrThrow(),
this.OUR_NUMBER,
this.OUR_UUID,
undefined,
@ -401,6 +367,33 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return window.ConversationController.get(this.get('conversationId'));
}
private getStatus(identifier: string): MessageStatusType | null {
const conversation = window.ConversationController.get(identifier);
if (!conversation) {
return null;
}
const e164 = conversation.get('e164');
const uuid = conversation.get('uuid');
const conversationId = conversation.get('id');
const readBy = this.get('read_by') || [];
if (includesAny(readBy, conversationId, e164, uuid)) {
return 'read';
}
const deliveredTo = this.get('delivered_to') || [];
if (includesAny(deliveredTo, conversationId, e164, uuid)) {
return 'delivered';
}
const sentTo = this.get('sent_to') || [];
if (includesAny(sentTo, conversationId, e164, uuid)) {
return 'sent';
}
return null;
}
getNotificationData(): { emoji?: string; text: string } {
const { attributes } = this;
@ -1063,13 +1056,13 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
isUnidentifiedDelivery(
contactId: string,
unidentifiedDeliveriesSet: Readonly<Set<string>>
lookup: Record<string, unknown>
): boolean {
if (isIncoming(this.attributes)) {
return Boolean(this.get('unidentifiedDeliveryReceived'));
}
return unidentifiedDeliveriesSet.has(contactId);
return Boolean(lookup[contactId]);
}
getSource(): string | undefined {
@ -1210,64 +1203,44 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const conversation = this.getConversation()!;
const currentRecipients = new Set<string>(
conversation
.getRecipients()
.map(identifier =>
window.ConversationController.getConversationId(identifier)
)
.filter(isNotNil)
);
const exists = (v: string | null): v is string => Boolean(v);
const intendedRecipients = (this.get('recipients') || [])
.map(identifier =>
window.ConversationController.getConversationId(identifier)
)
.filter(exists);
const successfulRecipients = (this.get('sent_to') || [])
.map(identifier =>
window.ConversationController.getConversationId(identifier)
)
.filter(exists);
const currentRecipients = conversation
.getRecipients()
.map(identifier =>
window.ConversationController.getConversationId(identifier)
)
.filter(exists);
const profileKey = conversation.get('profileSharing')
? await ourProfileKeyService.get()
: undefined;
// Determine retry recipients and get their most up-to-date addressing information
const oldSendStateByConversationId =
this.get('sendStateByConversationId') || {};
const recipients: Array<string> = [];
const newSendStateByConversationId = { ...oldSendStateByConversationId };
// eslint-disable-next-line no-restricted-syntax
for (const [conversationId, sendState] of Object.entries(
oldSendStateByConversationId
)) {
if (isSent(sendState.status)) {
continue;
}
const isStillInConversation = currentRecipients.has(conversationId);
if (!isStillInConversation) {
continue;
}
const recipient = window.ConversationController.get(
conversationId
)?.getSendTarget();
if (!recipient) {
continue;
}
newSendStateByConversationId[conversationId] = sendStateReducer(
sendState,
{
type: SendActionType.ManuallyRetried,
updatedAt: Date.now(),
}
);
recipients.push(recipient);
}
this.set('sendStateByConversationId', newSendStateByConversationId);
await window.Signal.Data.saveMessage(this.attributes, {
Message: window.Whisper.Message,
});
let recipients = _.intersection(intendedRecipients, currentRecipients);
recipients = _.without(recipients, ...successfulRecipients)
.map(id => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const c = window.ConversationController.get(id)!;
return c.getSendTarget();
})
.filter((recipient): recipient is string => recipient !== undefined);
if (!recipients.length) {
window.log.warn('retrySend: Nobody to send to!');
return undefined;
return window.Signal.Data.saveMessage(this.attributes, {
Message: window.Whisper.Message,
});
}
const attachmentsWithData = await Promise.all(
@ -1393,12 +1366,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
public hasSuccessfulDelivery(): boolean {
const sendStateByConversationId = this.get('sendStateByConversationId');
const withoutMe = omit(
sendStateByConversationId,
window.ConversationController.getOurConversationIdOrThrow()
);
return isEmpty(withoutMe) || someSendStatus(withoutMe, isSent);
const recipients = this.get('recipients') || [];
if (recipients.length === 0) {
return true;
}
return (this.get('sent_to') || []).length !== 0;
}
// Called when the user ran into an error with a specific user, wants to send to them
@ -1534,184 +1507,154 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
async send(
promise: Promise<CallbackResultType | void | null>
): Promise<void | Array<void>> {
const updateLeftPane =
this.getConversation()?.debouncedUpdateLastMessage || noop;
updateLeftPane();
let result:
| { success: true; value: CallbackResultType }
| {
success: false;
value: CustomError | CallbackResultType;
};
try {
const value = await (promise as Promise<CallbackResultType>);
result = { success: true, value };
} catch (err) {
result = { success: false, value: err };
const conversation = this.getConversation();
const updateLeftPane = conversation?.debouncedUpdateLastMessage;
if (updateLeftPane) {
updateLeftPane();
}
updateLeftPane();
return (promise as Promise<CallbackResultType>)
.then(async result => {
if (updateLeftPane) {
updateLeftPane();
}
const attributesToUpdate: Partial<MessageAttributesType> = {};
// This is used by sendSyncMessage, then set to null
if (result.dataMessage) {
this.set({ dataMessage: result.dataMessage });
}
// This is used by sendSyncMessage, then set to null
if ('dataMessage' in result.value && result.value.dataMessage) {
attributesToUpdate.dataMessage = result.value.dataMessage;
}
const sentTo = this.get('sent_to') || [];
this.set({
sent_to: _.union(sentTo, result.successfulIdentifiers),
sent: true,
expirationStartTimestamp: Date.now(),
unidentifiedDeliveries: _.union(
this.get('unidentifiedDeliveries') || [],
result.unidentifiedDeliveries
),
});
const sendStateByConversationId = {
...(this.get('sendStateByConversationId') || {}),
};
if (!this.doNotSave) {
await window.Signal.Data.saveMessage(this.attributes, {
Message: window.Whisper.Message,
});
}
const successfulIdentifiers: Array<string> =
'successfulIdentifiers' in result.value &&
Array.isArray(result.value.successfulIdentifiers)
? result.value.successfulIdentifiers
: [];
const sentToAtLeastOneRecipient =
result.success || Boolean(successfulIdentifiers.length);
if (updateLeftPane) {
updateLeftPane();
}
this.sendSyncMessage();
})
.catch((result: CustomError | CallbackResultType) => {
if (updateLeftPane) {
updateLeftPane();
}
successfulIdentifiers.forEach(identifier => {
const conversation = window.ConversationController.get(identifier);
if (!conversation) {
return;
}
if ('dataMessage' in result && result.dataMessage) {
this.set({ dataMessage: result.dataMessage });
}
// If we successfully sent to a user, we can remove our unregistered flag.
if (conversation.isEverUnregistered()) {
conversation.setRegistered();
}
let promises = [];
const previousSendState = getOwn(
sendStateByConversationId,
conversation.id
);
if (previousSendState) {
sendStateByConversationId[conversation.id] = sendStateReducer(
previousSendState,
{
type: SendActionType.Sent,
updatedAt: Date.now(),
// If we successfully sent to a user, we can remove our unregistered flag.
let successfulIdentifiers: Array<string>;
if ('successfulIdentifiers' in result) {
({ successfulIdentifiers = [] } = result);
} else {
successfulIdentifiers = [];
}
successfulIdentifiers.forEach((identifier: string) => {
const c = window.ConversationController.get(identifier);
if (c && c.isEverUnregistered()) {
c.setRegistered();
}
);
}
});
});
const previousUnidentifiedDeliveries =
this.get('unidentifiedDeliveries') || [];
const newUnidentifiedDeliveries =
'unidentifiedDeliveries' in result.value &&
Array.isArray(result.value.unidentifiedDeliveries)
? result.value.unidentifiedDeliveries
: [];
const isError = (e: unknown): e is CustomError => e instanceof Error;
const promises: Array<Promise<unknown>> = [];
if (isError(result)) {
this.saveErrors(result);
if (result.name === 'SignedPreKeyRotationError') {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
promises.push(window.getAccountManager()!.rotateSignedPreKey());
} else if (result.name === 'OutgoingIdentityKeyError') {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const c = window.ConversationController.get(result.number)!;
promises.push(c.getProfiles());
}
} else {
if (successfulIdentifiers.length > 0) {
const sentTo = this.get('sent_to') || [];
let errors: Array<CustomError>;
if (isCustomError(result.value)) {
errors = [result.value];
} else if (Array.isArray(result.value.errors)) {
({ errors } = result.value);
} else {
errors = [];
}
// 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
const unregisteredUserErrors = _.filter(
result.errors,
error => error.name === 'UnregisteredUserError'
);
unregisteredUserErrors.forEach(error => {
const c = window.ConversationController.get(error.identifier);
if (c) {
c.setUnregistered();
}
});
// 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> = [];
// 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 filteredErrors = _.reject(
result.errors,
error => error.name === 'UnregisteredUserError'
);
let hadSignedPreKeyRotationError = false;
errors.forEach(error => {
const conversation =
window.ConversationController.get(error.identifier) ||
window.ConversationController.get(error.number);
// We don't start the expiration timer if there are real errors
// left after filtering out all of the unregistered user errors.
const expirationStartTimestamp = filteredErrors.length
? null
: Date.now();
if (conversation) {
const previousSendState = getOwn(
sendStateByConversationId,
conversation.id
);
if (previousSendState) {
sendStateByConversationId[conversation.id] = sendStateReducer(
previousSendState,
{
type: SendActionType.Failed,
updatedAt: Date.now(),
}
this.saveErrors(filteredErrors);
this.set({
sent_to: _.union(sentTo, result.successfulIdentifiers),
sent: true,
expirationStartTimestamp,
unidentifiedDeliveries: _.union(
this.get('unidentifiedDeliveries') || [],
result.unidentifiedDeliveries
),
});
promises.push(this.sendSyncMessage());
} else if (result.errors) {
this.saveErrors(result.errors);
}
promises = promises.concat(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
_.map(result.errors, error => {
if (error.name === 'OutgoingIdentityKeyError') {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const c = window.ConversationController.get(
error.identifier || error.number
)!;
promises.push(c.getProfiles());
}
})
);
}
}
let shouldSaveError = true;
switch (error.name) {
case 'SignedPreKeyRotationError':
hadSignedPreKeyRotationError = true;
break;
case 'OutgoingIdentityKeyError': {
if (conversation) {
promises.push(conversation.getProfiles());
}
break;
if (updateLeftPane) {
updateLeftPane();
}
case 'UnregisteredUserError':
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);
}
});
if (hadSignedPreKeyRotationError) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
promises.push(window.getAccountManager()!.rotateSignedPreKey());
}
attributesToUpdate.sendStateByConversationId = sendStateByConversationId;
attributesToUpdate.expirationStartTimestamp = sentToAtLeastOneRecipient
? Date.now()
: undefined;
attributesToUpdate.unidentifiedDeliveries = union(
previousUnidentifiedDeliveries,
newUnidentifiedDeliveries
);
// We may overwrite this in the `saveErrors` call below.
attributesToUpdate.errors = [];
this.set(attributesToUpdate);
// We skip save because we'll save in the next step.
this.saveErrors(errorsToSave, { skipSave: true });
if (!this.doNotSave) {
await window.Signal.Data.saveMessage(this.attributes, {
Message: window.Whisper.Message,
return Promise.all(promises);
});
}
updateLeftPane();
if (sentToAtLeastOneRecipient) {
promises.push(this.sendSyncMessage());
}
await Promise.all(promises);
updateLeftPane();
}
// Currently used only for messages that have to be retried when the server
@ -1738,6 +1681,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const sendOptions = await getSendOptions(conv.attributes);
// We don't have to check `sent_to` here, because:
//
// 1. This happens only in private conversations
// 2. Messages to different device ids for the same identifier are sent
// in a single request to the server. So partial success is not
// possible.
await this.send(
handleMessageSend(
// TODO: DESKTOP-724
@ -1769,11 +1718,23 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const updateLeftPane = conv?.debouncedUpdateLastMessage;
try {
this.set({ expirationStartTimestamp: Date.now() });
this.set({
// These are the same as a normal send()
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
sent_to: [conv.getSendTarget()!],
sent: true,
expirationStartTimestamp: Date.now(),
});
const result: typeof window.WhatIsThis = await this.sendSyncMessage();
this.set({
// We have to do this afterward, since we didn't have a previous send!
unidentifiedDeliveries: result ? result.unidentifiedDeliveries : null,
// These are unique to a Note to Self message - immediately read/delivered
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
delivered_to: [window.ConversationController.getOurConversationId()!],
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
read_by: [window.ConversationController.getOurConversationId()!],
});
} catch (result) {
const errors = (result && result.errors) || [new Error('Unknown error')];
@ -1793,8 +1754,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
async sendSyncMessage(): Promise<WhatIsThis> {
const ourNumber = window.textsecure.storage.user.getNumber();
const ourUuid = window.textsecure.storage.user.getUuid();
const ourConversationId = window.ConversationController.getOurConversationIdOrThrow();
const {
wrap,
sendOptions,
@ -1815,34 +1774,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const conv = this.getConversation()!;
const sendEntries = Object.entries(
this.get('sendStateByConversationId') || {}
);
const sentEntries = filter(sendEntries, ([_conversationId, { status }]) =>
isSent(status)
);
const allConversationIdsSentTo = map(
sentEntries,
([conversationId]) => conversationId
);
const conversationIdsSentTo = filter(
allConversationIdsSentTo,
conversationId => conversationId !== ourConversationId
);
const unidentifiedDeliveries = this.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)
);
return wrap(
window.textsecure.messaging.sendSyncMessage({
encodedDataMessage: dataMessage,
@ -1851,38 +1782,15 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
destinationUuid: conv.get('uuid'),
expirationStartTimestamp:
this.get('expirationStartTimestamp') || null,
conversationIdsSentTo,
conversationIdsWithSealedSender,
sentTo: this.get('sent_to') || [],
unidentifiedDeliveries: this.get('unidentifiedDeliveries') || [],
isUpdate,
options: sendOptions,
})
).then(async (result: unknown) => {
let newSendStateByConversationId: undefined | SendStateByConversationId;
const sendStateByConversationId =
this.get('sendStateByConversationId') || {};
const ourOldSendState = getOwn(
sendStateByConversationId,
ourConversationId
);
if (ourOldSendState) {
const ourNewSendState = sendStateReducer(ourOldSendState, {
type: SendActionType.Sent,
updatedAt: Date.now(),
});
if (ourNewSendState !== ourOldSendState) {
newSendStateByConversationId = {
...sendStateByConversationId,
[ourConversationId]: ourNewSendState,
};
}
}
this.set({
synced: true,
dataMessage: null,
...(newSendStateByConversationId
? { sendStateByConversationId: newSendStateByConversationId }
: {}),
});
// Return early, skip the save
@ -2547,66 +2455,29 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
`handleDataMessage: Updating message ${message.idForLogging()} with received transcript`
);
let sentTo = [];
let unidentifiedDeliveries = [];
if (Array.isArray(data.unidentifiedStatus)) {
sentTo = data.unidentifiedStatus.map(
(item: typeof window.WhatIsThis) => item.destination
);
const unidentified = _.filter(data.unidentifiedStatus, item =>
Boolean(item.unidentified)
);
unidentifiedDeliveries = unidentified.map(item => item.destination);
}
const toUpdate = window.MessageController.register(
existingMessage.id,
existingMessage
);
const unidentifiedDeliveriesSet = new Set<string>(
toUpdate.get('unidentifiedDeliveries') ?? []
);
const sendStateByConversationId = {
...(toUpdate.get('sendStateByConversationId') || {}),
};
const unidentifiedStatus: Array<SyncMessageClass.Sent.UnidentifiedDeliveryStatus> = Array.isArray(
data.unidentifiedStatus
)
? data.unidentifiedStatus
: [];
unidentifiedStatus.forEach(
({ destinationUuid, destination, unidentified }) => {
const identifier = destinationUuid || destination;
if (!identifier) {
return;
}
const destinationConversationId = window.ConversationController.ensureContactIds(
{
uuid: destinationUuid,
e164: destination,
highTrust: true,
}
);
if (!destinationConversationId) {
return;
}
const previousSendState = getOwn(
sendStateByConversationId,
destinationConversationId
);
if (previousSendState) {
sendStateByConversationId[
destinationConversationId
] = sendStateReducer(previousSendState, {
type: SendActionType.Sent,
updatedAt: isNormalNumber(data.timestamp)
? data.timestamp
: Date.now(),
});
}
if (unidentified) {
unidentifiedDeliveriesSet.add(identifier);
}
}
);
toUpdate.set({
sendStateByConversationId,
unidentifiedDeliveries: [...unidentifiedDeliveriesSet],
sent_to: _.union(toUpdate.get('sent_to'), sentTo),
unidentifiedDeliveries: _.union(
toUpdate.get('unidentifiedDeliveries'),
unidentifiedDeliveries
),
});
await window.Signal.Data.saveMessage(toUpdate.attributes, {
Message: window.Whisper.Message,
@ -3212,62 +3083,19 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
let changed = false;
if (type === 'outgoing') {
const sendActions = concat<{
destinationConversationId: string;
action: SendAction;
}>(
DeliveryReceipts.getSingleton()
.forMessage(conversation, message)
.map(receipt => ({
destinationConversationId: receipt.get('deliveredTo'),
action: {
type: SendActionType.GotDeliveryReceipt,
updatedAt: receipt.get('timestamp'),
},
})),
ReadReceipts.getSingleton()
.forMessage(conversation, message)
.map(receipt => ({
destinationConversationId: receipt.get('reader'),
action: {
type: SendActionType.GotReadReceipt,
updatedAt: receipt.get('timestamp'),
},
}))
const receipts = DeliveryReceipts.getSingleton().forMessage(
conversation,
message
);
const oldSendStateByConversationId =
this.get('sendStateByConversationId') || {};
const newSendStateByConversationId = reduce(
sendActions,
(
result: SendStateByConversationId,
{ destinationConversationId, action }
) => {
const oldSendState = getOwn(result, destinationConversationId);
if (!oldSendState) {
window.log.warn(
`Got a receipt for a conversation (${destinationConversationId}), but we have no record of sending to them`
);
return result;
}
const newSendState = sendStateReducer(oldSendState, action);
return {
...result,
[destinationConversationId]: newSendState,
};
},
oldSendStateByConversationId
);
if (
!isEqual(oldSendStateByConversationId, newSendStateByConversationId)
) {
message.set('sendStateByConversationId', newSendStateByConversationId);
receipts.forEach(receipt => {
message.set({
delivered: (message.get('delivered') || 0) + 1,
delivered_to: _.union(message.get('delivered_to') || [], [
receipt.get('deliveredTo'),
]),
});
changed = true;
}
});
}
if (type === 'incoming') {
@ -3300,6 +3128,34 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
}
if (type === 'outgoing') {
const reads = ReadReceipts.getSingleton().forMessage(
conversation,
message
);
if (reads.length) {
const readBy = reads.map(receipt => receipt.get('reader'));
message.set({
read_by: _.union(message.get('read_by'), readBy),
});
changed = true;
}
// A sync'd message to ourself is automatically considered read/delivered
if (isFirstRun && isMe(conversation.attributes)) {
message.set({
read_by: conversation.getRecipients(),
delivered_to: conversation.getRecipients(),
});
changed = true;
}
if (isFirstRun) {
message.set({ recipients: conversation.getRecipients() });
changed = true;
}
}
// Check for out-of-order view syncs
if (type === 'incoming' && isTapToView(message.attributes)) {
const viewSync = ViewSyncs.getSingleton().forMessage(message);
@ -3355,7 +3211,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
(isIncoming(attributes) ||
getMessagePropStatus(
attributes,
window.ConversationController.getOurConversationIdOrThrow(),
window.storage.get('read-receipt-setting', false)
) !== 'partial-sent')
) {