Move receipts and view/read syncs to new syncTasks system
This commit is contained in:
		
					parent
					
						
							
								1a263e63da
							
						
					
				
			
			
				commit
				
					
						75c32e86f0
					
				
			
		
					 33 changed files with 1242 additions and 612 deletions
				
			
		| 
						 | 
					@ -636,7 +636,7 @@ message SyncMessage {
 | 
				
			||||||
  message DeleteForMe {
 | 
					  message DeleteForMe {
 | 
				
			||||||
    message ConversationIdentifier {
 | 
					    message ConversationIdentifier {
 | 
				
			||||||
      oneof identifier {
 | 
					      oneof identifier {
 | 
				
			||||||
        string threadAci = 1;
 | 
					        string threadServiceId = 1;
 | 
				
			||||||
        bytes threadGroupId = 2;
 | 
					        bytes threadGroupId = 2;
 | 
				
			||||||
        string threadE164 = 3;
 | 
					        string threadE164 = 3;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
| 
						 | 
					@ -644,7 +644,7 @@ message SyncMessage {
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
    message AddressableMessage {
 | 
					    message AddressableMessage {
 | 
				
			||||||
      oneof author {
 | 
					      oneof author {
 | 
				
			||||||
        string authorAci = 1;
 | 
					        string authorServiceId = 1;
 | 
				
			||||||
        string authorE164 = 2;
 | 
					        string authorE164 = 2;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      optional uint64 sentTimestamp = 3;
 | 
					      optional uint64 sentTimestamp = 3;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										275
									
								
								ts/background.ts
									
										
									
									
									
								
							
							
						
						
									
										275
									
								
								ts/background.ts
									
										
									
									
									
								
							| 
						 | 
					@ -42,7 +42,10 @@ import { isNotNil } from './util/isNotNil';
 | 
				
			||||||
import { isBackupEnabled } from './util/isBackupEnabled';
 | 
					import { isBackupEnabled } from './util/isBackupEnabled';
 | 
				
			||||||
import { setAppLoadingScreenMessage } from './setAppLoadingScreenMessage';
 | 
					import { setAppLoadingScreenMessage } from './setAppLoadingScreenMessage';
 | 
				
			||||||
import { IdleDetector } from './IdleDetector';
 | 
					import { IdleDetector } from './IdleDetector';
 | 
				
			||||||
import { expiringMessagesDeletionService } from './services/expiringMessagesDeletion';
 | 
					import {
 | 
				
			||||||
 | 
					  initialize as initializeExpiringMessageService,
 | 
				
			||||||
 | 
					  update as updateExpiringMessagesService,
 | 
				
			||||||
 | 
					} from './services/expiringMessagesDeletion';
 | 
				
			||||||
import { tapToViewMessagesDeletionService } from './services/tapToViewMessagesDeletionService';
 | 
					import { tapToViewMessagesDeletionService } from './services/tapToViewMessagesDeletionService';
 | 
				
			||||||
import { getStoriesForRedux, loadStories } from './services/storyLoader';
 | 
					import { getStoriesForRedux, loadStories } from './services/storyLoader';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					@ -116,17 +119,12 @@ import * as Edits from './messageModifiers/Edits';
 | 
				
			||||||
import * as MessageReceipts from './messageModifiers/MessageReceipts';
 | 
					import * as MessageReceipts from './messageModifiers/MessageReceipts';
 | 
				
			||||||
import * as MessageRequests from './messageModifiers/MessageRequests';
 | 
					import * as MessageRequests from './messageModifiers/MessageRequests';
 | 
				
			||||||
import * as Reactions from './messageModifiers/Reactions';
 | 
					import * as Reactions from './messageModifiers/Reactions';
 | 
				
			||||||
import * as ReadSyncs from './messageModifiers/ReadSyncs';
 | 
					 | 
				
			||||||
import * as ViewOnceOpenSyncs from './messageModifiers/ViewOnceOpenSyncs';
 | 
					import * as ViewOnceOpenSyncs from './messageModifiers/ViewOnceOpenSyncs';
 | 
				
			||||||
import * as ViewSyncs from './messageModifiers/ViewSyncs';
 | 
					 | 
				
			||||||
import type { DeleteAttributesType } from './messageModifiers/Deletes';
 | 
					import type { DeleteAttributesType } from './messageModifiers/Deletes';
 | 
				
			||||||
import type { EditAttributesType } from './messageModifiers/Edits';
 | 
					import type { EditAttributesType } from './messageModifiers/Edits';
 | 
				
			||||||
import type { MessageReceiptAttributesType } from './messageModifiers/MessageReceipts';
 | 
					 | 
				
			||||||
import type { MessageRequestAttributesType } from './messageModifiers/MessageRequests';
 | 
					import type { MessageRequestAttributesType } from './messageModifiers/MessageRequests';
 | 
				
			||||||
import type { ReactionAttributesType } from './messageModifiers/Reactions';
 | 
					import type { ReactionAttributesType } from './messageModifiers/Reactions';
 | 
				
			||||||
import type { ReadSyncAttributesType } from './messageModifiers/ReadSyncs';
 | 
					 | 
				
			||||||
import type { ViewOnceOpenSyncAttributesType } from './messageModifiers/ViewOnceOpenSyncs';
 | 
					import type { ViewOnceOpenSyncAttributesType } from './messageModifiers/ViewOnceOpenSyncs';
 | 
				
			||||||
import type { ViewSyncAttributesType } from './messageModifiers/ViewSyncs';
 | 
					 | 
				
			||||||
import { ReadStatus } from './messages/MessageReadStatus';
 | 
					import { ReadStatus } from './messages/MessageReadStatus';
 | 
				
			||||||
import type { SendStateByConversationId } from './messages/MessageSendState';
 | 
					import type { SendStateByConversationId } from './messages/MessageSendState';
 | 
				
			||||||
import { SendStatus } from './messages/MessageSendState';
 | 
					import { SendStatus } from './messages/MessageSendState';
 | 
				
			||||||
| 
						 | 
					@ -202,7 +200,11 @@ import { getThemeType } from './util/getThemeType';
 | 
				
			||||||
import { AttachmentDownloadManager } from './jobs/AttachmentDownloadManager';
 | 
					import { AttachmentDownloadManager } from './jobs/AttachmentDownloadManager';
 | 
				
			||||||
import { onCallLinkUpdateSync } from './util/onCallLinkUpdateSync';
 | 
					import { onCallLinkUpdateSync } from './util/onCallLinkUpdateSync';
 | 
				
			||||||
import { CallMode } from './types/Calling';
 | 
					import { CallMode } from './types/Calling';
 | 
				
			||||||
 | 
					import type { SyncTaskType } from './util/syncTasks';
 | 
				
			||||||
import { queueSyncTasks } from './util/syncTasks';
 | 
					import { queueSyncTasks } from './util/syncTasks';
 | 
				
			||||||
 | 
					import type { ViewSyncTaskType } from './messageModifiers/ViewSyncs';
 | 
				
			||||||
 | 
					import type { ReceiptSyncTaskType } from './messageModifiers/MessageReceipts';
 | 
				
			||||||
 | 
					import type { ReadSyncTaskType } from './messageModifiers/ReadSyncs';
 | 
				
			||||||
import { isEnabled } from './RemoteConfig';
 | 
					import { isEnabled } from './RemoteConfig';
 | 
				
			||||||
import { AttachmentBackupManager } from './jobs/AttachmentBackupManager';
 | 
					import { AttachmentBackupManager } from './jobs/AttachmentBackupManager';
 | 
				
			||||||
import { getConversationIdForLogging } from './util/idForLogging';
 | 
					import { getConversationIdForLogging } from './util/idForLogging';
 | 
				
			||||||
| 
						 | 
					@ -1498,10 +1500,12 @@ export async function startApp(): Promise<void> {
 | 
				
			||||||
      window.Whisper.events.trigger('timetravel');
 | 
					      window.Whisper.events.trigger('timetravel');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    void expiringMessagesDeletionService.update();
 | 
					    initializeExpiringMessageService(singleProtoJobQueue);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void updateExpiringMessagesService();
 | 
				
			||||||
    void tapToViewMessagesDeletionService.update();
 | 
					    void tapToViewMessagesDeletionService.update();
 | 
				
			||||||
    window.Whisper.events.on('timetravel', () => {
 | 
					    window.Whisper.events.on('timetravel', () => {
 | 
				
			||||||
      void expiringMessagesDeletionService.update();
 | 
					      void updateExpiringMessagesService();
 | 
				
			||||||
      void tapToViewMessagesDeletionService.update();
 | 
					      void tapToViewMessagesDeletionService.update();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1833,7 +1837,9 @@ export async function startApp(): Promise<void> {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
          // Note: we always have to register our capabilities all at once, so we do this
 | 
					          // Note: we always have to register our capabilities all at once, so we do this
 | 
				
			||||||
          //   after connect on every startup
 | 
					          //   after connect on every startup
 | 
				
			||||||
          await server.registerCapabilities({});
 | 
					          await server.registerCapabilities({
 | 
				
			||||||
 | 
					            deleteSync: true,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
        } catch (error) {
 | 
					        } catch (error) {
 | 
				
			||||||
          log.error(
 | 
					          log.error(
 | 
				
			||||||
            'Error: Unable to register our capabilities.',
 | 
					            'Error: Unable to register our capabilities.',
 | 
				
			||||||
| 
						 | 
					@ -3221,47 +3227,51 @@ export async function startApp(): Promise<void> {
 | 
				
			||||||
    drop(MessageRequests.onResponse(attributes));
 | 
					    drop(MessageRequests.onResponse(attributes));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function onReadReceipt(event: Readonly<ReadEvent>): void {
 | 
					  async function onReadReceipt(event: Readonly<ReadEvent>): Promise<void> {
 | 
				
			||||||
    onReadOrViewReceipt({
 | 
					    return onReadOrViewReceipt({
 | 
				
			||||||
      logTitle: 'read receipt',
 | 
					      logTitle: 'read receipt',
 | 
				
			||||||
      event,
 | 
					      event,
 | 
				
			||||||
      type: MessageReceipts.MessageReceiptType.Read,
 | 
					      type: MessageReceipts.messageReceiptTypeSchema.enum.Read,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function onViewReceipt(event: Readonly<ViewEvent>): void {
 | 
					  async function onViewReceipt(event: Readonly<ViewEvent>): Promise<void> {
 | 
				
			||||||
    onReadOrViewReceipt({
 | 
					    return onReadOrViewReceipt({
 | 
				
			||||||
      logTitle: 'view receipt',
 | 
					      logTitle: 'view receipt',
 | 
				
			||||||
      event,
 | 
					      event,
 | 
				
			||||||
      type: MessageReceipts.MessageReceiptType.View,
 | 
					      type: MessageReceipts.messageReceiptTypeSchema.enum.View,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function onReadOrViewReceipt({
 | 
					  async function onReadOrViewReceipt({
 | 
				
			||||||
    event,
 | 
					    event,
 | 
				
			||||||
    logTitle,
 | 
					    logTitle,
 | 
				
			||||||
    type,
 | 
					    type,
 | 
				
			||||||
  }: Readonly<{
 | 
					  }: Readonly<{
 | 
				
			||||||
    event: ReadEvent | ViewEvent;
 | 
					    event: ReadEvent | ViewEvent;
 | 
				
			||||||
    logTitle: string;
 | 
					    logTitle: string;
 | 
				
			||||||
    type:
 | 
					    type: 'Read' | 'View';
 | 
				
			||||||
      | MessageReceipts.MessageReceiptType.Read
 | 
					  }>): Promise<void> {
 | 
				
			||||||
      | MessageReceipts.MessageReceiptType.View;
 | 
					    const { receipts, envelopeId, envelopeTimestamp, confirm } = event;
 | 
				
			||||||
  }>): void {
 | 
					    const logId = `onReadOrViewReceipt(type=${type}, envelope=${envelopeTimestamp}, envelopeId=${envelopeId})`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const syncTasks = receipts
 | 
				
			||||||
 | 
					      .map((receipt): SyncTaskType | undefined => {
 | 
				
			||||||
        const {
 | 
					        const {
 | 
				
			||||||
      envelopeTimestamp,
 | 
					 | 
				
			||||||
          timestamp,
 | 
					          timestamp,
 | 
				
			||||||
          source,
 | 
					          source,
 | 
				
			||||||
          sourceServiceId,
 | 
					          sourceServiceId,
 | 
				
			||||||
          sourceDevice,
 | 
					          sourceDevice,
 | 
				
			||||||
          wasSentEncrypted,
 | 
					          wasSentEncrypted,
 | 
				
			||||||
    } = event.receipt;
 | 
					        } = receipt;
 | 
				
			||||||
    const sourceConversation = window.ConversationController.lookupOrCreate({
 | 
					        const sourceConversation = window.ConversationController.lookupOrCreate(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
            serviceId: sourceServiceId,
 | 
					            serviceId: sourceServiceId,
 | 
				
			||||||
            e164: source,
 | 
					            e164: source,
 | 
				
			||||||
            reason: `onReadOrViewReceipt(${envelopeTimestamp})`,
 | 
					            reason: `onReadOrViewReceipt(${envelopeTimestamp})`,
 | 
				
			||||||
    });
 | 
					          }
 | 
				
			||||||
    strictAssert(sourceConversation, 'Failed to create conversation');
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        log.info(
 | 
					        log.info(
 | 
				
			||||||
          logTitle,
 | 
					          logTitle,
 | 
				
			||||||
          `${sourceServiceId || source}.${sourceDevice}`,
 | 
					          `${sourceServiceId || source}.${sourceDevice}`,
 | 
				
			||||||
| 
						 | 
					@ -3270,15 +3280,20 @@ export async function startApp(): Promise<void> {
 | 
				
			||||||
          timestamp
 | 
					          timestamp
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    strictAssert(
 | 
					        if (!sourceConversation) {
 | 
				
			||||||
      isServiceIdString(sourceServiceId),
 | 
					          log.error(`${logId}: Failed to create conversation`);
 | 
				
			||||||
      'onReadOrViewReceipt: Missing sourceServiceId'
 | 
					          return undefined;
 | 
				
			||||||
    );
 | 
					        }
 | 
				
			||||||
    strictAssert(sourceDevice, 'onReadOrViewReceipt: Missing sourceDevice');
 | 
					        if (!isServiceIdString(sourceServiceId)) {
 | 
				
			||||||
 | 
					          log.error(`${logId}: Missing sourceServiceId`);
 | 
				
			||||||
 | 
					          return undefined;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!sourceDevice) {
 | 
				
			||||||
 | 
					          log.error(`${logId}: Missing sourceDevice`);
 | 
				
			||||||
 | 
					          return undefined;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const attributes: MessageReceiptAttributesType = {
 | 
					        const data: ReceiptSyncTaskType = {
 | 
				
			||||||
      envelopeId: event.receipt.envelopeId,
 | 
					 | 
				
			||||||
      removeFromMessageReceiverCache: event.confirm,
 | 
					 | 
				
			||||||
          messageSentAt: timestamp,
 | 
					          messageSentAt: timestamp,
 | 
				
			||||||
          receiptTimestamp: envelopeTimestamp,
 | 
					          receiptTimestamp: envelopeTimestamp,
 | 
				
			||||||
          sourceConversationId: sourceConversation.id,
 | 
					          sourceConversationId: sourceConversation.id,
 | 
				
			||||||
| 
						 | 
					@ -3287,11 +3302,38 @@ export async function startApp(): Promise<void> {
 | 
				
			||||||
          type,
 | 
					          type,
 | 
				
			||||||
          wasSentEncrypted,
 | 
					          wasSentEncrypted,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    drop(MessageReceipts.onReceipt(attributes));
 | 
					        return {
 | 
				
			||||||
 | 
					          id: generateUuid(),
 | 
				
			||||||
 | 
					          attempts: 1,
 | 
				
			||||||
 | 
					          createdAt: Date.now(),
 | 
				
			||||||
 | 
					          data,
 | 
				
			||||||
 | 
					          envelopeId,
 | 
				
			||||||
 | 
					          sentAt: envelopeTimestamp,
 | 
				
			||||||
 | 
					          type,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .filter(isNotNil);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log.info(`${logId}: Saving ${syncTasks.length} sync tasks`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await window.Signal.Data.saveSyncTasks(syncTasks);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    confirm();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log.info(`${logId}: Queuing ${syncTasks.length} sync tasks`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await queueSyncTasks(syncTasks, window.Signal.Data.removeSyncTaskById);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log.info(`${logId}: Done`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async function onReadSync(ev: ReadSyncEvent): Promise<void> {
 | 
					  async function onReadSync(ev: ReadSyncEvent): Promise<void> {
 | 
				
			||||||
    const { envelopeTimestamp, sender, senderAci, timestamp } = ev.read;
 | 
					    const { reads, envelopeTimestamp, envelopeId, confirm } = ev;
 | 
				
			||||||
 | 
					    const logId = `onReadSync(envelope=${envelopeTimestamp}, envelopeId=${envelopeId})`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const syncTasks = reads
 | 
				
			||||||
 | 
					      .map((read): SyncTaskType | undefined => {
 | 
				
			||||||
 | 
					        const { sender, senderAci, timestamp } = read;
 | 
				
			||||||
        const readAt = envelopeTimestamp;
 | 
					        const readAt = envelopeTimestamp;
 | 
				
			||||||
        const { conversation: senderConversation } =
 | 
					        const { conversation: senderConversation } =
 | 
				
			||||||
          window.ConversationController.maybeMergeContacts({
 | 
					          window.ConversationController.maybeMergeContacts({
 | 
				
			||||||
| 
						 | 
					@ -3311,25 +3353,60 @@ export async function startApp(): Promise<void> {
 | 
				
			||||||
          timestamp
 | 
					          timestamp
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    strictAssert(senderId, 'onReadSync missing senderId');
 | 
					        if (!senderId) {
 | 
				
			||||||
    strictAssert(senderAci, 'onReadSync missing senderAci');
 | 
					          log.error(`${logId}: missing senderId`);
 | 
				
			||||||
    strictAssert(timestamp, 'onReadSync missing timestamp');
 | 
					          return undefined;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!senderAci) {
 | 
				
			||||||
 | 
					          log.error(`${logId}: missing senderAci`);
 | 
				
			||||||
 | 
					          return undefined;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!timestamp) {
 | 
				
			||||||
 | 
					          log.error(`${logId}: missing timestamp`);
 | 
				
			||||||
 | 
					          return undefined;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const attributes: ReadSyncAttributesType = {
 | 
					        const data: ReadSyncTaskType = {
 | 
				
			||||||
      envelopeId: ev.read.envelopeId,
 | 
					          type: 'ReadSync',
 | 
				
			||||||
      removeFromMessageReceiverCache: ev.confirm,
 | 
					 | 
				
			||||||
          senderId,
 | 
					          senderId,
 | 
				
			||||||
          sender,
 | 
					          sender,
 | 
				
			||||||
          senderAci,
 | 
					          senderAci,
 | 
				
			||||||
          timestamp,
 | 
					          timestamp,
 | 
				
			||||||
          readAt,
 | 
					          readAt,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          id: generateUuid(),
 | 
				
			||||||
 | 
					          attempts: 1,
 | 
				
			||||||
 | 
					          createdAt: Date.now(),
 | 
				
			||||||
 | 
					          data,
 | 
				
			||||||
 | 
					          envelopeId,
 | 
				
			||||||
 | 
					          sentAt: envelopeTimestamp,
 | 
				
			||||||
 | 
					          type: 'ReadSync',
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .filter(isNotNil);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await ReadSyncs.onSync(attributes);
 | 
					    log.info(`${logId}: Saving ${syncTasks.length} sync tasks`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await window.Signal.Data.saveSyncTasks(syncTasks);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    confirm();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log.info(`${logId}: Queuing ${syncTasks.length} sync tasks`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await queueSyncTasks(syncTasks, window.Signal.Data.removeSyncTaskById);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log.info(`${logId}: Done`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async function onViewSync(ev: ViewSyncEvent): Promise<void> {
 | 
					  async function onViewSync(ev: ViewSyncEvent): Promise<void> {
 | 
				
			||||||
    const { envelopeTimestamp, senderE164, senderAci, timestamp } = ev.view;
 | 
					    const { envelopeTimestamp, envelopeId, views, confirm } = ev;
 | 
				
			||||||
 | 
					    const logId = `onViewSync=(envelope=${envelopeTimestamp}, envelopeId=${envelopeId})`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const syncTasks = views
 | 
				
			||||||
 | 
					      .map((view): SyncTaskType | undefined => {
 | 
				
			||||||
 | 
					        const { senderAci, senderE164, timestamp } = view;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const { conversation: senderConversation } =
 | 
					        const { conversation: senderConversation } =
 | 
				
			||||||
          window.ConversationController.maybeMergeContacts({
 | 
					          window.ConversationController.maybeMergeContacts({
 | 
				
			||||||
            e164: senderE164,
 | 
					            e164: senderE164,
 | 
				
			||||||
| 
						 | 
					@ -3348,27 +3425,62 @@ export async function startApp(): Promise<void> {
 | 
				
			||||||
          timestamp
 | 
					          timestamp
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    strictAssert(senderId, 'onViewSync missing senderId');
 | 
					        if (!senderId) {
 | 
				
			||||||
    strictAssert(senderAci, 'onViewSync missing senderAci');
 | 
					          log.error(`${logId}: missing senderId`);
 | 
				
			||||||
    strictAssert(timestamp, 'onViewSync missing timestamp');
 | 
					          return undefined;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!senderAci) {
 | 
				
			||||||
 | 
					          log.error(`${logId}: missing senderAci`);
 | 
				
			||||||
 | 
					          return undefined;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!timestamp) {
 | 
				
			||||||
 | 
					          log.error(`${logId}: missing timestamp`);
 | 
				
			||||||
 | 
					          return undefined;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const attributes: ViewSyncAttributesType = {
 | 
					        const data: ViewSyncTaskType = {
 | 
				
			||||||
      envelopeId: ev.view.envelopeId,
 | 
					          type: 'ViewSync',
 | 
				
			||||||
      removeFromMessageReceiverCache: ev.confirm,
 | 
					 | 
				
			||||||
          senderId,
 | 
					          senderId,
 | 
				
			||||||
          senderE164,
 | 
					          senderE164,
 | 
				
			||||||
          senderAci,
 | 
					          senderAci,
 | 
				
			||||||
          timestamp,
 | 
					          timestamp,
 | 
				
			||||||
          viewedAt: envelopeTimestamp,
 | 
					          viewedAt: envelopeTimestamp,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          id: generateUuid(),
 | 
				
			||||||
 | 
					          attempts: 1,
 | 
				
			||||||
 | 
					          createdAt: Date.now(),
 | 
				
			||||||
 | 
					          data,
 | 
				
			||||||
 | 
					          envelopeId,
 | 
				
			||||||
 | 
					          sentAt: envelopeTimestamp,
 | 
				
			||||||
 | 
					          type: 'ViewSync',
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .filter(isNotNil);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await ViewSyncs.onSync(attributes);
 | 
					    log.info(`${logId}: Saving ${syncTasks.length} sync tasks`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await window.Signal.Data.saveSyncTasks(syncTasks);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    confirm();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log.info(`${logId}: Queuing ${syncTasks.length} sync tasks`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await queueSyncTasks(syncTasks, window.Signal.Data.removeSyncTaskById);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log.info(`${logId}: Done`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function onDeliveryReceipt(ev: DeliveryEvent): void {
 | 
					  async function onDeliveryReceipt(ev: DeliveryEvent): Promise<void> {
 | 
				
			||||||
    const { deliveryReceipt } = ev;
 | 
					    const { deliveryReceipts, envelopeId, envelopeTimestamp, confirm } = ev;
 | 
				
			||||||
 | 
					    const logId = `onDeliveryReceipt(envelope=${envelopeTimestamp}, envelopeId=${envelopeId})`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    strictAssert(envelopeTimestamp, `${logId}: missing envelopeTimestamp`);
 | 
				
			||||||
 | 
					    strictAssert(envelopeTimestamp, `${logId}: missing envelopeId`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const syncTasks = deliveryReceipts
 | 
				
			||||||
 | 
					      .map((deliveryReceipt): SyncTaskType | undefined => {
 | 
				
			||||||
        const {
 | 
					        const {
 | 
				
			||||||
      envelopeTimestamp,
 | 
					 | 
				
			||||||
          sourceServiceId,
 | 
					          sourceServiceId,
 | 
				
			||||||
          source,
 | 
					          source,
 | 
				
			||||||
          sourceDevice,
 | 
					          sourceDevice,
 | 
				
			||||||
| 
						 | 
					@ -3376,11 +3488,13 @@ export async function startApp(): Promise<void> {
 | 
				
			||||||
          wasSentEncrypted,
 | 
					          wasSentEncrypted,
 | 
				
			||||||
        } = deliveryReceipt;
 | 
					        } = deliveryReceipt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const sourceConversation = window.ConversationController.lookupOrCreate({
 | 
					        const sourceConversation = window.ConversationController.lookupOrCreate(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
            serviceId: sourceServiceId,
 | 
					            serviceId: sourceServiceId,
 | 
				
			||||||
            e164: source,
 | 
					            e164: source,
 | 
				
			||||||
            reason: `onDeliveryReceipt(${envelopeTimestamp})`,
 | 
					            reason: `onDeliveryReceipt(${envelopeTimestamp})`,
 | 
				
			||||||
    });
 | 
					          }
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        log.info(
 | 
					        log.info(
 | 
				
			||||||
          'delivery receipt from',
 | 
					          'delivery receipt from',
 | 
				
			||||||
| 
						 | 
					@ -3391,30 +3505,51 @@ export async function startApp(): Promise<void> {
 | 
				
			||||||
          `wasSentEncrypted=${wasSentEncrypted}`
 | 
					          `wasSentEncrypted=${wasSentEncrypted}`
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    strictAssert(
 | 
					        if (!isServiceIdString(sourceServiceId)) {
 | 
				
			||||||
      envelopeTimestamp,
 | 
					          log.error(`${logId}: missing valid sourceServiceId`);
 | 
				
			||||||
      'onDeliveryReceipt: missing envelopeTimestamp'
 | 
					          return undefined;
 | 
				
			||||||
    );
 | 
					        }
 | 
				
			||||||
    strictAssert(
 | 
					        if (!sourceDevice) {
 | 
				
			||||||
      isServiceIdString(sourceServiceId),
 | 
					          log.error(`${logId}: missing sourceDevice`);
 | 
				
			||||||
      'onDeliveryReceipt: missing valid sourceServiceId'
 | 
					          return undefined;
 | 
				
			||||||
    );
 | 
					        }
 | 
				
			||||||
    strictAssert(sourceDevice, 'onDeliveryReceipt: missing sourceDevice');
 | 
					        if (!sourceConversation) {
 | 
				
			||||||
    strictAssert(sourceConversation, 'onDeliveryReceipt: missing conversation');
 | 
					          log.error(`${logId}: missing conversation`);
 | 
				
			||||||
 | 
					          return undefined;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const attributes: MessageReceiptAttributesType = {
 | 
					        const data: ReceiptSyncTaskType = {
 | 
				
			||||||
      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: MessageReceipts.MessageReceiptType.Delivery,
 | 
					          type: MessageReceipts.messageReceiptTypeSchema.enum.Delivery,
 | 
				
			||||||
          wasSentEncrypted,
 | 
					          wasSentEncrypted,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          id: generateUuid(),
 | 
				
			||||||
 | 
					          attempts: 1,
 | 
				
			||||||
 | 
					          createdAt: Date.now(),
 | 
				
			||||||
 | 
					          data,
 | 
				
			||||||
 | 
					          envelopeId,
 | 
				
			||||||
 | 
					          sentAt: envelopeTimestamp,
 | 
				
			||||||
 | 
					          type: 'Delivery',
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .filter(isNotNil);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    drop(MessageReceipts.onReceipt(attributes));
 | 
					    log.info(`${logId}: Saving ${syncTasks.length} sync tasks`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await window.Signal.Data.saveSyncTasks(syncTasks);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    confirm();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log.info(`${logId}: Queuing ${syncTasks.length} sync tasks`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await queueSyncTasks(syncTasks, window.Signal.Data.removeSyncTaskById);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log.info(`${logId}: Done`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async function onDeleteForMeSync(ev: DeleteForMeSyncEvent) {
 | 
					  async function onDeleteForMeSync(ev: DeleteForMeSyncEvent) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { z } from 'zod';
 | 
				
			||||||
import { groupBy } from 'lodash';
 | 
					import { groupBy } from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { MessageModel } from '../models/messages';
 | 
					import type { MessageModel } from '../models/messages';
 | 
				
			||||||
| 
						 | 
					@ -10,7 +11,7 @@ import { isOutgoing, isStory } from '../state/selectors/message';
 | 
				
			||||||
import { getOwn } from '../util/getOwn';
 | 
					import { getOwn } from '../util/getOwn';
 | 
				
			||||||
import { missingCaseError } from '../util/missingCaseError';
 | 
					import { missingCaseError } from '../util/missingCaseError';
 | 
				
			||||||
import { createWaitBatcher } from '../util/waitBatcher';
 | 
					import { createWaitBatcher } from '../util/waitBatcher';
 | 
				
			||||||
import type { ServiceIdString } from '../types/ServiceId';
 | 
					import { isServiceIdString } from '../types/ServiceId';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  SendActionType,
 | 
					  SendActionType,
 | 
				
			||||||
  SendStatus,
 | 
					  SendStatus,
 | 
				
			||||||
| 
						 | 
					@ -23,7 +24,6 @@ import * as log from '../logging/log';
 | 
				
			||||||
import { getSourceServiceId } from '../messages/helpers';
 | 
					import { getSourceServiceId } from '../messages/helpers';
 | 
				
			||||||
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
 | 
					import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
 | 
				
			||||||
import { getMessageIdForLogging } from '../util/idForLogging';
 | 
					import { getMessageIdForLogging } from '../util/idForLogging';
 | 
				
			||||||
import { generateCacheKey } from './generateCacheKey';
 | 
					 | 
				
			||||||
import { getPropForTimestamp } from '../util/editHelpers';
 | 
					import { getPropForTimestamp } from '../util/editHelpers';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  DELETE_SENT_PROTO_BATCHER_WAIT_MS,
 | 
					  DELETE_SENT_PROTO_BATCHER_WAIT_MS,
 | 
				
			||||||
| 
						 | 
					@ -31,34 +31,30 @@ import {
 | 
				
			||||||
} from '../types/Receipt';
 | 
					} from '../types/Receipt';
 | 
				
			||||||
import { drop } from '../util/drop';
 | 
					import { drop } from '../util/drop';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const { deleteSentProtoRecipient } = dataInterface;
 | 
					const { deleteSentProtoRecipient, removeSyncTaskById } = dataInterface;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum MessageReceiptType {
 | 
					export const messageReceiptTypeSchema = z.enum(['Delivery', 'Read', 'View']);
 | 
				
			||||||
  Delivery = 'Delivery',
 | 
					
 | 
				
			||||||
  Read = 'Read',
 | 
					export type MessageReceiptType = z.infer<typeof messageReceiptTypeSchema>;
 | 
				
			||||||
  View = 'View',
 | 
					
 | 
				
			||||||
}
 | 
					export const receiptSyncTaskSchema = z.object({
 | 
				
			||||||
 | 
					  messageSentAt: z.number(),
 | 
				
			||||||
 | 
					  receiptTimestamp: z.number(),
 | 
				
			||||||
 | 
					  sourceConversationId: z.string(),
 | 
				
			||||||
 | 
					  sourceDevice: z.number(),
 | 
				
			||||||
 | 
					  sourceServiceId: z.string().refine(isServiceIdString),
 | 
				
			||||||
 | 
					  type: messageReceiptTypeSchema,
 | 
				
			||||||
 | 
					  wasSentEncrypted: z.boolean(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ReceiptSyncTaskType = z.infer<typeof receiptSyncTaskSchema>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type MessageReceiptAttributesType = {
 | 
					export type MessageReceiptAttributesType = {
 | 
				
			||||||
  envelopeId: string;
 | 
					  envelopeId: string;
 | 
				
			||||||
  messageSentAt: number;
 | 
					  syncTaskId: string;
 | 
				
			||||||
  receiptTimestamp: number;
 | 
					  receiptSync: ReceiptSyncTaskType;
 | 
				
			||||||
  removeFromMessageReceiverCache: () => void;
 | 
					 | 
				
			||||||
  sourceConversationId: string;
 | 
					 | 
				
			||||||
  sourceDevice: number;
 | 
					 | 
				
			||||||
  sourceServiceId: ServiceIdString;
 | 
					 | 
				
			||||||
  type: MessageReceiptType;
 | 
					 | 
				
			||||||
  wasSentEncrypted: boolean;
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getReceiptCacheKey(receipt: MessageReceiptAttributesType): string {
 | 
					 | 
				
			||||||
  return generateCacheKey({
 | 
					 | 
				
			||||||
    sender: receipt.sourceServiceId,
 | 
					 | 
				
			||||||
    timestamp: receipt.messageSentAt,
 | 
					 | 
				
			||||||
    type: receipt.type,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const cachedReceipts = new Map<string, MessageReceiptAttributesType>();
 | 
					const cachedReceipts = new Map<string, MessageReceiptAttributesType>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const processReceiptBatcher = createWaitBatcher({
 | 
					const processReceiptBatcher = createWaitBatcher({
 | 
				
			||||||
| 
						 | 
					@ -69,7 +65,7 @@ const processReceiptBatcher = createWaitBatcher({
 | 
				
			||||||
    // First group by sentAt, so that we can find the target message
 | 
					    // First group by sentAt, so that we can find the target message
 | 
				
			||||||
    const receiptsByMessageSentAt = groupBy(
 | 
					    const receiptsByMessageSentAt = groupBy(
 | 
				
			||||||
      receipts,
 | 
					      receipts,
 | 
				
			||||||
      receipt => receipt.messageSentAt
 | 
					      receipt => receipt.receiptSync.messageSentAt
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Once we find the message, we'll group them by messageId to process
 | 
					    // Once we find the message, we'll group them by messageId to process
 | 
				
			||||||
| 
						 | 
					@ -99,7 +95,7 @@ const processReceiptBatcher = createWaitBatcher({
 | 
				
			||||||
        continue;
 | 
					        continue;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      // All receipts have the same sentAt, so we can grab it from the first
 | 
					      // All receipts have the same sentAt, so we can grab it from the first
 | 
				
			||||||
      const sentAt = receiptsForMessageSentAt[0].messageSentAt;
 | 
					      const sentAt = receiptsForMessageSentAt[0].receiptSync.messageSentAt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const messagesMatchingTimestamp =
 | 
					      const messagesMatchingTimestamp =
 | 
				
			||||||
        // eslint-disable-next-line no-await-in-loop
 | 
					        // eslint-disable-next-line no-await-in-loop
 | 
				
			||||||
| 
						 | 
					@ -114,14 +110,16 @@ const processReceiptBatcher = createWaitBatcher({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (reaction) {
 | 
					        if (reaction) {
 | 
				
			||||||
          for (const receipt of receiptsForMessageSentAt) {
 | 
					          for (const receipt of receiptsForMessageSentAt) {
 | 
				
			||||||
 | 
					            const { receiptSync } = receipt;
 | 
				
			||||||
            log.info(
 | 
					            log.info(
 | 
				
			||||||
              'MesageReceipts.processReceiptBatcher: Got receipt for reaction',
 | 
					              'MesageReceipts.processReceiptBatcher: Got receipt for reaction',
 | 
				
			||||||
              receipt.messageSentAt,
 | 
					              receiptSync.messageSentAt,
 | 
				
			||||||
              receipt.type,
 | 
					              receiptSync.type,
 | 
				
			||||||
              receipt.sourceConversationId,
 | 
					              receiptSync.sourceConversationId,
 | 
				
			||||||
              receipt.sourceServiceId
 | 
					              receiptSync.sourceServiceId
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
            remove(receipt);
 | 
					            // eslint-disable-next-line no-await-in-loop
 | 
				
			||||||
 | 
					            await remove(receipt);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          continue;
 | 
					          continue;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -129,7 +127,7 @@ const processReceiptBatcher = createWaitBatcher({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      for (const receipt of receiptsForMessageSentAt) {
 | 
					      for (const receipt of receiptsForMessageSentAt) {
 | 
				
			||||||
        const targetMessage = getTargetMessage({
 | 
					        const targetMessage = getTargetMessage({
 | 
				
			||||||
          sourceConversationId: receipt.sourceConversationId,
 | 
					          sourceConversationId: receipt.receiptSync.sourceConversationId,
 | 
				
			||||||
          targetTimestamp: sentAt,
 | 
					          targetTimestamp: sentAt,
 | 
				
			||||||
          messagesMatchingTimestamp,
 | 
					          messagesMatchingTimestamp,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
| 
						 | 
					@ -144,7 +142,9 @@ const processReceiptBatcher = createWaitBatcher({
 | 
				
			||||||
              item.sendStateByConversationId &&
 | 
					              item.sendStateByConversationId &&
 | 
				
			||||||
              !item.deletedForEveryone &&
 | 
					              !item.deletedForEveryone &&
 | 
				
			||||||
              Boolean(
 | 
					              Boolean(
 | 
				
			||||||
                item.sendStateByConversationId[receipt.sourceConversationId]
 | 
					                item.sendStateByConversationId[
 | 
				
			||||||
 | 
					                  receipt.receiptSync.sourceConversationId
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
              )
 | 
					              )
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -154,12 +154,13 @@ const processReceiptBatcher = createWaitBatcher({
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            // Nope, no target message was found
 | 
					            // Nope, no target message was found
 | 
				
			||||||
 | 
					            const { receiptSync } = receipt;
 | 
				
			||||||
            log.info(
 | 
					            log.info(
 | 
				
			||||||
              'MessageReceipts.processReceiptBatcher: No message for receipt',
 | 
					              'MessageReceipts.processReceiptBatcher: No message for receipt',
 | 
				
			||||||
              receipt.messageSentAt,
 | 
					              receiptSync.messageSentAt,
 | 
				
			||||||
              receipt.type,
 | 
					              receiptSync.type,
 | 
				
			||||||
              receipt.sourceConversationId,
 | 
					              receiptSync.sourceConversationId,
 | 
				
			||||||
              receipt.sourceServiceId
 | 
					              receiptSync.sourceServiceId
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -190,7 +191,7 @@ async function processReceiptsForMessage(
 | 
				
			||||||
    messageId
 | 
					    messageId
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { updatedMessage, validReceipts } = updateMessageWithReceipts(
 | 
					  const { updatedMessage, validReceipts } = await updateMessageWithReceipts(
 | 
				
			||||||
    message,
 | 
					    message,
 | 
				
			||||||
    receipts
 | 
					    receipts
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
| 
						 | 
					@ -204,7 +205,8 @@ async function processReceiptsForMessage(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Confirm/remove receipts, and delete sent protos
 | 
					  // Confirm/remove receipts, and delete sent protos
 | 
				
			||||||
  for (const receipt of validReceipts) {
 | 
					  for (const receipt of validReceipts) {
 | 
				
			||||||
    remove(receipt);
 | 
					    // eslint-disable-next-line no-await-in-loop
 | 
				
			||||||
 | 
					    await remove(receipt);
 | 
				
			||||||
    drop(addToDeleteSentProtoBatcher(receipt, updatedMessage));
 | 
					    drop(addToDeleteSentProtoBatcher(receipt, updatedMessage));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -215,25 +217,27 @@ async function processReceiptsForMessage(
 | 
				
			||||||
  conversation?.debouncedUpdateLastMessage?.();
 | 
					  conversation?.debouncedUpdateLastMessage?.();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function updateMessageWithReceipts(
 | 
					async function updateMessageWithReceipts(
 | 
				
			||||||
  message: MessageAttributesType,
 | 
					  message: MessageAttributesType,
 | 
				
			||||||
  receipts: Array<MessageReceiptAttributesType>
 | 
					  receipts: Array<MessageReceiptAttributesType>
 | 
				
			||||||
): {
 | 
					): Promise<{
 | 
				
			||||||
  updatedMessage: MessageAttributesType;
 | 
					  updatedMessage: MessageAttributesType;
 | 
				
			||||||
  validReceipts: Array<MessageReceiptAttributesType>;
 | 
					  validReceipts: Array<MessageReceiptAttributesType>;
 | 
				
			||||||
} {
 | 
					}> {
 | 
				
			||||||
  const logId = `updateMessageWithReceipts(timestamp=${message.timestamp})`;
 | 
					  const logId = `updateMessageWithReceipts(timestamp=${message.timestamp})`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const toRemove: Array<MessageReceiptAttributesType> = [];
 | 
				
			||||||
  const receiptsToProcess = receipts.filter(receipt => {
 | 
					  const receiptsToProcess = receipts.filter(receipt => {
 | 
				
			||||||
    if (shouldDropReceipt(receipt, message)) {
 | 
					    if (shouldDropReceipt(receipt, message)) {
 | 
				
			||||||
 | 
					      const { receiptSync } = receipt;
 | 
				
			||||||
      log.info(
 | 
					      log.info(
 | 
				
			||||||
        `${logId}: Dropping a receipt ${receipt.type} for sentAt=${receipt.messageSentAt}`
 | 
					        `${logId}: Dropping a receipt ${receiptSync.type} for sentAt=${receiptSync.messageSentAt}`
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      remove(receipt);
 | 
					      toRemove.push(receipt);
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!cachedReceipts.has(getReceiptCacheKey(receipt))) {
 | 
					    if (!cachedReceipts.has(receipt.syncTaskId)) {
 | 
				
			||||||
      // Between the time it was received and now, this receipt has already been handled!
 | 
					      // Between the time it was received and now, this receipt has already been handled!
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -241,6 +245,8 @@ function updateMessageWithReceipts(
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await Promise.all(toRemove.map(remove));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  log.info(
 | 
					  log.info(
 | 
				
			||||||
    `${logId}: batch processing ${receipts.length}` +
 | 
					    `${logId}: batch processing ${receipts.length}` +
 | 
				
			||||||
      ` receipt${receipts.length === 1 ? '' : 's'}`
 | 
					      ` receipt${receipts.length === 1 ? '' : 's'}`
 | 
				
			||||||
| 
						 | 
					@ -287,9 +293,10 @@ const deleteSentProtoBatcher = createWaitBatcher({
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function remove(receipt: MessageReceiptAttributesType): void {
 | 
					async function remove(receipt: MessageReceiptAttributesType): Promise<void> {
 | 
				
			||||||
  cachedReceipts.delete(getReceiptCacheKey(receipt));
 | 
					  const { syncTaskId } = receipt;
 | 
				
			||||||
  receipt.removeFromMessageReceiverCache();
 | 
					  cachedReceipts.delete(syncTaskId);
 | 
				
			||||||
 | 
					  await removeSyncTaskById(syncTaskId);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getTargetMessage({
 | 
					function getTargetMessage({
 | 
				
			||||||
| 
						 | 
					@ -372,13 +379,13 @@ const shouldDropReceipt = (
 | 
				
			||||||
  receipt: MessageReceiptAttributesType,
 | 
					  receipt: MessageReceiptAttributesType,
 | 
				
			||||||
  message: MessageAttributesType
 | 
					  message: MessageAttributesType
 | 
				
			||||||
): boolean => {
 | 
					): boolean => {
 | 
				
			||||||
  const { type } = receipt;
 | 
					  const { type } = receipt.receiptSync;
 | 
				
			||||||
  switch (type) {
 | 
					  switch (type) {
 | 
				
			||||||
    case MessageReceiptType.Delivery:
 | 
					    case messageReceiptTypeSchema.Enum.Delivery:
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    case MessageReceiptType.Read:
 | 
					    case messageReceiptTypeSchema.Enum.Read:
 | 
				
			||||||
      return !window.storage.get('read-receipt-setting');
 | 
					      return !window.storage.get('read-receipt-setting');
 | 
				
			||||||
    case MessageReceiptType.View:
 | 
					    case messageReceiptTypeSchema.Enum.View:
 | 
				
			||||||
      if (isStory(message)) {
 | 
					      if (isStory(message)) {
 | 
				
			||||||
        return !window.Events.getStoryViewReceiptsEnabled();
 | 
					        return !window.Events.getStoryViewReceiptsEnabled();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
| 
						 | 
					@ -388,9 +395,9 @@ const shouldDropReceipt = (
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function forMessage(
 | 
					export async function forMessage(
 | 
				
			||||||
  message: MessageModel
 | 
					  message: MessageModel
 | 
				
			||||||
): Array<MessageReceiptAttributesType> {
 | 
					): Promise<Array<MessageReceiptAttributesType>> {
 | 
				
			||||||
  if (!isOutgoing(message.attributes) && !isStory(message.attributes)) {
 | 
					  if (!isOutgoing(message.attributes) && !isStory(message.attributes)) {
 | 
				
			||||||
    return [];
 | 
					    return [];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -408,20 +415,23 @@ export function forMessage(
 | 
				
			||||||
  const receiptValues = Array.from(cachedReceipts.values());
 | 
					  const receiptValues = Array.from(cachedReceipts.values());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const sentAt = getMessageSentTimestamp(message.attributes, { log });
 | 
					  const sentAt = getMessageSentTimestamp(message.attributes, { log });
 | 
				
			||||||
  const result = receiptValues.filter(item => item.messageSentAt === sentAt);
 | 
					  const result = receiptValues.filter(
 | 
				
			||||||
 | 
					    item => item.receiptSync.messageSentAt === sentAt
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
  if (result.length > 0) {
 | 
					  if (result.length > 0) {
 | 
				
			||||||
    log.info(`${logId}: found early receipts for message ${sentAt}`);
 | 
					    log.info(`${logId}: found early receipts for message ${sentAt}`);
 | 
				
			||||||
    result.forEach(receipt => {
 | 
					    await Promise.all(
 | 
				
			||||||
      remove(receipt);
 | 
					      result.map(async receipt => {
 | 
				
			||||||
    });
 | 
					        await remove(receipt);
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return result.filter(receipt => {
 | 
					  return result.filter(receipt => {
 | 
				
			||||||
    if (shouldDropReceipt(receipt, message.attributes)) {
 | 
					    if (shouldDropReceipt(receipt, message.attributes)) {
 | 
				
			||||||
      log.info(
 | 
					      log.info(
 | 
				
			||||||
        `${logId}: Dropping an early receipt ${receipt.type} for message ${sentAt}`
 | 
					        `${logId}: Dropping an early receipt ${receipt.receiptSync.type} for message ${sentAt}`
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      remove(receipt);
 | 
					 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -433,7 +443,7 @@ function getNewSendStateByConversationId(
 | 
				
			||||||
  oldSendStateByConversationId: SendStateByConversationId,
 | 
					  oldSendStateByConversationId: SendStateByConversationId,
 | 
				
			||||||
  receipt: MessageReceiptAttributesType
 | 
					  receipt: MessageReceiptAttributesType
 | 
				
			||||||
): SendStateByConversationId {
 | 
					): SendStateByConversationId {
 | 
				
			||||||
  const { receiptTimestamp, sourceConversationId, type } = receipt;
 | 
					  const { receiptTimestamp, sourceConversationId, type } = receipt.receiptSync;
 | 
				
			||||||
  const oldSendState = getOwn(
 | 
					  const oldSendState = getOwn(
 | 
				
			||||||
    oldSendStateByConversationId,
 | 
					    oldSendStateByConversationId,
 | 
				
			||||||
    sourceConversationId
 | 
					    sourceConversationId
 | 
				
			||||||
| 
						 | 
					@ -441,13 +451,13 @@ function getNewSendStateByConversationId(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let sendActionType: SendActionType;
 | 
					  let sendActionType: SendActionType;
 | 
				
			||||||
  switch (type) {
 | 
					  switch (type) {
 | 
				
			||||||
    case MessageReceiptType.Delivery:
 | 
					    case messageReceiptTypeSchema.enum.Delivery:
 | 
				
			||||||
      sendActionType = SendActionType.GotDeliveryReceipt;
 | 
					      sendActionType = SendActionType.GotDeliveryReceipt;
 | 
				
			||||||
      break;
 | 
					      break;
 | 
				
			||||||
    case MessageReceiptType.Read:
 | 
					    case messageReceiptTypeSchema.enum.Read:
 | 
				
			||||||
      sendActionType = SendActionType.GotReadReceipt;
 | 
					      sendActionType = SendActionType.GotReadReceipt;
 | 
				
			||||||
      break;
 | 
					      break;
 | 
				
			||||||
    case MessageReceiptType.View:
 | 
					    case messageReceiptTypeSchema.enum.View:
 | 
				
			||||||
      sendActionType = SendActionType.GotViewedReceipt;
 | 
					      sendActionType = SendActionType.GotViewedReceipt;
 | 
				
			||||||
      break;
 | 
					      break;
 | 
				
			||||||
    default:
 | 
					    default:
 | 
				
			||||||
| 
						 | 
					@ -467,7 +477,7 @@ function updateMessageSendStateWithReceipt(
 | 
				
			||||||
  message: MessageAttributesType,
 | 
					  message: MessageAttributesType,
 | 
				
			||||||
  receipt: MessageReceiptAttributesType
 | 
					  receipt: MessageReceiptAttributesType
 | 
				
			||||||
): Partial<MessageAttributesType> {
 | 
					): Partial<MessageAttributesType> {
 | 
				
			||||||
  const { messageSentAt } = receipt;
 | 
					  const { messageSentAt } = receipt.receiptSync;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const newAttributes: Partial<MessageAttributesType> = {};
 | 
					  const newAttributes: Partial<MessageAttributesType> = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -510,27 +520,34 @@ async function addToDeleteSentProtoBatcher(
 | 
				
			||||||
  receipt: MessageReceiptAttributesType,
 | 
					  receipt: MessageReceiptAttributesType,
 | 
				
			||||||
  message: MessageAttributesType
 | 
					  message: MessageAttributesType
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  const { sourceConversationId, type } = receipt;
 | 
					  const { receiptSync } = receipt;
 | 
				
			||||||
 | 
					  const {
 | 
				
			||||||
 | 
					    sourceConversationId,
 | 
				
			||||||
 | 
					    type,
 | 
				
			||||||
 | 
					    wasSentEncrypted,
 | 
				
			||||||
 | 
					    messageSentAt,
 | 
				
			||||||
 | 
					    sourceDevice,
 | 
				
			||||||
 | 
					  } = receiptSync;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (
 | 
					  if (
 | 
				
			||||||
    (type === MessageReceiptType.Delivery &&
 | 
					    (type === messageReceiptTypeSchema.enum.Delivery &&
 | 
				
			||||||
      wasDeliveredWithSealedSender(sourceConversationId, message) &&
 | 
					      wasDeliveredWithSealedSender(sourceConversationId, message) &&
 | 
				
			||||||
      receipt.wasSentEncrypted) ||
 | 
					      wasSentEncrypted) ||
 | 
				
			||||||
    type === MessageReceiptType.Read
 | 
					    type === messageReceiptTypeSchema.enum.Read
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    const recipient = window.ConversationController.get(sourceConversationId);
 | 
					    const recipient = window.ConversationController.get(sourceConversationId);
 | 
				
			||||||
    const recipientServiceId = recipient?.getServiceId();
 | 
					    const recipientServiceId = recipient?.getServiceId();
 | 
				
			||||||
    const deviceId = receipt.sourceDevice;
 | 
					    const deviceId = sourceDevice;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (recipientServiceId && deviceId) {
 | 
					    if (recipientServiceId && deviceId) {
 | 
				
			||||||
      await deleteSentProtoBatcher.add({
 | 
					      await deleteSentProtoBatcher.add({
 | 
				
			||||||
        timestamp: receipt.messageSentAt,
 | 
					        timestamp: messageSentAt,
 | 
				
			||||||
        recipientServiceId,
 | 
					        recipientServiceId,
 | 
				
			||||||
        deviceId,
 | 
					        deviceId,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      log.warn(
 | 
					      log.warn(
 | 
				
			||||||
        `MessageReceipts.deleteSentProto(sentAt=${receipt.messageSentAt}): ` +
 | 
					        `MessageReceipts.deleteSentProto(sentAt=${messageSentAt}): ` +
 | 
				
			||||||
          `Missing serviceId or deviceId for deliveredTo ${sourceConversationId}`
 | 
					          `Missing serviceId or deviceId for deliveredTo ${sourceConversationId}`
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -540,6 +557,6 @@ async function addToDeleteSentProtoBatcher(
 | 
				
			||||||
export async function onReceipt(
 | 
					export async function onReceipt(
 | 
				
			||||||
  receipt: MessageReceiptAttributesType
 | 
					  receipt: MessageReceiptAttributesType
 | 
				
			||||||
): Promise<void> {
 | 
					): Promise<void> {
 | 
				
			||||||
  cachedReceipts.set(getReceiptCacheKey(receipt), receipt);
 | 
					  cachedReceipts.set(receipt.syncTaskId, receipt);
 | 
				
			||||||
  await processReceiptBatcher.add(receipt);
 | 
					  await processReceiptBatcher.add(receipt);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,8 @@
 | 
				
			||||||
// Copyright 2017 Signal Messenger, LLC
 | 
					// Copyright 2017 Signal Messenger, LLC
 | 
				
			||||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
					// SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { AciString } from '../types/ServiceId';
 | 
					import { z } from 'zod';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { MessageModel } from '../models/messages';
 | 
					import type { MessageModel } from '../models/messages';
 | 
				
			||||||
import * as Errors from '../types/errors';
 | 
					import * as Errors from '../types/errors';
 | 
				
			||||||
import * as log from '../logging/log';
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
| 
						 | 
					@ -14,58 +15,69 @@ import { isMessageUnread } from '../util/isMessageUnread';
 | 
				
			||||||
import { notificationService } from '../services/notifications';
 | 
					import { notificationService } from '../services/notifications';
 | 
				
			||||||
import { queueUpdateMessage } from '../util/messageBatcher';
 | 
					import { queueUpdateMessage } from '../util/messageBatcher';
 | 
				
			||||||
import { strictAssert } from '../util/assert';
 | 
					import { strictAssert } from '../util/assert';
 | 
				
			||||||
import { generateCacheKey } from './generateCacheKey';
 | 
					import { isAciString } from '../util/isAciString';
 | 
				
			||||||
 | 
					import dataInterface from '../sql/Client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { removeSyncTaskById } = dataInterface;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const readSyncTaskSchema = z.object({
 | 
				
			||||||
 | 
					  type: z.literal('ReadSync').readonly(),
 | 
				
			||||||
 | 
					  readAt: z.number(),
 | 
				
			||||||
 | 
					  sender: z.string().optional(),
 | 
				
			||||||
 | 
					  senderAci: z.string().refine(isAciString),
 | 
				
			||||||
 | 
					  senderId: z.string(),
 | 
				
			||||||
 | 
					  timestamp: z.number(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ReadSyncTaskType = z.infer<typeof readSyncTaskSchema>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ReadSyncAttributesType = {
 | 
					export type ReadSyncAttributesType = {
 | 
				
			||||||
  envelopeId: string;
 | 
					  envelopeId: string;
 | 
				
			||||||
  readAt: number;
 | 
					  syncTaskId: string;
 | 
				
			||||||
  removeFromMessageReceiverCache: () => unknown;
 | 
					  readSync: ReadSyncTaskType;
 | 
				
			||||||
  sender?: string;
 | 
					 | 
				
			||||||
  senderAci: AciString;
 | 
					 | 
				
			||||||
  senderId: string;
 | 
					 | 
				
			||||||
  timestamp: number;
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const readSyncs = new Map<string, ReadSyncAttributesType>();
 | 
					const readSyncs = new Map<string, ReadSyncAttributesType>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function remove(sync: ReadSyncAttributesType): void {
 | 
					async function remove(sync: ReadSyncAttributesType): Promise<void> {
 | 
				
			||||||
  readSyncs.delete(
 | 
					  const { syncTaskId } = sync;
 | 
				
			||||||
    generateCacheKey({
 | 
					  readSyncs.delete(syncTaskId);
 | 
				
			||||||
      sender: sync.senderId,
 | 
					  await removeSyncTaskById(syncTaskId);
 | 
				
			||||||
      timestamp: sync.timestamp,
 | 
					 | 
				
			||||||
      type: 'readsync',
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  sync.removeFromMessageReceiverCache();
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function maybeItIsAReactionReadSync(
 | 
					async function maybeItIsAReactionReadSync(
 | 
				
			||||||
  sync: ReadSyncAttributesType
 | 
					  sync: ReadSyncAttributesType
 | 
				
			||||||
): Promise<void> {
 | 
					): Promise<void> {
 | 
				
			||||||
  const logId = `ReadSyncs.onSync(timestamp=${sync.timestamp})`;
 | 
					  const { readSync } = sync;
 | 
				
			||||||
 | 
					  const logId = `ReadSyncs.onSync(timestamp=${readSync.timestamp})`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const readReaction = await window.Signal.Data.markReactionAsRead(
 | 
					  const readReaction = await window.Signal.Data.markReactionAsRead(
 | 
				
			||||||
    sync.senderAci,
 | 
					    readSync.senderAci,
 | 
				
			||||||
    Number(sync.timestamp)
 | 
					    Number(readSync.timestamp)
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (
 | 
					  if (
 | 
				
			||||||
    !readReaction ||
 | 
					    !readReaction ||
 | 
				
			||||||
    readReaction?.targetAuthorAci !== window.storage.user.getCheckedAci()
 | 
					    readReaction?.targetAuthorAci !== window.storage.user.getCheckedAci()
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    log.info(`${logId} not found:`, sync.senderId, sync.sender, sync.senderAci);
 | 
					    log.info(
 | 
				
			||||||
 | 
					      `${logId} not found:`,
 | 
				
			||||||
 | 
					      readSync.senderId,
 | 
				
			||||||
 | 
					      readSync.sender,
 | 
				
			||||||
 | 
					      readSync.senderAci
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  log.info(
 | 
					  log.info(
 | 
				
			||||||
    `${logId} read reaction sync found:`,
 | 
					    `${logId} read reaction sync found:`,
 | 
				
			||||||
    readReaction.conversationId,
 | 
					    readReaction.conversationId,
 | 
				
			||||||
    sync.senderId,
 | 
					    readSync.senderId,
 | 
				
			||||||
    sync.sender,
 | 
					    readSync.sender,
 | 
				
			||||||
    sync.senderAci
 | 
					    readSync.senderAci
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  remove(sync);
 | 
					  await remove(sync);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  notificationService.removeBy({
 | 
					  notificationService.removeBy({
 | 
				
			||||||
    conversationId: readReaction.conversationId,
 | 
					    conversationId: readReaction.conversationId,
 | 
				
			||||||
| 
						 | 
					@ -75,9 +87,9 @@ async function maybeItIsAReactionReadSync(
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function forMessage(
 | 
					export async function forMessage(
 | 
				
			||||||
  message: MessageModel
 | 
					  message: MessageModel
 | 
				
			||||||
): ReadSyncAttributesType | null {
 | 
					): Promise<ReadSyncAttributesType | null> {
 | 
				
			||||||
  const logId = `ReadSyncs.forMessage(${getMessageIdForLogging(
 | 
					  const logId = `ReadSyncs.forMessage(${getMessageIdForLogging(
 | 
				
			||||||
    message.attributes
 | 
					    message.attributes
 | 
				
			||||||
  )})`;
 | 
					  )})`;
 | 
				
			||||||
| 
						 | 
					@ -92,13 +104,17 @@ export function forMessage(
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  const readSyncValues = Array.from(readSyncs.values());
 | 
					  const readSyncValues = Array.from(readSyncs.values());
 | 
				
			||||||
  const foundSync = readSyncValues.find(item => {
 | 
					  const foundSync = readSyncValues.find(item => {
 | 
				
			||||||
    return item.senderId === sender?.id && item.timestamp === messageTimestamp;
 | 
					    const { readSync } = item;
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      readSync.senderId === sender?.id &&
 | 
				
			||||||
 | 
					      readSync.timestamp === messageTimestamp
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  if (foundSync) {
 | 
					  if (foundSync) {
 | 
				
			||||||
    log.info(
 | 
					    log.info(
 | 
				
			||||||
      `${logId}: Found early read sync for message ${foundSync.timestamp}`
 | 
					      `${logId}: Found early read sync for message ${foundSync.readSync.timestamp}`
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    remove(foundSync);
 | 
					    await remove(foundSync);
 | 
				
			||||||
    return foundSync;
 | 
					    return foundSync;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -106,20 +122,15 @@ export function forMessage(
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
 | 
					export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
 | 
				
			||||||
  readSyncs.set(
 | 
					  const { readSync, syncTaskId } = sync;
 | 
				
			||||||
    generateCacheKey({
 | 
					 | 
				
			||||||
      sender: sync.senderId,
 | 
					 | 
				
			||||||
      timestamp: sync.timestamp,
 | 
					 | 
				
			||||||
      type: 'readsync',
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
    sync
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const logId = `ReadSyncs.onSync(timestamp=${sync.timestamp})`;
 | 
					  readSyncs.set(syncTaskId, sync);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const logId = `ReadSyncs.onSync(timestamp=${readSync.timestamp})`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const messages = await window.Signal.Data.getMessagesBySentAt(
 | 
					    const messages = await window.Signal.Data.getMessagesBySentAt(
 | 
				
			||||||
      sync.timestamp
 | 
					      readSync.timestamp
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const found = messages.find(item => {
 | 
					    const found = messages.find(item => {
 | 
				
			||||||
| 
						 | 
					@ -129,7 +140,7 @@ export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
 | 
				
			||||||
        reason: logId,
 | 
					        reason: logId,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return isIncoming(item) && sender?.id === sync.senderId;
 | 
					      return isIncoming(item) && sender?.id === readSync.senderId;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!found) {
 | 
					    if (!found) {
 | 
				
			||||||
| 
						 | 
					@ -144,8 +155,8 @@ export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
 | 
				
			||||||
      found,
 | 
					      found,
 | 
				
			||||||
      'ReadSyncs.onSync'
 | 
					      'ReadSyncs.onSync'
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const readAt = Math.min(sync.readAt, Date.now());
 | 
					    const readAt = Math.min(readSync.readAt, Date.now());
 | 
				
			||||||
    const newestSentAt = sync.timestamp;
 | 
					    const newestSentAt = readSync.timestamp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // If message is unread, we mark it read. Otherwise, we update the expiration
 | 
					    // If message is unread, we mark it read. Otherwise, we update the expiration
 | 
				
			||||||
    //   timer to the time specified by the read sync if it's earlier than
 | 
					    //   timer to the time specified by the read sync if it's earlier than
 | 
				
			||||||
| 
						 | 
					@ -193,9 +204,9 @@ export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queueUpdateMessage(message.attributes);
 | 
					    queueUpdateMessage(message.attributes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    remove(sync);
 | 
					    await remove(sync);
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    remove(sync);
 | 
					 | 
				
			||||||
    log.error(`${logId} error:`, Errors.toLogFormat(error));
 | 
					    log.error(`${logId} error:`, Errors.toLogFormat(error));
 | 
				
			||||||
 | 
					    await remove(sync);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,8 @@
 | 
				
			||||||
// 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 type { AciString } from '../types/ServiceId';
 | 
					import { z } from 'zod';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { MessageModel } from '../models/messages';
 | 
					import type { MessageModel } from '../models/messages';
 | 
				
			||||||
import * as Errors from '../types/errors';
 | 
					import * as Errors from '../types/errors';
 | 
				
			||||||
import * as log from '../logging/log';
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
| 
						 | 
					@ -15,35 +16,38 @@ import { markViewed } from '../services/MessageUpdater';
 | 
				
			||||||
import { notificationService } from '../services/notifications';
 | 
					import { notificationService } from '../services/notifications';
 | 
				
			||||||
import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
 | 
					import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
 | 
				
			||||||
import { queueUpdateMessage } from '../util/messageBatcher';
 | 
					import { queueUpdateMessage } from '../util/messageBatcher';
 | 
				
			||||||
import { generateCacheKey } from './generateCacheKey';
 | 
					 | 
				
			||||||
import { AttachmentDownloadUrgency } from '../jobs/AttachmentDownloadManager';
 | 
					import { AttachmentDownloadUrgency } from '../jobs/AttachmentDownloadManager';
 | 
				
			||||||
 | 
					import { isAciString } from '../util/isAciString';
 | 
				
			||||||
 | 
					import dataInterface from '../sql/Client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { removeSyncTaskById } = dataInterface;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const viewSyncTaskSchema = z.object({
 | 
				
			||||||
 | 
					  type: z.literal('ViewSync').readonly(),
 | 
				
			||||||
 | 
					  senderAci: z.string().refine(isAciString),
 | 
				
			||||||
 | 
					  senderE164: z.string().optional(),
 | 
				
			||||||
 | 
					  senderId: z.string(),
 | 
				
			||||||
 | 
					  timestamp: z.number(),
 | 
				
			||||||
 | 
					  viewedAt: z.number(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ViewSyncTaskType = z.infer<typeof viewSyncTaskSchema>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ViewSyncAttributesType = {
 | 
					export type ViewSyncAttributesType = {
 | 
				
			||||||
  envelopeId: string;
 | 
					  envelopeId: string;
 | 
				
			||||||
  removeFromMessageReceiverCache: () => unknown;
 | 
					  syncTaskId: string;
 | 
				
			||||||
  senderAci: AciString;
 | 
					  viewSync: ViewSyncTaskType;
 | 
				
			||||||
  senderE164?: string;
 | 
					 | 
				
			||||||
  senderId: string;
 | 
					 | 
				
			||||||
  timestamp: number;
 | 
					 | 
				
			||||||
  viewedAt: number;
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const viewSyncs = new Map<string, ViewSyncAttributesType>();
 | 
					const viewSyncs = new Map<string, ViewSyncAttributesType>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function remove(sync: ViewSyncAttributesType): void {
 | 
					async function remove(sync: ViewSyncAttributesType): Promise<void> {
 | 
				
			||||||
  viewSyncs.delete(
 | 
					  await removeSyncTaskById(sync.syncTaskId);
 | 
				
			||||||
    generateCacheKey({
 | 
					 | 
				
			||||||
      sender: sync.senderId,
 | 
					 | 
				
			||||||
      timestamp: sync.timestamp,
 | 
					 | 
				
			||||||
      type: 'viewsync',
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  sync.removeFromMessageReceiverCache();
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function forMessage(
 | 
					export async function forMessage(
 | 
				
			||||||
  message: MessageModel
 | 
					  message: MessageModel
 | 
				
			||||||
): Array<ViewSyncAttributesType> {
 | 
					): Promise<Array<ViewSyncAttributesType>> {
 | 
				
			||||||
  const logId = `ViewSyncs.forMessage(${getMessageIdForLogging(
 | 
					  const logId = `ViewSyncs.forMessage(${getMessageIdForLogging(
 | 
				
			||||||
    message.attributes
 | 
					    message.attributes
 | 
				
			||||||
  )})`;
 | 
					  )})`;
 | 
				
			||||||
| 
						 | 
					@ -60,7 +64,11 @@ export function forMessage(
 | 
				
			||||||
  const viewSyncValues = Array.from(viewSyncs.values());
 | 
					  const viewSyncValues = Array.from(viewSyncs.values());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const matchingSyncs = viewSyncValues.filter(item => {
 | 
					  const matchingSyncs = viewSyncValues.filter(item => {
 | 
				
			||||||
    return item.senderId === sender?.id && item.timestamp === messageTimestamp;
 | 
					    const { viewSync } = item;
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      viewSync.senderId === sender?.id &&
 | 
				
			||||||
 | 
					      viewSync.timestamp === messageTimestamp
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (matchingSyncs.length > 0) {
 | 
					  if (matchingSyncs.length > 0) {
 | 
				
			||||||
| 
						 | 
					@ -68,28 +76,24 @@ export function forMessage(
 | 
				
			||||||
      `${logId}: Found ${matchingSyncs.length} early view sync(s) for message ${messageTimestamp}`
 | 
					      `${logId}: Found ${matchingSyncs.length} early view sync(s) for message ${messageTimestamp}`
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  matchingSyncs.forEach(sync => {
 | 
					  await Promise.all(
 | 
				
			||||||
    remove(sync);
 | 
					    matchingSyncs.map(async sync => {
 | 
				
			||||||
  });
 | 
					      await remove(sync);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return matchingSyncs;
 | 
					  return matchingSyncs;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
 | 
					export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
 | 
				
			||||||
  viewSyncs.set(
 | 
					  viewSyncs.set(sync.syncTaskId, sync);
 | 
				
			||||||
    generateCacheKey({
 | 
					  const { viewSync } = sync;
 | 
				
			||||||
      sender: sync.senderId,
 | 
					 | 
				
			||||||
      timestamp: sync.timestamp,
 | 
					 | 
				
			||||||
      type: 'viewsync',
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
    sync
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const logId = `ViewSyncs.onSync(timestamp=${sync.timestamp})`;
 | 
					  const logId = `ViewSyncs.onSync(timestamp=${viewSync.timestamp})`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const messages = await window.Signal.Data.getMessagesBySentAt(
 | 
					    const messages = await window.Signal.Data.getMessagesBySentAt(
 | 
				
			||||||
      sync.timestamp
 | 
					      viewSync.timestamp
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const found = messages.find(item => {
 | 
					    const found = messages.find(item => {
 | 
				
			||||||
| 
						 | 
					@ -99,15 +103,15 @@ export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
 | 
				
			||||||
        reason: logId,
 | 
					        reason: logId,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return sender?.id === sync.senderId;
 | 
					      return sender?.id === viewSync.senderId;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!found) {
 | 
					    if (!found) {
 | 
				
			||||||
      log.info(
 | 
					      log.info(
 | 
				
			||||||
        `${logId}: nothing found`,
 | 
					        `${logId}: nothing found`,
 | 
				
			||||||
        sync.senderId,
 | 
					        viewSync.senderId,
 | 
				
			||||||
        sync.senderE164,
 | 
					        viewSync.senderE164,
 | 
				
			||||||
        sync.senderAci
 | 
					        viewSync.senderAci
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -123,7 +127,7 @@ export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (message.get('readStatus') !== ReadStatus.Viewed) {
 | 
					    if (message.get('readStatus') !== ReadStatus.Viewed) {
 | 
				
			||||||
      didChangeMessage = true;
 | 
					      didChangeMessage = true;
 | 
				
			||||||
      message.set(markViewed(message.attributes, sync.viewedAt));
 | 
					      message.set(markViewed(message.attributes, viewSync.viewedAt));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const attachments = message.get('attachments');
 | 
					      const attachments = message.get('attachments');
 | 
				
			||||||
      if (!attachments?.every(isDownloaded)) {
 | 
					      if (!attachments?.every(isDownloaded)) {
 | 
				
			||||||
| 
						 | 
					@ -154,9 +158,9 @@ export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
 | 
				
			||||||
      queueUpdateMessage(message.attributes);
 | 
					      queueUpdateMessage(message.attributes);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    remove(sync);
 | 
					    await remove(sync);
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    remove(sync);
 | 
					 | 
				
			||||||
    log.error(`${logId} error:`, Errors.toLogFormat(error));
 | 
					    log.error(`${logId} error:`, Errors.toLogFormat(error));
 | 
				
			||||||
 | 
					    await remove(sync);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2945,6 +2945,13 @@ export class ConversationModel extends window.Backbone
 | 
				
			||||||
      senderAci,
 | 
					      senderAci,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!this.get('active_at')) {
 | 
				
			||||||
 | 
					      log.warn(
 | 
				
			||||||
 | 
					        `addDeliveryIssue: ${this.idForLogging()} has no active_at, dropping delivery issue instead of adding`
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const message = {
 | 
					    const message = {
 | 
				
			||||||
      conversationId: this.id,
 | 
					      conversationId: this.id,
 | 
				
			||||||
      type: 'delivery-issue',
 | 
					      type: 'delivery-issue',
 | 
				
			||||||
| 
						 | 
					@ -3363,7 +3370,9 @@ export class ConversationModel extends window.Backbone
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const message = window.MessageCache.__DEPRECATED$getById(notificationId);
 | 
					    const message = window.MessageCache.__DEPRECATED$getById(notificationId);
 | 
				
			||||||
    if (message) {
 | 
					    if (message) {
 | 
				
			||||||
      await window.Signal.Data.removeMessage(message.id);
 | 
					      await window.Signal.Data.removeMessage(message.id, {
 | 
				
			||||||
 | 
					        singleProtoJobQueue,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -3404,7 +3413,9 @@ export class ConversationModel extends window.Backbone
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const message = window.MessageCache.__DEPRECATED$getById(notificationId);
 | 
					    const message = window.MessageCache.__DEPRECATED$getById(notificationId);
 | 
				
			||||||
    if (message) {
 | 
					    if (message) {
 | 
				
			||||||
      await window.Signal.Data.removeMessage(message.id);
 | 
					      await window.Signal.Data.removeMessage(message.id, {
 | 
				
			||||||
 | 
					        singleProtoJobQueue,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
| 
						 | 
					@ -5003,7 +5014,14 @@ export class ConversationModel extends window.Backbone
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    window.Signal.Data.updateConversation(this.attributes);
 | 
					    window.Signal.Data.updateConversation(this.attributes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (source === 'local-delete' && isEnabled('desktop.deleteSync.send')) {
 | 
					    const ourConversation =
 | 
				
			||||||
 | 
					      window.ConversationController.getOurConversationOrThrow();
 | 
				
			||||||
 | 
					    const capable = Boolean(ourConversation.get('capabilities')?.deleteSync);
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      source === 'local-delete' &&
 | 
				
			||||||
 | 
					      capable &&
 | 
				
			||||||
 | 
					      isEnabled('desktop.deleteSync.send')
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
      log.info(`${logId}: Preparing sync message`);
 | 
					      log.info(`${logId}: Preparing sync message`);
 | 
				
			||||||
      const timestamp = Date.now();
 | 
					      const timestamp = Date.now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5044,7 +5062,9 @@ export class ConversationModel extends window.Backbone
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    log.info(`${logId}: Starting delete`);
 | 
					    log.info(`${logId}: Starting delete`);
 | 
				
			||||||
    await window.Signal.Data.removeMessagesInConversation(this.id, {
 | 
					    await window.Signal.Data.removeMessagesInConversation(this.id, {
 | 
				
			||||||
 | 
					      fromSync: source !== 'local-delete-sync',
 | 
				
			||||||
      logId: this.idForLogging(),
 | 
					      logId: this.idForLogging(),
 | 
				
			||||||
 | 
					      singleProtoJobQueue,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    log.info(`${logId}: Delete complete`);
 | 
					    log.info(`${logId}: Delete complete`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -109,7 +109,7 @@ import {
 | 
				
			||||||
} from '../services/notifications';
 | 
					} from '../services/notifications';
 | 
				
			||||||
import type { LinkPreviewType } from '../types/message/LinkPreviews';
 | 
					import type { LinkPreviewType } from '../types/message/LinkPreviews';
 | 
				
			||||||
import * as log from '../logging/log';
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
import { cleanupMessage, deleteMessageData } from '../util/cleanup';
 | 
					import { deleteMessageData } from '../util/cleanup';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  getSource,
 | 
					  getSource,
 | 
				
			||||||
  getSourceServiceId,
 | 
					  getSourceServiceId,
 | 
				
			||||||
| 
						 | 
					@ -315,10 +315,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
 | 
				
			||||||
    this.set(attributes);
 | 
					    this.set(attributes);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async cleanup(): Promise<void> {
 | 
					 | 
				
			||||||
    await cleanupMessage(this.attributes);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async deleteData(): Promise<void> {
 | 
					  async deleteData(): Promise<void> {
 | 
				
			||||||
    await deleteMessageData(this.attributes);
 | 
					    await deleteMessageData(this.attributes);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,18 +4,21 @@
 | 
				
			||||||
import { batch } from 'react-redux';
 | 
					import { batch } from 'react-redux';
 | 
				
			||||||
import { debounce } from 'lodash';
 | 
					import { debounce } from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { MessageModel } from '../models/messages';
 | 
					 | 
				
			||||||
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
 | 
					import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
 | 
				
			||||||
import { sleep } from '../util/sleep';
 | 
					import { sleep } from '../util/sleep';
 | 
				
			||||||
import { SECOND } from '../util/durations';
 | 
					import { SECOND } from '../util/durations';
 | 
				
			||||||
import * as Errors from '../types/errors';
 | 
					import * as Errors from '../types/errors';
 | 
				
			||||||
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { MessageModel } from '../models/messages';
 | 
				
			||||||
 | 
					import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ExpiringMessagesDeletionService {
 | 
					class ExpiringMessagesDeletionService {
 | 
				
			||||||
  public update: typeof this.checkExpiringMessages;
 | 
					  public update: typeof this.checkExpiringMessages;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private timeout?: ReturnType<typeof setTimeout>;
 | 
					  private timeout?: ReturnType<typeof setTimeout>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor() {
 | 
					  constructor(private readonly singleProtoJobQueue: SingleProtoJobQueue) {
 | 
				
			||||||
    this.update = debounce(this.checkExpiringMessages, 1000);
 | 
					    this.update = debounce(this.checkExpiringMessages, 1000);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,7 +45,9 @@ class ExpiringMessagesDeletionService {
 | 
				
			||||||
        inMemoryMessages.push(message);
 | 
					        inMemoryMessages.push(message);
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await window.Signal.Data.removeMessages(messageIds);
 | 
					      await window.Signal.Data.removeMessages(messageIds, {
 | 
				
			||||||
 | 
					        singleProtoJobQueue: this.singleProtoJobQueue,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      batch(() => {
 | 
					      batch(() => {
 | 
				
			||||||
        inMemoryMessages.forEach(message => {
 | 
					        inMemoryMessages.forEach(message => {
 | 
				
			||||||
| 
						 | 
					@ -108,5 +113,21 @@ class ExpiringMessagesDeletionService {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const expiringMessagesDeletionService =
 | 
					// Because this service is used inside of Client.ts, it can't directly reference
 | 
				
			||||||
  new ExpiringMessagesDeletionService();
 | 
					//   SingleProtoJobQueue. Instead of direct access, it is provided once on startup.
 | 
				
			||||||
 | 
					export function initialize(singleProtoJobQueue: SingleProtoJobQueue): void {
 | 
				
			||||||
 | 
					  if (instance) {
 | 
				
			||||||
 | 
					    log.warn('Expiring Messages Deletion service is already initialized!');
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  instance = new ExpiringMessagesDeletionService(singleProtoJobQueue);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function update(): Promise<void> {
 | 
				
			||||||
 | 
					  if (!instance) {
 | 
				
			||||||
 | 
					    throw new Error('Expiring Messages Deletion service not yet initialized!');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  await instance.update();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let instance: ExpiringMessagesDeletionService;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,13 +2,11 @@
 | 
				
			||||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
					// SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { ipcRenderer as ipc } from 'electron';
 | 
					import { ipcRenderer as ipc } from 'electron';
 | 
				
			||||||
import PQueue from 'p-queue';
 | 
					 | 
				
			||||||
import { batch } from 'react-redux';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { has, get, groupBy, isTypedArray, last, map, omit } from 'lodash';
 | 
					import { has, get, groupBy, isTypedArray, last, map, omit } from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { deleteExternalFiles } from '../types/Conversation';
 | 
					import { deleteExternalFiles } from '../types/Conversation';
 | 
				
			||||||
import { expiringMessagesDeletionService } from '../services/expiringMessagesDeletion';
 | 
					import { update as updateExpiringMessagesService } from '../services/expiringMessagesDeletion';
 | 
				
			||||||
import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService';
 | 
					import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService';
 | 
				
			||||||
import * as Bytes from '../Bytes';
 | 
					import * as Bytes from '../Bytes';
 | 
				
			||||||
import { createBatcher } from '../util/batcher';
 | 
					import { createBatcher } from '../util/batcher';
 | 
				
			||||||
| 
						 | 
					@ -24,12 +22,7 @@ import * as Errors from '../types/errors';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { StoredJob } from '../jobs/types';
 | 
					import type { StoredJob } from '../jobs/types';
 | 
				
			||||||
import { formatJobForInsert } from '../jobs/formatJobForInsert';
 | 
					import { formatJobForInsert } from '../jobs/formatJobForInsert';
 | 
				
			||||||
import {
 | 
					import { cleanupMessages } from '../util/cleanup';
 | 
				
			||||||
  cleanupMessage,
 | 
					 | 
				
			||||||
  cleanupMessageFromMemory,
 | 
					 | 
				
			||||||
  deleteMessageData,
 | 
					 | 
				
			||||||
} from '../util/cleanup';
 | 
					 | 
				
			||||||
import { drop } from '../util/drop';
 | 
					 | 
				
			||||||
import { ipcInvoke, doShutdown } from './channels';
 | 
					import { ipcInvoke, doShutdown } from './channels';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type {
 | 
					import type {
 | 
				
			||||||
| 
						 | 
					@ -60,12 +53,12 @@ import type {
 | 
				
			||||||
  KyberPreKeyType,
 | 
					  KyberPreKeyType,
 | 
				
			||||||
  StoredKyberPreKeyType,
 | 
					  StoredKyberPreKeyType,
 | 
				
			||||||
} from './Interface';
 | 
					} from './Interface';
 | 
				
			||||||
import { MINUTE } from '../util/durations';
 | 
					 | 
				
			||||||
import { getMessageIdForLogging } from '../util/idForLogging';
 | 
					import { getMessageIdForLogging } from '../util/idForLogging';
 | 
				
			||||||
import type { MessageAttributesType } from '../model-types';
 | 
					import type { MessageAttributesType } from '../model-types';
 | 
				
			||||||
import { incrementMessageCounter } from '../util/incrementMessageCounter';
 | 
					import { incrementMessageCounter } from '../util/incrementMessageCounter';
 | 
				
			||||||
import { generateSnippetAroundMention } from '../util/search';
 | 
					import { generateSnippetAroundMention } from '../util/search';
 | 
				
			||||||
import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
 | 
					import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
 | 
				
			||||||
 | 
					import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ERASE_SQL_KEY = 'erase-sql-key';
 | 
					const ERASE_SQL_KEY = 'erase-sql-key';
 | 
				
			||||||
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
 | 
					const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
 | 
				
			||||||
| 
						 | 
					@ -104,6 +97,8 @@ const exclusiveInterface: ClientExclusiveInterface = {
 | 
				
			||||||
  removeConversation,
 | 
					  removeConversation,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  searchMessages,
 | 
					  searchMessages,
 | 
				
			||||||
 | 
					  removeMessage,
 | 
				
			||||||
 | 
					  removeMessages,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getRecentStoryReplies,
 | 
					  getRecentStoryReplies,
 | 
				
			||||||
  getOlderMessagesByConversation,
 | 
					  getOlderMessagesByConversation,
 | 
				
			||||||
| 
						 | 
					@ -125,8 +120,6 @@ const exclusiveInterface: ClientExclusiveInterface = {
 | 
				
			||||||
type ClientOverridesType = ClientExclusiveInterface &
 | 
					type ClientOverridesType = ClientExclusiveInterface &
 | 
				
			||||||
  Pick<
 | 
					  Pick<
 | 
				
			||||||
    ServerInterface,
 | 
					    ServerInterface,
 | 
				
			||||||
    | 'removeMessage'
 | 
					 | 
				
			||||||
    | 'removeMessages'
 | 
					 | 
				
			||||||
    | 'saveAttachmentDownloadJob'
 | 
					    | 'saveAttachmentDownloadJob'
 | 
				
			||||||
    | 'saveMessage'
 | 
					    | 'saveMessage'
 | 
				
			||||||
    | 'saveMessages'
 | 
					    | 'saveMessages'
 | 
				
			||||||
| 
						 | 
					@ -142,8 +135,6 @@ const channels: ServerInterface = new Proxy({} as ServerInterface, {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const clientExclusiveOverrides: ClientOverridesType = {
 | 
					const clientExclusiveOverrides: ClientOverridesType = {
 | 
				
			||||||
  ...exclusiveInterface,
 | 
					  ...exclusiveInterface,
 | 
				
			||||||
  removeMessage,
 | 
					 | 
				
			||||||
  removeMessages,
 | 
					 | 
				
			||||||
  saveAttachmentDownloadJob,
 | 
					  saveAttachmentDownloadJob,
 | 
				
			||||||
  saveMessage,
 | 
					  saveMessage,
 | 
				
			||||||
  saveMessages,
 | 
					  saveMessages,
 | 
				
			||||||
| 
						 | 
					@ -562,7 +553,7 @@ async function saveMessage(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  softAssert(isValidUuid(id), 'saveMessage: messageId is not a UUID');
 | 
					  softAssert(isValidUuid(id), 'saveMessage: messageId is not a UUID');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void expiringMessagesDeletionService.update();
 | 
					  void updateExpiringMessagesService();
 | 
				
			||||||
  void tapToViewMessagesDeletionService.update();
 | 
					  void tapToViewMessagesDeletionService.update();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return id;
 | 
					  return id;
 | 
				
			||||||
| 
						 | 
					@ -577,26 +568,39 @@ async function saveMessages(
 | 
				
			||||||
    options
 | 
					    options
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void expiringMessagesDeletionService.update();
 | 
					  void updateExpiringMessagesService();
 | 
				
			||||||
  void tapToViewMessagesDeletionService.update();
 | 
					  void tapToViewMessagesDeletionService.update();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return result;
 | 
					  return result;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function removeMessage(id: string): Promise<void> {
 | 
					async function removeMessage(
 | 
				
			||||||
 | 
					  id: string,
 | 
				
			||||||
 | 
					  options: {
 | 
				
			||||||
 | 
					    singleProtoJobQueue: SingleProtoJobQueue;
 | 
				
			||||||
 | 
					    fromSync?: boolean;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					): Promise<void> {
 | 
				
			||||||
  const message = await channels.getMessageById(id);
 | 
					  const message = await channels.getMessageById(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Note: It's important to have a fully database-hydrated model to delete here because
 | 
					  // Note: It's important to have a fully database-hydrated model to delete here because
 | 
				
			||||||
  //   it needs to delete all associated on-disk files along with the database delete.
 | 
					  //   it needs to delete all associated on-disk files along with the database delete.
 | 
				
			||||||
  if (message) {
 | 
					  if (message) {
 | 
				
			||||||
    await channels.removeMessage(id);
 | 
					    await channels.removeMessage(id);
 | 
				
			||||||
    await cleanupMessage(message);
 | 
					    await cleanupMessages([message], {
 | 
				
			||||||
 | 
					      ...options,
 | 
				
			||||||
 | 
					      markCallHistoryDeleted: dataInterface.markCallHistoryDeleted,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function deleteAndCleanup(
 | 
					export async function deleteAndCleanup(
 | 
				
			||||||
  messages: Array<MessageAttributesType>,
 | 
					  messages: Array<MessageAttributesType>,
 | 
				
			||||||
  logId: string
 | 
					  logId: string,
 | 
				
			||||||
 | 
					  options: {
 | 
				
			||||||
 | 
					    fromSync?: boolean;
 | 
				
			||||||
 | 
					    singleProtoJobQueue: SingleProtoJobQueue;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
): Promise<void> {
 | 
					): Promise<void> {
 | 
				
			||||||
  const ids = messages.map(message => message.id);
 | 
					  const ids = messages.map(message => message.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -604,37 +608,26 @@ export async function deleteAndCleanup(
 | 
				
			||||||
  await channels.removeMessages(ids);
 | 
					  await channels.removeMessages(ids);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  log.info(`deleteAndCleanup/${logId}: Cleanup for ${ids.length} messages...`);
 | 
					  log.info(`deleteAndCleanup/${logId}: Cleanup for ${ids.length} messages...`);
 | 
				
			||||||
  await _cleanupMessages(messages);
 | 
					  await cleanupMessages(messages, {
 | 
				
			||||||
 | 
					    ...options,
 | 
				
			||||||
 | 
					    markCallHistoryDeleted: dataInterface.markCallHistoryDeleted,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  log.info(`deleteAndCleanup/${logId}: Complete`);
 | 
					  log.info(`deleteAndCleanup/${logId}: Complete`);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function _cleanupMessages(
 | 
					 | 
				
			||||||
  messages: ReadonlyArray<MessageAttributesType>
 | 
					 | 
				
			||||||
): Promise<void> {
 | 
					 | 
				
			||||||
  // First, remove messages from memory, so we can batch the updates in redux
 | 
					 | 
				
			||||||
  batch(() => {
 | 
					 | 
				
			||||||
    messages.forEach(message => cleanupMessageFromMemory(message));
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Then, handle any asynchronous actions (e.g. deleting data from disk)
 | 
					 | 
				
			||||||
  const queue = new PQueue({ concurrency: 3, timeout: MINUTE * 30 });
 | 
					 | 
				
			||||||
  drop(
 | 
					 | 
				
			||||||
    queue.addAll(
 | 
					 | 
				
			||||||
      messages.map(
 | 
					 | 
				
			||||||
        (message: MessageAttributesType) => async () =>
 | 
					 | 
				
			||||||
          deleteMessageData(message)
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  await queue.onIdle();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function removeMessages(
 | 
					async function removeMessages(
 | 
				
			||||||
  messageIds: ReadonlyArray<string>
 | 
					  messageIds: ReadonlyArray<string>,
 | 
				
			||||||
 | 
					  options: {
 | 
				
			||||||
 | 
					    fromSync?: boolean;
 | 
				
			||||||
 | 
					    singleProtoJobQueue: SingleProtoJobQueue;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
): Promise<void> {
 | 
					): Promise<void> {
 | 
				
			||||||
  const messages = await channels.getMessagesById(messageIds);
 | 
					  const messages = await channels.getMessagesById(messageIds);
 | 
				
			||||||
  await _cleanupMessages(messages);
 | 
					  await cleanupMessages(messages, {
 | 
				
			||||||
 | 
					    ...options,
 | 
				
			||||||
 | 
					    markCallHistoryDeleted: dataInterface.markCallHistoryDeleted,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
  await channels.removeMessages(messageIds);
 | 
					  await channels.removeMessages(messageIds);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -686,9 +679,13 @@ async function removeMessagesInConversation(
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    logId,
 | 
					    logId,
 | 
				
			||||||
    receivedAt,
 | 
					    receivedAt,
 | 
				
			||||||
 | 
					    singleProtoJobQueue,
 | 
				
			||||||
 | 
					    fromSync,
 | 
				
			||||||
  }: {
 | 
					  }: {
 | 
				
			||||||
 | 
					    fromSync?: boolean;
 | 
				
			||||||
    logId: string;
 | 
					    logId: string;
 | 
				
			||||||
    receivedAt?: number;
 | 
					    receivedAt?: number;
 | 
				
			||||||
 | 
					    singleProtoJobQueue: SingleProtoJobQueue;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
): Promise<void> {
 | 
					): Promise<void> {
 | 
				
			||||||
  let messages;
 | 
					  let messages;
 | 
				
			||||||
| 
						 | 
					@ -713,7 +710,7 @@ async function removeMessagesInConversation(
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // eslint-disable-next-line no-await-in-loop
 | 
					    // eslint-disable-next-line no-await-in-loop
 | 
				
			||||||
    await deleteAndCleanup(messages, logId);
 | 
					    await deleteAndCleanup(messages, logId, { fromSync, singleProtoJobQueue });
 | 
				
			||||||
  } while (messages.length > 0);
 | 
					  } while (messages.length > 0);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,6 +34,7 @@ import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
 | 
				
			||||||
import type { GroupSendEndorsementsData } from '../types/GroupSendEndorsements';
 | 
					import type { GroupSendEndorsementsData } from '../types/GroupSendEndorsements';
 | 
				
			||||||
import type { SyncTaskType } from '../util/syncTasks';
 | 
					import type { SyncTaskType } from '../util/syncTasks';
 | 
				
			||||||
import type { AttachmentBackupJobType } from '../types/AttachmentBackup';
 | 
					import type { AttachmentBackupJobType } from '../types/AttachmentBackup';
 | 
				
			||||||
 | 
					import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type AdjacentMessagesByConversationOptionsType = Readonly<{
 | 
					export type AdjacentMessagesByConversationOptionsType = Readonly<{
 | 
				
			||||||
  conversationId: string;
 | 
					  conversationId: string;
 | 
				
			||||||
| 
						 | 
					@ -557,8 +558,6 @@ export type DataInterface = {
 | 
				
			||||||
    arrayOfMessages: ReadonlyArray<MessageType>,
 | 
					    arrayOfMessages: ReadonlyArray<MessageType>,
 | 
				
			||||||
    options: { forceSave?: boolean; ourAci: AciString }
 | 
					    options: { forceSave?: boolean; ourAci: AciString }
 | 
				
			||||||
  ) => Promise<Array<string>>;
 | 
					  ) => Promise<Array<string>>;
 | 
				
			||||||
  removeMessage: (id: string) => Promise<void>;
 | 
					 | 
				
			||||||
  removeMessages: (ids: ReadonlyArray<string>) => Promise<void>;
 | 
					 | 
				
			||||||
  pageMessages: (
 | 
					  pageMessages: (
 | 
				
			||||||
    cursor?: PageMessagesCursorType
 | 
					    cursor?: PageMessagesCursorType
 | 
				
			||||||
  ) => Promise<PageMessagesResultType>;
 | 
					  ) => Promise<PageMessagesResultType>;
 | 
				
			||||||
| 
						 | 
					@ -667,6 +666,7 @@ export type DataInterface = {
 | 
				
			||||||
    conversationId: string;
 | 
					    conversationId: string;
 | 
				
			||||||
  }): Promise<MessageType | undefined>;
 | 
					  }): Promise<MessageType | undefined>;
 | 
				
			||||||
  getAllCallHistory: () => Promise<ReadonlyArray<CallHistoryDetails>>;
 | 
					  getAllCallHistory: () => Promise<ReadonlyArray<CallHistoryDetails>>;
 | 
				
			||||||
 | 
					  markCallHistoryDeleted: (callId: string) => Promise<void>;
 | 
				
			||||||
  clearCallHistory: (beforeTimestamp: number) => Promise<Array<string>>;
 | 
					  clearCallHistory: (beforeTimestamp: number) => Promise<Array<string>>;
 | 
				
			||||||
  cleanupCallHistoryMessages: () => Promise<void>;
 | 
					  cleanupCallHistoryMessages: () => Promise<void>;
 | 
				
			||||||
  getCallHistoryUnreadCount(): Promise<number>;
 | 
					  getCallHistoryUnreadCount(): Promise<number>;
 | 
				
			||||||
| 
						 | 
					@ -929,6 +929,8 @@ export type ServerInterface = DataInterface & {
 | 
				
			||||||
    options?: { limit?: number };
 | 
					    options?: { limit?: number };
 | 
				
			||||||
    contactServiceIdsMatchingQuery?: Array<ServiceIdString>;
 | 
					    contactServiceIdsMatchingQuery?: Array<ServiceIdString>;
 | 
				
			||||||
  }) => Promise<Array<ServerSearchResultMessageType>>;
 | 
					  }) => Promise<Array<ServerSearchResultMessageType>>;
 | 
				
			||||||
 | 
					  removeMessage: (id: string) => Promise<void>;
 | 
				
			||||||
 | 
					  removeMessages: (ids: ReadonlyArray<string>) => Promise<void>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getRecentStoryReplies(
 | 
					  getRecentStoryReplies(
 | 
				
			||||||
    storyId: string,
 | 
					    storyId: string,
 | 
				
			||||||
| 
						 | 
					@ -1022,6 +1024,20 @@ export type ClientExclusiveInterface = {
 | 
				
			||||||
  removeConversation: (id: string) => Promise<void>;
 | 
					  removeConversation: (id: string) => Promise<void>;
 | 
				
			||||||
  flushUpdateConversationBatcher: () => Promise<void>;
 | 
					  flushUpdateConversationBatcher: () => Promise<void>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  removeMessage: (
 | 
				
			||||||
 | 
					    id: string,
 | 
				
			||||||
 | 
					    options: {
 | 
				
			||||||
 | 
					      fromSync?: boolean;
 | 
				
			||||||
 | 
					      singleProtoJobQueue: SingleProtoJobQueue;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ) => Promise<void>;
 | 
				
			||||||
 | 
					  removeMessages: (
 | 
				
			||||||
 | 
					    ids: ReadonlyArray<string>,
 | 
				
			||||||
 | 
					    options: {
 | 
				
			||||||
 | 
					      fromSync?: boolean;
 | 
				
			||||||
 | 
					      singleProtoJobQueue: SingleProtoJobQueue;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ) => Promise<void>;
 | 
				
			||||||
  searchMessages: ({
 | 
					  searchMessages: ({
 | 
				
			||||||
    query,
 | 
					    query,
 | 
				
			||||||
    conversationId,
 | 
					    conversationId,
 | 
				
			||||||
| 
						 | 
					@ -1084,8 +1100,10 @@ export type ClientExclusiveInterface = {
 | 
				
			||||||
  removeMessagesInConversation: (
 | 
					  removeMessagesInConversation: (
 | 
				
			||||||
    conversationId: string,
 | 
					    conversationId: string,
 | 
				
			||||||
    options: {
 | 
					    options: {
 | 
				
			||||||
 | 
					      fromSync?: boolean;
 | 
				
			||||||
      logId: string;
 | 
					      logId: string;
 | 
				
			||||||
      receivedAt?: number;
 | 
					      receivedAt?: number;
 | 
				
			||||||
 | 
					      singleProtoJobQueue: SingleProtoJobQueue;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  ) => Promise<void>;
 | 
					  ) => Promise<void>;
 | 
				
			||||||
  removeOtherData: () => Promise<void>;
 | 
					  removeOtherData: () => Promise<void>;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -347,6 +347,7 @@ const dataInterface: ServerInterface = {
 | 
				
			||||||
  getLastConversationMessage,
 | 
					  getLastConversationMessage,
 | 
				
			||||||
  getAllCallHistory,
 | 
					  getAllCallHistory,
 | 
				
			||||||
  clearCallHistory,
 | 
					  clearCallHistory,
 | 
				
			||||||
 | 
					  markCallHistoryDeleted,
 | 
				
			||||||
  cleanupCallHistoryMessages,
 | 
					  cleanupCallHistoryMessages,
 | 
				
			||||||
  getCallHistoryUnreadCount,
 | 
					  getCallHistoryUnreadCount,
 | 
				
			||||||
  markCallHistoryRead,
 | 
					  markCallHistoryRead,
 | 
				
			||||||
| 
						 | 
					@ -3635,6 +3636,19 @@ async function clearCallHistory(
 | 
				
			||||||
  })();
 | 
					  })();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function markCallHistoryDeleted(callId: string): Promise<void> {
 | 
				
			||||||
 | 
					  const db = await getWritableInstance();
 | 
				
			||||||
 | 
					  const [query, params] = sql`
 | 
				
			||||||
 | 
					    UPDATE callsHistory
 | 
				
			||||||
 | 
					    SET
 | 
				
			||||||
 | 
					      status = ${DirectCallStatus.Deleted},
 | 
				
			||||||
 | 
					      timestamp = ${Date.now()}
 | 
				
			||||||
 | 
					    WHERE callId = ${callId};
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  db.prepare(query).run(params);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function cleanupCallHistoryMessages(): Promise<void> {
 | 
					async function cleanupCallHistoryMessages(): Promise<void> {
 | 
				
			||||||
  const db = await getWritableInstance();
 | 
					  const db = await getWritableInstance();
 | 
				
			||||||
  return db
 | 
					  return db
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -195,6 +195,7 @@ import {
 | 
				
			||||||
} from '../../util/deleteForMe';
 | 
					} from '../../util/deleteForMe';
 | 
				
			||||||
import { MAX_MESSAGE_COUNT } from '../../util/deleteForMe.types';
 | 
					import { MAX_MESSAGE_COUNT } from '../../util/deleteForMe.types';
 | 
				
			||||||
import { isEnabled } from '../../RemoteConfig';
 | 
					import { isEnabled } from '../../RemoteConfig';
 | 
				
			||||||
 | 
					import type { CapabilitiesType } from '../../textsecure/WebAPI';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// State
 | 
					// State
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -265,6 +266,7 @@ export type ConversationType = ReadonlyDeep<
 | 
				
			||||||
    firstName?: string;
 | 
					    firstName?: string;
 | 
				
			||||||
    profileName?: string;
 | 
					    profileName?: string;
 | 
				
			||||||
    profileLastUpdatedAt?: number;
 | 
					    profileLastUpdatedAt?: number;
 | 
				
			||||||
 | 
					    capabilities?: CapabilitiesType;
 | 
				
			||||||
    username?: string;
 | 
					    username?: string;
 | 
				
			||||||
    about?: string;
 | 
					    about?: string;
 | 
				
			||||||
    aboutText?: string;
 | 
					    aboutText?: string;
 | 
				
			||||||
| 
						 | 
					@ -1753,7 +1755,9 @@ function deleteMessages({
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await window.Signal.Data.removeMessages(messageIds);
 | 
					    await window.Signal.Data.removeMessages(messageIds, {
 | 
				
			||||||
 | 
					      singleProtoJobQueue,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    popPanelForConversation()(dispatch, getState, undefined);
 | 
					    popPanelForConversation()(dispatch, getState, undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1761,7 +1765,11 @@ function deleteMessages({
 | 
				
			||||||
      dispatch(scrollToMessage(conversationId, nearbyMessageId));
 | 
					      dispatch(scrollToMessage(conversationId, nearbyMessageId));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!isEnabled('desktop.deleteSync.send')) {
 | 
					    const ourConversation =
 | 
				
			||||||
 | 
					      window.ConversationController.getOurConversationOrThrow();
 | 
				
			||||||
 | 
					    const capable = Boolean(ourConversation.get('capabilities')?.deleteSync);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!capable || !isEnabled('desktop.deleteSync.send')) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (messages.length === 0) {
 | 
					    if (messages.length === 0) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -69,6 +69,7 @@ import {
 | 
				
			||||||
  conversationQueueJobEnum,
 | 
					  conversationQueueJobEnum,
 | 
				
			||||||
} from '../../jobs/conversationJobQueue';
 | 
					} from '../../jobs/conversationJobQueue';
 | 
				
			||||||
import { ReceiptType } from '../../types/Receipt';
 | 
					import { ReceiptType } from '../../types/Receipt';
 | 
				
			||||||
 | 
					import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type StoryDataType = ReadonlyDeep<
 | 
					export type StoryDataType = ReadonlyDeep<
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
| 
						 | 
					@ -284,7 +285,7 @@ function deleteGroupStoryReply(
 | 
				
			||||||
  messageId: string
 | 
					  messageId: string
 | 
				
			||||||
): ThunkAction<void, RootStateType, unknown, StoryReplyDeletedActionType> {
 | 
					): ThunkAction<void, RootStateType, unknown, StoryReplyDeletedActionType> {
 | 
				
			||||||
  return async dispatch => {
 | 
					  return async dispatch => {
 | 
				
			||||||
    await window.Signal.Data.removeMessage(messageId);
 | 
					    await window.Signal.Data.removeMessage(messageId, { singleProtoJobQueue });
 | 
				
			||||||
    dispatch({
 | 
					    dispatch({
 | 
				
			||||||
      type: STORY_REPLY_DELETED,
 | 
					      type: STORY_REPLY_DELETED,
 | 
				
			||||||
      payload: messageId,
 | 
					      payload: messageId,
 | 
				
			||||||
| 
						 | 
					@ -1408,10 +1409,7 @@ function removeAllContactStories(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    log.info(`${logId}: removing ${messages.length} stories`);
 | 
					    log.info(`${logId}: removing ${messages.length} stories`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await Promise.all([
 | 
					    await dataInterface.removeMessages(messageIds, { singleProtoJobQueue });
 | 
				
			||||||
      messages.map(m => m.cleanup()),
 | 
					 | 
				
			||||||
      await dataInterface.removeMessages(messageIds),
 | 
					 | 
				
			||||||
    ]);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dispatch({
 | 
					    dispatch({
 | 
				
			||||||
      type: 'NOOP',
 | 
					      type: 'NOOP',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										38
									
								
								ts/state/selectors/items-extra.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								ts/state/selectors/items-extra.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,38 @@
 | 
				
			||||||
 | 
					// Copyright 2019 Signal Messenger, LLC
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { createSelector } from 'reselect';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { getUserACI } from './user';
 | 
				
			||||||
 | 
					import { getConversationSelector } from './conversations';
 | 
				
			||||||
 | 
					import { getRemoteConfig, isRemoteConfigFlagEnabled } from './items';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { AciString } from '../../types/ServiceId';
 | 
				
			||||||
 | 
					import type { ConfigMapType } from '../../RemoteConfig';
 | 
				
			||||||
 | 
					import type { GetConversationByIdType } from './conversations';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getDeleteSyncSendEnabled = createSelector(
 | 
				
			||||||
 | 
					  getUserACI,
 | 
				
			||||||
 | 
					  getConversationSelector,
 | 
				
			||||||
 | 
					  getRemoteConfig,
 | 
				
			||||||
 | 
					  (
 | 
				
			||||||
 | 
					    aci: AciString | undefined,
 | 
				
			||||||
 | 
					    conversationSelector: GetConversationByIdType,
 | 
				
			||||||
 | 
					    remoteConfig: ConfigMapType
 | 
				
			||||||
 | 
					  ): boolean => {
 | 
				
			||||||
 | 
					    if (!aci) {
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const ourConversation = conversationSelector(aci);
 | 
				
			||||||
 | 
					    if (!ourConversation) {
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { capabilities } = ourConversation;
 | 
				
			||||||
 | 
					    if (!capabilities || !capabilities.deleteSync) {
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return isRemoteConfigFlagEnabled(remoteConfig, 'desktop.deleteSync.send');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
| 
						 | 
					@ -127,13 +127,6 @@ export const isInternalUser = createSelector(
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getDeleteSyncSendEnabled = createSelector(
 | 
					 | 
				
			||||||
  getRemoteConfig,
 | 
					 | 
				
			||||||
  (remoteConfig: ConfigMapType): boolean => {
 | 
					 | 
				
			||||||
    return isRemoteConfigFlagEnabled(remoteConfig, 'desktop.deleteSync.send');
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Note: ts/util/stories is the other place this check is done
 | 
					// Note: ts/util/stories is the other place this check is done
 | 
				
			||||||
export const getStoriesEnabled = createSelector(
 | 
					export const getStoriesEnabled = createSelector(
 | 
				
			||||||
  getItems,
 | 
					  getItems,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,10 +41,8 @@ import {
 | 
				
			||||||
import { getHasStoriesSelector } from '../selectors/stories2';
 | 
					import { getHasStoriesSelector } from '../selectors/stories2';
 | 
				
			||||||
import { getIntl, getTheme, getUserACI } from '../selectors/user';
 | 
					import { getIntl, getTheme, getUserACI } from '../selectors/user';
 | 
				
			||||||
import { useItemsActions } from '../ducks/items';
 | 
					import { useItemsActions } from '../ducks/items';
 | 
				
			||||||
import {
 | 
					import { getLocalDeleteWarningShown } from '../selectors/items';
 | 
				
			||||||
  getDeleteSyncSendEnabled,
 | 
					import { getDeleteSyncSendEnabled } from '../selectors/items-extra';
 | 
				
			||||||
  getLocalDeleteWarningShown,
 | 
					 | 
				
			||||||
} from '../selectors/items';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type OwnProps = {
 | 
					export type OwnProps = {
 | 
				
			||||||
  id: string;
 | 
					  id: string;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,10 +17,8 @@ import {
 | 
				
			||||||
} from '../selectors/conversations';
 | 
					} from '../selectors/conversations';
 | 
				
			||||||
import { getDeleteMessagesProps } from '../selectors/globalModals';
 | 
					import { getDeleteMessagesProps } from '../selectors/globalModals';
 | 
				
			||||||
import { useItemsActions } from '../ducks/items';
 | 
					import { useItemsActions } from '../ducks/items';
 | 
				
			||||||
import {
 | 
					import { getLocalDeleteWarningShown } from '../selectors/items';
 | 
				
			||||||
  getLocalDeleteWarningShown,
 | 
					import { getDeleteSyncSendEnabled } from '../selectors/items-extra';
 | 
				
			||||||
  getDeleteSyncSendEnabled,
 | 
					 | 
				
			||||||
} from '../selectors/items';
 | 
					 | 
				
			||||||
import { LocalDeleteWarningModal } from '../../components/LocalDeleteWarningModal';
 | 
					import { LocalDeleteWarningModal } from '../../components/LocalDeleteWarningModal';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SmartDeleteMessagesModal = memo(
 | 
					export const SmartDeleteMessagesModal = memo(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -59,9 +59,9 @@ describe('filterAndSortConversations', () => {
 | 
				
			||||||
    check({
 | 
					    check({
 | 
				
			||||||
      searchTerm: '9876',
 | 
					      searchTerm: '9876',
 | 
				
			||||||
      input: [
 | 
					      input: [
 | 
				
			||||||
        { title: 'no' },
 | 
					        { title: 'no', e164: undefined },
 | 
				
			||||||
        { title: 'yes', e164: '+16505559876' },
 | 
					        { title: 'yes', e164: '+16505559876' },
 | 
				
			||||||
        { title: 'no' },
 | 
					        { title: 'no', e164: undefined },
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
      expected: [{ title: 'yes' }],
 | 
					      expected: [{ title: 'yes' }],
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,10 +7,13 @@ import { assert } from 'chai';
 | 
				
			||||||
import { type AciString, generateAci } from '../types/ServiceId';
 | 
					import { type AciString, generateAci } from '../types/ServiceId';
 | 
				
			||||||
import type { MessageAttributesType } from '../model-types';
 | 
					import type { MessageAttributesType } from '../model-types';
 | 
				
			||||||
import { SendStatus } from '../messages/MessageSendState';
 | 
					import { SendStatus } from '../messages/MessageSendState';
 | 
				
			||||||
import {
 | 
					import type {
 | 
				
			||||||
  type MessageReceiptAttributesType,
 | 
					  MessageReceiptAttributesType,
 | 
				
			||||||
  MessageReceiptType,
 | 
					  MessageReceiptType,
 | 
				
			||||||
 | 
					} from '../messageModifiers/MessageReceipts';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
  onReceipt,
 | 
					  onReceipt,
 | 
				
			||||||
 | 
					  messageReceiptTypeSchema,
 | 
				
			||||||
} from '../messageModifiers/MessageReceipts';
 | 
					} from '../messageModifiers/MessageReceipts';
 | 
				
			||||||
import { ReadStatus } from '../messages/MessageReadStatus';
 | 
					import { ReadStatus } from '../messages/MessageReadStatus';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,14 +34,16 @@ describe('MessageReceipts', () => {
 | 
				
			||||||
  ): MessageReceiptAttributesType {
 | 
					  ): MessageReceiptAttributesType {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      envelopeId: uuid(),
 | 
					      envelopeId: uuid(),
 | 
				
			||||||
 | 
					      syncTaskId: uuid(),
 | 
				
			||||||
 | 
					      receiptSync: {
 | 
				
			||||||
        messageSentAt,
 | 
					        messageSentAt,
 | 
				
			||||||
        receiptTimestamp: 1,
 | 
					        receiptTimestamp: 1,
 | 
				
			||||||
      removeFromMessageReceiverCache: () => null,
 | 
					 | 
				
			||||||
        sourceConversationId,
 | 
					        sourceConversationId,
 | 
				
			||||||
        sourceDevice: 1,
 | 
					        sourceDevice: 1,
 | 
				
			||||||
        sourceServiceId: generateAci(),
 | 
					        sourceServiceId: generateAci(),
 | 
				
			||||||
        type,
 | 
					        type,
 | 
				
			||||||
        wasSentEncrypted: true,
 | 
					        wasSentEncrypted: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  it('processes all receipts in a batch', async () => {
 | 
					  it('processes all receipts in a batch', async () => {
 | 
				
			||||||
| 
						 | 
					@ -78,10 +83,18 @@ describe('MessageReceipts', () => {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await Promise.all([
 | 
					    await Promise.all([
 | 
				
			||||||
      onReceipt(generateReceipt('aaaa', sentAt, MessageReceiptType.Delivery)),
 | 
					      onReceipt(
 | 
				
			||||||
      onReceipt(generateReceipt('bbbb', sentAt, MessageReceiptType.Delivery)),
 | 
					        generateReceipt('aaaa', sentAt, messageReceiptTypeSchema.enum.Delivery)
 | 
				
			||||||
      onReceipt(generateReceipt('cccc', sentAt, MessageReceiptType.Read)),
 | 
					      ),
 | 
				
			||||||
      onReceipt(generateReceipt('aaaa', sentAt, MessageReceiptType.Read)),
 | 
					      onReceipt(
 | 
				
			||||||
 | 
					        generateReceipt('bbbb', sentAt, messageReceiptTypeSchema.enum.Delivery)
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      onReceipt(
 | 
				
			||||||
 | 
					        generateReceipt('cccc', sentAt, messageReceiptTypeSchema.enum.Read)
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      onReceipt(
 | 
				
			||||||
 | 
					        generateReceipt('aaaa', sentAt, messageReceiptTypeSchema.enum.Read)
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const messageFromDatabase = await window.Signal.Data.getMessageById(id);
 | 
					    const messageFromDatabase = await window.Signal.Data.getMessageById(id);
 | 
				
			||||||
| 
						 | 
					@ -154,20 +167,48 @@ describe('MessageReceipts', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await Promise.all([
 | 
					    await Promise.all([
 | 
				
			||||||
      // send receipts for original message
 | 
					      // send receipts for original message
 | 
				
			||||||
      onReceipt(generateReceipt('aaaa', sentAt, MessageReceiptType.Delivery)),
 | 
					      onReceipt(
 | 
				
			||||||
      onReceipt(generateReceipt('bbbb', sentAt, MessageReceiptType.Delivery)),
 | 
					        generateReceipt('aaaa', sentAt, messageReceiptTypeSchema.enum.Delivery)
 | 
				
			||||||
      onReceipt(generateReceipt('cccc', sentAt, MessageReceiptType.Read)),
 | 
					      ),
 | 
				
			||||||
      onReceipt(generateReceipt('aaaa', sentAt, MessageReceiptType.Read)),
 | 
					      onReceipt(
 | 
				
			||||||
 | 
					        generateReceipt('bbbb', sentAt, messageReceiptTypeSchema.enum.Delivery)
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      onReceipt(
 | 
				
			||||||
 | 
					        generateReceipt('cccc', sentAt, messageReceiptTypeSchema.enum.Read)
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      onReceipt(
 | 
				
			||||||
 | 
					        generateReceipt('aaaa', sentAt, messageReceiptTypeSchema.enum.Read)
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // and send receipts for edited message
 | 
					      // and send receipts for edited message
 | 
				
			||||||
      onReceipt(
 | 
					      onReceipt(
 | 
				
			||||||
        generateReceipt('aaaa', editedSentAt, MessageReceiptType.Delivery)
 | 
					        generateReceipt(
 | 
				
			||||||
 | 
					          'aaaa',
 | 
				
			||||||
 | 
					          editedSentAt,
 | 
				
			||||||
 | 
					          messageReceiptTypeSchema.enum.Delivery
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      onReceipt(
 | 
					      onReceipt(
 | 
				
			||||||
        generateReceipt('bbbb', editedSentAt, MessageReceiptType.Delivery)
 | 
					        generateReceipt(
 | 
				
			||||||
 | 
					          'bbbb',
 | 
				
			||||||
 | 
					          editedSentAt,
 | 
				
			||||||
 | 
					          messageReceiptTypeSchema.enum.Delivery
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      onReceipt(
 | 
				
			||||||
 | 
					        generateReceipt(
 | 
				
			||||||
 | 
					          'cccc',
 | 
				
			||||||
 | 
					          editedSentAt,
 | 
				
			||||||
 | 
					          messageReceiptTypeSchema.enum.Read
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      onReceipt(
 | 
				
			||||||
 | 
					        generateReceipt(
 | 
				
			||||||
 | 
					          'bbbb',
 | 
				
			||||||
 | 
					          editedSentAt,
 | 
				
			||||||
 | 
					          messageReceiptTypeSchema.enum.Read
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      onReceipt(generateReceipt('cccc', editedSentAt, MessageReceiptType.Read)),
 | 
					 | 
				
			||||||
      onReceipt(generateReceipt('bbbb', editedSentAt, MessageReceiptType.Read)),
 | 
					 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const messageFromDatabase = await window.Signal.Data.getMessageById(id);
 | 
					    const messageFromDatabase = await window.Signal.Data.getMessageById(id);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,7 @@ import { v4 as generateUuid } from 'uuid';
 | 
				
			||||||
import dataInterface from '../../sql/Client';
 | 
					import dataInterface from '../../sql/Client';
 | 
				
			||||||
import { generateAci } from '../../types/ServiceId';
 | 
					import { generateAci } from '../../types/ServiceId';
 | 
				
			||||||
import { constantTimeEqual, getRandomBytes } from '../../Crypto';
 | 
					import { constantTimeEqual, getRandomBytes } from '../../Crypto';
 | 
				
			||||||
 | 
					import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const {
 | 
					const {
 | 
				
			||||||
  _getAllSentProtoMessageIds,
 | 
					  _getAllSentProtoMessageIds,
 | 
				
			||||||
| 
						 | 
					@ -148,7 +149,7 @@ describe('sql/sendLog', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assert.strictEqual(actual.timestamp, proto.timestamp);
 | 
					    assert.strictEqual(actual.timestamp, proto.timestamp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await removeMessage(id);
 | 
					    await removeMessage(id, { singleProtoJobQueue });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assert.lengthOf(await getAllSentProtos(), 0);
 | 
					    assert.lengthOf(await getAllSentProtos(), 0);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,7 @@ import { SignalProtocolStore } from '../../SignalProtocolStore';
 | 
				
			||||||
import type { ConversationModel } from '../../models/conversations';
 | 
					import type { ConversationModel } from '../../models/conversations';
 | 
				
			||||||
import * as KeyChangeListener from '../../textsecure/KeyChangeListener';
 | 
					import * as KeyChangeListener from '../../textsecure/KeyChangeListener';
 | 
				
			||||||
import * as Bytes from '../../Bytes';
 | 
					import * as Bytes from '../../Bytes';
 | 
				
			||||||
 | 
					import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('KeyChangeListener', () => {
 | 
					describe('KeyChangeListener', () => {
 | 
				
			||||||
  let oldNumberId: string | undefined;
 | 
					  let oldNumberId: string | undefined;
 | 
				
			||||||
| 
						 | 
					@ -69,6 +70,7 @@ describe('KeyChangeListener', () => {
 | 
				
			||||||
  afterEach(async () => {
 | 
					  afterEach(async () => {
 | 
				
			||||||
    await window.Signal.Data.removeMessagesInConversation(convo.id, {
 | 
					    await window.Signal.Data.removeMessagesInConversation(convo.id, {
 | 
				
			||||||
      logId: ourServiceIdWithKeyChange,
 | 
					      logId: ourServiceIdWithKeyChange,
 | 
				
			||||||
 | 
					      singleProtoJobQueue,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    await window.Signal.Data.removeConversation(convo.id);
 | 
					    await window.Signal.Data.removeConversation(convo.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -106,6 +108,7 @@ describe('KeyChangeListener', () => {
 | 
				
			||||||
    afterEach(async () => {
 | 
					    afterEach(async () => {
 | 
				
			||||||
      await window.Signal.Data.removeMessagesInConversation(groupConvo.id, {
 | 
					      await window.Signal.Data.removeMessagesInConversation(groupConvo.id, {
 | 
				
			||||||
        logId: ourServiceIdWithKeyChange,
 | 
					        logId: ourServiceIdWithKeyChange,
 | 
				
			||||||
 | 
					        singleProtoJobQueue,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      await window.Signal.Data.removeConversation(groupConvo.id);
 | 
					      await window.Signal.Data.removeConversation(groupConvo.id);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -137,6 +137,8 @@ import type {
 | 
				
			||||||
  DeleteForMeSyncEventData,
 | 
					  DeleteForMeSyncEventData,
 | 
				
			||||||
  DeleteForMeSyncTarget,
 | 
					  DeleteForMeSyncTarget,
 | 
				
			||||||
  ConversationToDelete,
 | 
					  ConversationToDelete,
 | 
				
			||||||
 | 
					  ViewSyncEventData,
 | 
				
			||||||
 | 
					  ReadSyncEventData,
 | 
				
			||||||
} from './messageReceiverEvents';
 | 
					} from './messageReceiverEvents';
 | 
				
			||||||
import * as log from '../logging/log';
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
import * as durations from '../util/durations';
 | 
					import * as durations from '../util/durations';
 | 
				
			||||||
| 
						 | 
					@ -1728,15 +1730,17 @@ export default class MessageReceiver
 | 
				
			||||||
    await this.dispatchAndWait(
 | 
					    await this.dispatchAndWait(
 | 
				
			||||||
      getEnvelopeId(envelope),
 | 
					      getEnvelopeId(envelope),
 | 
				
			||||||
      new DeliveryEvent(
 | 
					      new DeliveryEvent(
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
          envelopeId: envelope.id,
 | 
					 | 
				
			||||||
            timestamp: envelope.timestamp,
 | 
					            timestamp: envelope.timestamp,
 | 
				
			||||||
          envelopeTimestamp: envelope.timestamp,
 | 
					 | 
				
			||||||
            source: envelope.source,
 | 
					            source: envelope.source,
 | 
				
			||||||
            sourceServiceId: envelope.sourceServiceId,
 | 
					            sourceServiceId: envelope.sourceServiceId,
 | 
				
			||||||
            sourceDevice: envelope.sourceDevice,
 | 
					            sourceDevice: envelope.sourceDevice,
 | 
				
			||||||
            wasSentEncrypted: false,
 | 
					            wasSentEncrypted: false,
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        envelope.id,
 | 
				
			||||||
 | 
					        envelope.timestamp,
 | 
				
			||||||
        this.removeFromCache.bind(this, envelope)
 | 
					        this.removeFromCache.bind(this, envelope)
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
| 
						 | 
					@ -2907,22 +2911,22 @@ export default class MessageReceiver
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const logId = getEnvelopeId(envelope);
 | 
					    const logId = getEnvelopeId(envelope);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await Promise.all(
 | 
					    const receipts = receiptMessage.timestamp.map(rawTimestamp => ({
 | 
				
			||||||
      receiptMessage.timestamp.map(async rawTimestamp => {
 | 
					 | 
				
			||||||
        const ev = new EventClass(
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            envelopeId: envelope.id,
 | 
					 | 
				
			||||||
      timestamp: rawTimestamp?.toNumber(),
 | 
					      timestamp: rawTimestamp?.toNumber(),
 | 
				
			||||||
            envelopeTimestamp: envelope.timestamp,
 | 
					 | 
				
			||||||
      source: envelope.source,
 | 
					      source: envelope.source,
 | 
				
			||||||
      sourceServiceId: envelope.sourceServiceId,
 | 
					      sourceServiceId: envelope.sourceServiceId,
 | 
				
			||||||
      sourceDevice: envelope.sourceDevice,
 | 
					      sourceDevice: envelope.sourceDevice,
 | 
				
			||||||
            wasSentEncrypted: true,
 | 
					      wasSentEncrypted: true as const,
 | 
				
			||||||
          },
 | 
					    }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.dispatchAndWait(
 | 
				
			||||||
 | 
					      logId,
 | 
				
			||||||
 | 
					      new EventClass(
 | 
				
			||||||
 | 
					        receipts,
 | 
				
			||||||
 | 
					        envelope.id,
 | 
				
			||||||
 | 
					        envelope.timestamp,
 | 
				
			||||||
        this.removeFromCache.bind(this, envelope)
 | 
					        this.removeFromCache.bind(this, envelope)
 | 
				
			||||||
        );
 | 
					      )
 | 
				
			||||||
        await this.dispatchAndWait(logId, ev);
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3469,10 +3473,8 @@ export default class MessageReceiver
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    logUnexpectedUrgentValue(envelope, 'readSync');
 | 
					    logUnexpectedUrgentValue(envelope, 'readSync');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const results = [];
 | 
					    const reads = read.map(
 | 
				
			||||||
    for (const { timestamp, sender, senderAci } of read) {
 | 
					      ({ timestamp, sender, senderAci }): ReadSyncEventData => ({
 | 
				
			||||||
      const ev = new ReadSyncEvent(
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
        envelopeId: envelope.id,
 | 
					        envelopeId: envelope.id,
 | 
				
			||||||
        envelopeTimestamp: envelope.timestamp,
 | 
					        envelopeTimestamp: envelope.timestamp,
 | 
				
			||||||
        timestamp: timestamp?.toNumber(),
 | 
					        timestamp: timestamp?.toNumber(),
 | 
				
			||||||
| 
						 | 
					@ -3480,12 +3482,18 @@ export default class MessageReceiver
 | 
				
			||||||
        senderAci: senderAci
 | 
					        senderAci: senderAci
 | 
				
			||||||
          ? normalizeAci(senderAci, 'handleRead.senderAci')
 | 
					          ? normalizeAci(senderAci, 'handleRead.senderAci')
 | 
				
			||||||
          : undefined,
 | 
					          : undefined,
 | 
				
			||||||
        },
 | 
					      })
 | 
				
			||||||
        this.removeFromCache.bind(this, envelope)
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.dispatchAndWait(
 | 
				
			||||||
 | 
					      logId,
 | 
				
			||||||
 | 
					      new ReadSyncEvent(
 | 
				
			||||||
 | 
					        reads,
 | 
				
			||||||
 | 
					        envelope.id,
 | 
				
			||||||
 | 
					        envelope.timestamp,
 | 
				
			||||||
 | 
					        this.removeFromCache.bind(this, envelope)
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
      results.push(this.dispatchAndWait(logId, ev));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    await Promise.all(results);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async handleViewed(
 | 
					  private async handleViewed(
 | 
				
			||||||
| 
						 | 
					@ -3497,23 +3505,25 @@ export default class MessageReceiver
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    logUnexpectedUrgentValue(envelope, 'viewSync');
 | 
					    logUnexpectedUrgentValue(envelope, 'viewSync');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await Promise.all(
 | 
					    const views = viewed.map(
 | 
				
			||||||
      viewed.map(async ({ timestamp, senderE164, senderAci }) => {
 | 
					      ({ timestamp, senderE164, senderAci }): ViewSyncEventData => ({
 | 
				
			||||||
        const ev = new ViewSyncEvent(
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            envelopeId: envelope.id,
 | 
					 | 
				
			||||||
            envelopeTimestamp: envelope.timestamp,
 | 
					 | 
				
			||||||
        timestamp: timestamp?.toNumber(),
 | 
					        timestamp: timestamp?.toNumber(),
 | 
				
			||||||
        senderE164: dropNull(senderE164),
 | 
					        senderE164: dropNull(senderE164),
 | 
				
			||||||
        senderAci: senderAci
 | 
					        senderAci: senderAci
 | 
				
			||||||
          ? normalizeAci(senderAci, 'handleViewed.senderAci')
 | 
					          ? normalizeAci(senderAci, 'handleViewed.senderAci')
 | 
				
			||||||
          : undefined,
 | 
					          : undefined,
 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          this.removeFromCache.bind(this, envelope)
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        await this.dispatchAndWait(logId, ev);
 | 
					 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.dispatchAndWait(
 | 
				
			||||||
 | 
					      logId,
 | 
				
			||||||
 | 
					      new ViewSyncEvent(
 | 
				
			||||||
 | 
					        views,
 | 
				
			||||||
 | 
					        envelope.id,
 | 
				
			||||||
 | 
					        envelope.timestamp,
 | 
				
			||||||
 | 
					        this.removeFromCache.bind(this, envelope)
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async handleCallEvent(
 | 
					  private async handleCallEvent(
 | 
				
			||||||
| 
						 | 
					@ -3663,7 +3673,19 @@ export default class MessageReceiver
 | 
				
			||||||
                ? processConversationToDelete(item.conversation, logId)
 | 
					                ? processConversationToDelete(item.conversation, logId)
 | 
				
			||||||
                : undefined;
 | 
					                : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              if (messages?.length && conversation) {
 | 
					              if (!conversation) {
 | 
				
			||||||
 | 
					                log.warn(
 | 
				
			||||||
 | 
					                  `${logId}/handleDeleteForMeSync/messageDeletes: No target conversation`
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                return undefined;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              if (!messages?.length) {
 | 
				
			||||||
 | 
					                log.warn(
 | 
				
			||||||
 | 
					                  `${logId}/handleDeleteForMeSync/messageDeletes: No target messages`
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                return undefined;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              // We want each message in its own task
 | 
					              // We want each message in its own task
 | 
				
			||||||
              return messages.map(innerItem => {
 | 
					              return messages.map(innerItem => {
 | 
				
			||||||
                return {
 | 
					                return {
 | 
				
			||||||
| 
						 | 
					@ -3673,9 +3695,6 @@ export default class MessageReceiver
 | 
				
			||||||
                  timestamp,
 | 
					                  timestamp,
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
              });
 | 
					              });
 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              return undefined;
 | 
					 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
            .filter(isNotNil);
 | 
					            .filter(isNotNil);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3692,7 +3711,19 @@ export default class MessageReceiver
 | 
				
			||||||
                ? processConversationToDelete(item.conversation, logId)
 | 
					                ? processConversationToDelete(item.conversation, logId)
 | 
				
			||||||
                : undefined;
 | 
					                : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              if (mostRecentMessages?.length && conversation) {
 | 
					              if (!conversation) {
 | 
				
			||||||
 | 
					                log.warn(
 | 
				
			||||||
 | 
					                  `${logId}/handleDeleteForMeSync/conversationDeletes: No target conversation`
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                return undefined;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              if (!mostRecentMessages?.length) {
 | 
				
			||||||
 | 
					                log.warn(
 | 
				
			||||||
 | 
					                  `${logId}/handleDeleteForMeSync/conversationDeletes: No target messages`
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                return undefined;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              return {
 | 
					              return {
 | 
				
			||||||
                type: 'delete-conversation' as const,
 | 
					                type: 'delete-conversation' as const,
 | 
				
			||||||
                conversation,
 | 
					                conversation,
 | 
				
			||||||
| 
						 | 
					@ -3700,9 +3731,6 @@ export default class MessageReceiver
 | 
				
			||||||
                mostRecentMessages,
 | 
					                mostRecentMessages,
 | 
				
			||||||
                timestamp,
 | 
					                timestamp,
 | 
				
			||||||
              };
 | 
					              };
 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              return undefined;
 | 
					 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
            .filter(isNotNil);
 | 
					            .filter(isNotNil);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3716,15 +3744,18 @@ export default class MessageReceiver
 | 
				
			||||||
                ? processConversationToDelete(item.conversation, logId)
 | 
					                ? processConversationToDelete(item.conversation, logId)
 | 
				
			||||||
                : undefined;
 | 
					                : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              if (conversation) {
 | 
					              if (!conversation) {
 | 
				
			||||||
 | 
					                log.warn(
 | 
				
			||||||
 | 
					                  `${logId}/handleDeleteForMeSync/localOnlyConversationDeletes: No target conversation`
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                return undefined;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              return {
 | 
					              return {
 | 
				
			||||||
                type: 'delete-local-conversation' as const,
 | 
					                type: 'delete-local-conversation' as const,
 | 
				
			||||||
                conversation,
 | 
					                conversation,
 | 
				
			||||||
                timestamp,
 | 
					                timestamp,
 | 
				
			||||||
              };
 | 
					              };
 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              return undefined;
 | 
					 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
            .filter(isNotNil);
 | 
					            .filter(isNotNil);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3969,16 +4000,33 @@ function processMessageToDelete(
 | 
				
			||||||
    return undefined;
 | 
					    return undefined;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (target.authorAci) {
 | 
					  const { authorServiceId } = target;
 | 
				
			||||||
 | 
					  if (authorServiceId) {
 | 
				
			||||||
 | 
					    if (isAciString(authorServiceId)) {
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
        type: 'aci' as const,
 | 
					        type: 'aci' as const,
 | 
				
			||||||
        authorAci: normalizeAci(
 | 
					        authorAci: normalizeAci(
 | 
				
			||||||
        target.authorAci,
 | 
					          authorServiceId,
 | 
				
			||||||
        `${logId}/processMessageToDelete`
 | 
					          `${logId}/processMessageToDelete/aci`
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        sentAt,
 | 
					        sentAt,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (isPniString(authorServiceId)) {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        type: 'pni' as const,
 | 
				
			||||||
 | 
					        authorPni: normalizePni(
 | 
				
			||||||
 | 
					          authorServiceId,
 | 
				
			||||||
 | 
					          `${logId}/processMessageToDelete/pni`
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        sentAt,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    log.error(
 | 
				
			||||||
 | 
					      `${logId}/processMessageToDelete: invalid authorServiceId, Dropping AddressableMessage.`
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    return undefined;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  if (target.authorE164) {
 | 
					  if (target.authorE164) {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      type: 'e164' as const,
 | 
					      type: 'e164' as const,
 | 
				
			||||||
| 
						 | 
					@ -3997,14 +4045,26 @@ function processConversationToDelete(
 | 
				
			||||||
  target: Proto.SyncMessage.DeleteForMe.IConversationIdentifier,
 | 
					  target: Proto.SyncMessage.DeleteForMe.IConversationIdentifier,
 | 
				
			||||||
  logId: string
 | 
					  logId: string
 | 
				
			||||||
): ConversationToDelete | undefined {
 | 
					): ConversationToDelete | undefined {
 | 
				
			||||||
  const { threadAci, threadGroupId, threadE164 } = target;
 | 
					  const { threadServiceId, threadGroupId, threadE164 } = target;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (threadAci) {
 | 
					  if (threadServiceId) {
 | 
				
			||||||
 | 
					    if (isAciString(threadServiceId)) {
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
        type: 'aci' as const,
 | 
					        type: 'aci' as const,
 | 
				
			||||||
      aci: normalizeAci(threadAci, `${logId}/threadAci`),
 | 
					        aci: normalizeAci(threadServiceId, `${logId}/aci`),
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (isPniString(threadServiceId)) {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        type: 'pni' as const,
 | 
				
			||||||
 | 
					        pni: normalizePni(threadServiceId, `${logId}/pni`),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    log.error(
 | 
				
			||||||
 | 
					      `${logId}/processConversationToDelete: Invalid threadServiceId, dropping ConversationIdentifier.`
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    return undefined;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  if (threadGroupId) {
 | 
					  if (threadGroupId) {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      type: 'group' as const,
 | 
					      type: 'group' as const,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -89,6 +89,14 @@ import type {
 | 
				
			||||||
  MessageToDelete,
 | 
					  MessageToDelete,
 | 
				
			||||||
} from './messageReceiverEvents';
 | 
					} from './messageReceiverEvents';
 | 
				
			||||||
import { getConversationFromTarget } from '../util/deleteForMe';
 | 
					import { getConversationFromTarget } from '../util/deleteForMe';
 | 
				
			||||||
 | 
					import type { CallDetails } from '../types/CallDisposition';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AdhocCallStatus,
 | 
				
			||||||
 | 
					  DirectCallStatus,
 | 
				
			||||||
 | 
					  GroupCallStatus,
 | 
				
			||||||
 | 
					} from '../types/CallDisposition';
 | 
				
			||||||
 | 
					import { getProtoForCallHistory } from '../util/callDisposition';
 | 
				
			||||||
 | 
					import { CallMode } from '../types/Calling';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type SendMetadataType = {
 | 
					export type SendMetadataType = {
 | 
				
			||||||
  [serviceId: ServiceIdString]: {
 | 
					  [serviceId: ServiceIdString]: {
 | 
				
			||||||
| 
						 | 
					@ -1567,6 +1575,71 @@ export default class MessageSender {
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static getClearCallHistoryMessage(timestamp: number): SingleProtoJobData {
 | 
				
			||||||
 | 
					    const ourAci = window.textsecure.storage.user.getCheckedAci();
 | 
				
			||||||
 | 
					    const callLogEvent = new Proto.SyncMessage.CallLogEvent({
 | 
				
			||||||
 | 
					      type: Proto.SyncMessage.CallLogEvent.Type.CLEAR,
 | 
				
			||||||
 | 
					      timestamp: Long.fromNumber(timestamp),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const syncMessage = MessageSender.createSyncMessage();
 | 
				
			||||||
 | 
					    syncMessage.callLogEvent = callLogEvent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const contentMessage = new Proto.Content();
 | 
				
			||||||
 | 
					    contentMessage.syncMessage = syncMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      contentHint: ContentHint.RESENDABLE,
 | 
				
			||||||
 | 
					      serviceId: ourAci,
 | 
				
			||||||
 | 
					      isSyncMessage: true,
 | 
				
			||||||
 | 
					      protoBase64: Bytes.toBase64(
 | 
				
			||||||
 | 
					        Proto.Content.encode(contentMessage).finish()
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      type: 'callLogEventSync',
 | 
				
			||||||
 | 
					      urgent: false,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static getDeleteCallEvent(callDetails: CallDetails): SingleProtoJobData {
 | 
				
			||||||
 | 
					    const ourAci = window.textsecure.storage.user.getCheckedAci();
 | 
				
			||||||
 | 
					    const { mode } = callDetails;
 | 
				
			||||||
 | 
					    let status;
 | 
				
			||||||
 | 
					    if (mode === CallMode.Adhoc) {
 | 
				
			||||||
 | 
					      status = AdhocCallStatus.Deleted;
 | 
				
			||||||
 | 
					    } else if (mode === CallMode.Direct) {
 | 
				
			||||||
 | 
					      status = DirectCallStatus.Deleted;
 | 
				
			||||||
 | 
					    } else if (mode === CallMode.Group) {
 | 
				
			||||||
 | 
					      status = GroupCallStatus.Deleted;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      throw missingCaseError(mode);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const callEvent = getProtoForCallHistory({
 | 
				
			||||||
 | 
					      ...callDetails,
 | 
				
			||||||
 | 
					      status,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const syncMessage = MessageSender.createSyncMessage();
 | 
				
			||||||
 | 
					    syncMessage.callEvent = callEvent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const contentMessage = new Proto.Content();
 | 
				
			||||||
 | 
					    contentMessage.syncMessage = syncMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      contentHint: ContentHint.RESENDABLE,
 | 
				
			||||||
 | 
					      serviceId: ourAci,
 | 
				
			||||||
 | 
					      isSyncMessage: true,
 | 
				
			||||||
 | 
					      protoBase64: Bytes.toBase64(
 | 
				
			||||||
 | 
					        Proto.Content.encode(contentMessage).finish()
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      type: 'callLogEventSync',
 | 
				
			||||||
 | 
					      urgent: false,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async syncReadMessages(
 | 
					  async syncReadMessages(
 | 
				
			||||||
    reads: ReadonlyArray<{
 | 
					    reads: ReadonlyArray<{
 | 
				
			||||||
      senderAci?: AciString;
 | 
					      senderAci?: AciString;
 | 
				
			||||||
| 
						 | 
					@ -2353,9 +2426,11 @@ function toAddressableMessage(message: MessageToDelete) {
 | 
				
			||||||
  targetMessage.sentTimestamp = Long.fromNumber(message.sentAt);
 | 
					  targetMessage.sentTimestamp = Long.fromNumber(message.sentAt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (message.type === 'aci') {
 | 
					  if (message.type === 'aci') {
 | 
				
			||||||
    targetMessage.authorAci = message.authorAci;
 | 
					    targetMessage.authorServiceId = message.authorAci;
 | 
				
			||||||
  } else if (message.type === 'e164') {
 | 
					  } else if (message.type === 'e164') {
 | 
				
			||||||
    targetMessage.authorE164 = message.authorE164;
 | 
					    targetMessage.authorE164 = message.authorE164;
 | 
				
			||||||
 | 
					  } else if (message.type === 'pni') {
 | 
				
			||||||
 | 
					    targetMessage.authorServiceId = message.authorPni;
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    throw missingCaseError(message);
 | 
					    throw missingCaseError(message);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -2368,7 +2443,9 @@ function toConversationIdentifier(conversation: ConversationToDelete) {
 | 
				
			||||||
    new Proto.SyncMessage.DeleteForMe.ConversationIdentifier();
 | 
					    new Proto.SyncMessage.DeleteForMe.ConversationIdentifier();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (conversation.type === 'aci') {
 | 
					  if (conversation.type === 'aci') {
 | 
				
			||||||
    targetConversation.threadAci = conversation.aci;
 | 
					    targetConversation.threadServiceId = conversation.aci;
 | 
				
			||||||
 | 
					  } else if (conversation.type === 'pni') {
 | 
				
			||||||
 | 
					    targetConversation.threadServiceId = conversation.pni;
 | 
				
			||||||
  } else if (conversation.type === 'group') {
 | 
					  } else if (conversation.type === 'group') {
 | 
				
			||||||
    targetConversation.threadGroupId = Bytes.fromBase64(conversation.groupId);
 | 
					    targetConversation.threadGroupId = Bytes.fromBase64(conversation.groupId);
 | 
				
			||||||
  } else if (conversation.type === 'e164') {
 | 
					  } else if (conversation.type === 'e164') {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -703,8 +703,12 @@ export type WebAPIConnectType = {
 | 
				
			||||||
  connect: (options: WebAPIConnectOptionsType) => WebAPIType;
 | 
					  connect: (options: WebAPIConnectOptionsType) => WebAPIType;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type CapabilitiesType = Record<string, never>;
 | 
					export type CapabilitiesType = {
 | 
				
			||||||
export type CapabilitiesUploadType = Record<string, never>;
 | 
					  deleteSync: boolean;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export type CapabilitiesUploadType = {
 | 
				
			||||||
 | 
					  deleteSync: true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type StickerPackManifestType = Uint8Array;
 | 
					type StickerPackManifestType = Uint8Array;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,11 @@ import type { PublicKey } from '@signalapp/libsignal-client';
 | 
				
			||||||
import { z } from 'zod';
 | 
					import { z } from 'zod';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { SignalService as Proto } from '../protobuf';
 | 
					import type { SignalService as Proto } from '../protobuf';
 | 
				
			||||||
import type { ServiceIdString, AciString } from '../types/ServiceId';
 | 
					import {
 | 
				
			||||||
 | 
					  type ServiceIdString,
 | 
				
			||||||
 | 
					  type AciString,
 | 
				
			||||||
 | 
					  isPniString,
 | 
				
			||||||
 | 
					} from '../types/ServiceId';
 | 
				
			||||||
import type { StoryDistributionIdString } from '../types/StoryDistributionId';
 | 
					import type { StoryDistributionIdString } from '../types/StoryDistributionId';
 | 
				
			||||||
import type {
 | 
					import type {
 | 
				
			||||||
  ProcessedEnvelope,
 | 
					  ProcessedEnvelope,
 | 
				
			||||||
| 
						 | 
					@ -93,7 +97,6 @@ export class EnvelopeUnsealedEvent extends Event {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Emitted when we queue previously-decrypted events from the cache
 | 
					 | 
				
			||||||
export class EnvelopeQueuedEvent extends Event {
 | 
					export class EnvelopeQueuedEvent extends Event {
 | 
				
			||||||
  constructor(public readonly envelope: ProcessedEnvelope) {
 | 
					  constructor(public readonly envelope: ProcessedEnvelope) {
 | 
				
			||||||
    super('envelopeQueued');
 | 
					    super('envelopeQueued');
 | 
				
			||||||
| 
						 | 
					@ -113,9 +116,7 @@ export class ConfirmableEvent extends Event {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type DeliveryEventData = Readonly<{
 | 
					export type DeliveryEventData = Readonly<{
 | 
				
			||||||
  envelopeId: string;
 | 
					 | 
				
			||||||
  timestamp: number;
 | 
					  timestamp: number;
 | 
				
			||||||
  envelopeTimestamp: number;
 | 
					 | 
				
			||||||
  source?: string;
 | 
					  source?: string;
 | 
				
			||||||
  sourceServiceId?: ServiceIdString;
 | 
					  sourceServiceId?: ServiceIdString;
 | 
				
			||||||
  sourceDevice?: number;
 | 
					  sourceDevice?: number;
 | 
				
			||||||
| 
						 | 
					@ -124,7 +125,9 @@ export type DeliveryEventData = Readonly<{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class DeliveryEvent extends ConfirmableEvent {
 | 
					export class DeliveryEvent extends ConfirmableEvent {
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    public readonly deliveryReceipt: DeliveryEventData,
 | 
					    public readonly deliveryReceipts: ReadonlyArray<DeliveryEventData>,
 | 
				
			||||||
 | 
					    public readonly envelopeId: string,
 | 
				
			||||||
 | 
					    public readonly envelopeTimestamp: number,
 | 
				
			||||||
    confirm: ConfirmCallback
 | 
					    confirm: ConfirmCallback
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    super('delivery', confirm);
 | 
					    super('delivery', confirm);
 | 
				
			||||||
| 
						 | 
					@ -245,9 +248,7 @@ export class MessageEvent extends ConfirmableEvent {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ReadOrViewEventData = Readonly<{
 | 
					export type ReadOrViewEventData = Readonly<{
 | 
				
			||||||
  envelopeId: string;
 | 
					 | 
				
			||||||
  timestamp: number;
 | 
					  timestamp: number;
 | 
				
			||||||
  envelopeTimestamp: number;
 | 
					 | 
				
			||||||
  source?: string;
 | 
					  source?: string;
 | 
				
			||||||
  sourceServiceId?: ServiceIdString;
 | 
					  sourceServiceId?: ServiceIdString;
 | 
				
			||||||
  sourceDevice?: number;
 | 
					  sourceDevice?: number;
 | 
				
			||||||
| 
						 | 
					@ -256,7 +257,9 @@ export type ReadOrViewEventData = Readonly<{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ReadEvent extends ConfirmableEvent {
 | 
					export class ReadEvent extends ConfirmableEvent {
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    public readonly receipt: ReadOrViewEventData,
 | 
					    public readonly receipts: ReadonlyArray<ReadOrViewEventData>,
 | 
				
			||||||
 | 
					    public readonly envelopeId: string,
 | 
				
			||||||
 | 
					    public readonly envelopeTimestamp: number,
 | 
				
			||||||
    confirm: ConfirmCallback
 | 
					    confirm: ConfirmCallback
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    super('read', confirm);
 | 
					    super('read', confirm);
 | 
				
			||||||
| 
						 | 
					@ -265,7 +268,9 @@ export class ReadEvent extends ConfirmableEvent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ViewEvent extends ConfirmableEvent {
 | 
					export class ViewEvent extends ConfirmableEvent {
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    public readonly receipt: ReadOrViewEventData,
 | 
					    public readonly receipts: ReadonlyArray<ReadOrViewEventData>,
 | 
				
			||||||
 | 
					    public readonly envelopeId: string,
 | 
				
			||||||
 | 
					    public readonly envelopeTimestamp: number,
 | 
				
			||||||
    confirm: ConfirmCallback
 | 
					    confirm: ConfirmCallback
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    super('view', confirm);
 | 
					    super('view', confirm);
 | 
				
			||||||
| 
						 | 
					@ -405,7 +410,9 @@ export type ReadSyncEventData = Readonly<{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ReadSyncEvent extends ConfirmableEvent {
 | 
					export class ReadSyncEvent extends ConfirmableEvent {
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    public readonly read: ReadSyncEventData,
 | 
					    public readonly reads: ReadonlyArray<ReadSyncEventData>,
 | 
				
			||||||
 | 
					    public readonly envelopeId: string,
 | 
				
			||||||
 | 
					    public readonly envelopeTimestamp: number,
 | 
				
			||||||
    confirm: ConfirmCallback
 | 
					    confirm: ConfirmCallback
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    super('readSync', confirm);
 | 
					    super('readSync', confirm);
 | 
				
			||||||
| 
						 | 
					@ -413,16 +420,16 @@ export class ReadSyncEvent extends ConfirmableEvent {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ViewSyncEventData = Readonly<{
 | 
					export type ViewSyncEventData = Readonly<{
 | 
				
			||||||
  envelopeId: string;
 | 
					 | 
				
			||||||
  timestamp?: number;
 | 
					  timestamp?: number;
 | 
				
			||||||
  envelopeTimestamp: number;
 | 
					 | 
				
			||||||
  senderE164?: string;
 | 
					  senderE164?: string;
 | 
				
			||||||
  senderAci?: AciString;
 | 
					  senderAci?: AciString;
 | 
				
			||||||
}>;
 | 
					}>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ViewSyncEvent extends ConfirmableEvent {
 | 
					export class ViewSyncEvent extends ConfirmableEvent {
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    public readonly view: ViewSyncEventData,
 | 
					    public readonly views: ReadonlyArray<ViewSyncEventData>,
 | 
				
			||||||
 | 
					    public readonly envelopeId: string,
 | 
				
			||||||
 | 
					    public readonly envelopeTimestamp: number,
 | 
				
			||||||
    confirm: ConfirmCallback
 | 
					    confirm: ConfirmCallback
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    super('viewSync', confirm);
 | 
					    super('viewSync', confirm);
 | 
				
			||||||
| 
						 | 
					@ -470,15 +477,16 @@ const messageToDeleteSchema = z.union([
 | 
				
			||||||
    authorE164: z.string(),
 | 
					    authorE164: z.string(),
 | 
				
			||||||
    sentAt: z.number(),
 | 
					    sentAt: z.number(),
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
 | 
					  z.object({
 | 
				
			||||||
 | 
					    type: z.literal('pni').readonly(),
 | 
				
			||||||
 | 
					    authorPni: z.string().refine(isPniString),
 | 
				
			||||||
 | 
					    sentAt: z.number(),
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
]);
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type MessageToDelete = z.infer<typeof messageToDeleteSchema>;
 | 
					export type MessageToDelete = z.infer<typeof messageToDeleteSchema>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const conversationToDeleteSchema = z.union([
 | 
					const conversationToDeleteSchema = z.union([
 | 
				
			||||||
  z.object({
 | 
					 | 
				
			||||||
    type: z.literal('group').readonly(),
 | 
					 | 
				
			||||||
    groupId: z.string(),
 | 
					 | 
				
			||||||
  }),
 | 
					 | 
				
			||||||
  z.object({
 | 
					  z.object({
 | 
				
			||||||
    type: z.literal('aci').readonly(),
 | 
					    type: z.literal('aci').readonly(),
 | 
				
			||||||
    aci: z.string().refine(isAciString),
 | 
					    aci: z.string().refine(isAciString),
 | 
				
			||||||
| 
						 | 
					@ -487,6 +495,14 @@ const conversationToDeleteSchema = z.union([
 | 
				
			||||||
    type: z.literal('e164').readonly(),
 | 
					    type: z.literal('e164').readonly(),
 | 
				
			||||||
    e164: z.string(),
 | 
					    e164: z.string(),
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
 | 
					  z.object({
 | 
				
			||||||
 | 
					    type: z.literal('group').readonly(),
 | 
				
			||||||
 | 
					    groupId: z.string(),
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					  z.object({
 | 
				
			||||||
 | 
					    type: z.literal('pni').readonly(),
 | 
				
			||||||
 | 
					    pni: z.string().refine(isPniString),
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
]);
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ConversationToDelete = z.infer<typeof conversationToDeleteSchema>;
 | 
					export type ConversationToDelete = z.infer<typeof conversationToDeleteSchema>;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -280,7 +280,7 @@ function shouldSyncStatus(callStatus: CallStatus) {
 | 
				
			||||||
  return statusToProto[callStatus] != null;
 | 
					  return statusToProto[callStatus] != null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getProtoForCallHistory(
 | 
					export function getProtoForCallHistory(
 | 
				
			||||||
  callHistory: CallHistoryDetails
 | 
					  callHistory: CallHistoryDetails
 | 
				
			||||||
): Proto.SyncMessage.ICallEvent | null {
 | 
					): Proto.SyncMessage.ICallEvent | null {
 | 
				
			||||||
  const event = statusToProto[callHistory.status];
 | 
					  const event = statusToProto[callHistory.status];
 | 
				
			||||||
| 
						 | 
					@ -1026,7 +1026,10 @@ async function saveCallHistory({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (isDeleted) {
 | 
					  if (isDeleted) {
 | 
				
			||||||
    if (prevMessage != null) {
 | 
					    if (prevMessage != null) {
 | 
				
			||||||
      await window.Signal.Data.removeMessage(prevMessage.id);
 | 
					      await window.Signal.Data.removeMessage(prevMessage.id, {
 | 
				
			||||||
 | 
					        fromSync: true,
 | 
				
			||||||
 | 
					        singleProtoJobQueue,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return callHistory;
 | 
					    return callHistory;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -1209,32 +1212,10 @@ export async function clearCallHistoryDataAndSync(): Promise<void> {
 | 
				
			||||||
      window.MessageCache.__DEPRECATED$unregister(messageId);
 | 
					      window.MessageCache.__DEPRECATED$unregister(messageId);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const ourAci = window.textsecure.storage.user.getCheckedAci();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const callLogEvent = new Proto.SyncMessage.CallLogEvent({
 | 
					 | 
				
			||||||
      type: Proto.SyncMessage.CallLogEvent.Type.CLEAR,
 | 
					 | 
				
			||||||
      timestamp: Long.fromNumber(timestamp),
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const syncMessage = MessageSender.createSyncMessage();
 | 
					 | 
				
			||||||
    syncMessage.callLogEvent = callLogEvent;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const contentMessage = new Proto.Content();
 | 
					 | 
				
			||||||
    contentMessage.syncMessage = syncMessage;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    log.info('clearCallHistory: Queueing sync message');
 | 
					    log.info('clearCallHistory: Queueing sync message');
 | 
				
			||||||
    await singleProtoJobQueue.add({
 | 
					    await singleProtoJobQueue.add(
 | 
				
			||||||
      contentHint: ContentHint.RESENDABLE,
 | 
					      MessageSender.getClearCallHistoryMessage(timestamp)
 | 
				
			||||||
      serviceId: ourAci,
 | 
					    );
 | 
				
			||||||
      isSyncMessage: true,
 | 
					 | 
				
			||||||
      protoBase64: Bytes.toBase64(
 | 
					 | 
				
			||||||
        Proto.Content.encode(contentMessage).finish()
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      type: 'callLogEventSync',
 | 
					 | 
				
			||||||
      urgent: false,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    log.error('clearCallHistory: Failed to clear call history', error);
 | 
					    log.error('clearCallHistory: Failed to clear call history', error);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,17 +1,67 @@
 | 
				
			||||||
// 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 PQueue from 'p-queue';
 | 
				
			||||||
 | 
					import { batch } from 'react-redux';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { MessageAttributesType } from '../model-types.d';
 | 
					import type { MessageAttributesType } from '../model-types.d';
 | 
				
			||||||
import { deletePackReference } from '../types/Stickers';
 | 
					import { deletePackReference } from '../types/Stickers';
 | 
				
			||||||
import { isStory } from '../messages/helpers';
 | 
					import { isStory } from '../messages/helpers';
 | 
				
			||||||
import { isDirectConversation } from './whatTypeOfConversation';
 | 
					import { isDirectConversation } from './whatTypeOfConversation';
 | 
				
			||||||
import * as log from '../logging/log';
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
 | 
					import { getCallHistorySelector } from '../state/selectors/callHistory';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DirectCallStatus,
 | 
				
			||||||
 | 
					  GroupCallStatus,
 | 
				
			||||||
 | 
					  AdhocCallStatus,
 | 
				
			||||||
 | 
					} from '../types/CallDisposition';
 | 
				
			||||||
 | 
					import { getMessageIdForLogging } from './idForLogging';
 | 
				
			||||||
 | 
					import type { SingleProtoJobQueue } from '../jobs/singleProtoJobQueue';
 | 
				
			||||||
 | 
					import { MINUTE } from './durations';
 | 
				
			||||||
 | 
					import { drop } from './drop';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function cleanupMessage(
 | 
					export async function cleanupMessages(
 | 
				
			||||||
  message: MessageAttributesType
 | 
					  messages: ReadonlyArray<MessageAttributesType>,
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    fromSync,
 | 
				
			||||||
 | 
					    markCallHistoryDeleted,
 | 
				
			||||||
 | 
					    singleProtoJobQueue,
 | 
				
			||||||
 | 
					  }: {
 | 
				
			||||||
 | 
					    fromSync?: boolean;
 | 
				
			||||||
 | 
					    markCallHistoryDeleted: (callId: string) => Promise<void>;
 | 
				
			||||||
 | 
					    singleProtoJobQueue: SingleProtoJobQueue;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
): Promise<void> {
 | 
					): Promise<void> {
 | 
				
			||||||
  cleanupMessageFromMemory(message);
 | 
					  // First, handle any calls that need to be deleted
 | 
				
			||||||
 | 
					  const inMemoryQueue = new PQueue({ concurrency: 3, timeout: MINUTE * 30 });
 | 
				
			||||||
 | 
					  drop(
 | 
				
			||||||
 | 
					    inMemoryQueue.addAll(
 | 
				
			||||||
 | 
					      messages.map((message: MessageAttributesType) => async () => {
 | 
				
			||||||
 | 
					        await maybeDeleteCall(message, {
 | 
				
			||||||
 | 
					          fromSync,
 | 
				
			||||||
 | 
					          markCallHistoryDeleted,
 | 
				
			||||||
 | 
					          singleProtoJobQueue,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  await inMemoryQueue.onIdle();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Then, remove messages from memory, so we can batch the updates in redux
 | 
				
			||||||
 | 
					  batch(() => {
 | 
				
			||||||
 | 
					    messages.forEach(message => cleanupMessageFromMemory(message));
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Then, handle any asynchronous actions (e.g. deleting data from disk)
 | 
				
			||||||
 | 
					  const unloadedQueue = new PQueue({ concurrency: 3, timeout: MINUTE * 30 });
 | 
				
			||||||
 | 
					  drop(
 | 
				
			||||||
 | 
					    unloadedQueue.addAll(
 | 
				
			||||||
 | 
					      messages.map((message: MessageAttributesType) => async () => {
 | 
				
			||||||
        await deleteMessageData(message);
 | 
					        await deleteMessageData(message);
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  await unloadedQueue.onIdle();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** Removes a message from redux caches & backbone, but does NOT delete files on disk,
 | 
					/** Removes a message from redux caches & backbone, but does NOT delete files on disk,
 | 
				
			||||||
| 
						 | 
					@ -122,3 +172,50 @@ export async function deleteMessageData(
 | 
				
			||||||
    await deletePackReference(message.id, packId);
 | 
					    await deletePackReference(message.id, packId);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function maybeDeleteCall(
 | 
				
			||||||
 | 
					  message: MessageAttributesType,
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    fromSync,
 | 
				
			||||||
 | 
					    markCallHistoryDeleted,
 | 
				
			||||||
 | 
					    singleProtoJobQueue,
 | 
				
			||||||
 | 
					  }: {
 | 
				
			||||||
 | 
					    fromSync?: boolean;
 | 
				
			||||||
 | 
					    markCallHistoryDeleted: (callId: string) => Promise<void>;
 | 
				
			||||||
 | 
					    singleProtoJobQueue: SingleProtoJobQueue;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					): Promise<void> {
 | 
				
			||||||
 | 
					  const { callId } = message;
 | 
				
			||||||
 | 
					  const logId = `maybeDeleteCall(${getMessageIdForLogging(message)})`;
 | 
				
			||||||
 | 
					  if (!callId) {
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const callHistory = getCallHistorySelector(window.reduxStore.getState())(
 | 
				
			||||||
 | 
					    callId
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  if (!callHistory) {
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (
 | 
				
			||||||
 | 
					    callHistory.status === DirectCallStatus.Pending ||
 | 
				
			||||||
 | 
					    callHistory.status === GroupCallStatus.Joined ||
 | 
				
			||||||
 | 
					    callHistory.status === GroupCallStatus.OutgoingRing ||
 | 
				
			||||||
 | 
					    callHistory.status === GroupCallStatus.Ringing ||
 | 
				
			||||||
 | 
					    callHistory.status === AdhocCallStatus.Pending
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    log.warn(
 | 
				
			||||||
 | 
					      `${logId}: Call status is ${callHistory.status}; not deleting from Call Tab`
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!fromSync) {
 | 
				
			||||||
 | 
					    await singleProtoJobQueue.add(
 | 
				
			||||||
 | 
					      window.textsecure.MessageSender.getDeleteCallEvent(callHistory)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  await markCallHistoryDeleted(callId);
 | 
				
			||||||
 | 
					  window.reduxActions.callHistory.removeCallHistory(callId);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,7 +24,9 @@ import type {
 | 
				
			||||||
  ConversationToDelete,
 | 
					  ConversationToDelete,
 | 
				
			||||||
  MessageToDelete,
 | 
					  MessageToDelete,
 | 
				
			||||||
} from '../textsecure/messageReceiverEvents';
 | 
					} from '../textsecure/messageReceiverEvents';
 | 
				
			||||||
import type { AciString } from '../types/ServiceId';
 | 
					import { isPniString } from '../types/ServiceId';
 | 
				
			||||||
 | 
					import type { AciString, PniString } from '../types/ServiceId';
 | 
				
			||||||
 | 
					import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const {
 | 
					const {
 | 
				
			||||||
  getMessagesBySentAt,
 | 
					  getMessagesBySentAt,
 | 
				
			||||||
| 
						 | 
					@ -48,12 +50,16 @@ export function doesMessageMatch({
 | 
				
			||||||
  const conversationMatches = message.conversationId === conversationId;
 | 
					  const conversationMatches = message.conversationId === conversationId;
 | 
				
			||||||
  const aciMatches =
 | 
					  const aciMatches =
 | 
				
			||||||
    query.authorAci && author?.attributes.serviceId === query.authorAci;
 | 
					    query.authorAci && author?.attributes.serviceId === query.authorAci;
 | 
				
			||||||
 | 
					  const pniMatches =
 | 
				
			||||||
 | 
					    query.authorPni && author?.attributes.serviceId === query.authorPni;
 | 
				
			||||||
  const e164Matches =
 | 
					  const e164Matches =
 | 
				
			||||||
    query.authorE164 && author?.attributes.e164 === query.authorE164;
 | 
					    query.authorE164 && author?.attributes.e164 === query.authorE164;
 | 
				
			||||||
  const timestampMatches = sentTimestamps.has(query.sentAt);
 | 
					  const timestampMatches = sentTimestamps.has(query.sentAt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return Boolean(
 | 
					  return Boolean(
 | 
				
			||||||
    conversationMatches && timestampMatches && (aciMatches || e164Matches)
 | 
					    conversationMatches &&
 | 
				
			||||||
 | 
					      timestampMatches &&
 | 
				
			||||||
 | 
					      (aciMatches || e164Matches || pniMatches)
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -91,7 +97,10 @@ export async function deleteMessage(
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await deleteAndCleanup([found], logId);
 | 
					  await deleteAndCleanup([found], logId, {
 | 
				
			||||||
 | 
					    fromSync: true,
 | 
				
			||||||
 | 
					    singleProtoJobQueue,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return true;
 | 
					  return true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -113,8 +122,10 @@ export async function deleteConversation(
 | 
				
			||||||
    const { received_at: receivedAt } = newestMessage;
 | 
					    const { received_at: receivedAt } = newestMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await removeMessagesInConversation(conversation.id, {
 | 
					    await removeMessagesInConversation(conversation.id, {
 | 
				
			||||||
 | 
					      fromSync: true,
 | 
				
			||||||
      receivedAt,
 | 
					      receivedAt,
 | 
				
			||||||
      logId: `${logId}(receivedAt=${receivedAt})`,
 | 
					      logId: `${logId}(receivedAt=${receivedAt})`,
 | 
				
			||||||
 | 
					      singleProtoJobQueue,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -170,6 +181,9 @@ export function getConversationFromTarget(
 | 
				
			||||||
  if (type === 'e164') {
 | 
					  if (type === 'e164') {
 | 
				
			||||||
    return window.ConversationController.get(targetConversation.e164);
 | 
					    return window.ConversationController.get(targetConversation.e164);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  if (type === 'pni') {
 | 
				
			||||||
 | 
					    return window.ConversationController.get(targetConversation.pni);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  throw missingCaseError(type);
 | 
					  throw missingCaseError(type);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -178,6 +192,7 @@ type MessageQuery = {
 | 
				
			||||||
  sentAt: number;
 | 
					  sentAt: number;
 | 
				
			||||||
  authorAci?: AciString;
 | 
					  authorAci?: AciString;
 | 
				
			||||||
  authorE164?: string;
 | 
					  authorE164?: string;
 | 
				
			||||||
 | 
					  authorPni?: PniString;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getMessageQueryFromTarget(
 | 
					export function getMessageQueryFromTarget(
 | 
				
			||||||
| 
						 | 
					@ -191,6 +206,13 @@ export function getMessageQueryFromTarget(
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return { sentAt, authorAci: targetMessage.authorAci };
 | 
					    return { sentAt, authorAci: targetMessage.authorAci };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  if (type === 'pni') {
 | 
				
			||||||
 | 
					    if (!isPniString(targetMessage.authorPni)) {
 | 
				
			||||||
 | 
					      throw new Error('Provided authorPni was not a PNI!');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return { sentAt, authorPni: targetMessage.authorPni };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (type === 'e164') {
 | 
					  if (type === 'e164') {
 | 
				
			||||||
    return { sentAt, authorE164: targetMessage.authorE164 };
 | 
					    return { sentAt, authorE164: targetMessage.authorE164 };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@
 | 
				
			||||||
import * as log from '../logging/log';
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
import { calculateExpirationTimestamp } from './expirationTimer';
 | 
					import { calculateExpirationTimestamp } from './expirationTimer';
 | 
				
			||||||
import { DAY } from './durations';
 | 
					import { DAY } from './durations';
 | 
				
			||||||
 | 
					import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function findAndDeleteOnboardingStoryIfExists(): Promise<void> {
 | 
					export async function findAndDeleteOnboardingStoryIfExists(): Promise<void> {
 | 
				
			||||||
  const existingOnboardingStoryMessageIds = window.storage.get(
 | 
					  const existingOnboardingStoryMessageIds = window.storage.get(
 | 
				
			||||||
| 
						 | 
					@ -43,7 +44,9 @@ export async function findAndDeleteOnboardingStoryIfExists(): Promise<void> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  log.info('findAndDeleteOnboardingStoryIfExists: removing onboarding stories');
 | 
					  log.info('findAndDeleteOnboardingStoryIfExists: removing onboarding stories');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await window.Signal.Data.removeMessages(existingOnboardingStoryMessageIds);
 | 
					  await window.Signal.Data.removeMessages(existingOnboardingStoryMessageIds, {
 | 
				
			||||||
 | 
					    singleProtoJobQueue,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await window.storage.put('existingOnboardingStoryMessageIds', undefined);
 | 
					  await window.storage.put('existingOnboardingStoryMessageIds', undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -217,6 +217,7 @@ export function getConversation(model: ConversationModel): ConversationType {
 | 
				
			||||||
    profileName: getProfileName(attributes),
 | 
					    profileName: getProfileName(attributes),
 | 
				
			||||||
    profileSharing: attributes.profileSharing,
 | 
					    profileSharing: attributes.profileSharing,
 | 
				
			||||||
    profileLastUpdatedAt: attributes.profileLastUpdatedAt,
 | 
					    profileLastUpdatedAt: attributes.profileLastUpdatedAt,
 | 
				
			||||||
 | 
					    capabilities: attributes.capabilities,
 | 
				
			||||||
    sharingPhoneNumber: attributes.sharingPhoneNumber,
 | 
					    sharingPhoneNumber: attributes.sharingPhoneNumber,
 | 
				
			||||||
    publicParams: attributes.publicParams,
 | 
					    publicParams: attributes.publicParams,
 | 
				
			||||||
    secretParams: attributes.secretParams,
 | 
					    secretParams: attributes.secretParams,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ import type { ConversationAttributesType } from '../model-types.d';
 | 
				
			||||||
import { hasErrors } from '../state/selectors/message';
 | 
					import { hasErrors } from '../state/selectors/message';
 | 
				
			||||||
import { readSyncJobQueue } from '../jobs/readSyncJobQueue';
 | 
					import { readSyncJobQueue } from '../jobs/readSyncJobQueue';
 | 
				
			||||||
import { notificationService } from '../services/notifications';
 | 
					import { notificationService } from '../services/notifications';
 | 
				
			||||||
import { expiringMessagesDeletionService } from '../services/expiringMessagesDeletion';
 | 
					import { update as updateExpiringMessagesService } from '../services/expiringMessagesDeletion';
 | 
				
			||||||
import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService';
 | 
					import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService';
 | 
				
			||||||
import { isGroup, isDirectConversation } from './whatTypeOfConversation';
 | 
					import { isGroup, isDirectConversation } from './whatTypeOfConversation';
 | 
				
			||||||
import * as log from '../logging/log';
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
| 
						 | 
					@ -196,7 +196,7 @@ export async function markConversationRead(
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void expiringMessagesDeletionService.update();
 | 
					  void updateExpiringMessagesService();
 | 
				
			||||||
  void tapToViewMessagesDeletionService.update();
 | 
					  void tapToViewMessagesDeletionService.update();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return true;
 | 
					  return true;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,6 +29,7 @@ import { getSourceServiceId } from '../messages/helpers';
 | 
				
			||||||
import { missingCaseError } from './missingCaseError';
 | 
					import { missingCaseError } from './missingCaseError';
 | 
				
			||||||
import { reduce } from './iterables';
 | 
					import { reduce } from './iterables';
 | 
				
			||||||
import { strictAssert } from './assert';
 | 
					import { strictAssert } from './assert';
 | 
				
			||||||
 | 
					import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum ModifyTargetMessageResult {
 | 
					export enum ModifyTargetMessageResult {
 | 
				
			||||||
  Modified = 'Modified',
 | 
					  Modified = 'Modified',
 | 
				
			||||||
| 
						 | 
					@ -55,24 +56,28 @@ export async function modifyTargetMessage(
 | 
				
			||||||
  const syncDeletes = await DeletesForMe.forMessage(message.attributes);
 | 
					  const syncDeletes = await DeletesForMe.forMessage(message.attributes);
 | 
				
			||||||
  if (syncDeletes.length) {
 | 
					  if (syncDeletes.length) {
 | 
				
			||||||
    if (!isFirstRun) {
 | 
					    if (!isFirstRun) {
 | 
				
			||||||
      await window.Signal.Data.removeMessage(message.id);
 | 
					      await window.Signal.Data.removeMessage(message.id, {
 | 
				
			||||||
 | 
					        fromSync: true,
 | 
				
			||||||
 | 
					        singleProtoJobQueue,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return ModifyTargetMessageResult.Deleted;
 | 
					    return ModifyTargetMessageResult.Deleted;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (type === 'outgoing' || (type === 'story' && ourAci === sourceServiceId)) {
 | 
					  if (type === 'outgoing' || (type === 'story' && ourAci === sourceServiceId)) {
 | 
				
			||||||
    const sendActions = MessageReceipts.forMessage(message).map(receipt => {
 | 
					    const receipts = await MessageReceipts.forMessage(message);
 | 
				
			||||||
 | 
					    const sendActions = receipts.map(({ receiptSync }) => {
 | 
				
			||||||
      let sendActionType: SendActionType;
 | 
					      let sendActionType: SendActionType;
 | 
				
			||||||
      const receiptType = receipt.type;
 | 
					      const receiptType = receiptSync.type;
 | 
				
			||||||
      switch (receiptType) {
 | 
					      switch (receiptType) {
 | 
				
			||||||
        case MessageReceipts.MessageReceiptType.Delivery:
 | 
					        case MessageReceipts.messageReceiptTypeSchema.enum.Delivery:
 | 
				
			||||||
          sendActionType = SendActionType.GotDeliveryReceipt;
 | 
					          sendActionType = SendActionType.GotDeliveryReceipt;
 | 
				
			||||||
          break;
 | 
					          break;
 | 
				
			||||||
        case MessageReceipts.MessageReceiptType.Read:
 | 
					        case MessageReceipts.messageReceiptTypeSchema.enum.Read:
 | 
				
			||||||
          sendActionType = SendActionType.GotReadReceipt;
 | 
					          sendActionType = SendActionType.GotReadReceipt;
 | 
				
			||||||
          break;
 | 
					          break;
 | 
				
			||||||
        case MessageReceipts.MessageReceiptType.View:
 | 
					        case MessageReceipts.messageReceiptTypeSchema.enum.View:
 | 
				
			||||||
          sendActionType = SendActionType.GotViewedReceipt;
 | 
					          sendActionType = SendActionType.GotViewedReceipt;
 | 
				
			||||||
          break;
 | 
					          break;
 | 
				
			||||||
        default:
 | 
					        default:
 | 
				
			||||||
| 
						 | 
					@ -80,10 +85,10 @@ export async function modifyTargetMessage(
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
        destinationConversationId: receipt.sourceConversationId,
 | 
					        destinationConversationId: receiptSync.sourceConversationId,
 | 
				
			||||||
        action: {
 | 
					        action: {
 | 
				
			||||||
          type: sendActionType,
 | 
					          type: sendActionType,
 | 
				
			||||||
          updatedAt: receipt.receiptTimestamp,
 | 
					          updatedAt: receiptSync.receiptTimestamp,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
| 
						 | 
					@ -123,10 +128,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.forMessage(message);
 | 
					    const maybeSingleReadSync = await ReadSyncs.forMessage(message);
 | 
				
			||||||
    const readSyncs = readSync ? [readSync] : [];
 | 
					    const readSyncs = maybeSingleReadSync ? [maybeSingleReadSync] : [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const viewSyncs = ViewSyncs.forMessage(message);
 | 
					    const viewSyncs = await ViewSyncs.forMessage(message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const isGroupStoryReply =
 | 
					    const isGroupStoryReply =
 | 
				
			||||||
      isGroup(conversation.attributes) && message.get('storyId');
 | 
					      isGroup(conversation.attributes) && message.get('storyId');
 | 
				
			||||||
| 
						 | 
					@ -134,8 +139,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.readAt),
 | 
					        ...readSyncs.map(({ readSync }) => readSync.readAt),
 | 
				
			||||||
        ...viewSyncs.map(sync => sync.viewedAt)
 | 
					        ...viewSyncs.map(({ viewSync }) => viewSync.viewedAt)
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (message.get('expireTimer')) {
 | 
					      if (message.get('expireTimer')) {
 | 
				
			||||||
| 
						 | 
					@ -180,7 +185,7 @@ export async function modifyTargetMessage(
 | 
				
			||||||
    if (!isFirstRun && message.getPendingMarkRead()) {
 | 
					    if (!isFirstRun && message.getPendingMarkRead()) {
 | 
				
			||||||
      const markReadAt = message.getPendingMarkRead();
 | 
					      const markReadAt = message.getPendingMarkRead();
 | 
				
			||||||
      message.setPendingMarkRead(undefined);
 | 
					      message.setPendingMarkRead(undefined);
 | 
				
			||||||
      const newestSentAt = readSync?.timestamp;
 | 
					      const newestSentAt = maybeSingleReadSync?.readSync.timestamp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // This is primarily to allow the conversation to mark all older
 | 
					      // This is primarily to allow the conversation to mark all older
 | 
				
			||||||
      // messages as read, as is done when we receive a read sync for
 | 
					      // messages as read, as is done when we receive a read sync for
 | 
				
			||||||
| 
						 | 
					@ -207,7 +212,7 @@ export async function modifyTargetMessage(
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (isStory(message.attributes)) {
 | 
					  if (isStory(message.attributes)) {
 | 
				
			||||||
    const viewSyncs = ViewSyncs.forMessage(message);
 | 
					    const viewSyncs = await ViewSyncs.forMessage(message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (viewSyncs.length !== 0) {
 | 
					    if (viewSyncs.length !== 0) {
 | 
				
			||||||
      message.set({
 | 
					      message.set({
 | 
				
			||||||
| 
						 | 
					@ -218,7 +223,7 @@ export async function modifyTargetMessage(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const markReadAt = Math.min(
 | 
					      const markReadAt = Math.min(
 | 
				
			||||||
        Date.now(),
 | 
					        Date.now(),
 | 
				
			||||||
        ...viewSyncs.map(sync => sync.viewedAt)
 | 
					        ...viewSyncs.map(({ viewSync }) => viewSync.viewedAt)
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      message.setPendingMarkRead(
 | 
					      message.setPendingMarkRead(
 | 
				
			||||||
        Math.min(message.getPendingMarkRead() ?? Date.now(), markReadAt)
 | 
					        Math.min(message.getPendingMarkRead() ?? Date.now(), markReadAt)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@
 | 
				
			||||||
import { z } from 'zod';
 | 
					import { z } from 'zod';
 | 
				
			||||||
import type { ZodSchema } from 'zod';
 | 
					import type { ZodSchema } from 'zod';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { drop } from './drop';
 | 
				
			||||||
import * as log from '../logging/log';
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
import * as DeletesForMe from '../messageModifiers/DeletesForMe';
 | 
					import * as DeletesForMe from '../messageModifiers/DeletesForMe';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					@ -11,18 +12,31 @@ import {
 | 
				
			||||||
  deleteConversationSchema,
 | 
					  deleteConversationSchema,
 | 
				
			||||||
  deleteLocalConversationSchema,
 | 
					  deleteLocalConversationSchema,
 | 
				
			||||||
} from '../textsecure/messageReceiverEvents';
 | 
					} from '../textsecure/messageReceiverEvents';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  receiptSyncTaskSchema,
 | 
				
			||||||
 | 
					  onReceipt,
 | 
				
			||||||
 | 
					} from '../messageModifiers/MessageReceipts';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  deleteConversation,
 | 
					  deleteConversation,
 | 
				
			||||||
  deleteLocalOnlyConversation,
 | 
					  deleteLocalOnlyConversation,
 | 
				
			||||||
  getConversationFromTarget,
 | 
					  getConversationFromTarget,
 | 
				
			||||||
} from './deleteForMe';
 | 
					} from './deleteForMe';
 | 
				
			||||||
import { drop } from './drop';
 | 
					import {
 | 
				
			||||||
 | 
					  onSync as onReadSync,
 | 
				
			||||||
 | 
					  readSyncTaskSchema,
 | 
				
			||||||
 | 
					} from '../messageModifiers/ReadSyncs';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  onSync as onViewSync,
 | 
				
			||||||
 | 
					  viewSyncTaskSchema,
 | 
				
			||||||
 | 
					} from '../messageModifiers/ViewSyncs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const syncTaskDataSchema = z.union([
 | 
					const syncTaskDataSchema = z.union([
 | 
				
			||||||
  deleteMessageSchema,
 | 
					  deleteMessageSchema,
 | 
				
			||||||
  deleteConversationSchema,
 | 
					  deleteConversationSchema,
 | 
				
			||||||
  deleteLocalConversationSchema,
 | 
					  deleteLocalConversationSchema,
 | 
				
			||||||
 | 
					  receiptSyncTaskSchema,
 | 
				
			||||||
 | 
					  readSyncTaskSchema,
 | 
				
			||||||
 | 
					  viewSyncTaskSchema,
 | 
				
			||||||
]);
 | 
					]);
 | 
				
			||||||
export type SyncTaskData = z.infer<typeof syncTaskDataSchema>;
 | 
					export type SyncTaskData = z.infer<typeof syncTaskDataSchema>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,6 +54,11 @@ const SCHEMAS_BY_TYPE: Record<SyncTaskData['type'], ZodSchema> = {
 | 
				
			||||||
  'delete-message': deleteMessageSchema,
 | 
					  'delete-message': deleteMessageSchema,
 | 
				
			||||||
  'delete-conversation': deleteConversationSchema,
 | 
					  'delete-conversation': deleteConversationSchema,
 | 
				
			||||||
  'delete-local-conversation': deleteLocalConversationSchema,
 | 
					  'delete-local-conversation': deleteLocalConversationSchema,
 | 
				
			||||||
 | 
					  Delivery: receiptSyncTaskSchema,
 | 
				
			||||||
 | 
					  Read: receiptSyncTaskSchema,
 | 
				
			||||||
 | 
					  View: receiptSyncTaskSchema,
 | 
				
			||||||
 | 
					  ReadSync: readSyncTaskSchema,
 | 
				
			||||||
 | 
					  ViewSync: viewSyncTaskSchema,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function toLogId(task: SyncTaskType) {
 | 
					function toLogId(task: SyncTaskType) {
 | 
				
			||||||
| 
						 | 
					@ -77,14 +96,15 @@ export async function queueSyncTasks(
 | 
				
			||||||
    const { data: parsed } = parseResult;
 | 
					    const { data: parsed } = parseResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (parsed.type === 'delete-message') {
 | 
					    if (parsed.type === 'delete-message') {
 | 
				
			||||||
      // eslint-disable-next-line no-await-in-loop
 | 
					      drop(
 | 
				
			||||||
      await DeletesForMe.onDelete({
 | 
					        DeletesForMe.onDelete({
 | 
				
			||||||
          conversation: parsed.conversation,
 | 
					          conversation: parsed.conversation,
 | 
				
			||||||
          envelopeId,
 | 
					          envelopeId,
 | 
				
			||||||
          message: parsed.message,
 | 
					          message: parsed.message,
 | 
				
			||||||
          syncTaskId: id,
 | 
					          syncTaskId: id,
 | 
				
			||||||
          timestamp: sentAt,
 | 
					          timestamp: sentAt,
 | 
				
			||||||
      });
 | 
					        })
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
    } else if (parsed.type === 'delete-conversation') {
 | 
					    } else if (parsed.type === 'delete-conversation') {
 | 
				
			||||||
      const {
 | 
					      const {
 | 
				
			||||||
        conversation: targetConversation,
 | 
					        conversation: targetConversation,
 | 
				
			||||||
| 
						 | 
					@ -133,6 +153,39 @@ export async function queueSyncTasks(
 | 
				
			||||||
          log.info(`${logId}: Done; result=${result}`);
 | 
					          log.info(`${logId}: Done; result=${result}`);
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					    } else if (
 | 
				
			||||||
 | 
					      parsed.type === 'Delivery' ||
 | 
				
			||||||
 | 
					      parsed.type === 'Read' ||
 | 
				
			||||||
 | 
					      parsed.type === 'View'
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      drop(
 | 
				
			||||||
 | 
					        onReceipt({
 | 
				
			||||||
 | 
					          envelopeId,
 | 
				
			||||||
 | 
					          receiptSync: parsed,
 | 
				
			||||||
 | 
					          syncTaskId: id,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else if (parsed.type === 'ReadSync') {
 | 
				
			||||||
 | 
					      drop(
 | 
				
			||||||
 | 
					        onReadSync({
 | 
				
			||||||
 | 
					          envelopeId,
 | 
				
			||||||
 | 
					          readSync: parsed,
 | 
				
			||||||
 | 
					          syncTaskId: id,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else if (parsed.type === 'ViewSync') {
 | 
				
			||||||
 | 
					      drop(
 | 
				
			||||||
 | 
					        onViewSync({
 | 
				
			||||||
 | 
					          envelopeId,
 | 
				
			||||||
 | 
					          viewSync: parsed,
 | 
				
			||||||
 | 
					          syncTaskId: id,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      const parsedType: never = parsed.type;
 | 
				
			||||||
 | 
					      log.error(`${logId}: Encountered job of type ${parsedType}, removing`);
 | 
				
			||||||
 | 
					      // eslint-disable-next-line no-await-in-loop
 | 
				
			||||||
 | 
					      await removeSyncTaskById(id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue