Merge branch 'charlesmchen/outgoingMessageState'

This commit is contained in:
Matthew Chen 2018-04-25 10:49:30 -04:00
commit 3291ca66c1
20 changed files with 1044 additions and 610 deletions

View File

@ -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 = "<group>"; };
34A9105E1FFEB113000C4745 /* OWSBackup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackup.h; sourceTree = "<group>"; };
34A9105F1FFEB114000C4745 /* OWSBackup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackup.m; sourceTree = "<group>"; };
34ABB2C22090C59600C727A6 /* OWSResaveCollectionDBMigration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSResaveCollectionDBMigration.m; sourceTree = "<group>"; };
34ABB2C32090C59700C727A6 /* OWSResaveCollectionDBMigration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSResaveCollectionDBMigration.h; sourceTree = "<group>"; };
34B0796B1FCF46B000E248C2 /* MainAppContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MainAppContext.m; sourceTree = "<group>"; };
34B0796C1FCF46B000E248C2 /* MainAppContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MainAppContext.h; sourceTree = "<group>"; };
34B0796E1FD07B1E00E248C2 /* SignalShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SignalShareExtension.entitlements; sourceTree = "<group>"; };
@ -847,6 +853,8 @@
34D2CCDD206939B200CB1A14 /* DebugUIMessagesAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessagesAction.h; sourceTree = "<group>"; };
34D2CCDE206939B400CB1A14 /* DebugUIMessagesAssetLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessagesAssetLoader.h; sourceTree = "<group>"; };
34D2CCE220693A1700CB1A14 /* DebugUIMessagesUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessagesUtils.h; sourceTree = "<group>"; };
34D5872D208E2C4100D2255A /* OWS109OutgoingMessageState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWS109OutgoingMessageState.m; sourceTree = "<group>"; };
34D5872E208E2C4100D2255A /* OWS109OutgoingMessageState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWS109OutgoingMessageState.h; sourceTree = "<group>"; };
34D5CCA71EAE3D30005515DB /* AvatarViewHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvatarViewHelper.h; sourceTree = "<group>"; };
34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AvatarViewHelper.m; sourceTree = "<group>"; };
34D8C0231ED3673300188D7C /* DebugUIMessages.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessages.h; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

File diff suppressed because it is too large Load Diff

View File

@ -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.")
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -0,0 +1,46 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWS109OutgoingMessageState.h"
#import <SignalServiceKit/OWSPrimaryStorage.h>
#import <SignalServiceKit/TSOutgoingMessage.h>
#import <YapDatabase/YapDatabaseTransaction.h>
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

View File

@ -10,6 +10,7 @@
#import "OWS105AttachmentFilePaths.h"
#import "OWS107LegacySounds.h"
#import "OWS108CallLoggingPreference.h"
#import "OWS109OutgoingMessageState.h"
#import "OWSDatabaseMigration.h"
#import <SignalMessaging/SignalMessaging-Swift.h>
#import <SignalServiceKit/AppContext.h>
@ -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]
];
}

View File

@ -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

View File

@ -0,0 +1,80 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSResaveCollectionDBMigration.h"
#import <YapDatabase/YapDatabaseConnection.h>
#import <YapDatabase/YapDatabaseTransaction.h>
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<NSString *> *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<NSString *> *)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

View File

@ -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

View File

@ -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];

View File

@ -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<NSString *, NSNumber *> *recipientDeliveryMap;
// Map of "recipient id"-to-"read time" of the recipients who have read the message.
@property (atomic, readonly) NSDictionary<NSString *, NSNumber *> *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<NSString *> *)recipientIds;
// All recipients of this message who we are currently trying to send to (queued, uploading or during send).
- (NSArray<NSString *> *)sendingRecipientIds;
// All recipients of this message to whom it has been sent and delivered.
- (NSArray<NSString *> *)deliveredRecipientIds;
// All recipients of this message to whom it has been sent, delivered and read.
- (NSArray<NSString *> *)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

View File

@ -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<NSString *> *sentRecipients;
@property (atomic) TSGroupMetaMessage groupMetaMessage;
@property (atomic) NSDictionary<NSString *, NSNumber *> *recipientDeliveryMap;
@property (atomic) NSDictionary<NSString *, NSNumber *> *recipientReadMap;
@property (atomic, nullable) NSDictionary<NSString *, TSOutgoingMessageRecipientState *> *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<NSString *, NSNumber *> *_Nullable recipientDeliveryMap =
[coder decodeObjectForKey:@"recipientDeliveryMap"];
NSDictionary<NSString *, NSNumber *> *_Nullable recipientReadMap = [coder decodeObjectForKey:@"recipientReadMap"];
NSArray<NSString *> *_Nullable sentRecipients = [coder decodeObjectForKey:@"sentRecipients"];
NSMutableDictionary<NSString *, TSOutgoingMessageRecipientState *> *recipientStateMap = [NSMutableDictionary new];
// Our default recipient list is the current thread members.
__block NSArray<NSString *> *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<NSString *, TSOutgoingMessageRecipientState *> *recipientStateMap = [NSMutableDictionary new];
NSArray<NSString *> *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<TSOutgoingMessageRecipientState *> *)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<NSString *> *)recipientIds
{
return self.recipientStateMap.allKeys;
}
- (NSArray<NSString *> *)sendingRecipientIds
{
NSMutableArray<NSString *> *result = [NSMutableArray new];
for (NSString *recipientId in self.recipientStateMap) {
TSOutgoingMessageRecipientState *recipientState = self.recipientStateMap[recipientId];
if (recipientState.state == OWSOutgoingMessageRecipientStateSending) {
[result addObject:recipientId];
}
}
return result;
}
- (NSArray<NSString *> *)deliveredRecipientIds
{
NSMutableArray<NSString *> *result = [NSMutableArray new];
for (NSString *recipientId in self.recipientStateMap) {
TSOutgoingMessageRecipientState *recipientState = self.recipientStateMap[recipientId];
if (recipientState.deliveryTimestamp != nil) {
[result addObject:recipientId];
}
}
return result;
}
- (NSArray<NSString *> *)readRecipientIds
{
NSMutableArray<NSString *> *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<NSString *, id> *_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<NSString *, NSNumber *> *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<NSString *> *)sentRecipients
{
@synchronized(self)
{
return _sentRecipients;
}
}
- (void)setSentRecipients:(NSArray<NSString *> *)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<NSString *, NSNumber *> *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,

View File

@ -44,9 +44,8 @@ static NSString *const OWSFailedMessagesJobMessageStateIndex = @"index_outoing_m
NSMutableArray<NSString *> *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++;
}

View File

@ -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];
}

View File

@ -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<SignalRecipient *> *)getRecipients:(NSArray<NSString *> *)identifiers error:(NSError **)error
- (NSArray<SignalRecipient *> *)getRecipientsForRecipientIds:(NSArray<NSString *> *)recipientIds error:(NSError **)error
{
OWSAssert(error);
error = nil;
NSMutableArray<SignalRecipient *> *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<NSString *> *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<NSString *> *sendingRecipientIds = [NSMutableSet setWithArray:message.sendingRecipientIds];
[sendingRecipientIds intersectSet:[NSSet setWithArray:gThread.groupModel.groupMemberIds]];
NSError *error;
NSArray<SignalRecipient *> *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<TOCFuture *> *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();
});