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
120
ts/background.ts
120
ts/background.ts
|
@ -21,10 +21,7 @@ import createTaskWithTimeout, {
|
||||||
resumeTasksWithTimeout,
|
resumeTasksWithTimeout,
|
||||||
reportLongRunningTasks,
|
reportLongRunningTasks,
|
||||||
} from './textsecure/TaskWithTimeout';
|
} from './textsecure/TaskWithTimeout';
|
||||||
import type {
|
import type { MessageAttributesType } from './model-types.d';
|
||||||
MessageAttributesType,
|
|
||||||
ReactionAttributesType,
|
|
||||||
} from './model-types.d';
|
|
||||||
import * as Bytes from './Bytes';
|
import * as Bytes from './Bytes';
|
||||||
import * as Timers from './Timers';
|
import * as Timers from './Timers';
|
||||||
import * as indexedDb from './indexeddb';
|
import * as indexedDb from './indexeddb';
|
||||||
|
@ -116,15 +113,13 @@ import { actionCreators } from './state/actions';
|
||||||
import * as Deletes from './messageModifiers/Deletes';
|
import * as Deletes from './messageModifiers/Deletes';
|
||||||
import type { EditAttributesType } from './messageModifiers/Edits';
|
import type { EditAttributesType } from './messageModifiers/Edits';
|
||||||
import * as Edits from './messageModifiers/Edits';
|
import * as Edits from './messageModifiers/Edits';
|
||||||
import {
|
import type { ReactionAttributesType } from './messageModifiers/Reactions';
|
||||||
MessageReceipts,
|
import * as MessageReceipts from './messageModifiers/MessageReceipts';
|
||||||
MessageReceiptType,
|
import * as MessageRequests from './messageModifiers/MessageRequests';
|
||||||
} from './messageModifiers/MessageReceipts';
|
import * as Reactions from './messageModifiers/Reactions';
|
||||||
import { MessageRequests } from './messageModifiers/MessageRequests';
|
import * as ReadSyncs from './messageModifiers/ReadSyncs';
|
||||||
import { Reactions } from './messageModifiers/Reactions';
|
import * as ViewSyncs from './messageModifiers/ViewSyncs';
|
||||||
import { ReadSyncs } from './messageModifiers/ReadSyncs';
|
import * as ViewOnceOpenSyncs from './messageModifiers/ViewOnceOpenSyncs';
|
||||||
import { ViewSyncs } from './messageModifiers/ViewSyncs';
|
|
||||||
import { ViewOnceOpenSyncs } from './messageModifiers/ViewOnceOpenSyncs';
|
|
||||||
import type { DeleteAttributesType } from './messageModifiers/Deletes';
|
import type { DeleteAttributesType } from './messageModifiers/Deletes';
|
||||||
import type { MessageReceiptAttributesType } from './messageModifiers/MessageReceipts';
|
import type { MessageReceiptAttributesType } from './messageModifiers/MessageReceipts';
|
||||||
import type { MessageRequestAttributesType } from './messageModifiers/MessageRequests';
|
import type { MessageRequestAttributesType } from './messageModifiers/MessageRequests';
|
||||||
|
@ -982,8 +977,7 @@ export async function startApp(): Promise<void> {
|
||||||
|
|
||||||
optimizeFTS();
|
optimizeFTS();
|
||||||
|
|
||||||
// Don't block on the following operation
|
drop(window.Signal.Data.ensureFilePermissions());
|
||||||
void window.Signal.Data.ensureFilePermissions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setAppLoadingScreenMessage(window.i18n('icu:loading'), window.i18n);
|
setAppLoadingScreenMessage(window.i18n('icu:loading'), window.i18n);
|
||||||
|
@ -2002,8 +1996,7 @@ export async function startApp(): Promise<void> {
|
||||||
throw new Error('Expected challenge handler to be initialized');
|
throw new Error('Expected challenge handler to be initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intentionally not awaiting
|
drop(challengeHandler.onOnline());
|
||||||
void challengeHandler.onOnline();
|
|
||||||
|
|
||||||
reconnectBackOff.reset();
|
reconnectBackOff.reset();
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -2464,6 +2457,8 @@ export async function startApp(): Promise<void> {
|
||||||
|
|
||||||
log.info('Queuing incoming reaction for', reaction.targetTimestamp);
|
log.info('Queuing incoming reaction for', reaction.targetTimestamp);
|
||||||
const attributes: ReactionAttributesType = {
|
const attributes: ReactionAttributesType = {
|
||||||
|
envelopeId: data.envelopeId,
|
||||||
|
removeFromMessageReceiverCache: confirm,
|
||||||
emoji: reaction.emoji,
|
emoji: reaction.emoji,
|
||||||
fromId: fromConversation.id,
|
fromId: fromConversation.id,
|
||||||
remove: reaction.remove,
|
remove: reaction.remove,
|
||||||
|
@ -2473,11 +2468,8 @@ export async function startApp(): Promise<void> {
|
||||||
targetTimestamp: reaction.targetTimestamp,
|
targetTimestamp: reaction.targetTimestamp,
|
||||||
timestamp,
|
timestamp,
|
||||||
};
|
};
|
||||||
const reactionModel = Reactions.getSingleton().add(attributes);
|
|
||||||
|
|
||||||
drop(Reactions.getSingleton().onReaction(reactionModel));
|
drop(Reactions.onReaction(attributes));
|
||||||
|
|
||||||
confirm();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2548,7 +2540,7 @@ export async function startApp(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't wait for handleDataMessage, as it has its own per-conversation queueing
|
// Don't wait for handleDataMessage, as it has its own per-conversation queueing
|
||||||
void message.handleDataMessage(data.message, event.confirm);
|
drop(message.handleDataMessage(data.message, event.confirm));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onProfileKeyUpdate({
|
async function onProfileKeyUpdate({
|
||||||
|
@ -2793,12 +2785,14 @@ export async function startApp(): Promise<void> {
|
||||||
|
|
||||||
if (!isValidReactionEmoji(reaction.emoji)) {
|
if (!isValidReactionEmoji(reaction.emoji)) {
|
||||||
log.warn('Received an invalid reaction emoji. Dropping it');
|
log.warn('Received an invalid reaction emoji. Dropping it');
|
||||||
event.confirm();
|
confirm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('Queuing sent reaction for', reaction.targetTimestamp);
|
log.info('Queuing sent reaction for', reaction.targetTimestamp);
|
||||||
const attributes: ReactionAttributesType = {
|
const attributes: ReactionAttributesType = {
|
||||||
|
envelopeId: data.envelopeId,
|
||||||
|
removeFromMessageReceiverCache: confirm,
|
||||||
emoji: reaction.emoji,
|
emoji: reaction.emoji,
|
||||||
fromId: window.ConversationController.getOurConversationIdOrThrow(),
|
fromId: window.ConversationController.getOurConversationIdOrThrow(),
|
||||||
remove: reaction.remove,
|
remove: reaction.remove,
|
||||||
|
@ -2808,11 +2802,7 @@ export async function startApp(): Promise<void> {
|
||||||
targetTimestamp: reaction.targetTimestamp,
|
targetTimestamp: reaction.targetTimestamp,
|
||||||
timestamp,
|
timestamp,
|
||||||
};
|
};
|
||||||
const reactionModel = Reactions.getSingleton().add(attributes);
|
drop(Reactions.onReaction(attributes));
|
||||||
|
|
||||||
drop(Reactions.getSingleton().onReaction(reactionModel));
|
|
||||||
|
|
||||||
event.confirm();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2867,9 +2857,11 @@ export async function startApp(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't wait for handleDataMessage, as it has its own per-conversation queueing
|
// Don't wait for handleDataMessage, as it has its own per-conversation queueing
|
||||||
void message.handleDataMessage(data.message, event.confirm, {
|
drop(
|
||||||
data,
|
message.handleDataMessage(data.message, event.confirm, {
|
||||||
});
|
data,
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageDescriptor = {
|
type MessageDescriptor = {
|
||||||
|
@ -3041,21 +3033,18 @@ export async function startApp(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onViewOnceOpenSync(ev: ViewOnceOpenSyncEvent): void {
|
function onViewOnceOpenSync(ev: ViewOnceOpenSyncEvent): void {
|
||||||
ev.confirm();
|
|
||||||
|
|
||||||
const { source, sourceAci, timestamp } = ev;
|
const { source, sourceAci, timestamp } = ev;
|
||||||
log.info(`view once open sync ${source} ${timestamp}`);
|
log.info(`view once open sync ${source} ${timestamp}`);
|
||||||
strictAssert(sourceAci, 'ViewOnceOpen without sourceAci');
|
strictAssert(sourceAci, 'ViewOnceOpen without sourceAci');
|
||||||
strictAssert(timestamp, 'ViewOnceOpen without timestamp');
|
strictAssert(timestamp, 'ViewOnceOpen without timestamp');
|
||||||
|
|
||||||
const attributes: ViewOnceOpenSyncAttributesType = {
|
const attributes: ViewOnceOpenSyncAttributesType = {
|
||||||
|
removeFromMessageReceiverCache: ev.confirm,
|
||||||
source,
|
source,
|
||||||
sourceAci,
|
sourceAci,
|
||||||
timestamp,
|
timestamp,
|
||||||
};
|
};
|
||||||
const sync = ViewOnceOpenSyncs.getSingleton().add(attributes);
|
drop(ViewOnceOpenSyncs.onSync(attributes));
|
||||||
|
|
||||||
void ViewOnceOpenSyncs.getSingleton().onSync(sync);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onFetchLatestSync(ev: FetchLatestEvent): Promise<void> {
|
async function onFetchLatestSync(ev: FetchLatestEvent): Promise<void> {
|
||||||
|
@ -3126,8 +3115,6 @@ export async function startApp(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMessageRequestResponse(ev: MessageRequestResponseEvent): void {
|
function onMessageRequestResponse(ev: MessageRequestResponseEvent): void {
|
||||||
ev.confirm();
|
|
||||||
|
|
||||||
const { threadE164, threadAci, groupV2Id, messageRequestResponseType } = ev;
|
const { threadE164, threadAci, groupV2Id, messageRequestResponseType } = ev;
|
||||||
|
|
||||||
log.info('onMessageRequestResponse', {
|
log.info('onMessageRequestResponse', {
|
||||||
|
@ -3142,22 +3129,24 @@ export async function startApp(): Promise<void> {
|
||||||
'onMessageRequestResponse: missing type'
|
'onMessageRequestResponse: missing type'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
strictAssert(ev.envelopeId, 'onMessageRequestResponse: no envelope id');
|
||||||
|
|
||||||
const attributes: MessageRequestAttributesType = {
|
const attributes: MessageRequestAttributesType = {
|
||||||
|
envelopeId: ev.envelopeId,
|
||||||
|
removeFromMessageReceiverCache: ev.confirm,
|
||||||
threadE164,
|
threadE164,
|
||||||
threadAci,
|
threadAci,
|
||||||
groupV2Id,
|
groupV2Id,
|
||||||
type: messageRequestResponseType,
|
type: messageRequestResponseType,
|
||||||
};
|
};
|
||||||
const sync = MessageRequests.getSingleton().add(attributes);
|
drop(MessageRequests.onResponse(attributes));
|
||||||
|
|
||||||
void MessageRequests.getSingleton().onResponse(sync);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReadReceipt(event: Readonly<ReadEvent>): void {
|
function onReadReceipt(event: Readonly<ReadEvent>): void {
|
||||||
onReadOrViewReceipt({
|
onReadOrViewReceipt({
|
||||||
logTitle: 'read receipt',
|
logTitle: 'read receipt',
|
||||||
event,
|
event,
|
||||||
type: MessageReceiptType.Read,
|
type: MessageReceipts.MessageReceiptType.Read,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3165,7 +3154,7 @@ export async function startApp(): Promise<void> {
|
||||||
onReadOrViewReceipt({
|
onReadOrViewReceipt({
|
||||||
logTitle: 'view receipt',
|
logTitle: 'view receipt',
|
||||||
event,
|
event,
|
||||||
type: MessageReceiptType.View,
|
type: MessageReceipts.MessageReceiptType.View,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3176,7 +3165,9 @@ export async function startApp(): Promise<void> {
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
event: ReadEvent | ViewEvent;
|
event: ReadEvent | ViewEvent;
|
||||||
logTitle: string;
|
logTitle: string;
|
||||||
type: MessageReceiptType.Read | MessageReceiptType.View;
|
type:
|
||||||
|
| MessageReceipts.MessageReceiptType.Read
|
||||||
|
| MessageReceipts.MessageReceiptType.View;
|
||||||
}>): void {
|
}>): void {
|
||||||
const {
|
const {
|
||||||
envelopeTimestamp,
|
envelopeTimestamp,
|
||||||
|
@ -3200,8 +3191,6 @@ export async function startApp(): Promise<void> {
|
||||||
timestamp
|
timestamp
|
||||||
);
|
);
|
||||||
|
|
||||||
event.confirm();
|
|
||||||
|
|
||||||
strictAssert(
|
strictAssert(
|
||||||
isServiceIdString(sourceServiceId),
|
isServiceIdString(sourceServiceId),
|
||||||
'onReadOrViewReceipt: Missing sourceServiceId'
|
'onReadOrViewReceipt: Missing sourceServiceId'
|
||||||
|
@ -3209,6 +3198,8 @@ export async function startApp(): Promise<void> {
|
||||||
strictAssert(sourceDevice, 'onReadOrViewReceipt: Missing sourceDevice');
|
strictAssert(sourceDevice, 'onReadOrViewReceipt: Missing sourceDevice');
|
||||||
|
|
||||||
const attributes: MessageReceiptAttributesType = {
|
const attributes: MessageReceiptAttributesType = {
|
||||||
|
envelopeId: event.receipt.envelopeId,
|
||||||
|
removeFromMessageReceiverCache: event.confirm,
|
||||||
messageSentAt: timestamp,
|
messageSentAt: timestamp,
|
||||||
receiptTimestamp: envelopeTimestamp,
|
receiptTimestamp: envelopeTimestamp,
|
||||||
sourceConversationId: sourceConversation.id,
|
sourceConversationId: sourceConversation.id,
|
||||||
|
@ -3217,13 +3208,10 @@ export async function startApp(): Promise<void> {
|
||||||
type,
|
type,
|
||||||
wasSentEncrypted,
|
wasSentEncrypted,
|
||||||
};
|
};
|
||||||
const receipt = MessageReceipts.getSingleton().add(attributes);
|
drop(MessageReceipts.onReceipt(attributes));
|
||||||
|
|
||||||
// Note: We do not wait for completion here
|
|
||||||
void MessageReceipts.getSingleton().onReceipt(receipt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReadSync(ev: ReadSyncEvent): Promise<void> {
|
async function onReadSync(ev: ReadSyncEvent): Promise<void> {
|
||||||
const { envelopeTimestamp, sender, senderAci, timestamp } = ev.read;
|
const { envelopeTimestamp, sender, senderAci, timestamp } = ev.read;
|
||||||
const readAt = envelopeTimestamp;
|
const readAt = envelopeTimestamp;
|
||||||
const { conversation: senderConversation } =
|
const { conversation: senderConversation } =
|
||||||
|
@ -3249,22 +3237,19 @@ export async function startApp(): Promise<void> {
|
||||||
strictAssert(timestamp, 'onReadSync missing timestamp');
|
strictAssert(timestamp, 'onReadSync missing timestamp');
|
||||||
|
|
||||||
const attributes: ReadSyncAttributesType = {
|
const attributes: ReadSyncAttributesType = {
|
||||||
|
envelopeId: ev.read.envelopeId,
|
||||||
|
removeFromMessageReceiverCache: ev.confirm,
|
||||||
senderId,
|
senderId,
|
||||||
sender,
|
sender,
|
||||||
senderAci,
|
senderAci,
|
||||||
timestamp,
|
timestamp,
|
||||||
readAt,
|
readAt,
|
||||||
};
|
};
|
||||||
const receipt = ReadSyncs.getSingleton().add(attributes);
|
|
||||||
|
|
||||||
receipt.on('remove', ev.confirm);
|
await ReadSyncs.onSync(attributes);
|
||||||
|
|
||||||
// Note: Here we wait, because we want read states to be in the database
|
|
||||||
// before we move on.
|
|
||||||
return ReadSyncs.getSingleton().onSync(receipt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onViewSync(ev: ViewSyncEvent): Promise<void> {
|
async function onViewSync(ev: ViewSyncEvent): Promise<void> {
|
||||||
const { envelopeTimestamp, senderE164, senderAci, timestamp } = ev.view;
|
const { envelopeTimestamp, senderE164, senderAci, timestamp } = ev.view;
|
||||||
const { conversation: senderConversation } =
|
const { conversation: senderConversation } =
|
||||||
window.ConversationController.maybeMergeContacts({
|
window.ConversationController.maybeMergeContacts({
|
||||||
|
@ -3289,19 +3274,16 @@ export async function startApp(): Promise<void> {
|
||||||
strictAssert(timestamp, 'onViewSync missing timestamp');
|
strictAssert(timestamp, 'onViewSync missing timestamp');
|
||||||
|
|
||||||
const attributes: ViewSyncAttributesType = {
|
const attributes: ViewSyncAttributesType = {
|
||||||
|
envelopeId: ev.view.envelopeId,
|
||||||
|
removeFromMessageReceiverCache: ev.confirm,
|
||||||
senderId,
|
senderId,
|
||||||
senderE164,
|
senderE164,
|
||||||
senderAci,
|
senderAci,
|
||||||
timestamp,
|
timestamp,
|
||||||
viewedAt: envelopeTimestamp,
|
viewedAt: envelopeTimestamp,
|
||||||
};
|
};
|
||||||
const receipt = ViewSyncs.getSingleton().add(attributes);
|
|
||||||
|
|
||||||
receipt.on('remove', ev.confirm);
|
await ViewSyncs.onSync(attributes);
|
||||||
|
|
||||||
// Note: Here we wait, because we want viewed states to be in the database
|
|
||||||
// before we move on.
|
|
||||||
return ViewSyncs.getSingleton().onSync(receipt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDeliveryReceipt(ev: DeliveryEvent): void {
|
function onDeliveryReceipt(ev: DeliveryEvent): void {
|
||||||
|
@ -3315,8 +3297,6 @@ export async function startApp(): Promise<void> {
|
||||||
wasSentEncrypted,
|
wasSentEncrypted,
|
||||||
} = deliveryReceipt;
|
} = deliveryReceipt;
|
||||||
|
|
||||||
ev.confirm();
|
|
||||||
|
|
||||||
const sourceConversation = window.ConversationController.lookupOrCreate({
|
const sourceConversation = window.ConversationController.lookupOrCreate({
|
||||||
serviceId: sourceServiceId,
|
serviceId: sourceServiceId,
|
||||||
e164: source,
|
e164: source,
|
||||||
|
@ -3343,18 +3323,18 @@ export async function startApp(): Promise<void> {
|
||||||
strictAssert(sourceDevice, 'onDeliveryReceipt: missing sourceDevice');
|
strictAssert(sourceDevice, 'onDeliveryReceipt: missing sourceDevice');
|
||||||
|
|
||||||
const attributes: MessageReceiptAttributesType = {
|
const attributes: MessageReceiptAttributesType = {
|
||||||
|
envelopeId: ev.deliveryReceipt.envelopeId,
|
||||||
|
removeFromMessageReceiverCache: ev.confirm,
|
||||||
messageSentAt: timestamp,
|
messageSentAt: timestamp,
|
||||||
receiptTimestamp: envelopeTimestamp,
|
receiptTimestamp: envelopeTimestamp,
|
||||||
sourceConversationId: sourceConversation?.id,
|
sourceConversationId: sourceConversation?.id,
|
||||||
sourceServiceId,
|
sourceServiceId,
|
||||||
sourceDevice,
|
sourceDevice,
|
||||||
type: MessageReceiptType.Delivery,
|
type: MessageReceipts.MessageReceiptType.Delivery,
|
||||||
wasSentEncrypted,
|
wasSentEncrypted,
|
||||||
};
|
};
|
||||||
const receipt = MessageReceipts.getSingleton().add(attributes);
|
|
||||||
|
|
||||||
// Note: We don't wait for completion here
|
drop(MessageReceipts.onReceipt(attributes));
|
||||||
void MessageReceipts.getSingleton().onReceipt(receipt);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import * as log from '../logging/log';
|
||||||
import * as Errors from '../types/errors';
|
import * as Errors from '../types/errors';
|
||||||
import { deleteForEveryone } from '../util/deleteForEveryone';
|
import { deleteForEveryone } from '../util/deleteForEveryone';
|
||||||
import { drop } from '../util/drop';
|
import { drop } from '../util/drop';
|
||||||
import { filter, size } from '../util/iterables';
|
|
||||||
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
|
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
|
||||||
|
|
||||||
export type DeleteAttributesType = {
|
export type DeleteAttributesType = {
|
||||||
|
@ -20,28 +19,33 @@ export type DeleteAttributesType = {
|
||||||
|
|
||||||
const deletes = new Map<string, DeleteAttributesType>();
|
const deletes = new Map<string, DeleteAttributesType>();
|
||||||
|
|
||||||
|
function remove(del: DeleteAttributesType): void {
|
||||||
|
del.removeFromMessageReceiverCache();
|
||||||
|
deletes.delete(del.envelopeId);
|
||||||
|
}
|
||||||
|
|
||||||
export function forMessage(
|
export function forMessage(
|
||||||
messageAttributes: MessageAttributesType
|
messageAttributes: MessageAttributesType
|
||||||
): Array<DeleteAttributesType> {
|
): Array<DeleteAttributesType> {
|
||||||
const sentTimestamps = getMessageSentTimestampSet(messageAttributes);
|
const sentTimestamps = getMessageSentTimestampSet(messageAttributes);
|
||||||
const matchingDeletes = filter(deletes, ([_envelopeId, item]) => {
|
const deleteValues = Array.from(deletes.values());
|
||||||
|
|
||||||
|
const matchingDeletes = deleteValues.filter(item => {
|
||||||
return (
|
return (
|
||||||
item.fromId === getContactId(messageAttributes) &&
|
item.fromId === getContactId(messageAttributes) &&
|
||||||
sentTimestamps.has(item.targetSentTimestamp)
|
sentTimestamps.has(item.targetSentTimestamp)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (size(matchingDeletes) > 0) {
|
if (!matchingDeletes.length) {
|
||||||
log.info('Found early DOE for message');
|
return [];
|
||||||
const result = Array.from(matchingDeletes);
|
|
||||||
result.forEach(([envelopeId, del]) => {
|
|
||||||
del.removeFromMessageReceiverCache();
|
|
||||||
deletes.delete(envelopeId);
|
|
||||||
});
|
|
||||||
return result.map(([_envelopeId, item]) => item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
log.info('Found early DOE for message');
|
||||||
|
matchingDeletes.forEach(del => {
|
||||||
|
remove(del);
|
||||||
|
});
|
||||||
|
return matchingDeletes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function onDelete(del: DeleteAttributesType): Promise<void> {
|
export async function onDelete(del: DeleteAttributesType): Promise<void> {
|
||||||
|
@ -88,11 +92,11 @@ export async function onDelete(del: DeleteAttributesType): Promise<void> {
|
||||||
|
|
||||||
await deleteForEveryone(message, del);
|
await deleteForEveryone(message, del);
|
||||||
|
|
||||||
deletes.delete(del.envelopeId);
|
remove(del);
|
||||||
del.removeFromMessageReceiverCache();
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
remove(del);
|
||||||
log.error(`${logId}: error`, Errors.toLogFormat(error));
|
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 Errors from '../types/errors';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import { drop } from '../util/drop';
|
import { drop } from '../util/drop';
|
||||||
import { filter, size } from '../util/iterables';
|
|
||||||
import { getContactId } from '../messages/helpers';
|
import { getContactId } from '../messages/helpers';
|
||||||
import { handleEditMessage } from '../util/handleEditMessage';
|
import { handleEditMessage } from '../util/handleEditMessage';
|
||||||
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
|
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
|
||||||
|
@ -22,6 +21,11 @@ export type EditAttributesType = {
|
||||||
|
|
||||||
const edits = new Map<string, EditAttributesType>();
|
const edits = new Map<string, EditAttributesType>();
|
||||||
|
|
||||||
|
function remove(edit: EditAttributesType): void {
|
||||||
|
edits.delete(edit.envelopeId);
|
||||||
|
edit.removeFromMessageReceiverCache();
|
||||||
|
}
|
||||||
|
|
||||||
export function forMessage(
|
export function forMessage(
|
||||||
messageAttributes: Pick<
|
messageAttributes: Pick<
|
||||||
MessageAttributesType,
|
MessageAttributesType,
|
||||||
|
@ -34,22 +38,22 @@ export function forMessage(
|
||||||
>
|
>
|
||||||
): Array<EditAttributesType> {
|
): Array<EditAttributesType> {
|
||||||
const sentAt = getMessageSentTimestamp(messageAttributes, { log });
|
const sentAt = getMessageSentTimestamp(messageAttributes, { log });
|
||||||
const matchingEdits = filter(edits, ([_envelopeId, item]) => {
|
const editValues = Array.from(edits.values());
|
||||||
|
|
||||||
|
const matchingEdits = editValues.filter(item => {
|
||||||
return (
|
return (
|
||||||
item.targetSentTimestamp === sentAt &&
|
item.targetSentTimestamp === sentAt &&
|
||||||
item.fromId === getContactId(messageAttributes)
|
item.fromId === getContactId(messageAttributes)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (size(matchingEdits) > 0) {
|
if (matchingEdits.length > 0) {
|
||||||
const result: Array<EditAttributesType> = [];
|
|
||||||
const editsLogIds: Array<number> = [];
|
const editsLogIds: Array<number> = [];
|
||||||
|
|
||||||
Array.from(matchingEdits).forEach(([envelopeId, item]) => {
|
const result = matchingEdits.map(item => {
|
||||||
result.push(item);
|
|
||||||
editsLogIds.push(item.message.sent_at);
|
editsLogIds.push(item.message.sent_at);
|
||||||
edits.delete(envelopeId);
|
remove(item);
|
||||||
item.removeFromMessageReceiverCache();
|
return item;
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
|
@ -99,7 +103,6 @@ export async function onEdit(edit: EditAttributesType): Promise<void> {
|
||||||
|
|
||||||
if (!targetMessage) {
|
if (!targetMessage) {
|
||||||
log.info(`${logId}: No message`);
|
log.info(`${logId}: No message`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,11 +113,11 @@ export async function onEdit(edit: EditAttributesType): Promise<void> {
|
||||||
|
|
||||||
await handleEditMessage(message.attributes, edit);
|
await handleEditMessage(message.attributes, edit);
|
||||||
|
|
||||||
edits.delete(edit.envelopeId);
|
remove(edit);
|
||||||
edit.removeFromMessageReceiverCache();
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
remove(edit);
|
||||||
log.error(`${logId} error:`, Errors.toLogFormat(error));
|
log.error(`${logId} error:`, Errors.toLogFormat(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
// Copyright 2016 Signal Messenger, LLC
|
// Copyright 2016 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
/* eslint-disable max-classes-per-file */
|
|
||||||
|
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { Collection, Model } from 'backbone';
|
|
||||||
|
|
||||||
import type { MessageModel } from '../models/messages';
|
import type { MessageModel } from '../models/messages';
|
||||||
import type { MessageAttributesType } from '../model-types.d';
|
import type { MessageAttributesType } from '../model-types.d';
|
||||||
|
@ -26,6 +23,7 @@ import * as log from '../logging/log';
|
||||||
import { getSourceServiceId } from '../messages/helpers';
|
import { getSourceServiceId } from '../messages/helpers';
|
||||||
import { queueUpdateMessage } from '../util/messageBatcher';
|
import { queueUpdateMessage } from '../util/messageBatcher';
|
||||||
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
|
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
|
||||||
|
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||||
|
|
||||||
const { deleteSentProtoRecipient } = dataInterface;
|
const { deleteSentProtoRecipient } = dataInterface;
|
||||||
|
|
||||||
|
@ -36,18 +34,18 @@ export enum MessageReceiptType {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageReceiptAttributesType = {
|
export type MessageReceiptAttributesType = {
|
||||||
|
envelopeId: string;
|
||||||
messageSentAt: number;
|
messageSentAt: number;
|
||||||
receiptTimestamp: number;
|
receiptTimestamp: number;
|
||||||
sourceServiceId: ServiceIdString;
|
removeFromMessageReceiverCache: () => unknown;
|
||||||
sourceConversationId: string;
|
sourceConversationId: string;
|
||||||
sourceDevice: number;
|
sourceDevice: number;
|
||||||
|
sourceServiceId: ServiceIdString;
|
||||||
type: MessageReceiptType;
|
type: MessageReceiptType;
|
||||||
wasSentEncrypted: boolean;
|
wasSentEncrypted: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
class MessageReceiptModel extends Model<MessageReceiptAttributesType> {}
|
const receipts = new Map<string, MessageReceiptAttributesType>();
|
||||||
|
|
||||||
let singleton: MessageReceipts | undefined;
|
|
||||||
|
|
||||||
const deleteSentProtoBatcher = createWaitBatcher({
|
const deleteSentProtoBatcher = createWaitBatcher({
|
||||||
name: 'deleteSentProtoBatcher',
|
name: 'deleteSentProtoBatcher',
|
||||||
|
@ -79,6 +77,11 @@ const deleteSentProtoBatcher = createWaitBatcher({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function remove(receipt: MessageReceiptAttributesType): void {
|
||||||
|
receipts.delete(receipt.envelopeId);
|
||||||
|
receipt.removeFromMessageReceiverCache();
|
||||||
|
}
|
||||||
|
|
||||||
async function getTargetMessage(
|
async function getTargetMessage(
|
||||||
sourceId: string,
|
sourceId: string,
|
||||||
serviceId: ServiceIdString,
|
serviceId: ServiceIdString,
|
||||||
|
@ -124,10 +127,10 @@ const wasDeliveredWithSealedSender = (
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldDropReceipt = (
|
const shouldDropReceipt = (
|
||||||
receipt: MessageReceiptModel,
|
receipt: MessageReceiptAttributesType,
|
||||||
message: MessageModel
|
message: MessageModel
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const type = receipt.get('type');
|
const { type } = receipt;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MessageReceiptType.Delivery:
|
case MessageReceiptType.Delivery:
|
||||||
return false;
|
return false;
|
||||||
|
@ -143,248 +146,245 @@ const shouldDropReceipt = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export class MessageReceipts extends Collection<MessageReceiptModel> {
|
export function forMessage(
|
||||||
static getSingleton(): MessageReceipts {
|
message: MessageModel
|
||||||
if (!singleton) {
|
): Array<MessageReceiptAttributesType> {
|
||||||
singleton = new MessageReceipts();
|
if (!isOutgoing(message.attributes) && !isStory(message.attributes)) {
|
||||||
}
|
return [];
|
||||||
|
|
||||||
return singleton;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
forMessage(message: MessageModel): Array<MessageReceiptModel> {
|
const logId = `MessageReceipts.forMessage(${getMessageIdForLogging(
|
||||||
if (!isOutgoing(message.attributes) && !isStory(message.attributes)) {
|
message.attributes
|
||||||
return [];
|
)})`;
|
||||||
}
|
|
||||||
|
|
||||||
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||||
const sourceServiceId = getSourceServiceId(message.attributes);
|
const sourceServiceId = getSourceServiceId(message.attributes);
|
||||||
if (ourAci !== sourceServiceId) {
|
if (ourAci !== sourceServiceId) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const sentAt = getMessageSentTimestamp(message.attributes, { log });
|
const receiptValues = Array.from(receipts.values());
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
return result.filter(receipt => {
|
||||||
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');
|
|
||||||
|
|
||||||
if (shouldDropReceipt(receipt, message)) {
|
if (shouldDropReceipt(receipt, message)) {
|
||||||
log.info(
|
log.info(
|
||||||
`MessageReceipts: Dropping a receipt ${receipt.get('type')} ` +
|
`${logId}: Dropping an early receipt ${receipt.type} for message ${sentAt}`
|
||||||
`for message ${messageSentAt}`
|
|
||||||
);
|
);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasChanges = false;
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const editHistory = message.get('editHistory') ?? [];
|
function getNewSendStateByConversationId(
|
||||||
const newEditHistory = editHistory?.map(edit => {
|
oldSendStateByConversationId: SendStateByConversationId,
|
||||||
if (messageSentAt !== edit.timestamp) {
|
receipt: MessageReceiptAttributesType
|
||||||
return edit;
|
): SendStateByConversationId {
|
||||||
}
|
const { receiptTimestamp, sourceConversationId, type } = receipt;
|
||||||
|
|
||||||
const oldSendStateByConversationId = edit.sendStateByConversationId ?? {};
|
const oldSendState = getOwn(
|
||||||
const newSendStateByConversationId = this.getNewSendStateByConversationId(
|
oldSendStateByConversationId,
|
||||||
oldSendStateByConversationId,
|
sourceConversationId
|
||||||
receipt
|
) ?? { status: SendStatus.Sent, updatedAt: undefined };
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
let sendActionType: SendActionType;
|
||||||
...edit,
|
switch (type) {
|
||||||
sendStateByConversationId: newSendStateByConversationId,
|
case MessageReceiptType.Delivery:
|
||||||
};
|
sendActionType = SendActionType.GotDeliveryReceipt;
|
||||||
});
|
break;
|
||||||
if (!isEqual(newEditHistory, editHistory)) {
|
case MessageReceiptType.Read:
|
||||||
message.set('editHistory', newEditHistory);
|
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;
|
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> {
|
if (hasChanges) {
|
||||||
const messageSentAt = receipt.get('messageSentAt');
|
queueUpdateMessage(message.attributes);
|
||||||
const sourceConversationId = receipt.get('sourceConversationId');
|
|
||||||
const sourceServiceId = receipt.get('sourceServiceId');
|
|
||||||
const type = receipt.get('type');
|
|
||||||
|
|
||||||
try {
|
// notify frontend listeners
|
||||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
const conversation = window.ConversationController.get(
|
||||||
messageSentAt
|
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
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 { 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 = {
|
export type MessageRequestAttributesType = {
|
||||||
threadE164?: string;
|
envelopeId: string;
|
||||||
threadAci?: AciString;
|
|
||||||
groupV2Id?: string;
|
groupV2Id?: string;
|
||||||
|
removeFromMessageReceiverCache: () => unknown;
|
||||||
|
threadAci?: AciString;
|
||||||
|
threadE164?: string;
|
||||||
type: number;
|
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> {
|
export function forConversation(
|
||||||
static getSingleton(): MessageRequests {
|
conversation: ConversationModel
|
||||||
if (!singleton) {
|
): MessageRequestAttributesType | null {
|
||||||
singleton = new MessageRequests();
|
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.getServiceId()) {
|
||||||
if (conversation.get('e164')) {
|
const syncByServiceId = messageRequestValues.find(
|
||||||
const syncByE164 = this.findWhere({
|
item => item.threadAci === conversation.getServiceId()
|
||||||
threadE164: conversation.get('e164'),
|
);
|
||||||
});
|
if (syncByServiceId) {
|
||||||
if (syncByE164) {
|
log.info(`${logId}: Found early message request response for serviceId`);
|
||||||
log.info(
|
remove(syncByServiceId);
|
||||||
`Found early message request response for E164 ${conversation.idForLogging()}`
|
return syncByServiceId;
|
||||||
);
|
|
||||||
this.remove(syncByE164);
|
|
||||||
return syncByE164;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
// V2 group
|
||||||
try {
|
if (conversation.get('groupId')) {
|
||||||
const threadE164 = sync.get('threadE164');
|
const syncByGroupId = messageRequestValues.find(
|
||||||
const threadAci = sync.get('threadAci');
|
item => item.groupV2Id === conversation.get('groupId')
|
||||||
const groupV2Id = sync.get('groupV2Id');
|
);
|
||||||
|
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
|
export async function onResponse(
|
||||||
if (groupV2Id) {
|
sync: MessageRequestAttributesType
|
||||||
conversation = window.ConversationController.get(groupV2Id);
|
): Promise<void> {
|
||||||
}
|
messageRequests.set(sync.envelopeId, sync);
|
||||||
if (!conversation && (threadE164 || threadAci)) {
|
const { threadE164, threadAci, groupV2Id } = sync;
|
||||||
conversation = window.ConversationController.lookupOrCreate({
|
|
||||||
e164: threadE164,
|
|
||||||
serviceId: threadAci,
|
|
||||||
reason: 'MessageRequests.onResponse',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!conversation) {
|
const logId = `MessageRequests.onResponse(groupv2(${groupV2Id}) ${threadAci} ${threadE164})`;
|
||||||
log.warn(
|
|
||||||
`Received message request response for unknown conversation: groupv2(${groupV2Id}) ${threadAci} ${threadE164}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
fromSync: true,
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.remove(sync);
|
remove(sync);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('MessageRequests.onResponse error:', Errors.toLogFormat(error));
|
remove(sync);
|
||||||
}
|
log.error(`${logId} error:`, Errors.toLogFormat(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,197 +1,220 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
/* eslint-disable max-classes-per-file */
|
import type { AciString } from '../types/ServiceId';
|
||||||
|
|
||||||
import { Collection, Model } from 'backbone';
|
|
||||||
import type { ConversationModel } from '../models/conversations';
|
import type { ConversationModel } from '../models/conversations';
|
||||||
|
import type { MessageAttributesType } from '../model-types.d';
|
||||||
import type { MessageModel } from '../models/messages';
|
import type { MessageModel } from '../models/messages';
|
||||||
import type {
|
import type { ReactionSource } from '../reactions/ReactionSource';
|
||||||
MessageAttributesType,
|
|
||||||
ReactionAttributesType,
|
|
||||||
} from '../model-types.d';
|
|
||||||
import * as Errors from '../types/errors';
|
import * as Errors from '../types/errors';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import { getContactId, getContact } from '../messages/helpers';
|
import { getContactId, getContact } from '../messages/helpers';
|
||||||
|
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||||
|
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
|
||||||
import { isDirectConversation, isMe } from '../util/whatTypeOfConversation';
|
import { isDirectConversation, isMe } from '../util/whatTypeOfConversation';
|
||||||
import { isOutgoing, isStory } from '../state/selectors/message';
|
import { isOutgoing, isStory } from '../state/selectors/message';
|
||||||
import { strictAssert } from '../util/assert';
|
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> {
|
function remove(reaction: ReactionAttributesType): void {
|
||||||
static getSingleton(): Reactions {
|
reactions.delete(reaction.envelopeId);
|
||||||
if (!singleton) {
|
reaction.removeFromMessageReceiverCache();
|
||||||
singleton = new Reactions();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return singleton;
|
export function forMessage(
|
||||||
}
|
message: MessageModel
|
||||||
|
): Array<ReactionAttributesType> {
|
||||||
|
const logId = `Reactions.forMessage(${getMessageIdForLogging(
|
||||||
|
message.attributes
|
||||||
|
)})`;
|
||||||
|
|
||||||
forMessage(message: MessageModel): Array<ReactionModel> {
|
const reactionValues = Array.from(reactions.values());
|
||||||
const sentTimestamps = getMessageSentTimestampSet(message.attributes);
|
const sentTimestamps = getMessageSentTimestampSet(message.attributes);
|
||||||
if (isOutgoing(message.attributes)) {
|
if (isOutgoing(message.attributes)) {
|
||||||
const outgoingReactions = this.filter(item =>
|
const outgoingReactions = reactionValues.filter(item =>
|
||||||
sentTimestamps.has(item.get('targetTimestamp'))
|
sentTimestamps.has(item.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
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return messages.find(m => {
|
if (outgoingReactions.length > 0) {
|
||||||
const contact = getContact(m);
|
log.info(`${logId}: Found early reaction for outgoing message`);
|
||||||
|
outgoingReactions.forEach(item => {
|
||||||
if (!contact) {
|
remove(item);
|
||||||
return false;
|
});
|
||||||
}
|
return outgoingReactions;
|
||||||
|
}
|
||||||
const mcid = contact.get('id');
|
|
||||||
return mcid === targetConversationId;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onReaction(reaction: ReactionModel): Promise<void> {
|
const senderId = getContactId(message.attributes);
|
||||||
try {
|
const reactionsBySource = reactionValues.filter(re => {
|
||||||
// The conversation the target message was in; we have to find it in the database
|
const targetSender = window.ConversationController.lookupOrCreate({
|
||||||
// to to figure that out.
|
serviceId: re.targetAuthorAci,
|
||||||
const targetAuthorConversation =
|
reason: logId,
|
||||||
window.ConversationController.lookupOrCreate({
|
});
|
||||||
serviceId: reaction.get('targetAuthorAci'),
|
return (
|
||||||
reason: 'Reactions.onReaction',
|
targetSender?.id === senderId && sentTimestamps.has(re.targetTimestamp)
|
||||||
});
|
);
|
||||||
const targetConversationId = targetAuthorConversation?.id;
|
});
|
||||||
if (!targetConversationId) {
|
|
||||||
throw new Error(
|
if (reactionsBySource.length > 0) {
|
||||||
'onReaction: No conversationId returned from lookupOrCreate!'
|
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');
|
if (!targetConversation) {
|
||||||
strictAssert(
|
log.info(
|
||||||
generatedMessage,
|
`${logId}: No target conversation for reaction`,
|
||||||
'Story reactions must provide storyReactionMessage'
|
reaction.targetAuthorAci,
|
||||||
);
|
reaction.targetTimestamp
|
||||||
const fromConversation = window.ConversationController.get(
|
|
||||||
generatedMessage.get('conversationId')
|
|
||||||
);
|
);
|
||||||
|
remove(reaction);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
let targetConversation: ConversationModel | undefined | null;
|
// awaiting is safe since `onReaction` is never called from inside the queue
|
||||||
|
await targetConversation.queueJob('Reactions.onReaction', async () => {
|
||||||
const targetMessageCheck = await this.findMessage(
|
log.info(`${logId}: handling`);
|
||||||
reaction.get('targetTimestamp'),
|
|
||||||
targetConversationId
|
|
||||||
);
|
|
||||||
if (!targetMessageCheck) {
|
|
||||||
log.info(
|
|
||||||
'No message for reaction',
|
|
||||||
reaction.get('timestamp'),
|
|
||||||
'targeting',
|
|
||||||
reaction.get('targetAuthorAci'),
|
|
||||||
reaction.get('targetTimestamp')
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// Thanks TS.
|
||||||
|
if (!targetConversation) {
|
||||||
|
remove(reaction);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
// Message is fetched inside the conversation queue so we have the
|
||||||
fromConversation &&
|
// most recent data
|
||||||
isStory(targetMessageCheck) &&
|
const targetMessage = await findMessage(
|
||||||
isDirectConversation(fromConversation.attributes) &&
|
reaction.targetTimestamp,
|
||||||
!isMe(fromConversation.attributes)
|
targetConversationId
|
||||||
) {
|
);
|
||||||
targetConversation = fromConversation;
|
|
||||||
|
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 {
|
} else {
|
||||||
targetConversation =
|
await generatedMessage.handleReaction(reaction, {
|
||||||
await window.ConversationController.getConversationForTargetMessage(
|
storyMessage: targetMessage,
|
||||||
targetConversationId,
|
});
|
||||||
reaction.get('targetTimestamp')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!targetConversation) {
|
remove(reaction);
|
||||||
log.info(
|
});
|
||||||
'No target conversation for reaction',
|
} catch (error) {
|
||||||
reaction.get('targetAuthorAci'),
|
remove(reaction);
|
||||||
reaction.get('targetTimestamp')
|
log.error(`${logId} error:`, Errors.toLogFormat(error));
|
||||||
);
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +1,52 @@
|
||||||
// Copyright 2017 Signal Messenger, LLC
|
// Copyright 2017 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
/* eslint-disable max-classes-per-file */
|
import type { AciString } from '../types/ServiceId';
|
||||||
|
|
||||||
import { Collection, Model } from 'backbone';
|
|
||||||
|
|
||||||
import type { MessageModel } from '../models/messages';
|
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 { isIncoming } from '../state/selectors/message';
|
||||||
import { isMessageUnread } from '../util/isMessageUnread';
|
import { isMessageUnread } from '../util/isMessageUnread';
|
||||||
import { notificationService } from '../services/notifications';
|
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 { queueUpdateMessage } from '../util/messageBatcher';
|
||||||
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
|
|
||||||
|
|
||||||
export type ReadSyncAttributesType = {
|
export type ReadSyncAttributesType = {
|
||||||
senderId: string;
|
envelopeId: string;
|
||||||
|
readAt: number;
|
||||||
|
removeFromMessageReceiverCache: () => unknown;
|
||||||
sender?: string;
|
sender?: string;
|
||||||
senderAci: AciString;
|
senderAci: AciString;
|
||||||
|
senderId: string;
|
||||||
timestamp: number;
|
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(
|
const readReaction = await window.Signal.Data.markReactionAsRead(
|
||||||
sync.get('senderAci'),
|
sync.senderAci,
|
||||||
Number(sync.get('timestamp'))
|
Number(sync.timestamp)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!readReaction) {
|
if (!readReaction) {
|
||||||
log.info(
|
log.info(`${logId} not found:`, sync.senderId, sync.sender, sync.senderAci);
|
||||||
'Nothing found for read sync',
|
|
||||||
sync.get('senderId'),
|
|
||||||
sync.get('sender'),
|
|
||||||
sync.get('senderAci'),
|
|
||||||
sync.get('timestamp')
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remove(sync);
|
||||||
|
|
||||||
notificationService.removeBy({
|
notificationService.removeBy({
|
||||||
conversationId: readReaction.conversationId,
|
conversationId: readReaction.conversationId,
|
||||||
emoji: readReaction.emoji,
|
emoji: readReaction.emoji,
|
||||||
|
@ -53,109 +55,110 @@ async function maybeItIsAReactionReadSync(sync: ReadSyncModel): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReadSyncs extends Collection {
|
export function forMessage(
|
||||||
static getSingleton(): ReadSyncs {
|
message: MessageModel
|
||||||
if (!singleton) {
|
): ReadSyncAttributesType | null {
|
||||||
singleton = new ReadSyncs();
|
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 {
|
return 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;
|
export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
|
||||||
}
|
readSyncs.set(sync.envelopeId, sync);
|
||||||
|
|
||||||
async onSync(sync: ReadSyncModel): Promise<void> {
|
const logId = `ReadSyncs.onSync(timestamp=${sync.timestamp})`;
|
||||||
try {
|
|
||||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
|
||||||
sync.get('timestamp')
|
|
||||||
);
|
|
||||||
|
|
||||||
const found = messages.find(item => {
|
try {
|
||||||
const sender = window.ConversationController.lookupOrCreate({
|
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||||
e164: item.source,
|
sync.timestamp
|
||||||
serviceId: item.sourceServiceId,
|
);
|
||||||
reason: 'ReadSyncs.onSync',
|
|
||||||
});
|
|
||||||
|
|
||||||
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) {
|
return isIncoming(item) && sender?.id === sync.senderId;
|
||||||
await maybeItIsAReactionReadSync(sync);
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationService.removeBy({ messageId: found.id });
|
if (!found) {
|
||||||
|
await maybeItIsAReactionReadSync(sync);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const message = window.MessageController.register(found.id, found);
|
notificationService.removeBy({ messageId: found.id });
|
||||||
const readAt = Math.min(sync.get('readAt'), Date.now());
|
|
||||||
|
|
||||||
// If message is unread, we mark it read. Otherwise, we update the expiration
|
const message = window.MessageController.register(found.id, found);
|
||||||
// timer to the time specified by the read sync if it's earlier than
|
const readAt = Math.min(sync.readAt, Date.now());
|
||||||
// the previous read time.
|
|
||||||
if (isMessageUnread(message.attributes)) {
|
|
||||||
// TODO DESKTOP-1509: use MessageUpdater.markRead once this is TS
|
|
||||||
message.markRead(readAt, { skipSave: true });
|
|
||||||
|
|
||||||
const updateConversation = async () => {
|
// If message is unread, we mark it read. Otherwise, we update the expiration
|
||||||
// onReadMessage may result in messages older than this one being
|
// timer to the time specified by the read sync if it's earlier than
|
||||||
// marked read. We want those messages to have the same expire timer
|
// the previous read time.
|
||||||
// start time as this one, so we pass the readAt value through.
|
if (isMessageUnread(message.attributes)) {
|
||||||
void message.getConversation()?.onReadMessage(message, readAt);
|
// TODO DESKTOP-1509: use MessageUpdater.markRead once this is TS
|
||||||
};
|
message.markRead(readAt, { skipSave: true });
|
||||||
|
|
||||||
// only available during initialization
|
const updateConversation = async () => {
|
||||||
if (StartupQueue.isAvailable()) {
|
// onReadMessage may result in messages older than this one being
|
||||||
const conversation = message.getConversation();
|
// marked read. We want those messages to have the same expire timer
|
||||||
if (conversation) {
|
// start time as this one, so we pass the readAt value through.
|
||||||
StartupQueue.add(
|
void message.getConversation()?.onReadMessage(message, readAt);
|
||||||
conversation.get('id'),
|
};
|
||||||
message.get('sent_at'),
|
|
||||||
updateConversation
|
// only available during initialization
|
||||||
);
|
if (StartupQueue.isAvailable()) {
|
||||||
}
|
const conversation = message.getConversation();
|
||||||
} else {
|
if (conversation) {
|
||||||
// not awaiting since we don't want to block work happening in the
|
StartupQueue.add(
|
||||||
// eventHandlerQueue
|
conversation.get('id'),
|
||||||
void updateConversation();
|
message.get('sent_at'),
|
||||||
|
updateConversation
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const now = Date.now();
|
// not awaiting since we don't want to block work happening in the
|
||||||
const existingTimestamp = message.get('expirationStartTimestamp');
|
// eventHandlerQueue
|
||||||
const expirationStartTimestamp = Math.min(
|
void updateConversation();
|
||||||
now,
|
|
||||||
Math.min(existingTimestamp || now, readAt || now)
|
|
||||||
);
|
|
||||||
message.set({ expirationStartTimestamp });
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
queueUpdateMessage(message.attributes);
|
const now = Date.now();
|
||||||
|
const existingTimestamp = message.get('expirationStartTimestamp');
|
||||||
this.remove(sync);
|
const expirationStartTimestamp = Math.min(
|
||||||
} catch (error) {
|
now,
|
||||||
log.error('ReadSyncs.onSync error:', Errors.toLogFormat(error));
|
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
|
// Copyright 2019 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 { 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 = {
|
export type ViewOnceOpenSyncAttributesType = {
|
||||||
|
removeFromMessageReceiverCache: () => unknown;
|
||||||
source?: string;
|
source?: string;
|
||||||
sourceAci: AciString;
|
sourceAci: AciString;
|
||||||
timestamp: number;
|
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> {
|
export function forMessage(
|
||||||
static getSingleton(): ViewOnceOpenSyncs {
|
message: MessageModel
|
||||||
if (!singleton) {
|
): ViewOnceOpenSyncAttributesType | null {
|
||||||
singleton = new ViewOnceOpenSyncs();
|
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 syncBySource = viewOnceSyncValues.find(item => {
|
||||||
const syncBySourceAci = this.find(item => {
|
return (
|
||||||
return (
|
item.source === message.get('source') &&
|
||||||
item.get('sourceAci') === message.get('sourceServiceId') &&
|
item.timestamp === message.get('sent_at')
|
||||||
item.get('timestamp') === message.get('sent_at')
|
);
|
||||||
);
|
});
|
||||||
});
|
if (syncBySource) {
|
||||||
if (syncBySourceAci) {
|
log.info(`${logId}: Found early view once open sync for message`);
|
||||||
log.info('Found early view once open sync for message');
|
remove(syncBySource);
|
||||||
this.remove(syncBySourceAci);
|
return syncBySource;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onSync(sync: ViewOnceOpenSyncModel): Promise<void> {
|
return null;
|
||||||
try {
|
}
|
||||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
|
||||||
sync.get('timestamp')
|
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 syncSource = sync.source;
|
||||||
const itemSourceAci = item.sourceServiceId;
|
const syncSourceAci = sync.sourceAci;
|
||||||
const syncSourceAci = sync.get('sourceAci');
|
const syncTimestamp = sync.timestamp;
|
||||||
const itemSource = item.source;
|
const wasMessageFound = Boolean(found);
|
||||||
const syncSource = sync.get('source');
|
log.info(`${logId} receive:`, {
|
||||||
|
syncSource,
|
||||||
|
syncSourceAci,
|
||||||
|
syncTimestamp,
|
||||||
|
wasMessageFound,
|
||||||
|
});
|
||||||
|
|
||||||
return Boolean(
|
if (!found) {
|
||||||
(itemSourceAci && syncSourceAci && itemSourceAci === syncSourceAci) ||
|
return;
|
||||||
(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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 { AciString } from '../types/ServiceId';
|
||||||
|
import type { MessageModel } from '../models/messages';
|
||||||
import * as Errors from '../types/errors';
|
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 * as log from '../logging/log';
|
||||||
import { GiftBadgeStates } from '../components/conversation/Message';
|
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 { 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 = {
|
export type ViewSyncAttributesType = {
|
||||||
senderId: string;
|
envelopeId: string;
|
||||||
senderE164?: string;
|
removeFromMessageReceiverCache: () => unknown;
|
||||||
senderAci: AciString;
|
senderAci: AciString;
|
||||||
|
senderE164?: string;
|
||||||
|
senderId: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
viewedAt: 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 {
|
export function forMessage(
|
||||||
static getSingleton(): ViewSyncs {
|
message: MessageModel
|
||||||
if (!singleton) {
|
): Array<ViewSyncAttributesType> {
|
||||||
singleton = new ViewSyncs();
|
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> {
|
return matchingSyncs;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
async onSync(sync: ViewSyncModel): Promise<void> {
|
export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
|
||||||
try {
|
viewSyncs.set(sync.envelopeId, sync);
|
||||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
|
||||||
sync.get('timestamp')
|
|
||||||
);
|
|
||||||
|
|
||||||
const found = messages.find(item => {
|
const logId = `ViewSyncs.onSync(timestamp=${sync.timestamp})`;
|
||||||
const sender = window.ConversationController.lookupOrCreate({
|
|
||||||
e164: item.source,
|
|
||||||
serviceId: item.sourceServiceId,
|
|
||||||
reason: 'ViewSyncs.onSync',
|
|
||||||
});
|
|
||||||
|
|
||||||
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) {
|
return sender?.id === sync.senderId;
|
||||||
log.info(
|
});
|
||||||
'Nothing found for view sync',
|
|
||||||
sync.get('senderId'),
|
if (!found) {
|
||||||
sync.get('senderE164'),
|
log.info(
|
||||||
sync.get('senderAci'),
|
`${logId}: nothing found`,
|
||||||
sync.get('timestamp')
|
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;
|
if (updatedFields) {
|
||||||
}
|
message.set(updatedFields);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
14
ts/model-types.d.ts
vendored
14
ts/model-types.d.ts
vendored
|
@ -23,7 +23,6 @@ import { SignalService as Proto } from './protobuf';
|
||||||
import type { AvatarDataType } from './types/Avatar';
|
import type { AvatarDataType } from './types/Avatar';
|
||||||
import type { AciString, PniString, ServiceIdString } from './types/ServiceId';
|
import type { AciString, PniString, ServiceIdString } from './types/ServiceId';
|
||||||
import type { StoryDistributionIdString } from './types/StoryDistributionId';
|
import type { StoryDistributionIdString } from './types/StoryDistributionId';
|
||||||
import type { ReactionSource } from './reactions/ReactionSource';
|
|
||||||
import type { SeenStatus } from './MessageSeenStatus';
|
import type { SeenStatus } from './MessageSeenStatus';
|
||||||
import type { GiftBadgeStates } from './components/conversation/Message';
|
import type { GiftBadgeStates } from './components/conversation/Message';
|
||||||
import type { LinkPreviewType } from './types/message/LinkPreviews';
|
import type { LinkPreviewType } from './types/message/LinkPreviews';
|
||||||
|
@ -507,16 +506,3 @@ export declare class ConversationModelCollectionType extends Backbone.Collection
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare class MessageModelCollectionType extends Backbone.Collection<MessageModel> {}
|
export declare class MessageModelCollectionType extends Backbone.Collection<MessageModel> {}
|
||||||
|
|
||||||
export type ReactionAttributesType = {
|
|
||||||
emoji: string;
|
|
||||||
fromId: string;
|
|
||||||
remove?: boolean;
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
|
@ -129,7 +129,7 @@ import {
|
||||||
conversationJobQueue,
|
conversationJobQueue,
|
||||||
conversationQueueJobEnum,
|
conversationQueueJobEnum,
|
||||||
} from '../jobs/conversationJobQueue';
|
} from '../jobs/conversationJobQueue';
|
||||||
import type { ReactionModel } from '../messageModifiers/Reactions';
|
import type { ReactionAttributesType } from '../messageModifiers/Reactions';
|
||||||
import { isAnnouncementGroupReady } from '../util/isAnnouncementGroupReady';
|
import { isAnnouncementGroupReady } from '../util/isAnnouncementGroupReady';
|
||||||
import { getProfile } from '../util/getProfile';
|
import { getProfile } from '../util/getProfile';
|
||||||
import { SEALED_SENDER } from '../types/SealedSender';
|
import { SEALED_SENDER } from '../types/SealedSender';
|
||||||
|
@ -4900,7 +4900,7 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
async notify(
|
async notify(
|
||||||
message: Readonly<MessageModel>,
|
message: Readonly<MessageModel>,
|
||||||
reaction?: Readonly<ReactionModel>
|
reaction?: Readonly<ReactionAttributesType>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// As a performance optimization don't perform any work if notifications are
|
// As a performance optimization don't perform any work if notifications are
|
||||||
// disabled.
|
// disabled.
|
||||||
|
@ -4938,7 +4938,7 @@ export class ConversationModel extends window.Backbone
|
||||||
const isMessageInDirectConversation = isDirectConversation(this.attributes);
|
const isMessageInDirectConversation = isDirectConversation(this.attributes);
|
||||||
|
|
||||||
const sender = reaction
|
const sender = reaction
|
||||||
? window.ConversationController.get(reaction.get('fromId'))
|
? window.ConversationController.get(reaction.fromId)
|
||||||
: getContact(message.attributes);
|
: getContact(message.attributes);
|
||||||
const senderName = sender
|
const senderName = sender
|
||||||
? sender.getTitle()
|
? sender.getTitle()
|
||||||
|
@ -4967,7 +4967,13 @@ export class ConversationModel extends window.Backbone
|
||||||
isExpiringMessage,
|
isExpiringMessage,
|
||||||
message: message.getNotificationText(),
|
message: message.getNotificationText(),
|
||||||
messageId,
|
messageId,
|
||||||
reaction: reaction ? reaction.toJSON() : null,
|
reaction: reaction
|
||||||
|
? {
|
||||||
|
emoji: reaction.emoji,
|
||||||
|
targetAuthorAci: reaction.targetAuthorAci,
|
||||||
|
targetTimestamp: reaction.targetTimestamp,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
sentAt: message.get('timestamp'),
|
sentAt: message.get('timestamp'),
|
||||||
type: reaction ? NotificationType.Reaction : NotificationType.Message,
|
type: reaction ? NotificationType.Reaction : NotificationType.Message,
|
||||||
});
|
});
|
||||||
|
|
|
@ -112,7 +112,7 @@ import {
|
||||||
getCallSelector,
|
getCallSelector,
|
||||||
getActiveCall,
|
getActiveCall,
|
||||||
} from '../state/selectors/calling';
|
} from '../state/selectors/calling';
|
||||||
import type { ReactionModel } from '../messageModifiers/Reactions';
|
import type { ReactionAttributesType } from '../messageModifiers/Reactions';
|
||||||
import { ReactionSource } from '../reactions/ReactionSource';
|
import { ReactionSource } from '../reactions/ReactionSource';
|
||||||
import * as LinkPreview from '../types/LinkPreview';
|
import * as LinkPreview from '../types/LinkPreview';
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
|
@ -2922,7 +2922,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleReaction(
|
async handleReaction(
|
||||||
reaction: ReactionModel,
|
reaction: ReactionAttributesType,
|
||||||
{
|
{
|
||||||
storyMessage,
|
storyMessage,
|
||||||
shouldPersist = true,
|
shouldPersist = true,
|
||||||
|
@ -2955,22 +2955,21 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFromThisDevice =
|
const isFromThisDevice = reaction.source === ReactionSource.FromThisDevice;
|
||||||
reaction.get('source') === ReactionSource.FromThisDevice;
|
const isFromSync = reaction.source === ReactionSource.FromSync;
|
||||||
const isFromSync = reaction.get('source') === ReactionSource.FromSync;
|
|
||||||
const isFromSomeoneElse =
|
const isFromSomeoneElse =
|
||||||
reaction.get('source') === ReactionSource.FromSomeoneElse;
|
reaction.source === ReactionSource.FromSomeoneElse;
|
||||||
strictAssert(
|
strictAssert(
|
||||||
isFromThisDevice || isFromSync || isFromSomeoneElse,
|
isFromThisDevice || isFromSync || isFromSomeoneElse,
|
||||||
'Reaction can only be from this device, from sync, or from someone else'
|
'Reaction can only be from this device, from sync, or from someone else'
|
||||||
);
|
);
|
||||||
|
|
||||||
const newReaction: MessageReactionType = {
|
const newReaction: MessageReactionType = {
|
||||||
emoji: reaction.get('remove') ? undefined : reaction.get('emoji'),
|
emoji: reaction.remove ? undefined : reaction.emoji,
|
||||||
fromId: reaction.get('fromId'),
|
fromId: reaction.fromId,
|
||||||
targetAuthorAci: reaction.get('targetAuthorAci'),
|
targetAuthorAci: reaction.targetAuthorAci,
|
||||||
targetTimestamp: reaction.get('targetTimestamp'),
|
targetTimestamp: reaction.targetTimestamp,
|
||||||
timestamp: reaction.get('timestamp'),
|
timestamp: reaction.timestamp,
|
||||||
isSentByConversationId: isFromThisDevice
|
isSentByConversationId: isFromThisDevice
|
||||||
? zipObject(conversation.getMemberConversationIds(), repeat(false))
|
? zipObject(conversation.getMemberConversationIds(), repeat(false))
|
||||||
: undefined,
|
: undefined,
|
||||||
|
@ -2997,7 +2996,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const generatedMessage = reaction.get('storyReactionMessage');
|
const generatedMessage = reaction.storyReactionMessage;
|
||||||
strictAssert(
|
strictAssert(
|
||||||
generatedMessage,
|
generatedMessage,
|
||||||
'Story reactions must provide storyReactionMessage'
|
'Story reactions must provide storyReactionMessage'
|
||||||
|
@ -3016,9 +3015,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
: undefined,
|
: undefined,
|
||||||
storyId: storyMessage.id,
|
storyId: storyMessage.id,
|
||||||
storyReaction: {
|
storyReaction: {
|
||||||
emoji: reaction.get('emoji'),
|
emoji: reaction.emoji,
|
||||||
targetAuthorAci: reaction.get('targetAuthorAci'),
|
targetAuthorAci: reaction.targetAuthorAci,
|
||||||
targetTimestamp: reaction.get('targetTimestamp'),
|
targetTimestamp: reaction.targetTimestamp,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3036,8 +3035,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
generatedMessage.attributes
|
generatedMessage.attributes
|
||||||
),
|
),
|
||||||
storyId: getMessageIdForLogging(storyMessage),
|
storyId: getMessageIdForLogging(storyMessage),
|
||||||
targetTimestamp: reaction.get('targetTimestamp'),
|
targetTimestamp: reaction.targetTimestamp,
|
||||||
timestamp: reaction.get('timestamp'),
|
timestamp: reaction.timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
const messageToAdd = window.MessageController.register(
|
const messageToAdd = window.MessageController.register(
|
||||||
|
@ -3091,7 +3090,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
this.clearNotifications(oldReaction);
|
this.clearNotifications(oldReaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reaction.get('remove')) {
|
if (reaction.remove) {
|
||||||
log.info(
|
log.info(
|
||||||
'handleReaction: removing reaction for message',
|
'handleReaction: removing reaction for message',
|
||||||
this.idForLogging()
|
this.idForLogging()
|
||||||
|
@ -3101,7 +3100,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
reactions = oldReactions.filter(
|
reactions = oldReactions.filter(
|
||||||
re =>
|
re =>
|
||||||
!isNewReactionReplacingPrevious(re, newReaction) ||
|
!isNewReactionReplacingPrevious(re, newReaction) ||
|
||||||
re.timestamp > reaction.get('timestamp')
|
re.timestamp > reaction.timestamp
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
reactions = oldReactions.filter(
|
reactions = oldReactions.filter(
|
||||||
|
@ -3111,10 +3110,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
this.set({ reactions });
|
this.set({ reactions });
|
||||||
|
|
||||||
await window.Signal.Data.removeReactionFromConversation({
|
await window.Signal.Data.removeReactionFromConversation({
|
||||||
emoji: reaction.get('emoji'),
|
emoji: reaction.emoji,
|
||||||
fromId: reaction.get('fromId'),
|
fromId: reaction.fromId,
|
||||||
targetAuthorServiceId: reaction.get('targetAuthorAci'),
|
targetAuthorServiceId: reaction.targetAuthorAci,
|
||||||
targetTimestamp: reaction.get('targetTimestamp'),
|
targetTimestamp: reaction.targetTimestamp,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log.info(
|
log.info(
|
||||||
|
@ -3126,9 +3125,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
if (isFromSync) {
|
if (isFromSync) {
|
||||||
const ourReactions = [
|
const ourReactions = [
|
||||||
newReaction,
|
newReaction,
|
||||||
...oldReactions.filter(
|
...oldReactions.filter(re => re.fromId === reaction.fromId),
|
||||||
re => re.fromId === reaction.get('fromId')
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
reactionToAdd = maxBy(ourReactions, 'timestamp') || newReaction;
|
reactionToAdd = maxBy(ourReactions, 'timestamp') || newReaction;
|
||||||
} else {
|
} else {
|
||||||
|
@ -3136,7 +3133,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
reactions = oldReactions.filter(
|
reactions = oldReactions.filter(
|
||||||
re => !isNewReactionReplacingPrevious(re, reaction.attributes)
|
re => !isNewReactionReplacingPrevious(re, reaction)
|
||||||
);
|
);
|
||||||
reactions.push(reactionToAdd);
|
reactions.push(reactionToAdd);
|
||||||
this.set({ reactions });
|
this.set({ reactions });
|
||||||
|
@ -3147,12 +3144,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
|
|
||||||
await window.Signal.Data.addReaction({
|
await window.Signal.Data.addReaction({
|
||||||
conversationId: this.get('conversationId'),
|
conversationId: this.get('conversationId'),
|
||||||
emoji: reaction.get('emoji'),
|
emoji: reaction.emoji,
|
||||||
fromId: reaction.get('fromId'),
|
fromId: reaction.fromId,
|
||||||
messageId: this.id,
|
messageId: this.id,
|
||||||
messageReceivedAt: this.get('received_at'),
|
messageReceivedAt: this.get('received_at'),
|
||||||
targetAuthorAci: reaction.get('targetAuthorAci'),
|
targetAuthorAci: reaction.targetAuthorAci,
|
||||||
targetTimestamp: reaction.get('targetTimestamp'),
|
targetTimestamp: reaction.targetTimestamp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3173,7 +3170,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
'New story reaction must have an emoji'
|
'New story reaction must have an emoji'
|
||||||
);
|
);
|
||||||
|
|
||||||
const generatedMessage = reaction.get('storyReactionMessage');
|
const generatedMessage = reaction.storyReactionMessage;
|
||||||
strictAssert(
|
strictAssert(
|
||||||
generatedMessage,
|
generatedMessage,
|
||||||
'Story reactions must provide storyReactionmessage'
|
'Story reactions must provide storyReactionmessage'
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import noop from 'lodash/noop';
|
||||||
import { v4 as generateUuid } from 'uuid';
|
import { v4 as generateUuid } from 'uuid';
|
||||||
|
|
||||||
import { ReactionModel } from '../messageModifiers/Reactions';
|
import type { ReactionAttributesType } from '../messageModifiers/Reactions';
|
||||||
import { ReactionSource } from './ReactionSource';
|
import { ReactionSource } from './ReactionSource';
|
||||||
import { getMessageById } from '../messages/getMessageById';
|
import { getMessageById } from '../messages/getMessageById';
|
||||||
import { getSourceServiceId, isStory } from '../messages/helpers';
|
import { getSourceServiceId, isStory } from '../messages/helpers';
|
||||||
|
@ -98,7 +99,9 @@ export async function enqueueReactionForSend({
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const reaction = new ReactionModel({
|
const reaction: ReactionAttributesType = {
|
||||||
|
envelopeId: generateUuid(),
|
||||||
|
removeFromMessageReceiverCache: noop,
|
||||||
emoji,
|
emoji,
|
||||||
fromId: window.ConversationController.getOurConversationIdOrThrow(),
|
fromId: window.ConversationController.getOurConversationIdOrThrow(),
|
||||||
remove,
|
remove,
|
||||||
|
@ -107,7 +110,7 @@ export async function enqueueReactionForSend({
|
||||||
targetAuthorAci,
|
targetAuthorAci,
|
||||||
targetTimestamp,
|
targetTimestamp,
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
};
|
||||||
|
|
||||||
await message.handleReaction(reaction, { storyMessage });
|
await message.handleReaction(reaction, { storyMessage });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1675,6 +1675,7 @@ export default class MessageReceiver
|
||||||
getEnvelopeId(envelope),
|
getEnvelopeId(envelope),
|
||||||
new DeliveryEvent(
|
new DeliveryEvent(
|
||||||
{
|
{
|
||||||
|
envelopeId: envelope.id,
|
||||||
timestamp: envelope.timestamp,
|
timestamp: envelope.timestamp,
|
||||||
envelopeTimestamp: envelope.timestamp,
|
envelopeTimestamp: envelope.timestamp,
|
||||||
source: envelope.source,
|
source: envelope.source,
|
||||||
|
@ -2857,6 +2858,7 @@ export default class MessageReceiver
|
||||||
receiptMessage.timestamp.map(async rawTimestamp => {
|
receiptMessage.timestamp.map(async rawTimestamp => {
|
||||||
const ev = new EventClass(
|
const ev = new EventClass(
|
||||||
{
|
{
|
||||||
|
envelopeId: envelope.id,
|
||||||
timestamp: rawTimestamp?.toNumber(),
|
timestamp: rawTimestamp?.toNumber(),
|
||||||
envelopeTimestamp: envelope.timestamp,
|
envelopeTimestamp: envelope.timestamp,
|
||||||
source: envelope.source,
|
source: envelope.source,
|
||||||
|
@ -3255,6 +3257,7 @@ export default class MessageReceiver
|
||||||
|
|
||||||
const ev = new MessageRequestResponseEvent(
|
const ev = new MessageRequestResponseEvent(
|
||||||
{
|
{
|
||||||
|
envelopeId: envelope.id,
|
||||||
threadE164: dropNull(sync.threadE164),
|
threadE164: dropNull(sync.threadE164),
|
||||||
threadAci: sync.threadAci
|
threadAci: sync.threadAci
|
||||||
? normalizeAci(
|
? normalizeAci(
|
||||||
|
@ -3393,6 +3396,7 @@ export default class MessageReceiver
|
||||||
for (const { timestamp, sender, senderAci } of read) {
|
for (const { timestamp, sender, senderAci } of read) {
|
||||||
const ev = new ReadSyncEvent(
|
const ev = new ReadSyncEvent(
|
||||||
{
|
{
|
||||||
|
envelopeId: envelope.id,
|
||||||
envelopeTimestamp: envelope.timestamp,
|
envelopeTimestamp: envelope.timestamp,
|
||||||
timestamp: timestamp?.toNumber(),
|
timestamp: timestamp?.toNumber(),
|
||||||
sender: dropNull(sender),
|
sender: dropNull(sender),
|
||||||
|
@ -3420,6 +3424,7 @@ export default class MessageReceiver
|
||||||
viewed.map(async ({ timestamp, senderE164, senderAci }) => {
|
viewed.map(async ({ timestamp, senderE164, senderAci }) => {
|
||||||
const ev = new ViewSyncEvent(
|
const ev = new ViewSyncEvent(
|
||||||
{
|
{
|
||||||
|
envelopeId: envelope.id,
|
||||||
envelopeTimestamp: envelope.timestamp,
|
envelopeTimestamp: envelope.timestamp,
|
||||||
timestamp: timestamp?.toNumber(),
|
timestamp: timestamp?.toNumber(),
|
||||||
senderE164: dropNull(senderE164),
|
senderE164: dropNull(senderE164),
|
||||||
|
|
|
@ -110,6 +110,7 @@ export class ConfirmableEvent extends Event {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DeliveryEventData = Readonly<{
|
export type DeliveryEventData = Readonly<{
|
||||||
|
envelopeId: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
envelopeTimestamp: number;
|
envelopeTimestamp: number;
|
||||||
source?: string;
|
source?: string;
|
||||||
|
@ -240,6 +241,7 @@ export class MessageEvent extends ConfirmableEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReadOrViewEventData = Readonly<{
|
export type ReadOrViewEventData = Readonly<{
|
||||||
|
envelopeId: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
envelopeTimestamp: number;
|
envelopeTimestamp: number;
|
||||||
source?: string;
|
source?: string;
|
||||||
|
@ -301,6 +303,7 @@ export class ViewOnceOpenSyncEvent extends ConfirmableEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageRequestResponseOptions = {
|
export type MessageRequestResponseOptions = {
|
||||||
|
envelopeId: string;
|
||||||
threadE164?: string;
|
threadE164?: string;
|
||||||
threadAci?: AciString;
|
threadAci?: AciString;
|
||||||
messageRequestResponseType: Proto.SyncMessage.IMessageRequestResponse['type'];
|
messageRequestResponseType: Proto.SyncMessage.IMessageRequestResponse['type'];
|
||||||
|
@ -319,8 +322,11 @@ export class MessageRequestResponseEvent extends ConfirmableEvent {
|
||||||
|
|
||||||
public readonly groupV2Id?: string;
|
public readonly groupV2Id?: string;
|
||||||
|
|
||||||
|
public readonly envelopeId?: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
|
envelopeId,
|
||||||
threadE164,
|
threadE164,
|
||||||
threadAci,
|
threadAci,
|
||||||
messageRequestResponseType,
|
messageRequestResponseType,
|
||||||
|
@ -331,6 +337,7 @@ export class MessageRequestResponseEvent extends ConfirmableEvent {
|
||||||
) {
|
) {
|
||||||
super('messageRequestResponse', confirm);
|
super('messageRequestResponse', confirm);
|
||||||
|
|
||||||
|
this.envelopeId = envelopeId;
|
||||||
this.threadE164 = threadE164;
|
this.threadE164 = threadE164;
|
||||||
this.threadAci = threadAci;
|
this.threadAci = threadAci;
|
||||||
this.messageRequestResponseType = messageRequestResponseType;
|
this.messageRequestResponseType = messageRequestResponseType;
|
||||||
|
@ -374,6 +381,7 @@ export class StickerPackEvent extends ConfirmableEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReadSyncEventData = Readonly<{
|
export type ReadSyncEventData = Readonly<{
|
||||||
|
envelopeId: string;
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
envelopeTimestamp: number;
|
envelopeTimestamp: number;
|
||||||
sender?: string;
|
sender?: string;
|
||||||
|
@ -390,6 +398,7 @@ export class ReadSyncEvent extends ConfirmableEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ViewSyncEventData = Readonly<{
|
export type ViewSyncEventData = Readonly<{
|
||||||
|
envelopeId: string;
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
envelopeTimestamp: number;
|
envelopeTimestamp: number;
|
||||||
senderE164?: string;
|
senderE164?: string;
|
||||||
|
|
|
@ -9,17 +9,14 @@ import type { SendStateByConversationId } from '../messages/MessageSendState';
|
||||||
import * as Edits from '../messageModifiers/Edits';
|
import * as Edits from '../messageModifiers/Edits';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import * as Deletes from '../messageModifiers/Deletes';
|
import * as Deletes from '../messageModifiers/Deletes';
|
||||||
import {
|
import * as MessageReceipts from '../messageModifiers/MessageReceipts';
|
||||||
MessageReceipts,
|
import * as Reactions from '../messageModifiers/Reactions';
|
||||||
MessageReceiptType,
|
import * as ReadSyncs from '../messageModifiers/ReadSyncs';
|
||||||
} from '../messageModifiers/MessageReceipts';
|
import * as ViewOnceOpenSyncs from '../messageModifiers/ViewOnceOpenSyncs';
|
||||||
import { Reactions } from '../messageModifiers/Reactions';
|
import * as ViewSyncs from '../messageModifiers/ViewSyncs';
|
||||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||||
import { ReadSyncs } from '../messageModifiers/ReadSyncs';
|
|
||||||
import { SeenStatus } from '../MessageSeenStatus';
|
import { SeenStatus } from '../MessageSeenStatus';
|
||||||
import { SendActionType, sendStateReducer } from '../messages/MessageSendState';
|
import { SendActionType, sendStateReducer } from '../messages/MessageSendState';
|
||||||
import { ViewOnceOpenSyncs } from '../messageModifiers/ViewOnceOpenSyncs';
|
|
||||||
import { ViewSyncs } from '../messageModifiers/ViewSyncs';
|
|
||||||
import { canConversationBeUnarchived } from './canConversationBeUnarchived';
|
import { canConversationBeUnarchived } from './canConversationBeUnarchived';
|
||||||
import { deleteForEveryone } from './deleteForEveryone';
|
import { deleteForEveryone } from './deleteForEveryone';
|
||||||
import { handleEditMessage } from './handleEditMessage';
|
import { handleEditMessage } from './handleEditMessage';
|
||||||
|
@ -48,33 +45,31 @@ export async function modifyTargetMessage(
|
||||||
const sourceServiceId = getSourceServiceId(message.attributes);
|
const sourceServiceId = getSourceServiceId(message.attributes);
|
||||||
|
|
||||||
if (type === 'outgoing' || (type === 'story' && ourAci === sourceServiceId)) {
|
if (type === 'outgoing' || (type === 'story' && ourAci === sourceServiceId)) {
|
||||||
const sendActions = MessageReceipts.getSingleton()
|
const sendActions = MessageReceipts.forMessage(message).map(receipt => {
|
||||||
.forMessage(message)
|
let sendActionType: SendActionType;
|
||||||
.map(receipt => {
|
const receiptType = receipt.type;
|
||||||
let sendActionType: SendActionType;
|
switch (receiptType) {
|
||||||
const receiptType = receipt.get('type');
|
case MessageReceipts.MessageReceiptType.Delivery:
|
||||||
switch (receiptType) {
|
sendActionType = SendActionType.GotDeliveryReceipt;
|
||||||
case MessageReceiptType.Delivery:
|
break;
|
||||||
sendActionType = SendActionType.GotDeliveryReceipt;
|
case MessageReceipts.MessageReceiptType.Read:
|
||||||
break;
|
sendActionType = SendActionType.GotReadReceipt;
|
||||||
case MessageReceiptType.Read:
|
break;
|
||||||
sendActionType = SendActionType.GotReadReceipt;
|
case MessageReceipts.MessageReceiptType.View:
|
||||||
break;
|
sendActionType = SendActionType.GotViewedReceipt;
|
||||||
case MessageReceiptType.View:
|
break;
|
||||||
sendActionType = SendActionType.GotViewedReceipt;
|
default:
|
||||||
break;
|
throw missingCaseError(receiptType);
|
||||||
default:
|
}
|
||||||
throw missingCaseError(receiptType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destinationConversationId: receipt.get('sourceConversationId'),
|
destinationConversationId: receipt.sourceConversationId,
|
||||||
action: {
|
action: {
|
||||||
type: sendActionType,
|
type: sendActionType,
|
||||||
updatedAt: receipt.get('receiptTimestamp'),
|
updatedAt: receipt.receiptTimestamp,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const oldSendStateByConversationId =
|
const oldSendStateByConversationId =
|
||||||
message.get('sendStateByConversationId') || {};
|
message.get('sendStateByConversationId') || {};
|
||||||
|
@ -111,10 +106,10 @@ export async function modifyTargetMessage(
|
||||||
if (type === 'incoming') {
|
if (type === 'incoming') {
|
||||||
// In a followup (see DESKTOP-2100), we want to make `ReadSyncs#forMessage` return
|
// In a followup (see DESKTOP-2100), we want to make `ReadSyncs#forMessage` return
|
||||||
// an array, not an object. This array wrapping makes that future a bit easier.
|
// an array, not an object. This array wrapping makes that future a bit easier.
|
||||||
const readSync = ReadSyncs.getSingleton().forMessage(message);
|
const readSync = ReadSyncs.forMessage(message);
|
||||||
const readSyncs = readSync ? [readSync] : [];
|
const readSyncs = readSync ? [readSync] : [];
|
||||||
|
|
||||||
const viewSyncs = ViewSyncs.getSingleton().forMessage(message);
|
const viewSyncs = ViewSyncs.forMessage(message);
|
||||||
|
|
||||||
const isGroupStoryReply =
|
const isGroupStoryReply =
|
||||||
isGroup(conversation.attributes) && message.get('storyId');
|
isGroup(conversation.attributes) && message.get('storyId');
|
||||||
|
@ -122,8 +117,8 @@ export async function modifyTargetMessage(
|
||||||
if (readSyncs.length !== 0 || viewSyncs.length !== 0) {
|
if (readSyncs.length !== 0 || viewSyncs.length !== 0) {
|
||||||
const markReadAt = Math.min(
|
const markReadAt = Math.min(
|
||||||
Date.now(),
|
Date.now(),
|
||||||
...readSyncs.map(sync => sync.get('readAt')),
|
...readSyncs.map(sync => sync.readAt),
|
||||||
...viewSyncs.map(sync => sync.get('viewedAt'))
|
...viewSyncs.map(sync => sync.viewedAt)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (message.get('expireTimer')) {
|
if (message.get('expireTimer')) {
|
||||||
|
@ -181,8 +176,7 @@ export async function modifyTargetMessage(
|
||||||
|
|
||||||
// Check for out-of-order view once open syncs
|
// Check for out-of-order view once open syncs
|
||||||
if (isTapToView(message.attributes)) {
|
if (isTapToView(message.attributes)) {
|
||||||
const viewOnceOpenSync =
|
const viewOnceOpenSync = ViewOnceOpenSyncs.forMessage(message);
|
||||||
ViewOnceOpenSyncs.getSingleton().forMessage(message);
|
|
||||||
if (viewOnceOpenSync) {
|
if (viewOnceOpenSync) {
|
||||||
await message.markViewOnceMessageViewed({ fromSync: true });
|
await message.markViewOnceMessageViewed({ fromSync: true });
|
||||||
changed = true;
|
changed = true;
|
||||||
|
@ -191,7 +185,7 @@ export async function modifyTargetMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isStory(message.attributes)) {
|
if (isStory(message.attributes)) {
|
||||||
const viewSyncs = ViewSyncs.getSingleton().forMessage(message);
|
const viewSyncs = ViewSyncs.forMessage(message);
|
||||||
|
|
||||||
if (viewSyncs.length !== 0) {
|
if (viewSyncs.length !== 0) {
|
||||||
message.set({
|
message.set({
|
||||||
|
@ -202,7 +196,7 @@ export async function modifyTargetMessage(
|
||||||
|
|
||||||
const markReadAt = Math.min(
|
const markReadAt = Math.min(
|
||||||
Date.now(),
|
Date.now(),
|
||||||
...viewSyncs.map(sync => sync.get('viewedAt'))
|
...viewSyncs.map(sync => sync.viewedAt)
|
||||||
);
|
);
|
||||||
message.setPendingMarkRead(
|
message.setPendingMarkRead(
|
||||||
Math.min(message.getPendingMarkRead() ?? Date.now(), markReadAt)
|
Math.min(message.getPendingMarkRead() ?? Date.now(), markReadAt)
|
||||||
|
@ -220,12 +214,12 @@ export async function modifyTargetMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Does message message have any pending, previously-received associated reactions?
|
// Does message message have any pending, previously-received associated reactions?
|
||||||
const reactions = Reactions.getSingleton().forMessage(message);
|
const reactions = Reactions.forMessage(message);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
reactions.map(async reaction => {
|
reactions.map(async reaction => {
|
||||||
if (isStory(message.attributes)) {
|
if (isStory(message.attributes)) {
|
||||||
// We don't set changed = true here, because we don't modify the original story
|
// We don't set changed = true here, because we don't modify the original story
|
||||||
const generatedMessage = reaction.get('storyReactionMessage');
|
const generatedMessage = reaction.storyReactionMessage;
|
||||||
strictAssert(
|
strictAssert(
|
||||||
generatedMessage,
|
generatedMessage,
|
||||||
'Story reactions must provide storyReactionMessage'
|
'Story reactions must provide storyReactionMessage'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue