diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSExpirationTimerView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSExpirationTimerView.m index 15532f8da..4cd15ca41 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSExpirationTimerView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSExpirationTimerView.m @@ -159,8 +159,8 @@ const CGFloat kExpirationTimerViewSize = 16.f; OWSFail(@"initialDurationSeconds was unexpectedly 0"); return; } - - CGFloat ratioRemaining = (CGFloat)timeUntilFlashing / (CGFloat)self.initialDurationSeconds; + + CGFloat ratioRemaining = (CGFloat)secondsLeft / (CGFloat)self.initialDurationSeconds; CGFloat ratioComplete = CGFloatClamp((CGFloat)1.0 - ratioRemaining, 0, 1.0); CGPoint startPosition = CGPointMake(0, self.fullHourglassImageView.height * ratioComplete); diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index e38bafbf9..9e5aaad63 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -3278,7 +3278,7 @@ NS_ASSUME_NONNULL_BEGIN attachmentIds:@[] expiresInSeconds:0 quotedMessage:nil]; - [message markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:NO]; + [message markAsReadNowWithSendReadReceipt:NO transaction:transaction]; break; } case 1: { @@ -3316,7 +3316,7 @@ NS_ASSUME_NONNULL_BEGIN ] expiresInSeconds:0 quotedMessage:nil]; - [message markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:NO]; + [message markAsReadNowWithSendReadReceipt:NO transaction:transaction]; break; } case 3: { @@ -3767,7 +3767,7 @@ NS_ASSUME_NONNULL_BEGIN attachmentIds:[NSMutableArray new] expiresInSeconds:0 quotedMessage:nil]; - [message markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:NO]; + [message markAsReadNowWithSendReadReceipt:NO transaction:transaction]; } { TSOutgoingMessage *message = @@ -4105,7 +4105,7 @@ NS_ASSUME_NONNULL_BEGIN attachmentIds:attachmentIds expiresInSeconds:0 quotedMessage:quotedMessage]; - [message markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:NO]; + [message markAsReadNowWithSendReadReceipt:NO transaction:transaction]; return message; } diff --git a/SignalMessaging/utils/ThreadUtil.h b/SignalMessaging/utils/ThreadUtil.h index e6cefa0d6..130fbf492 100644 --- a/SignalMessaging/utils/ThreadUtil.h +++ b/SignalMessaging/utils/ThreadUtil.h @@ -26,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable, readonly) NSNumber *unreadIndicatorPosition; // If there are unseen messages in the thread, this is the timestamp -// of the oldest unseen messaage. +// of the oldest unseen message. // // Once we enter messages view, we mark all messages read, so we need // a snapshot of what the first unread message was when we entered the diff --git a/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.h b/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.h index dc3bdbf35..efd28287f 100644 --- a/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.h +++ b/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.h @@ -8,6 +8,8 @@ NS_ASSUME_NONNULL_BEGIN #define OWSDisappearingMessagesConfigurationDefaultExpirationDuration kDayInterval +@class YapDatabaseReadTransaction; + @interface OWSDisappearingMessagesConfiguration : TSYapDatabaseObject - (instancetype)initDefaultWithThreadId:(NSString *)threadId; @@ -21,7 +23,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) BOOL dictionaryValueDidChange; @property (readonly, getter=isNewRecord) BOOL newRecord; -+ (instancetype)fetchOrCreateDefaultWithThreadId:(NSString *)threadId; ++ (instancetype)fetchOrCreateDefaultWithThreadId:(NSString *)threadId + transaction:(YapDatabaseReadTransaction *)transaction; + (NSArray *)validDurationsSeconds; diff --git a/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.m b/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.m index 0ca190426..4fa0f343e 100644 --- a/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.m +++ b/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.m @@ -51,8 +51,10 @@ NS_ASSUME_NONNULL_BEGIN } + (instancetype)fetchOrCreateDefaultWithThreadId:(NSString *)threadId + transaction:(YapDatabaseReadTransaction *)transaction { - OWSDisappearingMessagesConfiguration *savedConfiguration = [self fetchObjectWithUniqueID:threadId]; + OWSDisappearingMessagesConfiguration *savedConfiguration = + [self fetchObjectWithUniqueID:threadId transaction:transaction]; if (savedConfiguration) { return savedConfiguration; } else { diff --git a/SignalServiceKit/src/Contacts/TSThread.m b/SignalServiceKit/src/Contacts/TSThread.m index b4798cdfa..f9cc5fe4c 100644 --- a/SignalServiceKit/src/Contacts/TSThread.m +++ b/SignalServiceKit/src/Contacts/TSThread.m @@ -3,6 +3,7 @@ // #import "TSThread.h" +#import "NSDate+OWS.h" #import "OWSPrimaryStorage.h" #import "OWSReadTracking.h" #import "TSDatabaseView.h" @@ -230,7 +231,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction { for (id message in [self unseenMessagesWithTransaction:transaction]) { - [message markAsReadWithTransaction:transaction sendReadReceipt:YES updateExpiration:YES]; + [message markAsReadAtTimestamp:[NSDate ows_millisecondTimeStamp] sendReadReceipt:YES transaction:transaction]; } // Just to be defensive, we'll also check for unread messages. diff --git a/SignalServiceKit/src/Devices/OWSLinkedDeviceReadReceipt.h b/SignalServiceKit/src/Devices/OWSLinkedDeviceReadReceipt.h index 8a4728c56..eb5d5f79a 100644 --- a/SignalServiceKit/src/Devices/OWSLinkedDeviceReadReceipt.h +++ b/SignalServiceKit/src/Devices/OWSLinkedDeviceReadReceipt.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "TSYapDatabaseObject.h" @@ -9,12 +9,15 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSLinkedDeviceReadReceipt : TSYapDatabaseObject @property (nonatomic, readonly) NSString *senderId; -@property (nonatomic, readonly) uint64_t timestamp; +@property (nonatomic, readonly) uint64_t messageIdTimestamp; +@property (nonatomic, readonly) uint64_t readTimestamp; -- (instancetype)initWithSenderId:(NSString *)senderId timestamp:(uint64_t)timestamp; +- (instancetype)initWithSenderId:(NSString *)senderId + messageIdTimestamp:(uint64_t)messageIdtimestamp + readTimestamp:(uint64_t)readTimestamp; + (nullable OWSLinkedDeviceReadReceipt *)findLinkedDeviceReadReceiptWithSenderId:(NSString *)senderId - timestamp:(uint64_t)timestamp + messageIdTimestamp:(uint64_t)messageIdTimestamp transaction: (YapDatabaseReadTransaction *)transaction; diff --git a/SignalServiceKit/src/Devices/OWSLinkedDeviceReadReceipt.m b/SignalServiceKit/src/Devices/OWSLinkedDeviceReadReceipt.m index 8a402067a..7dfa06420 100644 --- a/SignalServiceKit/src/Devices/OWSLinkedDeviceReadReceipt.m +++ b/SignalServiceKit/src/Devices/OWSLinkedDeviceReadReceipt.m @@ -8,37 +8,67 @@ NS_ASSUME_NONNULL_BEGIN @implementation OWSLinkedDeviceReadReceipt -- (instancetype)initWithSenderId:(NSString *)senderId timestamp:(uint64_t)timestamp +- (instancetype)initWithSenderId:(NSString *)senderId + messageIdTimestamp:(uint64_t)messageIdTimestamp + readTimestamp:(uint64_t)readTimestamp { - OWSAssert(senderId.length > 0 && timestamp > 0); + OWSAssert(senderId.length > 0 && messageIdTimestamp > 0); - self = [super initWithUniqueId:[OWSLinkedDeviceReadReceipt uniqueIdForSenderId:senderId timestamp:timestamp]]; + NSString *receiptId = + [OWSLinkedDeviceReadReceipt uniqueIdForSenderId:senderId messageIdTimestamp:messageIdTimestamp]; + self = [super initWithUniqueId:receiptId]; if (!self) { return self; } _senderId = senderId; - _timestamp = timestamp; + _messageIdTimestamp = messageIdTimestamp; + _readTimestamp = readTimestamp; return self; } -+ (NSString *)uniqueIdForSenderId:(NSString *)senderId timestamp:(uint64_t)timestamp +- (nullable instancetype)initWithCoder:(NSCoder *)coder { - OWSAssert(senderId.length > 0 && timestamp > 0); + self = [super initWithCoder:coder]; + if (!self) { + return self; + } + + // renamed timestamp -> messageIdTimestamp + if (!_messageIdTimestamp) { + NSNumber *_Nullable legacyTimestamp = (NSNumber *)[coder decodeObjectForKey:@"timestamp"]; + OWSAssert(legacyTimestamp.unsignedLongLongValue > 0); + _messageIdTimestamp = legacyTimestamp.unsignedLongLongValue; + } + + // For legacy objects, before we were tracking read time, use the original messages "sent" timestamp + // as the local read time. This will always be at least a little bit earlier than the message was + // actually read, which isn't ideal, but safer than persisting a disappearing message too long, especially + // since we know they read it on their linked desktop. + if (_readTimestamp == 0) { + _readTimestamp = _messageIdTimestamp; + } - return [NSString stringWithFormat:@"%@-%llu", senderId, timestamp]; + return self; +} + ++ (NSString *)uniqueIdForSenderId:(NSString *)senderId messageIdTimestamp:(uint64_t)messageIdTimestamp +{ + OWSAssert(senderId.length > 0 && messageIdTimestamp > 0); + + return [NSString stringWithFormat:@"%@-%llu", senderId, messageIdTimestamp]; } + (nullable OWSLinkedDeviceReadReceipt *)findLinkedDeviceReadReceiptWithSenderId:(NSString *)senderId - timestamp:(uint64_t)timestamp + messageIdTimestamp:(uint64_t)messageIdTimestamp transaction: (YapDatabaseReadTransaction *)transaction { OWSAssert(transaction); - - return [OWSLinkedDeviceReadReceipt fetchObjectWithUniqueID:[self uniqueIdForSenderId:senderId timestamp:timestamp] - transaction:transaction]; + NSString *receiptId = + [OWSLinkedDeviceReadReceipt uniqueIdForSenderId:senderId messageIdTimestamp:messageIdTimestamp]; + return [OWSLinkedDeviceReadReceipt fetchObjectWithUniqueID:receiptId transaction:transaction]; } @end diff --git a/SignalServiceKit/src/Devices/OWSReadReceiptsForLinkedDevicesMessage.m b/SignalServiceKit/src/Devices/OWSReadReceiptsForLinkedDevicesMessage.m index a72f10257..8ffde41e1 100644 --- a/SignalServiceKit/src/Devices/OWSReadReceiptsForLinkedDevicesMessage.m +++ b/SignalServiceKit/src/Devices/OWSReadReceiptsForLinkedDevicesMessage.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSReadReceiptsForLinkedDevicesMessage.h" @@ -35,7 +35,7 @@ NS_ASSUME_NONNULL_BEGIN OWSSignalServiceProtosSyncMessageReadBuilder *readProtoBuilder = [OWSSignalServiceProtosSyncMessageReadBuilder new]; [readProtoBuilder setSender:readReceipt.senderId]; - [readProtoBuilder setTimestamp:readReceipt.timestamp]; + [readProtoBuilder setTimestamp:readReceipt.messageIdTimestamp]; [syncMessageBuilder addRead:[readProtoBuilder build]]; } diff --git a/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.m b/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.m index 72a2177dc..999b4d508 100644 --- a/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.m +++ b/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.m @@ -134,9 +134,13 @@ NS_ASSUME_NONNULL_BEGIN } if (transcript.isExpirationTimerUpdate) { - [OWSDisappearingMessagesJob becomeConsistentWithConfigurationForMessage:outgoingMessage - contactsManager:self.contactsManager]; + [[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithConfigurationForMessage:outgoingMessage + contactsManager:self.contactsManager + transaction:transaction]; // early return to avoid saving an empty incoming message. + OWSAssert(transcript.body.length == 0); + OWSAssert(outgoingMessage.attachmentIds.count == 0); + return; } @@ -147,10 +151,12 @@ NS_ASSUME_NONNULL_BEGIN [outgoingMessage saveWithTransaction:transaction]; [outgoingMessage updateWithWasSentFromLinkedDeviceWithTransaction:transaction]; - [OWSDisappearingMessagesJob becomeConsistentWithConfigurationForMessage:outgoingMessage - contactsManager:self.contactsManager]; - [OWSDisappearingMessagesJob setExpirationForMessage:outgoingMessage - expirationStartedAt:transcript.expirationStartedAt]; + [[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithConfigurationForMessage:outgoingMessage + contactsManager:self.contactsManager + transaction:transaction]; + [[OWSDisappearingMessagesJob sharedJob] setExpirationForMessage:outgoingMessage + expirationStartedAt:transcript.expirationStartedAt + transaction:transaction]; [self.readReceiptManager applyEarlyReadReceiptsForOutgoingMessageFromLinkedDevice:outgoingMessage transaction:transaction]; diff --git a/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.m b/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.m index 12b7006d2..1391aa271 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.m @@ -49,6 +49,13 @@ NS_ASSUME_NONNULL_BEGIN return self; } +- (BOOL)shouldUseReceiptDateForSorting +{ + // Use the timestamp, not the "received at" timestamp to sort, + // since we're creating these interactions after the fact and back-dating them. + return NO; +} + - (NSString *)description { if (self.createdByRemoteName) { diff --git a/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.m b/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.m index 497aca868..363605a73 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.m @@ -179,14 +179,19 @@ NSUInteger TSErrorMessageSchemaVersion = 1; #pragma mark - OWSReadTracking +- (uint64_t)expireStartedAt +{ + return 0; +} + - (BOOL)shouldAffectUnreadCounts { return NO; } -- (void)markAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction - sendReadReceipt:(BOOL)sendReadReceipt - updateExpiration:(BOOL)updateExpiration +- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp + sendReadReceipt:(BOOL)sendReadReceipt + transaction:(YapDatabaseReadWriteTransaction *)transaction { OWSAssert(transaction); @@ -200,7 +205,7 @@ NSUInteger TSErrorMessageSchemaVersion = 1; [self saveWithTransaction:transaction]; [self touchThreadWithTransaction:transaction]; - // Ignore sendReadReceipt and updateExpiration; they don't apply to error messages. + // Ignore sendReadReceipt - it doesn't apply to error messages. } @end diff --git a/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage.h b/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage.h index 823b22515..3e5f49fdc 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage.h @@ -74,6 +74,10 @@ NS_ASSUME_NONNULL_BEGIN - (NSString *)messageAuthorId; +// convenience method for expiring a message which was just read +- (void)markAsReadNowWithSendReadReceipt:(BOOL)sendReadReceipt + transaction:(YapDatabaseReadWriteTransaction *)transaction; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage.m b/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage.m index 87209f0a1..97944bc4f 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSIncomingMessage.m @@ -3,6 +3,7 @@ // #import "TSIncomingMessage.h" +#import "NSDate+OWS.h" #import "NSNotificationCenter+OWS.h" #import "OWSDisappearingMessagesConfiguration.h" #import "OWSDisappearingMessagesJob.h" @@ -134,25 +135,37 @@ NS_ASSUME_NONNULL_BEGIN return YES; } -- (void)markAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction - sendReadReceipt:(BOOL)sendReadReceipt - updateExpiration:(BOOL)updateExpiration +- (void)markAsReadNowWithSendReadReceipt:(BOOL)sendReadReceipt + transaction:(YapDatabaseReadWriteTransaction *)transaction; +{ + [self markAsReadAtTimestamp:[NSDate ows_millisecondTimeStamp] + sendReadReceipt:sendReadReceipt + transaction:transaction]; +} + +- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp + sendReadReceipt:(BOOL)sendReadReceipt + transaction:(YapDatabaseReadWriteTransaction *)transaction; { OWSAssert(transaction); - if (_read) { + if (_read && readTimestamp <= self.expireStartedAt) { return; } - DDLogDebug( - @"%@ marking as read uniqueId: %@ which has timestamp: %llu", self.logTag, self.uniqueId, self.timestamp); + NSTimeInterval secondsAgoRead = ([NSDate ows_millisecondTimeStamp] - readTimestamp) / 1000; + DDLogDebug(@"%@ marking uniqueId: %@ which has timestamp: %llu as read: %f seconds ago", + self.logTag, + self.uniqueId, + self.timestamp, + secondsAgoRead); _read = YES; [self saveWithTransaction:transaction]; [self touchThreadWithTransaction:transaction]; - if (updateExpiration) { - [OWSDisappearingMessagesJob setExpirationForMessage:self]; - } + [[OWSDisappearingMessagesJob sharedJob] startAnyExpirationForMessage:self + expirationStartedAt:readTimestamp + transaction:transaction]; if (sendReadReceipt) { [OWSReadReceiptManager.sharedManager messageWasReadLocally:self]; diff --git a/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.m b/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.m index 4c53c8cc5..10abeb18b 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.m @@ -129,9 +129,14 @@ NSUInteger TSInfoMessageSchemaVersion = 1; return NO; } -- (void)markAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction - sendReadReceipt:(BOOL)sendReadReceipt - updateExpiration:(BOOL)updateExpiration +- (uint64_t)expireStartedAt +{ + return 0; +} + +- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp + sendReadReceipt:(BOOL)sendReadReceipt + transaction:(YapDatabaseReadWriteTransaction *)transaction { OWSAssert(transaction); @@ -145,7 +150,7 @@ NSUInteger TSInfoMessageSchemaVersion = 1; [self saveWithTransaction:transaction]; [self touchThreadWithTransaction:transaction]; - // Ignore sendReadReceipt and updateExpiration; they don't apply to info messages. + // Ignore sendReadReceipt, it doesn't apply to info messages. } @end diff --git a/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.h b/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.h index 9b6820a89..032c61add 100644 --- a/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.h +++ b/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.h @@ -7,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN @class OWSPrimaryStorage; @class TSMessage; @class TSThread; +@class YapDatabaseReadWriteTransaction; @protocol ContactsManagerProtocol; @@ -16,9 +17,13 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; -+ (void)setExpirationsForThread:(TSThread *)thread; -+ (void)setExpirationForMessage:(TSMessage *)message; -+ (void)setExpirationForMessage:(TSMessage *)message expirationStartedAt:(uint64_t)expirationStartedAt; +- (void)startAnyExpirationForMessage:(TSMessage *)message + expirationStartedAt:(uint64_t)expirationStartedAt + transaction:(YapDatabaseReadWriteTransaction *_Nonnull)transaction; + +- (void)setExpirationForMessage:(TSMessage *)message + expirationStartedAt:(uint64_t)expirationStartedAt + transaction:(YapDatabaseReadWriteTransaction *_Nonnull)transaction; /** * Synchronize our disappearing messages settings with that of the given message. Useful so we can @@ -31,8 +36,9 @@ NS_ASSUME_NONNULL_BEGIN * @param contactsManager * Provides the contact name responsible for any configuration changes in an info message. */ -+ (void)becomeConsistentWithConfigurationForMessage:(TSMessage *)message - contactsManager:(id)contactsManager; +- (void)becomeConsistentWithConfigurationForMessage:(TSMessage *)message + contactsManager:(id)contactsManager + transaction:(YapDatabaseReadWriteTransaction *)transaction; // Clean up any messages that expired since last launch immediately // and continue cleaning in the background. diff --git a/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.m b/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.m index 3f7e83eb5..a68437b23 100644 --- a/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.m +++ b/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.m @@ -17,6 +17,7 @@ #import "TSMessage.h" NS_ASSUME_NONNULL_BEGIN + // Can we move to Signal-iOS? @interface OWSDisappearingMessagesJob () @@ -24,13 +25,25 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) OWSDisappearingMessagesFinder *disappearingMessagesFinder; ++ (dispatch_queue_t)serialQueue; + // These three properties should only be accessed on the main thread. @property (nonatomic) BOOL hasStarted; -@property (nonatomic, nullable) NSTimer *timer; -@property (nonatomic, nullable) NSDate *timerScheduleDate; +@property (nonatomic, nullable) NSTimer *nextDisappearanceTimer; +@property (nonatomic, nullable) NSDate *nextDisappearanceDate; +@property (nonatomic, nullable) NSTimer *fallbackTimer; @end +void AssertIsOnDisappearingMessagesQueue() +{ +#ifdef DEBUG + if (@available(iOS 10.0, *)) { + dispatch_assert_queue(OWSDisappearingMessagesJob.serialQueue); + } +#endif +} + #pragma mark - @implementation OWSDisappearingMessagesJob @@ -55,6 +68,18 @@ NS_ASSUME_NONNULL_BEGIN _databaseConnection = primaryStorage.newDatabaseConnection; _disappearingMessagesFinder = [OWSDisappearingMessagesFinder new]; + // suspenders in case a deletion schedule is missed. + NSTimeInterval kFallBackTimerInterval = 5 * kMinuteInterval; + [AppReadiness runNowOrWhenAppIsReady:^{ + if (CurrentAppContext().isMainApp) { + self.fallbackTimer = [NSTimer weakScheduledTimerWithTimeInterval:kFallBackTimerInterval + target:self + selector:@selector(fallbackTimerDidFire) + userInfo:nil + repeats:YES]; + } + }]; + OWSSingletonAssert(); [[NSNotificationCenter defaultCenter] addObserver:self @@ -84,96 +109,79 @@ NS_ASSUME_NONNULL_BEGIN return queue; } -// This method should only be called on the serialQueue. -- (void)run +- (NSUInteger)deleteExpiredMessages { + AssertIsOnDisappearingMessagesQueue(); + uint64_t now = [NSDate ows_millisecondTimeStamp]; OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; - __block uint expirationCount = 0; + __block NSUInteger expirationCount = 0; [self.databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { [self.disappearingMessagesFinder enumerateExpiredMessagesWithBlock:^(TSMessage *message) { // sanity check if (message.expiresAt > now) { - DDLogError( + OWSFail( @"%@ Refusing to remove message which doesn't expire until: %lld", self.logTag, message.expiresAt); return; } - DDLogDebug(@"%@ Removing message which expired at: %lld", self.logTag, message.expiresAt); + DDLogInfo(@"%@ Removing message which expired at: %lld", self.logTag, message.expiresAt); [message removeWithTransaction:transaction]; expirationCount++; } transaction:transaction]; }]; - DDLogDebug(@"%@ Removed %u expired messages", self.logTag, expirationCount); + DDLogDebug(@"%@ Removed %tu expired messages", self.logTag, expirationCount); backgroundTask = nil; + return expirationCount; } -// This method should only be called on the serialQueue. -- (void)runLoop +// deletes any expired messages and schedules the next run. +- (NSUInteger)runLoop { - DDLogVerbose(@"%@ Run", self.logTag); + DDLogVerbose(@"%@ in runLoop", self.logTag); + AssertIsOnDisappearingMessagesQueue(); - [self run]; + NSUInteger deletedCount = [self deleteExpiredMessages]; - uint64_t now = [NSDate ows_millisecondTimeStamp]; __block NSNumber *nextExpirationTimestampNumber; [self.databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { nextExpirationTimestampNumber = [self.disappearingMessagesFinder nextExpirationTimestampWithTransaction:transaction]; }]; + if (!nextExpirationTimestampNumber) { - // In theory we could kill the loop here. It should resume when the next expiring message is saved, - // But this is a safeguard for any race conditions that exist while running the job as a new message is saved. DDLogDebug(@"%@ No more expiring messages.", self.logTag); - [self runLater]; - return; + return deletedCount; } - uint64_t nextExpirationAt = [nextExpirationTimestampNumber unsignedLongLongValue]; - [self runByDate:[NSDate ows_dateWithMillisecondsSince1970:MAX(nextExpirationAt, now)]]; -} + uint64_t nextExpirationAt = nextExpirationTimestampNumber.unsignedLongLongValue; + NSDate *nextEpirationDate = [NSDate ows_dateWithMillisecondsSince1970:nextExpirationAt]; + [self scheduleRunByDate:nextEpirationDate]; -+ (void)setExpirationForMessage:(TSMessage *)message -{ - dispatch_async(self.serialQueue, ^{ - [[self sharedJob] setExpirationForMessage:message]; - }); + return deletedCount; } -- (void)setExpirationForMessage:(TSMessage *)message +- (void)startAnyExpirationForMessage:(TSMessage *)message + expirationStartedAt:(uint64_t)expirationStartedAt + transaction:(YapDatabaseReadWriteTransaction *_Nonnull)transaction { if (!message.isExpiringMessage) { return; } OWSDisappearingMessagesConfiguration *disappearingConfig = - [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:message.uniqueThreadId]; + [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:message.uniqueThreadId transaction:transaction]; if (!disappearingConfig.isEnabled) { return; } - [self setExpirationForMessage:message expirationStartedAt:[NSDate ows_millisecondTimeStamp]]; -} - -+ (void)setExpirationForMessage:(TSMessage *)message expirationStartedAt:(uint64_t)expirationStartedAt -{ - dispatch_async(self.serialQueue, ^{ - [[self sharedJob] setExpirationForMessage:message expirationStartedAt:expirationStartedAt]; - }); -} - -// This method should only be called on the serialQueue. -- (void)setExpirationForMessage:(TSMessage *)message expirationStartedAt:(uint64_t)expirationStartedAt -{ - [self.databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { - [self setExpirationForMessage:message expirationStartedAt:expirationStartedAt transaction:transaction]; - }]; + [self setExpirationForMessage:message expirationStartedAt:expirationStartedAt transaction:transaction]; } - (void)setExpirationForMessage:(TSMessage *)message @@ -186,114 +194,80 @@ NS_ASSUME_NONNULL_BEGIN return; } - int startedSecondsAgo = [NSDate new].timeIntervalSince1970 - expirationStartedAt / 1000.0; - DDLogDebug(@"%@ Starting expiration for message read %d seconds ago", self.logTag, startedSecondsAgo); + NSTimeInterval startedSecondsAgo = ([NSDate ows_millisecondTimeStamp] - expirationStartedAt) / 1000.0; + DDLogDebug(@"%@ Starting expiration for message read %f seconds ago", self.logTag, startedSecondsAgo); // Don't clobber if multiple actions simultaneously triggered expiration. if (message.expireStartedAt == 0 || message.expireStartedAt > expirationStartedAt) { [message updateWithExpireStartedAt:expirationStartedAt transaction:transaction]; } - // Necessary that the async expiration run happens *after* the message is saved with expiration configuration. - [self runByDate:[NSDate ows_dateWithMillisecondsSince1970:message.expiresAt]]; -} - -+ (void)setExpirationsForThread:(TSThread *)thread -{ - dispatch_async(self.serialQueue, ^{ - [[self sharedJob] setExpirationsForThread:thread]; - }); -} - -// This method should only be called on the serialQueue. -- (void)setExpirationsForThread:(TSThread *)thread -{ - OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; - - uint64_t now = [NSDate ows_millisecondTimeStamp]; - [self.databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { - [self.disappearingMessagesFinder - enumerateUnstartedExpiringMessagesInThread:thread - block:^(TSMessage *_Nonnull message) { - DDLogWarn( - @"%@ Starting expiring message which should have already " - @"been started.", - self.logTag); - // specify "now" in case D.M. have since been disabled, but we have - // existing unstarted expiring messages that still need to expire. - [self setExpirationForMessage:message - expirationStartedAt:now - transaction:transaction]; - } - transaction:transaction]; - }]; - - backgroundTask = nil; -} - -+ (void)becomeConsistentWithConfigurationForMessage:(TSMessage *)message - contactsManager:(id)contactsManager -{ - [[self sharedJob] becomeConsistentWithConfigurationForMessage:message contactsManager:contactsManager]; + [transaction addCompletionQueue:nil + completionBlock:^{ + // Necessary that the async expiration run happens *after* the message is saved with it's new + // expiration configuration. + [self scheduleRunByDate:[NSDate ows_dateWithMillisecondsSince1970:message.expiresAt]]; + }]; } - (void)becomeConsistentWithConfigurationForMessage:(TSMessage *)message contactsManager:(id)contactsManager + transaction:(YapDatabaseReadWriteTransaction *)transaction { OWSAssert(message); OWSAssert(contactsManager); - + __block OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; - - dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{ - // Become eventually consistent in the case that the remote changed their settings at the same time. - // Also in case remote doesn't support expiring messages - OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration = - [OWSDisappearingMessagesConfiguration fetchOrCreateDefaultWithThreadId:message.uniqueThreadId]; - - BOOL changed = NO; - if (message.expiresInSeconds == 0) { - if (disappearingMessagesConfiguration.isEnabled) { - changed = YES; - DDLogWarn(@"%@ Received remote message which had no expiration set, disabling our expiration to become " - @"consistent.", - self.logTag); - disappearingMessagesConfiguration.enabled = NO; - [disappearingMessagesConfiguration save]; - } - } else if (message.expiresInSeconds != disappearingMessagesConfiguration.durationSeconds) { + + // Become eventually consistent in the case that the remote changed their settings at the same time. + // Also in case remote doesn't support expiring messages + OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration = + [OWSDisappearingMessagesConfiguration fetchOrCreateDefaultWithThreadId:message.uniqueThreadId + transaction:transaction]; + + BOOL changed = NO; + if (message.expiresInSeconds == 0) { + if (disappearingMessagesConfiguration.isEnabled) { changed = YES; - DDLogInfo(@"%@ Received remote message with different expiration set, updating our expiration to become " + DDLogWarn(@"%@ Received remote message which had no expiration set, disabling our expiration to become " @"consistent.", - self.logTag); - disappearingMessagesConfiguration.enabled = YES; - disappearingMessagesConfiguration.durationSeconds = message.expiresInSeconds; - [disappearingMessagesConfiguration save]; - } - - if (!changed) { - return; - } - - if ([message isKindOfClass:[TSIncomingMessage class]]) { - TSIncomingMessage *incomingMessage = (TSIncomingMessage *)message; - NSString *contactName = [contactsManager displayNameForPhoneIdentifier:incomingMessage.messageAuthorId]; - - // We want the info message to appear _before_ the message. - [[[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:message.timestamp - 1 - thread:message.thread - configuration:disappearingMessagesConfiguration - createdByRemoteName:contactName] save]; - } else { - // We want the info message to appear _before_ the message. - [[[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:message.timestamp - 1 - thread:message.thread - configuration:disappearingMessagesConfiguration] - save]; + self.logTag); + disappearingMessagesConfiguration.enabled = NO; + [disappearingMessagesConfiguration saveWithTransaction:transaction]; } - - backgroundTask = nil; - }); + } else if (message.expiresInSeconds != disappearingMessagesConfiguration.durationSeconds) { + changed = YES; + DDLogInfo(@"%@ Received remote message with different expiration set, updating our expiration to become " + @"consistent.", + self.logTag); + disappearingMessagesConfiguration.enabled = YES; + disappearingMessagesConfiguration.durationSeconds = message.expiresInSeconds; + [disappearingMessagesConfiguration saveWithTransaction:transaction]; + } + + if (!changed) { + return; + } + + if ([message isKindOfClass:[TSIncomingMessage class]]) { + TSIncomingMessage *incomingMessage = (TSIncomingMessage *)message; + NSString *contactName = [contactsManager displayNameForPhoneIdentifier:incomingMessage.messageAuthorId]; + + // We want the info message to appear _before_ the message. + [[[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:message.timestampForSorting - 1 + thread:message.thread + configuration:disappearingMessagesConfiguration + createdByRemoteName:contactName] + saveWithTransaction:transaction]; + } else { + // We want the info message to appear _before_ the message. + [[[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:message.timestampForSorting - 1 + thread:message.thread + configuration:disappearingMessagesConfiguration] + saveWithTransaction:transaction]; + } + + backgroundTask = nil; } - (void)startIfNecessary @@ -304,33 +278,27 @@ NS_ASSUME_NONNULL_BEGIN } self.hasStarted = YES; - [self runNow]; + dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{ + [self runLoop]; + }); }); } -- (void)runNow -{ - [self runByDate:[NSDate new] ignoreMinDelay:YES]; -} - -- (NSTimeInterval)maxDelaySeconds -{ - // Don't run less often than once per N minutes. - return 5 * kMinuteInterval; -} - -// Waits the maximum amount of time to run again. -- (void)runLater +- (NSDateFormatter *)dateFormatter { - [self runByDate:[NSDate dateWithTimeIntervalSinceNow:self.maxDelaySeconds] ignoreMinDelay:YES]; -} + static NSDateFormatter *dateFormatter; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dateFormatter = [NSDateFormatter new]; + dateFormatter.dateStyle = NSDateFormatterNoStyle; + dateFormatter.timeStyle = kCFDateFormatterMediumStyle; + dateFormatter.locale = [NSLocale systemLocale]; + }); -- (void)runByDate:(NSDate *)date -{ - [self runByDate:date ignoreMinDelay:NO]; + return dateFormatter; } -- (void)runByDate:(NSDate *)date ignoreMinDelay:(BOOL)ignoreMinDelay +- (void)scheduleRunByDate:(NSDate *)date { OWSAssert(date); @@ -340,44 +308,39 @@ NS_ASSUME_NONNULL_BEGIN return; } - NSDateFormatter *dateFormatter = [NSDateFormatter new]; - dateFormatter.dateStyle = NSDateFormatterNoStyle; - dateFormatter.timeStyle = kCFDateFormatterMediumStyle; - dateFormatter.locale = [NSLocale systemLocale]; - // Don't run more often than once per second. - const NSTimeInterval kMinDelaySeconds = ignoreMinDelay ? 0.f : 1.f; - NSTimeInterval delaySeconds - = MAX(kMinDelaySeconds, MIN(self.maxDelaySeconds, [date timeIntervalSinceDate:[NSDate new]])); - NSDate *timerScheduleDate = [NSDate dateWithTimeIntervalSinceNow:delaySeconds]; - if (self.timerScheduleDate && [timerScheduleDate timeIntervalSinceDate:self.timerScheduleDate] > 0) { - DDLogVerbose(@"%@ Request to run at %@ (%d sec.) ignored due to scheduled run at %@ (%d sec.)", + const NSTimeInterval kMinDelaySeconds = 1.0; + NSTimeInterval delaySeconds = MAX(kMinDelaySeconds, date.timeIntervalSinceNow); + NSDate *newTimerScheduleDate = [NSDate dateWithTimeIntervalSinceNow:delaySeconds]; + if (self.nextDisappearanceDate && [self.nextDisappearanceDate isBeforeDate:newTimerScheduleDate]) { + DDLogVerbose(@"%@ Request to run at %@ (%d sec.) ignored due to earlier scheduled run at %@ (%d sec.)", self.logTag, - [dateFormatter stringFromDate:date], + [self.dateFormatter stringFromDate:date], (int)round(MAX(0, [date timeIntervalSinceDate:[NSDate new]])), - [dateFormatter stringFromDate:self.timerScheduleDate], - (int)round(MAX(0, [self.timerScheduleDate timeIntervalSinceDate:[NSDate new]]))); + [self.dateFormatter stringFromDate:self.nextDisappearanceDate], + (int)round(MAX(0, [self.nextDisappearanceDate timeIntervalSinceDate:[NSDate new]]))); return; } // Update Schedule DDLogVerbose(@"%@ Scheduled run at %@ (%d sec.)", self.logTag, - [dateFormatter stringFromDate:timerScheduleDate], - (int)round(MAX(0, [timerScheduleDate timeIntervalSinceDate:[NSDate new]]))); - [self resetTimer]; - self.timerScheduleDate = timerScheduleDate; - self.timer = [NSTimer weakScheduledTimerWithTimeInterval:delaySeconds - target:self - selector:@selector(timerDidFire) - userInfo:nil - repeats:NO]; + [self.dateFormatter stringFromDate:newTimerScheduleDate], + (int)round(MAX(0, [newTimerScheduleDate timeIntervalSinceDate:[NSDate new]]))); + [self resetNextDisappearanceTimer]; + self.nextDisappearanceDate = newTimerScheduleDate; + self.nextDisappearanceTimer = [NSTimer weakScheduledTimerWithTimeInterval:delaySeconds + target:self + selector:@selector(disappearanceTimerDidFire) + userInfo:nil + repeats:NO]; }); } -- (void)timerDidFire +- (void)disappearanceTimerDidFire { OWSAssertIsOnMainThread(); + DDLogDebug(@"%@ in %s", self.logTag, __PRETTY_FUNCTION__); if (!CurrentAppContext().isMainAppAndActive) { // Don't schedule run when inactive or not in main app. @@ -385,20 +348,43 @@ NS_ASSUME_NONNULL_BEGIN return; } - [self resetTimer]; + [self resetNextDisappearanceTimer]; dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{ [self runLoop]; }); } -- (void)resetTimer +- (void)fallbackTimerDidFire +{ + OWSAssertIsOnMainThread(); + DDLogDebug(@"%@ in %s", self.logTag, __PRETTY_FUNCTION__); + + BOOL recentlyScheduledDisappearanceTimer = NO; + if (fabs(self.nextDisappearanceDate.timeIntervalSinceNow) < 1.0) { + recentlyScheduledDisappearanceTimer = YES; + } + + dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{ + NSUInteger deletedCount = [self runLoop]; + + // Normally deletions should happen via the disappearanceTimer, to make sure that they're timely. + // So, if we're deleting something via the fallback timer, something may have gone wrong. The + // exception is if we're in close proximity to the disappearanceTimer, in which case a race condition + // is inevitable. + if (!recentlyScheduledDisappearanceTimer && deletedCount > 0) { + OWSProdLogAndFail(@"%@ unexpectedly deleted disappearing messages via fallback timer."); + } + }); +} + +- (void)resetNextDisappearanceTimer { OWSAssertIsOnMainThread(); - [self.timer invalidate]; - self.timer = nil; - self.timerScheduleDate = nil; + [self.nextDisappearanceTimer invalidate]; + self.nextDisappearanceTimer = nil; + self.nextDisappearanceDate = nil; } #pragma mark - Notifications @@ -408,7 +394,9 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertIsOnMainThread(); [AppReadiness runNowOrWhenAppIsReady:^{ - [self runNow]; + dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{ + [self runLoop]; + }); }]; } @@ -416,7 +404,7 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssertIsOnMainThread(); - [self resetTimer]; + [self resetNextDisappearanceTimer]; } @end diff --git a/SignalServiceKit/src/Messages/OWSMessageManager.m b/SignalServiceKit/src/Messages/OWSMessageManager.m index 1c64c563b..901f929b5 100644 --- a/SignalServiceKit/src/Messages/OWSMessageManager.m +++ b/SignalServiceKit/src/Messages/OWSMessageManager.m @@ -708,8 +708,8 @@ NS_ASSUME_NONNULL_BEGIN }); } else if (syncMessage.read.count > 0) { DDLogInfo(@"%@ Received %ld read receipt(s)", self.logTag, (u_long)syncMessage.read.count); - [OWSReadReceiptManager.sharedManager processReadReceiptsFromLinkedDevice:syncMessage.read + readTimestamp:envelope.timestamp transaction:transaction]; } else if (syncMessage.hasVerified) { DDLogInfo(@"%@ Received verification state for %@", self.logTag, syncMessage.verified.destination); @@ -1074,7 +1074,6 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(transaction); OWSAssert([TSAccountManager isRegistered]); - NSString *localNumber = [TSAccountManager localNumber]; if (!thread) { OWSFail(@"%@ Can't finalize without thread", self.logTag); @@ -1087,12 +1086,10 @@ NS_ASSUME_NONNULL_BEGIN [incomingMessage saveWithTransaction:transaction]; - // Any messages sent from the current user - from this device or another - should be - // automatically marked as read. - BOOL shouldMarkMessageAsRead = [envelope.source isEqualToString:localNumber]; - if (shouldMarkMessageAsRead) { + // Any messages sent from the current user - from this device or another - should be automatically marked as read. + if ([envelope.source isEqualToString:TSAccountManager.localNumber]) { // Don't send a read receipt for messages sent by ourselves. - [incomingMessage markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:YES]; + [incomingMessage markAsReadAtTimestamp:envelope.timestamp sendReadReceipt:NO transaction:transaction]; } TSQuotedMessage *_Nullable quotedMessage = incomingMessage.quotedMessage; @@ -1130,8 +1127,9 @@ NS_ASSUME_NONNULL_BEGIN [OWSReadReceiptManager.sharedManager applyEarlyReadReceiptsForIncomingMessage:incomingMessage transaction:transaction]; - [OWSDisappearingMessagesJob becomeConsistentWithConfigurationForMessage:incomingMessage - contactsManager:self.contactsManager]; + [[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithConfigurationForMessage:incomingMessage + contactsManager:self.contactsManager + transaction:transaction]; // Update thread preview in inbox [thread touchWithTransaction:transaction]; diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.h b/SignalServiceKit/src/Messages/OWSMessageSender.h index 4539b9ff0..6b65f8f9b 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.h +++ b/SignalServiceKit/src/Messages/OWSMessageSender.h @@ -84,14 +84,6 @@ NS_SWIFT_NAME(MessageSender) success:(void (^)(void))successHandler failure:(void (^)(NSError *error))failureHandler; -/** - * Set local configuration to match that of the of `outgoingMessage`'s sender - * - * We do this because messages and async message latency make it possible for thread participants disappearing messags - * configuration to get out of sync. - */ -- (void)becomeConsistentWithDisappearingConfigurationForMessage:(TSOutgoingMessage *)outgoingMessage; - @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index 266419d65..bc0501905 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -7,6 +7,7 @@ #import "ContactsUpdater.h" #import "NSData+keyVersionByte.h" #import "NSData+messagePadding.h" +#import "NSDate+OWS.h" #import "NSError+MessageSending.h" #import "OWSBackgroundTask.h" #import "OWSBlockingManager.h" @@ -1052,13 +1053,11 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; [self sendSyncTranscriptForMessage:message]; } - [OWSDisappearingMessagesJob setExpirationForMessage:message]; -} - -- (void)becomeConsistentWithDisappearingConfigurationForMessage:(TSOutgoingMessage *)outgoingMessage -{ - [OWSDisappearingMessagesJob becomeConsistentWithConfigurationForMessage:outgoingMessage - contactsManager:self.contactsManager]; + [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [[OWSDisappearingMessagesJob sharedJob] startAnyExpirationForMessage:message + expirationStartedAt:[NSDate ows_millisecondTimeStamp] + transaction:transaction]; + }]; } - (void)handleSendToMyself:(TSOutgoingMessage *)outgoingMessage diff --git a/SignalServiceKit/src/Messages/OWSReadReceiptManager.h b/SignalServiceKit/src/Messages/OWSReadReceiptManager.h index 0201cb0cb..36a2a14ad 100644 --- a/SignalServiceKit/src/Messages/OWSReadReceiptManager.h +++ b/SignalServiceKit/src/Messages/OWSReadReceiptManager.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // NS_ASSUME_NONNULL_BEGIN @@ -8,8 +8,8 @@ NS_ASSUME_NONNULL_BEGIN @class TSIncomingMessage; @class TSOutgoingMessage; @class TSThread; -@class YapDatabaseReadWriteTransaction; @class YapDatabaseReadTransaction; +@class YapDatabaseReadWriteTransaction; extern NSString *const kIncomingMessageMarkedAsReadNotification; @@ -50,6 +50,7 @@ extern NSString *const kIncomingMessageMarkedAsReadNotification; #pragma mark - Linked Device Read Receipts - (void)processReadReceiptsFromLinkedDevice:(NSArray *)readReceiptProtos + readTimestamp:(uint64_t)readTimestamp transaction:(YapDatabaseReadWriteTransaction *)transaction; - (void)applyEarlyReadReceiptsForIncomingMessage:(TSIncomingMessage *)message diff --git a/SignalServiceKit/src/Messages/OWSReadReceiptManager.m b/SignalServiceKit/src/Messages/OWSReadReceiptManager.m index 25587a5b5..ca54eae1d 100644 --- a/SignalServiceKit/src/Messages/OWSReadReceiptManager.m +++ b/SignalServiceKit/src/Messages/OWSReadReceiptManager.m @@ -4,6 +4,7 @@ #import "OWSReadReceiptManager.h" #import "AppReadiness.h" +#import "NSDate+OWS.h" #import "NSNotificationCenter+OWS.h" #import "OWSLinkedDeviceReadReceipt.h" #import "OWSMessageSender.h" @@ -289,6 +290,7 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [self markAsReadBeforeTimestamp:timestamp thread:thread + readTimestamp:[NSDate ows_millisecondTimeStamp] wasLocal:YES transaction:transaction]; }]; @@ -307,10 +309,12 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE OWSAssert(messageAuthorId.length > 0); OWSLinkedDeviceReadReceipt *newReadReceipt = - [[OWSLinkedDeviceReadReceipt alloc] initWithSenderId:messageAuthorId timestamp:message.timestamp]; + [[OWSLinkedDeviceReadReceipt alloc] initWithSenderId:messageAuthorId + messageIdTimestamp:message.timestamp + readTimestamp:[NSDate ows_millisecondTimeStamp]]; OWSLinkedDeviceReadReceipt *_Nullable oldReadReceipt = self.toLinkedDevicesReadReceiptMap[threadUniqueId]; - if (oldReadReceipt && oldReadReceipt.timestamp > newReadReceipt.timestamp) { + if (oldReadReceipt && oldReadReceipt.messageIdTimestamp > newReadReceipt.messageIdTimestamp) { // If there's an existing "linked device" read receipt for the same thread with // a newer timestamp, discard this "linked device" read receipt. DDLogVerbose(@"%@ Ignoring redundant read receipt for linked devices.", self.logTag); @@ -426,19 +430,25 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE OWSLinkedDeviceReadReceipt *_Nullable readReceipt = [OWSLinkedDeviceReadReceipt findLinkedDeviceReadReceiptWithSenderId:senderId - timestamp:timestamp + messageIdTimestamp:timestamp transaction:transaction]; if (!readReceipt) { return; } - [message markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:YES]; + + [message markAsReadAtTimestamp:readReceipt.readTimestamp sendReadReceipt:NO transaction:transaction]; [readReceipt removeWithTransaction:transaction]; - [[NSNotificationCenter defaultCenter] postNotificationNameAsync:kIncomingMessageMarkedAsReadNotification - object:message]; + [transaction addCompletionQueue:nil + completionBlock:^{ + [[NSNotificationCenter defaultCenter] + postNotificationNameAsync:kIncomingMessageMarkedAsReadNotification + object:message]; + }]; } - (void)processReadReceiptsFromLinkedDevice:(NSArray *)readReceiptProtos + readTimestamp:(uint64_t)readTimestamp transaction:(YapDatabaseReadWriteTransaction *)transaction { OWSAssert(readReceiptProtos); @@ -446,41 +456,53 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE for (OWSSignalServiceProtosSyncMessageRead *readReceiptProto in readReceiptProtos) { NSString *_Nullable senderId = readReceiptProto.sender; - uint64_t timestamp = readReceiptProto.timestamp; - BOOL isValid = senderId.length > 0 && timestamp > 0; - if (!isValid) { + uint64_t messageIdTimestamp = readReceiptProto.timestamp; + + if (senderId.length == 0) { + OWSProdLogAndFail(@"%@ in %s senderId was unexpectedly nil", self.logTag, __PRETTY_FUNCTION__); continue; } - - NSArray *messages = (NSArray *) [TSInteraction interactionsWithTimestamp:timestamp - ofClass:[TSIncomingMessage class] - withTransaction:transaction]; + + if (messageIdTimestamp == 0) { + OWSProdLogAndFail(@"%@ in %s messageIdTimestamp was unexpectedly 0", self.logTag, __PRETTY_FUNCTION__); + continue; + } + + NSArray *messages + = (NSArray *)[TSInteraction interactionsWithTimestamp:messageIdTimestamp + ofClass:[TSIncomingMessage class] + withTransaction:transaction]; if (messages.count > 0) { for (TSIncomingMessage *message in messages) { + NSTimeInterval secondsSinceRead = [NSDate new].timeIntervalSince1970 - readTimestamp / 1000; OWSAssert([message isKindOfClass:[TSIncomingMessage class]]); - - [self markAsReadOnLinkedDevice:message - transaction:transaction]; + DDLogDebug(@"%@ read on linked device %f seconds ago", self.logTag, secondsSinceRead); + [self markAsReadOnLinkedDevice:message readTimestamp:readTimestamp transaction:transaction]; } } else { // Received read receipt for unknown incoming message. // Persist in case we receive the incoming message later. - OWSLinkedDeviceReadReceipt *readReceipt = [[OWSLinkedDeviceReadReceipt alloc] initWithSenderId:senderId timestamp:timestamp]; + OWSLinkedDeviceReadReceipt *readReceipt = + [[OWSLinkedDeviceReadReceipt alloc] initWithSenderId:senderId + messageIdTimestamp:messageIdTimestamp + readTimestamp:readTimestamp]; [readReceipt saveWithTransaction:transaction]; } } } - (void)markAsReadOnLinkedDevice:(TSIncomingMessage *)message + readTimestamp:(uint64_t)readTimestamp transaction:(YapDatabaseReadWriteTransaction *)transaction { OWSAssert(message); OWSAssert(transaction); - // Use timestampForSorting which reflects local sort order, rather than timestamp + // Use `timestampForSorting` which reflects local received order, rather than `timestamp` // which reflect sender time. [self markAsReadBeforeTimestamp:message.timestampForSorting thread:[message threadWithTransaction:transaction] + readTimestamp:readTimestamp wasLocal:NO transaction:transaction]; } @@ -489,15 +511,16 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE - (void)markAsReadBeforeTimestamp:(uint64_t)timestamp thread:(TSThread *)thread + readTimestamp:(uint64_t)readTimestamp wasLocal:(BOOL)wasLocal transaction:(YapDatabaseReadWriteTransaction *)transaction { OWSAssert(timestamp > 0); OWSAssert(thread); OWSAssert(transaction); - - NSMutableArray> *interactions = [NSMutableArray new]; - + + NSMutableArray> *newlyReadList = [NSMutableArray new]; + [[TSDatabaseView unseenDatabaseViewExtension:transaction] enumerateRowsInGroup:thread.uniqueId usingBlock:^(NSString *collection, @@ -525,26 +548,32 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE OWSAssert(!possiblyRead.read); if (!possiblyRead.read) { - [interactions addObject:possiblyRead]; + [newlyReadList addObject:possiblyRead]; + } else if (readTimestamp < possiblyRead.expireStartedAt) { + [newlyReadList addObject:possiblyRead]; } }]; - - if (interactions.count < 1) { + + if (newlyReadList.count < 1) { return; } if (wasLocal) { - DDLogError(@"Marking %zd messages as read locally.", interactions.count); + DDLogError(@"Marking %zu messages as read locally.", newlyReadList.count); } else { - DDLogError(@"Marking %zd messages as read by linked device.", interactions.count); + DDLogError(@"Marking %zu messages as read by linked device.", newlyReadList.count); } - for (id possiblyRead in interactions) { - [possiblyRead markAsReadWithTransaction:transaction sendReadReceipt:wasLocal updateExpiration:YES]; - - if ([possiblyRead isKindOfClass:[TSIncomingMessage class]]) { - TSIncomingMessage *incomingMessage = (TSIncomingMessage *)possiblyRead; - [[NSNotificationCenter defaultCenter] postNotificationNameAsync:kIncomingMessageMarkedAsReadNotification - object:incomingMessage]; + for (id readItem in newlyReadList) { + [readItem markAsReadAtTimestamp:readTimestamp sendReadReceipt:wasLocal transaction:transaction]; + + if ([readItem isKindOfClass:[TSIncomingMessage class]]) { + TSIncomingMessage *incomingMessage = (TSIncomingMessage *)readItem; + [transaction addCompletionQueue:nil + completionBlock:^{ + [[NSNotificationCenter defaultCenter] + postNotificationNameAsync:kIncomingMessageMarkedAsReadNotification + object:incomingMessage]; + }]; } } } diff --git a/SignalServiceKit/src/Messages/OWSReadTracking.h b/SignalServiceKit/src/Messages/OWSReadTracking.h index ed3dd27be..5686e744f 100644 --- a/SignalServiceKit/src/Messages/OWSReadTracking.h +++ b/SignalServiceKit/src/Messages/OWSReadTracking.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // @class YapDatabaseReadWriteTransaction; @@ -15,16 +15,18 @@ */ @property (nonatomic, readonly, getter=wasRead) BOOL read; +@property (nonatomic, readonly) uint64_t expireStartedAt; @property (nonatomic, readonly) uint64_t timestampForSorting; @property (nonatomic, readonly) NSString *uniqueThreadId; + - (BOOL)shouldAffectUnreadCounts; /** - * Used for *responding* to a remote read receipt or in response to user activity. + * Used both for *responding* to a remote read receipt and in response to the local user's activity. */ -- (void)markAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction - sendReadReceipt:(BOOL)sendReadReceipt - updateExpiration:(BOOL)updateExpiration; +- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp + sendReadReceipt:(BOOL)sendReadReceipt + transaction:(YapDatabaseReadWriteTransaction *)transaction; @end diff --git a/SignalServiceKit/src/Messages/TSCall.m b/SignalServiceKit/src/Messages/TSCall.m index a0b05c891..7b37efb3a 100644 --- a/SignalServiceKit/src/Messages/TSCall.m +++ b/SignalServiceKit/src/Messages/TSCall.m @@ -89,15 +89,21 @@ NSUInteger TSCallCurrentSchemaVersion = 1; #pragma mark - OWSReadTracking +- (uint64_t)expireStartedAt +{ + return 0; +} + - (BOOL)shouldAffectUnreadCounts { return YES; } -- (void)markAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction - sendReadReceipt:(BOOL)sendReadReceipt - updateExpiration:(BOOL)updateExpiration +- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp + sendReadReceipt:(BOOL)sendReadReceipt + transaction:(YapDatabaseReadWriteTransaction *)transaction { + OWSAssert(transaction); if (_read) { @@ -110,7 +116,7 @@ NSUInteger TSCallCurrentSchemaVersion = 1; [self saveWithTransaction:transaction]; [self touchThreadWithTransaction:transaction]; - // Ignore sendReadReceipt and updateExpiration; they don't apply to calls. + // Ignore sendReadReceipt - it doesn't apply to calls. } #pragma mark - Methods @@ -127,7 +133,7 @@ NSUInteger TSCallCurrentSchemaVersion = 1; [self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { [self saveWithTransaction:transaction]; - + // redraw any thread-related unread count UI. [self touchThreadWithTransaction:transaction]; }];