Merge branch 'mkirk/disappearing-messages'

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

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

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

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

@ -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<NSNumber *> *)validDurationsSeconds;

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

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

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

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

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

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

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

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

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

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

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

@ -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<ContactsManagerProtocol>)contactsManager;
- (void)becomeConsistentWithConfigurationForMessage:(TSMessage *)message
contactsManager:(id<ContactsManagerProtocol>)contactsManager
transaction:(YapDatabaseReadWriteTransaction *)transaction;
// Clean up any messages that expired since last launch immediately
// and continue cleaning in the background.

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

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

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

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

@ -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<OWSSignalServiceProtosSyncMessageRead *> *)readReceiptProtos
readTimestamp:(uint64_t)readTimestamp
transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)applyEarlyReadReceiptsForIncomingMessage:(TSIncomingMessage *)message

@ -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<OWSSignalServiceProtosSyncMessageRead *> *)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<TSIncomingMessage *> *messages = (NSArray<TSIncomingMessage *> *) [TSInteraction interactionsWithTimestamp:timestamp
ofClass:[TSIncomingMessage class]
withTransaction:transaction];
if (messageIdTimestamp == 0) {
OWSProdLogAndFail(@"%@ in %s messageIdTimestamp was unexpectedly 0", self.logTag, __PRETTY_FUNCTION__);
continue;
}
NSArray<TSIncomingMessage *> *messages
= (NSArray<TSIncomingMessage *> *)[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<id<OWSReadTracking>> *interactions = [NSMutableArray new];
NSMutableArray<id<OWSReadTracking>> *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<OWSReadTracking> 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<OWSReadTracking> 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];
}];
}
}
}

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

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

Loading…
Cancel
Save