Merge branch 'mkirk/disappearing-messages'

pull/1/head
Michael Kirk 7 years ago
commit 3950d2c546

@ -160,7 +160,7 @@ const CGFloat kExpirationTimerViewSize = 16.f;
return; 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); CGFloat ratioComplete = CGFloatClamp((CGFloat)1.0 - ratioRemaining, 0, 1.0);
CGPoint startPosition = CGPointMake(0, self.fullHourglassImageView.height * ratioComplete); CGPoint startPosition = CGPointMake(0, self.fullHourglassImageView.height * ratioComplete);

@ -3278,7 +3278,7 @@ NS_ASSUME_NONNULL_BEGIN
attachmentIds:@[] attachmentIds:@[]
expiresInSeconds:0 expiresInSeconds:0
quotedMessage:nil]; quotedMessage:nil];
[message markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:NO]; [message markAsReadNowWithSendReadReceipt:NO transaction:transaction];
break; break;
} }
case 1: { case 1: {
@ -3316,7 +3316,7 @@ NS_ASSUME_NONNULL_BEGIN
] ]
expiresInSeconds:0 expiresInSeconds:0
quotedMessage:nil]; quotedMessage:nil];
[message markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:NO]; [message markAsReadNowWithSendReadReceipt:NO transaction:transaction];
break; break;
} }
case 3: { case 3: {
@ -3767,7 +3767,7 @@ NS_ASSUME_NONNULL_BEGIN
attachmentIds:[NSMutableArray new] attachmentIds:[NSMutableArray new]
expiresInSeconds:0 expiresInSeconds:0
quotedMessage:nil]; quotedMessage:nil];
[message markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:NO]; [message markAsReadNowWithSendReadReceipt:NO transaction:transaction];
} }
{ {
TSOutgoingMessage *message = TSOutgoingMessage *message =
@ -4105,7 +4105,7 @@ NS_ASSUME_NONNULL_BEGIN
attachmentIds:attachmentIds attachmentIds:attachmentIds
expiresInSeconds:0 expiresInSeconds:0
quotedMessage:quotedMessage]; quotedMessage:quotedMessage];
[message markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:NO]; [message markAsReadNowWithSendReadReceipt:NO transaction:transaction];
return message; return message;
} }

@ -26,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, nullable, readonly) NSNumber *unreadIndicatorPosition; @property (nonatomic, nullable, readonly) NSNumber *unreadIndicatorPosition;
// If there are unseen messages in the thread, this is the timestamp // 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 // 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 // a snapshot of what the first unread message was when we entered the

@ -8,6 +8,8 @@ NS_ASSUME_NONNULL_BEGIN
#define OWSDisappearingMessagesConfigurationDefaultExpirationDuration kDayInterval #define OWSDisappearingMessagesConfigurationDefaultExpirationDuration kDayInterval
@class YapDatabaseReadTransaction;
@interface OWSDisappearingMessagesConfiguration : TSYapDatabaseObject @interface OWSDisappearingMessagesConfiguration : TSYapDatabaseObject
- (instancetype)initDefaultWithThreadId:(NSString *)threadId; - (instancetype)initDefaultWithThreadId:(NSString *)threadId;
@ -21,7 +23,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) BOOL dictionaryValueDidChange; @property (nonatomic, readonly) BOOL dictionaryValueDidChange;
@property (readonly, getter=isNewRecord) BOOL newRecord; @property (readonly, getter=isNewRecord) BOOL newRecord;
+ (instancetype)fetchOrCreateDefaultWithThreadId:(NSString *)threadId; + (instancetype)fetchOrCreateDefaultWithThreadId:(NSString *)threadId
transaction:(YapDatabaseReadTransaction *)transaction;
+ (NSArray<NSNumber *> *)validDurationsSeconds; + (NSArray<NSNumber *> *)validDurationsSeconds;

