Do not confirm messages until we have handled them
This commit is contained in:
parent
29aa188c0f
commit
04f716986c
16 changed files with 990 additions and 960 deletions
|
@ -7,7 +7,6 @@ import * as log from '../logging/log';
|
|||
import * as Errors from '../types/errors';
|
||||
import { deleteForEveryone } from '../util/deleteForEveryone';
|
||||
import { drop } from '../util/drop';
|
||||
import { filter, size } from '../util/iterables';
|
||||
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
|
||||
|
||||
export type DeleteAttributesType = {
|
||||
|
@ -20,28 +19,33 @@ export type DeleteAttributesType = {
|
|||
|
||||
const deletes = new Map<string, DeleteAttributesType>();
|
||||
|
||||
function remove(del: DeleteAttributesType): void {
|
||||
del.removeFromMessageReceiverCache();
|
||||
deletes.delete(del.envelopeId);
|
||||
}
|
||||
|
||||
export function forMessage(
|
||||
messageAttributes: MessageAttributesType
|
||||
): Array<DeleteAttributesType> {
|
||||
const sentTimestamps = getMessageSentTimestampSet(messageAttributes);
|
||||
const matchingDeletes = filter(deletes, ([_envelopeId, item]) => {
|
||||
const deleteValues = Array.from(deletes.values());
|
||||
|
||||
const matchingDeletes = deleteValues.filter(item => {
|
||||
return (
|
||||
item.fromId === getContactId(messageAttributes) &&
|
||||
sentTimestamps.has(item.targetSentTimestamp)
|
||||
);
|
||||
});
|
||||
|
||||
if (size(matchingDeletes) > 0) {
|
||||
log.info('Found early DOE for message');
|
||||
const result = Array.from(matchingDeletes);
|
||||
result.forEach(([envelopeId, del]) => {
|
||||
del.removeFromMessageReceiverCache();
|
||||
deletes.delete(envelopeId);
|
||||
});
|
||||
return result.map(([_envelopeId, item]) => item);
|
||||
if (!matchingDeletes.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [];
|
||||
log.info('Found early DOE for message');
|
||||
matchingDeletes.forEach(del => {
|
||||
remove(del);
|
||||
});
|
||||
return matchingDeletes;
|
||||
}
|
||||
|
||||
export async function onDelete(del: DeleteAttributesType): Promise<void> {
|
||||
|
@ -88,11 +92,11 @@ export async function onDelete(del: DeleteAttributesType): Promise<void> {
|
|||
|
||||
await deleteForEveryone(message, del);
|
||||
|
||||
deletes.delete(del.envelopeId);
|
||||
del.removeFromMessageReceiverCache();
|
||||
remove(del);
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
remove(del);
|
||||
log.error(`${logId}: error`, Errors.toLogFormat(error));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import type { MessageAttributesType } from '../model-types.d';
|
|||
import * as Errors from '../types/errors';
|
||||
import * as log from '../logging/log';
|
||||
import { drop } from '../util/drop';
|
||||
import { filter, size } from '../util/iterables';
|
||||
import { getContactId } from '../messages/helpers';
|
||||
import { handleEditMessage } from '../util/handleEditMessage';
|
||||
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
|
||||
|
@ -22,6 +21,11 @@ export type EditAttributesType = {
|
|||
|
||||
const edits = new Map<string, EditAttributesType>();
|
||||
|
||||
function remove(edit: EditAttributesType): void {
|
||||
edits.delete(edit.envelopeId);
|
||||
edit.removeFromMessageReceiverCache();
|
||||
}
|
||||
|
||||
export function forMessage(
|
||||
messageAttributes: Pick<
|
||||
MessageAttributesType,
|
||||
|
@ -34,22 +38,22 @@ export function forMessage(
|
|||
>
|
||||
): Array<EditAttributesType> {
|
||||
const sentAt = getMessageSentTimestamp(messageAttributes, { log });
|
||||
const matchingEdits = filter(edits, ([_envelopeId, item]) => {
|
||||
const editValues = Array.from(edits.values());
|
||||
|
||||
const matchingEdits = editValues.filter(item => {
|
||||
return (
|
||||
item.targetSentTimestamp === sentAt &&
|
||||
item.fromId === getContactId(messageAttributes)
|
||||
);
|
||||
});
|
||||
|
||||
if (size(matchingEdits) > 0) {
|
||||
const result: Array<EditAttributesType> = [];
|
||||
if (matchingEdits.length > 0) {
|
||||
const editsLogIds: Array<number> = [];
|
||||
|
||||
Array.from(matchingEdits).forEach(([envelopeId, item]) => {
|
||||
result.push(item);
|
||||
const result = matchingEdits.map(item => {
|
||||
editsLogIds.push(item.message.sent_at);
|
||||
edits.delete(envelopeId);
|
||||
item.removeFromMessageReceiverCache();
|
||||
remove(item);
|
||||
return item;
|
||||
});
|
||||
|
||||
log.info(
|
||||
|
@ -99,7 +103,6 @@ export async function onEdit(edit: EditAttributesType): Promise<void> {
|
|||
|
||||
if (!targetMessage) {
|
||||
log.info(`${logId}: No message`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -110,11 +113,11 @@ export async function onEdit(edit: EditAttributesType): Promise<void> {
|
|||
|
||||
await handleEditMessage(message.attributes, edit);
|
||||
|
||||
edits.delete(edit.envelopeId);
|
||||
edit.removeFromMessageReceiverCache();
|
||||
remove(edit);
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
remove(edit);
|
||||
log.error(`${logId} error:`, Errors.toLogFormat(error));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
// Copyright 2016 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
import { Collection, Model } from 'backbone';
|
||||
|
||||
import type { MessageModel } from '../models/messages';
|
||||
import type { MessageAttributesType } from '../model-types.d';
|
||||
|
@ -26,6 +23,7 @@ import * as log from '../logging/log';
|
|||
import { getSourceServiceId } from '../messages/helpers';
|
||||
import { queueUpdateMessage } from '../util/messageBatcher';
|
||||
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
|
||||
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||
|
||||
const { deleteSentProtoRecipient } = dataInterface;
|
||||
|
||||
|
@ -36,18 +34,18 @@ export enum MessageReceiptType {
|
|||
}
|
||||
|
||||
export type MessageReceiptAttributesType = {
|
||||
envelopeId: string;
|
||||
messageSentAt: number;
|
||||
receiptTimestamp: number;
|
||||
sourceServiceId: ServiceIdString;
|
||||
removeFromMessageReceiverCache: () => unknown;
|
||||
sourceConversationId: string;
|
||||
sourceDevice: number;
|
||||
sourceServiceId: ServiceIdString;
|
||||
type: MessageReceiptType;
|
||||
wasSentEncrypted: boolean;
|
||||
};
|
||||
|
||||
class MessageReceiptModel extends Model<MessageReceiptAttributesType> {}
|
||||
|
||||
let singleton: MessageReceipts | undefined;
|
||||
const receipts = new Map<string, MessageReceiptAttributesType>();
|
||||
|
||||
const deleteSentProtoBatcher = createWaitBatcher({
|
||||
name: 'deleteSentProtoBatcher',
|
||||
|
@ -79,6 +77,11 @@ const deleteSentProtoBatcher = createWaitBatcher({
|
|||
},
|
||||
});
|
||||
|
||||
function remove(receipt: MessageReceiptAttributesType): void {
|
||||
receipts.delete(receipt.envelopeId);
|
||||
receipt.removeFromMessageReceiverCache();
|
||||
}
|
||||
|
||||
async function getTargetMessage(
|
||||
sourceId: string,
|
||||
serviceId: ServiceIdString,
|
||||
|
@ -124,10 +127,10 @@ const wasDeliveredWithSealedSender = (
|
|||
);
|
||||
|
||||
const shouldDropReceipt = (
|
||||
receipt: MessageReceiptModel,
|
||||
receipt: MessageReceiptAttributesType,
|
||||
message: MessageModel
|
||||
): boolean => {
|
||||
const type = receipt.get('type');
|
||||
const { type } = receipt;
|
||||
switch (type) {
|
||||
case MessageReceiptType.Delivery:
|
||||
return false;
|
||||
|
@ -143,248 +146,245 @@ const shouldDropReceipt = (
|
|||
}
|
||||
};
|
||||
|
||||
export class MessageReceipts extends Collection<MessageReceiptModel> {
|
||||
static getSingleton(): MessageReceipts {
|
||||
if (!singleton) {
|
||||
singleton = new MessageReceipts();
|
||||
}
|
||||
|
||||
return singleton;
|
||||
export function forMessage(
|
||||
message: MessageModel
|
||||
): Array<MessageReceiptAttributesType> {
|
||||
if (!isOutgoing(message.attributes) && !isStory(message.attributes)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
forMessage(message: MessageModel): Array<MessageReceiptModel> {
|
||||
if (!isOutgoing(message.attributes) && !isStory(message.attributes)) {
|
||||
return [];
|
||||
}
|
||||
const logId = `MessageReceipts.forMessage(${getMessageIdForLogging(
|
||||
message.attributes
|
||||
)})`;
|
||||
|
||||
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||
const sourceServiceId = getSourceServiceId(message.attributes);
|
||||
if (ourAci !== sourceServiceId) {
|
||||
return [];
|
||||
}
|
||||
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||
const sourceServiceId = getSourceServiceId(message.attributes);
|
||||
if (ourAci !== sourceServiceId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sentAt = getMessageSentTimestamp(message.attributes, { log });
|
||||
const receipts = this.filter(
|
||||
receipt => receipt.get('messageSentAt') === sentAt
|
||||
);
|
||||
if (receipts.length) {
|
||||
log.info(`MessageReceipts: found early receipts for message ${sentAt}`);
|
||||
this.remove(receipts);
|
||||
}
|
||||
return receipts.filter(receipt => {
|
||||
if (shouldDropReceipt(receipt, message)) {
|
||||
log.info(
|
||||
`MessageReceipts: Dropping an early receipt ${receipt.get('type')} ` +
|
||||
`for message ${sentAt}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const receiptValues = Array.from(receipts.values());
|
||||
|
||||
return true;
|
||||
const sentAt = getMessageSentTimestamp(message.attributes, { log });
|
||||
const result = receiptValues.filter(item => item.messageSentAt === sentAt);
|
||||
if (result.length > 0) {
|
||||
log.info(`${logId}: found early receipts for message ${sentAt}`);
|
||||
result.forEach(receipt => {
|
||||
remove(receipt);
|
||||
});
|
||||
}
|
||||
|
||||
private getNewSendStateByConversationId(
|
||||
oldSendStateByConversationId: SendStateByConversationId,
|
||||
receipt: MessageReceiptModel
|
||||
): SendStateByConversationId {
|
||||
const receiptTimestamp = receipt.get('receiptTimestamp');
|
||||
const sourceConversationId = receipt.get('sourceConversationId');
|
||||
const type = receipt.get('type');
|
||||
|
||||
const oldSendState = getOwn(
|
||||
oldSendStateByConversationId,
|
||||
sourceConversationId
|
||||
) ?? { status: SendStatus.Sent, updatedAt: undefined };
|
||||
|
||||
let sendActionType: SendActionType;
|
||||
switch (type) {
|
||||
case MessageReceiptType.Delivery:
|
||||
sendActionType = SendActionType.GotDeliveryReceipt;
|
||||
break;
|
||||
case MessageReceiptType.Read:
|
||||
sendActionType = SendActionType.GotReadReceipt;
|
||||
break;
|
||||
case MessageReceiptType.View:
|
||||
sendActionType = SendActionType.GotViewedReceipt;
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(type);
|
||||
}
|
||||
|
||||
const newSendState = sendStateReducer(oldSendState, {
|
||||
type: sendActionType,
|
||||
updatedAt: receiptTimestamp,
|
||||
});
|
||||
|
||||
return {
|
||||
...oldSendStateByConversationId,
|
||||
[sourceConversationId]: newSendState,
|
||||
};
|
||||
}
|
||||
|
||||
private async updateMessageSendState(
|
||||
receipt: MessageReceiptModel,
|
||||
message: MessageModel
|
||||
): Promise<void> {
|
||||
const messageSentAt = receipt.get('messageSentAt');
|
||||
|
||||
return result.filter(receipt => {
|
||||
if (shouldDropReceipt(receipt, message)) {
|
||||
log.info(
|
||||
`MessageReceipts: Dropping a receipt ${receipt.get('type')} ` +
|
||||
`for message ${messageSentAt}`
|
||||
`${logId}: Dropping an early receipt ${receipt.type} for message ${sentAt}`
|
||||
);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
let hasChanges = false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
const editHistory = message.get('editHistory') ?? [];
|
||||
const newEditHistory = editHistory?.map(edit => {
|
||||
if (messageSentAt !== edit.timestamp) {
|
||||
return edit;
|
||||
}
|
||||
function getNewSendStateByConversationId(
|
||||
oldSendStateByConversationId: SendStateByConversationId,
|
||||
receipt: MessageReceiptAttributesType
|
||||
): SendStateByConversationId {
|
||||
const { receiptTimestamp, sourceConversationId, type } = receipt;
|
||||
|
||||
const oldSendStateByConversationId = edit.sendStateByConversationId ?? {};
|
||||
const newSendStateByConversationId = this.getNewSendStateByConversationId(
|
||||
oldSendStateByConversationId,
|
||||
receipt
|
||||
);
|
||||
const oldSendState = getOwn(
|
||||
oldSendStateByConversationId,
|
||||
sourceConversationId
|
||||
) ?? { status: SendStatus.Sent, updatedAt: undefined };
|
||||
|
||||
return {
|
||||
...edit,
|
||||
sendStateByConversationId: newSendStateByConversationId,
|
||||
};
|
||||
});
|
||||
if (!isEqual(newEditHistory, editHistory)) {
|
||||
message.set('editHistory', newEditHistory);
|
||||
let sendActionType: SendActionType;
|
||||
switch (type) {
|
||||
case MessageReceiptType.Delivery:
|
||||
sendActionType = SendActionType.GotDeliveryReceipt;
|
||||
break;
|
||||
case MessageReceiptType.Read:
|
||||
sendActionType = SendActionType.GotReadReceipt;
|
||||
break;
|
||||
case MessageReceiptType.View:
|
||||
sendActionType = SendActionType.GotViewedReceipt;
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(type);
|
||||
}
|
||||
|
||||
const newSendState = sendStateReducer(oldSendState, {
|
||||
type: sendActionType,
|
||||
updatedAt: receiptTimestamp,
|
||||
});
|
||||
|
||||
return {
|
||||
...oldSendStateByConversationId,
|
||||
[sourceConversationId]: newSendState,
|
||||
};
|
||||
}
|
||||
|
||||
async function updateMessageSendState(
|
||||
receipt: MessageReceiptAttributesType,
|
||||
message: MessageModel
|
||||
): Promise<void> {
|
||||
const { messageSentAt } = receipt;
|
||||
const logId = `MessageReceipts.updateMessageSendState(sentAt=${receipt.messageSentAt})`;
|
||||
|
||||
if (shouldDropReceipt(receipt, message)) {
|
||||
log.info(
|
||||
`${logId}: Dropping a receipt ${receipt.type} for message ${messageSentAt}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let hasChanges = false;
|
||||
|
||||
const editHistory = message.get('editHistory') ?? [];
|
||||
const newEditHistory = editHistory?.map(edit => {
|
||||
if (messageSentAt !== edit.timestamp) {
|
||||
return edit;
|
||||
}
|
||||
|
||||
const oldSendStateByConversationId = edit.sendStateByConversationId ?? {};
|
||||
const newSendStateByConversationId = getNewSendStateByConversationId(
|
||||
oldSendStateByConversationId,
|
||||
receipt
|
||||
);
|
||||
|
||||
return {
|
||||
...edit,
|
||||
sendStateByConversationId: newSendStateByConversationId,
|
||||
};
|
||||
});
|
||||
if (!isEqual(newEditHistory, editHistory)) {
|
||||
message.set('editHistory', newEditHistory);
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
const editMessageTimestamp = message.get('editMessageTimestamp');
|
||||
if (
|
||||
messageSentAt === message.get('timestamp') ||
|
||||
messageSentAt === editMessageTimestamp
|
||||
) {
|
||||
const oldSendStateByConversationId =
|
||||
message.get('sendStateByConversationId') ?? {};
|
||||
const newSendStateByConversationId = getNewSendStateByConversationId(
|
||||
oldSendStateByConversationId,
|
||||
receipt
|
||||
);
|
||||
|
||||
// The send state may not change. For example, this can happen if we get a read
|
||||
// receipt before a delivery receipt.
|
||||
if (!isEqual(oldSendStateByConversationId, newSendStateByConversationId)) {
|
||||
message.set('sendStateByConversationId', newSendStateByConversationId);
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
const editMessageTimestamp = message.get('editMessageTimestamp');
|
||||
if (
|
||||
messageSentAt === message.get('timestamp') ||
|
||||
messageSentAt === editMessageTimestamp
|
||||
) {
|
||||
const oldSendStateByConversationId =
|
||||
message.get('sendStateByConversationId') ?? {};
|
||||
const newSendStateByConversationId = this.getNewSendStateByConversationId(
|
||||
oldSendStateByConversationId,
|
||||
receipt
|
||||
);
|
||||
|
||||
// The send state may not change. For example, this can happen if we get a read
|
||||
// receipt before a delivery receipt.
|
||||
if (
|
||||
!isEqual(oldSendStateByConversationId, newSendStateByConversationId)
|
||||
) {
|
||||
message.set('sendStateByConversationId', newSendStateByConversationId);
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
queueUpdateMessage(message.attributes);
|
||||
|
||||
// notify frontend listeners
|
||||
const conversation = window.ConversationController.get(
|
||||
message.get('conversationId')
|
||||
);
|
||||
const updateLeftPane = conversation
|
||||
? conversation.debouncedUpdateLastMessage
|
||||
: undefined;
|
||||
if (updateLeftPane) {
|
||||
updateLeftPane();
|
||||
}
|
||||
}
|
||||
|
||||
const sourceConversationId = receipt.get('sourceConversationId');
|
||||
const type = receipt.get('type');
|
||||
|
||||
if (
|
||||
(type === MessageReceiptType.Delivery &&
|
||||
wasDeliveredWithSealedSender(sourceConversationId, message) &&
|
||||
receipt.get('wasSentEncrypted')) ||
|
||||
type === MessageReceiptType.Read
|
||||
) {
|
||||
const recipient = window.ConversationController.get(sourceConversationId);
|
||||
const recipientServiceId = recipient?.getServiceId();
|
||||
const deviceId = receipt.get('sourceDevice');
|
||||
|
||||
if (recipientServiceId && deviceId) {
|
||||
await Promise.all([
|
||||
deleteSentProtoBatcher.add({
|
||||
timestamp: messageSentAt,
|
||||
recipientServiceId,
|
||||
deviceId,
|
||||
}),
|
||||
|
||||
// We want the above call to not be delayed when testing with
|
||||
// CI.
|
||||
window.SignalCI
|
||||
? deleteSentProtoBatcher.flushAndWait()
|
||||
: Promise.resolve(),
|
||||
]);
|
||||
} else {
|
||||
log.warn(
|
||||
`MessageReceipts.onReceipt: Missing serviceId or deviceId for deliveredTo ${sourceConversationId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onReceipt(receipt: MessageReceiptModel): Promise<void> {
|
||||
const messageSentAt = receipt.get('messageSentAt');
|
||||
const sourceConversationId = receipt.get('sourceConversationId');
|
||||
const sourceServiceId = receipt.get('sourceServiceId');
|
||||
const type = receipt.get('type');
|
||||
if (hasChanges) {
|
||||
queueUpdateMessage(message.attributes);
|
||||
|
||||
try {
|
||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||
messageSentAt
|
||||
// notify frontend listeners
|
||||
const conversation = window.ConversationController.get(
|
||||
message.get('conversationId')
|
||||
);
|
||||
const updateLeftPane = conversation
|
||||
? conversation.debouncedUpdateLastMessage
|
||||
: undefined;
|
||||
if (updateLeftPane) {
|
||||
updateLeftPane();
|
||||
}
|
||||
}
|
||||
|
||||
const { sourceConversationId, type } = receipt;
|
||||
|
||||
if (
|
||||
(type === MessageReceiptType.Delivery &&
|
||||
wasDeliveredWithSealedSender(sourceConversationId, message) &&
|
||||
receipt.wasSentEncrypted) ||
|
||||
type === MessageReceiptType.Read
|
||||
) {
|
||||
const recipient = window.ConversationController.get(sourceConversationId);
|
||||
const recipientServiceId = recipient?.getServiceId();
|
||||
const deviceId = receipt.sourceDevice;
|
||||
|
||||
if (recipientServiceId && deviceId) {
|
||||
await Promise.all([
|
||||
deleteSentProtoBatcher.add({
|
||||
timestamp: messageSentAt,
|
||||
recipientServiceId,
|
||||
deviceId,
|
||||
}),
|
||||
|
||||
// We want the above call to not be delayed when testing with
|
||||
// CI.
|
||||
window.SignalCI
|
||||
? deleteSentProtoBatcher.flushAndWait()
|
||||
: Promise.resolve(),
|
||||
]);
|
||||
} else {
|
||||
log.warn(
|
||||
`${logId}: Missing serviceId or deviceId for deliveredTo ${sourceConversationId}`
|
||||
);
|
||||
|
||||
const message = await getTargetMessage(
|
||||
sourceConversationId,
|
||||
sourceServiceId,
|
||||
messages
|
||||
);
|
||||
|
||||
if (message) {
|
||||
await this.updateMessageSendState(receipt, message);
|
||||
} else {
|
||||
// We didn't find any messages but maybe it's a story sent message
|
||||
const targetMessages = messages.filter(
|
||||
item =>
|
||||
item.storyDistributionListId &&
|
||||
item.sendStateByConversationId &&
|
||||
!item.deletedForEveryone &&
|
||||
Boolean(item.sendStateByConversationId[sourceConversationId])
|
||||
);
|
||||
|
||||
// Nope, no target message was found
|
||||
if (!targetMessages.length) {
|
||||
log.info(
|
||||
'MessageReceipts: No message for receipt',
|
||||
type,
|
||||
sourceConversationId,
|
||||
sourceServiceId,
|
||||
messageSentAt
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
targetMessages.map(msg => {
|
||||
const model = window.MessageController.register(msg.id, msg);
|
||||
return this.updateMessageSendState(receipt, model);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.remove(receipt);
|
||||
} catch (error) {
|
||||
log.error('MessageReceipts.onReceipt error:', Errors.toLogFormat(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function onReceipt(
|
||||
receipt: MessageReceiptAttributesType
|
||||
): Promise<void> {
|
||||
receipts.set(receipt.envelopeId, receipt);
|
||||
|
||||
const { messageSentAt, sourceConversationId, sourceServiceId, type } =
|
||||
receipt;
|
||||
|
||||
const logId = `MessageReceipts.onReceipt(sentAt=${receipt.messageSentAt})`;
|
||||
|
||||
try {
|
||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||
messageSentAt
|
||||
);
|
||||
|
||||
const message = await getTargetMessage(
|
||||
sourceConversationId,
|
||||
sourceServiceId,
|
||||
messages
|
||||
);
|
||||
|
||||
if (message) {
|
||||
await updateMessageSendState(receipt, message);
|
||||
} else {
|
||||
// We didn't find any messages but maybe it's a story sent message
|
||||
const targetMessages = messages.filter(
|
||||
item =>
|
||||
item.storyDistributionListId &&
|
||||
item.sendStateByConversationId &&
|
||||
!item.deletedForEveryone &&
|
||||
Boolean(item.sendStateByConversationId[sourceConversationId])
|
||||
);
|
||||
|
||||
// Nope, no target message was found
|
||||
if (!targetMessages.length) {
|
||||
log.info(
|
||||
`${logId}: No message for receipt`,
|
||||
type,
|
||||
sourceConversationId,
|
||||
sourceServiceId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
targetMessages.map(msg => {
|
||||
const model = window.MessageController.register(msg.id, msg);
|
||||
return updateMessageSendState(receipt, model);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
remove(receipt);
|
||||
} catch (error) {
|
||||
remove(receipt);
|
||||
log.error(`${logId} error:`, Errors.toLogFormat(error));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,112 +1,115 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { Collection, Model } from 'backbone';
|
||||
import type { ConversationModel } from '../models/conversations';
|
||||
import * as log from '../logging/log';
|
||||
import * as Errors from '../types/errors';
|
||||
import type { AciString } from '../types/ServiceId';
|
||||
import type { ConversationModel } from '../models/conversations';
|
||||
import * as Errors from '../types/errors';
|
||||
import * as log from '../logging/log';
|
||||
import { drop } from '../util/drop';
|
||||
import { getConversationIdForLogging } from '../util/idForLogging';
|
||||
|
||||
export type MessageRequestAttributesType = {
|
||||
threadE164?: string;
|
||||
threadAci?: AciString;
|
||||
envelopeId: string;
|
||||
groupV2Id?: string;
|
||||
removeFromMessageReceiverCache: () => unknown;
|
||||
threadAci?: AciString;
|
||||
threadE164?: string;
|
||||
type: number;
|
||||
};
|
||||
|
||||
class MessageRequestModel extends Model<MessageRequestAttributesType> {}
|
||||
const messageRequests = new Map<string, MessageRequestAttributesType>();
|
||||
|
||||
let singleton: MessageRequests | undefined;
|
||||
function remove(sync: MessageRequestAttributesType): void {
|
||||
messageRequests.delete(sync.envelopeId);
|
||||
sync.removeFromMessageReceiverCache();
|
||||
}
|
||||
|
||||
export class MessageRequests extends Collection<MessageRequestModel> {
|
||||
static getSingleton(): MessageRequests {
|
||||
if (!singleton) {
|
||||
singleton = new MessageRequests();
|
||||
export function forConversation(
|
||||
conversation: ConversationModel
|
||||
): MessageRequestAttributesType | null {
|
||||
const logId = `MessageRequests.forConversation(${getConversationIdForLogging(
|
||||
conversation.attributes
|
||||
)})`;
|
||||
|
||||
const messageRequestValues = Array.from(messageRequests.values());
|
||||
|
||||
if (conversation.get('e164')) {
|
||||
const syncByE164 = messageRequestValues.find(
|
||||
item => item.threadE164 === conversation.get('e164')
|
||||
);
|
||||
if (syncByE164) {
|
||||
log.info(`${logId}: Found early message request response for E164`);
|
||||
remove(syncByE164);
|
||||
return syncByE164;
|
||||
}
|
||||
|
||||
return singleton;
|
||||
}
|
||||
|
||||
forConversation(conversation: ConversationModel): MessageRequestModel | null {
|
||||
if (conversation.get('e164')) {
|
||||
const syncByE164 = this.findWhere({
|
||||
threadE164: conversation.get('e164'),
|
||||
});
|
||||
if (syncByE164) {
|
||||
log.info(
|
||||
`Found early message request response for E164 ${conversation.idForLogging()}`
|
||||
);
|
||||
this.remove(syncByE164);
|
||||
return syncByE164;
|
||||
}
|
||||
if (conversation.getServiceId()) {
|
||||
const syncByServiceId = messageRequestValues.find(
|
||||
item => item.threadAci === conversation.getServiceId()
|
||||
);
|
||||
if (syncByServiceId) {
|
||||
log.info(`${logId}: Found early message request response for serviceId`);
|
||||
remove(syncByServiceId);
|
||||
return syncByServiceId;
|
||||
}
|
||||
|
||||
if (conversation.getServiceId()) {
|
||||
const syncByAci = this.findWhere({
|
||||
threadAci: conversation.getServiceId(),
|
||||
});
|
||||
if (syncByAci) {
|
||||
log.info(
|
||||
`Found early message request response for aci ${conversation.idForLogging()}`
|
||||
);
|
||||
this.remove(syncByAci);
|
||||
return syncByAci;
|
||||
}
|
||||
}
|
||||
|
||||
// V2 group
|
||||
if (conversation.get('groupId')) {
|
||||
const syncByGroupId = this.findWhere({
|
||||
groupV2Id: conversation.get('groupId'),
|
||||
});
|
||||
if (syncByGroupId) {
|
||||
log.info(
|
||||
`Found early message request response for group v2 ID ${conversation.idForLogging()}`
|
||||
);
|
||||
this.remove(syncByGroupId);
|
||||
return syncByGroupId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async onResponse(sync: MessageRequestModel): Promise<void> {
|
||||
try {
|
||||
const threadE164 = sync.get('threadE164');
|
||||
const threadAci = sync.get('threadAci');
|
||||
const groupV2Id = sync.get('groupV2Id');
|
||||
// V2 group
|
||||
if (conversation.get('groupId')) {
|
||||
const syncByGroupId = messageRequestValues.find(
|
||||
item => item.groupV2Id === conversation.get('groupId')
|
||||
);
|
||||
if (syncByGroupId) {
|
||||
log.info(`${logId}: Found early message request response for gv2`);
|
||||
remove(syncByGroupId);
|
||||
return syncByGroupId;
|
||||
}
|
||||
}
|
||||
|
||||
let conversation;
|
||||
return null;
|
||||
}
|
||||
|
||||
// We multiplex between GV1/GV2 groups here, but we don't kick off migrations
|
||||
if (groupV2Id) {
|
||||
conversation = window.ConversationController.get(groupV2Id);
|
||||
}
|
||||
if (!conversation && (threadE164 || threadAci)) {
|
||||
conversation = window.ConversationController.lookupOrCreate({
|
||||
e164: threadE164,
|
||||
serviceId: threadAci,
|
||||
reason: 'MessageRequests.onResponse',
|
||||
});
|
||||
}
|
||||
export async function onResponse(
|
||||
sync: MessageRequestAttributesType
|
||||
): Promise<void> {
|
||||
messageRequests.set(sync.envelopeId, sync);
|
||||
const { threadE164, threadAci, groupV2Id } = sync;
|
||||
|
||||
if (!conversation) {
|
||||
log.warn(
|
||||
`Received message request response for unknown conversation: groupv2(${groupV2Id}) ${threadAci} ${threadE164}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const logId = `MessageRequests.onResponse(groupv2(${groupV2Id}) ${threadAci} ${threadE164})`;
|
||||
|
||||
void conversation.applyMessageRequestResponse(sync.get('type'), {
|
||||
try {
|
||||
let conversation;
|
||||
|
||||
// We multiplex between GV1/GV2 groups here, but we don't kick off migrations
|
||||
if (groupV2Id) {
|
||||
conversation = window.ConversationController.get(groupV2Id);
|
||||
}
|
||||
if (!conversation && (threadE164 || threadAci)) {
|
||||
conversation = window.ConversationController.lookupOrCreate({
|
||||
e164: threadE164,
|
||||
serviceId: threadAci,
|
||||
reason: logId,
|
||||
});
|
||||
}
|
||||
|
||||
if (!conversation) {
|
||||
log.warn(
|
||||
`${logId}: received message request response for unknown conversation`
|
||||
);
|
||||
remove(sync);
|
||||
return;
|
||||
}
|
||||
|
||||
drop(
|
||||
conversation.applyMessageRequestResponse(sync.type, {
|
||||
fromSync: true,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
this.remove(sync);
|
||||
} catch (error) {
|
||||
log.error('MessageRequests.onResponse error:', Errors.toLogFormat(error));
|
||||
}
|
||||
remove(sync);
|
||||
} catch (error) {
|
||||
remove(sync);
|
||||
log.error(`${logId} error:`, Errors.toLogFormat(error));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,197 +1,220 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { Collection, Model } from 'backbone';
|
||||
import type { AciString } from '../types/ServiceId';
|
||||
import type { ConversationModel } from '../models/conversations';
|
||||
import type { MessageAttributesType } from '../model-types.d';
|
||||
import type { MessageModel } from '../models/messages';
|
||||
import type {
|
||||
MessageAttributesType,
|
||||
ReactionAttributesType,
|
||||
} from '../model-types.d';
|
||||
import type { ReactionSource } from '../reactions/ReactionSource';
|
||||
import * as Errors from '../types/errors';
|
||||
import * as log from '../logging/log';
|
||||
import { getContactId, getContact } from '../messages/helpers';
|
||||
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
|
||||
import { isDirectConversation, isMe } from '../util/whatTypeOfConversation';
|
||||
import { isOutgoing, isStory } from '../state/selectors/message';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
|
||||
|
||||
export class ReactionModel extends Model<ReactionAttributesType> {}
|
||||
export type ReactionAttributesType = {
|
||||
emoji: string;
|
||||
envelopeId: string;
|
||||
fromId: string;
|
||||
remove?: boolean;
|
||||
removeFromMessageReceiverCache: () => unknown;
|
||||
source: ReactionSource;
|
||||
// Necessary to put 1:1 story replies into the right conversation - not the same
|
||||
// conversation as the target message!
|
||||
storyReactionMessage?: MessageModel;
|
||||
targetAuthorAci: AciString;
|
||||
targetTimestamp: number;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
let singleton: Reactions | undefined;
|
||||
const reactions = new Map<string, ReactionAttributesType>();
|
||||
|
||||
export class Reactions extends Collection<ReactionModel> {
|
||||
static getSingleton(): Reactions {
|
||||
if (!singleton) {
|
||||
singleton = new Reactions();
|
||||
}
|
||||
function remove(reaction: ReactionAttributesType): void {
|
||||
reactions.delete(reaction.envelopeId);
|
||||
reaction.removeFromMessageReceiverCache();
|
||||
}
|
||||
|
||||
return singleton;
|
||||
}
|
||||
export function forMessage(
|
||||
message: MessageModel
|
||||
): Array<ReactionAttributesType> {
|
||||
const logId = `Reactions.forMessage(${getMessageIdForLogging(
|
||||
message.attributes
|
||||
)})`;
|
||||
|
||||
forMessage(message: MessageModel): Array<ReactionModel> {
|
||||
const sentTimestamps = getMessageSentTimestampSet(message.attributes);
|
||||
if (isOutgoing(message.attributes)) {
|
||||
const outgoingReactions = this.filter(item =>
|
||||
sentTimestamps.has(item.get('targetTimestamp'))
|
||||
);
|
||||
|
||||
if (outgoingReactions.length > 0) {
|
||||
log.info('Found early reaction for outgoing message');
|
||||
this.remove(outgoingReactions);
|
||||
return outgoingReactions;
|
||||
}
|
||||
}
|
||||
|
||||
const senderId = getContactId(message.attributes);
|
||||
const reactionsBySource = this.filter(re => {
|
||||
const targetSender = window.ConversationController.lookupOrCreate({
|
||||
serviceId: re.get('targetAuthorAci'),
|
||||
reason: 'Reactions.forMessage',
|
||||
});
|
||||
const targetTimestamp = re.get('targetTimestamp');
|
||||
return (
|
||||
targetSender?.id === senderId && sentTimestamps.has(targetTimestamp)
|
||||
);
|
||||
});
|
||||
|
||||
if (reactionsBySource.length > 0) {
|
||||
log.info('Found early reaction for message');
|
||||
this.remove(reactionsBySource);
|
||||
return reactionsBySource;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private async findMessage(
|
||||
targetTimestamp: number,
|
||||
targetConversationId: string
|
||||
): Promise<MessageAttributesType | undefined> {
|
||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||
targetTimestamp
|
||||
const reactionValues = Array.from(reactions.values());
|
||||
const sentTimestamps = getMessageSentTimestampSet(message.attributes);
|
||||
if (isOutgoing(message.attributes)) {
|
||||
const outgoingReactions = reactionValues.filter(item =>
|
||||
sentTimestamps.has(item.targetTimestamp)
|
||||
);
|
||||
|
||||
return messages.find(m => {
|
||||
const contact = getContact(m);
|
||||
|
||||
if (!contact) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mcid = contact.get('id');
|
||||
return mcid === targetConversationId;
|
||||
});
|
||||
if (outgoingReactions.length > 0) {
|
||||
log.info(`${logId}: Found early reaction for outgoing message`);
|
||||
outgoingReactions.forEach(item => {
|
||||
remove(item);
|
||||
});
|
||||
return outgoingReactions;
|
||||
}
|
||||
}
|
||||
|
||||
async onReaction(reaction: ReactionModel): Promise<void> {
|
||||
try {
|
||||
// The conversation the target message was in; we have to find it in the database
|
||||
// to to figure that out.
|
||||
const targetAuthorConversation =
|
||||
window.ConversationController.lookupOrCreate({
|
||||
serviceId: reaction.get('targetAuthorAci'),
|
||||
reason: 'Reactions.onReaction',
|
||||
});
|
||||
const targetConversationId = targetAuthorConversation?.id;
|
||||
if (!targetConversationId) {
|
||||
throw new Error(
|
||||
'onReaction: No conversationId returned from lookupOrCreate!'
|
||||
const senderId = getContactId(message.attributes);
|
||||
const reactionsBySource = reactionValues.filter(re => {
|
||||
const targetSender = window.ConversationController.lookupOrCreate({
|
||||
serviceId: re.targetAuthorAci,
|
||||
reason: logId,
|
||||
});
|
||||
return (
|
||||
targetSender?.id === senderId && sentTimestamps.has(re.targetTimestamp)
|
||||
);
|
||||
});
|
||||
|
||||
if (reactionsBySource.length > 0) {
|
||||
log.info(`${logId}: Found early reaction for message`);
|
||||
reactionsBySource.forEach(item => {
|
||||
remove(item);
|
||||
item.removeFromMessageReceiverCache();
|
||||
});
|
||||
return reactionsBySource;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function findMessage(
|
||||
targetTimestamp: number,
|
||||
targetConversationId: string
|
||||
): Promise<MessageAttributesType | undefined> {
|
||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||
targetTimestamp
|
||||
);
|
||||
|
||||
return messages.find(m => {
|
||||
const contact = getContact(m);
|
||||
|
||||
if (!contact) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mcid = contact.get('id');
|
||||
return mcid === targetConversationId;
|
||||
});
|
||||
}
|
||||
|
||||
export async function onReaction(
|
||||
reaction: ReactionAttributesType
|
||||
): Promise<void> {
|
||||
reactions.set(reaction.envelopeId, reaction);
|
||||
|
||||
const logId = `Reactions.onReaction(timestamp=${reaction.timestamp};target=${reaction.targetTimestamp})`;
|
||||
|
||||
try {
|
||||
// The conversation the target message was in; we have to find it in the database
|
||||
// to to figure that out.
|
||||
const targetAuthorConversation =
|
||||
window.ConversationController.lookupOrCreate({
|
||||
serviceId: reaction.targetAuthorAci,
|
||||
reason: logId,
|
||||
});
|
||||
const targetConversationId = targetAuthorConversation?.id;
|
||||
if (!targetConversationId) {
|
||||
throw new Error(
|
||||
`${logId} Error: No conversationId returned from lookupOrCreate!`
|
||||
);
|
||||
}
|
||||
|
||||
const generatedMessage = reaction.storyReactionMessage;
|
||||
strictAssert(
|
||||
generatedMessage,
|
||||
`${logId} strictAssert: Story reactions must provide storyReactionMessage`
|
||||
);
|
||||
const fromConversation = window.ConversationController.get(
|
||||
generatedMessage.get('conversationId')
|
||||
);
|
||||
|
||||
let targetConversation: ConversationModel | undefined | null;
|
||||
|
||||
const targetMessageCheck = await findMessage(
|
||||
reaction.targetTimestamp,
|
||||
targetConversationId
|
||||
);
|
||||
if (!targetMessageCheck) {
|
||||
log.info(
|
||||
`${logId}: No message for reaction`,
|
||||
'targeting',
|
||||
reaction.targetAuthorAci
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
fromConversation &&
|
||||
isStory(targetMessageCheck) &&
|
||||
isDirectConversation(fromConversation.attributes) &&
|
||||
!isMe(fromConversation.attributes)
|
||||
) {
|
||||
targetConversation = fromConversation;
|
||||
} else {
|
||||
targetConversation =
|
||||
await window.ConversationController.getConversationForTargetMessage(
|
||||
targetConversationId,
|
||||
reaction.targetTimestamp
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const generatedMessage = reaction.get('storyReactionMessage');
|
||||
strictAssert(
|
||||
generatedMessage,
|
||||
'Story reactions must provide storyReactionMessage'
|
||||
);
|
||||
const fromConversation = window.ConversationController.get(
|
||||
generatedMessage.get('conversationId')
|
||||
if (!targetConversation) {
|
||||
log.info(
|
||||
`${logId}: No target conversation for reaction`,
|
||||
reaction.targetAuthorAci,
|
||||
reaction.targetTimestamp
|
||||
);
|
||||
remove(reaction);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let targetConversation: ConversationModel | undefined | null;
|
||||
|
||||
const targetMessageCheck = await this.findMessage(
|
||||
reaction.get('targetTimestamp'),
|
||||
targetConversationId
|
||||
);
|
||||
if (!targetMessageCheck) {
|
||||
log.info(
|
||||
'No message for reaction',
|
||||
reaction.get('timestamp'),
|
||||
'targeting',
|
||||
reaction.get('targetAuthorAci'),
|
||||
reaction.get('targetTimestamp')
|
||||
);
|
||||
// awaiting is safe since `onReaction` is never called from inside the queue
|
||||
await targetConversation.queueJob('Reactions.onReaction', async () => {
|
||||
log.info(`${logId}: handling`);
|
||||
|
||||
// Thanks TS.
|
||||
if (!targetConversation) {
|
||||
remove(reaction);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
fromConversation &&
|
||||
isStory(targetMessageCheck) &&
|
||||
isDirectConversation(fromConversation.attributes) &&
|
||||
!isMe(fromConversation.attributes)
|
||||
) {
|
||||
targetConversation = fromConversation;
|
||||
// Message is fetched inside the conversation queue so we have the
|
||||
// most recent data
|
||||
const targetMessage = await findMessage(
|
||||
reaction.targetTimestamp,
|
||||
targetConversationId
|
||||
);
|
||||
|
||||
if (!targetMessage) {
|
||||
remove(reaction);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = window.MessageController.register(
|
||||
targetMessage.id,
|
||||
targetMessage
|
||||
);
|
||||
|
||||
// Use the generated message in ts/background.ts to create a message
|
||||
// if the reaction is targeted at a story.
|
||||
if (!isStory(targetMessage)) {
|
||||
await message.handleReaction(reaction);
|
||||
} else {
|
||||
targetConversation =
|
||||
await window.ConversationController.getConversationForTargetMessage(
|
||||
targetConversationId,
|
||||
reaction.get('targetTimestamp')
|
||||
);
|
||||
await generatedMessage.handleReaction(reaction, {
|
||||
storyMessage: targetMessage,
|
||||
});
|
||||
}
|
||||
|
||||
if (!targetConversation) {
|
||||
log.info(
|
||||
'No target conversation for reaction',
|
||||
reaction.get('targetAuthorAci'),
|
||||
reaction.get('targetTimestamp')
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// awaiting is safe since `onReaction` is never called from inside the queue
|
||||
await targetConversation.queueJob('Reactions.onReaction', async () => {
|
||||
log.info('Handling reaction for', reaction.get('targetTimestamp'));
|
||||
|
||||
// Thanks TS.
|
||||
if (!targetConversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Message is fetched inside the conversation queue so we have the
|
||||
// most recent data
|
||||
const targetMessage = await this.findMessage(
|
||||
reaction.get('targetTimestamp'),
|
||||
targetConversationId
|
||||
);
|
||||
|
||||
if (!targetMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = window.MessageController.register(
|
||||
targetMessage.id,
|
||||
targetMessage
|
||||
);
|
||||
|
||||
// Use the generated message in ts/background.ts to create a message
|
||||
// if the reaction is targeted at a story.
|
||||
if (!isStory(targetMessage)) {
|
||||
await message.handleReaction(reaction);
|
||||
} else {
|
||||
await generatedMessage.handleReaction(reaction, {
|
||||
storyMessage: targetMessage,
|
||||
});
|
||||
}
|
||||
|
||||
this.remove(reaction);
|
||||
});
|
||||
} catch (error) {
|
||||
log.error('Reactions.onReaction error:', Errors.toLogFormat(error));
|
||||
}
|
||||
remove(reaction);
|
||||
});
|
||||
} catch (error) {
|
||||
remove(reaction);
|
||||
log.error(`${logId} error:`, Errors.toLogFormat(error));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,50 +1,52 @@
|
|||
// Copyright 2017 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { Collection, Model } from 'backbone';
|
||||
|
||||
import type { AciString } from '../types/ServiceId';
|
||||
import type { MessageModel } from '../models/messages';
|
||||
import * as Errors from '../types/errors';
|
||||
import * as log from '../logging/log';
|
||||
import { StartupQueue } from '../util/StartupQueue';
|
||||
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
|
||||
import { isIncoming } from '../state/selectors/message';
|
||||
import { isMessageUnread } from '../util/isMessageUnread';
|
||||
import { notificationService } from '../services/notifications';
|
||||
import * as log from '../logging/log';
|
||||
import * as Errors from '../types/errors';
|
||||
import type { AciString } from '../types/ServiceId';
|
||||
import { StartupQueue } from '../util/StartupQueue';
|
||||
import { queueUpdateMessage } from '../util/messageBatcher';
|
||||
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
|
||||
|
||||
export type ReadSyncAttributesType = {
|
||||
senderId: string;
|
||||
envelopeId: string;
|
||||
readAt: number;
|
||||
removeFromMessageReceiverCache: () => unknown;
|
||||
sender?: string;
|
||||
senderAci: AciString;
|
||||
senderId: string;
|
||||
timestamp: number;
|
||||
readAt: number;
|
||||
};
|
||||
|
||||
class ReadSyncModel extends Model<ReadSyncAttributesType> {}
|
||||
const readSyncs = new Map<string, ReadSyncAttributesType>();
|
||||
|
||||
let singleton: ReadSyncs | undefined;
|
||||
function remove(sync: ReadSyncAttributesType): void {
|
||||
readSyncs.delete(sync.envelopeId);
|
||||
sync.removeFromMessageReceiverCache();
|
||||
}
|
||||
|
||||
async function maybeItIsAReactionReadSync(
|
||||
sync: ReadSyncAttributesType
|
||||
): Promise<void> {
|
||||
const logId = `ReadSyncs.onSync(timestamp=${sync.timestamp})`;
|
||||
|
||||
async function maybeItIsAReactionReadSync(sync: ReadSyncModel): Promise<void> {
|
||||
const readReaction = await window.Signal.Data.markReactionAsRead(
|
||||
sync.get('senderAci'),
|
||||
Number(sync.get('timestamp'))
|
||||
sync.senderAci,
|
||||
Number(sync.timestamp)
|
||||
);
|
||||
|
||||
if (!readReaction) {
|
||||
log.info(
|
||||
'Nothing found for read sync',
|
||||
sync.get('senderId'),
|
||||
sync.get('sender'),
|
||||
sync.get('senderAci'),
|
||||
sync.get('timestamp')
|
||||
);
|
||||
log.info(`${logId} not found:`, sync.senderId, sync.sender, sync.senderAci);
|
||||
return;
|
||||
}
|
||||
|
||||
remove(sync);
|
||||
|
||||
notificationService.removeBy({
|
||||
conversationId: readReaction.conversationId,
|
||||
emoji: readReaction.emoji,
|
||||
|
@ -53,109 +55,110 @@ async function maybeItIsAReactionReadSync(sync: ReadSyncModel): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
export class ReadSyncs extends Collection {
|
||||
static getSingleton(): ReadSyncs {
|
||||
if (!singleton) {
|
||||
singleton = new ReadSyncs();
|
||||
}
|
||||
export function forMessage(
|
||||
message: MessageModel
|
||||
): ReadSyncAttributesType | null {
|
||||
const logId = `ReadSyncs.forMessage(${getMessageIdForLogging(
|
||||
message.attributes
|
||||
)})`;
|
||||
|
||||
return singleton;
|
||||
const sender = window.ConversationController.lookupOrCreate({
|
||||
e164: message.get('source'),
|
||||
serviceId: message.get('sourceServiceId'),
|
||||
reason: logId,
|
||||
});
|
||||
const messageTimestamp = getMessageSentTimestamp(message.attributes, {
|
||||
log,
|
||||
});
|
||||
const readSyncValues = Array.from(readSyncs.values());
|
||||
const foundSync = readSyncValues.find(item => {
|
||||
return item.senderId === sender?.id && item.timestamp === messageTimestamp;
|
||||
});
|
||||
if (foundSync) {
|
||||
log.info(
|
||||
`${logId}: Found early read sync for message ${foundSync.timestamp}`
|
||||
);
|
||||
remove(foundSync);
|
||||
return foundSync;
|
||||
}
|
||||
|
||||
forMessage(message: MessageModel): ReadSyncModel | null {
|
||||
const sender = window.ConversationController.lookupOrCreate({
|
||||
e164: message.get('source'),
|
||||
serviceId: message.get('sourceServiceId'),
|
||||
reason: 'ReadSyncs.forMessage',
|
||||
});
|
||||
const messageTimestamp = getMessageSentTimestamp(message.attributes, {
|
||||
log,
|
||||
});
|
||||
const sync = this.find(item => {
|
||||
return (
|
||||
item.get('senderId') === sender?.id &&
|
||||
item.get('timestamp') === messageTimestamp
|
||||
);
|
||||
});
|
||||
if (sync) {
|
||||
log.info(`Found early read sync for message ${sync.get('timestamp')}`);
|
||||
this.remove(sync);
|
||||
return sync;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
|
||||
readSyncs.set(sync.envelopeId, sync);
|
||||
|
||||
async onSync(sync: ReadSyncModel): Promise<void> {
|
||||
try {
|
||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||
sync.get('timestamp')
|
||||
);
|
||||
const logId = `ReadSyncs.onSync(timestamp=${sync.timestamp})`;
|
||||
|
||||
const found = messages.find(item => {
|
||||
const sender = window.ConversationController.lookupOrCreate({
|
||||
e164: item.source,
|
||||
serviceId: item.sourceServiceId,
|
||||
reason: 'ReadSyncs.onSync',
|
||||
});
|
||||
try {
|
||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||
sync.timestamp
|
||||
);
|
||||
|
||||
return isIncoming(item) && sender?.id === sync.get('senderId');
|
||||
const found = messages.find(item => {
|
||||
const sender = window.ConversationController.lookupOrCreate({
|
||||
e164: item.source,
|
||||
serviceId: item.sourceServiceId,
|
||||
reason: logId,
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
await maybeItIsAReactionReadSync(sync);
|
||||
return;
|
||||
}
|
||||
return isIncoming(item) && sender?.id === sync.senderId;
|
||||
});
|
||||
|
||||
notificationService.removeBy({ messageId: found.id });
|
||||
if (!found) {
|
||||
await maybeItIsAReactionReadSync(sync);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = window.MessageController.register(found.id, found);
|
||||
const readAt = Math.min(sync.get('readAt'), Date.now());
|
||||
notificationService.removeBy({ messageId: found.id });
|
||||
|
||||
// If message is unread, we mark it read. Otherwise, we update the expiration
|
||||
// timer to the time specified by the read sync if it's earlier than
|
||||
// the previous read time.
|
||||
if (isMessageUnread(message.attributes)) {
|
||||
// TODO DESKTOP-1509: use MessageUpdater.markRead once this is TS
|
||||
message.markRead(readAt, { skipSave: true });
|
||||
const message = window.MessageController.register(found.id, found);
|
||||
const readAt = Math.min(sync.readAt, Date.now());
|
||||
|
||||
const updateConversation = async () => {
|
||||
// onReadMessage may result in messages older than this one being
|
||||
// marked read. We want those messages to have the same expire timer
|
||||
// start time as this one, so we pass the readAt value through.
|
||||
void message.getConversation()?.onReadMessage(message, readAt);
|
||||
};
|
||||
// If message is unread, we mark it read. Otherwise, we update the expiration
|
||||
// timer to the time specified by the read sync if it's earlier than
|
||||
// the previous read time.
|
||||
if (isMessageUnread(message.attributes)) {
|
||||
// TODO DESKTOP-1509: use MessageUpdater.markRead once this is TS
|
||||
message.markRead(readAt, { skipSave: true });
|
||||
|
||||
// only available during initialization
|
||||
if (StartupQueue.isAvailable()) {
|
||||
const conversation = message.getConversation();
|
||||
if (conversation) {
|
||||
StartupQueue.add(
|
||||
conversation.get('id'),
|
||||
message.get('sent_at'),
|
||||
updateConversation
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// not awaiting since we don't want to block work happening in the
|
||||
// eventHandlerQueue
|
||||
void updateConversation();
|
||||
const updateConversation = async () => {
|
||||
// onReadMessage may result in messages older than this one being
|
||||
// marked read. We want those messages to have the same expire timer
|
||||
// start time as this one, so we pass the readAt value through.
|
||||
void message.getConversation()?.onReadMessage(message, readAt);
|
||||
};
|
||||
|
||||
// only available during initialization
|
||||
if (StartupQueue.isAvailable()) {
|
||||
const conversation = message.getConversation();
|
||||
if (conversation) {
|
||||
StartupQueue.add(
|
||||
conversation.get('id'),
|
||||
message.get('sent_at'),
|
||||
updateConversation
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const now = Date.now();
|
||||
const existingTimestamp = message.get('expirationStartTimestamp');
|
||||
const expirationStartTimestamp = Math.min(
|
||||
now,
|
||||
Math.min(existingTimestamp || now, readAt || now)
|
||||
);
|
||||
message.set({ expirationStartTimestamp });
|
||||
// not awaiting since we don't want to block work happening in the
|
||||
// eventHandlerQueue
|
||||
void updateConversation();
|
||||
}
|
||||
|
||||
queueUpdateMessage(message.attributes);
|
||||
|
||||
this.remove(sync);
|
||||
} catch (error) {
|
||||
log.error('ReadSyncs.onSync error:', Errors.toLogFormat(error));
|
||||
} else {
|
||||
const now = Date.now();
|
||||
const existingTimestamp = message.get('expirationStartTimestamp');
|
||||
const expirationStartTimestamp = Math.min(
|
||||
now,
|
||||
Math.min(existingTimestamp || now, readAt || now)
|
||||
);
|
||||
message.set({ expirationStartTimestamp });
|
||||
}
|
||||
|
||||
queueUpdateMessage(message.attributes);
|
||||
|
||||
remove(sync);
|
||||
} catch (error) {
|
||||
remove(sync);
|
||||
log.error(`${logId} error:`, Errors.toLogFormat(error));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,100 +1,108 @@
|
|||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { Collection, Model } from 'backbone';
|
||||
import type { MessageModel } from '../models/messages';
|
||||
import * as log from '../logging/log';
|
||||
import * as Errors from '../types/errors';
|
||||
import type { AciString } from '../types/ServiceId';
|
||||
import type { MessageModel } from '../models/messages';
|
||||
import * as Errors from '../types/errors';
|
||||
import * as log from '../logging/log';
|
||||
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||
|
||||
export type ViewOnceOpenSyncAttributesType = {
|
||||
removeFromMessageReceiverCache: () => unknown;
|
||||
source?: string;
|
||||
sourceAci: AciString;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
class ViewOnceOpenSyncModel extends Model<ViewOnceOpenSyncAttributesType> {}
|
||||
const viewOnceSyncs = new Map<number, ViewOnceOpenSyncAttributesType>();
|
||||
|
||||
let singleton: ViewOnceOpenSyncs | undefined;
|
||||
function remove(sync: ViewOnceOpenSyncAttributesType): void {
|
||||
viewOnceSyncs.delete(sync.timestamp);
|
||||
sync.removeFromMessageReceiverCache();
|
||||
}
|
||||
|
||||
export class ViewOnceOpenSyncs extends Collection<ViewOnceOpenSyncModel> {
|
||||
static getSingleton(): ViewOnceOpenSyncs {
|
||||
if (!singleton) {
|
||||
singleton = new ViewOnceOpenSyncs();
|
||||
}
|
||||
export function forMessage(
|
||||
message: MessageModel
|
||||
): ViewOnceOpenSyncAttributesType | null {
|
||||
const logId = `ViewOnceOpenSyncs.forMessage(${getMessageIdForLogging(
|
||||
message.attributes
|
||||
)})`;
|
||||
|
||||
return singleton;
|
||||
const viewOnceSyncValues = Array.from(viewOnceSyncs.values());
|
||||
|
||||
const syncBySourceServiceId = viewOnceSyncValues.find(item => {
|
||||
return (
|
||||
item.sourceAci === message.get('sourceServiceId') &&
|
||||
item.timestamp === message.get('sent_at')
|
||||
);
|
||||
});
|
||||
|
||||
if (syncBySourceServiceId) {
|
||||
log.info(`${logId}: Found early view once open sync for message`);
|
||||
remove(syncBySourceServiceId);
|
||||
return syncBySourceServiceId;
|
||||
}
|
||||
|
||||
forMessage(message: MessageModel): ViewOnceOpenSyncModel | null {
|
||||
const syncBySourceAci = this.find(item => {
|
||||
return (
|
||||
item.get('sourceAci') === message.get('sourceServiceId') &&
|
||||
item.get('timestamp') === message.get('sent_at')
|
||||
);
|
||||
});
|
||||
if (syncBySourceAci) {
|
||||
log.info('Found early view once open sync for message');
|
||||
this.remove(syncBySourceAci);
|
||||
return syncBySourceAci;
|
||||
}
|
||||
|
||||
const syncBySource = this.find(item => {
|
||||
return (
|
||||
item.get('source') === message.get('source') &&
|
||||
item.get('timestamp') === message.get('sent_at')
|
||||
);
|
||||
});
|
||||
if (syncBySource) {
|
||||
log.info('Found early view once open sync for message');
|
||||
this.remove(syncBySource);
|
||||
return syncBySource;
|
||||
}
|
||||
|
||||
return null;
|
||||
const syncBySource = viewOnceSyncValues.find(item => {
|
||||
return (
|
||||
item.source === message.get('source') &&
|
||||
item.timestamp === message.get('sent_at')
|
||||
);
|
||||
});
|
||||
if (syncBySource) {
|
||||
log.info(`${logId}: Found early view once open sync for message`);
|
||||
remove(syncBySource);
|
||||
return syncBySource;
|
||||
}
|
||||
|
||||
async onSync(sync: ViewOnceOpenSyncModel): Promise<void> {
|
||||
try {
|
||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||
sync.get('timestamp')
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function onSync(
|
||||
sync: ViewOnceOpenSyncAttributesType
|
||||
): Promise<void> {
|
||||
viewOnceSyncs.set(sync.timestamp, sync);
|
||||
|
||||
const logId = `ViewOnceOpenSyncs.onSync(timestamp=${sync.timestamp})`;
|
||||
|
||||
try {
|
||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||
sync.timestamp
|
||||
);
|
||||
|
||||
const found = messages.find(item => {
|
||||
const itemSourceAci = item.sourceServiceId;
|
||||
const syncSourceAci = sync.sourceAci;
|
||||
const itemSource = item.source;
|
||||
const syncSource = sync.source;
|
||||
|
||||
return Boolean(
|
||||
(itemSourceAci && syncSourceAci && itemSourceAci === syncSourceAci) ||
|
||||
(itemSource && syncSource && itemSource === syncSource)
|
||||
);
|
||||
});
|
||||
|
||||
const found = messages.find(item => {
|
||||
const itemSourceAci = item.sourceServiceId;
|
||||
const syncSourceAci = sync.get('sourceAci');
|
||||
const itemSource = item.source;
|
||||
const syncSource = sync.get('source');
|
||||
const syncSource = sync.source;
|
||||
const syncSourceAci = sync.sourceAci;
|
||||
const syncTimestamp = sync.timestamp;
|
||||
const wasMessageFound = Boolean(found);
|
||||
log.info(`${logId} receive:`, {
|
||||
syncSource,
|
||||
syncSourceAci,
|
||||
syncTimestamp,
|
||||
wasMessageFound,
|
||||
});
|
||||
|
||||
return Boolean(
|
||||
(itemSourceAci && syncSourceAci && itemSourceAci === syncSourceAci) ||
|
||||
(itemSource && syncSource && itemSource === syncSource)
|
||||
);
|
||||
});
|
||||
|
||||
const syncSource = sync.get('source');
|
||||
const syncSourceAci = sync.get('sourceAci');
|
||||
const syncTimestamp = sync.get('timestamp');
|
||||
const wasMessageFound = Boolean(found);
|
||||
log.info('Receive view once open sync:', {
|
||||
syncSource,
|
||||
syncSourceAci,
|
||||
syncTimestamp,
|
||||
wasMessageFound,
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = window.MessageController.register(found.id, found);
|
||||
await message.markViewOnceMessageViewed({ fromSync: true });
|
||||
|
||||
this.remove(sync);
|
||||
} catch (error) {
|
||||
log.error('ViewOnceOpenSyncs.onSync error:', Errors.toLogFormat(error));
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = window.MessageController.register(found.id, found);
|
||||
await message.markViewOnceMessageViewed({ fromSync: true });
|
||||
|
||||
viewOnceSyncs.delete(sync.timestamp);
|
||||
sync.removeFromMessageReceiverCache();
|
||||
} catch (error) {
|
||||
log.error(`${logId} error:`, Errors.toLogFormat(error));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,136 +1,142 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { Collection, Model } from 'backbone';
|
||||
|
||||
import type { MessageModel } from '../models/messages';
|
||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||
import { markViewed } from '../services/MessageUpdater';
|
||||
import { isDownloaded } from '../types/Attachment';
|
||||
import type { AciString } from '../types/ServiceId';
|
||||
import type { MessageModel } from '../models/messages';
|
||||
import * as Errors from '../types/errors';
|
||||
import { isIncoming } from '../state/selectors/message';
|
||||
import { notificationService } from '../services/notifications';
|
||||
import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
|
||||
import * as log from '../logging/log';
|
||||
import { GiftBadgeStates } from '../components/conversation/Message';
|
||||
import { queueUpdateMessage } from '../util/messageBatcher';
|
||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
|
||||
import { isDownloaded } from '../types/Attachment';
|
||||
import { isIncoming } from '../state/selectors/message';
|
||||
import { markViewed } from '../services/MessageUpdater';
|
||||
import { notificationService } from '../services/notifications';
|
||||
import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
|
||||
import { queueUpdateMessage } from '../util/messageBatcher';
|
||||
|
||||
export type ViewSyncAttributesType = {
|
||||
senderId: string;
|
||||
senderE164?: string;
|
||||
envelopeId: string;
|
||||
removeFromMessageReceiverCache: () => unknown;
|
||||
senderAci: AciString;
|
||||
senderE164?: string;
|
||||
senderId: string;
|
||||
timestamp: number;
|
||||
viewedAt: number;
|
||||
};
|
||||
|
||||
class ViewSyncModel extends Model<ViewSyncAttributesType> {}
|
||||
const viewSyncs = new Map<string, ViewSyncAttributesType>();
|
||||
|
||||
let singleton: ViewSyncs | undefined;
|
||||
function remove(sync: ViewSyncAttributesType): void {
|
||||
viewSyncs.delete(sync.envelopeId);
|
||||
sync.removeFromMessageReceiverCache();
|
||||
}
|
||||
|
||||
export class ViewSyncs extends Collection {
|
||||
static getSingleton(): ViewSyncs {
|
||||
if (!singleton) {
|
||||
singleton = new ViewSyncs();
|
||||
}
|
||||
export function forMessage(
|
||||
message: MessageModel
|
||||
): Array<ViewSyncAttributesType> {
|
||||
const logId = `ViewSyncs.forMessage(${getMessageIdForLogging(
|
||||
message.attributes
|
||||
)})`;
|
||||
|
||||
return singleton;
|
||||
const sender = window.ConversationController.lookupOrCreate({
|
||||
e164: message.get('source'),
|
||||
serviceId: message.get('sourceServiceId'),
|
||||
reason: logId,
|
||||
});
|
||||
const messageTimestamp = getMessageSentTimestamp(message.attributes, {
|
||||
log,
|
||||
});
|
||||
|
||||
const viewSyncValues = Array.from(viewSyncs.values());
|
||||
|
||||
const matchingSyncs = viewSyncValues.filter(item => {
|
||||
return item.senderId === sender?.id && item.timestamp === messageTimestamp;
|
||||
});
|
||||
|
||||
if (matchingSyncs.length > 0) {
|
||||
log.info(
|
||||
`${logId}: Found ${matchingSyncs.length} early view sync(s) for message ${messageTimestamp}`
|
||||
);
|
||||
}
|
||||
matchingSyncs.forEach(sync => {
|
||||
remove(sync);
|
||||
});
|
||||
|
||||
forMessage(message: MessageModel): Array<ViewSyncModel> {
|
||||
const sender = window.ConversationController.lookupOrCreate({
|
||||
e164: message.get('source'),
|
||||
serviceId: message.get('sourceServiceId'),
|
||||
reason: 'ViewSyncs.forMessage',
|
||||
});
|
||||
const messageTimestamp = getMessageSentTimestamp(message.attributes, {
|
||||
log,
|
||||
});
|
||||
const syncs = this.filter(item => {
|
||||
return (
|
||||
item.get('senderId') === sender?.id &&
|
||||
item.get('timestamp') === messageTimestamp
|
||||
);
|
||||
});
|
||||
if (syncs.length) {
|
||||
log.info(
|
||||
`Found ${syncs.length} early view sync(s) for message ${messageTimestamp}`
|
||||
);
|
||||
this.remove(syncs);
|
||||
}
|
||||
return syncs;
|
||||
}
|
||||
return matchingSyncs;
|
||||
}
|
||||
|
||||
async onSync(sync: ViewSyncModel): Promise<void> {
|
||||
try {
|
||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||
sync.get('timestamp')
|
||||
);
|
||||
export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
|
||||
viewSyncs.set(sync.envelopeId, sync);
|
||||
|
||||
const found = messages.find(item => {
|
||||
const sender = window.ConversationController.lookupOrCreate({
|
||||
e164: item.source,
|
||||
serviceId: item.sourceServiceId,
|
||||
reason: 'ViewSyncs.onSync',
|
||||
});
|
||||
const logId = `ViewSyncs.onSync(timestamp=${sync.timestamp})`;
|
||||
|
||||
return sender?.id === sync.get('senderId');
|
||||
try {
|
||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||
sync.timestamp
|
||||
);
|
||||
|
||||
const found = messages.find(item => {
|
||||
const sender = window.ConversationController.lookupOrCreate({
|
||||
e164: item.source,
|
||||
serviceId: item.sourceServiceId,
|
||||
reason: logId,
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
log.info(
|
||||
'Nothing found for view sync',
|
||||
sync.get('senderId'),
|
||||
sync.get('senderE164'),
|
||||
sync.get('senderAci'),
|
||||
sync.get('timestamp')
|
||||
return sender?.id === sync.senderId;
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
log.info(
|
||||
`${logId}: nothing found`,
|
||||
sync.senderId,
|
||||
sync.senderE164,
|
||||
sync.senderAci
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
notificationService.removeBy({ messageId: found.id });
|
||||
|
||||
const message = window.MessageController.register(found.id, found);
|
||||
let didChangeMessage = false;
|
||||
|
||||
if (message.get('readStatus') !== ReadStatus.Viewed) {
|
||||
didChangeMessage = true;
|
||||
message.set(markViewed(message.attributes, sync.viewedAt));
|
||||
|
||||
const attachments = message.get('attachments');
|
||||
if (!attachments?.every(isDownloaded)) {
|
||||
const updatedFields = await queueAttachmentDownloads(
|
||||
message.attributes
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
notificationService.removeBy({ messageId: found.id });
|
||||
|
||||
const message = window.MessageController.register(found.id, found);
|
||||
let didChangeMessage = false;
|
||||
|
||||
if (message.get('readStatus') !== ReadStatus.Viewed) {
|
||||
didChangeMessage = true;
|
||||
message.set(markViewed(message.attributes, sync.get('viewedAt')));
|
||||
|
||||
const attachments = message.get('attachments');
|
||||
if (!attachments?.every(isDownloaded)) {
|
||||
const updatedFields = await queueAttachmentDownloads(
|
||||
message.attributes
|
||||
);
|
||||
if (updatedFields) {
|
||||
message.set(updatedFields);
|
||||
}
|
||||
if (updatedFields) {
|
||||
message.set(updatedFields);
|
||||
}
|
||||
}
|
||||
|
||||
const giftBadge = message.get('giftBadge');
|
||||
if (giftBadge) {
|
||||
didChangeMessage = true;
|
||||
message.set({
|
||||
giftBadge: {
|
||||
...giftBadge,
|
||||
state: isIncoming(message.attributes)
|
||||
? GiftBadgeStates.Redeemed
|
||||
: GiftBadgeStates.Opened,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (didChangeMessage) {
|
||||
queueUpdateMessage(message.attributes);
|
||||
}
|
||||
|
||||
this.remove(sync);
|
||||
} catch (error) {
|
||||
log.error('ViewSyncs.onSync error:', Errors.toLogFormat(error));
|
||||
}
|
||||
|
||||
const giftBadge = message.get('giftBadge');
|
||||
if (giftBadge) {
|
||||
didChangeMessage = true;
|
||||
message.set({
|
||||
giftBadge: {
|
||||
...giftBadge,
|
||||
state: isIncoming(message.attributes)
|
||||
? GiftBadgeStates.Redeemed
|
||||
: GiftBadgeStates.Opened,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (didChangeMessage) {
|
||||
queueUpdateMessage(message.attributes);
|
||||
}
|
||||
|
||||
remove(sync);
|
||||
} catch (error) {
|
||||
remove(sync);
|
||||
log.error(`${logId} error:`, Errors.toLogFormat(error));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue