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

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

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

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

@ -212,7 +212,9 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
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)
guard recipientStatus == recipientStatusGroup else {
@ -560,7 +562,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.")
}
}

@ -13,6 +13,7 @@ import SignalMessaging
case delivered
case read
case failed
case skipped
}
// 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
// might be misleading.
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)
guard let recipientState = outgoingMessage.recipientState(forRecipientId: recipientId) else {
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)
}
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)
}
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)
case .sending:
if outgoingMessage.hasAttachments() {
assert(outgoingMessage.messageState == .sending)
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)
}
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)
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")
return (status:.failed, shortStatusMessage:shortStatusMessage, longStatusMessage:longStatusMessage)
} else if outgoingMessage.messageState == .sentToService ||
outgoingMessage.wasSent(toRecipient:recipientId) {
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.")
return (status:.skipped, shortStatusMessage:statusMessage, longStatusMessage:statusMessage)
}
}
@ -153,40 +154,31 @@ class MessageRecipientStatusUtils: NSObject {
referenceView: UIView) -> 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 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")
comment: "message footer while attachment is uploading")
} else {
return NSLocalizedString("MESSAGE_STATUS_SENDING",
comment:"message status while message is 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 NSLocalizedString("MESSAGE_STATUS_READ", comment: "message footer for read messages")
}
let recipientDeliveryMap = outgoingMessage.recipientDeliveryMap
if recipientDeliveryMap.count > 0 {
if outgoingMessage.deliveredRecipientIds().count > 0 {
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",
comment:"message footer for sent messages")
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")
comment: "message footer for sent messages")
}
}
@ -194,26 +186,19 @@ class MessageRecipientStatusUtils: NSObject {
// See comments above.
public class func recipientStatus(outgoingMessage: TSOutgoingMessage) -> MessageRecipientStatus {
switch outgoingMessage.messageState {
case .unsent:
case .failed:
return .failed
case .attemptingOut:
case .sending:
if outgoingMessage.hasAttachments() {
return .uploading
} else {
return .sending
}
case .sentToService:
let recipientReadMap = outgoingMessage.recipientReadMap
if recipientReadMap.count > 0 {
case .sent:
if outgoingMessage.readRecipientIds().count > 0 {
return .read
}
let recipientDeliveryMap = outgoingMessage.recipientDeliveryMap
if recipientDeliveryMap.count > 0 {
return .delivered
}
if outgoingMessage.wasDelivered {
if outgoingMessage.deliveredRecipientIds().count > 0 {
return .delivered
}

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

@ -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 "OWSSignalServiceProtos.pb.h"
#import "TSOutgoingMessage.h"
#import "TSThread.h"
NS_ASSUME_NONNULL_BEGIN
@ -48,8 +49,8 @@ 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]];
[sentBuilder setDestination:self.recipientIdentifierForMessage];
[sentBuilder setMessage:[self.message buildDataMessage:self.recipientIdentifierForMessage]];
[sentBuilder setExpirationStartTimestamp:self.message.timestamp];
[syncMessageBuilder setSentBuilder:sentBuilder];
@ -57,6 +58,12 @@ NS_ASSUME_NONNULL_BEGIN
return syncMessageBuilder;
}
// Note that this will return nil for group messages.
- (nullable NSString *)recipientIdentifierForMessage
{
return self.message.thread.contactIdentifier;
}
@end
NS_ASSUME_NONNULL_END

@ -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,16 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
@class OWSSignalServiceProtosDataMessageBuilder;
@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
- (instancetype)initMessageWithTimestamp:(uint64_t)timestamp
@ -75,11 +101,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 +111,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,36 +151,70 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
- (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
- (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;
#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)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;
// 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
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;
- (nullable NSNumber *)firstRecipientReadTimestamp;
@end

@ -21,28 +21,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 +53,6 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
@implementation TSOutgoingMessage
@synthesize sentRecipients = _sentRecipients;
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
@ -60,22 +61,95 @@ 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.
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
messageBody:(nullable NSString *)body
attachmentId:(nullable NSString *)attachmentId
@ -156,8 +230,6 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
return self;
}
_messageState = TSOutgoingMessageStateAttemptingOut;
_sentRecipients = [NSArray new];
_hasSyncedTranscript = NO;
if ([thread isKindOfClass:TSGroupThread.class]) {
@ -177,9 +249,55 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
_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;
}
- (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
{
if (self.groupMetaMessage == TSGroupMessageDeliver || self.groupMetaMessage == TSGroupMessageUnspecified) {
@ -203,23 +321,39 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
[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
{
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 if (self.recipientStateMap.count < 1) {
return YES;
} else {
return self.maxMessageState >= OWSOutgoingMessageRecipientStateSent;
}
}
@ -233,6 +367,67 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
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
- (void)updateWithSendingError:(NSError *)error
@ -242,27 +437,49 @@ 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
- (void)updateWithAllSendingRecipientsMarkedAsFailedWithTansaction:(YapDatabaseReadWriteTransaction *)transaction
{
[self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self updateWithMessageState:messageState transaction: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 == OWSOutgoingMessageRecipientStateSending) {
recipientState.state = OWSOutgoingMessageRecipientStateFailed;
}
}
[message updateMessageState];
}];
}
- (void)updateWithMessageState:(TSOutgoingMessageState)messageState
transaction:(YapDatabaseReadWriteTransaction *)transaction
- (void)updateWithMarkingAllUnsentRecipientsAsSendingWithTransaction:(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 == OWSOutgoingMessageRecipientStateFailed) {
recipientState.state = OWSOutgoingMessageRecipientStateSending;
}
}
[message updateMessageState];
}];
}
@ -293,135 +510,167 @@ 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);
// TODO: I suspect we're double-calling this method.
[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;
[message updateMessageState];
}];
}
- (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;
[message updateMessageState];
}];
}
- (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];
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
- (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
- (void)updateWithReadRecipientId:(NSString *)recipientId
readTimestamp:(uint64_t)readTimestamp
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
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);
[self applyChangeToSelfAndLatestCopy:transaction
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
readTimestamp:(uint64_t)readTimestamp
transaction:(YapDatabaseReadWriteTransaction *)transaction
- (void)updateWithSingleGroupRecipient:(NSString *)singleGroupRecipient
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(recipientId.length > 0);
OWSAssert(transaction);
OWSAssert(singleGroupRecipient.length > 0);
[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 *recipientState =
[TSOutgoingMessageRecipientState new];
recipientState.state = OWSOutgoingMessageRecipientStateSending;
[message setRecipientStateMap:@{
singleGroupRecipient : recipientState,
}];
[message updateMessageState];
}];
}
- (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;
}
}
[message updateMessageState];
}];
}
#pragma mark -
- (OWSSignalServiceProtosDataMessageBuilder *)dataMessageBuilder
@ -437,7 +686,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,

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

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

@ -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,38 @@ 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".
//
// 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;
NSArray<SignalRecipient *> *recipients =
[self getRecipients:gThread.groupModel.groupMemberIds error:&error];
[self getRecipientsForRecipientIds:sendingRecipientIds.allObjects error:&error];
if (recipients.count == 0) {
if (!error) {
@ -491,7 +524,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.
@ -562,6 +595,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
success:^{
DDLogInfo(@"%@ Marking group message as sent to recipient: %@", self.logTag, recipient.uniqueId);
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
// Mark this recipient as "sent".
[message updateWithSentRecipient:recipient.uniqueId transaction:transaction];
}];
[futureSource trySetResult:@1];
@ -589,12 +623,7 @@ 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]) {
if (![message.sendingRecipientIds containsObject: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);
@ -904,7 +933,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();
});

Loading…
Cancel
Save