@ -51,8 +51,10 @@ NS_ASSUME_NONNULL_BEGIN
} }
+ (instancetype)fetchOrCreateDefaultWithThreadId:(NSString *)threadId + (instancetype)fetchOrCreateDefaultWithThreadId:(NSString *)threadId
transaction:(YapDatabaseReadTransaction *)transaction
{ {
OWSDisappearingMessagesConfiguration *savedConfiguration = [self fetchObjectWithUniqueID:threadId]; OWSDisappearingMessagesConfiguration *savedConfiguration =
[self fetchObjectWithUniqueID:threadId transaction:transaction];
if (savedConfiguration) { if (savedConfiguration) {
return savedConfiguration; return savedConfiguration;
} else { } else {

@ -3,6 +3,7 @@
// //
#import "TSThread.h" #import "TSThread.h"
#import "NSDate+OWS.h"
#import "OWSPrimaryStorage.h" #import "OWSPrimaryStorage.h"
#import "OWSReadTracking.h" #import "OWSReadTracking.h"
#import "TSDatabaseView.h" #import "TSDatabaseView.h"
@ -230,7 +231,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction - (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{ {
for (id<OWSReadTracking> message in [self unseenMessagesWithTransaction:transaction]) { for (id<OWSReadTracking> 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. // Just to be defensive, we'll also check for unread messages.

@ -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" #import "TSYapDatabaseObject.h"
@ -9,12 +9,15 @@ NS_ASSUME_NONNULL_BEGIN
@interface OWSLinkedDeviceReadReceipt : TSYapDatabaseObject @interface OWSLinkedDeviceReadReceipt : TSYapDatabaseObject
@property (nonatomic, readonly) NSString *senderId; @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 + (nullable OWSLinkedDeviceReadReceipt *)findLinkedDeviceReadReceiptWithSenderId:(NSString *)senderId
timestamp:(uint64_t)timestamp messageIdTimestamp:(uint64_t)messageIdTimestamp
transaction: transaction:
(YapDatabaseReadTransaction *)transaction; (YapDatabaseReadTransaction *)transaction;

@ -8,37 +8,67 @@ NS_ASSUME_NONNULL_BEGIN
@implementation OWSLinkedDeviceReadReceipt @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) { if (!self) {
return self; return self;
} }
_senderId = senderId; _senderId = senderId;
_timestamp = timestamp; _messageIdTimestamp = messageIdTimestamp;
_readTimestamp = readTimestamp;
return self; 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 + (nullable OWSLinkedDeviceReadReceipt *)findLinkedDeviceReadReceiptWithSenderId:(NSString *)senderId
timestamp:(uint64_t)timestamp messageIdTimestamp:(uint64_t)messageIdTimestamp
transaction: transaction:
(YapDatabaseReadTransaction *)transaction (YapDatabaseReadTransaction *)transaction
{ {
OWSAssert(transaction); OWSAssert(transaction);
NSString *receiptId =
return [OWSLinkedDeviceReadReceipt fetchObjectWithUniqueID:[self uniqueIdForSenderId:senderId timestamp:timestamp] [OWSLinkedDeviceReadReceipt uniqueIdForSenderId:senderId messageIdTimestamp:messageIdTimestamp];
transaction:transaction]; return [OWSLinkedDeviceReadReceipt fetchObjectWithUniqueID:receiptId transaction:transaction];
} }
@end @end

@ -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" #import "OWSReadReceiptsForLinkedDevicesMessage.h"
@ -35,7 +35,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSSignalServiceProtosSyncMessageReadBuilder *readProtoBuilder = OWSSignalServiceProtosSyncMessageReadBuilder *readProtoBuilder =
[OWSSignalServiceProtosSyncMessageReadBuilder new]; [OWSSignalServiceProtosSyncMessageReadBuilder new];
[readProtoBuilder setSender:readReceipt.senderId]; [readProtoBuilder setSender:readReceipt.senderId];
[readProtoBuilder setTimestamp:readReceipt.timestamp]; [readProtoBuilder setTimestamp:readReceipt.messageIdTimestamp];
[syncMessageBuilder addRead:[readProtoBuilder build]]; [syncMessageBuilder addRead:[readProtoBuilder build]];
} }

@ -134,9 +134,13 @@ NS_ASSUME_NONNULL_BEGIN
} }
if (transcript.isExpirationTimerUpdate) { if (transcript.isExpirationTimerUpdate) {
[OWSDisappearingMessagesJob becomeConsistentWithConfigurationForMessage:outgoingMessage [[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithConfigurationForMessage:outgoingMessage
contactsManager:self.contactsManager]; contactsManager:self.contactsManager
transaction:transaction];
// early return to avoid saving an empty incoming message. // early return to avoid saving an empty incoming message.
OWSAssert(transcript.body.length == 0);
OWSAssert(outgoingMessage.attachmentIds.count == 0);
return; return;
} }
@ -147,10 +151,12 @@ NS_ASSUME_NONNULL_BEGIN
[outgoingMessage saveWithTransaction:transaction]; [outgoingMessage saveWithTransaction:transaction];
[outgoingMessage updateWithWasSentFromLinkedDeviceWithTransaction:transaction]; [outgoingMessage updateWithWasSentFromLinkedDeviceWithTransaction:transaction];
[OWSDisappearingMessagesJob becomeConsistentWithConfigurationForMessage:outgoingMessage [[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithConfigurationForMessage:outgoingMessage
contactsManager:self.contactsManager]; contactsManager:self.contactsManager
[OWSDisappearingMessagesJob setExpirationForMessage:outgoingMessage transaction:transaction];
expirationStartedAt:transcript.expirationStartedAt]; [[OWSDisappearingMessagesJob sharedJob] setExpirationForMessage:outgoingMessage
expirationStartedAt:transcript.expirationStartedAt
transaction:transaction];
[self.readReceiptManager applyEarlyReadReceiptsForOutgoingMessageFromLinkedDevice:outgoingMessage [self.readReceiptManager applyEarlyReadReceiptsForOutgoingMessageFromLinkedDevice:outgoingMessage
transaction:transaction]; transaction:transaction];

@ -49,6 +49,13 @@ NS_ASSUME_NONNULL_BEGIN
return self; 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 - (NSString *)description
{ {
if (self.createdByRemoteName) { if (self.createdByRemoteName) {

@ -179,14 +179,19 @@ NSUInteger TSErrorMessageSchemaVersion = 1;
#pragma mark - OWSReadTracking #pragma mark - OWSReadTracking
- (uint64_t)expireStartedAt
{
return 0;
}
- (BOOL)shouldAffectUnreadCounts - (BOOL)shouldAffectUnreadCounts
{ {
return NO; return NO;
} }
- (void)markAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction - (void)markAsReadAtTimestamp:(uint64_t)readTimestamp
sendReadReceipt:(BOOL)sendReadReceipt sendReadReceipt:(BOOL)sendReadReceipt
updateExpiration:(BOOL)updateExpiration transaction:(YapDatabaseReadWriteTransaction *)transaction
{ {
OWSAssert(transaction); OWSAssert(transaction);
@ -200,7 +205,7 @@ NSUInteger TSErrorMessageSchemaVersion = 1;
[self saveWithTransaction:transaction]; [self saveWithTransaction:transaction];
[self touchThreadWithTransaction: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 @end

@ -74,6 +74,10 @@ NS_ASSUME_NONNULL_BEGIN
- (NSString *)messageAuthorId; - (NSString *)messageAuthorId;
// convenience method for expiring a message which was just read
- (void)markAsReadNowWithSendReadReceipt:(BOOL)sendReadReceipt
transaction:(YapDatabaseReadWriteTransaction *)transaction;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

@ -3,6 +3,7 @@
// //
#import "TSIncomingMessage.h" #import "TSIncomingMessage.h"
#import "NSDate+OWS.h"
#import "NSNotificationCenter+OWS.h" #import "NSNotificationCenter+OWS.h"
#import "OWSDisappearingMessagesConfiguration.h" #import "OWSDisappearingMessagesConfiguration.h"
#import "OWSDisappearingMessagesJob.h" #import "OWSDisappearingMessagesJob.h"
@ -134,25 +135,37 @@ NS_ASSUME_NONNULL_BEGIN
return YES; return YES;
} }
- (void)markAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction - (void)markAsReadNowWithSendReadReceipt:(BOOL)sendReadReceipt
sendReadReceipt:(BOOL)sendReadReceipt transaction:(YapDatabaseReadWriteTransaction *)transaction;
updateExpiration:(BOOL)updateExpiration {
[self markAsReadAtTimestamp:[NSDate ows_millisecondTimeStamp]
sendReadReceipt:sendReadReceipt
transaction:transaction];
}
- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp
sendReadReceipt:(BOOL)sendReadReceipt
transaction:(YapDatabaseReadWriteTransaction *)transaction;
{ {
OWSAssert(transaction); OWSAssert(transaction);
if (_read) { if (_read && readTimestamp <= self.expireStartedAt) {
return; return;
} }
DDLogDebug( NSTimeInterval secondsAgoRead = ([NSDate ows_millisecondTimeStamp] - readTimestamp) / 1000;
@"%@ marking as read uniqueId: %@ which has timestamp: %llu", self.logTag, self.uniqueId, self.timestamp); DDLogDebug(@"%@ marking uniqueId: %@ which has timestamp: %llu as read: %f seconds ago",
self.logTag,
self.uniqueId,
self.timestamp,
secondsAgoRead);
_read = YES; _read = YES;
[self saveWithTransaction:transaction]; [self saveWithTransaction:transaction];
[self touchThreadWithTransaction:transaction]; [self touchThreadWithTransaction:transaction];
if (updateExpiration) { [[OWSDisappearingMessagesJob sharedJob] startAnyExpirationForMessage:self
[OWSDisappearingMessagesJob setExpirationForMessage:self]; expirationStartedAt:readTimestamp
} transaction:transaction];
if (sendReadReceipt) { if (sendReadReceipt) {
[OWSReadReceiptManager.sharedManager messageWasReadLocally:self]; [OWSReadReceiptManager.sharedManager messageWasReadLocally:self];

@ -129,9 +129,14 @@ NSUInteger TSInfoMessageSchemaVersion = 1;
return NO; return NO;
} }
- (void)markAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction - (uint64_t)expireStartedAt
sendReadReceipt:(BOOL)sendReadReceipt {
updateExpiration:(BOOL)updateExpiration return 0;
}
- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp
sendReadReceipt:(BOOL)sendReadReceipt
transaction:(YapDatabaseReadWriteTransaction *)transaction
{ {
OWSAssert(transaction); OWSAssert(transaction);
@ -145,7 +150,7 @@ NSUInteger TSInfoMessageSchemaVersion = 1;
[self saveWithTransaction:transaction]; [self saveWithTransaction:transaction];
[self touchThreadWithTransaction: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 @end

@ -7,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN
@class OWSPrimaryStorage; @class OWSPrimaryStorage;
@class TSMessage; @class TSMessage;
@class TSThread; @class TSThread;
@class YapDatabaseReadWriteTransaction;
@protocol ContactsManagerProtocol; @protocol ContactsManagerProtocol;
@ -16,9 +17,13 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)init NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE;
+ (void)setExpirationsForThread:(TSThread *)thread; - (void)startAnyExpirationForMessage:(TSMessage *)message
+ (void)setExpirationForMessage:(TSMessage *)message; expirationStartedAt:(uint64_t)expirationStartedAt
+ (void)setExpirationForMessage:(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 * 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 * @param contactsManager
* Provides the contact name responsible for any configuration changes in an info message. * Provides the contact name responsible for any configuration changes in an info message.
*/ */
+ (void)becomeConsistentWithConfigurationForMessage:(TSMessage *)message - (void)becomeConsistentWithConfigurationForMessage:(TSMessage *)message
contactsManager:(id<ContactsManagerProtocol>)contactsManager; contactsManager:(id<ContactsManagerProtocol>)contactsManager
transaction:(YapDatabaseReadWriteTransaction *)transaction;
// Clean up any messages that expired since last launch immediately // Clean up any messages that expired since last launch immediately
// and continue cleaning in the background. // and continue cleaning in the background.

@ -17,6 +17,7 @@
#import "TSMessage.h" #import "TSMessage.h"
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
// Can we move to Signal-iOS? // Can we move to Signal-iOS?
@interface OWSDisappearingMessagesJob () @interface OWSDisappearingMessagesJob ()
@ -24,13 +25,25 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) OWSDisappearingMessagesFinder *disappearingMessagesFinder; @property (nonatomic, readonly) OWSDisappearingMessagesFinder *disappearingMessagesFinder;
+ (dispatch_queue_t)serialQueue;
// These three properties should only be accessed on the main thread. // These three properties should only be accessed on the main thread.
@property (nonatomic) BOOL hasStarted; @property (nonatomic) BOOL hasStarted;
@property (nonatomic, nullable) NSTimer *timer; @property (nonatomic, nullable) NSTimer *nextDisappearanceTimer;
@property (nonatomic, nullable) NSDate *timerScheduleDate; @property (nonatomic, nullable) NSDate *nextDisappearanceDate;
@property (nonatomic, nullable) NSTimer *fallbackTimer;
@end @end
void AssertIsOnDisappearingMessagesQueue()
{
#ifdef DEBUG
if (@available(iOS 10.0, *)) {
dispatch_assert_queue(OWSDisappearingMessagesJob.serialQueue);
}
#endif
}
#pragma mark - #pragma mark -
@implementation OWSDisappearingMessagesJob @implementation OWSDisappearingMessagesJob
@ -55,6 +68,18 @@ NS_ASSUME_NONNULL_BEGIN
_databaseConnection = primaryStorage.newDatabaseConnection; _databaseConnection = primaryStorage.newDatabaseConnection;
_disappearingMessagesFinder = [OWSDisappearingMessagesFinder new]; _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(); OWSSingletonAssert();
[[NSNotificationCenter defaultCenter] addObserver:self [[NSNotificationCenter defaultCenter] addObserver:self
@ -84,96 +109,79 @@ NS_ASSUME_NONNULL_BEGIN
return queue; return queue;
} }
// This method should only be called on the serialQueue. - (NSUInteger)deleteExpiredMessages
- (void)run
{ {
AssertIsOnDisappearingMessagesQueue();
uint64_t now = [NSDate ows_millisecondTimeStamp]; uint64_t now = [NSDate ows_millisecondTimeStamp];
OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
__block uint expirationCount = 0; __block NSUInteger expirationCount = 0;
[self.databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { [self.databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
[self.disappearingMessagesFinder enumerateExpiredMessagesWithBlock:^(TSMessage *message) { [self.disappearingMessagesFinder enumerateExpiredMessagesWithBlock:^(TSMessage *message) {
// sanity check // sanity check
if (message.expiresAt > now) { if (message.expiresAt > now) {
DDLogError( OWSFail(
@"%@ Refusing to remove message which doesn't expire until: %lld", self.logTag, message.expiresAt); @"%@ Refusing to remove message which doesn't expire until: %lld", self.logTag, message.expiresAt);
return; 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]; [message removeWithTransaction:transaction];
expirationCount++; expirationCount++;
} }
transaction:transaction]; transaction:transaction];
}]; }];
DDLogDebug(@"%@ Removed %u expired messages", self.logTag, expirationCount); DDLogDebug(@"%@ Removed %tu expired messages", self.logTag, expirationCount);
backgroundTask = nil; backgroundTask = nil;
return expirationCount;
} }
// This method should only be called on the serialQueue. // deletes any expired messages and schedules the next run.
- (void)runLoop - (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; __block NSNumber *nextExpirationTimestampNumber;
[self.databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { [self.databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
nextExpirationTimestampNumber = nextExpirationTimestampNumber =
[self.disappearingMessagesFinder nextExpirationTimestampWithTransaction:transaction]; [self.disappearingMessagesFinder nextExpirationTimestampWithTransaction:transaction];
}]; }];
if (!nextExpirationTimestampNumber) { 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); DDLogDebug(@"%@ No more expiring messages.", self.logTag);
[self runLater]; return deletedCount;
return;
} }
uint64_t nextExpirationAt = [nextExpirationTimestampNumber unsignedLongLongValue]; uint64_t nextExpirationAt = nextExpirationTimestampNumber.unsignedLongLongValue;
[self runByDate:[NSDate ows_dateWithMillisecondsSince1970:MAX(nextExpirationAt, now)]]; NSDate *nextEpirationDate = [NSDate ows_dateWithMillisecondsSince1970:nextExpirationAt];
} [self scheduleRunByDate:nextEpirationDate];
+ (void)setExpirationForMessage:(TSMessage *)message return deletedCount;
{
dispatch_async(self.serialQueue, ^{
[[self sharedJob] setExpirationForMessage:message];
});
} }
- (void)setExpirationForMessage:(TSMessage *)message - (void)startAnyExpirationForMessage:(TSMessage *)message
expirationStartedAt:(uint64_t)expirationStartedAt
transaction:(YapDatabaseReadWriteTransaction *_Nonnull)transaction
{ {
if (!message.isExpiringMessage) { if (!message.isExpiringMessage) {
return; return;
} }
OWSDisappearingMessagesConfiguration *disappearingConfig = OWSDisappearingMessagesConfiguration *disappearingConfig =
[OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:message.uniqueThreadId]; [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:message.uniqueThreadId transaction:transaction];
if (!disappearingConfig.isEnabled) { if (!disappearingConfig.isEnabled) {
return; return;
} }
[self setExpirationForMessage:message expirationStartedAt:[NSDate ows_millisecondTimeStamp]]; [self setExpirationForMessage:message expirationStartedAt:expirationStartedAt transaction:transaction];
}
+ (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];
}];
} }
- (void)setExpirationForMessage:(TSMessage *)message - (void)setExpirationForMessage:(TSMessage *)message
@ -186,114 +194,80 @@ NS_ASSUME_NONNULL_BEGIN
return; return;
} }
int startedSecondsAgo = [NSDate new].timeIntervalSince1970 - expirationStartedAt / 1000.0; NSTimeInterval startedSecondsAgo = ([NSDate ows_millisecondTimeStamp] - expirationStartedAt) / 1000.0;
DDLogDebug(@"%@ Starting expiration for message read %d seconds ago", self.logTag, startedSecondsAgo); DDLogDebug(@"%@ Starting expiration for message read %f seconds ago", self.logTag, startedSecondsAgo);
// Don't clobber if multiple actions simultaneously triggered expiration. // Don't clobber if multiple actions simultaneously triggered expiration.
if (message.expireStartedAt == 0 || message.expireStartedAt > expirationStartedAt) { if (message.expireStartedAt == 0 || message.expireStartedAt > expirationStartedAt) {
[message updateWithExpireStartedAt:expirationStartedAt transaction:transaction]; [message updateWithExpireStartedAt:expirationStartedAt transaction:transaction];
} }
// Necessary that the async expiration run happens *after* the message is saved with expiration configuration. [transaction addCompletionQueue:nil
[self runByDate:[NSDate ows_dateWithMillisecondsSince1970:message.expiresAt]]; completionBlock:^{
} // Necessary that the async expiration run happens *after* the message is saved with it's new
// expiration configuration.
+ (void)setExpirationsForThread:(TSThread *)thread [self scheduleRunByDate:[NSDate ows_dateWithMillisecondsSince1970:message.expiresAt]];
{ }];
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<ContactsManagerProtocol>)contactsManager
{
[[self sharedJob] becomeConsistentWithConfigurationForMessage:message contactsManager:contactsManager];
} }
- (void)becomeConsistentWithConfigurationForMessage:(TSMessage *)message - (void)becomeConsistentWithConfigurationForMessage:(TSMessage *)message
contactsManager:(id<ContactsManagerProtocol>)contactsManager contactsManager:(id<ContactsManagerProtocol>)contactsManager
transaction:(YapDatabaseReadWriteTransaction *)transaction
{ {
OWSAssert(message); OWSAssert(message);
OWSAssert(contactsManager); OWSAssert(contactsManager);
__block OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; __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.
// 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
// Also in case remote doesn't support expiring messages OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration =
OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration = [OWSDisappearingMessagesConfiguration fetchOrCreateDefaultWithThreadId:message.uniqueThreadId
[OWSDisappearingMessagesConfiguration fetchOrCreateDefaultWithThreadId:message.uniqueThreadId]; transaction:transaction];
BOOL changed = NO; BOOL changed = NO;
if (message.expiresInSeconds == 0) { if (message.expiresInSeconds == 0) {
if (disappearingMessagesConfiguration.isEnabled) { 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) {
changed = YES; 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.", @"consistent.",
self.logTag); self.logTag);
disappearingMessagesConfiguration.enabled = YES; disappearingMessagesConfiguration.enabled = NO;
disappearingMessagesConfiguration.durationSeconds = message.expiresInSeconds; [disappearingMessagesConfiguration saveWithTransaction:transaction];
[disappearingMessagesConfiguration save];
} }
} 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) { if (!changed) {
return; return;
} }
if ([message isKindOfClass:[TSIncomingMessage class]]) { if ([message isKindOfClass:[TSIncomingMessage class]]) {
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)message; TSIncomingMessage *incomingMessage = (TSIncomingMessage *)message;
NSString *contactName = [contactsManager displayNameForPhoneIdentifier:incomingMessage.messageAuthorId]; NSString *contactName = [contactsManager displayNameForPhoneIdentifier:incomingMessage.messageAuthorId];
// We want the info message to appear _before_ the message. // We want the info message to appear _before_ the message.
[[[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:message.timestamp - 1 [[[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:message.timestampForSorting - 1
thread:message.thread thread:message.thread
configuration:disappearingMessagesConfiguration configuration:disappearingMessagesConfiguration
createdByRemoteName:contactName] save]; createdByRemoteName:contactName]
} else { saveWithTransaction:transaction];
// We want the info message to appear _before_ the message. } else {
[[[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:message.timestamp - 1 // We want the info message to appear _before_ the message.
thread:message.thread [[[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:message.timestampForSorting - 1
configuration:disappearingMessagesConfiguration] thread:message.thread
save]; configuration:disappearingMessagesConfiguration]
} saveWithTransaction:transaction];
}
backgroundTask = nil; backgroundTask = nil;
});
} }
- (void)startIfNecessary - (void)startIfNecessary
@ -304,33 +278,27 @@ NS_ASSUME_NONNULL_BEGIN
} }
self.hasStarted = YES; self.hasStarted = YES;
[self runNow]; dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{
[self runLoop];
});
}); });
} }
- (void)runNow - (NSDateFormatter *)dateFormatter
{ {
[self runByDate:[NSDate new] ignoreMinDelay:YES]; static NSDateFormatter *dateFormatter;
} static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
- (NSTimeInterval)maxDelaySeconds dateFormatter = [NSDateFormatter new];
{ dateFormatter.dateStyle = NSDateFormatterNoStyle;
// Don't run less often than once per N minutes. dateFormatter.timeStyle = kCFDateFormatterMediumStyle;
return 5 * kMinuteInterval; dateFormatter.locale = [NSLocale systemLocale];
} });
// Waits the maximum amount of time to run again.
- (void)runLater
{
[self runByDate:[NSDate dateWithTimeIntervalSinceNow:self.maxDelaySeconds] ignoreMinDelay:YES];
}
- (void)runByDate:(NSDate *)date return dateFormatter;
{
[self runByDate:date ignoreMinDelay:NO];
} }
- (void)runByDate:(NSDate *)date ignoreMinDelay:(BOOL)ignoreMinDelay - (void)scheduleRunByDate:(NSDate *)date
{ {
OWSAssert(date); OWSAssert(date);
@ -340,44 +308,39 @@ NS_ASSUME_NONNULL_BEGIN
return; return;
} }
NSDateFormatter *dateFormatter = [NSDateFormatter new];
dateFormatter.dateStyle = NSDateFormatterNoStyle;
dateFormatter.timeStyle = kCFDateFormatterMediumStyle;
dateFormatter.locale = [NSLocale systemLocale];
// Don't run more often than once per second. // Don't run more often than once per second.
const NSTimeInterval kMinDelaySeconds = ignoreMinDelay ? 0.f : 1.f; const NSTimeInterval kMinDelaySeconds = 1.0;
NSTimeInterval delaySeconds NSTimeInterval delaySeconds = MAX(kMinDelaySeconds, date.timeIntervalSinceNow);
= MAX(kMinDelaySeconds, MIN(self.maxDelaySeconds, [date timeIntervalSinceDate:[NSDate new]])); NSDate *newTimerScheduleDate = [NSDate dateWithTimeIntervalSinceNow:delaySeconds];
NSDate *timerScheduleDate = [NSDate dateWithTimeIntervalSinceNow:delaySeconds]; if (self.nextDisappearanceDate && [self.nextDisappearanceDate isBeforeDate:newTimerScheduleDate]) {
if (self.timerScheduleDate && [timerScheduleDate timeIntervalSinceDate:self.timerScheduleDate] > 0) { DDLogVerbose(@"%@ Request to run at %@ (%d sec.) ignored due to earlier scheduled run at %@ (%d sec.)",
DDLogVerbose(@"%@ Request to run at %@ (%d sec.) ignored due to scheduled run at %@ (%d sec.)",
self.logTag, self.logTag,
[dateFormatter stringFromDate:date], [self.dateFormatter stringFromDate:date],
(int)round(MAX(0, [date timeIntervalSinceDate:[NSDate new]])), (int)round(MAX(0, [date timeIntervalSinceDate:[NSDate new]])),
[dateFormatter stringFromDate:self.timerScheduleDate], [self.dateFormatter stringFromDate:self.nextDisappearanceDate],
(int)round(MAX(0, [self.timerScheduleDate timeIntervalSinceDate:[NSDate new]]))); (int)round(MAX(0, [self.nextDisappearanceDate timeIntervalSinceDate:[NSDate new]])));
return; return;
} }
// Update Schedule // Update Schedule
DDLogVerbose(@"%@ Scheduled run at %@ (%d sec.)", DDLogVerbose(@"%@ Scheduled run at %@ (%d sec.)",
self.logTag, self.logTag,
[dateFormatter stringFromDate:timerScheduleDate], [self.dateFormatter stringFromDate:newTimerScheduleDate],
(int)round(MAX(0, [timerScheduleDate timeIntervalSinceDate:[NSDate new]]))); (int)round(MAX(0, [newTimerScheduleDate timeIntervalSinceDate:[NSDate new]])));
[self resetTimer]; [self resetNextDisappearanceTimer];
self.timerScheduleDate = timerScheduleDate; self.nextDisappearanceDate = newTimerScheduleDate;
self.timer = [NSTimer weakScheduledTimerWithTimeInterval:delaySeconds self.nextDisappearanceTimer = [NSTimer weakScheduledTimerWithTimeInterval:delaySeconds
target:self target:self
selector:@selector(timerDidFire) selector:@selector(disappearanceTimerDidFire)
userInfo:nil userInfo:nil
repeats:NO]; repeats:NO];
}); });
} }
- (void)timerDidFire - (void)disappearanceTimerDidFire
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
DDLogDebug(@"%@ in %s", self.logTag, __PRETTY_FUNCTION__);
if (!CurrentAppContext().isMainAppAndActive) { if (!CurrentAppContext().isMainAppAndActive) {
// Don't schedule run when inactive or not in main app. // Don't schedule run when inactive or not in main app.
@ -385,20 +348,43 @@ NS_ASSUME_NONNULL_BEGIN
return; return;
} }
[self resetTimer]; [self resetNextDisappearanceTimer];
dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{ dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{
[self runLoop]; [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(); OWSAssertIsOnMainThread();
[self.timer invalidate]; [self.nextDisappearanceTimer invalidate];
self.timer = nil; self.nextDisappearanceTimer = nil;
self.timerScheduleDate = nil; self.nextDisappearanceDate = nil;
} }
#pragma mark - Notifications #pragma mark - Notifications
@ -408,7 +394,9 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
[AppReadiness runNowOrWhenAppIsReady:^{ [AppReadiness runNowOrWhenAppIsReady:^{
[self runNow]; dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{
[self runLoop];
});
}]; }];
} }
@ -416,7 +404,7 @@ NS_ASSUME_NONNULL_BEGIN
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
[self resetTimer]; [self resetNextDisappearanceTimer];
} }
@end @end

@ -708,8 +708,8 @@ NS_ASSUME_NONNULL_BEGIN
}); });
} else if (syncMessage.read.count > 0) { } else if (syncMessage.read.count > 0) {
DDLogInfo(@"%@ Received %ld read receipt(s)", self.logTag, (u_long)syncMessage.read.count); DDLogInfo(@"%@ Received %ld read receipt(s)", self.logTag, (u_long)syncMessage.read.count);
[OWSReadReceiptManager.sharedManager processReadReceiptsFromLinkedDevice:syncMessage.read [OWSReadReceiptManager.sharedManager processReadReceiptsFromLinkedDevice:syncMessage.read
readTimestamp:envelope.timestamp
transaction:transaction]; transaction:transaction];
} else if (syncMessage.hasVerified) { } else if (syncMessage.hasVerified) {
DDLogInfo(@"%@ Received verification state for %@", self.logTag, syncMessage.verified.destination); DDLogInfo(@"%@ Received verification state for %@", self.logTag, syncMessage.verified.destination);
@ -1074,7 +1074,6 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert(transaction); OWSAssert(transaction);
OWSAssert([TSAccountManager isRegistered]); OWSAssert([TSAccountManager isRegistered]);
NSString *localNumber = [TSAccountManager localNumber];
if (!thread) { if (!thread) {
OWSFail(@"%@ Can't finalize without thread", self.logTag); OWSFail(@"%@ Can't finalize without thread", self.logTag);
@ -1087,12 +1086,10 @@ NS_ASSUME_NONNULL_BEGIN
[incomingMessage saveWithTransaction:transaction]; [incomingMessage saveWithTransaction:transaction];
// Any messages sent from the current user - from this device or another - should be // Any messages sent from the current user - from this device or another - should be automatically marked as read.
// automatically marked as read. if ([envelope.source isEqualToString:TSAccountManager.localNumber]) {
BOOL shouldMarkMessageAsRead = [envelope.source isEqualToString:localNumber];
if (shouldMarkMessageAsRead) {
// Don't send a read receipt for messages sent by ourselves. // 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; TSQuotedMessage *_Nullable quotedMessage = incomingMessage.quotedMessage;
@ -1130,8 +1127,9 @@ NS_ASSUME_NONNULL_BEGIN
[OWSReadReceiptManager.sharedManager applyEarlyReadReceiptsForIncomingMessage:incomingMessage [OWSReadReceiptManager.sharedManager applyEarlyReadReceiptsForIncomingMessage:incomingMessage
transaction:transaction]; transaction:transaction];
[OWSDisappearingMessagesJob becomeConsistentWithConfigurationForMessage:incomingMessage [[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithConfigurationForMessage:incomingMessage
contactsManager:self.contactsManager]; contactsManager:self.contactsManager
transaction:transaction];
// Update thread preview in inbox // Update thread preview in inbox
[thread touchWithTransaction:transaction]; [thread touchWithTransaction:transaction];

@ -84,14 +84,6 @@ NS_SWIFT_NAME(MessageSender)
success:(void (^)(void))successHandler success:(void (^)(void))successHandler
failure:(void (^)(NSError *error))failureHandler; 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 @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

@ -7,6 +7,7 @@
#import "ContactsUpdater.h" #import "ContactsUpdater.h"
#import "NSData+keyVersionByte.h" #import "NSData+keyVersionByte.h"
#import "NSData+messagePadding.h" #import "NSData+messagePadding.h"
#import "NSDate+OWS.h"
#import "NSError+MessageSending.h" #import "NSError+MessageSending.h"
#import "OWSBackgroundTask.h" #import "OWSBackgroundTask.h"
#import "OWSBlockingManager.h" #import "OWSBlockingManager.h"
@ -1052,13 +1053,11 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
[self sendSyncTranscriptForMessage:message]; [self sendSyncTranscriptForMessage:message];
} }
[OWSDisappearingMessagesJob setExpirationForMessage:message]; [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
} [[OWSDisappearingMessagesJob sharedJob] startAnyExpirationForMessage:message
expirationStartedAt:[NSDate ows_millisecondTimeStamp]
- (void)becomeConsistentWithDisappearingConfigurationForMessage:(TSOutgoingMessage *)outgoingMessage transaction:transaction];
{ }];
[OWSDisappearingMessagesJob becomeConsistentWithConfigurationForMessage:outgoingMessage
contactsManager:self.contactsManager];
} }
- (void)handleSendToMyself:(TSOutgoingMessage *)outgoingMessage - (void)handleSendToMyself:(TSOutgoingMessage *)outgoingMessage

