Rework outgoing message state.

pull/1/head
Matthew Chen 7 years ago
parent 56c53cdff2
commit 9275c67818

@ -499,7 +499,7 @@ NS_ASSUME_NONNULL_BEGIN
// Ignore taps on links in outgoing messages that haven't been sent yet, as // Ignore taps on links in outgoing messages that haven't been sent yet, as
// this interferes with "tap to retry". // this interferes with "tap to retry".
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction; TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
shouldIgnoreEvents = outgoingMessage.messageState != TSOutgoingMessageStateSentToService; shouldIgnoreEvents = outgoingMessage.messageState != TSOutgoingMessageStateSent;
} }
[self.class loadForTextDisplay:self.bodyTextView [self.class loadForTextDisplay:self.bodyTextView
text:self.displayableBodyText.displayText text:self.displayableBodyText.displayText
@ -1026,7 +1026,7 @@ NS_ASSUME_NONNULL_BEGIN
return NO; return NO;
} }
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction; TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
return outgoingMessage.messageState == TSOutgoingMessageStateAttemptingOut; return outgoingMessage.messageState == TSOutgoingMessageStateSending;
} }
- (OWSMessagesBubbleImageFactory *)bubbleFactory - (OWSMessagesBubbleImageFactory *)bubbleFactory
@ -1085,9 +1085,9 @@ NS_ASSUME_NONNULL_BEGIN
if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) { if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction; TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
if (outgoingMessage.messageState == TSOutgoingMessageStateUnsent) { if (outgoingMessage.messageState == TSOutgoingMessageStateFailed) {
return; return;
} else if (outgoingMessage.messageState == TSOutgoingMessageStateAttemptingOut) { } else if (outgoingMessage.messageState == TSOutgoingMessageStateSending) {
// Ignore taps on outgoing messages being sent. // Ignore taps on outgoing messages being sent.
return; return;
} }

@ -108,7 +108,7 @@ NS_ASSUME_NONNULL_BEGIN
return NO; return NO;
} }
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction; TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
return outgoingMessage.messageState == TSOutgoingMessageStateUnsent; return outgoingMessage.messageState == TSOutgoingMessageStateFailed;
} }
- (UIImage *)failedSendBadge - (UIImage *)failedSendBadge
@ -527,10 +527,10 @@ NS_ASSUME_NONNULL_BEGIN
if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) { if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction; TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
if (outgoingMessage.messageState == TSOutgoingMessageStateUnsent) { if (outgoingMessage.messageState == TSOutgoingMessageStateFailed) {
// Ignore long press on unsent messages. // Ignore long press on unsent messages.
return; return;
} else if (outgoingMessage.messageState == TSOutgoingMessageStateAttemptingOut) { } else if (outgoingMessage.messageState == TSOutgoingMessageStateSending) {
// Ignore long press on outgoing messages being sent. // Ignore long press on outgoing messages being sent.
return; return;
} }

@ -4663,7 +4663,7 @@ typedef enum : NSUInteger {
MessageRecipientStatus recipientStatus = MessageRecipientStatus recipientStatus =
[MessageRecipientStatusUtils recipientStatusWithOutgoingMessage:outgoingMessage]; [MessageRecipientStatusUtils recipientStatusWithOutgoingMessage:outgoingMessage];
if (outgoingMessage.messageState == TSOutgoingMessageStateUnsent) { if (outgoingMessage.messageState == TSOutgoingMessageStateFailed) {
// always show "failed to send" status // always show "failed to send" status
shouldHideRecipientStatus = NO; shouldHideRecipientStatus = NO;
} else { } else {

@ -662,8 +662,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
} else if (action == self.replyActionSelector) { } else if (action == self.replyActionSelector) {
if ([self.interaction isKindOfClass:[TSOutgoingMessage class]]) { if ([self.interaction isKindOfClass:[TSOutgoingMessage class]]) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.interaction; TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.interaction;
if (outgoingMessage.messageState == TSOutgoingMessageStateUnsent if (outgoingMessage.messageState == TSOutgoingMessageStateFailed
|| outgoingMessage.messageState == TSOutgoingMessageStateAttemptingOut) { || outgoingMessage.messageState == TSOutgoingMessageStateSending) {
// Don't let users reply to messages which aren't yet delivered to the service. // Don't let users reply to messages which aren't yet delivered to the service.
return NO; return NO;
} }

File diff suppressed because it is too large Load Diff

@ -212,7 +212,9 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
groupRows.append(divider) groupRows.append(divider)
} }
for recipientId in thread.recipientIdentifiers { let messageRecipientIds = outgoingMessage.recipientIds()
for recipientId in messageRecipientIds {
let (recipientStatus, shortStatusMessage, _) = MessageRecipientStatusUtils.recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, recipientId: recipientId, referenceView: self.view) let (recipientStatus, shortStatusMessage, _) = MessageRecipientStatusUtils.recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, recipientId: recipientId, referenceView: self.view)
guard recipientStatus == recipientStatusGroup else { guard recipientStatus == recipientStatusGroup else {
@ -560,7 +562,10 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
comment: "Status label for messages which are read.") comment: "Status label for messages which are read.")
case .failed: case .failed:
return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_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.")
} }
} }

