Fix for unread syncs and ooo reactions

This commit is contained in:
Josh Perez 2021-03-12 20:22:36 -05:00 committed by Josh Perez
parent 55f0beaa6d
commit 62e04a1bbd
7 changed files with 178 additions and 102 deletions

View file

@ -69,6 +69,7 @@
if (message.isUnread()) { if (message.isUnread()) {
await message.markRead(readAt, { skipSave: true }); await message.markRead(readAt, { skipSave: true });
const updateConversation = () => {
// onReadMessage may result in messages older than this one being // onReadMessage may result in messages older than this one being
// marked read. We want those messages to have the same expire timer // marked read. We want those messages to have the same expire timer
// start time as this one, so we pass the readAt value through. // start time as this one, so we pass the readAt value through.
@ -76,6 +77,19 @@
if (conversation) { if (conversation) {
conversation.onReadMessage(message, readAt); conversation.onReadMessage(message, readAt);
} }
};
if (window.startupProcessingQueue) {
const conversation = message.getConversation();
if (conversation) {
window.startupProcessingQueue.add(
conversation.get('id'),
updateConversation
);
}
} else {
updateConversation();
}
} else { } else {
const now = Date.now(); const now = Date.now();
const existingTimestamp = message.get('expirationStartTimestamp'); const existingTimestamp = message.get('expirationStartTimestamp');

View file

@ -9,6 +9,7 @@ import { isWindowDragElement } from './util/isWindowDragElement';
import { assert } from './util/assert'; import { assert } from './util/assert';
export async function startApp(): Promise<void> { export async function startApp(): Promise<void> {
window.startupProcessingQueue = new window.Signal.Util.StartupQueue();
window.attachmentDownloadQueue = []; window.attachmentDownloadQueue = [];
try { try {
window.log.info('Initializing SQL in renderer'); window.log.info('Initializing SQL in renderer');
@ -2061,13 +2062,18 @@ export async function startApp(): Promise<void> {
clearInterval(interval!); clearInterval(interval!);
interval = null; interval = null;
view.onEmpty(); view.onEmpty();
window.logAppLoadedEvent(); window.logAppLoadedEvent();
if (messageReceiver) {
window.log.info( window.log.info(
'App loaded - messages:', 'App loaded - messages:',
messageReceiver.getProcessedCount() messageReceiver.getProcessedCount()
); );
}
window.sqlInitializer.goBackToMainProcess(); window.sqlInitializer.goBackToMainProcess();
window.Signal.Util.setBatchingStrategy(false); window.Signal.Util.setBatchingStrategy(false);
const attachmentDownloadQueue = window.attachmentDownloadQueue || []; const attachmentDownloadQueue = window.attachmentDownloadQueue || [];
const THREE_DAYS_AGO = Date.now() - 3600 * 72 * 1000; const THREE_DAYS_AGO = Date.now() - 3600 * 72 * 1000;
const MAX_ATTACHMENT_MSGS_TO_DOWNLOAD = 250; const MAX_ATTACHMENT_MSGS_TO_DOWNLOAD = 250;
@ -2081,7 +2087,12 @@ export async function startApp(): Promise<void> {
attachmentsToDownload.length, attachmentsToDownload.length,
attachmentDownloadQueue.length attachmentDownloadQueue.length
); );
window.attachmentDownloadQueue = undefined;
if (window.startupProcessingQueue) {
window.startupProcessingQueue.flush();
window.startupProcessingQueue = undefined;
}
const messagesWithDownloads = await Promise.all( const messagesWithDownloads = await Promise.all(
attachmentsToDownload.map(message => attachmentsToDownload.map(message =>
message.queueAttachmentDownloads() message.queueAttachmentDownloads()

View file

@ -3532,21 +3532,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return; return;
} }
if (type === 'outgoing') {
const receipts = window.Whisper.DeliveryReceipts.forMessage(
conversation,
message
);
receipts.forEach(receipt =>
message.set({
delivered: (message.get('delivered') || 0) + 1,
delivered_to: _.union(message.get('delivered_to') || [], [
receipt.get('deliveredTo'),
]),
})
);
}
attributes.active_at = now; attributes.active_at = now;
conversation.set(attributes); conversation.set(attributes);
@ -3608,6 +3593,158 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
} }
} }
if (dataMessage.profileKey) {
const profileKey = dataMessage.profileKey.toString('base64');
if (
source === window.textsecure.storage.user.getNumber() ||
sourceUuid === window.textsecure.storage.user.getUuid()
) {
conversation.set({ profileSharing: true });
} else if (conversation.isPrivate()) {
conversation.setProfileKey(profileKey);
} else {
const localId = window.ConversationController.ensureContactIds({
e164: source,
uuid: sourceUuid,
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
window.ConversationController.get(localId)!.setProfileKey(
profileKey
);
}
}
if (message.isTapToView() && type === 'outgoing') {
await message.eraseContents();
}
if (
type === 'incoming' &&
message.isTapToView() &&
!message.isValidTapToView()
) {
window.log.warn(
`Received tap to view message ${message.idForLogging()} with invalid data. Erasing contents.`
);
message.set({
isTapToViewInvalid: true,
});
await message.eraseContents();
}
}
const conversationTimestamp = conversation.get('timestamp');
if (
!conversationTimestamp ||
message.get('sent_at') > conversationTimestamp
) {
conversation.set({
lastMessage: message.getNotificationText(),
timestamp: message.get('sent_at'),
});
}
window.MessageController.register(
message.id,
message as typeof window.WhatIsThis
);
conversation.incrementMessageCount();
window.Signal.Data.updateConversation(conversation.attributes);
// Only queue attachments for downloads if this is an outgoing message
// or we've accepted the conversation
const reduxState = window.reduxStore.getState();
const attachments = this.get('attachments') || [];
const shouldHoldOffDownload =
(isImage(attachments) || isVideo(attachments)) &&
isInCall(reduxState);
if (
this.hasAttachmentDownloads() &&
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(this.getConversation()!.getAccepted() || message.isOutgoing()) &&
!shouldHoldOffDownload
) {
if (window.attachmentDownloadQueue) {
window.attachmentDownloadQueue.unshift(message);
window.log.info(
'Adding to attachmentDownloadQueue',
message.get('sent_at')
);
} else {
await message.queueAttachmentDownloads();
}
}
await this.modifyTargetMessage(conversation, isGroupV2);
window.log.info(
'handleDataMessage: Batching save for',
message.get('sent_at')
);
this.saveAndNotify(conversation, isGroupV2, confirm);
} catch (error) {
const errorForLog = error && error.stack ? error.stack : error;
window.log.error(
'handleDataMessage',
message.idForLogging(),
'error:',
errorForLog
);
throw error;
}
});
}
async saveAndNotify(
conversation: ConversationModel,
isGroupV2: boolean,
confirm: () => void
): Promise<void> {
await window.Signal.Util.saveNewMessageBatcher.add(this.attributes);
window.log.info('Message saved', this.get('sent_at'));
conversation.trigger('newmessage', this);
await this.modifyTargetMessage(conversation, isGroupV2);
if (this.get('unread')) {
await conversation.notify(this);
}
// Increment the sent message count if this is an outgoing message
if (this.get('type') === 'outgoing') {
conversation.incrementSentMessageCount();
}
window.Whisper.events.trigger('incrementProgress');
confirm();
}
async modifyTargetMessage(
conversation: ConversationModel,
isGroupV2: boolean
): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const message = this;
const type = message.get('type');
if (type === 'outgoing') {
const receipts = window.Whisper.DeliveryReceipts.forMessage(
conversation,
message
);
receipts.forEach(receipt =>
message.set({
delivered: (message.get('delivered') || 0) + 1,
delivered_to: _.union(message.get('delivered_to') || [], [
receipt.get('deliveredTo'),
]),
})
);
}
if (!isGroupV2) {
if (type === 'incoming') { if (type === 'incoming') {
const readSync = window.Whisper.ReadSyncs.forMessage(message); const readSync = window.Whisper.ReadSyncs.forMessage(message);
if (readSync) { if (readSync) {
@ -3661,44 +3798,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
message.set({ recipients: conversation.getRecipients() }); message.set({ recipients: conversation.getRecipients() });
} }
if (dataMessage.profileKey) {
const profileKey = dataMessage.profileKey.toString('base64');
if (
source === window.textsecure.storage.user.getNumber() ||
sourceUuid === window.textsecure.storage.user.getUuid()
) {
conversation.set({ profileSharing: true });
} else if (conversation.isPrivate()) {
conversation.setProfileKey(profileKey);
} else {
const localId = window.ConversationController.ensureContactIds({
e164: source,
uuid: sourceUuid,
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
window.ConversationController.get(localId)!.setProfileKey(
profileKey
);
}
}
if (message.isTapToView() && type === 'outgoing') {
await message.eraseContents();
}
if (
type === 'incoming' &&
message.isTapToView() &&
!message.isValidTapToView()
) {
window.log.warn(
`Received tap to view message ${message.idForLogging()} with invalid data. Erasing contents.`
);
message.set({
isTapToViewInvalid: true,
});
await message.eraseContents();
}
// Check for out-of-order view syncs // Check for out-of-order view syncs
if (type === 'incoming' && message.isTapToView()) { if (type === 'incoming' && message.isTapToView()) {
const viewSync = window.Whisper.ViewSyncs.forMessage(message); const viewSync = window.Whisper.ViewSyncs.forMessage(message);
@ -3708,48 +3807,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
} }
} }
const conversationTimestamp = conversation.get('timestamp');
if (
!conversationTimestamp ||
message.get('sent_at') > conversationTimestamp
) {
conversation.set({
lastMessage: message.getNotificationText(),
timestamp: message.get('sent_at'),
});
}
window.MessageController.register(
message.id,
message as typeof window.WhatIsThis
);
conversation.incrementMessageCount();
window.Signal.Data.updateConversation(conversation.attributes);
// Only queue attachments for downloads if this is an outgoing message
// or we've accepted the conversation
const reduxState = window.reduxStore.getState();
const attachments = this.get('attachments') || [];
const shouldHoldOffDownload =
(isImage(attachments) || isVideo(attachments)) &&
isInCall(reduxState);
if (
this.hasAttachmentDownloads() &&
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(this.getConversation()!.getAccepted() || message.isOutgoing()) &&
!shouldHoldOffDownload
) {
if (window.attachmentDownloadQueue) {
window.attachmentDownloadQueue.unshift(message);
window.log.info(
'Adding to attachmentDownloadQueue',
message.get('sent_at')
);
} else {
await message.queueAttachmentDownloads();
}
}
// Does this message have any pending, previously-received associated reactions? // Does this message have any pending, previously-received associated reactions?
const reactions = window.Whisper.Reactions.forMessage(message); const reactions = window.Whisper.Reactions.forMessage(message);
await Promise.all( await Promise.all(
@ -3764,46 +3821,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
window.Signal.Util.deleteForEveryone(message, del, false) window.Signal.Util.deleteForEveryone(message, del, false)
) )
); );
window.log.info(
'handleDataMessage: Batching save for',
message.get('sent_at')
);
this.saveAndNotify(conversation, confirm);
} catch (error) {
const errorForLog = error && error.stack ? error.stack : error;
window.log.error(
'handleDataMessage',
message.idForLogging(),
'error:',
errorForLog
);
throw error;
}
});
}
async saveAndNotify(
conversation: ConversationModel,
confirm: () => void
): Promise<void> {
await window.Signal.Util.saveNewMessageBatcher.add(this.attributes);
window.log.info('Message saved', this.get('sent_at'));
conversation.trigger('newmessage', this);
if (this.get('unread')) {
await conversation.notify(this);
}
// Increment the sent message count if this is an outgoing message
if (this.get('type') === 'outgoing') {
conversation.incrementSentMessageCount();
}
window.Whisper.events.trigger('incrementProgress');
confirm();
} }
async handleReaction( async handleReaction(

View file

@ -441,6 +441,7 @@ class MessageReceiverInner extends EventTarget {
); );
this.cacheAndHandle(envelope, plaintext, request); this.cacheAndHandle(envelope, plaintext, request);
this.processedCount += 1;
} catch (e) { } catch (e) {
request.respond(500, 'Bad encrypted websocket message'); request.respond(500, 'Bad encrypted websocket message');
window.log.error( window.log.error(
@ -787,7 +788,6 @@ class MessageReceiverInner extends EventTarget {
removeFromCache(envelope: EnvelopeClass) { removeFromCache(envelope: EnvelopeClass) {
const { id } = envelope; const { id } = envelope;
this.cacheRemoveBatcher.add(id); this.cacheRemoveBatcher.add(id);
this.processedCount += 1;
} }
// Same as handleEnvelope, just without the decryption step. Necessary for handling // Same as handleEnvelope, just without the decryption step. Necessary for handling

30
ts/util/StartupQueue.ts Normal file
View file

@ -0,0 +1,30 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export class StartupQueue {
set: Set<string>;
items: Array<() => void>;
constructor() {
this.set = new Set();
this.items = [];
}
add(id: string, f: () => void): void {
if (this.set.has(id)) {
return;
}
this.items.push(f);
this.set.add(id);
}
flush(): void {
const { items } = this;
window.log.info('StartupQueue: Processing', items.length, 'actions');
items.forEach(f => f());
this.items = [];
this.set.clear();
}
}

View file

@ -33,10 +33,12 @@ import {
sessionStructureToArrayBuffer, sessionStructureToArrayBuffer,
} from './sessionTranslation'; } from './sessionTranslation';
import * as zkgroup from './zkgroup'; import * as zkgroup from './zkgroup';
import { StartupQueue } from './StartupQueue';
export { export {
GoogleChrome, GoogleChrome,
Registration, Registration,
StartupQueue,
arrayBufferToObjectURL, arrayBufferToObjectURL,
combineNames, combineNames,
createBatcher, createBatcher,

2
ts/window.d.ts vendored
View file

@ -94,6 +94,7 @@ import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
import { MIMEType } from './types/MIME'; import { MIMEType } from './types/MIME';
import { ElectronLocaleType } from './util/mapToSupportLocale'; import { ElectronLocaleType } from './util/mapToSupportLocale';
import { SignalProtocolStore } from './LibSignalStore'; import { SignalProtocolStore } from './LibSignalStore';
import { StartupQueue } from './util/StartupQueue';
export { Long } from 'long'; export { Long } from 'long';
@ -138,6 +139,7 @@ declare global {
WhatIsThis: WhatIsThis; WhatIsThis: WhatIsThis;
attachmentDownloadQueue: Array<MessageModel> | undefined; attachmentDownloadQueue: Array<MessageModel> | undefined;
startupProcessingQueue: StartupQueue | undefined;
baseAttachmentsPath: string; baseAttachmentsPath: string;
baseStickersPath: string; baseStickersPath: string;
baseTempPath: string; baseTempPath: string;