@ -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 NS_ASSUME_NONNULL_BEGIN
@ -8,8 +8,8 @@ NS_ASSUME_NONNULL_BEGIN
@class TSIncomingMessage; @class TSIncomingMessage;
@class TSOutgoingMessage; @class TSOutgoingMessage;
@class TSThread; @class TSThread;
@class YapDatabaseReadWriteTransaction;
@class YapDatabaseReadTransaction; @class YapDatabaseReadTransaction;
@class YapDatabaseReadWriteTransaction;
extern NSString *const kIncomingMessageMarkedAsReadNotification; extern NSString *const kIncomingMessageMarkedAsReadNotification;
@ -50,6 +50,7 @@ extern NSString *const kIncomingMessageMarkedAsReadNotification;
#pragma mark - Linked Device Read Receipts #pragma mark - Linked Device Read Receipts
- (void)processReadReceiptsFromLinkedDevice:(NSArray<OWSSignalServiceProtosSyncMessageRead *> *)readReceiptProtos - (void)processReadReceiptsFromLinkedDevice:(NSArray<OWSSignalServiceProtosSyncMessageRead *> *)readReceiptProtos
readTimestamp:(uint64_t)readTimestamp
transaction:(YapDatabaseReadWriteTransaction *)transaction; transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)applyEarlyReadReceiptsForIncomingMessage:(TSIncomingMessage *)message - (void)applyEarlyReadReceiptsForIncomingMessage:(TSIncomingMessage *)message

@ -4,6 +4,7 @@
#import "OWSReadReceiptManager.h" #import "OWSReadReceiptManager.h"
#import "AppReadiness.h" #import "AppReadiness.h"
#import "NSDate+OWS.h"
#import "NSNotificationCenter+OWS.h" #import "NSNotificationCenter+OWS.h"
#import "OWSLinkedDeviceReadReceipt.h" #import "OWSLinkedDeviceReadReceipt.h"
#import "OWSMessageSender.h" #import "OWSMessageSender.h"
@ -289,6 +290,7 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self markAsReadBeforeTimestamp:timestamp [self markAsReadBeforeTimestamp:timestamp
thread:thread thread:thread
readTimestamp:[NSDate ows_millisecondTimeStamp]
wasLocal:YES wasLocal:YES
transaction:transaction]; transaction:transaction];
}]; }];
@ -307,10 +309,12 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
OWSAssert(messageAuthorId.length > 0); OWSAssert(messageAuthorId.length > 0);
OWSLinkedDeviceReadReceipt *newReadReceipt = 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]; 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 // If there's an existing "linked device" read receipt for the same thread with
// a newer timestamp, discard this "linked device" read receipt. // a newer timestamp, discard this "linked device" read receipt.
DDLogVerbose(@"%@ Ignoring redundant read receipt for linked devices.", self.logTag); DDLogVerbose(@"%@ Ignoring redundant read receipt for linked devices.", self.logTag);
@ -426,19 +430,25 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
OWSLinkedDeviceReadReceipt *_Nullable readReceipt = OWSLinkedDeviceReadReceipt *_Nullable readReceipt =
[OWSLinkedDeviceReadReceipt findLinkedDeviceReadReceiptWithSenderId:senderId [OWSLinkedDeviceReadReceipt findLinkedDeviceReadReceiptWithSenderId:senderId
timestamp:timestamp messageIdTimestamp:timestamp
transaction:transaction]; transaction:transaction];
if (!readReceipt) { if (!readReceipt) {
return; return;
} }
[message markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:YES];
[message markAsReadAtTimestamp:readReceipt.readTimestamp sendReadReceipt:NO transaction:transaction];
[readReceipt removeWithTransaction:transaction]; [readReceipt removeWithTransaction:transaction];
[[NSNotificationCenter defaultCenter] postNotificationNameAsync:kIncomingMessageMarkedAsReadNotification [transaction addCompletionQueue:nil
object:message]; completionBlock:^{
[[NSNotificationCenter defaultCenter]
postNotificationNameAsync:kIncomingMessageMarkedAsReadNotification
object:message];
}];
} }
- (void)processReadReceiptsFromLinkedDevice:(NSArray<OWSSignalServiceProtosSyncMessageRead *> *)readReceiptProtos - (void)processReadReceiptsFromLinkedDevice:(NSArray<OWSSignalServiceProtosSyncMessageRead *> *)readReceiptProtos
readTimestamp:(uint64_t)readTimestamp
transaction:(YapDatabaseReadWriteTransaction *)transaction transaction:(YapDatabaseReadWriteTransaction *)transaction
{ {
OWSAssert(readReceiptProtos); OWSAssert(readReceiptProtos);
@ -446,41 +456,53 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
for (OWSSignalServiceProtosSyncMessageRead *readReceiptProto in readReceiptProtos) { for (OWSSignalServiceProtosSyncMessageRead *readReceiptProto in readReceiptProtos) {
NSString *_Nullable senderId = readReceiptProto.sender; NSString *_Nullable senderId = readReceiptProto.sender;
uint64_t timestamp = readReceiptProto.timestamp; uint64_t messageIdTimestamp = readReceiptProto.timestamp;
BOOL isValid = senderId.length > 0 && timestamp > 0;
if (!isValid) { if (senderId.length == 0) {
OWSProdLogAndFail(@"%@ in %s senderId was unexpectedly nil", self.logTag, __PRETTY_FUNCTION__);
continue;
}
if (messageIdTimestamp == 0) {
OWSProdLogAndFail(@"%@ in %s messageIdTimestamp was unexpectedly 0", self.logTag, __PRETTY_FUNCTION__);
continue; continue;
} }
NSArray<TSIncomingMessage *> *messages = (NSArray<TSIncomingMessage *> *) [TSInteraction interactionsWithTimestamp:timestamp NSArray<TSIncomingMessage *> *messages
ofClass:[TSIncomingMessage class] = (NSArray<TSIncomingMessage *> *)[TSInteraction interactionsWithTimestamp:messageIdTimestamp
withTransaction:transaction]; ofClass:[TSIncomingMessage class]
withTransaction:transaction];
if (messages.count > 0) { if (messages.count > 0) {
for (TSIncomingMessage *message in messages) { for (TSIncomingMessage *message in messages) {
NSTimeInterval secondsSinceRead = [NSDate new].timeIntervalSince1970 - readTimestamp / 1000;
OWSAssert([message isKindOfClass:[TSIncomingMessage class]]); OWSAssert([message isKindOfClass:[TSIncomingMessage class]]);
DDLogDebug(@"%@ read on linked device %f seconds ago", self.logTag, secondsSinceRead);
[self markAsReadOnLinkedDevice:message [self markAsReadOnLinkedDevice:message readTimestamp:readTimestamp transaction:transaction];
transaction:transaction];
} }
} else { } else {
// Received read receipt for unknown incoming message. // Received read receipt for unknown incoming message.
// Persist in case we receive the incoming message later. // 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]; [readReceipt saveWithTransaction:transaction];
} }
} }
} }
- (void)markAsReadOnLinkedDevice:(TSIncomingMessage *)message - (void)markAsReadOnLinkedDevice:(TSIncomingMessage *)message
readTimestamp:(uint64_t)readTimestamp
transaction:(YapDatabaseReadWriteTransaction *)transaction transaction:(YapDatabaseReadWriteTransaction *)transaction
{ {
OWSAssert(message); OWSAssert(message);
OWSAssert(transaction); 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. // which reflect sender time.
[self markAsReadBeforeTimestamp:message.timestampForSorting [self markAsReadBeforeTimestamp:message.timestampForSorting
thread:[message threadWithTransaction:transaction] thread:[message threadWithTransaction:transaction]
readTimestamp:readTimestamp
wasLocal:NO wasLocal:NO
transaction:transaction]; transaction:transaction];
} }
@ -489,6 +511,7 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
- (void)markAsReadBeforeTimestamp:(uint64_t)timestamp - (void)markAsReadBeforeTimestamp:(uint64_t)timestamp
thread:(TSThread *)thread thread:(TSThread *)thread
readTimestamp:(uint64_t)readTimestamp
wasLocal:(BOOL)wasLocal wasLocal:(BOOL)wasLocal
transaction:(YapDatabaseReadWriteTransaction *)transaction transaction:(YapDatabaseReadWriteTransaction *)transaction
{ {
@ -496,7 +519,7 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
OWSAssert(thread); OWSAssert(thread);
OWSAssert(transaction); OWSAssert(transaction);
NSMutableArray<id<OWSReadTracking>> *interactions = [NSMutableArray new]; NSMutableArray<id<OWSReadTracking>> *newlyReadList = [NSMutableArray new];
[[TSDatabaseView unseenDatabaseViewExtension:transaction] [[TSDatabaseView unseenDatabaseViewExtension:transaction]
enumerateRowsInGroup:thread.uniqueId enumerateRowsInGroup:thread.uniqueId
@ -525,26 +548,32 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
OWSAssert(!possiblyRead.read); OWSAssert(!possiblyRead.read);
if (!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; return;
} }
if (wasLocal) { if (wasLocal) {
DDLogError(@"Marking %zd messages as read locally.", interactions.count); DDLogError(@"Marking %zu messages as read locally.", newlyReadList.count);
} else { } 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<OWSReadTracking> possiblyRead in interactions) { for (id<OWSReadTracking> readItem in newlyReadList) {
[possiblyRead markAsReadWithTransaction:transaction sendReadReceipt:wasLocal updateExpiration:YES]; [readItem markAsReadAtTimestamp:readTimestamp sendReadReceipt:wasLocal transaction:transaction];
if ([possiblyRead isKindOfClass:[TSIncomingMessage class]]) { if ([readItem isKindOfClass:[TSIncomingMessage class]]) {
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)possiblyRead; TSIncomingMessage *incomingMessage = (TSIncomingMessage *)readItem;
[[NSNotificationCenter defaultCenter] postNotificationNameAsync:kIncomingMessageMarkedAsReadNotification [transaction addCompletionQueue:nil
object:incomingMessage]; completionBlock:^{
[[NSNotificationCenter defaultCenter]
postNotificationNameAsync:kIncomingMessageMarkedAsReadNotification
object:incomingMessage];
}];
} }
} }
} }