@ -13,6 +13,7 @@ import SignalMessaging
case delivered case delivered
case read case read
case failed case failed
case skipped
} }
// Our per-recipient status messages are "biased towards success" // Our per-recipient status messages are "biased towards success"
@ -93,57 +94,57 @@ class MessageRecipientStatusUtils: NSObject {
// so we fall back to `TSOutgoingMessageState` which is not per-recipient and therefore // so we fall back to `TSOutgoingMessageState` which is not per-recipient and therefore
// might be misleading. // might be misleading.
let recipientReadMap = outgoingMessage.recipientReadMap guard let recipientState = outgoingMessage.recipientState(forRecipientId: recipientId) else {
if let readTimestamp = recipientReadMap[recipientId] { let shortStatusMessage = NSLocalizedString("MESSAGE_STATUS_FAILED_SHORT", comment: "status message for failed messages")
assert(outgoingMessage.messageState == .sentToService) let longStatusMessage = NSLocalizedString("MESSAGE_STATUS_FAILED", comment: "message footer for failed messages")
let timestampString = DateUtil.formatPastTimestampRelativeToNow(readTimestamp.uint64Value, return (status:.failed, shortStatusMessage:shortStatusMessage, longStatusMessage:longStatusMessage)
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 switch recipientState.state {
if let deliveryTimestamp = recipientDeliveryMap[recipientId] { case .failed:
assert(outgoingMessage.messageState == .sentToService) let shortStatusMessage = NSLocalizedString("MESSAGE_STATUS_FAILED_SHORT", comment: "status message for failed messages")
let timestampString = DateUtil.formatPastTimestampRelativeToNow(deliveryTimestamp.uint64Value, let longStatusMessage = NSLocalizedString("MESSAGE_STATUS_FAILED", comment: "message footer for failed messages")
isRTL:referenceView.isRTL()) return (status:.failed, shortStatusMessage:shortStatusMessage, longStatusMessage:longStatusMessage)
let shortStatusMessage = timestampString case .sending:
let longStatusMessage = NSLocalizedString("MESSAGE_STATUS_DELIVERED", if outgoingMessage.hasAttachments() {
comment:"message status for message delivered to their recipient.").rtlSafeAppend(" ", referenceView:referenceView) assert(outgoingMessage.messageState == .sending)
.rtlSafeAppend(timestampString, referenceView:referenceView)
return (status:.delivered, shortStatusMessage:shortStatusMessage, longStatusMessage:longStatusMessage)
}
if outgoingMessage.wasDelivered { let statusMessage = NSLocalizedString("MESSAGE_STATUS_UPLOADING",
let statusMessage = NSLocalizedString("MESSAGE_STATUS_DELIVERED", comment: "message footer while attachment is uploading")
comment:"message status for message delivered to their recipient.") return (status:.uploading, shortStatusMessage:statusMessage, longStatusMessage:statusMessage)
return (status:.delivered, shortStatusMessage:statusMessage, longStatusMessage:statusMessage) } else {
} assert(outgoingMessage.messageState == .sending)
if outgoingMessage.messageState == .unsent { let statusMessage = NSLocalizedString("MESSAGE_STATUS_SENDING",
let shortStatusMessage = NSLocalizedString("MESSAGE_STATUS_FAILED_SHORT", comment:"status message for failed messages") comment: "message status while message is sending.")
let longStatusMessage = NSLocalizedString("MESSAGE_STATUS_FAILED", comment:"message footer for failed messages") return (status:.sending, shortStatusMessage:statusMessage, longStatusMessage:statusMessage)
return (status:.failed, shortStatusMessage:shortStatusMessage, longStatusMessage:longStatusMessage) }
} else if outgoingMessage.messageState == .sentToService || case .sent:
outgoingMessage.wasSent(toRecipient:recipientId) { 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 = let statusMessage =
NSLocalizedString("MESSAGE_STATUS_SENT", NSLocalizedString("MESSAGE_STATUS_SENT",
comment:"message footer for sent messages") comment: "message footer for sent messages")
return (status:.sent, shortStatusMessage:statusMessage, longStatusMessage:statusMessage) return (status:.sent, shortStatusMessage:statusMessage, longStatusMessage:statusMessage)
} else if outgoingMessage.hasAttachments() { case .skipped:
assert(outgoingMessage.messageState == .attemptingOut) let statusMessage = NSLocalizedString("MESSAGE_STATUS_RECIPIENT_SKIPPED",
comment: "message status if message delivery to a recipient is skipped.")
let statusMessage = NSLocalizedString("MESSAGE_STATUS_UPLOADING", return (status:.skipped, shortStatusMessage:statusMessage, longStatusMessage:statusMessage)
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)
} }
} }
@ -153,40 +154,31 @@ class MessageRecipientStatusUtils: NSObject {
referenceView: UIView) -> String { referenceView: UIView) -> String {
switch outgoingMessage.messageState { switch outgoingMessage.messageState {
case .unsent: case .failed:
// Use the "long" version of this message here. // Use the "long" version of this message here.
return NSLocalizedString("MESSAGE_STATUS_FAILED", comment:"message footer for failed messages") return NSLocalizedString("MESSAGE_STATUS_FAILED", comment: "message footer for failed messages")
case .attemptingOut: case .sending:
if outgoingMessage.hasAttachments() { if outgoingMessage.hasAttachments() {
return NSLocalizedString("MESSAGE_STATUS_UPLOADING", return NSLocalizedString("MESSAGE_STATUS_UPLOADING",
comment:"message footer while attachment is uploading") comment: "message footer while attachment is uploading")
} else { } else {
return NSLocalizedString("MESSAGE_STATUS_SENDING", return NSLocalizedString("MESSAGE_STATUS_SENDING",
comment:"message status while message is sending.") comment: "message status while message is sending.")
} }
case .sentToService: case .sent:
let recipientReadMap = outgoingMessage.recipientReadMap if outgoingMessage.readRecipientIds().count > 0 {
if recipientReadMap.count > 0 { return NSLocalizedString("MESSAGE_STATUS_READ", comment: "message footer for read messages")
return NSLocalizedString("MESSAGE_STATUS_READ", comment:"message footer for read messages")
} }
if outgoingMessage.deliveredRecipientIds().count > 0 {
let recipientDeliveryMap = outgoingMessage.recipientDeliveryMap
if recipientDeliveryMap.count > 0 {
return NSLocalizedString("MESSAGE_STATUS_DELIVERED", return NSLocalizedString("MESSAGE_STATUS_DELIVERED",
comment:"message status for message delivered to their recipient.") 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", return NSLocalizedString("MESSAGE_STATUS_SENT",
comment:"message footer for sent messages") comment: "message footer for sent messages")
default: default:
owsFail("Message has unexpected status: \(outgoingMessage.messageState).") owsFail("Message has unexpected status: \(outgoingMessage.messageState).")
return NSLocalizedString("MESSAGE_STATUS_SENT", return NSLocalizedString("MESSAGE_STATUS_SENT",
comment:"message footer for sent messages") comment: "message footer for sent messages")
} }
} }
@ -194,26 +186,19 @@ class MessageRecipientStatusUtils: NSObject {
// See comments above. // See comments above.
public class func recipientStatus(outgoingMessage: TSOutgoingMessage) -> MessageRecipientStatus { public class func recipientStatus(outgoingMessage: TSOutgoingMessage) -> MessageRecipientStatus {
switch outgoingMessage.messageState { switch outgoingMessage.messageState {
case .unsent: case .failed:
return .failed return .failed
case .attemptingOut: case .sending:
if outgoingMessage.hasAttachments() { if outgoingMessage.hasAttachments() {
return .uploading return .uploading
} else { } else {
return .sending return .sending
} }
case .sentToService: case .sent:
let recipientReadMap = outgoingMessage.recipientReadMap if outgoingMessage.readRecipientIds().count > 0 {
if recipientReadMap.count > 0 {
return .read return .read
} }
if outgoingMessage.deliveredRecipientIds().count > 0 {
let recipientDeliveryMap = outgoingMessage.recipientDeliveryMap
if recipientDeliveryMap.count > 0 {
return .delivered
}
if outgoingMessage.wasDelivered {
return .delivered return .delivered
} }

@ -45,9 +45,9 @@ public class OWSMessagesBubbleImageFactory: NSObject {
return self.incoming return self.incoming
} else if let outgoingMessage = message as? TSOutgoingMessage { } else if let outgoingMessage = message as? TSOutgoingMessage {
switch outgoingMessage.messageState { switch outgoingMessage.messageState {
case .unsent: case .failed:
return outgoingFailed return outgoingFailed
case .attemptingOut: case .sending:
return currentlyOutgoing return currentlyOutgoing
default: default:
return outgoing return outgoing
@ -75,9 +75,9 @@ public class OWSMessagesBubbleImageFactory: NSObject {
return OWSMessagesBubbleImageFactory.bubbleColorIncoming return OWSMessagesBubbleImageFactory.bubbleColorIncoming
} else if let outgoingMessage = message as? TSOutgoingMessage { } else if let outgoingMessage = message as? TSOutgoingMessage {
switch outgoingMessage.messageState { switch outgoingMessage.messageState {
case .unsent: case .failed:
return OWSMessagesBubbleImageFactory.bubbleColorOutgoingUnsent return OWSMessagesBubbleImageFactory.bubbleColorOutgoingUnsent
case .attemptingOut: case .sending:
return OWSMessagesBubbleImageFactory.bubbleColorOutgoingSending return OWSMessagesBubbleImageFactory.bubbleColorOutgoingSending
default: default:
return OWSMessagesBubbleImageFactory.bubbleColorOutgoingSent return OWSMessagesBubbleImageFactory.bubbleColorOutgoingSent

@ -1,10 +1,11 @@
// //
// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// //
#import "OWSOutgoingSentMessageTranscript.h" #import "OWSOutgoingSentMessageTranscript.h"
#import "OWSSignalServiceProtos.pb.h" #import "OWSSignalServiceProtos.pb.h"
#import "TSOutgoingMessage.h" #import "TSOutgoingMessage.h"
#import "TSThread.h"
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@ -48,8 +49,8 @@ NS_ASSUME_NONNULL_BEGIN
OWSSignalServiceProtosSyncMessageSentBuilder *sentBuilder = [OWSSignalServiceProtosSyncMessageSentBuilder new]; OWSSignalServiceProtosSyncMessageSentBuilder *sentBuilder = [OWSSignalServiceProtosSyncMessageSentBuilder new];
[sentBuilder setTimestamp:self.message.timestamp]; [sentBuilder setTimestamp:self.message.timestamp];
[sentBuilder setDestination:self.message.recipientIdentifier]; [sentBuilder setDestination:self.recipientIdentifierForMessage];
[sentBuilder setMessage:[self.message buildDataMessage:self.message.recipientIdentifier]]; [sentBuilder setMessage:[self.message buildDataMessage:self.recipientIdentifierForMessage]];
[sentBuilder setExpirationStartTimestamp:self.message.timestamp]; [sentBuilder setExpirationStartTimestamp:self.message.timestamp];
[syncMessageBuilder setSentBuilder:sentBuilder]; [syncMessageBuilder setSentBuilder:sentBuilder];
@ -57,6 +58,12 @@ NS_ASSUME_NONNULL_BEGIN
return syncMessageBuilder; return syncMessageBuilder;
} }
// Note that this will return nil for group messages.
- (nullable NSString *)recipientIdentifierForMessage
{
return self.message.thread.contactIdentifier;
}
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

@ -11,14 +11,30 @@ typedef NS_ENUM(NSInteger, TSOutgoingMessageState) {
// a) Enqueued for sending. // a) Enqueued for sending.
// b) Waiting on attachment upload(s). // b) Waiting on attachment upload(s).
// c) Being sent to the service. // c) Being sent to the service.
TSOutgoingMessageStateAttemptingOut, TSOutgoingMessageStateSending,
// The failure state. // The failure state.
TSOutgoingMessageStateUnsent, TSOutgoingMessageStateFailed,
// These two enum values have been combined into TSOutgoingMessageStateSentToService. // These two enum values have been combined into TSOutgoingMessageStateSent.
TSOutgoingMessageStateSent_OBSOLETE, TSOutgoingMessageStateSent_OBSOLETE,
TSOutgoingMessageStateDelivered_OBSOLETE, TSOutgoingMessageStateDelivered_OBSOLETE,
// The message has been sent to the service. // 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) { typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
@ -35,6 +51,16 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
@class OWSSignalServiceProtosDataMessageBuilder; @class OWSSignalServiceProtosDataMessageBuilder;
@class SignalRecipient; @class SignalRecipient;
@interface TSOutgoingMessageRecipientState : NSObject
@property (atomic, readonly) OWSOutgoingMessageRecipientState state;
@property (atomic, nullable, readonly) NSNumber *deliveryTimestamp;
@property (atomic, nullable, readonly) NSNumber *readTimestamp;
@end
#pragma mark -
@interface TSOutgoingMessage : TSMessage @interface TSOutgoingMessage : TSMessage
- (instancetype)initMessageWithTimestamp:(uint64_t)timestamp - (instancetype)initMessageWithTimestamp:(uint64_t)timestamp
@ -75,11 +101,7 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
+ (instancetype)outgoingMessageInThread:(nullable TSThread *)thread + (instancetype)outgoingMessageInThread:(nullable TSThread *)thread
groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage; groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage;
@property (atomic, readonly) TSOutgoingMessageState messageState; @property (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 (atomic, readonly) BOOL hasSyncedTranscript; @property (atomic, readonly) BOOL hasSyncedTranscript;
@property (atomic, readonly) NSString *customMessage; @property (atomic, readonly) NSString *customMessage;
@ -89,27 +111,13 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
@property (atomic, readonly) TSGroupMetaMessage groupMetaMessage; @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; @property (nonatomic, readonly) BOOL isVoiceMessage;
// This property won't be accurate for legacy messages. // This property won't be accurate for legacy messages.
@property (atomic, readonly) BOOL isFromLinkedDevice; @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; @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. * The data representation of this message, to be encrypted, before being sent.
*/ */
@ -143,36 +151,70 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
- (BOOL)shouldBeSaved; - (BOOL)shouldBeSaved;
- (NSArray<NSString *> *)recipientIds;
- (NSArray<NSString *> *)sendingRecipientIds;
- (NSArray<NSString *> *)deliveredRecipientIds;
- (NSArray<NSString *> *)readRecipientIds;
- (NSUInteger)sentRecipientsCount;
- (nullable TSOutgoingMessageRecipientState *)recipientStateForRecipientId:(NSString *)recipientId;
#pragma mark - Update With... Methods #pragma mark - Update With... Methods
- (void)updateWithMessageState:(TSOutgoingMessageState)messageState; // This method is used to record a successful send to one recipient.
- (void)updateWithMessageState:(TSOutgoingMessageState)messageState - (void)updateWithSentRecipient:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction;
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;
#ifdef DEBUG
// This method is used to forge the message state for fake messages.
- (void)updateWithFakeMessageState:(TSOutgoingMessageState)messageState
transaction:(YapDatabaseReadWriteTransaction *)transaction;
#endif
// This method is used to record a failed send to all "sending" recipients.
- (void)updateWithSendingError:(NSError *)error; - (void)updateWithSendingError:(NSError *)error;
- (void)updateWithHasSyncedTranscript:(BOOL)hasSyncedTranscript - (void)updateWithHasSyncedTranscript:(BOOL)hasSyncedTranscript
transaction:(YapDatabaseReadWriteTransaction *)transaction; transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)updateWithCustomMessage:(NSString *)customMessage transaction:(YapDatabaseReadWriteTransaction *)transaction; - (void)updateWithCustomMessage:(NSString *)customMessage transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)updateWithCustomMessage:(NSString *)customMessage; - (void)updateWithCustomMessage:(NSString *)customMessage;
// This method is used to record a successful delivery to one recipient.
//
// deliveryTimestamp is an optional parameter, since legacy // deliveryTimestamp is an optional parameter, since legacy
// delivery receipts don't have a "delivery timestamp". Those // delivery receipts don't have a "delivery timestamp". Those
// messages repurpose the "timestamp" field to indicate when the // messages repurpose the "timestamp" field to indicate when the
// corresponding message was originally sent. // corresponding message was originally sent.
- (void)updateWithDeliveredToRecipientId:(NSString *)recipientId - (void)updateWithDeliveredRecipient:(NSString *)recipientId
deliveryTimestamp:(NSNumber *_Nullable)deliveryTimestamp deliveryTimestamp:(NSNumber *_Nullable)deliveryTimestamp
transaction:(YapDatabaseReadWriteTransaction *)transaction; transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)updateWithWasSentFromLinkedDeviceWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; - (void)updateWithWasSentFromLinkedDeviceWithTransaction:(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)updateWithSingleGroupRecipient:(NSString *)singleGroupRecipient - (void)updateWithSingleGroupRecipient:(NSString *)singleGroupRecipient
transaction:(YapDatabaseReadWriteTransaction *)transaction; transaction:(YapDatabaseReadWriteTransaction *)transaction;
// This method is used to record a successful "read" by one recipient.
- (void)updateWithReadRecipientId:(NSString *)recipientId - (void)updateWithReadRecipientId:(NSString *)recipientId
readTimestamp:(uint64_t)readTimestamp readTimestamp:(uint64_t)readTimestamp
transaction:(YapDatabaseReadWriteTransaction *)transaction; transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (nullable NSNumber *)firstRecipientReadTimestamp;
#pragma mark - Sent Recipients
- (NSUInteger)sentRecipientsCount; - (nullable NSNumber *)firstRecipientReadTimestamp;
- (BOOL)wasSentToRecipient:(NSString *)contactId;
- (void)updateWithSentRecipient:(NSString *)contactId transaction:(YapDatabaseReadWriteTransaction *)transaction;
@end @end

@ -21,28 +21,31 @@ NS_ASSUME_NONNULL_BEGIN
NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRecipientAll"; 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 () @interface TSOutgoingMessage ()
@property (atomic) TSOutgoingMessageState messageState;
@property (atomic) BOOL hasSyncedTranscript; @property (atomic) BOOL hasSyncedTranscript;
@property (atomic) NSString *customMessage; @property (atomic) NSString *customMessage;
@property (atomic) NSString *mostRecentFailureText; @property (atomic) NSString *mostRecentFailureText;
@property (atomic) BOOL wasDelivered;
@property (atomic) NSString *singleGroupRecipient;
@property (atomic) BOOL isFromLinkedDevice; @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) TSGroupMetaMessage groupMetaMessage;
@property (atomic) NSDictionary<NSString *, NSNumber *> *recipientDeliveryMap; @property (atomic, nullable) NSDictionary<NSString *, TSOutgoingMessageRecipientState *> *recipientStateMap;
@property (atomic) NSDictionary<NSString *, NSNumber *> *recipientReadMap;
@end @end
@ -50,8 +53,6 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
@implementation TSOutgoingMessage @implementation TSOutgoingMessage
@synthesize sentRecipients = _sentRecipients;
- (instancetype)initWithCoder:(NSCoder *)coder - (instancetype)initWithCoder:(NSCoder *)coder
{ {
self = [super initWithCoder:coder]; self = [super initWithCoder:coder];
@ -60,22 +61,95 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
if (!_attachmentFilenameMap) { if (!_attachmentFilenameMap) {
_attachmentFilenameMap = [NSMutableDictionary new]; _attachmentFilenameMap = [NSMutableDictionary new];
} }
// Migrate message state. if (!self.recipientStateMap) {
if (_messageState == TSOutgoingMessageStateSent_OBSOLETE) { [self migrateRecipientStateMapWithCoder:coder];
_messageState = TSOutgoingMessageStateSentToService; OWSAssert(self.recipientStateMap);
} else if (_messageState == TSOutgoingMessageStateDelivered_OBSOLETE) {
_messageState = TSOutgoingMessageStateSentToService;
_wasDelivered = YES;
}
if (!_sentRecipients) {
_sentRecipients = [NSArray new];
} }
} }
return self; 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.
NSArray<NSString *> *recipientIds = [self.thread 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) {
// 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];
[self updateMessageState];
}
+ (instancetype)outgoingMessageInThread:(nullable TSThread *)thread + (instancetype)outgoingMessageInThread:(nullable TSThread *)thread
messageBody:(nullable NSString *)body messageBody:(nullable NSString *)body
attachmentId:(nullable NSString *)attachmentId attachmentId:(nullable NSString *)attachmentId
@ -156,8 +230,6 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
return self; return self;
} }
_messageState = TSOutgoingMessageStateAttemptingOut;
_sentRecipients = [NSArray new];
_hasSyncedTranscript = NO; _hasSyncedTranscript = NO;
if ([thread isKindOfClass:TSGroupThread.class]) { if ([thread isKindOfClass:TSGroupThread.class]) {
@ -177,9 +249,55 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
_attachmentFilenameMap = [NSMutableDictionary new]; _attachmentFilenameMap = [NSMutableDictionary new];
NSMutableDictionary<NSString *, TSOutgoingMessageRecipientState *> *recipientStateMap = [NSMutableDictionary new];
NSArray<NSString *> *recipientIds = [self.thread recipientIdentifiers];
for (NSString *recipientId in recipientIds) {
TSOutgoingMessageRecipientState *recipientState = [TSOutgoingMessageRecipientState new];
recipientState.state = OWSOutgoingMessageRecipientStateSending;
recipientStateMap[recipientId] = recipientState;
}
self.recipientStateMap = [recipientStateMap copy];
[self updateMessageState];
return self; return self;
} }
- (void)updateMessageState
{
// self.messageState = [TSOutgoingMessage messageStateForRecipientStates:self.recipientStateMap.allValues];
}
- (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 - (BOOL)shouldBeSaved
{ {
if (self.groupMetaMessage == TSGroupMessageDeliver || self.groupMetaMessage == TSGroupMessageUnspecified) { if (self.groupMetaMessage == TSGroupMessageDeliver || self.groupMetaMessage == TSGroupMessageUnspecified) {
@ -203,23 +321,39 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
[super saveWithTransaction:transaction]; [super saveWithTransaction:transaction];
} }
- (nullable NSString *)recipientIdentifier - (OWSOutgoingMessageRecipientState)maxMessageState
{
OWSOutgoingMessageRecipientState result = OWSOutgoingMessageRecipientStateMin;
for (TSOutgoingMessageRecipientState *recipientState in self.recipientStateMap.allValues) {
result = MAX(recipientState.state, result);
}
return result;
}
- (OWSOutgoingMessageRecipientState)minMessageState
{ {
return self.thread.contactIdentifier; OWSOutgoingMessageRecipientState result = OWSOutgoingMessageRecipientStateMax;
for (TSOutgoingMessageRecipientState *recipientState in self.recipientStateMap.allValues) {
result = MIN(recipientState.state, result);
}
return result;
} }
- (BOOL)shouldStartExpireTimer:(YapDatabaseReadTransaction *)transaction - (BOOL)shouldStartExpireTimer:(YapDatabaseReadTransaction *)transaction
{ {
switch (self.messageState) { // It's not clear if we should wait until _all_ recipients have reached "sent or later"
case TSOutgoingMessageStateSentToService: // (which could never occur if one group member is unregistered) or only wait until
return self.isExpiringMessage; // the first recipient has reached "sent or later" (which could cause partially delivered
case TSOutgoingMessageStateAttemptingOut: // messages to expire). For now, we'll do the latter.
case TSOutgoingMessageStateUnsent: //
return NO; // TODO: Revisit this decision.
case TSOutgoingMessageStateSent_OBSOLETE:
case TSOutgoingMessageStateDelivered_OBSOLETE: if (!self.isExpiringMessage) {
OWSFail(@"%@ Obsolete message state.", self.logTag); return NO;
return self.isExpiringMessage; } else if (self.recipientStateMap.count < 1) {
return YES;
} else {
return self.maxMessageState >= OWSOutgoingMessageRecipientStateSent;
} }
} }
@ -233,6 +367,67 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
return OWSInteractionType_OutgoingMessage; return OWSInteractionType_OutgoingMessage;
} }
- (NSArray<NSString *> *)recipientIds
{
return [self.recipientStateMap.allKeys copy];
}
- (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;
}
#pragma mark - Update With... Methods #pragma mark - Update With... Methods
- (void)updateWithSendingError:(NSError *)error - (void)updateWithSendingError:(NSError *)error
@ -242,27 +437,49 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
[self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self applyChangeToSelfAndLatestCopy:transaction [self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) { 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]; [message setMostRecentFailureText:error.localizedDescription];
}]; }];
}]; }];
} }
- (void)updateWithMessageState:(TSOutgoingMessageState)messageState - (void)updateWithAllSendingRecipientsMarkedAsFailedWithTansaction:(YapDatabaseReadWriteTransaction *)transaction
{ {
[self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { OWSAssert(transaction);
[self updateWithMessageState:messageState transaction:transaction];
}]; [self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) {
// Mark any "sending" recipients as "failed."
for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap
.allValues) {
if (recipientState.state == OWSOutgoingMessageRecipientStateSending) {
recipientState.state = OWSOutgoingMessageRecipientStateFailed;
}
}
[message updateMessageState];
}];
} }
- (void)updateWithMessageState:(TSOutgoingMessageState)messageState - (void)updateWithMarkingAllUnsentRecipientsAsSendingWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
transaction:(YapDatabaseReadWriteTransaction *)transaction
{ {
OWSAssert(transaction); OWSAssert(transaction);
[self applyChangeToSelfAndLatestCopy:transaction [self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) { changeBlock:^(TSOutgoingMessage *message) {
[message setMessageState:messageState]; // Mark any "sending" recipients as "failed."
for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap
.allValues) {
if (recipientState.state == OWSOutgoingMessageRecipientStateFailed) {
recipientState.state = OWSOutgoingMessageRecipientStateSending;
}
}
[message updateMessageState];
}]; }];
} }
@ -293,135 +510,167 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
}]; }];
} }
- (void)updateWithDeliveredToRecipientId:(NSString *)recipientId - (void)updateWithSentRecipient:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction
deliveryTimestamp:(NSNumber *_Nullable)deliveryTimestamp
transaction:(YapDatabaseReadWriteTransaction *)transaction
{ {
OWSAssert(recipientId.length > 0); OWSAssert(recipientId.length > 0);
OWSAssert(transaction); OWSAssert(transaction);
// TODO: I suspect we're double-calling this method.
[self applyChangeToSelfAndLatestCopy:transaction [self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) { changeBlock:^(TSOutgoingMessage *message) {
TSOutgoingMessageRecipientState *_Nullable recipientState
if (deliveryTimestamp) { = message.recipientStateMap[recipientId];
NSMutableDictionary<NSString *, NSNumber *> *recipientDeliveryMap if (!recipientState) {
= (message.recipientDeliveryMap ? [message.recipientDeliveryMap mutableCopy] OWSFail(@"%@ Missing recipient state for recipient: %@", self.logTag, recipientId);
: [NSMutableDictionary new]); return;
recipientDeliveryMap[recipientId] = deliveryTimestamp;
message.recipientDeliveryMap = [recipientDeliveryMap copy];
} }
recipientState.state = OWSOutgoingMessageRecipientStateSent;
[message setWasDelivered:YES]; [message updateMessageState];
}]; }];
} }
- (void)updateWithWasSentFromLinkedDeviceWithTransaction:(YapDatabaseReadWriteTransaction *)transaction - (void)updateWithSkippedRecipient:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction
{ {
OWSAssert(recipientId.length > 0);
OWSAssert(transaction); OWSAssert(transaction);
[self applyChangeToSelfAndLatestCopy:transaction [self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) { changeBlock:^(TSOutgoingMessage *message) {
[message setMessageState:TSOutgoingMessageStateSentToService]; TSOutgoingMessageRecipientState *_Nullable recipientState
[message setWasDelivered:YES]; = message.recipientStateMap[recipientId];
[message setIsFromLinkedDevice:YES]; if (!recipientState) {
OWSFail(@"%@ Missing recipient state for recipient: %@", self.logTag, recipientId);
return;
}
recipientState.state = OWSOutgoingMessageRecipientStateSkipped;
[message updateMessageState];
}]; }];
} }
- (void)updateWithSingleGroupRecipient:(NSString *)singleGroupRecipient - (void)updateWithDeliveredRecipient:(NSString *)recipientId
transaction:(YapDatabaseReadWriteTransaction *)transaction deliveryTimestamp:(NSNumber *_Nullable)deliveryTimestamp
transaction:(YapDatabaseReadWriteTransaction *)transaction
{ {
OWSAssert(recipientId.length > 0);
OWSAssert(transaction); OWSAssert(transaction);
OWSAssert(singleGroupRecipient.length > 0);
[self applyChangeToSelfAndLatestCopy:transaction [self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) { changeBlock:^(TSOutgoingMessage *message) {
[message setSingleGroupRecipient:singleGroupRecipient]; TSOutgoingMessageRecipientState *_Nullable recipientState
= message.recipientStateMap[recipientId];
if (!recipientState) {
OWSFail(@"%@ Missing recipient state for delivered recipient: %@",
self.logTag,
recipientId);
return;
}
recipientState.state = OWSOutgoingMessageRecipientStateSent;
recipientState.deliveryTimestamp = deliveryTimestamp;
[message updateMessageState];
}]; }];
} }
#pragma mark - Sent Recipients - (void)updateWithReadRecipientId:(NSString *)recipientId
readTimestamp:(uint64_t)readTimestamp
- (NSArray<NSString *> *)sentRecipients transaction:(YapDatabaseReadWriteTransaction *)transaction
{
@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); OWSAssert(recipientId.length > 0);
OWSAssert(transaction);
return self.sentRecipients.count; [self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) {
TSOutgoingMessageRecipientState *_Nullable recipientState
= message.recipientStateMap[recipientId];
if (!recipientState) {
OWSFail(@"%@ Missing recipient state for delivered recipient: %@",
self.logTag,
recipientId);
return;
}
recipientState.state = OWSOutgoingMessageRecipientStateSent;
recipientState.readTimestamp = @(readTimestamp);
[message updateMessageState];
}];
} }
- (void)updateWithSentRecipient:(NSString *)contactId transaction:(YapDatabaseReadWriteTransaction *)transaction - (void)updateWithWasSentFromLinkedDeviceWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{ {
OWSAssert(transaction); OWSAssert(transaction);
[self applyChangeToSelfAndLatestCopy:transaction [self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) { changeBlock:^(TSOutgoingMessage *message) {
[message addSentRecipient:contactId]; // Mark any "sending" recipients as "sent."
for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap
.allValues) {
if (recipientState.state == OWSOutgoingMessageRecipientStateSending) {
recipientState.state = OWSOutgoingMessageRecipientStateSent;
}
}
[message setIsFromLinkedDevice:YES];
[message updateMessageState];
}]; }];
} }
- (void)updateWithReadRecipientId:(NSString *)recipientId - (void)updateWithSingleGroupRecipient:(NSString *)singleGroupRecipient
readTimestamp:(uint64_t)readTimestamp transaction:(YapDatabaseReadWriteTransaction *)transaction
transaction:(YapDatabaseReadWriteTransaction *)transaction
{ {
OWSAssert(recipientId.length > 0);
OWSAssert(transaction); OWSAssert(transaction);
OWSAssert(singleGroupRecipient.length > 0);
[self applyChangeToSelfAndLatestCopy:transaction [self applyChangeToSelfAndLatestCopy:transaction
changeBlock:^(TSOutgoingMessage *message) { changeBlock:^(TSOutgoingMessage *message) {
NSMutableDictionary<NSString *, NSNumber *> *recipientReadMap TSOutgoingMessageRecipientState *recipientState =
= (message.recipientReadMap ? [message.recipientReadMap mutableCopy] [TSOutgoingMessageRecipientState new];
: [NSMutableDictionary new]); recipientState.state = OWSOutgoingMessageRecipientStateSending;
recipientReadMap[recipientId] = @(readTimestamp); [message setRecipientStateMap:@{
message.recipientReadMap = [recipientReadMap copy]; singleGroupRecipient : recipientState,
}];
[message updateMessageState];
}]; }];
} }
- (nullable NSNumber *)firstRecipientReadTimestamp - (nullable NSNumber *)firstRecipientReadTimestamp
{ {
NSNumber *result = nil; NSNumber *result = nil;
for (NSNumber *timestamp in self.recipientReadMap.allValues) { for (TSOutgoingMessageRecipientState *recipientState in self.recipientStateMap.allValues) {
if (!result || (result.unsignedLongLongValue > timestamp.unsignedLongLongValue)) { if (!recipientState.readTimestamp) {
result = timestamp; continue;
}
if (!result || (result.unsignedLongLongValue > recipientState.readTimestamp.unsignedLongLongValue)) {
result = recipientState.readTimestamp;
} }
} }
return result; 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;
}
}
[message updateMessageState];
}];
}
#pragma mark - #pragma mark -
- (OWSSignalServiceProtosDataMessageBuilder *)dataMessageBuilder - (OWSSignalServiceProtosDataMessageBuilder *)dataMessageBuilder
@ -437,7 +686,7 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
[builder setBody:self.body]; [builder setBody:self.body];
} else { } else {
OWSFail(@"%@ message body length too long.", self.logTag); OWSFail(@"%@ message body length too long.", self.logTag);
NSString *truncatedBody = self.body; NSString *truncatedBody = [self.body copy];
while ([truncatedBody lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kOversizeTextMessageSizeThreshold) { while ([truncatedBody lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kOversizeTextMessageSizeThreshold) {
DDLogError(@"%@ truncating body which is too long: %tu", DDLogError(@"%@ truncating body which is too long: %tu",
self.logTag, self.logTag,

@ -44,9 +44,8 @@ static NSString *const OWSFailedMessagesJobMessageStateIndex = @"index_outoing_m
NSMutableArray<NSString *> *messageIds = [NSMutableArray new]; NSMutableArray<NSString *> *messageIds = [NSMutableArray new];
NSString *formattedString = [NSString stringWithFormat:@"WHERE %@ == %d", NSString *formattedString = [NSString
OWSFailedMessagesJobMessageStateColumn, stringWithFormat:@"WHERE %@ == %d", OWSFailedMessagesJobMessageStateColumn, (int)TSOutgoingMessageStateSending];
(int)TSOutgoingMessageStateAttemptingOut];
YapDatabaseQuery *query = [YapDatabaseQuery queryWithFormat:formattedString]; YapDatabaseQuery *query = [YapDatabaseQuery queryWithFormat:formattedString];
[[transaction ext:OWSFailedMessagesJobMessageStateIndex] [[transaction ext:OWSFailedMessagesJobMessageStateIndex]
enumerateKeysMatchingQuery:query enumerateKeysMatchingQuery:query
@ -83,8 +82,8 @@ static NSString *const OWSFailedMessagesJobMessageStateIndex = @"index_outoing_m
readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
[self enumerateAttemptingOutMessagesWithBlock:^(TSOutgoingMessage *message) { [self enumerateAttemptingOutMessagesWithBlock:^(TSOutgoingMessage *message) {
// sanity check // sanity check
OWSAssert(message.messageState == TSOutgoingMessageStateAttemptingOut); OWSAssert(message.messageState == TSOutgoingMessageStateSending);
if (message.messageState != TSOutgoingMessageStateAttemptingOut) { if (message.messageState != TSOutgoingMessageStateSending) {
DDLogError(@"%@ Refusing to mark as unsent message with state: %d", DDLogError(@"%@ Refusing to mark as unsent message with state: %d",
self.logTag, self.logTag,
(int)message.messageState); (int)message.messageState);
@ -92,8 +91,8 @@ static NSString *const OWSFailedMessagesJobMessageStateIndex = @"index_outoing_m
} }
DDLogDebug(@"%@ marking message as unsent: %@", self.logTag, message.uniqueId); DDLogDebug(@"%@ marking message as unsent: %@", self.logTag, message.uniqueId);
[message updateWithMessageState:TSOutgoingMessageStateUnsent transaction:transaction]; [message updateWithAllSendingRecipientsMarkedAsFailedWithTansaction:transaction];
OWSAssert(message.messageState == TSOutgoingMessageStateUnsent); OWSAssert(message.messageState == TSOutgoingMessageStateFailed);
count++; count++;
} }

@ -262,9 +262,9 @@ NS_ASSUME_NONNULL_BEGIN
timestamp); timestamp);
} }
for (TSOutgoingMessage *outgoingMessage in messages) { for (TSOutgoingMessage *outgoingMessage in messages) {
[outgoingMessage updateWithDeliveredToRecipientId:recipientId [outgoingMessage updateWithDeliveredRecipient:recipientId
deliveryTimestamp:deliveryTimestamp deliveryTimestamp:deliveryTimestamp
transaction:transaction]; transaction:transaction];
} }
} }
} }

@ -186,7 +186,8 @@ void AssertIsOnSendingQueue()
- (void)didSucceed - (void)didSucceed
{ {
[self.message updateWithMessageState:TSOutgoingMessageStateSentToService]; OWSAssert(self.message.messageState == TSOutgoingMessageStateSent);
self.successHandler(); self.successHandler();
} }
@ -311,7 +312,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// All outgoing messages should be saved at the time they are enqueued. // All outgoing messages should be saved at the time they are enqueued.
[message saveWithTransaction:transaction]; [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]; 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]; NSMutableArray<SignalRecipient *> *recipients = [NSMutableArray new];
for (NSString *recipientId in identifiers) { for (NSString *recipientId in recipientIds) {
SignalRecipient *existingRecipient = [SignalRecipient recipientWithTextSecureIdentifier:recipientId]; SignalRecipient *existingRecipient = [SignalRecipient recipientWithTextSecureIdentifier:recipientId];
if (existingRecipient) { if (existingRecipient) {
@ -451,11 +457,38 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
TSThread *thread = message.thread; TSThread *thread = message.thread;
if ([thread isKindOfClass:[TSGroupThread class]]) { if ([thread isKindOfClass:[TSGroupThread class]]) {
TSGroupThread *gThread = (TSGroupThread *)thread; 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".
//
// TODO: We should also mark as "skipped" group members who no longer have signal accounts.
[message updateWithSkippedRecipient:recipientId transaction:transaction];
}
}];
}
NSMutableSet<NSString *> *sendingRecipientIds = [NSMutableSet setWithArray:message.sendingRecipientIds];
[sendingRecipientIds intersectSet:[NSSet setWithArray:gThread.groupModel.groupMemberIds]];
NSError *error; NSError *error;
NSArray<SignalRecipient *> *recipients = NSArray<SignalRecipient *> *recipients =
[self getRecipients:gThread.groupModel.groupMemberIds error:&error]; [self getRecipientsForRecipientIds:sendingRecipientIds.allObjects error:&error];
if (recipients.count == 0) { if (recipients.count == 0) {
if (!error) { if (!error) {
@ -491,7 +524,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// you might, for example, have a pending outgoing message when // you might, for example, have a pending outgoing message when
// you block them. // you block them.
OWSAssert(recipientContactId.length > 0); OWSAssert(recipientContactId.length > 0);
if ([_blockingManager isRecipientIdBlocked:recipientContactId]) { if ([self.blockingManager isRecipientIdBlocked:recipientContactId]) {
DDLogInfo(@"%@ skipping 1:1 send to blocked contact: %@", self.logTag, recipientContactId); DDLogInfo(@"%@ skipping 1:1 send to blocked contact: %@", self.logTag, recipientContactId);
NSError *error = OWSErrorMakeMessageSendFailedToBlockListError(); NSError *error = OWSErrorMakeMessageSendFailedToBlockListError();
// No need to retry - the user will continue to be blocked. // No need to retry - the user will continue to be blocked.
@ -562,6 +595,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
success:^{ success:^{
DDLogInfo(@"%@ Marking group message as sent to recipient: %@", self.logTag, recipient.uniqueId); DDLogInfo(@"%@ Marking group message as sent to recipient: %@", self.logTag, recipient.uniqueId);
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
// Mark this recipient as "sent".
[message updateWithSentRecipient:recipient.uniqueId transaction:transaction]; [message updateWithSentRecipient:recipient.uniqueId transaction:transaction];
}]; }];
[futureSource trySetResult:@1]; [futureSource trySetResult:@1];
@ -589,12 +623,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
if ([recipientId isEqualToString:[TSAccountManager localNumber]]) { if ([recipientId isEqualToString:[TSAccountManager localNumber]]) {
continue; continue;
} }
// We don't need to sent the message to all group members if if (![message.sendingRecipientIds containsObject:recipientId]) {
// 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 // Skip recipients we have already sent this message to (on an
// earlier retry, perhaps). // earlier retry, perhaps).
DDLogInfo(@"%@ Skipping group message recipient; already sent: %@", self.logTag, recipient.uniqueId); DDLogInfo(@"%@ Skipping group message recipient; already sent: %@", self.logTag, recipient.uniqueId);
@ -904,7 +933,11 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
} }
dispatch_async([OWSDispatch sendingQueue], ^{ dispatch_async([OWSDispatch sendingQueue], ^{
[recipient save]; [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[recipient saveWithTransaction:transaction];
[message updateWithSentRecipient:recipient.uniqueId transaction:transaction];
}];
[self handleMessageSentLocally:message]; [self handleMessageSentLocally:message];
successHandler(); successHandler();
}); });

Loading…
Cancel
Save