diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 7ba4351dee..75c407f468 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -165,6 +165,8 @@ 3496744F2076ACD000080B5F /* LongTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496744E2076ACCE00080B5F /* LongTextViewController.swift */; }; 34A55F3720485465002CC6DE /* OWS2FARegistrationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A55F3520485464002CC6DE /* OWS2FARegistrationViewController.m */; }; 34A910601FFEB114000C4745 /* OWSBackup.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A9105F1FFEB114000C4745 /* OWSBackup.m */; }; + 34ABB2C42090C59700C727A6 /* OWSResaveCollectionDBMigration.m in Sources */ = {isa = PBXBuildFile; fileRef = 34ABB2C22090C59600C727A6 /* OWSResaveCollectionDBMigration.m */; }; + 34ABB2C52090C59700C727A6 /* OWSResaveCollectionDBMigration.h in Headers */ = {isa = PBXBuildFile; fileRef = 34ABB2C32090C59700C727A6 /* OWSResaveCollectionDBMigration.h */; }; 34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B0796B1FCF46B000E248C2 /* MainAppContext.m */; }; 34B3F8751E8DF1700035BE1A /* CallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F83B1E8DF1700035BE1A /* CallViewController.swift */; }; 34B3F8771E8DF1700035BE1A /* ContactsPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F83E1E8DF1700035BE1A /* ContactsPicker.swift */; }; @@ -214,6 +216,8 @@ 34D2CCDA2062E7D000CB1A14 /* OWSScreenLockUI.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */; }; 34D2CCDF206939B400CB1A14 /* DebugUIMessagesAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCDB206939B100CB1A14 /* DebugUIMessagesAction.m */; }; 34D2CCE0206939B400CB1A14 /* DebugUIMessagesAssetLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCDC206939B200CB1A14 /* DebugUIMessagesAssetLoader.m */; }; + 34D5872F208E2C4200D2255A /* OWS109OutgoingMessageState.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5872D208E2C4100D2255A /* OWS109OutgoingMessageState.m */; }; + 34D58730208E2C4200D2255A /* OWS109OutgoingMessageState.h in Headers */ = {isa = PBXBuildFile; fileRef = 34D5872E208E2C4100D2255A /* OWS109OutgoingMessageState.h */; }; 34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */; }; 34D8C0271ED3673300188D7C /* DebugUIMessages.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C0241ED3673300188D7C /* DebugUIMessages.m */; }; 34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C0261ED3673300188D7C /* DebugUITableViewController.m */; }; @@ -758,6 +762,8 @@ 34A55F3620485464002CC6DE /* OWS2FARegistrationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWS2FARegistrationViewController.h; sourceTree = ""; }; 34A9105E1FFEB113000C4745 /* OWSBackup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackup.h; sourceTree = ""; }; 34A9105F1FFEB114000C4745 /* OWSBackup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackup.m; sourceTree = ""; }; + 34ABB2C22090C59600C727A6 /* OWSResaveCollectionDBMigration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSResaveCollectionDBMigration.m; sourceTree = ""; }; + 34ABB2C32090C59700C727A6 /* OWSResaveCollectionDBMigration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSResaveCollectionDBMigration.h; sourceTree = ""; }; 34B0796B1FCF46B000E248C2 /* MainAppContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MainAppContext.m; sourceTree = ""; }; 34B0796C1FCF46B000E248C2 /* MainAppContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MainAppContext.h; sourceTree = ""; }; 34B0796E1FD07B1E00E248C2 /* SignalShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SignalShareExtension.entitlements; sourceTree = ""; }; @@ -847,6 +853,8 @@ 34D2CCDD206939B200CB1A14 /* DebugUIMessagesAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessagesAction.h; sourceTree = ""; }; 34D2CCDE206939B400CB1A14 /* DebugUIMessagesAssetLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessagesAssetLoader.h; sourceTree = ""; }; 34D2CCE220693A1700CB1A14 /* DebugUIMessagesUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessagesUtils.h; sourceTree = ""; }; + 34D5872D208E2C4100D2255A /* OWS109OutgoingMessageState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWS109OutgoingMessageState.m; sourceTree = ""; }; + 34D5872E208E2C4100D2255A /* OWS109OutgoingMessageState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWS109OutgoingMessageState.h; sourceTree = ""; }; 34D5CCA71EAE3D30005515DB /* AvatarViewHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvatarViewHelper.h; sourceTree = ""; }; 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AvatarViewHelper.m; sourceTree = ""; }; 34D8C0231ED3673300188D7C /* DebugUIMessages.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessages.h; sourceTree = ""; }; @@ -1525,10 +1533,14 @@ 4503F1C1204711D200CEE724 /* OWS107LegacySounds.m */, 4598198C204E2F28009414F2 /* OWS108CallLoggingPreference.h */, 4598198D204E2F28009414F2 /* OWS108CallLoggingPreference.m */, + 34D5872E208E2C4100D2255A /* OWS109OutgoingMessageState.h */, + 34D5872D208E2C4100D2255A /* OWS109OutgoingMessageState.m */, 346129931FD1E30000532771 /* OWSDatabaseMigration.h */, 346129941FD1E30000532771 /* OWSDatabaseMigration.m */, 346129E51FD5C0C600532771 /* OWSDatabaseMigrationRunner.h */, 346129E41FD5C0C600532771 /* OWSDatabaseMigrationRunner.m */, + 34ABB2C32090C59700C727A6 /* OWSResaveCollectionDBMigration.h */, + 34ABB2C22090C59600C727A6 /* OWSResaveCollectionDBMigration.m */, ); path = migrations; sourceTree = ""; @@ -2351,6 +2363,7 @@ 346129E71FD5C0C600532771 /* OWSDatabaseMigrationRunner.h in Headers */, 344D6CEA20069E070042AF96 /* SelectRecipientViewController.h in Headers */, 34480B521FD0A7A400BC14EF /* OWSLogger.h in Headers */, + 34ABB2C52090C59700C727A6 /* OWSResaveCollectionDBMigration.h in Headers */, 459B775D207BA4810071D0AB /* OWSQuotedReplyModel.h in Headers */, 34612A001FD5F31400532771 /* OWS105AttachmentFilePaths.h in Headers */, 346129F61FD5F31400532771 /* OWS103EnableVideoCalling.h in Headers */, @@ -2379,6 +2392,7 @@ 34480B611FD0A98800BC14EF /* UIColor+OWS.h in Headers */, 453518961FC63DBF00210559 /* SignalMessaging.h in Headers */, 3461295A1FD1D74C00532771 /* Environment.h in Headers */, + 34D58730208E2C4200D2255A /* OWS109OutgoingMessageState.h in Headers */, 4598198E204E2F28009414F2 /* OWS108CallLoggingPreference.h in Headers */, 34480B631FD0A98800BC14EF /* UIView+OWS.h in Headers */, 451F8A4B1FD715E1005CB9DA /* OWSGroupAvatarBuilder.h in Headers */, @@ -3045,6 +3059,7 @@ 451F8A3D1FD713CA005CB9DA /* ThreadViewHelper.m in Sources */, 346129AD1FD1F34E00532771 /* ImageCache.swift in Sources */, 452C7CA72037628B003D51A5 /* Weak.swift in Sources */, + 34D5872F208E2C4200D2255A /* OWS109OutgoingMessageState.m in Sources */, 451F8A341FD710C3005CB9DA /* ConversationSearcher.swift in Sources */, 346129341FD1A88700532771 /* OWSSwiftUtils.swift in Sources */, 346129FE1FD5F31400532771 /* OWS106EnsureProfileComplete.swift in Sources */, @@ -3089,6 +3104,7 @@ 346129721FD1D74C00532771 /* SignalKeyingStorage.m in Sources */, 34480B561FD0A7A400BC14EF /* DebugLogger.m in Sources */, 459B775C207BA46C0071D0AB /* OWSQuotedReplyModel.m in Sources */, + 34ABB2C42090C59700C727A6 /* OWSResaveCollectionDBMigration.m in Sources */, 4551DB5A205C562300C8AE75 /* Collection+OWS.swift in Sources */, 3461293C1FD1D46A00532771 /* OWSMath.m in Sources */, 451F8A391FD711D6005CB9DA /* ContactsViewHelper.m in Sources */, @@ -3425,7 +3441,11 @@ "DEBUG=1", "$(inherited)", ); - "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = "DEBUG=1 $(inherited) SSK_BUILDING_FOR_TESTS=1"; + "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = ( + "DEBUG=1", + "$(inherited)", + "SSK_BUILDING_FOR_TESTS=1", + ); GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index 9bd4f21557..3e05346ce8 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -499,7 +499,7 @@ NS_ASSUME_NONNULL_BEGIN // Ignore taps on links in outgoing messages that haven't been sent yet, as // this interferes with "tap to retry". TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction; - shouldIgnoreEvents = outgoingMessage.messageState != TSOutgoingMessageStateSentToService; + shouldIgnoreEvents = outgoingMessage.messageState != TSOutgoingMessageStateSent; } [self.class loadForTextDisplay:self.bodyTextView text:self.displayableBodyText.displayText @@ -1026,7 +1026,7 @@ NS_ASSUME_NONNULL_BEGIN return NO; } TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction; - return outgoingMessage.messageState == TSOutgoingMessageStateAttemptingOut; + return outgoingMessage.messageState == TSOutgoingMessageStateSending; } - (OWSMessagesBubbleImageFactory *)bubbleFactory @@ -1085,9 +1085,9 @@ NS_ASSUME_NONNULL_BEGIN if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) { TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction; - if (outgoingMessage.messageState == TSOutgoingMessageStateUnsent) { + if (outgoingMessage.messageState == TSOutgoingMessageStateFailed) { return; - } else if (outgoingMessage.messageState == TSOutgoingMessageStateAttemptingOut) { + } else if (outgoingMessage.messageState == TSOutgoingMessageStateSending) { // Ignore taps on outgoing messages being sent. return; } diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 6c19879c3d..7e08837e7f 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -108,7 +108,7 @@ NS_ASSUME_NONNULL_BEGIN return NO; } TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction; - return outgoingMessage.messageState == TSOutgoingMessageStateUnsent; + return outgoingMessage.messageState == TSOutgoingMessageStateFailed; } - (UIImage *)failedSendBadge @@ -316,7 +316,7 @@ NS_ASSUME_NONNULL_BEGIN if (!self.viewItem.shouldHideRecipientStatus || hasExpirationTimer) { TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)message; NSString *statusMessage = - [MessageRecipientStatusUtils statusMessageWithOutgoingMessage:outgoingMessage referenceView:self]; + [MessageRecipientStatusUtils receiptMessageWithOutgoingMessage:outgoingMessage referenceView:self]; attributedText = [[NSAttributedString alloc] initWithString:statusMessage attributes:@{}]; } } else if (self.viewItem.isGroupThread) { @@ -505,10 +505,10 @@ NS_ASSUME_NONNULL_BEGIN if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) { TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction; - if (outgoingMessage.messageState == TSOutgoingMessageStateUnsent) { + if (outgoingMessage.messageState == TSOutgoingMessageStateFailed) { [self.delegate didTapFailedOutgoingMessage:outgoingMessage]; return; - } else if (outgoingMessage.messageState == TSOutgoingMessageStateAttemptingOut) { + } else if (outgoingMessage.messageState == TSOutgoingMessageStateSending) { // Ignore taps on outgoing messages being sent. return; } @@ -527,10 +527,10 @@ NS_ASSUME_NONNULL_BEGIN if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) { TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction; - if (outgoingMessage.messageState == TSOutgoingMessageStateUnsent) { + if (outgoingMessage.messageState == TSOutgoingMessageStateFailed) { // Ignore long press on unsent messages. return; - } else if (outgoingMessage.messageState == TSOutgoingMessageStateAttemptingOut) { + } else if (outgoingMessage.messageState == TSOutgoingMessageStateSending) { // Ignore long press on outgoing messages being sent. return; } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index ebc7cdaf75..59a4910ee6 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -4651,7 +4651,7 @@ typedef enum : NSUInteger { // Update the "shouldShowDate" property of the view items. OWSInteractionType lastInteractionType = OWSInteractionType_Unknown; - MessageRecipientStatus lastRecipientStatus = MessageRecipientStatusUploading; + MessageReceiptStatus lastReceiptStatus = MessageReceiptStatusUploading; NSString *_Nullable lastIncomingSenderId = nil; for (ConversationViewItem *viewItem in viewItems.reverseObjectEnumerator) { BOOL shouldHideRecipientStatus = NO; @@ -4660,20 +4660,21 @@ typedef enum : NSUInteger { if (interactionType == OWSInteractionType_OutgoingMessage) { TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)viewItem.interaction; - MessageRecipientStatus recipientStatus = - [MessageRecipientStatusUtils recipientStatusWithOutgoingMessage:outgoingMessage]; + MessageReceiptStatus receiptStatus = + [MessageRecipientStatusUtils recipientStatusWithOutgoingMessage:outgoingMessage + referenceView:self.view]; - if (outgoingMessage.messageState == TSOutgoingMessageStateUnsent) { + if (outgoingMessage.messageState == TSOutgoingMessageStateFailed) { // always show "failed to send" status shouldHideRecipientStatus = NO; } else { shouldHideRecipientStatus - = (interactionType == lastInteractionType && recipientStatus == lastRecipientStatus); + = (interactionType == lastInteractionType && receiptStatus == lastReceiptStatus); } shouldHideBubbleTail = interactionType == lastInteractionType; - lastRecipientStatus = recipientStatus; + lastReceiptStatus = receiptStatus; } else if (interactionType == OWSInteractionType_IncomingMessage) { TSIncomingMessage *incomingMessage = (TSIncomingMessage *)viewItem.interaction; NSString *incomingSenderId = incomingMessage.authorId; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index 2a50d9e2e8..4070f192ae 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -662,8 +662,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) } else if (action == self.replyActionSelector) { if ([self.interaction isKindOfClass:[TSOutgoingMessage class]]) { TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.interaction; - if (outgoingMessage.messageState == TSOutgoingMessageStateUnsent - || outgoingMessage.messageState == TSOutgoingMessageStateAttemptingOut) { + if (outgoingMessage.messageState == TSOutgoingMessageStateFailed + || outgoingMessage.messageState == TSOutgoingMessageStateSending) { // Don't let users reply to messages which aren't yet delivered to the service. return NO; } diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index ef3ffc1dda..1d6b883ec7 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -1148,146 +1148,126 @@ NS_ASSUME_NONNULL_BEGIN if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Jpeg ⚠️"]]; } [actions addObjectsFromArray:@[ - [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], - [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], - [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], - [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:YES], - [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], - [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO], + [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:YES], + [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateSending hasCaption:NO], + [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateSending hasCaption:YES], + [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO], + [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateSent hasCaption:YES], ]]; if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Gif ⚠️"]]; } [actions addObjectsFromArray:@[ // Don't bother with multiple GIF states. - [self fakeOutgoingGifAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], - [self fakeOutgoingLargeGifAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], + [self fakeOutgoingGifAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO], + [self fakeOutgoingLargeGifAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO], ]]; if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Mp3 ⚠️"]]; } [actions addObjectsFromArray:@[ - [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:YES], - [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], - [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], - [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], - [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], - [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateSending hasCaption:YES], + [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateSending hasCaption:NO], + [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO], + [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateFailed hasCaption:YES], + [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateSent hasCaption:NO], + [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateSent hasCaption:YES], ]]; if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Mp4 ⚠️"]]; } [actions addObjectsFromArray:@[ - [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], - [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:YES], - [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], - [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], - [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], - [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateSending hasCaption:NO], + [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateSending hasCaption:YES], + [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO], + [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateFailed hasCaption:YES], + [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateSent hasCaption:NO], + [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateSent hasCaption:YES], ]]; if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Compact Landscape Png ⚠️"]]; } [actions addObjectsFromArray:@[ - [self fakeOutgoingCompactLandscapePngAction:thread - messageState:TSOutgoingMessageStateAttemptingOut - hasCaption:NO], - [self fakeOutgoingCompactLandscapePngAction:thread - messageState:TSOutgoingMessageStateAttemptingOut - hasCaption:YES], - [self fakeOutgoingCompactLandscapePngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], - [self fakeOutgoingCompactLandscapePngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], - [self fakeOutgoingCompactLandscapePngAction:thread - messageState:TSOutgoingMessageStateSentToService - hasCaption:NO], - [self fakeOutgoingCompactLandscapePngAction:thread - messageState:TSOutgoingMessageStateSentToService - hasCaption:YES], + [self fakeOutgoingCompactLandscapePngAction:thread messageState:TSOutgoingMessageStateSending hasCaption:NO], + [self fakeOutgoingCompactLandscapePngAction:thread messageState:TSOutgoingMessageStateSending hasCaption:YES], + [self fakeOutgoingCompactLandscapePngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO], + [self fakeOutgoingCompactLandscapePngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:YES], + [self fakeOutgoingCompactLandscapePngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO], + [self fakeOutgoingCompactLandscapePngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:YES], ]]; if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Compact Portrait Png ⚠️"]]; } [actions addObjectsFromArray:@[ - [self fakeOutgoingCompactPortraitPngAction:thread - messageState:TSOutgoingMessageStateAttemptingOut - hasCaption:NO], - [self fakeOutgoingCompactPortraitPngAction:thread - messageState:TSOutgoingMessageStateAttemptingOut - hasCaption:YES], - [self fakeOutgoingCompactPortraitPngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], - [self fakeOutgoingCompactPortraitPngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], - [self fakeOutgoingCompactPortraitPngAction:thread - messageState:TSOutgoingMessageStateSentToService - hasCaption:NO], - [self fakeOutgoingCompactPortraitPngAction:thread - messageState:TSOutgoingMessageStateSentToService - hasCaption:YES], + [self fakeOutgoingCompactPortraitPngAction:thread messageState:TSOutgoingMessageStateSending hasCaption:NO], + [self fakeOutgoingCompactPortraitPngAction:thread messageState:TSOutgoingMessageStateSending hasCaption:YES], + [self fakeOutgoingCompactPortraitPngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO], + [self fakeOutgoingCompactPortraitPngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:YES], + [self fakeOutgoingCompactPortraitPngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO], + [self fakeOutgoingCompactPortraitPngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:YES], ]]; if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Wide Landscape Png ⚠️"]]; } [actions addObjectsFromArray:@[ - [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], - [self fakeOutgoingWideLandscapePngAction:thread - messageState:TSOutgoingMessageStateAttemptingOut - hasCaption:YES], - [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], - [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], - [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], - [self fakeOutgoingWideLandscapePngAction:thread - messageState:TSOutgoingMessageStateSentToService - hasCaption:YES], + [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateSending hasCaption:NO], + [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateSending hasCaption:YES], + [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO], + [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:YES], + [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO], + [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:YES], ]]; if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Tall Portrait Png ⚠️"]]; } [actions addObjectsFromArray:@[ - [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], - [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:YES], - [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], - [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], - [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], - [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateSending hasCaption:NO], + [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateSending hasCaption:YES], + [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO], + [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:YES], + [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO], + [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:YES], ]]; if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Large Png ⚠️"]]; } [actions addObjectsFromArray:@[ - [self fakeOutgoingLargePngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], - [self fakeOutgoingLargePngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + [self fakeOutgoingLargePngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO], + [self fakeOutgoingLargePngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:YES], ]]; if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Tiny Png ⚠️"]]; } [actions addObjectsFromArray:@[ - [self fakeOutgoingTinyPngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], - [self fakeOutgoingTinyPngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + [self fakeOutgoingTinyPngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO], + [self fakeOutgoingTinyPngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:YES], ]]; if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Reserved Color Png ⚠️"]]; } [actions addObjectsFromArray:@[ @@ -1297,7 +1277,7 @@ NS_ASSUME_NONNULL_BEGIN backgroundColor:[UIColor whiteColor] textColor:[UIColor ows_signalBrandBlueColor] imageLabel:@"W" - messageState:TSOutgoingMessageStateUnsent + messageState:TSOutgoingMessageStateFailed hasCaption:YES], [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing White Png" @@ -1305,7 +1285,7 @@ NS_ASSUME_NONNULL_BEGIN backgroundColor:[UIColor whiteColor] textColor:[UIColor ows_signalBrandBlueColor] imageLabel:@"W" - messageState:TSOutgoingMessageStateAttemptingOut + messageState:TSOutgoingMessageStateSending hasCaption:YES], [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing White Png" @@ -1313,7 +1293,7 @@ NS_ASSUME_NONNULL_BEGIN backgroundColor:[UIColor whiteColor] textColor:[UIColor ows_signalBrandBlueColor] imageLabel:@"W" - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent hasCaption:YES], [self fakeOutgoingPngAction:thread @@ -1322,7 +1302,7 @@ NS_ASSUME_NONNULL_BEGIN backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingUnsent] textColor:[UIColor whiteColor] imageLabel:@"W" - messageState:TSOutgoingMessageStateUnsent + messageState:TSOutgoingMessageStateFailed hasCaption:YES], [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing 'Outgoing Unsent' Png" @@ -1330,7 +1310,7 @@ NS_ASSUME_NONNULL_BEGIN backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingUnsent] textColor:[UIColor whiteColor] imageLabel:@"W" - messageState:TSOutgoingMessageStateAttemptingOut + messageState:TSOutgoingMessageStateSending hasCaption:YES], [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing 'Outgoing Unsent' Png" @@ -1338,7 +1318,7 @@ NS_ASSUME_NONNULL_BEGIN backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingUnsent] textColor:[UIColor whiteColor] imageLabel:@"W" - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent hasCaption:YES], [self fakeOutgoingPngAction:thread @@ -1347,7 +1327,7 @@ NS_ASSUME_NONNULL_BEGIN backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingSending] textColor:[UIColor whiteColor] imageLabel:@"W" - messageState:TSOutgoingMessageStateUnsent + messageState:TSOutgoingMessageStateFailed hasCaption:YES], [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing 'Outgoing Sending' Png" @@ -1355,7 +1335,7 @@ NS_ASSUME_NONNULL_BEGIN backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingSending] textColor:[UIColor whiteColor] imageLabel:@"W" - messageState:TSOutgoingMessageStateAttemptingOut + messageState:TSOutgoingMessageStateSending hasCaption:YES], [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing 'Outgoing Sending' Png" @@ -1363,7 +1343,7 @@ NS_ASSUME_NONNULL_BEGIN backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingSending] textColor:[UIColor whiteColor] imageLabel:@"W" - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent hasCaption:YES], [self fakeOutgoingPngAction:thread @@ -1372,7 +1352,7 @@ NS_ASSUME_NONNULL_BEGIN backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingSent] textColor:[UIColor whiteColor] imageLabel:@"W" - messageState:TSOutgoingMessageStateUnsent + messageState:TSOutgoingMessageStateFailed hasCaption:YES], [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing 'Outgoing Sent' Png" @@ -1380,7 +1360,7 @@ NS_ASSUME_NONNULL_BEGIN backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingSent] textColor:[UIColor whiteColor] imageLabel:@"W" - messageState:TSOutgoingMessageStateAttemptingOut + messageState:TSOutgoingMessageStateSending hasCaption:YES], [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing 'Outgoing Sent' Png" @@ -1388,55 +1368,55 @@ NS_ASSUME_NONNULL_BEGIN backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingSent] textColor:[UIColor whiteColor] imageLabel:@"W" - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent hasCaption:YES], ]]; if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Tiny Pdf ⚠️"]]; } [actions addObjectsFromArray:@[ - [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], - [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:YES], - [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], - [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], - [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], - [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateSending hasCaption:NO], + [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateSending hasCaption:YES], + [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO], + [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:YES], + [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO], + [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateSent hasCaption:YES], ]]; if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Large Pdf ⚠️"]]; } [actions addObjectsFromArray:@[ - [self fakeOutgoingLargePdfAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + [self fakeOutgoingLargePdfAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO], ]]; if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Missing Png ⚠️"]]; } [actions addObjectsFromArray:@[ - [self fakeOutgoingMissingPngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + [self fakeOutgoingMissingPngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO], ]]; if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Large Pdf ⚠️"]]; } [actions addObjectsFromArray:@[ - [self fakeOutgoingMissingPdfAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + [self fakeOutgoingMissingPdfAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO], ]]; if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Oversize Text ⚠️"]]; } [actions addObjectsFromArray:@[ - [self fakeOutgoingOversizeTextAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], - [self fakeOutgoingOversizeTextAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], - [self fakeOutgoingOversizeTextAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], + [self fakeOutgoingOversizeTextAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO], + [self fakeOutgoingOversizeTextAction:thread messageState:TSOutgoingMessageStateSending hasCaption:NO], + [self fakeOutgoingOversizeTextAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO], ]]; // Incoming @@ -1837,31 +1817,31 @@ NS_ASSUME_NONNULL_BEGIN if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Statuses ⚠️"]]; } [actions addObjectsFromArray:@[ - [self fakeShortOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateUnsent], - [self fakeShortOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateAttemptingOut], - [self fakeShortOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSentToService], + [self fakeShortOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateFailed], + [self fakeShortOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSending], + [self fakeShortOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSent], [self fakeShortOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent isDelivered:YES isRead:NO], [self fakeShortOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent isDelivered:YES isRead:YES], ]]; if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Message Bodies ⚠️"]]; } for (NSString *messageBody in messageBodies) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:messageBody]]; } return actions; @@ -1905,7 +1885,7 @@ NS_ASSUME_NONNULL_BEGIN // Used fixed values for properties that shouldn't matter much. BOOL quotedMessageIsDelivered = NO; BOOL quotedMessageIsRead = NO; - TSOutgoingMessageState quotedMessageMessageState = TSOutgoingMessageStateSentToService; + TSOutgoingMessageState quotedMessageMessageState = TSOutgoingMessageStateSent; BOOL replyIsDelivered = NO; BOOL replyIsRead = NO; @@ -2063,7 +2043,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Short Text" @@ -2074,7 +2054,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:mediumText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Medium Text" @@ -2085,7 +2065,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Medium Text" @@ -2096,7 +2076,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:mediumText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Long Text" @@ -2107,7 +2087,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:longText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], ]]; if (includeLabels) { @@ -2124,7 +2104,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Jpg" @@ -2135,7 +2115,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Mp3" @@ -2146,7 +2126,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Mp3" @@ -2157,7 +2137,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Mp4" @@ -2168,7 +2148,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Mp4" @@ -2179,7 +2159,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Gif" @@ -2190,7 +2170,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Gif" @@ -2201,7 +2181,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Pdf" @@ -2212,7 +2192,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Missing Pdf" @@ -2223,7 +2203,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Tiny Png" @@ -2234,7 +2214,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Missing Png" @@ -2245,7 +2225,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], ]]; if (includeLabels) { @@ -2262,7 +2242,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Tall Portrait Png" @@ -2273,7 +2253,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:mediumText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Tall Portrait Png" @@ -2284,7 +2264,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Wide Landscape Png" @@ -2295,7 +2275,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Wide Landscape Png" @@ -2306,7 +2286,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:mediumText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Wide Landscape Png" @@ -2317,7 +2297,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:mediumText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Tiny Png" @@ -2328,7 +2308,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Tiny Png" @@ -2339,7 +2319,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:mediumText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], ]]; void (^directionActions)(BOOL, BOOL) = ^(BOOL isQuotedMessageIncoming, BOOL isReplyIncoming) { @@ -2353,7 +2333,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:isReplyIncoming replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], ]]; }; @@ -2380,7 +2360,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Mp3" @@ -2391,7 +2371,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Mp4" @@ -2402,7 +2382,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Gif" @@ -2413,7 +2393,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Pdf" @@ -2424,7 +2404,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Missing Pdf" @@ -2435,7 +2415,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Tiny Png" @@ -2446,7 +2426,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Missing Png" @@ -2457,7 +2437,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Short Text" @@ -2468,7 +2448,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateAttemptingOut], + replyMessageState:TSOutgoingMessageStateSending], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Short Text" @@ -2479,7 +2459,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], [self fakeQuotedReplyAction:thread quotedMessageLabel:@"Short Text" @@ -2490,7 +2470,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateUnsent], + replyMessageState:TSOutgoingMessageStateFailed], ]]; @@ -2509,7 +2489,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:[DebugUIMessagesAssetLoader tallPortraitPngInstance] - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], // Text -> Png + Text [self fakeQuotedReplyAction:thread @@ -2521,7 +2501,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:nil - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], // Text -> Png [self fakeQuotedReplyAction:thread @@ -2533,7 +2513,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:nil replyAssetLoader:[DebugUIMessagesAssetLoader tallPortraitPngInstance] - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], // Png -> Png + Text [self fakeQuotedReplyAction:thread @@ -2545,7 +2525,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:[DebugUIMessagesAssetLoader tallPortraitPngInstance] - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], // Png -> Portrait Png + Text [self fakeQuotedReplyAction:thread @@ -2557,7 +2537,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:[DebugUIMessagesAssetLoader tallPortraitPngInstance] - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], // Png -> Landscape Png + Text [self fakeQuotedReplyAction:thread @@ -2569,7 +2549,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:[DebugUIMessagesAssetLoader wideLandscapePngInstance] - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], // Png -> Landscape Png + Text @@ -2582,7 +2562,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:[DebugUIMessagesAssetLoader wideLandscapePngInstance] - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], // Png -> Landscape Png + Text [self fakeQuotedReplyAction:thread @@ -2594,7 +2574,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:shortText replyAssetLoader:[DebugUIMessagesAssetLoader wideLandscapePngInstance] - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], // Png -> Landscape Png + Text [self fakeQuotedReplyAction:thread @@ -2606,7 +2586,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:mediumText replyAssetLoader:[DebugUIMessagesAssetLoader wideLandscapePngInstance] - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], // Png -> Landscape Png + Text [self fakeQuotedReplyAction:thread @@ -2618,7 +2598,7 @@ NS_ASSUME_NONNULL_BEGIN isReplyIncoming:NO replyMessageBody:mediumText replyAssetLoader:[DebugUIMessagesAssetLoader wideLandscapePngInstance] - replyMessageState:TSOutgoingMessageStateSentToService], + replyMessageState:TSOutgoingMessageStateSent], ]]; return actions; @@ -2710,60 +2690,59 @@ NS_ASSUME_NONNULL_BEGIN if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Short Message Sequences ⚠️"]]; } [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"Incoming"]]; - [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService - text:@"Outgoing"]]; + [actions + addObject:[self fakeOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSent text:@"Outgoing"]]; [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"Incoming 1"]]; [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"Incoming 2"]]; [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"Incoming 3"]]; [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateUnsent + messageState:TSOutgoingMessageStateFailed text:@"Outgoing Unsent 1"]]; [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateUnsent + messageState:TSOutgoingMessageStateFailed text:@"Outgoing Unsent 2"]]; [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateAttemptingOut + messageState:TSOutgoingMessageStateSending text:@"Outgoing Sending 1"]]; [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateAttemptingOut + messageState:TSOutgoingMessageStateSending text:@"Outgoing Sending 2"]]; [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"Outgoing Sent 1"]]; [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"Outgoing Sent 2"]]; [actions addObject:[self fakeShortOutgoingTextMessageAction:thread text:@"Outgoing Delivered 1" - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent isDelivered:YES isRead:NO]]; [actions addObject:[self fakeShortOutgoingTextMessageAction:thread text:@"Outgoing Delivered 2" - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent isDelivered:YES isRead:NO]]; [actions addObject:[self fakeShortOutgoingTextMessageAction:thread text:@"Outgoing Read 1" - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent isDelivered:YES isRead:YES]]; [actions addObject:[self fakeShortOutgoingTextMessageAction:thread text:@"Outgoing Read 2" - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent isDelivered:YES isRead:YES]]; [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"Incoming"]]; if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Long Message Sequences ⚠️"]]; } @@ -2772,7 +2751,7 @@ NS_ASSUME_NONNULL_BEGIN [actions addObject:[self fakeIncomingTextMessageAction:thread text:[@"Incoming" stringByAppendingString:longText]]]; [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:[@"Outgoing" stringByAppendingString:longText]]]; [actions addObject:[self fakeIncomingTextMessageAction:thread text:[@"Incoming 1" stringByAppendingString:longText]]]; @@ -2781,43 +2760,43 @@ NS_ASSUME_NONNULL_BEGIN [actions addObject:[self fakeIncomingTextMessageAction:thread text:[@"Incoming 3" stringByAppendingString:longText]]]; [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateUnsent + messageState:TSOutgoingMessageStateFailed text:[@"Outgoing Unsent 1" stringByAppendingString:longText]]]; [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateUnsent + messageState:TSOutgoingMessageStateFailed text:[@"Outgoing Unsent 2" stringByAppendingString:longText]]]; [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateAttemptingOut + messageState:TSOutgoingMessageStateSending text:[@"Outgoing Sending 1" stringByAppendingString:longText]]]; [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateAttemptingOut + messageState:TSOutgoingMessageStateSending text:[@"Outgoing Sending 2" stringByAppendingString:longText]]]; [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:[@"Outgoing Sent 1" stringByAppendingString:longText]]]; [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:[@"Outgoing Sent 2" stringByAppendingString:longText]]]; [actions addObject:[self fakeShortOutgoingTextMessageAction:thread text:[@"Outgoing Delivered 1" stringByAppendingString:longText] - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent isDelivered:YES isRead:NO]]; [actions addObject:[self fakeShortOutgoingTextMessageAction:thread text:[@"Outgoing Delivered 2" stringByAppendingString:longText] - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent isDelivered:YES isRead:NO]]; [actions addObject:[self fakeShortOutgoingTextMessageAction:thread text:[@"Outgoing Read 1" stringByAppendingString:longText] - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent isDelivered:YES isRead:YES]]; [actions addObject:[self fakeShortOutgoingTextMessageAction:thread text:[@"Outgoing Read 2" stringByAppendingString:longText] - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent isDelivered:YES isRead:YES]]; [actions addObject:[self fakeIncomingTextMessageAction:thread text:[@"Incoming" stringByAppendingString:longText]]]; @@ -2849,7 +2828,7 @@ NS_ASSUME_NONNULL_BEGIN TSOutgoingMessage *message = [self createFakeOutgoingMessage:thread messageBody:messageBody fakeAssetLoader:nil - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent isDelivered:NO isRead:NO quotedMessage:nil @@ -2867,7 +2846,7 @@ NS_ASSUME_NONNULL_BEGIN if (includeLabels) { [actions addObject:[self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService + messageState:TSOutgoingMessageStateSent text:@"⚠️ Back-Dated ⚠️"]]; } @@ -3285,7 +3264,7 @@ NS_ASSUME_NONNULL_BEGIN [self createFakeOutgoingMessage:thread messageBody:randomText fakeAssetLoader:nil - messageState:TSOutgoingMessageStateUnsent + messageState:TSOutgoingMessageStateFailed isDelivered:NO isRead:NO quotedMessage:nil @@ -3336,7 +3315,7 @@ NS_ASSUME_NONNULL_BEGIN messageBody:nil attachmentId:attachmentStream.uniqueId filename:filename - messageState:TSOutgoingMessageStateUnsent + messageState:TSOutgoingMessageStateFailed isDelivered:NO isRead:NO isVoiceMessage:NO @@ -3707,7 +3686,7 @@ NS_ASSUME_NONNULL_BEGIN } for (TSOutgoingMessage *message in messages) { - [message updateWithMessageState:TSOutgoingMessageStateAttemptingOut transaction:initialTransaction]; + [message updateWithFakeMessageState:TSOutgoingMessageStateSending transaction:initialTransaction]; [message saveWithTransaction:initialTransaction]; } @@ -3781,11 +3760,9 @@ NS_ASSUME_NONNULL_BEGIN groupMetaMessage:TSGroupMessageUnspecified quotedMessage:nil]; [message saveWithTransaction:transaction]; - [message updateWithMessageState:TSOutgoingMessageStateSentToService transaction:transaction]; + [message updateWithFakeMessageState:TSOutgoingMessageStateSent transaction:transaction]; [message updateWithSentRecipient:recipientId transaction:transaction]; - [message updateWithDeliveredToRecipientId:recipientId - deliveryTimestamp:timestamp - transaction:transaction]; + [message updateWithDeliveredRecipient:recipientId deliveryTimestamp:timestamp transaction:transaction]; [message updateWithReadRecipientId:recipientId readTimestamp:timestamp.unsignedLongLongValue transaction:transaction]; @@ -3935,11 +3912,11 @@ NS_ASSUME_NONNULL_BEGIN if (hasCaption) { [label appendString:@" 🔤"]; } - if (outgoingMessageState == TSOutgoingMessageStateUnsent) { + if (outgoingMessageState == TSOutgoingMessageStateFailed) { [label appendString:@" (Unsent)"]; - } else if (outgoingMessageState == TSOutgoingMessageStateAttemptingOut) { + } else if (outgoingMessageState == TSOutgoingMessageStateSending) { [label appendString:@" (Sending)"]; - } else if (outgoingMessageState == TSOutgoingMessageStateSentToService) { + } else if (outgoingMessageState == TSOutgoingMessageStateSent) { if (isRead) { [label appendString:@" (Read)"]; } else if (isDelivered) { @@ -4024,13 +4001,13 @@ NS_ASSUME_NONNULL_BEGIN } [message saveWithTransaction:transaction]; - [message updateWithMessageState:messageState transaction:transaction]; + [message updateWithFakeMessageState:messageState transaction:transaction]; if (isDelivered) { NSString *_Nullable recipientId = thread.recipientIdentifiers.lastObject; OWSAssert(recipientId.length > 0); - [message updateWithDeliveredToRecipientId:recipientId - deliveryTimestamp:@([NSDate ows_millisecondTimeStamp]) - transaction:transaction]; + [message updateWithDeliveredRecipient:recipientId + deliveryTimestamp:@([NSDate ows_millisecondTimeStamp]) + transaction:transaction]; } if (isRead) { NSString *_Nullable recipientId = thread.recipientIdentifiers.lastObject; diff --git a/Signal/src/ViewControllers/MessageDetailViewController.swift b/Signal/src/ViewControllers/MessageDetailViewController.swift index a78178b2ac..3c77d34333 100644 --- a/Signal/src/ViewControllers/MessageDetailViewController.swift +++ b/Signal/src/ViewControllers/MessageDetailViewController.swift @@ -193,13 +193,14 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele let isGroupThread = thread.isGroupThread() - let recipientStatusGroups: [MessageRecipientStatus] = [ + let recipientStatusGroups: [MessageReceiptStatus] = [ .read, .uploading, .delivered, .sent, .sending, - .failed + .failed, + .skipped ] for recipientStatusGroup in recipientStatusGroups { var groupRows = [UIView]() @@ -212,8 +213,15 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele groupRows.append(divider) } - for recipientId in thread.recipientIdentifiers { - let (recipientStatus, shortStatusMessage, _) = MessageRecipientStatusUtils.recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, recipientId: recipientId, referenceView: self.view) + let messageRecipientIds = outgoingMessage.recipientIds() + + for recipientId in messageRecipientIds { + guard let recipientState = outgoingMessage.recipientState(forRecipientId: recipientId) else { + owsFail("\(self.logTag) no message status for recipient: \(recipientId).") + continue + } + + let (recipientStatus, shortStatusMessage, _) = MessageRecipientStatusUtils.recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, recipientState: recipientState, referenceView: self.view) guard recipientStatus == recipientStatusGroup else { continue @@ -541,8 +549,8 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele updateContent() } - private func string(for messageRecipientStatus: MessageRecipientStatus) -> String { - switch messageRecipientStatus { + private func string(for messageReceiptStatus: MessageReceiptStatus) -> String { + switch messageReceiptStatus { case .uploading: return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_UPLOADING", comment: "Status label for messages which are uploading.") @@ -560,7 +568,10 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele comment: "Status label for messages which are read.") case .failed: return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_FAILED", - comment: "Status label for messages which are failed.") + comment: "Status label for messages which are failed.") + case .skipped: + return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_SKIPPED", + comment: "Status label for messages which were skipped.") } } diff --git a/Signal/src/ViewControllers/Utils/MessageRecipientStatusUtils.swift b/Signal/src/ViewControllers/Utils/MessageRecipientStatusUtils.swift index b955b46781..9c25b0b250 100644 --- a/Signal/src/ViewControllers/Utils/MessageRecipientStatusUtils.swift +++ b/Signal/src/ViewControllers/Utils/MessageRecipientStatusUtils.swift @@ -6,222 +6,154 @@ import Foundation import SignalServiceKit import SignalMessaging -@objc enum MessageRecipientStatus: Int { +@objc enum MessageReceiptStatus: Int { case uploading case sending case sent case delivered case read case failed + case skipped } -// Our per-recipient status messages are "biased towards success" -// and reflect the most successful known state for that recipient. -// -// Our per-message status messages are "biased towards failure" -// and reflect the least successful known state for that message. -// -// Why? -// -// When showing the per-recipient status, we want to show the message -// as "read" even if delivery failed to another recipient of the same -// message. -// -// When showing the per-message status, we want to show the message -// as "failed" if delivery failed to any recipient, even if another -// receipient has read the message. -// -// Note also that for legacy reasons we have redundant and possibly -// conflicting state. Examples: -// -// * We could have an entry in the recipientReadMap for a message -// that has no entries in its recipientDeliveryMap. -// * We could have an entry in the recipientReadMap or recipientDeliveryMap -// for a message whose status is "attempting out" or "unsent". -// * We could have a message whose wasDelivered property is false but -// which has entries in its recipientDeliveryMap or recipientReadMap. -// * Etc. -// -// To resolve this ambiguity, we apply a "bias" towards success or -// failure. -class MessageRecipientStatusUtils: NSObject { +@objc +public class MessageRecipientStatusUtils: NSObject { // MARK: Initializers @available(*, unavailable, message:"do not instantiate this class.") private override init() { } - // This method is per-recipient and "biased towards success". - // See comments above. - public class func recipientStatus(outgoingMessage: TSOutgoingMessage, - recipientId: String, - referenceView: UIView) -> MessageRecipientStatus { - let (messageRecipientStatus, _, _) = recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, - recipientId: recipientId, + // This method is per-recipient. + class func recipientStatus(outgoingMessage: TSOutgoingMessage, + recipientState: TSOutgoingMessageRecipientState, + referenceView: UIView) -> MessageReceiptStatus { + let (messageReceiptStatus, _, _) = recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, + recipientState: recipientState, referenceView: referenceView) - return messageRecipientStatus + return messageReceiptStatus } - // This method is per-recipient and "biased towards success". - // See comments above. + // This method is per-recipient. + @objc public class func shortStatusMessage(outgoingMessage: TSOutgoingMessage, - recipientId: String, + recipientState: TSOutgoingMessageRecipientState, referenceView: UIView) -> String { let (_, shortStatusMessage, _) = recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, - recipientId: recipientId, + recipientState: recipientState, referenceView: referenceView) return shortStatusMessage } - // This method is per-recipient and "biased towards success". - // See comments above. + // This method is per-recipient. + @objc public class func longStatusMessage(outgoingMessage: TSOutgoingMessage, - recipientId: String, + recipientState: TSOutgoingMessageRecipientState, referenceView: UIView) -> String { let (_, _, longStatusMessage) = recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, - recipientId: recipientId, + recipientState: recipientState, referenceView: referenceView) return longStatusMessage } - // This method is per-recipient and "biased towards success". - // See comments above. - public class func recipientStatusAndStatusMessage(outgoingMessage: TSOutgoingMessage, - recipientId: String, - referenceView: UIView) -> (status: MessageRecipientStatus, shortStatusMessage: String, longStatusMessage: String) { - // Legacy messages don't have "recipient read" state or "per-recipient delivery" state, - // so we fall back to `TSOutgoingMessageState` which is not per-recipient and therefore - // might be misleading. + // This method is per-recipient. + class func recipientStatusAndStatusMessage(outgoingMessage: TSOutgoingMessage, + recipientState: TSOutgoingMessageRecipientState, + referenceView: UIView) -> (status: MessageReceiptStatus, shortStatusMessage: String, longStatusMessage: String) { - let recipientReadMap = outgoingMessage.recipientReadMap - if let readTimestamp = recipientReadMap[recipientId] { - assert(outgoingMessage.messageState == .sentToService) - let timestampString = DateUtil.formatPastTimestampRelativeToNow(readTimestamp.uint64Value, - isRTL:referenceView.isRTL()) - let shortStatusMessage = timestampString - let longStatusMessage = NSLocalizedString("MESSAGE_STATUS_READ", comment:"message footer for read messages").rtlSafeAppend(" ", referenceView:referenceView) - .rtlSafeAppend(timestampString, referenceView:referenceView) - return (status:.read, shortStatusMessage:shortStatusMessage, longStatusMessage:longStatusMessage) - } - - let recipientDeliveryMap = outgoingMessage.recipientDeliveryMap - if let deliveryTimestamp = recipientDeliveryMap[recipientId] { - assert(outgoingMessage.messageState == .sentToService) - let timestampString = DateUtil.formatPastTimestampRelativeToNow(deliveryTimestamp.uint64Value, - isRTL:referenceView.isRTL()) - let shortStatusMessage = timestampString - let longStatusMessage = NSLocalizedString("MESSAGE_STATUS_DELIVERED", - comment:"message status for message delivered to their recipient.").rtlSafeAppend(" ", referenceView:referenceView) - .rtlSafeAppend(timestampString, referenceView:referenceView) - return (status:.delivered, shortStatusMessage:shortStatusMessage, longStatusMessage:longStatusMessage) - } - - if outgoingMessage.wasDelivered { - let statusMessage = NSLocalizedString("MESSAGE_STATUS_DELIVERED", - comment:"message status for message delivered to their recipient.") - return (status:.delivered, shortStatusMessage:statusMessage, longStatusMessage:statusMessage) - } - - if outgoingMessage.messageState == .unsent { - let shortStatusMessage = NSLocalizedString("MESSAGE_STATUS_FAILED_SHORT", comment:"status message for failed messages") - let longStatusMessage = NSLocalizedString("MESSAGE_STATUS_FAILED", comment:"message footer for failed messages") + switch recipientState.state { + case .failed: + let shortStatusMessage = NSLocalizedString("MESSAGE_STATUS_FAILED_SHORT", comment: "status message for failed messages") + let longStatusMessage = NSLocalizedString("MESSAGE_STATUS_FAILED", comment: "message footer for failed messages") return (status:.failed, shortStatusMessage:shortStatusMessage, longStatusMessage:longStatusMessage) - } else if outgoingMessage.messageState == .sentToService || - outgoingMessage.wasSent(toRecipient:recipientId) { + case .sending: + if outgoingMessage.hasAttachments() { + assert(outgoingMessage.messageState == .sending) + + let statusMessage = NSLocalizedString("MESSAGE_STATUS_UPLOADING", + comment: "message footer while attachment is uploading") + return (status:.uploading, shortStatusMessage:statusMessage, longStatusMessage:statusMessage) + } else { + assert(outgoingMessage.messageState == .sending) + + let statusMessage = NSLocalizedString("MESSAGE_STATUS_SENDING", + comment: "message status while message is sending.") + return (status:.sending, shortStatusMessage:statusMessage, longStatusMessage:statusMessage) + } + case .sent: + if let readTimestamp = recipientState.readTimestamp { + let timestampString = DateUtil.formatPastTimestampRelativeToNow(readTimestamp.uint64Value, + isRTL: referenceView.isRTL()) + let shortStatusMessage = timestampString + let longStatusMessage = NSLocalizedString("MESSAGE_STATUS_READ", comment: "message footer for read messages").rtlSafeAppend(" ", referenceView: referenceView) + .rtlSafeAppend(timestampString, referenceView: referenceView) + return (status:.read, shortStatusMessage:shortStatusMessage, longStatusMessage:longStatusMessage) + } + if let deliveryTimestamp = recipientState.deliveryTimestamp { + let timestampString = DateUtil.formatPastTimestampRelativeToNow(deliveryTimestamp.uint64Value, + isRTL: referenceView.isRTL()) + let shortStatusMessage = timestampString + let longStatusMessage = NSLocalizedString("MESSAGE_STATUS_DELIVERED", + comment: "message status for message delivered to their recipient.").rtlSafeAppend(" ", referenceView: referenceView) + .rtlSafeAppend(timestampString, referenceView: referenceView) + return (status:.delivered, shortStatusMessage:shortStatusMessage, longStatusMessage:longStatusMessage) + } let statusMessage = NSLocalizedString("MESSAGE_STATUS_SENT", - comment:"message footer for sent messages") + comment: "message footer for sent messages") return (status:.sent, shortStatusMessage:statusMessage, longStatusMessage:statusMessage) - } else if outgoingMessage.hasAttachments() { - assert(outgoingMessage.messageState == .attemptingOut) - - let statusMessage = NSLocalizedString("MESSAGE_STATUS_UPLOADING", - comment:"message footer while attachment is uploading") - return (status:.uploading, shortStatusMessage:statusMessage, longStatusMessage:statusMessage) - } else { - assert(outgoingMessage.messageState == .attemptingOut) - - let statusMessage = NSLocalizedString("MESSAGE_STATUS_SENDING", - comment:"message status while message is sending.") - return (status:.sending, shortStatusMessage:statusMessage, longStatusMessage:statusMessage) + case .skipped: + let statusMessage = NSLocalizedString("MESSAGE_STATUS_RECIPIENT_SKIPPED", + comment: "message status if message delivery to a recipient is skipped. We skip delivering group messages to users who have left the group or deactivated their Signal account.") + return (status:.skipped, shortStatusMessage:statusMessage, longStatusMessage:statusMessage) } } - // This method is per-message and "biased towards failure". - // See comments above. - public class func statusMessage(outgoingMessage: TSOutgoingMessage, - referenceView: UIView) -> String { + // This method is per-message. + internal class func receiptStatusAndMessage(outgoingMessage: TSOutgoingMessage, + referenceView: UIView) -> (status: MessageReceiptStatus, message: String) { switch outgoingMessage.messageState { - case .unsent: + case .failed: // Use the "long" version of this message here. - return NSLocalizedString("MESSAGE_STATUS_FAILED", comment:"message footer for failed messages") - case .attemptingOut: + return (.failed, NSLocalizedString("MESSAGE_STATUS_FAILED", comment: "message footer for failed messages")) + case .sending: if outgoingMessage.hasAttachments() { - return NSLocalizedString("MESSAGE_STATUS_UPLOADING", - comment:"message footer while attachment is uploading") + return (.uploading, NSLocalizedString("MESSAGE_STATUS_UPLOADING", + comment: "message footer while attachment is uploading")) } else { - return NSLocalizedString("MESSAGE_STATUS_SENDING", - comment:"message status while message is sending.") + return (.sending, NSLocalizedString("MESSAGE_STATUS_SENDING", + comment: "message status while message is sending.")) } - case .sentToService: - let recipientReadMap = outgoingMessage.recipientReadMap - if recipientReadMap.count > 0 { - return NSLocalizedString("MESSAGE_STATUS_READ", comment:"message footer for read messages") + case .sent: + if outgoingMessage.readRecipientIds().count > 0 { + return (.read, NSLocalizedString("MESSAGE_STATUS_READ", comment: "message footer for read messages")) } - - let recipientDeliveryMap = outgoingMessage.recipientDeliveryMap - if recipientDeliveryMap.count > 0 { - return NSLocalizedString("MESSAGE_STATUS_DELIVERED", - comment:"message status for message delivered to their recipient.") + if outgoingMessage.deliveredRecipientIds().count > 0 { + return (.delivered, NSLocalizedString("MESSAGE_STATUS_DELIVERED", + comment: "message status for message delivered to their recipient.")) } - - if outgoingMessage.wasDelivered { - return NSLocalizedString("MESSAGE_STATUS_DELIVERED", - comment:"message status for message delivered to their recipient.") - } - - return NSLocalizedString("MESSAGE_STATUS_SENT", - comment:"message footer for sent messages") + return (.sent, NSLocalizedString("MESSAGE_STATUS_SENT", + comment: "message footer for sent messages")) default: - owsFail("Message has unexpected status: \(outgoingMessage.messageState).") - return NSLocalizedString("MESSAGE_STATUS_SENT", - comment:"message footer for sent messages") + owsFail("\(self.logTag) Message has unexpected status: \(outgoingMessage.messageState).") + return (.sent, NSLocalizedString("MESSAGE_STATUS_SENT", + comment: "message footer for sent messages")) } } - // This method is per-message and "biased towards failure". - // See comments above. - public class func recipientStatus(outgoingMessage: TSOutgoingMessage) -> MessageRecipientStatus { - switch outgoingMessage.messageState { - case .unsent: - return .failed - case .attemptingOut: - if outgoingMessage.hasAttachments() { - return .uploading - } else { - return .sending - } - case .sentToService: - let recipientReadMap = outgoingMessage.recipientReadMap - if recipientReadMap.count > 0 { - return .read - } + // This method is per-message. + public class func receiptMessage(outgoingMessage: TSOutgoingMessage, + referenceView: UIView) -> String { + let (_, message ) = receiptStatusAndMessage(outgoingMessage: outgoingMessage, + referenceView: referenceView) + return message + } - let recipientDeliveryMap = outgoingMessage.recipientDeliveryMap - if recipientDeliveryMap.count > 0 { - return .delivered - } - - if outgoingMessage.wasDelivered { - return .delivered - } - - return .sent - default: - owsFail("Message has unexpected status: \(outgoingMessage.messageState).") - - return .sent - } + // This method is per-message. + class func recipientStatus(outgoingMessage: TSOutgoingMessage, referenceView: UIView) -> MessageReceiptStatus { + let (status, _ ) = receiptStatusAndMessage(outgoingMessage: outgoingMessage, + referenceView: referenceView) + return status } } diff --git a/SignalMessaging/environment/migrations/OWS109OutgoingMessageState.h b/SignalMessaging/environment/migrations/OWS109OutgoingMessageState.h new file mode 100644 index 0000000000..6b6b23c5ec --- /dev/null +++ b/SignalMessaging/environment/migrations/OWS109OutgoingMessageState.h @@ -0,0 +1,13 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSResaveCollectionDBMigration.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OWS109OutgoingMessageState : OWSResaveCollectionDBMigration + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/environment/migrations/OWS109OutgoingMessageState.m b/SignalMessaging/environment/migrations/OWS109OutgoingMessageState.m new file mode 100644 index 0000000000..3d2e47a676 --- /dev/null +++ b/SignalMessaging/environment/migrations/OWS109OutgoingMessageState.m @@ -0,0 +1,46 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWS109OutgoingMessageState.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +// Increment a similar constant for every future DBMigration +static NSString *const OWS109OutgoingMessageStateMigrationId = @"109"; + +@implementation OWS109OutgoingMessageState + ++ (NSString *)migrationId +{ + return OWS109OutgoingMessageStateMigrationId; +} + +// Override parent migration +- (void)runUpWithCompletion:(OWSDatabaseMigrationCompletion)completion +{ + OWSAssert(completion); + + + OWSDatabaseConnection *dbConnection = (OWSDatabaseConnection *)self.primaryStorage.newDatabaseConnection; + + [self resaveDBCollection:TSOutgoingMessage.collection + filter:^(id entity) { + return [entity isKindOfClass:[TSOutgoingMessage class]]; + } + dbConnection:dbConnection + completion:^{ + DDLogInfo(@"Completed migration %@", self.uniqueId); + + [self save]; + + completion(); + }]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/environment/migrations/OWSDatabaseMigrationRunner.m b/SignalMessaging/environment/migrations/OWSDatabaseMigrationRunner.m index d5537cb1c8..7fff0cfefd 100644 --- a/SignalMessaging/environment/migrations/OWSDatabaseMigrationRunner.m +++ b/SignalMessaging/environment/migrations/OWSDatabaseMigrationRunner.m @@ -10,6 +10,7 @@ #import "OWS105AttachmentFilePaths.h" #import "OWS107LegacySounds.h" #import "OWS108CallLoggingPreference.h" +#import "OWS109OutgoingMessageState.h" #import "OWSDatabaseMigration.h" #import #import @@ -42,7 +43,8 @@ NS_ASSUME_NONNULL_BEGIN [[OWS105AttachmentFilePaths alloc] initWithPrimaryStorage:primaryStorage], [[OWS106EnsureProfileComplete alloc] initWithPrimaryStorage:primaryStorage], [[OWS107LegacySounds alloc] initWithPrimaryStorage:primaryStorage], - [[OWS108CallLoggingPreference alloc] initWithPrimaryStorage:primaryStorage] + [[OWS108CallLoggingPreference alloc] initWithPrimaryStorage:primaryStorage], + [[OWS109OutgoingMessageState alloc] initWithPrimaryStorage:primaryStorage] ]; } diff --git a/SignalMessaging/environment/migrations/OWSResaveCollectionDBMigration.h b/SignalMessaging/environment/migrations/OWSResaveCollectionDBMigration.h new file mode 100644 index 0000000000..8f793dfb35 --- /dev/null +++ b/SignalMessaging/environment/migrations/OWSResaveCollectionDBMigration.h @@ -0,0 +1,24 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSDatabaseMigration.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef BOOL (^DBRecordFilterBlock)(id record); + +@class YapDatabaseConnection; + +// Base class for migrations that resave all or a subset of +// records in a database collection. +@interface OWSResaveCollectionDBMigration : OWSDatabaseMigration + +- (void)resaveDBCollection:(NSString *)collection + filter:(nullable DBRecordFilterBlock)filter + dbConnection:(YapDatabaseConnection *)dbConnection + completion:(OWSDatabaseMigrationCompletion)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/environment/migrations/OWSResaveCollectionDBMigration.m b/SignalMessaging/environment/migrations/OWSResaveCollectionDBMigration.m new file mode 100644 index 0000000000..f38e678128 --- /dev/null +++ b/SignalMessaging/environment/migrations/OWSResaveCollectionDBMigration.m @@ -0,0 +1,80 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSResaveCollectionDBMigration.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSResaveCollectionDBMigration + +- (void)resaveDBCollection:(NSString *)collection + filter:(nullable DBRecordFilterBlock)filter + dbConnection:(YapDatabaseConnection *)dbConnection + completion:(OWSDatabaseMigrationCompletion)completion +{ + OWSAssert(collection.length > 0); + OWSAssert(dbConnection); + OWSAssert(completion); + + NSMutableArray *recordIds = [NSMutableArray new]; + [dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [recordIds addObjectsFromArray:[transaction allKeysInCollection:collection]]; + DDLogInfo(@"%@ Migrating %zd records from: %@.", self.logTag, recordIds.count, collection); + } + completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) + completionBlock:^{ + [self resaveBatch:recordIds + collection:collection + filter:filter + dbConnection:dbConnection + completion:completion]; + }]; +} + +- (void)resaveBatch:(NSMutableArray *)recordIds + collection:(NSString *)collection + filter:(nullable DBRecordFilterBlock)filter + dbConnection:(YapDatabaseConnection *)dbConnection + completion:(OWSDatabaseMigrationCompletion)completion +{ + OWSAssert(recordIds); + OWSAssert(collection.length > 0); + OWSAssert(dbConnection); + OWSAssert(completion); + + DDLogVerbose(@"%@ %s: %zd", self.logTag, __PRETTY_FUNCTION__, recordIds.count); + + if (recordIds.count < 1) { + completion(); + return; + } + + [dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + const int kBatchSize = 1000; + for (int i = 0; i < kBatchSize && recordIds.count > 0; i++) { + NSString *messageId = [recordIds lastObject]; + [recordIds removeLastObject]; + id record = [transaction objectForKey:messageId inCollection:collection]; + if (filter && !filter(record)) { + continue; + } + TSYapDatabaseObject *entity = (TSYapDatabaseObject *)record; + [entity saveWithTransaction:transaction]; + } + } + completionBlock:^{ + // Process the next batch. + [self resaveBatch:recordIds + collection:collection + filter:filter + dbConnection:dbConnection + completion:completion]; + }]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift b/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift index 83582cf4b7..5c2e50eada 100644 --- a/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift +++ b/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift @@ -45,9 +45,9 @@ public class OWSMessagesBubbleImageFactory: NSObject { return self.incoming } else if let outgoingMessage = message as? TSOutgoingMessage { switch outgoingMessage.messageState { - case .unsent: + case .failed: return outgoingFailed - case .attemptingOut: + case .sending: return currentlyOutgoing default: return outgoing @@ -75,9 +75,9 @@ public class OWSMessagesBubbleImageFactory: NSObject { return OWSMessagesBubbleImageFactory.bubbleColorIncoming } else if let outgoingMessage = message as? TSOutgoingMessage { switch outgoingMessage.messageState { - case .unsent: + case .failed: return OWSMessagesBubbleImageFactory.bubbleColorOutgoingUnsent - case .attemptingOut: + case .sending: return OWSMessagesBubbleImageFactory.bubbleColorOutgoingSending default: return OWSMessagesBubbleImageFactory.bubbleColorOutgoingSent diff --git a/SignalServiceKit/src/Messages/DeviceSyncing/OWSOutgoingSentMessageTranscript.m b/SignalServiceKit/src/Messages/DeviceSyncing/OWSOutgoingSentMessageTranscript.m index 0508fef5f9..4b0bf8b863 100644 --- a/SignalServiceKit/src/Messages/DeviceSyncing/OWSOutgoingSentMessageTranscript.m +++ b/SignalServiceKit/src/Messages/DeviceSyncing/OWSOutgoingSentMessageTranscript.m @@ -1,10 +1,12 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSOutgoingSentMessageTranscript.h" #import "OWSSignalServiceProtos.pb.h" +#import "TSAccountManager.h" #import "TSOutgoingMessage.h" +#import "TSThread.h" NS_ASSUME_NONNULL_BEGIN @@ -48,8 +50,12 @@ NS_ASSUME_NONNULL_BEGIN OWSSignalServiceProtosSyncMessageSentBuilder *sentBuilder = [OWSSignalServiceProtosSyncMessageSentBuilder new]; [sentBuilder setTimestamp:self.message.timestamp]; - [sentBuilder setDestination:self.message.recipientIdentifier]; - [sentBuilder setMessage:[self.message buildDataMessage:self.message.recipientIdentifier]]; + + NSString *_Nullable localNumber = [TSAccountManager localNumber]; + OWSAssert(localNumber.length > 0); + OWSAssert([localNumber isEqualToString:self.message.thread.contactIdentifier]); + [sentBuilder setDestination:localNumber]; + [sentBuilder setMessage:[self.message buildDataMessage:localNumber]]; [sentBuilder setExpirationStartTimestamp:self.message.timestamp]; [syncMessageBuilder setSentBuilder:sentBuilder]; diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h index dff04e3fdd..87aa0fccac 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h @@ -11,14 +11,30 @@ typedef NS_ENUM(NSInteger, TSOutgoingMessageState) { // a) Enqueued for sending. // b) Waiting on attachment upload(s). // c) Being sent to the service. - TSOutgoingMessageStateAttemptingOut, + TSOutgoingMessageStateSending, // The failure state. - TSOutgoingMessageStateUnsent, - // These two enum values have been combined into TSOutgoingMessageStateSentToService. + TSOutgoingMessageStateFailed, + // These two enum values have been combined into TSOutgoingMessageStateSent. TSOutgoingMessageStateSent_OBSOLETE, TSOutgoingMessageStateDelivered_OBSOLETE, // The message has been sent to the service. - TSOutgoingMessageStateSentToService, + TSOutgoingMessageStateSent, +}; + +// Used +typedef NS_ENUM(NSInteger, OWSOutgoingMessageRecipientState) { + // Message could not be sent to recipient. + OWSOutgoingMessageRecipientStateFailed = 0, + // Message is being sent to the recipient (enqueued, uploading or sending). + OWSOutgoingMessageRecipientStateSending, + // The message was not sent because the recipient is not valid. + // For example, this recipient may have left the group. + OWSOutgoingMessageRecipientStateSkipped, + // The message has been sent to the service. It may also have been delivered or read. + OWSOutgoingMessageRecipientStateSent, + + OWSOutgoingMessageRecipientStateMin = OWSOutgoingMessageRecipientStateFailed, + OWSOutgoingMessageRecipientStateMax = OWSOutgoingMessageRecipientStateSent, }; typedef NS_ENUM(NSInteger, TSGroupMetaMessage) { @@ -35,6 +51,18 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) { @class OWSSignalServiceProtosDataMessageBuilder; @class SignalRecipient; +@interface TSOutgoingMessageRecipientState : MTLModel + +@property (atomic, readonly) OWSOutgoingMessageRecipientState state; +// This property should only be set if state == .sent. +@property (atomic, nullable, readonly) NSNumber *deliveryTimestamp; +// This property should only be set if state == .sent. +@property (atomic, nullable, readonly) NSNumber *readTimestamp; + +@end + +#pragma mark - + @interface TSOutgoingMessage : TSMessage - (instancetype)initMessageWithTimestamp:(uint64_t)timestamp @@ -75,11 +103,7 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) { + (instancetype)outgoingMessageInThread:(nullable TSThread *)thread groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage; -@property (atomic, readonly) TSOutgoingMessageState messageState; - -// The message has been sent to the service and received by at least one recipient client. -// A recipient may have more than one client, and group message may have more than one recipient. -@property (atomic, readonly) BOOL wasDelivered; +@property (readonly) TSOutgoingMessageState messageState; @property (atomic, readonly) BOOL hasSyncedTranscript; @property (atomic, readonly) NSString *customMessage; @@ -89,27 +113,13 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) { @property (atomic, readonly) TSGroupMetaMessage groupMetaMessage; -// If set, this group message should only be sent to a single recipient. -@property (atomic, readonly) NSString *singleGroupRecipient; - @property (nonatomic, readonly) BOOL isVoiceMessage; // This property won't be accurate for legacy messages. @property (atomic, readonly) BOOL isFromLinkedDevice; -// Map of "recipient id"-to-"delivery time" of the recipients who have received the message. -@property (atomic, readonly) NSDictionary *recipientDeliveryMap; - -// Map of "recipient id"-to-"read time" of the recipients who have read the message. -@property (atomic, readonly) NSDictionary *recipientReadMap; - @property (nonatomic, readonly) BOOL isSilent; -/** - * Signal Identifier (e.g. e164 number) or nil if in a group thread. - */ -- (nullable NSString *)recipientIdentifier; - /** * The data representation of this message, to be encrypted, before being sent. */ @@ -143,37 +153,76 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) { - (BOOL)shouldBeSaved; +// All recipients of this message. +- (NSArray *)recipientIds; + +// All recipients of this message who we are currently trying to send to (queued, uploading or during send). +- (NSArray *)sendingRecipientIds; + +// All recipients of this message to whom it has been sent and delivered. +- (NSArray *)deliveredRecipientIds; + +// All recipients of this message to whom it has been sent, delivered and read. +- (NSArray *)readRecipientIds; + +// Number of recipients of this message to whom it has been sent. +- (NSUInteger)sentRecipientsCount; + +- (nullable TSOutgoingMessageRecipientState *)recipientStateForRecipientId:(NSString *)recipientId; + #pragma mark - Update With... Methods -- (void)updateWithMessageState:(TSOutgoingMessageState)messageState; -- (void)updateWithMessageState:(TSOutgoingMessageState)messageState - transaction:(YapDatabaseReadWriteTransaction *)transaction; +// This method is used to record a successful send to one recipient. +- (void)updateWithSentRecipient:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction; + +// This method is used to record a skipped send to one recipient. +- (void)updateWithSkippedRecipient:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction; + +// On app launch, all "sending" recipients should be marked as "failed". +- (void)updateWithAllSendingRecipientsMarkedAsFailedWithTansaction:(YapDatabaseReadWriteTransaction *)transaction; + +// When we start a message send, all "failed" recipients should be marked as "sending". +- (void)updateWithMarkingAllUnsentRecipientsAsSendingWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; + +// This method is used to forge the message state for fake messages. +// +// NOTE: This method should only be used by Debug UI, etc. +- (void)updateWithFakeMessageState:(TSOutgoingMessageState)messageState + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +// This method is used to record a failed send to all "sending" recipients. - (void)updateWithSendingError:(NSError *)error; + - (void)updateWithHasSyncedTranscript:(BOOL)hasSyncedTranscript transaction:(YapDatabaseReadWriteTransaction *)transaction; - (void)updateWithCustomMessage:(NSString *)customMessage transaction:(YapDatabaseReadWriteTransaction *)transaction; - (void)updateWithCustomMessage:(NSString *)customMessage; + +// This method is used to record a successful delivery to one recipient. +// // deliveryTimestamp is an optional parameter, since legacy // delivery receipts don't have a "delivery timestamp". Those // messages repurpose the "timestamp" field to indicate when the // corresponding message was originally sent. -- (void)updateWithDeliveredToRecipientId:(NSString *)recipientId - deliveryTimestamp:(NSNumber *_Nullable)deliveryTimestamp - transaction:(YapDatabaseReadWriteTransaction *)transaction; +- (void)updateWithDeliveredRecipient:(NSString *)recipientId + deliveryTimestamp:(NSNumber *_Nullable)deliveryTimestamp + transaction:(YapDatabaseReadWriteTransaction *)transaction; + - (void)updateWithWasSentFromLinkedDeviceWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; -- (void)updateWithSingleGroupRecipient:(NSString *)singleGroupRecipient - transaction:(YapDatabaseReadWriteTransaction *)transaction; + +// This method is used to rewrite the recipient list with a single recipient. +// It is used to reply to a "group info request", which should only be +// delivered to the requestor. +- (void)updateWithSendingToSingleGroupRecipient:(NSString *)singleGroupRecipient + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +// This method is used to record a successful "read" by one recipient. - (void)updateWithReadRecipientId:(NSString *)recipientId readTimestamp:(uint64_t)readTimestamp transaction:(YapDatabaseReadWriteTransaction *)transaction; + - (nullable NSNumber *)firstRecipientReadTimestamp; -#pragma mark - Sent Recipients - -- (NSUInteger)sentRecipientsCount; -- (BOOL)wasSentToRecipient:(NSString *)contactId; -- (void)updateWithSentRecipient:(NSString *)contactId transaction:(YapDatabaseReadWriteTransaction *)transaction; - @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m index 0e95c93af1..deb82e03e9 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m @@ -6,6 +6,7 @@ #import "NSDate+OWS.h" #import "OWSMessageSender.h" #import "OWSOutgoingSyncMessage.h" +#import "OWSPrimaryStorage.h" #import "OWSSignalServiceProtos.pb.h" #import "ProtoBuf+OWS.h" #import "SignalRecipient.h" @@ -21,28 +22,31 @@ NS_ASSUME_NONNULL_BEGIN NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRecipientAll"; +@interface TSOutgoingMessageRecipientState () + +@property (atomic) OWSOutgoingMessageRecipientState state; +@property (atomic, nullable) NSNumber *deliveryTimestamp; +@property (atomic, nullable) NSNumber *readTimestamp; + +@end + +#pragma mark - + +@implementation TSOutgoingMessageRecipientState + +@end + +#pragma mark - + @interface TSOutgoingMessage () -@property (atomic) TSOutgoingMessageState messageState; @property (atomic) BOOL hasSyncedTranscript; @property (atomic) NSString *customMessage; @property (atomic) NSString *mostRecentFailureText; -@property (atomic) BOOL wasDelivered; -@property (atomic) NSString *singleGroupRecipient; @property (atomic) BOOL isFromLinkedDevice; - -// For outgoing, non-legacy group messages sent from this client, this -// contains the list of recipients to whom the message has been sent. -// -// This collection can also be tested to avoid repeat delivery to the -// same recipient. -@property (atomic) NSArray *sentRecipients; - @property (atomic) TSGroupMetaMessage groupMetaMessage; -@property (atomic) NSDictionary *recipientDeliveryMap; - -@property (atomic) NSDictionary *recipientReadMap; +@property (atomic, nullable) NSDictionary *recipientStateMap; @end @@ -50,8 +54,6 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec @implementation TSOutgoingMessage -@synthesize sentRecipients = _sentRecipients; - - (instancetype)initWithCoder:(NSCoder *)coder { self = [super initWithCoder:coder]; @@ -60,22 +62,113 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec if (!_attachmentFilenameMap) { _attachmentFilenameMap = [NSMutableDictionary new]; } - - // Migrate message state. - if (_messageState == TSOutgoingMessageStateSent_OBSOLETE) { - _messageState = TSOutgoingMessageStateSentToService; - } else if (_messageState == TSOutgoingMessageStateDelivered_OBSOLETE) { - _messageState = TSOutgoingMessageStateSentToService; - _wasDelivered = YES; - } - if (!_sentRecipients) { - _sentRecipients = [NSArray new]; + + if (!self.recipientStateMap) { + [self migrateRecipientStateMapWithCoder:coder]; + OWSAssert(self.recipientStateMap); } } return self; } +- (void)migrateRecipientStateMapWithCoder:(NSCoder *)coder +{ + OWSAssert(!self.recipientStateMap); + OWSAssert(coder); + + // Determine the "overall message state." + TSOutgoingMessageState oldMessageState = TSOutgoingMessageStateFailed; + NSNumber *_Nullable messageStateValue = [coder decodeObjectForKey:@"messageState"]; + if (messageStateValue) { + oldMessageState = (TSOutgoingMessageState)messageStateValue.intValue; + } + + OWSOutgoingMessageRecipientState defaultState; + switch (oldMessageState) { + case TSOutgoingMessageStateFailed: + defaultState = OWSOutgoingMessageRecipientStateFailed; + break; + case TSOutgoingMessageStateSending: + defaultState = OWSOutgoingMessageRecipientStateSending; + break; + case TSOutgoingMessageStateSent: + case TSOutgoingMessageStateSent_OBSOLETE: + case TSOutgoingMessageStateDelivered_OBSOLETE: + // Convert legacy values. + defaultState = OWSOutgoingMessageRecipientStateSent; + break; + } + + // Try to leverage the "per-recipient state." + NSDictionary *_Nullable recipientDeliveryMap = + [coder decodeObjectForKey:@"recipientDeliveryMap"]; + NSDictionary *_Nullable recipientReadMap = [coder decodeObjectForKey:@"recipientReadMap"]; + NSArray *_Nullable sentRecipients = [coder decodeObjectForKey:@"sentRecipients"]; + + NSMutableDictionary *recipientStateMap = [NSMutableDictionary new]; + // Our default recipient list is the current thread members. + __block NSArray *recipientIds = @[]; + // To avoid deadlock while migrating these records, we use a dedicated + // migration connection. For legacy records (created more than ~9 months + // before the migration), we need to infer the recipient list for this + // message from the current thread membership. This inference isn't + // always accurate, so not using the same connection for both reads is + // acceptable. + [TSOutgoingMessage.dbMigrationConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + recipientIds = [[self threadWithTransaction:transaction] recipientIdentifiers]; + }]; + if (sentRecipients) { + // If we have a `sentRecipients` list, prefer that as it is more accurate. + recipientIds = sentRecipients; + } + NSString *_Nullable singleGroupRecipient = [coder decodeObjectForKey:@"singleGroupRecipient"]; + if (singleGroupRecipient) { + OWSFail(@"%@ unexpected single group recipient message.", self.logTag); + // If this is a "single group recipient message", treat it as such. + recipientIds = @[ + singleGroupRecipient, + ]; + } + + for (NSString *recipientId in recipientIds) { + TSOutgoingMessageRecipientState *recipientState = [TSOutgoingMessageRecipientState new]; + + NSNumber *_Nullable readTimestamp = recipientReadMap[recipientId]; + NSNumber *_Nullable deliveryTimestamp = recipientDeliveryMap[recipientId]; + if (readTimestamp) { + // If we have a read timestamp for this recipient, mark it as read. + recipientState.state = OWSOutgoingMessageRecipientStateSent; + recipientState.readTimestamp = readTimestamp; + // deliveryTimestamp might be nil here. + recipientState.deliveryTimestamp = deliveryTimestamp; + } else if (deliveryTimestamp) { + // If we have a delivery timestamp for this recipient, mark it as delivered. + recipientState.state = OWSOutgoingMessageRecipientStateSent; + recipientState.deliveryTimestamp = deliveryTimestamp; + } else if ([sentRecipients containsObject:recipientId]) { + // If this recipient is in `sentRecipients`, mark it as sent. + recipientState.state = OWSOutgoingMessageRecipientStateSent; + } else { + // Use the default state for this message. + recipientState.state = defaultState; + } + + recipientStateMap[recipientId] = recipientState; + } + self.recipientStateMap = [recipientStateMap copy]; +} + ++ (YapDatabaseConnection *)dbMigrationConnection +{ + static YapDatabaseConnection *connection = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + connection = [[OWSPrimaryStorage sharedManager] newDatabaseConnection]; + }); + return connection; +} + + (instancetype)outgoingMessageInThread:(nullable TSThread *)thread messageBody:(nullable NSString *)body attachmentId:(nullable NSString *)attachmentId @@ -156,8 +249,6 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec return self; } - _messageState = TSOutgoingMessageStateAttemptingOut; - _sentRecipients = [NSArray new]; _hasSyncedTranscript = NO; if ([thread isKindOfClass:TSGroupThread.class]) { @@ -177,9 +268,50 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec _attachmentFilenameMap = [NSMutableDictionary new]; + // New outgoing messages should immediately determine their + // recipient list from current thread state. + NSMutableDictionary *recipientStateMap = [NSMutableDictionary new]; + NSArray *recipientIds = [thread recipientIdentifiers]; + for (NSString *recipientId in recipientIds) { + TSOutgoingMessageRecipientState *recipientState = [TSOutgoingMessageRecipientState new]; + recipientState.state = OWSOutgoingMessageRecipientStateSending; + recipientStateMap[recipientId] = recipientState; + } + self.recipientStateMap = [recipientStateMap copy]; + return self; } +- (TSOutgoingMessageState)messageState +{ + return [TSOutgoingMessage messageStateForRecipientStates:self.recipientStateMap.allValues]; +} + ++ (TSOutgoingMessageState)messageStateForRecipientStates:(NSArray *)recipientStates +{ + OWSAssert(recipientStates); + + // If there are any "sending" recipients, consider this message "sending". + BOOL hasFailed = NO; + for (TSOutgoingMessageRecipientState *recipientState in recipientStates) { + if (recipientState.state == OWSOutgoingMessageRecipientStateSending) { + return TSOutgoingMessageStateSending; + } else if (recipientState.state == OWSOutgoingMessageRecipientStateFailed) { + hasFailed = YES; + } + } + + // If there are any "failed" recipients, consider this message "failed". + if (hasFailed) { + return TSOutgoingMessageStateFailed; + } + + // Otherwise, consider the message "sent". + // + // NOTE: This includes messages with no recipients. + return TSOutgoingMessageStateSent; +} + - (BOOL)shouldBeSaved { if (self.groupMetaMessage == TSGroupMessageDeliver || self.groupMetaMessage == TSGroupMessageUnspecified) { @@ -203,23 +335,29 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec [super saveWithTransaction:transaction]; } -- (nullable NSString *)recipientIdentifier +- (BOOL)hasSentToAnyRecipient { - return self.thread.contactIdentifier; + for (TSOutgoingMessageRecipientState *recipientState in self.recipientStateMap.allValues) { + if (recipientState.state == OWSOutgoingMessageRecipientStateSent) { + return YES; + } + } + return NO; } - (BOOL)shouldStartExpireTimer:(YapDatabaseReadTransaction *)transaction { - switch (self.messageState) { - case TSOutgoingMessageStateSentToService: - return self.isExpiringMessage; - case TSOutgoingMessageStateAttemptingOut: - case TSOutgoingMessageStateUnsent: - return NO; - case TSOutgoingMessageStateSent_OBSOLETE: - case TSOutgoingMessageStateDelivered_OBSOLETE: - OWSFail(@"%@ Obsolete message state.", self.logTag); - return self.isExpiringMessage; + // It's not clear if we should wait until _all_ recipients have reached "sent or later" + // (which could never occur if one group member is unregistered) or only wait until + // the first recipient has reached "sent or later" (which could cause partially delivered + // messages to expire). For now, we'll do the latter. + // + // TODO: Revisit this decision. + + if (!self.isExpiringMessage) { + return NO; + } else { + return self.hasSentToAnyRecipient; } } @@ -233,6 +371,67 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec return OWSInteractionType_OutgoingMessage; } +- (NSArray *)recipientIds +{ + return self.recipientStateMap.allKeys; +} + +- (NSArray *)sendingRecipientIds +{ + NSMutableArray *result = [NSMutableArray new]; + for (NSString *recipientId in self.recipientStateMap) { + TSOutgoingMessageRecipientState *recipientState = self.recipientStateMap[recipientId]; + if (recipientState.state == OWSOutgoingMessageRecipientStateSending) { + [result addObject:recipientId]; + } + } + return result; +} + +- (NSArray *)deliveredRecipientIds +{ + NSMutableArray *result = [NSMutableArray new]; + for (NSString *recipientId in self.recipientStateMap) { + TSOutgoingMessageRecipientState *recipientState = self.recipientStateMap[recipientId]; + if (recipientState.deliveryTimestamp != nil) { + [result addObject:recipientId]; + } + } + return result; +} + +- (NSArray *)readRecipientIds +{ + NSMutableArray *result = [NSMutableArray new]; + for (NSString *recipientId in self.recipientStateMap) { + TSOutgoingMessageRecipientState *recipientState = self.recipientStateMap[recipientId]; + if (recipientState.readTimestamp != nil) { + [result addObject:recipientId]; + } + } + return result; +} + +- (NSUInteger)sentRecipientsCount +{ + return [self.recipientStateMap.allValues + filteredArrayUsingPredicate:[NSPredicate + predicateWithBlock:^BOOL(TSOutgoingMessageRecipientState *recipientState, + NSDictionary *_Nullable bindings) { + return recipientState.state == OWSOutgoingMessageRecipientStateSent; + }]] + .count; +} + +- (nullable TSOutgoingMessageRecipientState *)recipientStateForRecipientId:(NSString *)recipientId +{ + OWSAssert(recipientId.length > 0); + + TSOutgoingMessageRecipientState *_Nullable result = self.recipientStateMap[recipientId]; + OWSAssert(result); + return [result copy]; +} + #pragma mark - Update With... Methods - (void)updateWithSendingError:(NSError *)error @@ -242,27 +441,47 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec [self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [self applyChangeToSelfAndLatestCopy:transaction changeBlock:^(TSOutgoingMessage *message) { - [message setMessageState:TSOutgoingMessageStateUnsent]; + // Mark any "sending" recipients as "failed." + for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap + .allValues) { + if (recipientState.state == OWSOutgoingMessageRecipientStateSending) { + recipientState.state = OWSOutgoingMessageRecipientStateFailed; + } + } [message setMostRecentFailureText:error.localizedDescription]; }]; }]; } -- (void)updateWithMessageState:(TSOutgoingMessageState)messageState -{ - [self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [self updateWithMessageState:messageState transaction:transaction]; - }]; -} - -- (void)updateWithMessageState:(TSOutgoingMessageState)messageState - transaction:(YapDatabaseReadWriteTransaction *)transaction +- (void)updateWithAllSendingRecipientsMarkedAsFailedWithTansaction:(YapDatabaseReadWriteTransaction *)transaction { OWSAssert(transaction); [self applyChangeToSelfAndLatestCopy:transaction changeBlock:^(TSOutgoingMessage *message) { - [message setMessageState:messageState]; + // Mark any "sending" recipients as "failed." + for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap + .allValues) { + if (recipientState.state == OWSOutgoingMessageRecipientStateSending) { + recipientState.state = OWSOutgoingMessageRecipientStateFailed; + } + } + }]; +} + +- (void)updateWithMarkingAllUnsentRecipientsAsSendingWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssert(transaction); + + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSOutgoingMessage *message) { + // Mark any "sending" recipients as "failed." + for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap + .allValues) { + if (recipientState.state == OWSOutgoingMessageRecipientStateFailed) { + recipientState.state = OWSOutgoingMessageRecipientStateSending; + } + } }]; } @@ -293,104 +512,62 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec }]; } -- (void)updateWithDeliveredToRecipientId:(NSString *)recipientId - deliveryTimestamp:(NSNumber *_Nullable)deliveryTimestamp - transaction:(YapDatabaseReadWriteTransaction *)transaction +- (void)updateWithSentRecipient:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction { OWSAssert(recipientId.length > 0); OWSAssert(transaction); [self applyChangeToSelfAndLatestCopy:transaction changeBlock:^(TSOutgoingMessage *message) { - - if (deliveryTimestamp) { - NSMutableDictionary *recipientDeliveryMap - = (message.recipientDeliveryMap ? [message.recipientDeliveryMap mutableCopy] - : [NSMutableDictionary new]); - recipientDeliveryMap[recipientId] = deliveryTimestamp; - message.recipientDeliveryMap = [recipientDeliveryMap copy]; + TSOutgoingMessageRecipientState *_Nullable recipientState + = message.recipientStateMap[recipientId]; + if (!recipientState) { + OWSFail(@"%@ Missing recipient state for recipient: %@", self.logTag, recipientId); + return; } - - [message setWasDelivered:YES]; + recipientState.state = OWSOutgoingMessageRecipientStateSent; }]; } -- (void)updateWithWasSentFromLinkedDeviceWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +- (void)updateWithSkippedRecipient:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction { + OWSAssert(recipientId.length > 0); OWSAssert(transaction); [self applyChangeToSelfAndLatestCopy:transaction changeBlock:^(TSOutgoingMessage *message) { - [message setMessageState:TSOutgoingMessageStateSentToService]; - [message setWasDelivered:YES]; - [message setIsFromLinkedDevice:YES]; + TSOutgoingMessageRecipientState *_Nullable recipientState + = message.recipientStateMap[recipientId]; + if (!recipientState) { + OWSFail(@"%@ Missing recipient state for recipient: %@", self.logTag, recipientId); + return; + } + recipientState.state = OWSOutgoingMessageRecipientStateSkipped; }]; } -- (void)updateWithSingleGroupRecipient:(NSString *)singleGroupRecipient - transaction:(YapDatabaseReadWriteTransaction *)transaction +- (void)updateWithDeliveredRecipient:(NSString *)recipientId + deliveryTimestamp:(NSNumber *_Nullable)deliveryTimestamp + transaction:(YapDatabaseReadWriteTransaction *)transaction { + OWSAssert(recipientId.length > 0); OWSAssert(transaction); - OWSAssert(singleGroupRecipient.length > 0); [self applyChangeToSelfAndLatestCopy:transaction changeBlock:^(TSOutgoingMessage *message) { - [message setSingleGroupRecipient:singleGroupRecipient]; - }]; -} - -#pragma mark - Sent Recipients - -- (NSArray *)sentRecipients -{ - @synchronized(self) - { - return _sentRecipients; - } -} - -- (void)setSentRecipients:(NSArray *)sentRecipients -{ - @synchronized(self) - { - _sentRecipients = [sentRecipients copy]; - } -} - -- (void)addSentRecipient:(NSString *)contactId -{ - @synchronized(self) - { - OWSAssert(_sentRecipients); - OWSAssert(contactId.length > 0); - - NSMutableArray *sentRecipients = [_sentRecipients mutableCopy]; - [sentRecipients addObject:contactId]; - _sentRecipients = [sentRecipients copy]; - } -} - -- (BOOL)wasSentToRecipient:(NSString *)contactId -{ - OWSAssert(self.sentRecipients); - OWSAssert(contactId.length > 0); - - return [self.sentRecipients containsObject:contactId]; -} - -- (NSUInteger)sentRecipientsCount -{ - OWSAssert(self.sentRecipients); - - return self.sentRecipients.count; -} - -- (void)updateWithSentRecipient:(NSString *)contactId transaction:(YapDatabaseReadWriteTransaction *)transaction -{ - OWSAssert(transaction); - [self applyChangeToSelfAndLatestCopy:transaction - changeBlock:^(TSOutgoingMessage *message) { - [message addSentRecipient:contactId]; + TSOutgoingMessageRecipientState *_Nullable recipientState + = message.recipientStateMap[recipientId]; + if (!recipientState) { + OWSFail(@"%@ Missing recipient state for delivered recipient: %@", + self.logTag, + recipientId); + return; + } + if (recipientState.state != OWSOutgoingMessageRecipientStateSent) { + DDLogWarn(@"%@ marking unsent message as delivered.", self.logTag); + } + recipientState.state = OWSOutgoingMessageRecipientStateSent; + recipientState.deliveryTimestamp = deliveryTimestamp; }]; } @@ -403,25 +580,96 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec [self applyChangeToSelfAndLatestCopy:transaction changeBlock:^(TSOutgoingMessage *message) { - NSMutableDictionary *recipientReadMap - = (message.recipientReadMap ? [message.recipientReadMap mutableCopy] - : [NSMutableDictionary new]); - recipientReadMap[recipientId] = @(readTimestamp); - message.recipientReadMap = [recipientReadMap copy]; + TSOutgoingMessageRecipientState *_Nullable recipientState + = message.recipientStateMap[recipientId]; + if (!recipientState) { + OWSFail(@"%@ Missing recipient state for delivered recipient: %@", + self.logTag, + recipientId); + return; + } + if (recipientState.state != OWSOutgoingMessageRecipientStateSent) { + DDLogWarn(@"%@ marking unsent message as delivered.", self.logTag); + } + recipientState.state = OWSOutgoingMessageRecipientStateSent; + recipientState.readTimestamp = @(readTimestamp); + }]; +} + +- (void)updateWithWasSentFromLinkedDeviceWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssert(transaction); + + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSOutgoingMessage *message) { + // Mark any "sending" recipients as "sent." + for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap + .allValues) { + if (recipientState.state == OWSOutgoingMessageRecipientStateSending) { + recipientState.state = OWSOutgoingMessageRecipientStateSent; + } + } + [message setIsFromLinkedDevice:YES]; + }]; +} + +- (void)updateWithSendingToSingleGroupRecipient:(NSString *)singleGroupRecipient + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssert(transaction); + OWSAssert(singleGroupRecipient.length > 0); + + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSOutgoingMessage *message) { + TSOutgoingMessageRecipientState *recipientState = + [TSOutgoingMessageRecipientState new]; + recipientState.state = OWSOutgoingMessageRecipientStateSending; + [message setRecipientStateMap:@{ + singleGroupRecipient : recipientState, + }]; }]; } - (nullable NSNumber *)firstRecipientReadTimestamp { NSNumber *result = nil; - for (NSNumber *timestamp in self.recipientReadMap.allValues) { - if (!result || (result.unsignedLongLongValue > timestamp.unsignedLongLongValue)) { - result = timestamp; + for (TSOutgoingMessageRecipientState *recipientState in self.recipientStateMap.allValues) { + if (!recipientState.readTimestamp) { + continue; + } + if (!result || (result.unsignedLongLongValue > recipientState.readTimestamp.unsignedLongLongValue)) { + result = recipientState.readTimestamp; } } return result; } +- (void)updateWithFakeMessageState:(TSOutgoingMessageState)messageState + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssert(transaction); + + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSOutgoingMessage *message) { + for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap + .allValues) { + switch (messageState) { + case TSOutgoingMessageStateSending: + recipientState.state = OWSOutgoingMessageRecipientStateSending; + break; + case TSOutgoingMessageStateFailed: + recipientState.state = OWSOutgoingMessageRecipientStateFailed; + break; + case TSOutgoingMessageStateSent: + recipientState.state = OWSOutgoingMessageRecipientStateSent; + break; + default: + OWSFail(@"%@ unexpected message state.", self.logTag); + break; + } + } + }]; +} #pragma mark - - (OWSSignalServiceProtosDataMessageBuilder *)dataMessageBuilder @@ -437,7 +685,7 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec [builder setBody:self.body]; } else { OWSFail(@"%@ message body length too long.", self.logTag); - NSString *truncatedBody = self.body; + NSString *truncatedBody = [self.body copy]; while ([truncatedBody lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kOversizeTextMessageSizeThreshold) { DDLogError(@"%@ truncating body which is too long: %tu", self.logTag, diff --git a/SignalServiceKit/src/Messages/OWSFailedMessagesJob.m b/SignalServiceKit/src/Messages/OWSFailedMessagesJob.m index 5ea221472c..01ad559c13 100644 --- a/SignalServiceKit/src/Messages/OWSFailedMessagesJob.m +++ b/SignalServiceKit/src/Messages/OWSFailedMessagesJob.m @@ -44,9 +44,8 @@ static NSString *const OWSFailedMessagesJobMessageStateIndex = @"index_outoing_m NSMutableArray *messageIds = [NSMutableArray new]; - NSString *formattedString = [NSString stringWithFormat:@"WHERE %@ == %d", - OWSFailedMessagesJobMessageStateColumn, - (int)TSOutgoingMessageStateAttemptingOut]; + NSString *formattedString = [NSString + stringWithFormat:@"WHERE %@ == %d", OWSFailedMessagesJobMessageStateColumn, (int)TSOutgoingMessageStateSending]; YapDatabaseQuery *query = [YapDatabaseQuery queryWithFormat:formattedString]; [[transaction ext:OWSFailedMessagesJobMessageStateIndex] enumerateKeysMatchingQuery:query @@ -83,8 +82,8 @@ static NSString *const OWSFailedMessagesJobMessageStateIndex = @"index_outoing_m readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { [self enumerateAttemptingOutMessagesWithBlock:^(TSOutgoingMessage *message) { // sanity check - OWSAssert(message.messageState == TSOutgoingMessageStateAttemptingOut); - if (message.messageState != TSOutgoingMessageStateAttemptingOut) { + OWSAssert(message.messageState == TSOutgoingMessageStateSending); + if (message.messageState != TSOutgoingMessageStateSending) { DDLogError(@"%@ Refusing to mark as unsent message with state: %d", self.logTag, (int)message.messageState); @@ -92,8 +91,8 @@ static NSString *const OWSFailedMessagesJobMessageStateIndex = @"index_outoing_m } DDLogDebug(@"%@ marking message as unsent: %@", self.logTag, message.uniqueId); - [message updateWithMessageState:TSOutgoingMessageStateUnsent transaction:transaction]; - OWSAssert(message.messageState == TSOutgoingMessageStateUnsent); + [message updateWithAllSendingRecipientsMarkedAsFailedWithTansaction:transaction]; + OWSAssert(message.messageState == TSOutgoingMessageStateFailed); count++; } diff --git a/SignalServiceKit/src/Messages/OWSMessageManager.m b/SignalServiceKit/src/Messages/OWSMessageManager.m index 901f929b59..10934acae8 100644 --- a/SignalServiceKit/src/Messages/OWSMessageManager.m +++ b/SignalServiceKit/src/Messages/OWSMessageManager.m @@ -262,9 +262,9 @@ NS_ASSUME_NONNULL_BEGIN timestamp); } for (TSOutgoingMessage *outgoingMessage in messages) { - [outgoingMessage updateWithDeliveredToRecipientId:recipientId - deliveryTimestamp:deliveryTimestamp - transaction:transaction]; + [outgoingMessage updateWithDeliveredRecipient:recipientId + deliveryTimestamp:deliveryTimestamp + transaction:transaction]; } } } @@ -891,7 +891,7 @@ NS_ASSUME_NONNULL_BEGIN [TSOutgoingMessage outgoingMessageInThread:gThread groupMetaMessage:TSGroupMessageUpdate]; [message updateWithCustomMessage:updateGroupInfo transaction:transaction]; // Only send this group update to the requester. - [message updateWithSingleGroupRecipient:envelope.source transaction:transaction]; + [message updateWithSendingToSingleGroupRecipient:envelope.source transaction:transaction]; [self sendGroupUpdateForThread:gThread message:message]; } diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index bc05019052..3421053217 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -186,7 +186,8 @@ void AssertIsOnSendingQueue() - (void)didSucceed { - [self.message updateWithMessageState:TSOutgoingMessageStateSentToService]; + OWSAssert(self.message.messageState == TSOutgoingMessageStateSent); + self.successHandler(); } @@ -311,7 +312,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; // All outgoing messages should be saved at the time they are enqueued. [message saveWithTransaction:transaction]; - [message updateWithMessageState:TSOutgoingMessageStateAttemptingOut transaction:transaction]; + // When we start a message send, all "failed" recipients should be marked as "sending". + [message updateWithMarkingAllUnsentRecipientsAsSendingWithTransaction:transaction]; }]; NSOperationQueue *sendingQueue = [self sendingQueueForMessage:message]; @@ -417,11 +419,15 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; }); } -- (NSArray *)getRecipients:(NSArray *)identifiers error:(NSError **)error +- (NSArray *)getRecipientsForRecipientIds:(NSArray *)recipientIds error:(NSError **)error { + OWSAssert(error); + + error = nil; + NSMutableArray *recipients = [NSMutableArray new]; - for (NSString *recipientId in identifiers) { + for (NSString *recipientId in recipientIds) { SignalRecipient *existingRecipient = [SignalRecipient recipientWithTextSecureIdentifier:recipientId]; if (existingRecipient) { @@ -451,11 +457,36 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; TSThread *thread = message.thread; if ([thread isKindOfClass:[TSGroupThread class]]) { + TSGroupThread *gThread = (TSGroupThread *)thread; + // Send to the intersection of: + // + // * "sending" recipients of the message. + // * members of the group. + // + // I.e. try to send a message IFF: + // + // * The recipient was in the group when the message was first tried to be sent. + // * The recipient is still in the group. + // * The recipient is in the "sending" state. + NSMutableSet *obsoleteRecipientIds = [NSMutableSet setWithArray:message.sendingRecipientIds]; + [obsoleteRecipientIds minusSet:[NSSet setWithArray:gThread.groupModel.groupMemberIds]]; + if (obsoleteRecipientIds.count > 0) { + [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + for (NSString *recipientId in obsoleteRecipientIds) { + // Mark this recipient as "skipped". + [message updateWithSkippedRecipient:recipientId transaction:transaction]; + } + }]; + } + + NSMutableSet *sendingRecipientIds = [NSMutableSet setWithArray:message.sendingRecipientIds]; + [sendingRecipientIds intersectSet:[NSSet setWithArray:gThread.groupModel.groupMemberIds]]; + NSError *error; NSArray *recipients = - [self getRecipients:gThread.groupModel.groupMemberIds error:&error]; + [self getRecipientsForRecipientIds:sendingRecipientIds.allObjects error:&error]; if (recipients.count == 0) { if (!error) { @@ -491,7 +522,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; // you might, for example, have a pending outgoing message when // you block them. OWSAssert(recipientContactId.length > 0); - if ([_blockingManager isRecipientIdBlocked:recipientContactId]) { + if ([self.blockingManager isRecipientIdBlocked:recipientContactId]) { DDLogInfo(@"%@ skipping 1:1 send to blocked contact: %@", self.logTag, recipientContactId); NSError *error = OWSErrorMakeMessageSendFailedToBlockListError(); // No need to retry - the user will continue to be blocked. @@ -560,10 +591,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; thread:thread attempts:OWSMessageSenderRetryAttempts success:^{ - DDLogInfo(@"%@ Marking group message as sent to recipient: %@", self.logTag, recipient.uniqueId); - [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [message updateWithSentRecipient:recipient.uniqueId transaction:transaction]; - }]; [futureSource trySetResult:@1]; } failure:^(NSError *error) { @@ -580,6 +607,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; failure:(RetryableFailureHandler)failureHandler { [self saveGroupMessage:message inThread:thread]; + NSMutableArray *futures = [NSMutableArray array]; for (SignalRecipient *recipient in recipients) { @@ -589,17 +617,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; if ([recipientId isEqualToString:[TSAccountManager localNumber]]) { continue; } - // We don't need to sent the message to all group members if - // it has a "single group recipient". - if (message.singleGroupRecipient && ![message.singleGroupRecipient isEqualToString:recipientId]) { - continue; - } - if ([message wasSentToRecipient:recipientId]) { - // Skip recipients we have already sent this message to (on an - // earlier retry, perhaps). - DDLogInfo(@"%@ Skipping group message recipient; already sent: %@", self.logTag, recipient.uniqueId); - continue; - } // ...otherwise we send. [futures addObject:[self sendMessageFuture:message recipient:recipient thread:thread]]; @@ -679,6 +696,11 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; thread:(TSThread *)thread { [self.dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + if (thread.isGroupThread) { + // Mark as "skipped" group members who no longer have signal accounts. + [message updateWithSkippedRecipient:recipient.recipientId transaction:transaction]; + } + [recipient removeWithTransaction:transaction]; [[TSInfoMessage userNotRegisteredMessageInThread:thread] saveWithTransaction:transaction]; @@ -904,7 +926,11 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; } dispatch_async([OWSDispatch sendingQueue], ^{ - [recipient save]; + [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [recipient saveWithTransaction:transaction]; + [message updateWithSentRecipient:recipient.uniqueId transaction:transaction]; + }]; + [self handleMessageSentLocally:message]; successHandler(); });