@ -1,5 +1,5 @@
// //
// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// //
@class YapDatabaseReadWriteTransaction; @class YapDatabaseReadWriteTransaction;
@ -15,16 +15,18 @@
*/ */
@property (nonatomic, readonly, getter=wasRead) BOOL read; @property (nonatomic, readonly, getter=wasRead) BOOL read;
@property (nonatomic, readonly) uint64_t expireStartedAt;
@property (nonatomic, readonly) uint64_t timestampForSorting; @property (nonatomic, readonly) uint64_t timestampForSorting;
@property (nonatomic, readonly) NSString *uniqueThreadId; @property (nonatomic, readonly) NSString *uniqueThreadId;
- (BOOL)shouldAffectUnreadCounts; - (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 - (void)markAsReadAtTimestamp:(uint64_t)readTimestamp
sendReadReceipt:(BOOL)sendReadReceipt sendReadReceipt:(BOOL)sendReadReceipt
updateExpiration:(BOOL)updateExpiration; transaction:(YapDatabaseReadWriteTransaction *)transaction;
@end @end

@ -89,15 +89,21 @@ NSUInteger TSCallCurrentSchemaVersion = 1;
#pragma mark - OWSReadTracking #pragma mark - OWSReadTracking
- (uint64_t)expireStartedAt
{
return 0;
}
- (BOOL)shouldAffectUnreadCounts - (BOOL)shouldAffectUnreadCounts
{ {
return YES; return YES;
} }
- (void)markAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction - (void)markAsReadAtTimestamp:(uint64_t)readTimestamp
sendReadReceipt:(BOOL)sendReadReceipt sendReadReceipt:(BOOL)sendReadReceipt
updateExpiration:(BOOL)updateExpiration transaction:(YapDatabaseReadWriteTransaction *)transaction
{ {
OWSAssert(transaction); OWSAssert(transaction);
if (_read) { if (_read) {
@ -110,7 +116,7 @@ NSUInteger TSCallCurrentSchemaVersion = 1;
[self saveWithTransaction:transaction]; [self saveWithTransaction:transaction];
[self touchThreadWithTransaction: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 #pragma mark - Methods

Loading…
Cancel
Save