Support for local deletes synced to all your devices
This commit is contained in:
		
					parent
					
						
							
								06f71a7ef8
							
						
					
				
			
			
				commit
				
					
						11eb1782a7
					
				
			
		
					 39 changed files with 2094 additions and 72 deletions
				
			
		| 
						 | 
					@ -5010,6 +5010,10 @@
 | 
				
			||||||
    "messageformat": "What devices would you like to delete {count, plural, one {this message} other {these messages}} from?",
 | 
					    "messageformat": "What devices would you like to delete {count, plural, one {this message} other {these messages}} from?",
 | 
				
			||||||
    "description": "within note to self conversation > delete selected messages > confirmation modal > description"
 | 
					    "description": "within note to self conversation > delete selected messages > confirmation modal > description"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  "icu:DeleteMessagesModal--description--noteToSelf--deleteSync": {
 | 
				
			||||||
 | 
					    "messageformat": "{count, plural, one {This message} other {These messages}} will be deleted from all your devices.",
 | 
				
			||||||
 | 
					    "description": "within note to self conversation > delete selected messages > confirmation modal > description"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  "icu:DeleteMessagesModal--deleteForMe": {
 | 
					  "icu:DeleteMessagesModal--deleteForMe": {
 | 
				
			||||||
    "messageformat": "Delete for me",
 | 
					    "messageformat": "Delete for me",
 | 
				
			||||||
    "description": "delete selected messages > confirmation modal > delete for me"
 | 
					    "description": "delete selected messages > confirmation modal > delete for me"
 | 
				
			||||||
| 
						 | 
					@ -5026,6 +5030,10 @@
 | 
				
			||||||
    "messageformat": "Delete from all devices",
 | 
					    "messageformat": "Delete from all devices",
 | 
				
			||||||
    "description": "within note to self conversation > delete selected messages > confirmation modal > delete from all devices (same as delete for everyone)"
 | 
					    "description": "within note to self conversation > delete selected messages > confirmation modal > delete from all devices (same as delete for everyone)"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  "icu:DeleteMessagesModal--noteToSelf--deleteSync": {
 | 
				
			||||||
 | 
					    "messageformat": "Delete",
 | 
				
			||||||
 | 
					    "description": "When delete sync is enabled, there is only one Delete option in Note to Self"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  "icu:DeleteMessagesModal__toast--TooManyMessagesToDeleteForEveryone": {
 | 
					  "icu:DeleteMessagesModal__toast--TooManyMessagesToDeleteForEveryone": {
 | 
				
			||||||
    "messageformat": "You can only select up to {count, plural, one {# message} other {# messages}} to delete for everyone",
 | 
					    "messageformat": "You can only select up to {count, plural, one {# message} other {# messages}} to delete for everyone",
 | 
				
			||||||
    "description": "delete selected messages > confirmation modal > deleted for everyone (disabled) > toast > too many messages to 'delete for everyone'"
 | 
					    "description": "delete selected messages > confirmation modal > deleted for everyone (disabled) > toast > too many messages to 'delete for everyone'"
 | 
				
			||||||
| 
						 | 
					@ -6292,6 +6300,18 @@
 | 
				
			||||||
    "messageformat": "Your connections can see your name and photo, and can see posts to \"My Story\" unless you hide it from them",
 | 
					    "messageformat": "Your connections can see your name and photo, and can see posts to \"My Story\" unless you hide it from them",
 | 
				
			||||||
    "description": "Additional information about signal connections and the stories they can see"
 | 
					    "description": "Additional information about signal connections and the stories they can see"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  "icu:LocalDeleteWarningModal__header": {
 | 
				
			||||||
 | 
					    "messageformat": "\"Delete for Me\" now deletes from all of your devices",
 | 
				
			||||||
 | 
					    "description": "Emphasized text at the top of the explainer dialog you get when you first delete a message or conversation"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "icu:LocalDeleteWarningModal__description": {
 | 
				
			||||||
 | 
					    "messageformat": "When you delete a message in a chat, the message will be deleted from your phone and all linked devices.",
 | 
				
			||||||
 | 
					    "description": "More detailed description of new delete behavior shown in explainer dialog"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "icu:LocalDeleteWarningModal__confirm": {
 | 
				
			||||||
 | 
					    "messageformat": "Got it",
 | 
				
			||||||
 | 
					    "description": "Button to dismiss the dialog explaining that 'delete for me' now syncs between devices"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  "icu:Stories__title": {
 | 
					  "icu:Stories__title": {
 | 
				
			||||||
    "messageformat": "Stories",
 | 
					    "messageformat": "Stories",
 | 
				
			||||||
    "description": "Title for the stories list"
 | 
					    "description": "Title for the stories list"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								images/local-delete-sync.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								images/local-delete-sync.svg
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 138 92"><defs><style>.b{fill:#8f8f8f;fill-rule:evenodd;}.b,.c,.d{stroke-width:0px;}.c{fill:#6b88e8;}.d{fill:#e4e7ff;}</style></defs><path class="d" d="m61.26,29.835h-16.112c-.636,0-1.249.101-1.824.284v16.431c.576.184,1.188.284,1.824.284h16.112c3.314,0,6-2.686,6-6v-5c0-3.314-2.686-6-6-6Z"/><path class="c" d="m95.372,57.835c0-3.314,2.686-6,6-6h23.139c3.314,0,6,2.686,6,6v8c0,3.314-2.686,6-6,6h-23.139c-3.314,0-6-2.686-6-6v-8Z"/><path class="d" d="m34.128,16.835c0-3.314,2.686-6,6-6h36.192c3.314,0,6,2.686,6,6v5c0,3.314-2.686,6-6,6h-36.192c-3.314,0-6-2.686-6-6v-5Z"/><path class="c" d="m18.198,69.902c0-2.761,2.239-5,5-5h9.879c2.762,0,5,2.239,5,5v4.667c0,2.761-2.238,5-5,5h-9.879c-2.761,0-5-2.239-5-5v-4.667Z"/><path class="d" d="m5.681,56.568c0-2.835,2.298-5.133,5.133-5.133h9.612c2.835,0,5.133,2.298,5.133,5.133h0c0,2.835-2.298,5.133-5.133,5.133h-9.612c-2.835,0-5.133-2.298-5.133-5.133h0Z"/><path class="d" d="m5.681,45.102c0-2.835,2.298-5.133,5.133-5.133h15.502c2.835,0,5.133,2.298,5.133,5.133h0c0,2.835-2.298,5.133-5.133,5.133h-15.502c-2.835,0-5.133-2.298-5.133-5.133h0Z"/><path class="b" d="m125,0H38.033c-7.089,0-12.835,5.746-12.835,12.835v10.632h-13.198C5.464,23.467.165,28.765.165,35.302v44.533c0,6.536,5.299,11.835,11.835,11.835h20.495c4.654,0,8.669-2.693,10.602-6.6h81.903c7.089,0,12.835-5.746,12.835-12.835V12.835c0-7.089-5.746-12.835-12.835-12.835ZM40.659,79.835c0,4.509-3.656,8.165-8.165,8.165H12c-4.509,0-8.165-3.656-8.165-8.165v-44.533c0-4.509,3.656-8.165,8.165-8.165h20.495c4.509,0,8.165,3.656,8.165,8.165v44.533Zm93.505-7.6c0,5.062-4.103,9.165-9.165,9.165H44.214c.068-.513.115-1.033.115-1.565v-44.533c0-6.536-5.299-11.835-11.835-11.835h-3.627v-10.632c0-5.062,4.103-9.165,9.165-9.165h86.967c5.062,0,9.165,4.103,9.165,9.165v59.4Z"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 1.8 KiB  | 
| 
						 | 
					@ -633,6 +633,43 @@ message SyncMessage {
 | 
				
			||||||
    optional uint64 timestamp = 2;
 | 
					    optional uint64 timestamp = 2;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  message DeleteForMe {
 | 
				
			||||||
 | 
					    message ConversationIdentifier {
 | 
				
			||||||
 | 
					      oneof identifier {
 | 
				
			||||||
 | 
					        string threadAci = 1;
 | 
				
			||||||
 | 
					        bytes threadGroupId = 2;
 | 
				
			||||||
 | 
					        string threadE164 = 3;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					    message AddressableMessage {
 | 
				
			||||||
 | 
					      oneof author {
 | 
				
			||||||
 | 
					        string authorAci = 1;
 | 
				
			||||||
 | 
					        string authorE164 = 2;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      optional uint64 sentTimestamp = 3;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    message MessageDeletes {
 | 
				
			||||||
 | 
					      optional ConversationIdentifier conversation = 1;
 | 
				
			||||||
 | 
					      repeated AddressableMessage messages = 2;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    message ConversationDelete {
 | 
				
			||||||
 | 
					      optional ConversationIdentifier conversation = 1;
 | 
				
			||||||
 | 
					      repeated AddressableMessage mostRecentMessages = 2;
 | 
				
			||||||
 | 
					      optional bool isFullDelete = 3;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    message LocalOnlyConversationDelete {
 | 
				
			||||||
 | 
					      optional ConversationIdentifier conversation = 1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    repeated MessageDeletes messageDeletes = 1;
 | 
				
			||||||
 | 
					    repeated ConversationDelete conversationDeletes = 2;
 | 
				
			||||||
 | 
					    repeated LocalOnlyConversationDelete localOnlyConversationDeletes = 3;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
  optional Sent                   sent                   = 1;
 | 
					  optional Sent                   sent                   = 1;
 | 
				
			||||||
  optional Contacts               contacts               = 2;
 | 
					  optional Contacts               contacts               = 2;
 | 
				
			||||||
  reserved /* groups */ 3;
 | 
					  reserved /* groups */ 3;
 | 
				
			||||||
| 
						 | 
					@ -654,6 +691,7 @@ message SyncMessage {
 | 
				
			||||||
  optional CallEvent              callEvent              = 19;
 | 
					  optional CallEvent              callEvent              = 19;
 | 
				
			||||||
  optional CallLinkUpdate         callLinkUpdate         = 20;
 | 
					  optional CallLinkUpdate         callLinkUpdate         = 20;
 | 
				
			||||||
  optional CallLogEvent           callLogEvent           = 21;
 | 
					  optional CallLogEvent           callLogEvent           = 21;
 | 
				
			||||||
 | 
					  optional DeleteForMe            deleteForMe            = 22;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
message AttachmentPointer {
 | 
					message AttachmentPointer {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										35
									
								
								stylesheets/components/LocalDeleteWarningModal.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								stylesheets/components/LocalDeleteWarningModal.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,35 @@
 | 
				
			||||||
 | 
					// Copyright 2022 Signal Messenger, LLC
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.LocalDeleteWarningModal__width-container {
 | 
				
			||||||
 | 
					  max-width: 440px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.LocalDeleteWarningModal__image {
 | 
				
			||||||
 | 
					  margin-block: 18px;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.LocalDeleteWarningModal__header {
 | 
				
			||||||
 | 
					  @include font-title-2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  margin-block: 18px;
 | 
				
			||||||
 | 
					  margin-inline: 8px;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.LocalDeleteWarningModal__description {
 | 
				
			||||||
 | 
					  margin-block: 12px;
 | 
				
			||||||
 | 
					  margin-inline: 8px;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.LocalDeleteWarningModal__button {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  margin-top: 49px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  button {
 | 
				
			||||||
 | 
					    padding-inline: 26px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -113,6 +113,7 @@
 | 
				
			||||||
@import './components/LeftPaneSearchInput.scss';
 | 
					@import './components/LeftPaneSearchInput.scss';
 | 
				
			||||||
@import './components/Lightbox.scss';
 | 
					@import './components/Lightbox.scss';
 | 
				
			||||||
@import './components/ListTile.scss';
 | 
					@import './components/ListTile.scss';
 | 
				
			||||||
 | 
					@import './components/LocalDeleteWarningModal.scss';
 | 
				
			||||||
@import './components/MediaEditor.scss';
 | 
					@import './components/MediaEditor.scss';
 | 
				
			||||||
@import './components/MediaQualitySelector.scss';
 | 
					@import './components/MediaQualitySelector.scss';
 | 
				
			||||||
@import './components/MessageAudio.scss';
 | 
					@import './components/MessageAudio.scss';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -50,7 +50,7 @@ export async function populateConversationWithMessages({
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  log.info(`${logId}: destroying all messages in ${conversationId}`);
 | 
					  log.info(`${logId}: destroying all messages in ${conversationId}`);
 | 
				
			||||||
  await conversation.destroyMessages();
 | 
					  await conversation.destroyMessages({ source: 'local-delete' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  log.info(`${logId}: adding ${messageCount} messages to ${conversationId}`);
 | 
					  log.info(`${logId}: adding ${messageCount} messages to ${conversationId}`);
 | 
				
			||||||
  let timestamp = Date.now();
 | 
					  let timestamp = Date.now();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,6 +18,8 @@ export type ConfigKeyType =
 | 
				
			||||||
  | 'desktop.calling.adhoc'
 | 
					  | 'desktop.calling.adhoc'
 | 
				
			||||||
  | 'desktop.clientExpiration'
 | 
					  | 'desktop.clientExpiration'
 | 
				
			||||||
  | 'desktop.backup.credentialFetch'
 | 
					  | 'desktop.backup.credentialFetch'
 | 
				
			||||||
 | 
					  | 'desktop.deleteSync.send'
 | 
				
			||||||
 | 
					  | 'desktop.deleteSync.receive'
 | 
				
			||||||
  | 'desktop.groupMultiTypingIndicators'
 | 
					  | 'desktop.groupMultiTypingIndicators'
 | 
				
			||||||
  | 'desktop.internalUser'
 | 
					  | 'desktop.internalUser'
 | 
				
			||||||
  | 'desktop.mediaQuality.levels'
 | 
					  | 'desktop.mediaQuality.levels'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -86,6 +86,7 @@ import type {
 | 
				
			||||||
  FetchLatestEvent,
 | 
					  FetchLatestEvent,
 | 
				
			||||||
  InvalidPlaintextEvent,
 | 
					  InvalidPlaintextEvent,
 | 
				
			||||||
  KeysEvent,
 | 
					  KeysEvent,
 | 
				
			||||||
 | 
					  DeleteForMeSyncEvent,
 | 
				
			||||||
  MessageEvent,
 | 
					  MessageEvent,
 | 
				
			||||||
  MessageEventData,
 | 
					  MessageEventData,
 | 
				
			||||||
  MessageRequestResponseEvent,
 | 
					  MessageRequestResponseEvent,
 | 
				
			||||||
| 
						 | 
					@ -111,21 +112,21 @@ import type { BadgesStateType } from './state/ducks/badges';
 | 
				
			||||||
import { areAnyCallsActiveOrRinging } from './state/selectors/calling';
 | 
					import { areAnyCallsActiveOrRinging } from './state/selectors/calling';
 | 
				
			||||||
import { badgeImageFileDownloader } from './badges/badgeImageFileDownloader';
 | 
					import { badgeImageFileDownloader } from './badges/badgeImageFileDownloader';
 | 
				
			||||||
import * as Deletes from './messageModifiers/Deletes';
 | 
					import * as Deletes from './messageModifiers/Deletes';
 | 
				
			||||||
import type { EditAttributesType } from './messageModifiers/Edits';
 | 
					 | 
				
			||||||
import * as Edits from './messageModifiers/Edits';
 | 
					import * as Edits from './messageModifiers/Edits';
 | 
				
			||||||
import type { ReactionAttributesType } from './messageModifiers/Reactions';
 | 
					 | 
				
			||||||
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 ReadSyncs from './messageModifiers/ReadSyncs';
 | 
				
			||||||
import * as ViewSyncs from './messageModifiers/ViewSyncs';
 | 
					 | 
				
			||||||
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 { MessageReceiptAttributesType } from './messageModifiers/MessageReceipts';
 | 
					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 { ReadSyncAttributesType } from './messageModifiers/ReadSyncs';
 | 
					import type { ReadSyncAttributesType } from './messageModifiers/ReadSyncs';
 | 
				
			||||||
import type { ViewSyncAttributesType } from './messageModifiers/ViewSyncs';
 | 
					 | 
				
			||||||
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';
 | 
				
			||||||
| 
						 | 
					@ -201,6 +202,8 @@ 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 { queueSyncTasks } from './util/syncTasks';
 | 
				
			||||||
 | 
					import { isEnabled } from './RemoteConfig';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function isOverHourIntoPast(timestamp: number): boolean {
 | 
					export function isOverHourIntoPast(timestamp: number): boolean {
 | 
				
			||||||
  return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
 | 
					  return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
 | 
				
			||||||
| 
						 | 
					@ -558,6 +561,24 @@ export async function startApp(): Promise<void> {
 | 
				
			||||||
      storage: window.storage,
 | 
					      storage: window.storage,
 | 
				
			||||||
      serverTrustRoot: window.getServerTrustRoot(),
 | 
					      serverTrustRoot: window.getServerTrustRoot(),
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    const onFirstEmpty = async () => {
 | 
				
			||||||
 | 
					      log.info('onFirstEmpty: Starting');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // We want to remove this handler on the next tick so we don't interfere with
 | 
				
			||||||
 | 
					      //   the other handlers being notified of this instance of the 'empty' event.
 | 
				
			||||||
 | 
					      setTimeout(() => {
 | 
				
			||||||
 | 
					        messageReceiver?.removeEventListener('empty', onFirstEmpty);
 | 
				
			||||||
 | 
					      }, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      log.info('onFirstEmpty: Fetching sync tasks');
 | 
				
			||||||
 | 
					      const syncTasks = await window.Signal.Data.getAllSyncTasks();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      log.info(`onFirstEmpty: Queuing ${syncTasks.length} sync tasks`);
 | 
				
			||||||
 | 
					      await queueSyncTasks(syncTasks, window.Signal.Data.removeSyncTaskById);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      log.info('onFirstEmpty: Done');
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    messageReceiver.addEventListener('empty', onFirstEmpty);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function queuedEventListener<E extends Event>(
 | 
					    function queuedEventListener<E extends Event>(
 | 
				
			||||||
      handler: (event: E) => Promise<void> | void,
 | 
					      handler: (event: E) => Promise<void> | void,
 | 
				
			||||||
| 
						 | 
					@ -691,6 +712,10 @@ export async function startApp(): Promise<void> {
 | 
				
			||||||
      'callLogEventSync',
 | 
					      'callLogEventSync',
 | 
				
			||||||
      queuedEventListener(onCallLogEventSync, false)
 | 
					      queuedEventListener(onCallLogEventSync, false)
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					    messageReceiver.addEventListener(
 | 
				
			||||||
 | 
					      'deleteForMeSync',
 | 
				
			||||||
 | 
					      queuedEventListener(onDeleteForMeSync, false)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!window.storage.get('defaultConversationColor')) {
 | 
					    if (!window.storage.get('defaultConversationColor')) {
 | 
				
			||||||
      drop(
 | 
					      drop(
 | 
				
			||||||
| 
						 | 
					@ -3384,6 +3409,41 @@ export async function startApp(): Promise<void> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    drop(MessageReceipts.onReceipt(attributes));
 | 
					    drop(MessageReceipts.onReceipt(attributes));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function onDeleteForMeSync(ev: DeleteForMeSyncEvent) {
 | 
				
			||||||
 | 
					    const { confirm, timestamp, envelopeId, deleteForMeSync } = ev;
 | 
				
			||||||
 | 
					    const logId = `onDeleteForMeSync(${timestamp})`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!isEnabled('desktop.deleteSync.receive')) {
 | 
				
			||||||
 | 
					      confirm();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // The user clearly knows about this feature; they did it on another device!
 | 
				
			||||||
 | 
					    drop(window.storage.put('localDeleteWarningShown', true));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log.info(`${logId}: Saving ${deleteForMeSync.length} sync tasks`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const now = Date.now();
 | 
				
			||||||
 | 
					    const syncTasks = deleteForMeSync.map(item => ({
 | 
				
			||||||
 | 
					      id: generateUuid(),
 | 
				
			||||||
 | 
					      attempts: 1,
 | 
				
			||||||
 | 
					      createdAt: now,
 | 
				
			||||||
 | 
					      data: item,
 | 
				
			||||||
 | 
					      envelopeId,
 | 
				
			||||||
 | 
					      sentAt: timestamp,
 | 
				
			||||||
 | 
					      type: item.type,
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					    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`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
window.startApp = startApp;
 | 
					window.startApp = startApp;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										78
									
								
								ts/components/DeleteMessagesModal.stories.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								ts/components/DeleteMessagesModal.stories.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,78 @@
 | 
				
			||||||
 | 
					// Copyright 2022 Signal Messenger, LLC
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import type { Meta, StoryFn } from '@storybook/react';
 | 
				
			||||||
 | 
					import { action } from '@storybook/addon-actions';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import enMessages from '../../_locales/en/messages.json';
 | 
				
			||||||
 | 
					import { setupI18n } from '../util/setupI18n';
 | 
				
			||||||
 | 
					import DeleteMessagesModal from './DeleteMessagesModal';
 | 
				
			||||||
 | 
					import type { DeleteMessagesModalProps } from './DeleteMessagesModal';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const i18n = setupI18n('en', enMessages);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  title: 'Components/DeleteMessagesModal',
 | 
				
			||||||
 | 
					  component: DeleteMessagesModal,
 | 
				
			||||||
 | 
					  args: {
 | 
				
			||||||
 | 
					    i18n,
 | 
				
			||||||
 | 
					    isMe: false,
 | 
				
			||||||
 | 
					    isDeleteSyncSendEnabled: false,
 | 
				
			||||||
 | 
					    canDeleteForEveryone: true,
 | 
				
			||||||
 | 
					    messageCount: 1,
 | 
				
			||||||
 | 
					    onClose: action('onClose'),
 | 
				
			||||||
 | 
					    onDeleteForMe: action('onDeleteForMe'),
 | 
				
			||||||
 | 
					    onDeleteForEveryone: action('onDeleteForEveryone'),
 | 
				
			||||||
 | 
					    showToast: action('showToast'),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					} satisfies Meta<DeleteMessagesModalProps>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function createProps(args: Partial<DeleteMessagesModalProps>) {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    i18n,
 | 
				
			||||||
 | 
					    isMe: false,
 | 
				
			||||||
 | 
					    isDeleteSyncSendEnabled: false,
 | 
				
			||||||
 | 
					    canDeleteForEveryone: true,
 | 
				
			||||||
 | 
					    messageCount: 1,
 | 
				
			||||||
 | 
					    onClose: action('onClose'),
 | 
				
			||||||
 | 
					    onDeleteForMe: action('onDeleteForMe'),
 | 
				
			||||||
 | 
					    onDeleteForEveryone: action('onDeleteForEveryone'),
 | 
				
			||||||
 | 
					    showToast: action('showToast'),
 | 
				
			||||||
 | 
					    ...args,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line react/function-component-definition
 | 
				
			||||||
 | 
					const Template: StoryFn<DeleteMessagesModalProps> = args => {
 | 
				
			||||||
 | 
					  return <DeleteMessagesModal {...args} />;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const OneMessage = Template.bind({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ThreeMessages = Template.bind({});
 | 
				
			||||||
 | 
					ThreeMessages.args = createProps({
 | 
				
			||||||
 | 
					  messageCount: 3,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const IsMe = Template.bind({});
 | 
				
			||||||
 | 
					IsMe.args = createProps({
 | 
				
			||||||
 | 
					  isMe: true,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const IsMeThreeMessages = Template.bind({});
 | 
				
			||||||
 | 
					IsMeThreeMessages.args = createProps({
 | 
				
			||||||
 | 
					  isMe: true,
 | 
				
			||||||
 | 
					  messageCount: 3,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DeleteSyncEnabled = Template.bind({});
 | 
				
			||||||
 | 
					DeleteSyncEnabled.args = createProps({
 | 
				
			||||||
 | 
					  isDeleteSyncSendEnabled: true,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const IsMeDeleteSyncEnabled = Template.bind({});
 | 
				
			||||||
 | 
					IsMeDeleteSyncEnabled.args = createProps({
 | 
				
			||||||
 | 
					  isDeleteSyncSendEnabled: true,
 | 
				
			||||||
 | 
					  isMe: true,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -8,8 +8,9 @@ import type { LocalizerType } from '../types/Util';
 | 
				
			||||||
import type { ShowToastAction } from '../state/ducks/toast';
 | 
					import type { ShowToastAction } from '../state/ducks/toast';
 | 
				
			||||||
import { ToastType } from '../types/Toast';
 | 
					import { ToastType } from '../types/Toast';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type DeleteMessagesModalProps = Readonly<{
 | 
					export type DeleteMessagesModalProps = Readonly<{
 | 
				
			||||||
  isMe: boolean;
 | 
					  isMe: boolean;
 | 
				
			||||||
 | 
					  isDeleteSyncSendEnabled: boolean;
 | 
				
			||||||
  canDeleteForEveryone: boolean;
 | 
					  canDeleteForEveryone: boolean;
 | 
				
			||||||
  i18n: LocalizerType;
 | 
					  i18n: LocalizerType;
 | 
				
			||||||
  messageCount: number;
 | 
					  messageCount: number;
 | 
				
			||||||
| 
						 | 
					@ -23,6 +24,7 @@ const MAX_DELETE_FOR_EVERYONE = 30;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function DeleteMessagesModal({
 | 
					export default function DeleteMessagesModal({
 | 
				
			||||||
  isMe,
 | 
					  isMe,
 | 
				
			||||||
 | 
					  isDeleteSyncSendEnabled,
 | 
				
			||||||
  canDeleteForEveryone,
 | 
					  canDeleteForEveryone,
 | 
				
			||||||
  i18n,
 | 
					  i18n,
 | 
				
			||||||
  messageCount,
 | 
					  messageCount,
 | 
				
			||||||
| 
						 | 
					@ -33,15 +35,22 @@ export default function DeleteMessagesModal({
 | 
				
			||||||
}: DeleteMessagesModalProps): JSX.Element {
 | 
					}: DeleteMessagesModalProps): JSX.Element {
 | 
				
			||||||
  const actions: Array<ActionSpec> = [];
 | 
					  const actions: Array<ActionSpec> = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const syncNoteToSelfDelete = isMe && isDeleteSyncSendEnabled;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let deleteForMeText = i18n('icu:DeleteMessagesModal--deleteForMe');
 | 
				
			||||||
 | 
					  if (syncNoteToSelfDelete) {
 | 
				
			||||||
 | 
					    deleteForMeText = i18n('icu:DeleteMessagesModal--noteToSelf--deleteSync');
 | 
				
			||||||
 | 
					  } else if (isMe) {
 | 
				
			||||||
 | 
					    deleteForMeText = i18n('icu:DeleteMessagesModal--deleteFromThisDevice');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  actions.push({
 | 
					  actions.push({
 | 
				
			||||||
    action: onDeleteForMe,
 | 
					    action: onDeleteForMe,
 | 
				
			||||||
    style: 'negative',
 | 
					    style: 'negative',
 | 
				
			||||||
    text: isMe
 | 
					    text: deleteForMeText,
 | 
				
			||||||
      ? i18n('icu:DeleteMessagesModal--deleteFromThisDevice')
 | 
					 | 
				
			||||||
      : i18n('icu:DeleteMessagesModal--deleteForMe'),
 | 
					 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (canDeleteForEveryone) {
 | 
					  if (canDeleteForEveryone && !syncNoteToSelfDelete) {
 | 
				
			||||||
    const tooManyMessages = messageCount > MAX_DELETE_FOR_EVERYONE;
 | 
					    const tooManyMessages = messageCount > MAX_DELETE_FOR_EVERYONE;
 | 
				
			||||||
    actions.push({
 | 
					    actions.push({
 | 
				
			||||||
      'aria-disabled': tooManyMessages,
 | 
					      'aria-disabled': tooManyMessages,
 | 
				
			||||||
| 
						 | 
					@ -63,6 +72,20 @@ export default function DeleteMessagesModal({
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let descriptionText = i18n('icu:DeleteMessagesModal--description', {
 | 
				
			||||||
 | 
					    count: messageCount,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  if (syncNoteToSelfDelete) {
 | 
				
			||||||
 | 
					    descriptionText = i18n(
 | 
				
			||||||
 | 
					      'icu:DeleteMessagesModal--description--noteToSelf--deleteSync',
 | 
				
			||||||
 | 
					      { count: messageCount }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  } else if (isMe) {
 | 
				
			||||||
 | 
					    descriptionText = i18n('icu:DeleteMessagesModal--description--noteToSelf', {
 | 
				
			||||||
 | 
					      count: messageCount,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <ConfirmationDialog
 | 
					    <ConfirmationDialog
 | 
				
			||||||
      actions={actions}
 | 
					      actions={actions}
 | 
				
			||||||
| 
						 | 
					@ -74,13 +97,7 @@ export default function DeleteMessagesModal({
 | 
				
			||||||
      })}
 | 
					      })}
 | 
				
			||||||
      moduleClassName="DeleteMessagesModal"
 | 
					      moduleClassName="DeleteMessagesModal"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      {isMe
 | 
					      {descriptionText}
 | 
				
			||||||
        ? i18n('icu:DeleteMessagesModal--description--noteToSelf', {
 | 
					 | 
				
			||||||
            count: messageCount,
 | 
					 | 
				
			||||||
          })
 | 
					 | 
				
			||||||
        : i18n('icu:DeleteMessagesModal--description', {
 | 
					 | 
				
			||||||
            count: messageCount,
 | 
					 | 
				
			||||||
          })}
 | 
					 | 
				
			||||||
    </ConfirmationDialog>
 | 
					    </ConfirmationDialog>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										30
									
								
								ts/components/LocalDeleteWarningModal.stories.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								ts/components/LocalDeleteWarningModal.stories.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					// Copyright 2022 Signal Messenger, LLC
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { Meta, StoryFn } from '@storybook/react';
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { action } from '@storybook/addon-actions';
 | 
				
			||||||
 | 
					import type { PropsType } from './LocalDeleteWarningModal';
 | 
				
			||||||
 | 
					import enMessages from '../../_locales/en/messages.json';
 | 
				
			||||||
 | 
					import { LocalDeleteWarningModal } from './LocalDeleteWarningModal';
 | 
				
			||||||
 | 
					import { setupI18n } from '../util/setupI18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const i18n = setupI18n('en', enMessages);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  title: 'Components/LocalDeleteWarningModal',
 | 
				
			||||||
 | 
					  component: LocalDeleteWarningModal,
 | 
				
			||||||
 | 
					  args: {
 | 
				
			||||||
 | 
					    i18n,
 | 
				
			||||||
 | 
					    onClose: action('onClose'),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					} satisfies Meta<PropsType>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line react/function-component-definition
 | 
				
			||||||
 | 
					const Template: StoryFn<PropsType> = args => (
 | 
				
			||||||
 | 
					  <LocalDeleteWarningModal {...args} />
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const Modal = Template.bind({});
 | 
				
			||||||
 | 
					Modal.args = {};
 | 
				
			||||||
							
								
								
									
										53
									
								
								ts/components/LocalDeleteWarningModal.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								ts/components/LocalDeleteWarningModal.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,53 @@
 | 
				
			||||||
 | 
					// Copyright 2022 Signal Messenger, LLC
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { LocalizerType } from '../types/Util';
 | 
				
			||||||
 | 
					import { Button, ButtonVariant } from './Button';
 | 
				
			||||||
 | 
					import { I18n } from './I18n';
 | 
				
			||||||
 | 
					import { Modal } from './Modal';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type PropsType = {
 | 
				
			||||||
 | 
					  i18n: LocalizerType;
 | 
				
			||||||
 | 
					  onClose: () => unknown;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function LocalDeleteWarningModal({
 | 
				
			||||||
 | 
					  i18n,
 | 
				
			||||||
 | 
					  onClose,
 | 
				
			||||||
 | 
					}: PropsType): JSX.Element {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Modal
 | 
				
			||||||
 | 
					      modalName="LocalDeleteWarningModal"
 | 
				
			||||||
 | 
					      moduleClassName="LocalDeleteWarningModal"
 | 
				
			||||||
 | 
					      i18n={i18n}
 | 
				
			||||||
 | 
					      onClose={onClose}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <div className="LocalDeleteWarningModal">
 | 
				
			||||||
 | 
					        <div className="LocalDeleteWarningModal__image">
 | 
				
			||||||
 | 
					          <img
 | 
				
			||||||
 | 
					            src="images/local-delete-sync.svg"
 | 
				
			||||||
 | 
					            height="92"
 | 
				
			||||||
 | 
					            width="138"
 | 
				
			||||||
 | 
					            alt=""
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className="LocalDeleteWarningModal__header">
 | 
				
			||||||
 | 
					          <I18n i18n={i18n} id="icu:LocalDeleteWarningModal__header" />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className="LocalDeleteWarningModal__description">
 | 
				
			||||||
 | 
					          <I18n i18n={i18n} id="icu:LocalDeleteWarningModal__description" />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className="LocalDeleteWarningModal__button">
 | 
				
			||||||
 | 
					          <Button onClick={onClose} variant={ButtonVariant.Primary}>
 | 
				
			||||||
 | 
					            <I18n i18n={i18n} id="icu:LocalDeleteWarningModal__confirm" />
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </Modal>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -46,6 +46,10 @@ const commonProps = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  i18n,
 | 
					  i18n,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  localDeleteWarningShown: true,
 | 
				
			||||||
 | 
					  isDeleteSyncSendEnabled: true,
 | 
				
			||||||
 | 
					  setLocalDeleteWarningShown: action('setLocalDeleteWarningShown'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onConversationAccept: action('onConversationAccept'),
 | 
					  onConversationAccept: action('onConversationAccept'),
 | 
				
			||||||
  onConversationArchive: action('onConversationArchive'),
 | 
					  onConversationArchive: action('onConversationArchive'),
 | 
				
			||||||
  onConversationBlock: action('onConversationBlock'),
 | 
					  onConversationBlock: action('onConversationBlock'),
 | 
				
			||||||
| 
						 | 
					@ -412,3 +416,32 @@ export function Unaccepted(): JSX.Element {
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function NeedsDeleteConfirmation(): JSX.Element {
 | 
				
			||||||
 | 
					  const [localDeleteWarningShown, setLocalDeleteWarningShown] =
 | 
				
			||||||
 | 
					    React.useState(false);
 | 
				
			||||||
 | 
					  const props = {
 | 
				
			||||||
 | 
					    ...commonProps,
 | 
				
			||||||
 | 
					    conversation: getDefaultConversation(),
 | 
				
			||||||
 | 
					    localDeleteWarningShown,
 | 
				
			||||||
 | 
					    setLocalDeleteWarningShown: () => setLocalDeleteWarningShown(true),
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const theme = useContext(StorybookThemeContext);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return <ConversationHeader {...props} theme={theme} />;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function NeedsDeleteConfirmationButNotEnabled(): JSX.Element {
 | 
				
			||||||
 | 
					  const [localDeleteWarningShown, setLocalDeleteWarningShown] =
 | 
				
			||||||
 | 
					    React.useState(false);
 | 
				
			||||||
 | 
					  const props = {
 | 
				
			||||||
 | 
					    ...commonProps,
 | 
				
			||||||
 | 
					    conversation: getDefaultConversation(),
 | 
				
			||||||
 | 
					    localDeleteWarningShown,
 | 
				
			||||||
 | 
					    isDeleteSyncSendEnabled: false,
 | 
				
			||||||
 | 
					    setLocalDeleteWarningShown: () => setLocalDeleteWarningShown(true),
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const theme = useContext(StorybookThemeContext);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return <ConversationHeader {...props} theme={theme} />;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,6 +38,7 @@ import {
 | 
				
			||||||
  MessageRequestState,
 | 
					  MessageRequestState,
 | 
				
			||||||
} from './MessageRequestActionsConfirmation';
 | 
					} from './MessageRequestActionsConfirmation';
 | 
				
			||||||
import type { MinimalConversation } from '../../hooks/useMinimalConversation';
 | 
					import type { MinimalConversation } from '../../hooks/useMinimalConversation';
 | 
				
			||||||
 | 
					import { LocalDeleteWarningModal } from '../LocalDeleteWarningModal';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function HeaderInfoTitle({
 | 
					function HeaderInfoTitle({
 | 
				
			||||||
  name,
 | 
					  name,
 | 
				
			||||||
| 
						 | 
					@ -92,6 +93,8 @@ export type PropsDataType = {
 | 
				
			||||||
  conversationName: ContactNameData;
 | 
					  conversationName: ContactNameData;
 | 
				
			||||||
  hasPanelShowing?: boolean;
 | 
					  hasPanelShowing?: boolean;
 | 
				
			||||||
  hasStories?: HasStories;
 | 
					  hasStories?: HasStories;
 | 
				
			||||||
 | 
					  localDeleteWarningShown: boolean;
 | 
				
			||||||
 | 
					  isDeleteSyncSendEnabled: boolean;
 | 
				
			||||||
  isMissingMandatoryProfileSharing?: boolean;
 | 
					  isMissingMandatoryProfileSharing?: boolean;
 | 
				
			||||||
  isSelectMode: boolean;
 | 
					  isSelectMode: boolean;
 | 
				
			||||||
  isSignalConversation?: boolean;
 | 
					  isSignalConversation?: boolean;
 | 
				
			||||||
| 
						 | 
					@ -102,6 +105,8 @@ export type PropsDataType = {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type PropsActionsType = {
 | 
					export type PropsActionsType = {
 | 
				
			||||||
 | 
					  setLocalDeleteWarningShown: () => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onConversationAccept: () => void;
 | 
					  onConversationAccept: () => void;
 | 
				
			||||||
  onConversationArchive: () => void;
 | 
					  onConversationArchive: () => void;
 | 
				
			||||||
  onConversationBlock: () => void;
 | 
					  onConversationBlock: () => void;
 | 
				
			||||||
| 
						 | 
					@ -147,10 +152,12 @@ export const ConversationHeader = memo(function ConversationHeader({
 | 
				
			||||||
  hasPanelShowing,
 | 
					  hasPanelShowing,
 | 
				
			||||||
  hasStories,
 | 
					  hasStories,
 | 
				
			||||||
  i18n,
 | 
					  i18n,
 | 
				
			||||||
 | 
					  isDeleteSyncSendEnabled,
 | 
				
			||||||
  isMissingMandatoryProfileSharing,
 | 
					  isMissingMandatoryProfileSharing,
 | 
				
			||||||
  isSelectMode,
 | 
					  isSelectMode,
 | 
				
			||||||
  isSignalConversation,
 | 
					  isSignalConversation,
 | 
				
			||||||
  isSMSOnly,
 | 
					  isSMSOnly,
 | 
				
			||||||
 | 
					  localDeleteWarningShown,
 | 
				
			||||||
  onConversationAccept,
 | 
					  onConversationAccept,
 | 
				
			||||||
  onConversationArchive,
 | 
					  onConversationArchive,
 | 
				
			||||||
  onConversationBlock,
 | 
					  onConversationBlock,
 | 
				
			||||||
| 
						 | 
					@ -174,6 +181,7 @@ export const ConversationHeader = memo(function ConversationHeader({
 | 
				
			||||||
  onViewRecentMedia,
 | 
					  onViewRecentMedia,
 | 
				
			||||||
  onViewUserStories,
 | 
					  onViewUserStories,
 | 
				
			||||||
  outgoingCallButtonStyle,
 | 
					  outgoingCallButtonStyle,
 | 
				
			||||||
 | 
					  setLocalDeleteWarningShown,
 | 
				
			||||||
  sharedGroupNames,
 | 
					  sharedGroupNames,
 | 
				
			||||||
  theme,
 | 
					  theme,
 | 
				
			||||||
}: PropsType): JSX.Element | null {
 | 
					}: PropsType): JSX.Element | null {
 | 
				
			||||||
| 
						 | 
					@ -223,13 +231,16 @@ export const ConversationHeader = memo(function ConversationHeader({
 | 
				
			||||||
      {hasDeleteMessagesConfirmation && (
 | 
					      {hasDeleteMessagesConfirmation && (
 | 
				
			||||||
        <DeleteMessagesConfirmationDialog
 | 
					        <DeleteMessagesConfirmationDialog
 | 
				
			||||||
          i18n={i18n}
 | 
					          i18n={i18n}
 | 
				
			||||||
          onDestoryMessages={() => {
 | 
					          isDeleteSyncSendEnabled={isDeleteSyncSendEnabled}
 | 
				
			||||||
 | 
					          localDeleteWarningShown={localDeleteWarningShown}
 | 
				
			||||||
 | 
					          onDestroyMessages={() => {
 | 
				
			||||||
            setHasDeleteMessagesConfirmation(false);
 | 
					            setHasDeleteMessagesConfirmation(false);
 | 
				
			||||||
            onConversationDeleteMessages();
 | 
					            onConversationDeleteMessages();
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
          onClose={() => {
 | 
					          onClose={() => {
 | 
				
			||||||
            setHasDeleteMessagesConfirmation(false);
 | 
					            setHasDeleteMessagesConfirmation(false);
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
 | 
					          setLocalDeleteWarningShown={setLocalDeleteWarningShown}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
      {hasLeaveGroupConfirmation && (
 | 
					      {hasLeaveGroupConfirmation && (
 | 
				
			||||||
| 
						 | 
					@ -923,14 +934,29 @@ function CannotLeaveGroupBecauseYouAreLastAdminAlert({
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function DeleteMessagesConfirmationDialog({
 | 
					function DeleteMessagesConfirmationDialog({
 | 
				
			||||||
 | 
					  isDeleteSyncSendEnabled,
 | 
				
			||||||
  i18n,
 | 
					  i18n,
 | 
				
			||||||
  onDestoryMessages,
 | 
					  localDeleteWarningShown,
 | 
				
			||||||
 | 
					  onDestroyMessages,
 | 
				
			||||||
  onClose,
 | 
					  onClose,
 | 
				
			||||||
 | 
					  setLocalDeleteWarningShown,
 | 
				
			||||||
}: {
 | 
					}: {
 | 
				
			||||||
 | 
					  isDeleteSyncSendEnabled: boolean;
 | 
				
			||||||
  i18n: LocalizerType;
 | 
					  i18n: LocalizerType;
 | 
				
			||||||
  onDestoryMessages: () => void;
 | 
					  localDeleteWarningShown: boolean;
 | 
				
			||||||
 | 
					  onDestroyMessages: () => void;
 | 
				
			||||||
  onClose: () => void;
 | 
					  onClose: () => void;
 | 
				
			||||||
 | 
					  setLocalDeleteWarningShown: () => void;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
 | 
					  if (!localDeleteWarningShown && isDeleteSyncSendEnabled) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <LocalDeleteWarningModal
 | 
				
			||||||
 | 
					        i18n={i18n}
 | 
				
			||||||
 | 
					        onClose={setLocalDeleteWarningShown}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <ConfirmationDialog
 | 
					    <ConfirmationDialog
 | 
				
			||||||
      dialogName="ConversationHeader.destroyMessages"
 | 
					      dialogName="ConversationHeader.destroyMessages"
 | 
				
			||||||
| 
						 | 
					@ -939,7 +965,7 @@ function DeleteMessagesConfirmationDialog({
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
      actions={[
 | 
					      actions={[
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          action: onDestoryMessages,
 | 
					          action: onDestroyMessages,
 | 
				
			||||||
          style: 'negative',
 | 
					          style: 'negative',
 | 
				
			||||||
          text: i18n('icu:delete'),
 | 
					          text: i18n('icu:delete'),
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										113
									
								
								ts/messageModifiers/DeletesForMe.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								ts/messageModifiers/DeletesForMe.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,113 @@
 | 
				
			||||||
 | 
					// Copyright 2020 Signal Messenger, LLC
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
 | 
					import * as Errors from '../types/errors';
 | 
				
			||||||
 | 
					import { drop } from '../util/drop';
 | 
				
			||||||
 | 
					import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { MessageAttributesType } from '../model-types';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					  ConversationToDelete,
 | 
				
			||||||
 | 
					  MessageToDelete,
 | 
				
			||||||
 | 
					} from '../textsecure/messageReceiverEvents';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  deleteMessage,
 | 
				
			||||||
 | 
					  doesMessageMatch,
 | 
				
			||||||
 | 
					  getConversationFromTarget,
 | 
				
			||||||
 | 
					  getMessageQueryFromTarget,
 | 
				
			||||||
 | 
					} from '../util/deleteForMe';
 | 
				
			||||||
 | 
					import dataInterface from '../sql/Client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { removeSyncTaskById } = dataInterface;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type DeleteForMeAttributesType = {
 | 
				
			||||||
 | 
					  conversation: ConversationToDelete;
 | 
				
			||||||
 | 
					  envelopeId: string;
 | 
				
			||||||
 | 
					  message: MessageToDelete;
 | 
				
			||||||
 | 
					  syncTaskId: string;
 | 
				
			||||||
 | 
					  timestamp: number;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const deletes = new Map<string, DeleteForMeAttributesType>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function remove(item: DeleteForMeAttributesType): Promise<void> {
 | 
				
			||||||
 | 
					  await removeSyncTaskById(item.syncTaskId);
 | 
				
			||||||
 | 
					  deletes.delete(item.envelopeId);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function forMessage(
 | 
				
			||||||
 | 
					  messageAttributes: MessageAttributesType
 | 
				
			||||||
 | 
					): Promise<Array<DeleteForMeAttributesType>> {
 | 
				
			||||||
 | 
					  const sentTimestamps = getMessageSentTimestampSet(messageAttributes);
 | 
				
			||||||
 | 
					  const deleteValues = Array.from(deletes.values());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const matchingDeletes = deleteValues.filter(item => {
 | 
				
			||||||
 | 
					    const itemConversation = getConversationFromTarget(item.conversation);
 | 
				
			||||||
 | 
					    const query = getMessageQueryFromTarget(item.message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!itemConversation) {
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return doesMessageMatch({
 | 
				
			||||||
 | 
					      conversationId: itemConversation.id,
 | 
				
			||||||
 | 
					      message: messageAttributes,
 | 
				
			||||||
 | 
					      query,
 | 
				
			||||||
 | 
					      sentTimestamps,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!matchingDeletes.length) {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  log.info('Found early DeleteForMe for message');
 | 
				
			||||||
 | 
					  await Promise.all(
 | 
				
			||||||
 | 
					    matchingDeletes.map(async item => {
 | 
				
			||||||
 | 
					      await remove(item);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  return matchingDeletes;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function onDelete(item: DeleteForMeAttributesType): Promise<void> {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const conversation = getConversationFromTarget(item.conversation);
 | 
				
			||||||
 | 
					    const message = getMessageQueryFromTarget(item.message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const logId = `DeletesForMe.onDelete(sentAt=${message.sentAt},timestamp=${item.timestamp},envelopeId=${item.envelopeId})`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    deletes.set(item.envelopeId, item);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!conversation) {
 | 
				
			||||||
 | 
					      log.warn(`${logId}: Conversation not found!`);
 | 
				
			||||||
 | 
					      await remove(item);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Do not await, since this a can deadlock the queue
 | 
				
			||||||
 | 
					    drop(
 | 
				
			||||||
 | 
					      conversation.queueJob('DeletesForMe.onDelete', async () => {
 | 
				
			||||||
 | 
					        log.info(`${logId}: Starting...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const result = await deleteMessage(
 | 
				
			||||||
 | 
					          conversation.id,
 | 
				
			||||||
 | 
					          item.message,
 | 
				
			||||||
 | 
					          logId
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        if (result) {
 | 
				
			||||||
 | 
					          await remove(item);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        log.info(`${logId}: Complete (result=${result})`);
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    log.error(
 | 
				
			||||||
 | 
					      `DeletesForMe.onDelete(task=${item.syncTaskId},envelopeId=${item.envelopeId}): Error`,
 | 
				
			||||||
 | 
					      Errors.toLogFormat(error)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    await remove(item);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -165,6 +165,12 @@ import OS from '../util/os/osMain';
 | 
				
			||||||
import { getMessageAuthorText } from '../util/getMessageAuthorText';
 | 
					import { getMessageAuthorText } from '../util/getMessageAuthorText';
 | 
				
			||||||
import { downscaleOutgoingAttachment } from '../util/attachments';
 | 
					import { downscaleOutgoingAttachment } from '../util/attachments';
 | 
				
			||||||
import { MessageRequestResponseEvent } from '../types/MessageRequestResponseEvent';
 | 
					import { MessageRequestResponseEvent } from '../types/MessageRequestResponseEvent';
 | 
				
			||||||
 | 
					import type { MessageToDelete } from '../textsecure/messageReceiverEvents';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getConversationToDelete,
 | 
				
			||||||
 | 
					  getMessageToDelete,
 | 
				
			||||||
 | 
					} from '../util/deleteForMe';
 | 
				
			||||||
 | 
					import { isEnabled } from '../RemoteConfig';
 | 
				
			||||||
import { getCallHistorySelector } from '../state/selectors/callHistory';
 | 
					import { getCallHistorySelector } from '../state/selectors/callHistory';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* eslint-disable more/no-then */
 | 
					/* eslint-disable more/no-then */
 | 
				
			||||||
| 
						 | 
					@ -186,6 +192,7 @@ const {
 | 
				
			||||||
  getOlderMessagesByConversation,
 | 
					  getOlderMessagesByConversation,
 | 
				
			||||||
  getMessageMetricsForConversation,
 | 
					  getMessageMetricsForConversation,
 | 
				
			||||||
  getMessageById,
 | 
					  getMessageById,
 | 
				
			||||||
 | 
					  getMostRecentAddressableMessages,
 | 
				
			||||||
  getNewerMessagesByConversation,
 | 
					  getNewerMessagesByConversation,
 | 
				
			||||||
} = window.Signal.Data;
 | 
					} = window.Signal.Data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2234,7 +2241,7 @@ export class ConversationModel extends window.Backbone
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (isDelete) {
 | 
					        if (isDelete) {
 | 
				
			||||||
          await this.destroyMessages();
 | 
					          await this.destroyMessages({ source: 'message-request' });
 | 
				
			||||||
          void this.updateLastMessage();
 | 
					          void this.updateLastMessage();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4449,7 +4456,6 @@ export class ConversationModel extends window.Backbone
 | 
				
			||||||
      source: providedSource,
 | 
					      source: providedSource,
 | 
				
			||||||
      fromSync = false,
 | 
					      fromSync = false,
 | 
				
			||||||
      isInitialSync = false,
 | 
					      isInitialSync = false,
 | 
				
			||||||
      fromGroupUpdate = false,
 | 
					 | 
				
			||||||
    }: {
 | 
					    }: {
 | 
				
			||||||
      reason: string;
 | 
					      reason: string;
 | 
				
			||||||
      receivedAt?: number;
 | 
					      receivedAt?: number;
 | 
				
			||||||
| 
						 | 
					@ -4458,7 +4464,6 @@ export class ConversationModel extends window.Backbone
 | 
				
			||||||
      source?: string;
 | 
					      source?: string;
 | 
				
			||||||
      fromSync?: boolean;
 | 
					      fromSync?: boolean;
 | 
				
			||||||
      isInitialSync?: boolean;
 | 
					      isInitialSync?: boolean;
 | 
				
			||||||
      fromGroupUpdate?: boolean;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  ): Promise<boolean | null | MessageModel | void> {
 | 
					  ): Promise<boolean | null | MessageModel | void> {
 | 
				
			||||||
    const isSetByOther = providedSource || providedSentAt !== undefined;
 | 
					    const isSetByOther = providedSource || providedSentAt !== undefined;
 | 
				
			||||||
| 
						 | 
					@ -4554,7 +4559,7 @@ export class ConversationModel extends window.Backbone
 | 
				
			||||||
      (isInitialSync && isFromSyncOperation) || isFromMe || isNoteToSelf;
 | 
					      (isInitialSync && isFromSyncOperation) || isFromMe || isNoteToSelf;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const id = generateGuid();
 | 
					    const id = generateGuid();
 | 
				
			||||||
    const model = new window.Whisper.Message({
 | 
					    const attributes = {
 | 
				
			||||||
      id,
 | 
					      id,
 | 
				
			||||||
      conversationId: this.id,
 | 
					      conversationId: this.id,
 | 
				
			||||||
      expirationTimerUpdate: {
 | 
					      expirationTimerUpdate: {
 | 
				
			||||||
| 
						 | 
					@ -4562,7 +4567,6 @@ export class ConversationModel extends window.Backbone
 | 
				
			||||||
        source,
 | 
					        source,
 | 
				
			||||||
        sourceServiceId,
 | 
					        sourceServiceId,
 | 
				
			||||||
        fromSync,
 | 
					        fromSync,
 | 
				
			||||||
        fromGroupUpdate,
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
 | 
					      flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
 | 
				
			||||||
      readStatus: shouldBeRead ? ReadStatus.Read : ReadStatus.Unread,
 | 
					      readStatus: shouldBeRead ? ReadStatus.Read : ReadStatus.Unread,
 | 
				
			||||||
| 
						 | 
					@ -4570,18 +4574,18 @@ export class ConversationModel extends window.Backbone
 | 
				
			||||||
      received_at: receivedAt ?? incrementMessageCounter(),
 | 
					      received_at: receivedAt ?? incrementMessageCounter(),
 | 
				
			||||||
      seenStatus: shouldBeRead ? SeenStatus.Seen : SeenStatus.Unseen,
 | 
					      seenStatus: shouldBeRead ? SeenStatus.Seen : SeenStatus.Unseen,
 | 
				
			||||||
      sent_at: sentAt,
 | 
					      sent_at: sentAt,
 | 
				
			||||||
      type: 'timer-notification',
 | 
					      timestamp: sentAt,
 | 
				
			||||||
      // TODO: DESKTOP-722
 | 
					      type: 'timer-notification' as const,
 | 
				
			||||||
    } as unknown as MessageAttributesType);
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await window.Signal.Data.saveMessage(model.attributes, {
 | 
					    await window.Signal.Data.saveMessage(attributes, {
 | 
				
			||||||
      ourAci: window.textsecure.storage.user.getCheckedAci(),
 | 
					      ourAci: window.textsecure.storage.user.getCheckedAci(),
 | 
				
			||||||
      forceSave: true,
 | 
					      forceSave: true,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const message = window.MessageCache.__DEPRECATED$register(
 | 
					    const message = window.MessageCache.__DEPRECATED$register(
 | 
				
			||||||
      id,
 | 
					      id,
 | 
				
			||||||
      model,
 | 
					      new window.Whisper.Message(attributes),
 | 
				
			||||||
      'updateExpirationTimer'
 | 
					      'updateExpirationTimer'
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4589,7 +4593,7 @@ export class ConversationModel extends window.Backbone
 | 
				
			||||||
    void this.updateUnread();
 | 
					    void this.updateUnread();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    log.info(
 | 
					    log.info(
 | 
				
			||||||
      `${logId}: added a notification received_at=${model.get('received_at')}`
 | 
					      `${logId}: added a notification received_at=${message.get('received_at')}`
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return message;
 | 
					    return message;
 | 
				
			||||||
| 
						 | 
					@ -4978,7 +4982,29 @@ export class ConversationModel extends window.Backbone
 | 
				
			||||||
    this.contactCollection!.reset(members);
 | 
					    this.contactCollection!.reset(members);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async destroyMessages(): Promise<void> {
 | 
					  async destroyMessages({
 | 
				
			||||||
 | 
					    source,
 | 
				
			||||||
 | 
					  }: {
 | 
				
			||||||
 | 
					    source: 'message-request' | 'local-delete-sync' | 'local-delete';
 | 
				
			||||||
 | 
					  }): Promise<void> {
 | 
				
			||||||
 | 
					    const logId = `destroyMessages(${this.idForLogging()})/${source}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log.info(`${logId}: Queuing job on conversation`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.queueJob(logId, async () => {
 | 
				
			||||||
 | 
					      log.info(`${logId}: Starting...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.destroyMessagesInner({ logId, source });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async destroyMessagesInner({
 | 
				
			||||||
 | 
					    logId,
 | 
				
			||||||
 | 
					    source,
 | 
				
			||||||
 | 
					  }: {
 | 
				
			||||||
 | 
					    logId: string;
 | 
				
			||||||
 | 
					    source: 'message-request' | 'local-delete-sync' | 'local-delete';
 | 
				
			||||||
 | 
					  }): Promise<void> {
 | 
				
			||||||
    this.set({
 | 
					    this.set({
 | 
				
			||||||
      lastMessage: null,
 | 
					      lastMessage: null,
 | 
				
			||||||
      lastMessageAuthor: null,
 | 
					      lastMessageAuthor: null,
 | 
				
			||||||
| 
						 | 
					@ -4988,9 +5014,50 @@ export class ConversationModel extends window.Backbone
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    window.Signal.Data.updateConversation(this.attributes);
 | 
					    window.Signal.Data.updateConversation(this.attributes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await window.Signal.Data.removeAllMessagesInConversation(this.id, {
 | 
					    if (source === 'local-delete' && isEnabled('desktop.deleteSync.send')) {
 | 
				
			||||||
 | 
					      log.info(`${logId}: Preparing sync message`);
 | 
				
			||||||
 | 
					      const timestamp = Date.now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const addressableMessages = await getMostRecentAddressableMessages(
 | 
				
			||||||
 | 
					        this.id
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      const mostRecentMessages: Array<MessageToDelete> = addressableMessages
 | 
				
			||||||
 | 
					        .map(getMessageToDelete)
 | 
				
			||||||
 | 
					        .filter(isNotNil)
 | 
				
			||||||
 | 
					        .slice(0, 5);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (mostRecentMessages.length > 0) {
 | 
				
			||||||
 | 
					        await singleProtoJobQueue.add(
 | 
				
			||||||
 | 
					          MessageSender.getDeleteForMeSyncMessage([
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              type: 'delete-conversation',
 | 
				
			||||||
 | 
					              conversation: getConversationToDelete(this.attributes),
 | 
				
			||||||
 | 
					              isFullDelete: true,
 | 
				
			||||||
 | 
					              mostRecentMessages,
 | 
				
			||||||
 | 
					              timestamp,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ])
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        await singleProtoJobQueue.add(
 | 
				
			||||||
 | 
					          MessageSender.getDeleteForMeSyncMessage([
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              type: 'delete-local-conversation',
 | 
				
			||||||
 | 
					              conversation: getConversationToDelete(this.attributes),
 | 
				
			||||||
 | 
					              timestamp,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ])
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      log.info(`${logId}: Sync message queue complete`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log.info(`${logId}: Starting delete`);
 | 
				
			||||||
 | 
					    await window.Signal.Data.removeMessagesInConversation(this.id, {
 | 
				
			||||||
      logId: this.idForLogging(),
 | 
					      logId: this.idForLogging(),
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    log.info(`${logId}: Delete complete`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getTitle(options?: { isShort?: boolean }): string {
 | 
					  getTitle(options?: { isShort?: boolean }): string {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -68,7 +68,11 @@ import {
 | 
				
			||||||
} from '../util/whatTypeOfConversation';
 | 
					} from '../util/whatTypeOfConversation';
 | 
				
			||||||
import { handleMessageSend } from '../util/handleMessageSend';
 | 
					import { handleMessageSend } from '../util/handleMessageSend';
 | 
				
			||||||
import { getSendOptions } from '../util/getSendOptions';
 | 
					import { getSendOptions } from '../util/getSendOptions';
 | 
				
			||||||
import { modifyTargetMessage } from '../util/modifyTargetMessage';
 | 
					import {
 | 
				
			||||||
 | 
					  modifyTargetMessage,
 | 
				
			||||||
 | 
					  ModifyTargetMessageResult,
 | 
				
			||||||
 | 
					} from '../util/modifyTargetMessage';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  getMessagePropStatus,
 | 
					  getMessagePropStatus,
 | 
				
			||||||
  hasErrors,
 | 
					  hasErrors,
 | 
				
			||||||
| 
						 | 
					@ -2177,7 +2181,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
 | 
				
			||||||
                receivedAt: message.get('received_at'),
 | 
					                receivedAt: message.get('received_at'),
 | 
				
			||||||
                receivedAtMS: message.get('received_at_ms'),
 | 
					                receivedAtMS: message.get('received_at_ms'),
 | 
				
			||||||
                sentAt: message.get('sent_at'),
 | 
					                sentAt: message.get('sent_at'),
 | 
				
			||||||
                fromGroupUpdate: isGroupUpdate(message.attributes),
 | 
					 | 
				
			||||||
                reason: idLog,
 | 
					                reason: idLog,
 | 
				
			||||||
              });
 | 
					              });
 | 
				
			||||||
            } else if (
 | 
					            } else if (
 | 
				
			||||||
| 
						 | 
					@ -2297,7 +2300,11 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const isFirstRun = true;
 | 
					        const isFirstRun = true;
 | 
				
			||||||
        await this.modifyTargetMessage(conversation, isFirstRun);
 | 
					        const result = await this.modifyTargetMessage(conversation, isFirstRun);
 | 
				
			||||||
 | 
					        if (result === ModifyTargetMessageResult.Deleted) {
 | 
				
			||||||
 | 
					          confirm();
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        log.info(`${idLog}: Batching save`);
 | 
					        log.info(`${idLog}: Batching save`);
 | 
				
			||||||
        void this.saveAndNotify(conversation, confirm);
 | 
					        void this.saveAndNotify(conversation, confirm);
 | 
				
			||||||
| 
						 | 
					@ -2320,10 +2327,16 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
 | 
				
			||||||
    // Once the message is saved to DB, we queue attachment downloads
 | 
					    // Once the message is saved to DB, we queue attachment downloads
 | 
				
			||||||
    await this.handleAttachmentDownloadsForNewMessage(conversation);
 | 
					    await this.handleAttachmentDownloadsForNewMessage(conversation);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    conversation.trigger('newmessage', this);
 | 
					    // We'd like to check for deletions before scheduling downloads, but if an edit comes
 | 
				
			||||||
 | 
					    //   in, we want to have kicked off attachment downloads for the original message.
 | 
				
			||||||
    const isFirstRun = false;
 | 
					    const isFirstRun = false;
 | 
				
			||||||
    await this.modifyTargetMessage(conversation, isFirstRun);
 | 
					    const result = await this.modifyTargetMessage(conversation, isFirstRun);
 | 
				
			||||||
 | 
					    if (result === ModifyTargetMessageResult.Deleted) {
 | 
				
			||||||
 | 
					      confirm();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    conversation.trigger('newmessage', this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (await shouldReplyNotifyUser(this.attributes, conversation)) {
 | 
					    if (await shouldReplyNotifyUser(this.attributes, conversation)) {
 | 
				
			||||||
      await conversation.notify(this);
 | 
					      await conversation.notify(this);
 | 
				
			||||||
| 
						 | 
					@ -2377,7 +2390,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
 | 
				
			||||||
  async modifyTargetMessage(
 | 
					  async modifyTargetMessage(
 | 
				
			||||||
    conversation: ConversationModel,
 | 
					    conversation: ConversationModel,
 | 
				
			||||||
    isFirstRun: boolean
 | 
					    isFirstRun: boolean
 | 
				
			||||||
  ): Promise<void> {
 | 
					  ): Promise<ModifyTargetMessageResult> {
 | 
				
			||||||
    return modifyTargetMessage(this, conversation, {
 | 
					    return modifyTargetMessage(this, conversation, {
 | 
				
			||||||
      isFirstRun,
 | 
					      isFirstRun,
 | 
				
			||||||
      skipEdits: false,
 | 
					      skipEdits: false,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -115,7 +115,7 @@ const exclusiveInterface: ClientExclusiveInterface = {
 | 
				
			||||||
  flushUpdateConversationBatcher,
 | 
					  flushUpdateConversationBatcher,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  shutdown,
 | 
					  shutdown,
 | 
				
			||||||
  removeAllMessagesInConversation,
 | 
					  removeMessagesInConversation,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  removeOtherData,
 | 
					  removeOtherData,
 | 
				
			||||||
  cleanupOrphanedAttachments,
 | 
					  cleanupOrphanedAttachments,
 | 
				
			||||||
| 
						 | 
					@ -592,6 +592,21 @@ async function removeMessage(id: string): Promise<void> {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function deleteAndCleanup(
 | 
				
			||||||
 | 
					  messages: Array<MessageAttributesType>,
 | 
				
			||||||
 | 
					  logId: string
 | 
				
			||||||
 | 
					): Promise<void> {
 | 
				
			||||||
 | 
					  const ids = messages.map(message => message.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  log.info(`deleteAndCleanup/${logId}: Deleting ${ids.length} messages...`);
 | 
				
			||||||
 | 
					  await channels.removeMessages(ids);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  log.info(`deleteAndCleanup/${logId}: Cleanup for ${ids.length} messages...`);
 | 
				
			||||||
 | 
					  await _cleanupMessages(messages);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  log.info(`deleteAndCleanup/${logId}: Complete`);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function _cleanupMessages(
 | 
					async function _cleanupMessages(
 | 
				
			||||||
  messages: ReadonlyArray<MessageAttributesType>
 | 
					  messages: ReadonlyArray<MessageAttributesType>
 | 
				
			||||||
): Promise<void> {
 | 
					): Promise<void> {
 | 
				
			||||||
| 
						 | 
					@ -664,12 +679,14 @@ async function getConversationRangeCenteredOnMessage(
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function removeAllMessagesInConversation(
 | 
					async function removeMessagesInConversation(
 | 
				
			||||||
  conversationId: string,
 | 
					  conversationId: string,
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    logId,
 | 
					    logId,
 | 
				
			||||||
 | 
					    receivedAt,
 | 
				
			||||||
  }: {
 | 
					  }: {
 | 
				
			||||||
    logId: string;
 | 
					    logId: string;
 | 
				
			||||||
 | 
					    receivedAt?: number;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
): Promise<void> {
 | 
					): Promise<void> {
 | 
				
			||||||
  let messages;
 | 
					  let messages;
 | 
				
			||||||
| 
						 | 
					@ -685,6 +702,7 @@ async function removeAllMessagesInConversation(
 | 
				
			||||||
      conversationId,
 | 
					      conversationId,
 | 
				
			||||||
      limit: chunkSize,
 | 
					      limit: chunkSize,
 | 
				
			||||||
      includeStoryReplies: true,
 | 
					      includeStoryReplies: true,
 | 
				
			||||||
 | 
					      receivedAt,
 | 
				
			||||||
      storyId: undefined,
 | 
					      storyId: undefined,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -692,15 +710,8 @@ async function removeAllMessagesInConversation(
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const ids = messages.map(message => message.id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    log.info(`removeAllMessagesInConversation/${logId}: Cleanup...`);
 | 
					 | 
				
			||||||
    // eslint-disable-next-line no-await-in-loop
 | 
					    // eslint-disable-next-line no-await-in-loop
 | 
				
			||||||
    await _cleanupMessages(messages);
 | 
					    await deleteAndCleanup(messages, logId);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    log.info(`removeAllMessagesInConversation/${logId}: Deleting...`);
 | 
					 | 
				
			||||||
    // eslint-disable-next-line no-await-in-loop
 | 
					 | 
				
			||||||
    await channels.removeMessages(ids);
 | 
					 | 
				
			||||||
  } while (messages.length > 0);
 | 
					  } while (messages.length > 0);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,6 +32,7 @@ import type {
 | 
				
			||||||
import type { CallLinkStateType, CallLinkType } from '../types/CallLink';
 | 
					import type { CallLinkStateType, CallLinkType } from '../types/CallLink';
 | 
				
			||||||
import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
 | 
					import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
 | 
				
			||||||
import type { GroupSendEndorsementsData } from '../types/GroupSendEndorsements';
 | 
					import type { GroupSendEndorsementsData } from '../types/GroupSendEndorsements';
 | 
				
			||||||
 | 
					import type { SyncTaskType } from '../util/syncTasks';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type AdjacentMessagesByConversationOptionsType = Readonly<{
 | 
					export type AdjacentMessagesByConversationOptionsType = Readonly<{
 | 
				
			||||||
  conversationId: string;
 | 
					  conversationId: string;
 | 
				
			||||||
| 
						 | 
					@ -716,6 +717,15 @@ export type DataInterface = {
 | 
				
			||||||
    ourAci: AciString,
 | 
					    ourAci: AciString,
 | 
				
			||||||
    opts: EditedMessageType
 | 
					    opts: EditedMessageType
 | 
				
			||||||
  ) => Promise<void>;
 | 
					  ) => Promise<void>;
 | 
				
			||||||
 | 
					  getMostRecentAddressableMessages: (
 | 
				
			||||||
 | 
					    conversationId: string,
 | 
				
			||||||
 | 
					    limit?: number
 | 
				
			||||||
 | 
					  ) => Promise<Array<MessageType>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  removeSyncTaskById: (id: string) => Promise<void>;
 | 
				
			||||||
 | 
					  saveSyncTasks: (tasks: Array<SyncTaskType>) => Promise<void>;
 | 
				
			||||||
 | 
					  getAllSyncTasks: () => Promise<Array<SyncTaskType>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getUnprocessedCount: () => Promise<number>;
 | 
					  getUnprocessedCount: () => Promise<number>;
 | 
				
			||||||
  getUnprocessedByIdsAndIncrementAttempts: (
 | 
					  getUnprocessedByIdsAndIncrementAttempts: (
 | 
				
			||||||
    ids: ReadonlyArray<string>
 | 
					    ids: ReadonlyArray<string>
 | 
				
			||||||
| 
						 | 
					@ -1043,10 +1053,11 @@ export type ClientExclusiveInterface = {
 | 
				
			||||||
  // Client-side only
 | 
					  // Client-side only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  shutdown: () => Promise<void>;
 | 
					  shutdown: () => Promise<void>;
 | 
				
			||||||
  removeAllMessagesInConversation: (
 | 
					  removeMessagesInConversation: (
 | 
				
			||||||
    conversationId: string,
 | 
					    conversationId: string,
 | 
				
			||||||
    options: {
 | 
					    options: {
 | 
				
			||||||
      logId: string;
 | 
					      logId: string;
 | 
				
			||||||
 | 
					      receivedAt?: number;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  ) => Promise<void>;
 | 
					  ) => Promise<void>;
 | 
				
			||||||
  removeOtherData: () => Promise<void>;
 | 
					  removeOtherData: () => Promise<void>;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										135
									
								
								ts/sql/Server.ts
									
										
									
									
									
								
							
							
						
						
									
										135
									
								
								ts/sql/Server.ts
									
										
									
									
									
								
							| 
						 | 
					@ -184,6 +184,9 @@ import {
 | 
				
			||||||
  attachmentDownloadJobSchema,
 | 
					  attachmentDownloadJobSchema,
 | 
				
			||||||
  type AttachmentDownloadJobType,
 | 
					  type AttachmentDownloadJobType,
 | 
				
			||||||
} from '../types/AttachmentDownload';
 | 
					} from '../types/AttachmentDownload';
 | 
				
			||||||
 | 
					import { MAX_SYNC_TASK_ATTEMPTS } from '../util/syncTasks.types';
 | 
				
			||||||
 | 
					import type { SyncTaskType } from '../util/syncTasks';
 | 
				
			||||||
 | 
					import { isMoreRecentThan } from '../util/timestamp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ConversationRow = Readonly<{
 | 
					type ConversationRow = Readonly<{
 | 
				
			||||||
  json: string;
 | 
					  json: string;
 | 
				
			||||||
| 
						 | 
					@ -360,6 +363,11 @@ const dataInterface: ServerInterface = {
 | 
				
			||||||
  getMessagesBetween,
 | 
					  getMessagesBetween,
 | 
				
			||||||
  getNearbyMessageFromDeletedSet,
 | 
					  getNearbyMessageFromDeletedSet,
 | 
				
			||||||
  saveEditedMessage,
 | 
					  saveEditedMessage,
 | 
				
			||||||
 | 
					  getMostRecentAddressableMessages,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  removeSyncTaskById,
 | 
				
			||||||
 | 
					  saveSyncTasks,
 | 
				
			||||||
 | 
					  getAllSyncTasks,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getUnprocessedCount,
 | 
					  getUnprocessedCount,
 | 
				
			||||||
  getUnprocessedByIdsAndIncrementAttempts,
 | 
					  getUnprocessedByIdsAndIncrementAttempts,
 | 
				
			||||||
| 
						 | 
					@ -2066,6 +2074,131 @@ function hasUserInitiatedMessages(conversationId: string): boolean {
 | 
				
			||||||
  return exists !== 0;
 | 
					  return exists !== 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getMostRecentAddressableMessages(
 | 
				
			||||||
 | 
					  conversationId: string,
 | 
				
			||||||
 | 
					  limit = 5
 | 
				
			||||||
 | 
					): Promise<Array<MessageType>> {
 | 
				
			||||||
 | 
					  const db = getReadonlyInstance();
 | 
				
			||||||
 | 
					  return getMostRecentAddressableMessagesSync(db, conversationId, limit);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getMostRecentAddressableMessagesSync(
 | 
				
			||||||
 | 
					  db: Database,
 | 
				
			||||||
 | 
					  conversationId: string,
 | 
				
			||||||
 | 
					  limit = 5
 | 
				
			||||||
 | 
					): Array<MessageType> {
 | 
				
			||||||
 | 
					  const [query, parameters] = sql`
 | 
				
			||||||
 | 
					    SELECT json FROM messages
 | 
				
			||||||
 | 
					    INDEXED BY messages_by_date_addressable
 | 
				
			||||||
 | 
					    WHERE
 | 
				
			||||||
 | 
					      conversationId IS ${conversationId} AND
 | 
				
			||||||
 | 
					      isAddressableMessage = 1
 | 
				
			||||||
 | 
					    ORDER BY received_at DESC, sent_at DESC
 | 
				
			||||||
 | 
					    LIMIT ${limit};
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const rows = db.prepare(query).all(parameters);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return rows.map(row => jsonToObject(row.json));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function removeSyncTaskById(id: string): Promise<void> {
 | 
				
			||||||
 | 
					  const db = await getWritableInstance();
 | 
				
			||||||
 | 
					  removeSyncTaskByIdSync(db, id);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export function removeSyncTaskByIdSync(db: Database, id: string): void {
 | 
				
			||||||
 | 
					  const [query, parameters] = sql`
 | 
				
			||||||
 | 
					    DELETE FROM syncTasks
 | 
				
			||||||
 | 
					    WHERE id IS ${id}
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  db.prepare(query).run(parameters);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					async function saveSyncTasks(tasks: Array<SyncTaskType>): Promise<void> {
 | 
				
			||||||
 | 
					  const db = await getWritableInstance();
 | 
				
			||||||
 | 
					  return saveSyncTasksSync(db, tasks);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export function saveSyncTasksSync(
 | 
				
			||||||
 | 
					  db: Database,
 | 
				
			||||||
 | 
					  tasks: Array<SyncTaskType>
 | 
				
			||||||
 | 
					): void {
 | 
				
			||||||
 | 
					  return db.transaction(() => {
 | 
				
			||||||
 | 
					    tasks.forEach(task => assertSync(saveSyncTaskSync(db, task)));
 | 
				
			||||||
 | 
					  })();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export function saveSyncTaskSync(db: Database, task: SyncTaskType): void {
 | 
				
			||||||
 | 
					  const { id, attempts, createdAt, data, envelopeId, sentAt, type } = task;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [query, parameters] = sql`
 | 
				
			||||||
 | 
					    INSERT INTO syncTasks (
 | 
				
			||||||
 | 
					      id,
 | 
				
			||||||
 | 
					      attempts,
 | 
				
			||||||
 | 
					      createdAt,
 | 
				
			||||||
 | 
					      data,
 | 
				
			||||||
 | 
					      envelopeId,
 | 
				
			||||||
 | 
					      sentAt,
 | 
				
			||||||
 | 
					      type
 | 
				
			||||||
 | 
					    ) VALUES (
 | 
				
			||||||
 | 
					      ${id},
 | 
				
			||||||
 | 
					      ${attempts},
 | 
				
			||||||
 | 
					      ${createdAt},
 | 
				
			||||||
 | 
					      ${objectToJSON(data)},
 | 
				
			||||||
 | 
					      ${envelopeId},
 | 
				
			||||||
 | 
					      ${sentAt},
 | 
				
			||||||
 | 
					      ${type}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  db.prepare(query).run(parameters);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					async function getAllSyncTasks(): Promise<Array<SyncTaskType>> {
 | 
				
			||||||
 | 
					  const db = await getWritableInstance();
 | 
				
			||||||
 | 
					  return getAllSyncTasksSync(db);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export function getAllSyncTasksSync(db: Database): Array<SyncTaskType> {
 | 
				
			||||||
 | 
					  return db.transaction(() => {
 | 
				
			||||||
 | 
					    const [selectAllQuery] = sql`
 | 
				
			||||||
 | 
					      SELECT * FROM syncTasks ORDER BY createdAt ASC, sentAt ASC, id ASC
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const rows = db.prepare(selectAllQuery).all();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const tasks: Array<SyncTaskType> = rows.map(row => ({
 | 
				
			||||||
 | 
					      ...row,
 | 
				
			||||||
 | 
					      data: jsonToObject(row.data),
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [query] = sql`
 | 
				
			||||||
 | 
					      UPDATE syncTasks
 | 
				
			||||||
 | 
					      SET attempts = attempts + 1
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					    db.prepare(query).run();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [toDelete, toReturn] = partition(tasks, task => {
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        isNormalNumber(task.attempts) &&
 | 
				
			||||||
 | 
					        task.attempts < MAX_SYNC_TASK_ATTEMPTS
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (isMoreRecentThan(task.createdAt, durations.WEEK)) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (toDelete.length > 0) {
 | 
				
			||||||
 | 
					      log.warn(`getAllSyncTasks: Removing ${toDelete.length} expired tasks`);
 | 
				
			||||||
 | 
					      toDelete.forEach(task => {
 | 
				
			||||||
 | 
					        assertSync(removeSyncTaskByIdSync(db, task.id));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return toReturn;
 | 
				
			||||||
 | 
					  })();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function saveMessageSync(
 | 
					function saveMessageSync(
 | 
				
			||||||
  db: Database,
 | 
					  db: Database,
 | 
				
			||||||
  data: MessageType,
 | 
					  data: MessageType,
 | 
				
			||||||
| 
						 | 
					@ -6036,6 +6169,7 @@ async function removeAll(): Promise<void> {
 | 
				
			||||||
      DELETE FROM storyDistributionMembers;
 | 
					      DELETE FROM storyDistributionMembers;
 | 
				
			||||||
      DELETE FROM storyDistributions;
 | 
					      DELETE FROM storyDistributions;
 | 
				
			||||||
      DELETE FROM storyReads;
 | 
					      DELETE FROM storyReads;
 | 
				
			||||||
 | 
					      DELETE FROM syncTasks;
 | 
				
			||||||
      DELETE FROM unprocessed;
 | 
					      DELETE FROM unprocessed;
 | 
				
			||||||
      DELETE FROM uninstalled_sticker_packs;
 | 
					      DELETE FROM uninstalled_sticker_packs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6078,6 +6212,7 @@ async function removeAllConfiguration(): Promise<void> {
 | 
				
			||||||
      DELETE FROM sendLogRecipients;
 | 
					      DELETE FROM sendLogRecipients;
 | 
				
			||||||
      DELETE FROM sessions;
 | 
					      DELETE FROM sessions;
 | 
				
			||||||
      DELETE FROM signedPreKeys;
 | 
					      DELETE FROM signedPreKeys;
 | 
				
			||||||
 | 
					      DELETE FROM syncTasks;
 | 
				
			||||||
      DELETE FROM unprocessed;
 | 
					      DELETE FROM unprocessed;
 | 
				
			||||||
      `
 | 
					      `
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,56 @@
 | 
				
			||||||
 | 
					// Copyright 2024 Signal Messenger, LLC
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { Database } from '@signalapp/better-sqlite3';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { LoggerType } from '../../types/Logging';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const version = 1060;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function updateToSchemaVersion1060(
 | 
				
			||||||
 | 
					  currentVersion: number,
 | 
				
			||||||
 | 
					  db: Database,
 | 
				
			||||||
 | 
					  logger: LoggerType
 | 
				
			||||||
 | 
					): void {
 | 
				
			||||||
 | 
					  if (currentVersion >= 1060) {
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  db.transaction(() => {
 | 
				
			||||||
 | 
					    db.exec(`
 | 
				
			||||||
 | 
					      ALTER TABLE messages
 | 
				
			||||||
 | 
					        ADD COLUMN isAddressableMessage INTEGER
 | 
				
			||||||
 | 
					        GENERATED ALWAYS AS (
 | 
				
			||||||
 | 
					          type IS NULL
 | 
				
			||||||
 | 
					          OR
 | 
				
			||||||
 | 
					          type IN (
 | 
				
			||||||
 | 
					            'incoming',
 | 
				
			||||||
 | 
					            'outgoing'
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					     
 | 
				
			||||||
 | 
					      CREATE INDEX messages_by_date_addressable
 | 
				
			||||||
 | 
					        ON messages (
 | 
				
			||||||
 | 
					          conversationId, isAddressableMessage, received_at, sent_at
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      CREATE TABLE syncTasks(
 | 
				
			||||||
 | 
					        id TEXT PRIMARY KEY NOT NULL,
 | 
				
			||||||
 | 
					        attempts INTEGER NOT NULL,
 | 
				
			||||||
 | 
					        createdAt INTEGER NOT NULL,
 | 
				
			||||||
 | 
					        data TEXT NOT NULL,
 | 
				
			||||||
 | 
					        envelopeId TEXT NOT NULL,
 | 
				
			||||||
 | 
					        sentAt INTEGER NOT NULL,
 | 
				
			||||||
 | 
					        type TEXT NOT NULL
 | 
				
			||||||
 | 
					      ) STRICT;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      CREATE INDEX syncTasks_order ON syncTasks (
 | 
				
			||||||
 | 
					        createdAt, sentAt, id
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    `);
 | 
				
			||||||
 | 
					  })();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  db.pragma('user_version = 1060');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  logger.info('updateToSchemaVersion1060: success!');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -80,10 +80,11 @@ import { updateToSchemaVersion1010 } from './1010-call-links-table';
 | 
				
			||||||
import { updateToSchemaVersion1020 } from './1020-self-merges';
 | 
					import { updateToSchemaVersion1020 } from './1020-self-merges';
 | 
				
			||||||
import { updateToSchemaVersion1030 } from './1030-unblock-event';
 | 
					import { updateToSchemaVersion1030 } from './1030-unblock-event';
 | 
				
			||||||
import { updateToSchemaVersion1040 } from './1040-undownloaded-backed-up-media';
 | 
					import { updateToSchemaVersion1040 } from './1040-undownloaded-backed-up-media';
 | 
				
			||||||
 | 
					import { updateToSchemaVersion1050 } from './1050-group-send-endorsements';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  updateToSchemaVersion1050,
 | 
					  updateToSchemaVersion1060,
 | 
				
			||||||
  version as MAX_VERSION,
 | 
					  version as MAX_VERSION,
 | 
				
			||||||
} from './1050-group-send-endorsements';
 | 
					} from './1060-addressable-messages-and-sync-tasks';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function updateToSchemaVersion1(
 | 
					function updateToSchemaVersion1(
 | 
				
			||||||
  currentVersion: number,
 | 
					  currentVersion: number,
 | 
				
			||||||
| 
						 | 
					@ -2025,12 +2026,14 @@ export const SCHEMA_VERSIONS = [
 | 
				
			||||||
  updateToSchemaVersion970,
 | 
					  updateToSchemaVersion970,
 | 
				
			||||||
  updateToSchemaVersion980,
 | 
					  updateToSchemaVersion980,
 | 
				
			||||||
  updateToSchemaVersion990,
 | 
					  updateToSchemaVersion990,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  updateToSchemaVersion1000,
 | 
					  updateToSchemaVersion1000,
 | 
				
			||||||
  updateToSchemaVersion1010,
 | 
					  updateToSchemaVersion1010,
 | 
				
			||||||
  updateToSchemaVersion1020,
 | 
					  updateToSchemaVersion1020,
 | 
				
			||||||
  updateToSchemaVersion1030,
 | 
					  updateToSchemaVersion1030,
 | 
				
			||||||
  updateToSchemaVersion1040,
 | 
					  updateToSchemaVersion1040,
 | 
				
			||||||
  updateToSchemaVersion1050,
 | 
					  updateToSchemaVersion1050,
 | 
				
			||||||
 | 
					  updateToSchemaVersion1060,
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class DBVersionFromFutureError extends Error {
 | 
					export class DBVersionFromFutureError extends Error {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { ThunkAction } from 'redux-thunk';
 | 
					import type { ThunkAction } from 'redux-thunk';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					  chunk,
 | 
				
			||||||
  difference,
 | 
					  difference,
 | 
				
			||||||
  fromPairs,
 | 
					  fromPairs,
 | 
				
			||||||
  isEqual,
 | 
					  isEqual,
 | 
				
			||||||
| 
						 | 
					@ -184,6 +185,16 @@ import { getConversationIdForLogging } from '../../util/idForLogging';
 | 
				
			||||||
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
 | 
					import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
 | 
				
			||||||
import MessageSender from '../../textsecure/SendMessage';
 | 
					import MessageSender from '../../textsecure/SendMessage';
 | 
				
			||||||
import { AttachmentDownloadUrgency } from '../../jobs/AttachmentDownloadManager';
 | 
					import { AttachmentDownloadUrgency } from '../../jobs/AttachmentDownloadManager';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					  DeleteForMeSyncEventData,
 | 
				
			||||||
 | 
					  MessageToDelete,
 | 
				
			||||||
 | 
					} from '../../textsecure/messageReceiverEvents';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getConversationToDelete,
 | 
				
			||||||
 | 
					  getMessageToDelete,
 | 
				
			||||||
 | 
					} from '../../util/deleteForMe';
 | 
				
			||||||
 | 
					import { MAX_MESSAGE_COUNT } from '../../util/deleteForMe.types';
 | 
				
			||||||
 | 
					import { isEnabled } from '../../RemoteConfig';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// State
 | 
					// State
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1703,21 +1714,27 @@ function deleteMessages({
 | 
				
			||||||
      throw new Error('deleteMessage: No conversation found');
 | 
					      throw new Error('deleteMessage: No conversation found');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await Promise.all(
 | 
					    const messages = (
 | 
				
			||||||
      messageIds.map(async messageId => {
 | 
					      await Promise.all(
 | 
				
			||||||
        const message = await __DEPRECATED$getMessageById(messageId);
 | 
					        messageIds.map(
 | 
				
			||||||
        if (!message) {
 | 
					          async (messageId): Promise<MessageToDelete | undefined> => {
 | 
				
			||||||
          throw new Error(`deleteMessages: Message ${messageId} missing!`);
 | 
					            const message = await __DEPRECATED$getMessageById(messageId);
 | 
				
			||||||
        }
 | 
					            if (!message) {
 | 
				
			||||||
 | 
					              throw new Error(`deleteMessages: Message ${messageId} missing!`);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const messageConversationId = message.get('conversationId');
 | 
					            const messageConversationId = message.get('conversationId');
 | 
				
			||||||
        if (conversationId !== messageConversationId) {
 | 
					            if (conversationId !== messageConversationId) {
 | 
				
			||||||
          throw new Error(
 | 
					              throw new Error(
 | 
				
			||||||
            `deleteMessages: message conversation ${messageConversationId} doesn't match provided conversation ${conversationId}`
 | 
					                `deleteMessages: message conversation ${messageConversationId} doesn't match provided conversation ${conversationId}`
 | 
				
			||||||
          );
 | 
					              );
 | 
				
			||||||
        }
 | 
					            }
 | 
				
			||||||
      })
 | 
					
 | 
				
			||||||
    );
 | 
					            return getMessageToDelete(message.attributes);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    ).filter(isNotNil);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let nearbyMessageId: string | null = null;
 | 
					    let nearbyMessageId: string | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1743,6 +1760,34 @@ function deleteMessages({
 | 
				
			||||||
    if (nearbyMessageId != null) {
 | 
					    if (nearbyMessageId != null) {
 | 
				
			||||||
      dispatch(scrollToMessage(conversationId, nearbyMessageId));
 | 
					      dispatch(scrollToMessage(conversationId, nearbyMessageId));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!isEnabled('desktop.deleteSync.send')) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (messages.length === 0) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const chunks = chunk(messages, MAX_MESSAGE_COUNT);
 | 
				
			||||||
 | 
					    const conversationToDelete = getConversationToDelete(
 | 
				
			||||||
 | 
					      conversation.attributes
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const timestamp = Date.now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Promise.all(
 | 
				
			||||||
 | 
					      chunks.map(async items => {
 | 
				
			||||||
 | 
					        const data: DeleteForMeSyncEventData = items.map(item => ({
 | 
				
			||||||
 | 
					          conversation: conversationToDelete,
 | 
				
			||||||
 | 
					          message: item,
 | 
				
			||||||
 | 
					          timestamp,
 | 
				
			||||||
 | 
					          type: 'delete-message' as const,
 | 
				
			||||||
 | 
					        }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await singleProtoJobQueue.add(
 | 
				
			||||||
 | 
					          MessageSender.getDeleteForMeSyncMessage(data)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1770,7 +1815,7 @@ function destroyMessages(
 | 
				
			||||||
          undefined
 | 
					          undefined
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await conversation.destroyMessages();
 | 
					        await conversation.destroyMessages({ source: 'local-delete' });
 | 
				
			||||||
        drop(conversation.updateLastMessage());
 | 
					        drop(conversation.updateLastMessage());
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -127,6 +127,13 @@ 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,
 | 
				
			||||||
| 
						 | 
					@ -242,3 +249,9 @@ export const getShowStickerPickerHint = createSelector(
 | 
				
			||||||
    return state.showStickerPickerHint ?? false;
 | 
					    return state.showStickerPickerHint ?? false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getLocalDeleteWarningShown = createSelector(
 | 
				
			||||||
 | 
					  getItems,
 | 
				
			||||||
 | 
					  (state: ItemsStateType): boolean =>
 | 
				
			||||||
 | 
					    Boolean(state.localDeleteWarningShown ?? false)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,6 +40,11 @@ import {
 | 
				
			||||||
} from '../selectors/conversations';
 | 
					} from '../selectors/conversations';
 | 
				
			||||||
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 {
 | 
				
			||||||
 | 
					  getDeleteSyncSendEnabled,
 | 
				
			||||||
 | 
					  getLocalDeleteWarningShown,
 | 
				
			||||||
 | 
					} from '../selectors/items';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type OwnProps = {
 | 
					export type OwnProps = {
 | 
				
			||||||
  id: string;
 | 
					  id: string;
 | 
				
			||||||
| 
						 | 
					@ -146,6 +151,7 @@ export const SmartConversationHeader = memo(function SmartConversationHeader({
 | 
				
			||||||
  const conversationName = useContactNameData(conversation);
 | 
					  const conversationName = useContactNameData(conversation);
 | 
				
			||||||
  strictAssert(conversationName, 'conversationName is required');
 | 
					  strictAssert(conversationName, 'conversationName is required');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const isDeleteSyncSendEnabled = useSelector(getDeleteSyncSendEnabled);
 | 
				
			||||||
  const isMissingMandatoryProfileSharing =
 | 
					  const isMissingMandatoryProfileSharing =
 | 
				
			||||||
    getIsMissingRequiredProfileSharing(conversation);
 | 
					    getIsMissingRequiredProfileSharing(conversation);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -248,6 +254,11 @@ export const SmartConversationHeader = memo(function SmartConversationHeader({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const minimalConversation = useMinimalConversation(conversation);
 | 
					  const minimalConversation = useMinimalConversation(conversation);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const localDeleteWarningShown = useSelector(getLocalDeleteWarningShown);
 | 
				
			||||||
 | 
					  const { putItem } = useItemsActions();
 | 
				
			||||||
 | 
					  const setLocalDeleteWarningShown = () =>
 | 
				
			||||||
 | 
					    putItem('localDeleteWarningShown', true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <ConversationHeader
 | 
					    <ConversationHeader
 | 
				
			||||||
      addedByName={addedByName}
 | 
					      addedByName={addedByName}
 | 
				
			||||||
| 
						 | 
					@ -258,6 +269,8 @@ export const SmartConversationHeader = memo(function SmartConversationHeader({
 | 
				
			||||||
      hasPanelShowing={hasPanelShowing}
 | 
					      hasPanelShowing={hasPanelShowing}
 | 
				
			||||||
      hasStories={hasStories}
 | 
					      hasStories={hasStories}
 | 
				
			||||||
      i18n={i18n}
 | 
					      i18n={i18n}
 | 
				
			||||||
 | 
					      localDeleteWarningShown={localDeleteWarningShown}
 | 
				
			||||||
 | 
					      isDeleteSyncSendEnabled={isDeleteSyncSendEnabled}
 | 
				
			||||||
      isMissingMandatoryProfileSharing={isMissingMandatoryProfileSharing}
 | 
					      isMissingMandatoryProfileSharing={isMissingMandatoryProfileSharing}
 | 
				
			||||||
      isSelectMode={isSelectMode}
 | 
					      isSelectMode={isSelectMode}
 | 
				
			||||||
      isSignalConversation={isSignalConversation(conversation)}
 | 
					      isSignalConversation={isSignalConversation(conversation)}
 | 
				
			||||||
| 
						 | 
					@ -287,6 +300,7 @@ export const SmartConversationHeader = memo(function SmartConversationHeader({
 | 
				
			||||||
      onViewRecentMedia={onViewRecentMedia}
 | 
					      onViewRecentMedia={onViewRecentMedia}
 | 
				
			||||||
      onViewUserStories={onViewUserStories}
 | 
					      onViewUserStories={onViewUserStories}
 | 
				
			||||||
      outgoingCallButtonStyle={outgoingCallButtonStyle}
 | 
					      outgoingCallButtonStyle={outgoingCallButtonStyle}
 | 
				
			||||||
 | 
					      setLocalDeleteWarningShown={setLocalDeleteWarningShown}
 | 
				
			||||||
      sharedGroupNames={conversation.sharedGroupNames}
 | 
					      sharedGroupNames={conversation.sharedGroupNames}
 | 
				
			||||||
      theme={theme}
 | 
					      theme={theme}
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,12 @@ import {
 | 
				
			||||||
  getLastSelectedMessage,
 | 
					  getLastSelectedMessage,
 | 
				
			||||||
} from '../selectors/conversations';
 | 
					} from '../selectors/conversations';
 | 
				
			||||||
import { getDeleteMessagesProps } from '../selectors/globalModals';
 | 
					import { getDeleteMessagesProps } from '../selectors/globalModals';
 | 
				
			||||||
 | 
					import { useItemsActions } from '../ducks/items';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getLocalDeleteWarningShown,
 | 
				
			||||||
 | 
					  getDeleteSyncSendEnabled,
 | 
				
			||||||
 | 
					} from '../selectors/items';
 | 
				
			||||||
 | 
					import { LocalDeleteWarningModal } from '../../components/LocalDeleteWarningModal';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SmartDeleteMessagesModal = memo(
 | 
					export const SmartDeleteMessagesModal = memo(
 | 
				
			||||||
  function SmartDeleteMessagesModal() {
 | 
					  function SmartDeleteMessagesModal() {
 | 
				
			||||||
| 
						 | 
					@ -36,6 +42,7 @@ export const SmartDeleteMessagesModal = memo(
 | 
				
			||||||
      [messageIds, isMe]
 | 
					      [messageIds, isMe]
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const canDeleteForEveryone = useSelector(getCanDeleteForEveryone);
 | 
					    const canDeleteForEveryone = useSelector(getCanDeleteForEveryone);
 | 
				
			||||||
 | 
					    const isDeleteSyncSendEnabled = useSelector(getDeleteSyncSendEnabled);
 | 
				
			||||||
    const lastSelectedMessage = useSelector(getLastSelectedMessage);
 | 
					    const lastSelectedMessage = useSelector(getLastSelectedMessage);
 | 
				
			||||||
    const i18n = useSelector(getIntl);
 | 
					    const i18n = useSelector(getIntl);
 | 
				
			||||||
    const { toggleDeleteMessagesModal } = useGlobalModalActions();
 | 
					    const { toggleDeleteMessagesModal } = useGlobalModalActions();
 | 
				
			||||||
| 
						 | 
					@ -69,11 +76,25 @@ export const SmartDeleteMessagesModal = memo(
 | 
				
			||||||
      onDelete?.();
 | 
					      onDelete?.();
 | 
				
			||||||
    }, [deleteMessagesForEveryone, messageIds, onDelete]);
 | 
					    }, [deleteMessagesForEveryone, messageIds, onDelete]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const localDeleteWarningShown = useSelector(getLocalDeleteWarningShown);
 | 
				
			||||||
 | 
					    const { putItem } = useItemsActions();
 | 
				
			||||||
 | 
					    if (!localDeleteWarningShown && isDeleteSyncSendEnabled) {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <LocalDeleteWarningModal
 | 
				
			||||||
 | 
					          i18n={i18n}
 | 
				
			||||||
 | 
					          onClose={() => {
 | 
				
			||||||
 | 
					            putItem('localDeleteWarningShown', true);
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <DeleteMessagesModal
 | 
					      <DeleteMessagesModal
 | 
				
			||||||
        isMe={isMe}
 | 
					        isMe={isMe}
 | 
				
			||||||
        canDeleteForEveryone={canDeleteForEveryone}
 | 
					        canDeleteForEveryone={canDeleteForEveryone}
 | 
				
			||||||
        i18n={i18n}
 | 
					        i18n={i18n}
 | 
				
			||||||
 | 
					        isDeleteSyncSendEnabled={isDeleteSyncSendEnabled}
 | 
				
			||||||
        messageCount={messageCount}
 | 
					        messageCount={messageCount}
 | 
				
			||||||
        onClose={handleClose}
 | 
					        onClose={handleClose}
 | 
				
			||||||
        onDeleteForMe={handleDeleteForMe}
 | 
					        onDeleteForMe={handleDeleteForMe}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -67,7 +67,7 @@ describe('KeyChangeListener', () => {
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  afterEach(async () => {
 | 
					  afterEach(async () => {
 | 
				
			||||||
    await window.Signal.Data.removeAllMessagesInConversation(convo.id, {
 | 
					    await window.Signal.Data.removeMessagesInConversation(convo.id, {
 | 
				
			||||||
      logId: ourServiceIdWithKeyChange,
 | 
					      logId: ourServiceIdWithKeyChange,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    await window.Signal.Data.removeConversation(convo.id);
 | 
					    await window.Signal.Data.removeConversation(convo.id);
 | 
				
			||||||
| 
						 | 
					@ -104,7 +104,7 @@ describe('KeyChangeListener', () => {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    afterEach(async () => {
 | 
					    afterEach(async () => {
 | 
				
			||||||
      await window.Signal.Data.removeAllMessagesInConversation(groupConvo.id, {
 | 
					      await window.Signal.Data.removeMessagesInConversation(groupConvo.id, {
 | 
				
			||||||
        logId: ourServiceIdWithKeyChange,
 | 
					        logId: ourServiceIdWithKeyChange,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      await window.Signal.Data.removeConversation(groupConvo.id);
 | 
					      await window.Signal.Data.removeConversation(groupConvo.id);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										302
									
								
								ts/test-node/sql/migration_1060_test.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										302
									
								
								ts/test-node/sql/migration_1060_test.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,302 @@
 | 
				
			||||||
 | 
					// Copyright 2024 Signal Messenger, LLC
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { assert } from 'chai';
 | 
				
			||||||
 | 
					import type { Database } from '@signalapp/better-sqlite3';
 | 
				
			||||||
 | 
					import SQL from '@signalapp/better-sqlite3';
 | 
				
			||||||
 | 
					import { v4 as generateGuid } from 'uuid';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getAllSyncTasksSync,
 | 
				
			||||||
 | 
					  getMostRecentAddressableMessagesSync,
 | 
				
			||||||
 | 
					  removeSyncTaskByIdSync,
 | 
				
			||||||
 | 
					  saveSyncTasksSync,
 | 
				
			||||||
 | 
					} from '../../sql/Server';
 | 
				
			||||||
 | 
					import { insertData, updateToVersion } from './helpers';
 | 
				
			||||||
 | 
					import { MAX_SYNC_TASK_ATTEMPTS } from '../../util/syncTasks.types';
 | 
				
			||||||
 | 
					import { WEEK } from '../../util/durations';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { MessageAttributesType } from '../../model-types';
 | 
				
			||||||
 | 
					import type { SyncTaskType } from '../../util/syncTasks';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* eslint-disable camelcase */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function generateMessage(json: MessageAttributesType) {
 | 
				
			||||||
 | 
					  const { conversationId, received_at, sent_at, type } = json;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    conversationId,
 | 
				
			||||||
 | 
					    json,
 | 
				
			||||||
 | 
					    received_at,
 | 
				
			||||||
 | 
					    sent_at,
 | 
				
			||||||
 | 
					    type,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('SQL/updateToSchemaVersion1060', () => {
 | 
				
			||||||
 | 
					  let db: Database;
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    db = new SQL(':memory:');
 | 
				
			||||||
 | 
					    updateToVersion(db, 1060);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  afterEach(() => {
 | 
				
			||||||
 | 
					    db.close();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('Addressable Messages', () => {
 | 
				
			||||||
 | 
					    describe('Storing of new attachment jobs', () => {
 | 
				
			||||||
 | 
					      it('returns only incoming/outgoing messages', () => {
 | 
				
			||||||
 | 
					        const conversationId = generateGuid();
 | 
				
			||||||
 | 
					        const otherConversationId = generateGuid();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        insertData(db, 'messages', [
 | 
				
			||||||
 | 
					          generateMessage({
 | 
				
			||||||
 | 
					            id: '1',
 | 
				
			||||||
 | 
					            conversationId,
 | 
				
			||||||
 | 
					            type: 'incoming',
 | 
				
			||||||
 | 
					            received_at: 1,
 | 
				
			||||||
 | 
					            sent_at: 1,
 | 
				
			||||||
 | 
					            timestamp: 1,
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					          generateMessage({
 | 
				
			||||||
 | 
					            id: '2',
 | 
				
			||||||
 | 
					            conversationId,
 | 
				
			||||||
 | 
					            type: 'story',
 | 
				
			||||||
 | 
					            received_at: 2,
 | 
				
			||||||
 | 
					            sent_at: 2,
 | 
				
			||||||
 | 
					            timestamp: 2,
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					          generateMessage({
 | 
				
			||||||
 | 
					            id: '3',
 | 
				
			||||||
 | 
					            conversationId,
 | 
				
			||||||
 | 
					            type: 'outgoing',
 | 
				
			||||||
 | 
					            received_at: 3,
 | 
				
			||||||
 | 
					            sent_at: 3,
 | 
				
			||||||
 | 
					            timestamp: 3,
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					          generateMessage({
 | 
				
			||||||
 | 
					            id: '4',
 | 
				
			||||||
 | 
					            conversationId,
 | 
				
			||||||
 | 
					            type: 'group-v1-migration',
 | 
				
			||||||
 | 
					            received_at: 4,
 | 
				
			||||||
 | 
					            sent_at: 4,
 | 
				
			||||||
 | 
					            timestamp: 4,
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					          generateMessage({
 | 
				
			||||||
 | 
					            id: '5',
 | 
				
			||||||
 | 
					            conversationId,
 | 
				
			||||||
 | 
					            type: 'group-v2-change',
 | 
				
			||||||
 | 
					            received_at: 5,
 | 
				
			||||||
 | 
					            sent_at: 5,
 | 
				
			||||||
 | 
					            timestamp: 5,
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					          generateMessage({
 | 
				
			||||||
 | 
					            id: '6',
 | 
				
			||||||
 | 
					            conversationId,
 | 
				
			||||||
 | 
					            type: 'incoming',
 | 
				
			||||||
 | 
					            received_at: 6,
 | 
				
			||||||
 | 
					            sent_at: 6,
 | 
				
			||||||
 | 
					            timestamp: 6,
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					          generateMessage({
 | 
				
			||||||
 | 
					            id: '7',
 | 
				
			||||||
 | 
					            conversationId,
 | 
				
			||||||
 | 
					            type: 'profile-change',
 | 
				
			||||||
 | 
					            received_at: 7,
 | 
				
			||||||
 | 
					            sent_at: 7,
 | 
				
			||||||
 | 
					            timestamp: 7,
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					          generateMessage({
 | 
				
			||||||
 | 
					            id: '8',
 | 
				
			||||||
 | 
					            conversationId: otherConversationId,
 | 
				
			||||||
 | 
					            type: 'incoming',
 | 
				
			||||||
 | 
					            received_at: 8,
 | 
				
			||||||
 | 
					            sent_at: 8,
 | 
				
			||||||
 | 
					            timestamp: 8,
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const messages = getMostRecentAddressableMessagesSync(
 | 
				
			||||||
 | 
					          db,
 | 
				
			||||||
 | 
					          conversationId
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert.lengthOf(messages, 3);
 | 
				
			||||||
 | 
					        assert.deepEqual(messages, [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            id: '6',
 | 
				
			||||||
 | 
					            conversationId,
 | 
				
			||||||
 | 
					            type: 'incoming',
 | 
				
			||||||
 | 
					            received_at: 6,
 | 
				
			||||||
 | 
					            sent_at: 6,
 | 
				
			||||||
 | 
					            timestamp: 6,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            id: '3',
 | 
				
			||||||
 | 
					            conversationId,
 | 
				
			||||||
 | 
					            type: 'outgoing',
 | 
				
			||||||
 | 
					            received_at: 3,
 | 
				
			||||||
 | 
					            sent_at: 3,
 | 
				
			||||||
 | 
					            timestamp: 3,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            id: '1',
 | 
				
			||||||
 | 
					            conversationId,
 | 
				
			||||||
 | 
					            type: 'incoming',
 | 
				
			||||||
 | 
					            received_at: 1,
 | 
				
			||||||
 | 
					            sent_at: 1,
 | 
				
			||||||
 | 
					            timestamp: 1,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it('ensures that index is used for getMostRecentAddressableMessagesSync, with storyId', () => {
 | 
				
			||||||
 | 
					        const { detail } = db
 | 
				
			||||||
 | 
					          .prepare(
 | 
				
			||||||
 | 
					            `
 | 
				
			||||||
 | 
					          EXPLAIN QUERY PLAN
 | 
				
			||||||
 | 
					          SELECT json FROM messages
 | 
				
			||||||
 | 
					          INDEXED BY messages_by_date_addressable
 | 
				
			||||||
 | 
					          WHERE
 | 
				
			||||||
 | 
					            conversationId IS 'not-important' AND
 | 
				
			||||||
 | 
					            isAddressableMessage = 1
 | 
				
			||||||
 | 
					          ORDER BY received_at DESC, sent_at DESC
 | 
				
			||||||
 | 
					          LIMIT 5;
 | 
				
			||||||
 | 
					          `
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					          .get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert.notInclude(detail, 'B-TREE');
 | 
				
			||||||
 | 
					        assert.notInclude(detail, 'SCAN');
 | 
				
			||||||
 | 
					        assert.include(
 | 
				
			||||||
 | 
					          detail,
 | 
				
			||||||
 | 
					          'SEARCH messages USING INDEX messages_by_date_addressable (conversationId=? AND isAddressableMessage=?)'
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('Sync Tasks', () => {
 | 
				
			||||||
 | 
					    it('creates tasks in bulk, and fetches all', () => {
 | 
				
			||||||
 | 
					      const now = Date.now();
 | 
				
			||||||
 | 
					      const expected: Array<SyncTaskType> = [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          id: generateGuid(),
 | 
				
			||||||
 | 
					          attempts: 1,
 | 
				
			||||||
 | 
					          createdAt: now + 1,
 | 
				
			||||||
 | 
					          data: {
 | 
				
			||||||
 | 
					            jsonField: 'one',
 | 
				
			||||||
 | 
					            data: 1,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          envelopeId: 'envelope-id-1',
 | 
				
			||||||
 | 
					          sentAt: 1,
 | 
				
			||||||
 | 
					          type: 'delete-conversation',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          id: generateGuid(),
 | 
				
			||||||
 | 
					          attempts: 2,
 | 
				
			||||||
 | 
					          createdAt: now + 2,
 | 
				
			||||||
 | 
					          data: {
 | 
				
			||||||
 | 
					            jsonField: 'two',
 | 
				
			||||||
 | 
					            data: 2,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          envelopeId: 'envelope-id-2',
 | 
				
			||||||
 | 
					          sentAt: 2,
 | 
				
			||||||
 | 
					          type: 'delete-conversation',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          id: generateGuid(),
 | 
				
			||||||
 | 
					          attempts: 3,
 | 
				
			||||||
 | 
					          createdAt: now + 3,
 | 
				
			||||||
 | 
					          data: {
 | 
				
			||||||
 | 
					            jsonField: 'three',
 | 
				
			||||||
 | 
					            data: 3,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          envelopeId: 'envelope-id-3',
 | 
				
			||||||
 | 
					          sentAt: 3,
 | 
				
			||||||
 | 
					          type: 'delete-conversation',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      saveSyncTasksSync(db, expected);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const actual = getAllSyncTasksSync(db);
 | 
				
			||||||
 | 
					      assert.deepEqual(expected, actual, 'before delete');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      removeSyncTaskByIdSync(db, expected[1].id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const actualAfterDelete = getAllSyncTasksSync(db);
 | 
				
			||||||
 | 
					      assert.deepEqual(
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					          { ...expected[0], attempts: 2 },
 | 
				
			||||||
 | 
					          { ...expected[2], attempts: 4 },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        actualAfterDelete,
 | 
				
			||||||
 | 
					        'after delete'
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('getAllSyncTasksSync expired tasks', () => {
 | 
				
			||||||
 | 
					      const now = Date.now();
 | 
				
			||||||
 | 
					      const twoWeeksAgo = now - WEEK * 2;
 | 
				
			||||||
 | 
					      const expected: Array<SyncTaskType> = [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          id: generateGuid(),
 | 
				
			||||||
 | 
					          attempts: MAX_SYNC_TASK_ATTEMPTS,
 | 
				
			||||||
 | 
					          createdAt: twoWeeksAgo,
 | 
				
			||||||
 | 
					          data: {
 | 
				
			||||||
 | 
					            jsonField: 'expired',
 | 
				
			||||||
 | 
					            data: 1,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          envelopeId: 'envelope-id-1',
 | 
				
			||||||
 | 
					          sentAt: 1,
 | 
				
			||||||
 | 
					          type: 'delete-conversation',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          id: generateGuid(),
 | 
				
			||||||
 | 
					          attempts: 2,
 | 
				
			||||||
 | 
					          createdAt: twoWeeksAgo,
 | 
				
			||||||
 | 
					          data: {
 | 
				
			||||||
 | 
					            jsonField: 'old-but-few-attemts',
 | 
				
			||||||
 | 
					            data: 2,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          envelopeId: 'envelope-id-2',
 | 
				
			||||||
 | 
					          sentAt: 2,
 | 
				
			||||||
 | 
					          type: 'delete-conversation',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          id: generateGuid(),
 | 
				
			||||||
 | 
					          attempts: MAX_SYNC_TASK_ATTEMPTS * 2,
 | 
				
			||||||
 | 
					          createdAt: now,
 | 
				
			||||||
 | 
					          data: {
 | 
				
			||||||
 | 
					            jsonField: 'new-but-many-attempts',
 | 
				
			||||||
 | 
					            data: 3,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          envelopeId: 'envelope-id-3',
 | 
				
			||||||
 | 
					          sentAt: 3,
 | 
				
			||||||
 | 
					          type: 'delete-conversation',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          id: generateGuid(),
 | 
				
			||||||
 | 
					          attempts: MAX_SYNC_TASK_ATTEMPTS - 1,
 | 
				
			||||||
 | 
					          createdAt: now + 1,
 | 
				
			||||||
 | 
					          data: {
 | 
				
			||||||
 | 
					            jsonField: 'new-and-fresh',
 | 
				
			||||||
 | 
					            data: 4,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          envelopeId: 'envelope-id-4',
 | 
				
			||||||
 | 
					          sentAt: 4,
 | 
				
			||||||
 | 
					          type: 'delete-conversation',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      saveSyncTasksSync(db, expected);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const actual = getAllSyncTasksSync(db);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      assert.lengthOf(actual, 3);
 | 
				
			||||||
 | 
					      assert.deepEqual([expected[1], expected[2], expected[3]], actual);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -130,6 +130,13 @@ import {
 | 
				
			||||||
  StoryRecipientUpdateEvent,
 | 
					  StoryRecipientUpdateEvent,
 | 
				
			||||||
  CallLogEventSyncEvent,
 | 
					  CallLogEventSyncEvent,
 | 
				
			||||||
  CallLinkUpdateSyncEvent,
 | 
					  CallLinkUpdateSyncEvent,
 | 
				
			||||||
 | 
					  DeleteForMeSyncEvent,
 | 
				
			||||||
 | 
					} from './messageReceiverEvents';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					  MessageToDelete,
 | 
				
			||||||
 | 
					  DeleteForMeSyncEventData,
 | 
				
			||||||
 | 
					  DeleteForMeSyncTarget,
 | 
				
			||||||
 | 
					  ConversationToDelete,
 | 
				
			||||||
} 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';
 | 
				
			||||||
| 
						 | 
					@ -686,6 +693,11 @@ export default class MessageReceiver
 | 
				
			||||||
    handler: (ev: CallLogEventSyncEvent) => void
 | 
					    handler: (ev: CallLogEventSyncEvent) => void
 | 
				
			||||||
  ): void;
 | 
					  ): void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public override addEventListener(
 | 
				
			||||||
 | 
					    name: 'deleteForMeSync',
 | 
				
			||||||
 | 
					    handler: (ev: DeleteForMeSyncEvent) => void
 | 
				
			||||||
 | 
					  ): void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public override addEventListener(name: string, handler: EventHandler): void {
 | 
					  public override addEventListener(name: string, handler: EventHandler): void {
 | 
				
			||||||
    return super.addEventListener(name, handler);
 | 
					    return super.addEventListener(name, handler);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -3165,6 +3177,9 @@ export default class MessageReceiver
 | 
				
			||||||
    if (syncMessage.callLogEvent) {
 | 
					    if (syncMessage.callLogEvent) {
 | 
				
			||||||
      return this.handleCallLogEvent(envelope, syncMessage.callLogEvent);
 | 
					      return this.handleCallLogEvent(envelope, syncMessage.callLogEvent);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (syncMessage.deleteForMe) {
 | 
				
			||||||
 | 
					      return this.handleDeleteForMeSync(envelope, syncMessage.deleteForMe);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.removeFromCache(envelope);
 | 
					    this.removeFromCache(envelope);
 | 
				
			||||||
    const envelopeId = getEnvelopeId(envelope);
 | 
					    const envelopeId = getEnvelopeId(envelope);
 | 
				
			||||||
| 
						 | 
					@ -3615,6 +3630,118 @@ export default class MessageReceiver
 | 
				
			||||||
    log.info('handleCallLogEvent: finished');
 | 
					    log.info('handleCallLogEvent: finished');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private async handleDeleteForMeSync(
 | 
				
			||||||
 | 
					    envelope: ProcessedEnvelope,
 | 
				
			||||||
 | 
					    deleteSync: Proto.SyncMessage.IDeleteForMe
 | 
				
			||||||
 | 
					  ): Promise<void> {
 | 
				
			||||||
 | 
					    const logId = getEnvelopeId(envelope);
 | 
				
			||||||
 | 
					    log.info('MessageReceiver.handleDeleteForMeSync', logId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    logUnexpectedUrgentValue(envelope, 'deleteForMeSync');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { timestamp } = envelope;
 | 
				
			||||||
 | 
					    let eventData: DeleteForMeSyncEventData = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      if (deleteSync.messageDeletes?.length) {
 | 
				
			||||||
 | 
					        const messageDeletes: Array<DeleteForMeSyncTarget> =
 | 
				
			||||||
 | 
					          deleteSync.messageDeletes
 | 
				
			||||||
 | 
					            .flatMap((item): Array<DeleteForMeSyncTarget> | undefined => {
 | 
				
			||||||
 | 
					              const messages = item.messages
 | 
				
			||||||
 | 
					                ?.map(message => processMessageToDelete(message, logId))
 | 
				
			||||||
 | 
					                .filter(isNotNil);
 | 
				
			||||||
 | 
					              const conversation = item.conversation
 | 
				
			||||||
 | 
					                ? processConversationToDelete(item.conversation, logId)
 | 
				
			||||||
 | 
					                : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              if (messages?.length && conversation) {
 | 
				
			||||||
 | 
					                // We want each message in its own task
 | 
				
			||||||
 | 
					                return messages.map(innerItem => {
 | 
				
			||||||
 | 
					                  return {
 | 
				
			||||||
 | 
					                    type: 'delete-message' as const,
 | 
				
			||||||
 | 
					                    message: innerItem,
 | 
				
			||||||
 | 
					                    conversation,
 | 
				
			||||||
 | 
					                    timestamp,
 | 
				
			||||||
 | 
					                  };
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              return undefined;
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .filter(isNotNil);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        eventData = eventData.concat(messageDeletes);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (deleteSync.conversationDeletes?.length) {
 | 
				
			||||||
 | 
					        const conversationDeletes: Array<DeleteForMeSyncTarget> =
 | 
				
			||||||
 | 
					          deleteSync.conversationDeletes
 | 
				
			||||||
 | 
					            .map(item => {
 | 
				
			||||||
 | 
					              const mostRecentMessages = item.mostRecentMessages
 | 
				
			||||||
 | 
					                ?.map(message => processMessageToDelete(message, logId))
 | 
				
			||||||
 | 
					                .filter(isNotNil);
 | 
				
			||||||
 | 
					              const conversation = item.conversation
 | 
				
			||||||
 | 
					                ? processConversationToDelete(item.conversation, logId)
 | 
				
			||||||
 | 
					                : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              if (mostRecentMessages?.length && conversation) {
 | 
				
			||||||
 | 
					                return {
 | 
				
			||||||
 | 
					                  type: 'delete-conversation' as const,
 | 
				
			||||||
 | 
					                  conversation,
 | 
				
			||||||
 | 
					                  isFullDelete: Boolean(item.isFullDelete),
 | 
				
			||||||
 | 
					                  mostRecentMessages,
 | 
				
			||||||
 | 
					                  timestamp,
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              return undefined;
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .filter(isNotNil);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        eventData = eventData.concat(conversationDeletes);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (deleteSync.localOnlyConversationDeletes?.length) {
 | 
				
			||||||
 | 
					        const localOnlyConversationDeletes: Array<DeleteForMeSyncTarget> =
 | 
				
			||||||
 | 
					          deleteSync.localOnlyConversationDeletes
 | 
				
			||||||
 | 
					            .map(item => {
 | 
				
			||||||
 | 
					              const conversation = item.conversation
 | 
				
			||||||
 | 
					                ? processConversationToDelete(item.conversation, logId)
 | 
				
			||||||
 | 
					                : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              if (conversation) {
 | 
				
			||||||
 | 
					                return {
 | 
				
			||||||
 | 
					                  type: 'delete-local-conversation' as const,
 | 
				
			||||||
 | 
					                  conversation,
 | 
				
			||||||
 | 
					                  timestamp,
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              return undefined;
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .filter(isNotNil);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        eventData = eventData.concat(localOnlyConversationDeletes);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (!eventData.length) {
 | 
				
			||||||
 | 
					        throw new Error(`${logId}: Nothing found in sync message!`);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error: unknown) {
 | 
				
			||||||
 | 
					      this.removeFromCache(envelope);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      throw error;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const deleteSyncEventSync = new DeleteForMeSyncEvent(
 | 
				
			||||||
 | 
					      eventData,
 | 
				
			||||||
 | 
					      timestamp,
 | 
				
			||||||
 | 
					      envelope.id,
 | 
				
			||||||
 | 
					      this.removeFromCache.bind(this, envelope)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.dispatchAndWait(logId, deleteSyncEventSync);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log.info('handleDeleteForMeSync: finished');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async handleContacts(
 | 
					  private async handleContacts(
 | 
				
			||||||
    envelope: ProcessedEnvelope,
 | 
					    envelope: ProcessedEnvelope,
 | 
				
			||||||
    contactSyncProto: Proto.SyncMessage.IContacts
 | 
					    contactSyncProto: Proto.SyncMessage.IContacts
 | 
				
			||||||
| 
						 | 
					@ -3820,3 +3947,70 @@ function envelopeTypeToCiphertextType(type: number | undefined): number {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  throw new Error(`envelopeTypeToCiphertextType: Unknown type ${type}`);
 | 
					  throw new Error(`envelopeTypeToCiphertextType: Unknown type ${type}`);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function processMessageToDelete(
 | 
				
			||||||
 | 
					  target: Proto.SyncMessage.DeleteForMe.IAddressableMessage,
 | 
				
			||||||
 | 
					  logId: string
 | 
				
			||||||
 | 
					): MessageToDelete | undefined {
 | 
				
			||||||
 | 
					  const sentAt = target.sentTimestamp?.toNumber();
 | 
				
			||||||
 | 
					  if (!isNumber(sentAt)) {
 | 
				
			||||||
 | 
					    log.warn(
 | 
				
			||||||
 | 
					      `${logId}/processMessageToDelete: No sentTimestamp found! Dropping AddressableMessage.`
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    return undefined;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (target.authorAci) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      type: 'aci' as const,
 | 
				
			||||||
 | 
					      authorAci: normalizeAci(
 | 
				
			||||||
 | 
					        target.authorAci,
 | 
				
			||||||
 | 
					        `${logId}/processMessageToDelete`
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      sentAt,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (target.authorE164) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      type: 'e164' as const,
 | 
				
			||||||
 | 
					      authorE164: target.authorE164,
 | 
				
			||||||
 | 
					      sentAt,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  log.warn(
 | 
				
			||||||
 | 
					    `${logId}/processMessageToDelete: No author field found! Dropping AddressableMessage.`
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  return undefined;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function processConversationToDelete(
 | 
				
			||||||
 | 
					  target: Proto.SyncMessage.DeleteForMe.IConversationIdentifier,
 | 
				
			||||||
 | 
					  logId: string
 | 
				
			||||||
 | 
					): ConversationToDelete | undefined {
 | 
				
			||||||
 | 
					  const { threadAci, threadGroupId, threadE164 } = target;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (threadAci) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      type: 'aci' as const,
 | 
				
			||||||
 | 
					      aci: normalizeAci(threadAci, `${logId}/threadAci`),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (threadGroupId) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      type: 'group' as const,
 | 
				
			||||||
 | 
					      groupId: Buffer.from(threadGroupId).toString('base64'),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (threadE164) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      type: 'e164' as const,
 | 
				
			||||||
 | 
					      e164: threadE164,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  log.warn(
 | 
				
			||||||
 | 
					    `${logId}/processConversationToDelete: No identifier field found! Dropping ConversationIdentifier.`
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  return undefined;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -82,6 +82,13 @@ import {
 | 
				
			||||||
} from '../types/EmbeddedContact';
 | 
					} from '../types/EmbeddedContact';
 | 
				
			||||||
import { missingCaseError } from '../util/missingCaseError';
 | 
					import { missingCaseError } from '../util/missingCaseError';
 | 
				
			||||||
import { drop } from '../util/drop';
 | 
					import { drop } from '../util/drop';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					  ConversationToDelete,
 | 
				
			||||||
 | 
					  DeleteForMeSyncEventData,
 | 
				
			||||||
 | 
					  DeleteMessageSyncTarget,
 | 
				
			||||||
 | 
					  MessageToDelete,
 | 
				
			||||||
 | 
					} from './messageReceiverEvents';
 | 
				
			||||||
 | 
					import { getConversationFromTarget } from '../util/deleteForMe';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type SendMetadataType = {
 | 
					export type SendMetadataType = {
 | 
				
			||||||
  [serviceId: ServiceIdString]: {
 | 
					  [serviceId: ServiceIdString]: {
 | 
				
			||||||
| 
						 | 
					@ -1475,6 +1482,91 @@ export default class MessageSender {
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static getDeleteForMeSyncMessage(
 | 
				
			||||||
 | 
					    data: DeleteForMeSyncEventData
 | 
				
			||||||
 | 
					  ): SingleProtoJobData {
 | 
				
			||||||
 | 
					    const myAci = window.textsecure.storage.user.getCheckedAci();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const deleteForMe = new Proto.SyncMessage.DeleteForMe();
 | 
				
			||||||
 | 
					    const messageDeletes: Map<
 | 
				
			||||||
 | 
					      string,
 | 
				
			||||||
 | 
					      Array<DeleteMessageSyncTarget>
 | 
				
			||||||
 | 
					    > = new Map();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    data.forEach(item => {
 | 
				
			||||||
 | 
					      if (item.type === 'delete-message') {
 | 
				
			||||||
 | 
					        const conversation = getConversationFromTarget(item.conversation);
 | 
				
			||||||
 | 
					        if (!conversation) {
 | 
				
			||||||
 | 
					          throw new Error(
 | 
				
			||||||
 | 
					            'getDeleteForMeSyncMessage: Failed to find conversation for delete-message'
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const existing = messageDeletes.get(conversation.id);
 | 
				
			||||||
 | 
					        if (existing) {
 | 
				
			||||||
 | 
					          existing.push(item);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          messageDeletes.set(conversation.id, [item]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else if (item.type === 'delete-conversation') {
 | 
				
			||||||
 | 
					        const mostRecentMessages =
 | 
				
			||||||
 | 
					          item.mostRecentMessages.map(toAddressableMessage);
 | 
				
			||||||
 | 
					        const conversation = toConversationIdentifier(item.conversation);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        deleteForMe.conversationDeletes = deleteForMe.conversationDeletes || [];
 | 
				
			||||||
 | 
					        deleteForMe.conversationDeletes.push({
 | 
				
			||||||
 | 
					          mostRecentMessages,
 | 
				
			||||||
 | 
					          conversation,
 | 
				
			||||||
 | 
					          isFullDelete: true,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else if (item.type === 'delete-local-conversation') {
 | 
				
			||||||
 | 
					        const conversation = toConversationIdentifier(item.conversation);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        deleteForMe.localOnlyConversationDeletes =
 | 
				
			||||||
 | 
					          deleteForMe.localOnlyConversationDeletes || [];
 | 
				
			||||||
 | 
					        deleteForMe.localOnlyConversationDeletes.push({
 | 
				
			||||||
 | 
					          conversation,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        throw missingCaseError(item);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (messageDeletes.size > 0) {
 | 
				
			||||||
 | 
					      for (const items of messageDeletes.values()) {
 | 
				
			||||||
 | 
					        const first = items[0];
 | 
				
			||||||
 | 
					        if (!first) {
 | 
				
			||||||
 | 
					          throw new Error('Failed to fetch first from items');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const messages = items.map(item => toAddressableMessage(item.message));
 | 
				
			||||||
 | 
					        const conversation = toConversationIdentifier(first.conversation);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        deleteForMe.messageDeletes = deleteForMe.messageDeletes || [];
 | 
				
			||||||
 | 
					        deleteForMe.messageDeletes.push({
 | 
				
			||||||
 | 
					          messages,
 | 
				
			||||||
 | 
					          conversation,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const syncMessage = this.createSyncMessage();
 | 
				
			||||||
 | 
					    syncMessage.deleteForMe = deleteForMe;
 | 
				
			||||||
 | 
					    const contentMessage = new Proto.Content();
 | 
				
			||||||
 | 
					    contentMessage.syncMessage = syncMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      contentHint: ContentHint.RESENDABLE,
 | 
				
			||||||
 | 
					      serviceId: myAci,
 | 
				
			||||||
 | 
					      isSyncMessage: true,
 | 
				
			||||||
 | 
					      protoBase64: Bytes.toBase64(
 | 
				
			||||||
 | 
					        Proto.Content.encode(contentMessage).finish()
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      type: 'deleteForMeSync',
 | 
				
			||||||
 | 
					      urgent: false,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async syncReadMessages(
 | 
					  async syncReadMessages(
 | 
				
			||||||
    reads: ReadonlyArray<{
 | 
					    reads: ReadonlyArray<{
 | 
				
			||||||
      senderAci?: AciString;
 | 
					      senderAci?: AciString;
 | 
				
			||||||
| 
						 | 
					@ -2253,3 +2345,37 @@ export default class MessageSender {
 | 
				
			||||||
    return this.server.sendChallengeResponse(challengeResponse);
 | 
					    return this.server.sendChallengeResponse(challengeResponse);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Helpers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function toAddressableMessage(message: MessageToDelete) {
 | 
				
			||||||
 | 
					  const targetMessage = new Proto.SyncMessage.DeleteForMe.AddressableMessage();
 | 
				
			||||||
 | 
					  targetMessage.sentTimestamp = Long.fromNumber(message.sentAt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (message.type === 'aci') {
 | 
				
			||||||
 | 
					    targetMessage.authorAci = message.authorAci;
 | 
				
			||||||
 | 
					  } else if (message.type === 'e164') {
 | 
				
			||||||
 | 
					    targetMessage.authorE164 = message.authorE164;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    throw missingCaseError(message);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return targetMessage;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function toConversationIdentifier(conversation: ConversationToDelete) {
 | 
				
			||||||
 | 
					  const targetConversation =
 | 
				
			||||||
 | 
					    new Proto.SyncMessage.DeleteForMe.ConversationIdentifier();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (conversation.type === 'aci') {
 | 
				
			||||||
 | 
					    targetConversation.threadAci = conversation.aci;
 | 
				
			||||||
 | 
					  } else if (conversation.type === 'group') {
 | 
				
			||||||
 | 
					    targetConversation.threadGroupId = Bytes.fromBase64(conversation.groupId);
 | 
				
			||||||
 | 
					  } else if (conversation.type === 'e164') {
 | 
				
			||||||
 | 
					    targetConversation.threadE164 = conversation.e164;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    throw missingCaseError(conversation);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return targetConversation;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@
 | 
				
			||||||
/* eslint-disable max-classes-per-file */
 | 
					/* eslint-disable max-classes-per-file */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { PublicKey } from '@signalapp/libsignal-client';
 | 
					import type { PublicKey } from '@signalapp/libsignal-client';
 | 
				
			||||||
 | 
					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, AciString } from '../types/ServiceId';
 | 
				
			||||||
| 
						 | 
					@ -15,6 +16,7 @@ import type {
 | 
				
			||||||
import type { ContactDetailsWithAvatar } from './ContactsParser';
 | 
					import type { ContactDetailsWithAvatar } from './ContactsParser';
 | 
				
			||||||
import type { CallEventDetails, CallLogEvent } from '../types/CallDisposition';
 | 
					import type { CallEventDetails, CallLogEvent } from '../types/CallDisposition';
 | 
				
			||||||
import type { CallLinkUpdateSyncType } from '../types/CallLink';
 | 
					import type { CallLinkUpdateSyncType } from '../types/CallLink';
 | 
				
			||||||
 | 
					import { isAciString } from '../util/isAciString';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class EmptyEvent extends Event {
 | 
					export class EmptyEvent extends Event {
 | 
				
			||||||
  constructor() {
 | 
					  constructor() {
 | 
				
			||||||
| 
						 | 
					@ -456,6 +458,78 @@ export class CallLinkUpdateSyncEvent extends ConfirmableEvent {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const messageToDeleteSchema = z.union([
 | 
				
			||||||
 | 
					  z.object({
 | 
				
			||||||
 | 
					    type: z.literal('aci').readonly(),
 | 
				
			||||||
 | 
					    authorAci: z.string().refine(isAciString),
 | 
				
			||||||
 | 
					    sentAt: z.number(),
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					  z.object({
 | 
				
			||||||
 | 
					    type: z.literal('e164').readonly(),
 | 
				
			||||||
 | 
					    authorE164: z.string(),
 | 
				
			||||||
 | 
					    sentAt: z.number(),
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type MessageToDelete = z.infer<typeof messageToDeleteSchema>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const conversationToDeleteSchema = z.union([
 | 
				
			||||||
 | 
					  z.object({
 | 
				
			||||||
 | 
					    type: z.literal('group').readonly(),
 | 
				
			||||||
 | 
					    groupId: z.string(),
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					  z.object({
 | 
				
			||||||
 | 
					    type: z.literal('aci').readonly(),
 | 
				
			||||||
 | 
					    aci: z.string().refine(isAciString),
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					  z.object({
 | 
				
			||||||
 | 
					    type: z.literal('e164').readonly(),
 | 
				
			||||||
 | 
					    e164: z.string(),
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ConversationToDelete = z.infer<typeof conversationToDeleteSchema>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const deleteMessageSchema = z.object({
 | 
				
			||||||
 | 
					  type: z.literal('delete-message').readonly(),
 | 
				
			||||||
 | 
					  conversation: conversationToDeleteSchema,
 | 
				
			||||||
 | 
					  message: messageToDeleteSchema,
 | 
				
			||||||
 | 
					  timestamp: z.number(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					export type DeleteMessageSyncTarget = z.infer<typeof deleteMessageSchema>;
 | 
				
			||||||
 | 
					export const deleteConversationSchema = z.object({
 | 
				
			||||||
 | 
					  type: z.literal('delete-conversation').readonly(),
 | 
				
			||||||
 | 
					  conversation: conversationToDeleteSchema,
 | 
				
			||||||
 | 
					  mostRecentMessages: z.array(messageToDeleteSchema),
 | 
				
			||||||
 | 
					  isFullDelete: z.boolean(),
 | 
				
			||||||
 | 
					  timestamp: z.number(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					export const deleteLocalConversationSchema = z.object({
 | 
				
			||||||
 | 
					  type: z.literal('delete-local-conversation').readonly(),
 | 
				
			||||||
 | 
					  conversation: conversationToDeleteSchema,
 | 
				
			||||||
 | 
					  timestamp: z.number(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					export const deleteForMeSyncTargetSchema = z.union([
 | 
				
			||||||
 | 
					  deleteMessageSchema,
 | 
				
			||||||
 | 
					  deleteConversationSchema,
 | 
				
			||||||
 | 
					  deleteLocalConversationSchema,
 | 
				
			||||||
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type DeleteForMeSyncTarget = z.infer<typeof deleteForMeSyncTargetSchema>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type DeleteForMeSyncEventData = ReadonlyArray<DeleteForMeSyncTarget>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class DeleteForMeSyncEvent extends ConfirmableEvent {
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    public readonly deleteForMeSync: DeleteForMeSyncEventData,
 | 
				
			||||||
 | 
					    public readonly timestamp: number,
 | 
				
			||||||
 | 
					    public readonly envelopeId: string,
 | 
				
			||||||
 | 
					    confirm: ConfirmCallback
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    super('deleteForMeSync', confirm);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type CallLogEventSyncEventData = Readonly<{
 | 
					export type CallLogEventSyncEventData = Readonly<{
 | 
				
			||||||
  event: CallLogEvent;
 | 
					  event: CallLogEvent;
 | 
				
			||||||
  timestamp: number;
 | 
					  timestamp: number;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								ts/types/Storage.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								ts/types/Storage.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -79,6 +79,7 @@ export type StorageAccessType = {
 | 
				
			||||||
  lastAttemptedToRefreshProfilesAt: number;
 | 
					  lastAttemptedToRefreshProfilesAt: number;
 | 
				
			||||||
  lastResortKeyUpdateTime: number;
 | 
					  lastResortKeyUpdateTime: number;
 | 
				
			||||||
  lastResortKeyUpdateTimePNI: number;
 | 
					  lastResortKeyUpdateTimePNI: number;
 | 
				
			||||||
 | 
					  localDeleteWarningShown: boolean;
 | 
				
			||||||
  masterKey: string;
 | 
					  masterKey: string;
 | 
				
			||||||
  masterKeyLastRequestTime: number;
 | 
					  masterKeyLastRequestTime: number;
 | 
				
			||||||
  maxPreKeyId: number;
 | 
					  maxPreKeyId: number;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,6 +23,7 @@ export const STORAGE_UI_KEYS: ReadonlyArray<keyof StorageAccessType> = [
 | 
				
			||||||
  'hasCompletedSafetyNumberOnboarding',
 | 
					  'hasCompletedSafetyNumberOnboarding',
 | 
				
			||||||
  'hasCompletedUsernameLinkOnboarding',
 | 
					  'hasCompletedUsernameLinkOnboarding',
 | 
				
			||||||
  'hide-menu-bar',
 | 
					  'hide-menu-bar',
 | 
				
			||||||
 | 
					  'localDeleteWarningShown',
 | 
				
			||||||
  'incoming-call-notification',
 | 
					  'incoming-call-notification',
 | 
				
			||||||
  'navTabsCollapsed',
 | 
					  'navTabsCollapsed',
 | 
				
			||||||
  'notification-draw-attention',
 | 
					  'notification-draw-attention',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										261
									
								
								ts/util/deleteForMe.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										261
									
								
								ts/util/deleteForMe.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,261 @@
 | 
				
			||||||
 | 
					// Copyright 2020 Signal Messenger, LLC
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { last, sortBy } from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
 | 
					import { isAciString } from './isAciString';
 | 
				
			||||||
 | 
					import { isGroup, isGroupV2 } from './whatTypeOfConversation';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getConversationIdForLogging,
 | 
				
			||||||
 | 
					  getMessageIdForLogging,
 | 
				
			||||||
 | 
					} from './idForLogging';
 | 
				
			||||||
 | 
					import { missingCaseError } from './missingCaseError';
 | 
				
			||||||
 | 
					import { getMessageSentTimestampSet } from './getMessageSentTimestampSet';
 | 
				
			||||||
 | 
					import { getAuthor } from '../messages/helpers';
 | 
				
			||||||
 | 
					import dataInterface, { deleteAndCleanup } from '../sql/Client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					  ConversationAttributesType,
 | 
				
			||||||
 | 
					  MessageAttributesType,
 | 
				
			||||||
 | 
					} from '../model-types';
 | 
				
			||||||
 | 
					import type { ConversationModel } from '../models/conversations';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					  ConversationToDelete,
 | 
				
			||||||
 | 
					  MessageToDelete,
 | 
				
			||||||
 | 
					} from '../textsecure/messageReceiverEvents';
 | 
				
			||||||
 | 
					import type { AciString } from '../types/ServiceId';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const {
 | 
				
			||||||
 | 
					  getMessagesBySentAt,
 | 
				
			||||||
 | 
					  getMostRecentAddressableMessages,
 | 
				
			||||||
 | 
					  removeMessagesInConversation,
 | 
				
			||||||
 | 
					} = dataInterface;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function doesMessageMatch({
 | 
				
			||||||
 | 
					  conversationId,
 | 
				
			||||||
 | 
					  message,
 | 
				
			||||||
 | 
					  query,
 | 
				
			||||||
 | 
					  sentTimestamps,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  message: MessageAttributesType;
 | 
				
			||||||
 | 
					  conversationId: string;
 | 
				
			||||||
 | 
					  query: MessageQuery;
 | 
				
			||||||
 | 
					  sentTimestamps: ReadonlySet<number>;
 | 
				
			||||||
 | 
					}): boolean {
 | 
				
			||||||
 | 
					  const author = getAuthor(message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const conversationMatches = message.conversationId === conversationId;
 | 
				
			||||||
 | 
					  const aciMatches =
 | 
				
			||||||
 | 
					    query.authorAci && author?.attributes.serviceId === query.authorAci;
 | 
				
			||||||
 | 
					  const e164Matches =
 | 
				
			||||||
 | 
					    query.authorE164 && author?.attributes.e164 === query.authorE164;
 | 
				
			||||||
 | 
					  const timestampMatches = sentTimestamps.has(query.sentAt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return Boolean(
 | 
				
			||||||
 | 
					    conversationMatches && timestampMatches && (aciMatches || e164Matches)
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function findMatchingMessage(
 | 
				
			||||||
 | 
					  conversationId: string,
 | 
				
			||||||
 | 
					  query: MessageQuery
 | 
				
			||||||
 | 
					): Promise<MessageAttributesType | undefined> {
 | 
				
			||||||
 | 
					  const sentAtMatches = await getMessagesBySentAt(query.sentAt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!sentAtMatches.length) {
 | 
				
			||||||
 | 
					    return undefined;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return sentAtMatches.find(message => {
 | 
				
			||||||
 | 
					    const sentTimestamps = getMessageSentTimestampSet(message);
 | 
				
			||||||
 | 
					    return doesMessageMatch({
 | 
				
			||||||
 | 
					      conversationId,
 | 
				
			||||||
 | 
					      message,
 | 
				
			||||||
 | 
					      query,
 | 
				
			||||||
 | 
					      sentTimestamps,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function deleteMessage(
 | 
				
			||||||
 | 
					  conversationId: string,
 | 
				
			||||||
 | 
					  targetMessage: MessageToDelete,
 | 
				
			||||||
 | 
					  logId: string
 | 
				
			||||||
 | 
					): Promise<boolean> {
 | 
				
			||||||
 | 
					  const query = getMessageQueryFromTarget(targetMessage);
 | 
				
			||||||
 | 
					  const found = await findMatchingMessage(conversationId, query);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!found) {
 | 
				
			||||||
 | 
					    log.warn(`${logId}: Couldn't find matching message`);
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await deleteAndCleanup([found], logId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function deleteConversation(
 | 
				
			||||||
 | 
					  conversation: ConversationModel,
 | 
				
			||||||
 | 
					  mostRecentMessages: Array<MessageToDelete>,
 | 
				
			||||||
 | 
					  isFullDelete: boolean,
 | 
				
			||||||
 | 
					  logId: string
 | 
				
			||||||
 | 
					): Promise<boolean> {
 | 
				
			||||||
 | 
					  const queries = mostRecentMessages.map(getMessageQueryFromTarget);
 | 
				
			||||||
 | 
					  const found = await Promise.all(
 | 
				
			||||||
 | 
					    queries.map(query => findMatchingMessage(conversation.id, query))
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const sorted = sortBy(found, 'received_at');
 | 
				
			||||||
 | 
					  const newestMessage = last(sorted);
 | 
				
			||||||
 | 
					  if (newestMessage) {
 | 
				
			||||||
 | 
					    const { received_at: receivedAt } = newestMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await removeMessagesInConversation(conversation.id, {
 | 
				
			||||||
 | 
					      receivedAt,
 | 
				
			||||||
 | 
					      logId: `${logId}(receivedAt=${receivedAt})`,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!newestMessage) {
 | 
				
			||||||
 | 
					    log.warn(`${logId}: Found no target messages for delete`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (isFullDelete) {
 | 
				
			||||||
 | 
					    log.info(`${logId}: isFullDelete=true, proceeding to local-only delete`);
 | 
				
			||||||
 | 
					    return deleteLocalOnlyConversation(conversation, logId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function deleteLocalOnlyConversation(
 | 
				
			||||||
 | 
					  conversation: ConversationModel,
 | 
				
			||||||
 | 
					  logId: string
 | 
				
			||||||
 | 
					): Promise<boolean> {
 | 
				
			||||||
 | 
					  const limit = 1;
 | 
				
			||||||
 | 
					  const messages = await getMostRecentAddressableMessages(
 | 
				
			||||||
 | 
					    conversation.id,
 | 
				
			||||||
 | 
					    limit
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  if (messages.length > 0) {
 | 
				
			||||||
 | 
					    log.warn(
 | 
				
			||||||
 | 
					      `${logId}: Attempted local-only delete but found an addressable message`
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // This will delete all messages and remove the conversation from the left pane.
 | 
				
			||||||
 | 
					  // We need to call destroyMessagesInner, since we're already in conversation.queueJob()
 | 
				
			||||||
 | 
					  await conversation.destroyMessagesInner({
 | 
				
			||||||
 | 
					    logId,
 | 
				
			||||||
 | 
					    source: 'local-delete-sync',
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getConversationFromTarget(
 | 
				
			||||||
 | 
					  targetConversation: ConversationToDelete
 | 
				
			||||||
 | 
					): ConversationModel | undefined {
 | 
				
			||||||
 | 
					  const { type } = targetConversation;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (type === 'aci') {
 | 
				
			||||||
 | 
					    return window.ConversationController.get(targetConversation.aci);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (type === 'group') {
 | 
				
			||||||
 | 
					    return window.ConversationController.get(targetConversation.groupId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (type === 'e164') {
 | 
				
			||||||
 | 
					    return window.ConversationController.get(targetConversation.e164);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  throw missingCaseError(type);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type MessageQuery = {
 | 
				
			||||||
 | 
					  sentAt: number;
 | 
				
			||||||
 | 
					  authorAci?: AciString;
 | 
				
			||||||
 | 
					  authorE164?: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getMessageQueryFromTarget(
 | 
				
			||||||
 | 
					  targetMessage: MessageToDelete
 | 
				
			||||||
 | 
					): MessageQuery {
 | 
				
			||||||
 | 
					  const { type, sentAt } = targetMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (type === 'aci') {
 | 
				
			||||||
 | 
					    if (!isAciString(targetMessage.authorAci)) {
 | 
				
			||||||
 | 
					      throw new Error('Provided authorAci was not an ACI!');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return { sentAt, authorAci: targetMessage.authorAci };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (type === 'e164') {
 | 
				
			||||||
 | 
					    return { sentAt, authorE164: targetMessage.authorE164 };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  throw missingCaseError(type);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getConversationToDelete(
 | 
				
			||||||
 | 
					  attributes: ConversationAttributesType
 | 
				
			||||||
 | 
					): ConversationToDelete {
 | 
				
			||||||
 | 
					  const { groupId, serviceId: aci, e164 } = attributes;
 | 
				
			||||||
 | 
					  const idForLogging = getConversationIdForLogging(attributes);
 | 
				
			||||||
 | 
					  const logId = `getConversationToDelete(${idForLogging})`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (isGroupV2(attributes) && groupId) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      type: 'group',
 | 
				
			||||||
 | 
					      groupId,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (isGroup(attributes)) {
 | 
				
			||||||
 | 
					    throw new Error(`${logId}: is a group, but not groupV2 or no groupId!`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (aci && isAciString(aci)) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      type: 'aci',
 | 
				
			||||||
 | 
					      aci,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (e164) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      type: 'e164',
 | 
				
			||||||
 | 
					      e164,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  throw new Error(`${logId}: No valid identifier found!`);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getMessageToDelete(
 | 
				
			||||||
 | 
					  attributes: MessageAttributesType
 | 
				
			||||||
 | 
					): MessageToDelete | undefined {
 | 
				
			||||||
 | 
					  const logId = `getMessageToDelete(${getMessageIdForLogging(attributes)})`;
 | 
				
			||||||
 | 
					  const { sent_at: sentAt } = attributes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const author = getAuthor(attributes);
 | 
				
			||||||
 | 
					  const authorAci = author?.get('serviceId');
 | 
				
			||||||
 | 
					  const authorE164 = author?.get('e164');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (authorAci && isAciString(authorAci)) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      type: 'aci' as const,
 | 
				
			||||||
 | 
					      authorAci,
 | 
				
			||||||
 | 
					      sentAt,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (authorE164) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      type: 'e164' as const,
 | 
				
			||||||
 | 
					      authorE164,
 | 
				
			||||||
 | 
					      sentAt,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  log.warn(`${logId}: Message was missing source ACI/e164`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return undefined;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								ts/util/deleteForMe.types.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								ts/util/deleteForMe.types.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					// Copyright 2020 Signal Messenger, LLC
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const MAX_MESSAGE_COUNT = 500;
 | 
				
			||||||
| 
						 | 
					@ -55,6 +55,7 @@ export const sendTypesEnum = z.enum([
 | 
				
			||||||
  'pniIdentitySync',
 | 
					  'pniIdentitySync',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Syncs, default non-urgent
 | 
					  // Syncs, default non-urgent
 | 
				
			||||||
 | 
					  'deleteForMeSync',
 | 
				
			||||||
  'fetchLatestManifestSync',
 | 
					  'fetchLatestManifestSync',
 | 
				
			||||||
  'fetchLocalProfileSync',
 | 
					  'fetchLocalProfileSync',
 | 
				
			||||||
  'messageRequestSync',
 | 
					  'messageRequestSync',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,7 @@ import type { SendStateByConversationId } from '../messages/MessageSendState';
 | 
				
			||||||
import * as Edits from '../messageModifiers/Edits';
 | 
					import * as Edits from '../messageModifiers/Edits';
 | 
				
			||||||
import * as log from '../logging/log';
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
import * as Deletes from '../messageModifiers/Deletes';
 | 
					import * as Deletes from '../messageModifiers/Deletes';
 | 
				
			||||||
 | 
					import * as DeletesForMe from '../messageModifiers/DeletesForMe';
 | 
				
			||||||
import * as MessageReceipts from '../messageModifiers/MessageReceipts';
 | 
					import * as MessageReceipts from '../messageModifiers/MessageReceipts';
 | 
				
			||||||
import * as Reactions from '../messageModifiers/Reactions';
 | 
					import * as Reactions from '../messageModifiers/Reactions';
 | 
				
			||||||
import * as ReadSyncs from '../messageModifiers/ReadSyncs';
 | 
					import * as ReadSyncs from '../messageModifiers/ReadSyncs';
 | 
				
			||||||
| 
						 | 
					@ -29,6 +30,12 @@ import { missingCaseError } from './missingCaseError';
 | 
				
			||||||
import { reduce } from './iterables';
 | 
					import { reduce } from './iterables';
 | 
				
			||||||
import { strictAssert } from './assert';
 | 
					import { strictAssert } from './assert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum ModifyTargetMessageResult {
 | 
				
			||||||
 | 
					  Modified = 'Modified',
 | 
				
			||||||
 | 
					  NotModified = 'MotModified',
 | 
				
			||||||
 | 
					  Deleted = 'Deleted',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// This function is called twice - once from handleDataMessage, and then again from
 | 
					// This function is called twice - once from handleDataMessage, and then again from
 | 
				
			||||||
//    saveAndNotify, a function called at the end of handleDataMessage as a cleanup for
 | 
					//    saveAndNotify, a function called at the end of handleDataMessage as a cleanup for
 | 
				
			||||||
//    any missed out-of-order events.
 | 
					//    any missed out-of-order events.
 | 
				
			||||||
| 
						 | 
					@ -36,7 +43,7 @@ export async function modifyTargetMessage(
 | 
				
			||||||
  message: MessageModel,
 | 
					  message: MessageModel,
 | 
				
			||||||
  conversation: ConversationModel,
 | 
					  conversation: ConversationModel,
 | 
				
			||||||
  options?: { isFirstRun: boolean; skipEdits: boolean }
 | 
					  options?: { isFirstRun: boolean; skipEdits: boolean }
 | 
				
			||||||
): Promise<void> {
 | 
					): Promise<ModifyTargetMessageResult> {
 | 
				
			||||||
  const { isFirstRun = false, skipEdits = false } = options ?? {};
 | 
					  const { isFirstRun = false, skipEdits = false } = options ?? {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const logId = `modifyTargetMessage/${message.idForLogging()}`;
 | 
					  const logId = `modifyTargetMessage/${message.idForLogging()}`;
 | 
				
			||||||
| 
						 | 
					@ -45,6 +52,15 @@ export async function modifyTargetMessage(
 | 
				
			||||||
  const ourAci = window.textsecure.storage.user.getCheckedAci();
 | 
					  const ourAci = window.textsecure.storage.user.getCheckedAci();
 | 
				
			||||||
  const sourceServiceId = getSourceServiceId(message.attributes);
 | 
					  const sourceServiceId = getSourceServiceId(message.attributes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const syncDeletes = await DeletesForMe.forMessage(message.attributes);
 | 
				
			||||||
 | 
					  if (syncDeletes.length) {
 | 
				
			||||||
 | 
					    if (!isFirstRun) {
 | 
				
			||||||
 | 
					      await window.Signal.Data.removeMessage(message.id);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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 sendActions = MessageReceipts.forMessage(message).map(receipt => {
 | 
				
			||||||
      let sendActionType: SendActionType;
 | 
					      let sendActionType: SendActionType;
 | 
				
			||||||
| 
						 | 
					@ -274,4 +290,8 @@ export async function modifyTargetMessage(
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return changed
 | 
				
			||||||
 | 
					    ? ModifyTargetMessageResult.Modified
 | 
				
			||||||
 | 
					    : ModifyTargetMessageResult.NotModified;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										138
									
								
								ts/util/syncTasks.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								ts/util/syncTasks.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,138 @@
 | 
				
			||||||
 | 
					// Copyright 2020 Signal Messenger, LLC
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { z } from 'zod';
 | 
				
			||||||
 | 
					import type { ZodSchema } from 'zod';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as log from '../logging/log';
 | 
				
			||||||
 | 
					import * as DeletesForMe from '../messageModifiers/DeletesForMe';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  deleteMessageSchema,
 | 
				
			||||||
 | 
					  deleteConversationSchema,
 | 
				
			||||||
 | 
					  deleteLocalConversationSchema,
 | 
				
			||||||
 | 
					} from '../textsecure/messageReceiverEvents';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  deleteConversation,
 | 
				
			||||||
 | 
					  deleteLocalOnlyConversation,
 | 
				
			||||||
 | 
					  getConversationFromTarget,
 | 
				
			||||||
 | 
					} from './deleteForMe';
 | 
				
			||||||
 | 
					import { drop } from './drop';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const syncTaskDataSchema = z.union([
 | 
				
			||||||
 | 
					  deleteMessageSchema,
 | 
				
			||||||
 | 
					  deleteConversationSchema,
 | 
				
			||||||
 | 
					  deleteLocalConversationSchema,
 | 
				
			||||||
 | 
					]);
 | 
				
			||||||
 | 
					export type SyncTaskData = z.infer<typeof syncTaskDataSchema>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type SyncTaskType = Readonly<{
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  attempts: number;
 | 
				
			||||||
 | 
					  createdAt: number;
 | 
				
			||||||
 | 
					  data: unknown;
 | 
				
			||||||
 | 
					  envelopeId: string;
 | 
				
			||||||
 | 
					  sentAt: number;
 | 
				
			||||||
 | 
					  type: SyncTaskData['type'];
 | 
				
			||||||
 | 
					}>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SCHEMAS_BY_TYPE: Record<SyncTaskData['type'], ZodSchema> = {
 | 
				
			||||||
 | 
					  'delete-message': deleteMessageSchema,
 | 
				
			||||||
 | 
					  'delete-conversation': deleteConversationSchema,
 | 
				
			||||||
 | 
					  'delete-local-conversation': deleteLocalConversationSchema,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function toLogId(task: SyncTaskType) {
 | 
				
			||||||
 | 
					  return `task=${task.id},timestamp:${task},type=${task.type},envelopeId=${task.envelopeId}`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function queueSyncTasks(
 | 
				
			||||||
 | 
					  tasks: Array<SyncTaskType>,
 | 
				
			||||||
 | 
					  removeSyncTaskById: (id: string) => Promise<void>
 | 
				
			||||||
 | 
					): Promise<void> {
 | 
				
			||||||
 | 
					  const logId = 'queueSyncTasks';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (let i = 0, max = tasks.length; i < max; i += 1) {
 | 
				
			||||||
 | 
					    const task = tasks[i];
 | 
				
			||||||
 | 
					    const { id, envelopeId, type, sentAt, data } = task;
 | 
				
			||||||
 | 
					    const innerLogId = `${logId}(${toLogId(task)})`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const schema = SCHEMAS_BY_TYPE[type];
 | 
				
			||||||
 | 
					    if (!schema) {
 | 
				
			||||||
 | 
					      log.error(`${innerLogId}: Schema not found. Deleting.`);
 | 
				
			||||||
 | 
					      // eslint-disable-next-line no-await-in-loop
 | 
				
			||||||
 | 
					      await removeSyncTaskById(id);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const parseResult = syncTaskDataSchema.safeParse(data);
 | 
				
			||||||
 | 
					    if (!parseResult.success) {
 | 
				
			||||||
 | 
					      log.error(
 | 
				
			||||||
 | 
					        `${innerLogId}: Failed to parse. Deleting. Error: ${parseResult.error}`
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      // eslint-disable-next-line no-await-in-loop
 | 
				
			||||||
 | 
					      await removeSyncTaskById(id);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { data: parsed } = parseResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (parsed.type === 'delete-message') {
 | 
				
			||||||
 | 
					      // eslint-disable-next-line no-await-in-loop
 | 
				
			||||||
 | 
					      await DeletesForMe.onDelete({
 | 
				
			||||||
 | 
					        conversation: parsed.conversation,
 | 
				
			||||||
 | 
					        envelopeId,
 | 
				
			||||||
 | 
					        message: parsed.message,
 | 
				
			||||||
 | 
					        syncTaskId: id,
 | 
				
			||||||
 | 
					        timestamp: sentAt,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else if (parsed.type === 'delete-conversation') {
 | 
				
			||||||
 | 
					      const {
 | 
				
			||||||
 | 
					        conversation: targetConversation,
 | 
				
			||||||
 | 
					        mostRecentMessages,
 | 
				
			||||||
 | 
					        isFullDelete,
 | 
				
			||||||
 | 
					      } = parsed;
 | 
				
			||||||
 | 
					      const conversation = getConversationFromTarget(targetConversation);
 | 
				
			||||||
 | 
					      if (!conversation) {
 | 
				
			||||||
 | 
					        log.error(`${innerLogId}: Conversation not found!`);
 | 
				
			||||||
 | 
					        continue;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      drop(
 | 
				
			||||||
 | 
					        conversation.queueJob(innerLogId, async () => {
 | 
				
			||||||
 | 
					          log.info(`${logId}: Starting...`);
 | 
				
			||||||
 | 
					          const result = await deleteConversation(
 | 
				
			||||||
 | 
					            conversation,
 | 
				
			||||||
 | 
					            mostRecentMessages,
 | 
				
			||||||
 | 
					            isFullDelete,
 | 
				
			||||||
 | 
					            innerLogId
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          if (result) {
 | 
				
			||||||
 | 
					            await removeSyncTaskById(id);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          log.info(`${logId}: Done, result=${result}`);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else if (parsed.type === 'delete-local-conversation') {
 | 
				
			||||||
 | 
					      const { conversation: targetConversation } = parsed;
 | 
				
			||||||
 | 
					      const conversation = getConversationFromTarget(targetConversation);
 | 
				
			||||||
 | 
					      if (!conversation) {
 | 
				
			||||||
 | 
					        log.error(`${innerLogId}: Conversation not found!`);
 | 
				
			||||||
 | 
					        continue;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      drop(
 | 
				
			||||||
 | 
					        conversation.queueJob(innerLogId, async () => {
 | 
				
			||||||
 | 
					          log.info(`${logId}: Starting...`);
 | 
				
			||||||
 | 
					          const result = await deleteLocalOnlyConversation(
 | 
				
			||||||
 | 
					            conversation,
 | 
				
			||||||
 | 
					            innerLogId
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // Note: we remove even with a 'false' result because we're only gonna
 | 
				
			||||||
 | 
					          //   get more messages in this conversation from here!
 | 
				
			||||||
 | 
					          await removeSyncTaskById(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          log.info(`${logId}: Done; result=${result}`);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								ts/util/syncTasks.types.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								ts/util/syncTasks.types.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					// Copyright 2020 Signal Messenger, LLC
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const MAX_SYNC_TASK_ATTEMPTS = 5;
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue