Refactor linked device read receipts.

// FREEBIE
This commit is contained in:
Matthew Chen 2017-09-26 11:45:22 -04:00
parent 289291e03d
commit ffe44e68be
14 changed files with 262 additions and 436 deletions

View file

@ -11,7 +11,7 @@
#import <SignalServiceKit/NSDate+OWS.h>
#import <SignalServiceKit/OWSMessageReceiver.h>
#import <SignalServiceKit/OWSMessageSender.h>
#import <SignalServiceKit/OWSReadReceiptsProcessor.h>
#import <SignalServiceKit/OWSReadReceiptManager.h>
#import <SignalServiceKit/OWSSignalService.h>
#import <SignalServiceKit/TSAccountManager.h>
#import <SignalServiceKit/TSIncomingMessage.h>
@ -85,7 +85,7 @@ NSString *const Signal_Message_MarkAsRead_Identifier = @"Signal_Message_MarkAsRe
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleMessageRead:)
name:OWSReadReceiptsProcessorMarkedMessageAsReadNotification
name:kMessageMarkedAsReadNotification
object:nil];
return self;
@ -93,6 +93,8 @@ NSString *const Signal_Message_MarkAsRead_Identifier = @"Signal_Message_MarkAsRe
- (void)handleMessageRead:(NSNotification *)notification
{
OWSAssert([NSThread isMainThread]);
if ([notification.object isKindOfClass:[TSIncomingMessage class]]) {
TSIncomingMessage *message = (TSIncomingMessage *)notification.object;

View file

@ -0,0 +1,21 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "TSYapDatabaseObject.h"
NS_ASSUME_NONNULL_BEGIN
@interface OWSLinkedDeviceReadReceipt : TSYapDatabaseObject
@property (nonatomic, readonly) NSString *senderId;
@property (nonatomic, readonly) uint64_t timestamp;
- (instancetype)initWithSenderId:(NSString *)senderId timestamp:(uint64_t)timestamp;
+ (nullable OWSLinkedDeviceReadReceipt *)linkedDeviceReadReceiptWithSenderId:(NSString *)senderId
timestamp:(uint64_t)timestamp;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,53 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "OWSLinkedDeviceReadReceipt.h"
NS_ASSUME_NONNULL_BEGIN
@implementation OWSLinkedDeviceReadReceipt
- (instancetype)initWithSenderId:(NSString *)senderId timestamp:(uint64_t)timestamp;
{
OWSAssert(senderId.length > 0 && timestamp > 0);
self = [super initWithUniqueId:[OWSLinkedDeviceReadReceipt uniqueIdForSenderId:senderId timestamp:timestamp]];
if (!self) {
return self;
}
_senderId = senderId;
_timestamp = timestamp;
return self;
}
+ (NSString *)uniqueIdForSenderId:(NSString *)senderId timestamp:(uint64_t)timestamp
{
OWSAssert(senderId.length > 0 && timestamp > 0);
return [NSString stringWithFormat:@"%@ %llu", senderId, timestamp];
}
+ (nullable OWSLinkedDeviceReadReceipt *)linkedDeviceReadReceiptWithSenderId:(NSString *)senderId
timestamp:(uint64_t)timestamp
{
return [OWSLinkedDeviceReadReceipt fetchObjectWithUniqueID:[self uniqueIdForSenderId:senderId timestamp:timestamp]];
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
@end
NS_ASSUME_NONNULL_END

View file

@ -1,23 +0,0 @@
// Copyright © 2016 Open Whisper Systems. All rights reserved.
#import "TSYapDatabaseObject.h"
NS_ASSUME_NONNULL_BEGIN
@class YapDatabase;
@interface OWSReadReceipt : TSYapDatabaseObject
@property (nonatomic, readonly) NSString *senderId;
@property (nonatomic, readonly) uint64_t timestamp;
@property (nonatomic, readonly, getter=isValid) BOOL valid;
@property (nonatomic, readonly) NSArray<NSString *> *validationErrorMessages;
- (instancetype)initWithSenderId:(NSString *)senderId timestamp:(uint64_t)timestamp;
+ (nullable instancetype)firstWithSenderId:(NSString *)senderId timestamp:(uint64_t)timestamp;
+ (void)asyncRegisterIndexOnSenderIdAndTimestampWithDatabase:(YapDatabase *)database;
@end
NS_ASSUME_NONNULL_END

View file

@ -1,140 +0,0 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "OWSReadReceipt.h"
#import <YapDatabase/YapDatabase.h>
#import <YapDatabase/YapDatabaseConnection.h>
#import <YapDatabase/YapDatabaseSecondaryIndex.h>
NS_ASSUME_NONNULL_BEGIN
NSString *const OWSReadReceiptIndexOnSenderIdAndTimestamp = @"OWSReadReceiptIndexOnSenderIdAndTimestamp";
NSString *const OWSReadReceiptColumnTimestamp = @"timestamp";
NSString *const OWSReadReceiptColumnSenderId = @"senderId";
@implementation OWSReadReceipt
- (instancetype)initWithSenderId:(NSString *)senderId timestamp:(uint64_t)timestamp;
{
self = [super init];
if (!self) {
return self;
}
NSMutableArray<NSString *> *validationErrorMessage = [NSMutableArray new];
if (!senderId) {
[validationErrorMessage addObject:@"Must specify sender id"];
}
_senderId = senderId;
if (!timestamp) {
[validationErrorMessage addObject:@"Must specify timestamp"];
}
_timestamp = timestamp;
_valid = validationErrorMessage.count == 0;
_validationErrorMessages = [validationErrorMessage copy];
return self;
}
- (instancetype)initWithCoder:(NSCoder *)decoder
{
self = [super initWithCoder:decoder];
if (!self) {
return nil;
}
_valid = YES;
_validationErrorMessages = @[];
return self;
}
+ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey
{
// Don't store ephemeral properties.
if ([propertyKey isEqualToString:@"valid"] || [propertyKey isEqualToString:@"validationErrorMessages"]) {
return MTLPropertyStorageNone;
} else {
return [super storageBehaviorForPropertyWithKey:propertyKey];
}
}
+ (void)asyncRegisterIndexOnSenderIdAndTimestampWithDatabase:(YapDatabase *)database
{
YapDatabaseSecondaryIndexSetup *setup = [YapDatabaseSecondaryIndexSetup new];
[setup addColumn:OWSReadReceiptColumnSenderId withType:YapDatabaseSecondaryIndexTypeText];
[setup addColumn:OWSReadReceiptColumnTimestamp withType:YapDatabaseSecondaryIndexTypeInteger];
YapDatabaseSecondaryIndexHandler *handler =
[YapDatabaseSecondaryIndexHandler withObjectBlock:^(YapDatabaseReadTransaction *transaction,
NSMutableDictionary *dict,
NSString *collection,
NSString *key,
id object) {
if ([object isKindOfClass:[OWSReadReceipt class]]) {
OWSReadReceipt *readReceipt = (OWSReadReceipt *)object;
dict[OWSReadReceiptColumnSenderId] = readReceipt.senderId;
dict[OWSReadReceiptColumnTimestamp] = @(readReceipt.timestamp);
}
}];
YapDatabaseSecondaryIndex *index = [[YapDatabaseSecondaryIndex alloc] initWithSetup:setup handler:handler];
[database
asyncRegisterExtension:index
withName:OWSReadReceiptIndexOnSenderIdAndTimestamp
completionBlock:^(BOOL ready) {
if (ready) {
DDLogDebug(@"%@ Successfully set up extension: %@",
self.tag,
OWSReadReceiptIndexOnSenderIdAndTimestamp);
} else {
DDLogError(
@"%@ Unable to setup extension: %@", self.tag, OWSReadReceiptIndexOnSenderIdAndTimestamp);
}
}];
}
+ (nullable instancetype)firstWithSenderId:(NSString *)senderId timestamp:(uint64_t)timestamp
{
__block OWSReadReceipt *foundReadReceipt;
NSString *queryFormat = [NSString
stringWithFormat:@"WHERE %@ = ? AND %@ = ?", OWSReadReceiptColumnSenderId, OWSReadReceiptColumnTimestamp];
YapDatabaseQuery *query = [YapDatabaseQuery queryWithFormat:queryFormat, senderId, @(timestamp)];
[[self dbReadConnection] readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
[[transaction ext:OWSReadReceiptIndexOnSenderIdAndTimestamp]
enumerateKeysAndObjectsMatchingQuery:query
usingBlock:^(NSString *collection, NSString *key, id object, BOOL *stop) {
if (![object isKindOfClass:[OWSReadReceipt class]]) {
DDLogError(@"%@ Unexpected object in index: %@", self.tag, object);
return;
}
foundReadReceipt = (OWSReadReceipt *)object;
*stop = YES;
}];
}];
return foundReadReceipt;
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
@end
NS_ASSUME_NONNULL_END

View file

@ -6,11 +6,11 @@
NS_ASSUME_NONNULL_BEGIN
@class OWSReadReceipt;
@class OWSLinkedDeviceReadReceipt;
@interface OWSReadReceiptsForLinkedDevicesMessage : OWSOutgoingSyncMessage
- (instancetype)initWithReadReceipts:(NSArray<OWSReadReceipt *> *)readReceipts;
- (instancetype)initWithReadReceipts:(NSArray<OWSLinkedDeviceReadReceipt *> *)readReceipts;
@end

View file

@ -3,20 +3,20 @@
//
#import "OWSReadReceiptsForLinkedDevicesMessage.h"
#import "OWSReadReceipt.h"
#import "OWSLinkedDeviceReadReceipt.h"
#import "OWSSignalServiceProtos.pb.h"
NS_ASSUME_NONNULL_BEGIN
@interface OWSReadReceiptsForLinkedDevicesMessage ()
@property (nonatomic, readonly) NSArray<OWSReadReceipt *> *readReceipts;
@property (nonatomic, readonly) NSArray<OWSLinkedDeviceReadReceipt *> *readReceipts;
@end
@implementation OWSReadReceiptsForLinkedDevicesMessage
- (instancetype)initWithReadReceipts:(NSArray<OWSReadReceipt *> *)readReceipts
- (instancetype)initWithReadReceipts:(NSArray<OWSLinkedDeviceReadReceipt *> *)readReceipts
{
self = [super init];
if (!self) {
@ -31,7 +31,7 @@ NS_ASSUME_NONNULL_BEGIN
- (OWSSignalServiceProtosSyncMessageBuilder *)syncMessageBuilder
{
OWSSignalServiceProtosSyncMessageBuilder *syncMessageBuilder = [OWSSignalServiceProtosSyncMessageBuilder new];
for (OWSReadReceipt *readReceipt in self.readReceipts) {
for (OWSLinkedDeviceReadReceipt *readReceipt in self.readReceipts) {
OWSSignalServiceProtosSyncMessageReadBuilder *readProtoBuilder =
[OWSSignalServiceProtosSyncMessageReadBuilder new];
[readProtoBuilder setSender:readReceipt.senderId];

View file

@ -4,7 +4,6 @@
#import "OWSReadReceiptsForSenderMessage.h"
#import "NSDate+OWS.h"
#import "OWSReadReceipt.h"
#import "OWSSignalServiceProtos.pb.h"
#import "SignalRecipient.h"

View file

@ -1,41 +0,0 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class OWSReadReceipt;
@class OWSSignalServiceProtosSyncMessageRead;
@class TSIncomingMessage;
@class TSStorageManager;
@class YapDatabaseReadWriteTransaction;
extern NSString *const OWSReadReceiptsProcessorMarkedMessageAsReadNotification;
// TODO:
@interface OWSReadReceiptsProcessor : NSObject
/**
* Mark existing messages as read from the given received read receipts.
*/
- (instancetype)initWithReadReceiptProtos:(NSArray<OWSSignalServiceProtosSyncMessageRead *> *)readReceiptProtos
storageManager:(TSStorageManager *)storageManager;
/**
* Mark a new message as read in the rare (but does happen!) case that we receive the read receipt before the message
* the read receipt refers to.
*/
- (instancetype)initWithIncomingMessage:(TSIncomingMessage *)incomingMessage
storageManager:(TSStorageManager *)storageManager;
- (instancetype)initWithReadReceipts:(NSArray<OWSReadReceipt *> *)readReceipts
storageManager:(TSStorageManager *)storageManager NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
- (void)process;
- (void)processWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
@end
NS_ASSUME_NONNULL_END

View file

@ -1,162 +0,0 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "OWSReadReceiptsProcessor.h"
#import "NSNotificationCenter+OWS.h"
#import "OWSDisappearingMessagesJob.h"
#import "OWSReadReceipt.h"
#import "OWSSignalServiceProtos.pb.h"
#import "TSContactThread.h"
#import "TSDatabaseView.h"
#import "TSIncomingMessage.h"
#import "TSStorageManager.h"
#import <YapDatabase/YapDatabaseConnection.h>
NS_ASSUME_NONNULL_BEGIN
NSString *const OWSReadReceiptsProcessorMarkedMessageAsReadNotification =
@"OWSReadReceiptsProcessorMarkedMessageAsReadNotification";
@interface OWSReadReceiptsProcessor ()
@property (nonatomic, readonly) NSArray<OWSReadReceipt *> *readReceipts;
@property (nonatomic, readonly) TSStorageManager *storageManager;
@end
@implementation OWSReadReceiptsProcessor
- (instancetype)initWithReadReceipts:(NSArray<OWSReadReceipt *> *)readReceipts
storageManager:(TSStorageManager *)storageManager;
{
self = [super init];
if (!self) {
return self;
}
_readReceipts = [readReceipts copy];
_storageManager = storageManager;
return self;
}
- (instancetype)initWithReadReceiptProtos:(NSArray<OWSSignalServiceProtosSyncMessageRead *> *)readReceiptProtos
storageManager:(TSStorageManager *)storageManager
{
NSMutableArray<OWSReadReceipt *> *readReceipts = [NSMutableArray new];
for (OWSSignalServiceProtosSyncMessageRead *readReceiptProto in readReceiptProtos) {
OWSReadReceipt *readReceipt =
[[OWSReadReceipt alloc] initWithSenderId:readReceiptProto.sender timestamp:readReceiptProto.timestamp];
if (readReceipt.isValid) {
[readReceipts addObject:readReceipt];
} else {
DDLogError(@"%@ Received invalid read receipt: %@", self.tag, readReceipt.validationErrorMessages);
}
}
return [self initWithReadReceipts:[readReceipts copy] storageManager:storageManager];
}
- (instancetype)initWithIncomingMessage:(TSIncomingMessage *)message storageManager:(TSStorageManager *)storageManager
{
NSString *messageAuthorId = message.messageAuthorId;
OWSAssert(messageAuthorId.length > 0);
OWSReadReceipt *readReceipt = [OWSReadReceipt firstWithSenderId:messageAuthorId timestamp:message.timestamp];
if (readReceipt) {
DDLogInfo(@"%@ Found prior read receipt for incoming message.", self.tag);
return [self initWithReadReceipts:@[ readReceipt ] storageManager:storageManager];
} else {
// no-op
return [self initWithReadReceipts:@[] storageManager:storageManager];
}
}
- (void)process
{
[[self.storageManager newDatabaseConnection] readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self processWithTransaction:transaction];
}];
}
- (void)processWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(transaction);
DDLogDebug(@"%@ Processing %ld read receipts.", self.tag, (unsigned long)self.readReceipts.count);
for (OWSReadReceipt *readReceipt in self.readReceipts) {
TSIncomingMessage *message = [TSIncomingMessage findMessageWithAuthorId:readReceipt.senderId
timestamp:readReceipt.timestamp
transaction:transaction];
if (message) {
OWSAssert(message.thread);
// Mark all unread messages in this thread that are older than message specified in the read
// receipt.
NSMutableArray<id<OWSReadTracking>> *interactionsToMarkAsRead = [NSMutableArray new];
// Always mark the message specified by the read receipt as read.
[interactionsToMarkAsRead addObject:message];
[[TSDatabaseView unseenDatabaseViewExtension:transaction]
enumerateRowsInGroup:message.uniqueThreadId
usingBlock:^(NSString *collection,
NSString *key,
id object,
id metadata,
NSUInteger index,
BOOL *stop) {
TSInteraction *interaction = object;
if (interaction.timestampForSorting > message.timestampForSorting) {
*stop = YES;
return;
}
id<OWSReadTracking> possiblyRead = (id<OWSReadTracking>)object;
OWSAssert(!possiblyRead.read);
[interactionsToMarkAsRead addObject:possiblyRead];
}];
for (id<OWSReadTracking> interaction in interactionsToMarkAsRead) {
// * Don't send a read receipt in response to a read receipt.
// * Don't update expiration; we'll do that in the next statement.
[interaction markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:NO];
if ([interaction isKindOfClass:[TSMessage class]]) {
TSMessage *otherMessage = (TSMessage *)interaction;
// Update expiration using the timestamp from the readReceipt.
[OWSDisappearingMessagesJob setExpirationForMessage:otherMessage
expirationStartedAt:readReceipt.timestamp];
// Fire event that will cancel any pending notifications for this message.
[[NSNotificationCenter defaultCenter]
postNotificationNameAsync:OWSReadReceiptsProcessorMarkedMessageAsReadNotification
object:otherMessage];
}
}
// If it was previously saved, no need to keep it around any longer.
[readReceipt removeWithTransaction:transaction];
} else {
DDLogDebug(@"%@ Received read receipt for an unknown message. Saving it for later.", self.tag);
[readReceipt saveWithTransaction:transaction];
}
}
}
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
@end
NS_ASSUME_NONNULL_END

View file

@ -19,7 +19,6 @@
#import "OWSIncomingSentMessageTranscript.h"
#import "OWSMessageSender.h"
#import "OWSReadReceiptManager.h"
#import "OWSReadReceiptsProcessor.h"
#import "OWSRecordTranscriptJob.h"
#import "OWSSyncContactsMessage.h"
#import "OWSSyncGroupsMessage.h"
@ -570,10 +569,8 @@ NS_ASSUME_NONNULL_BEGIN
} else if (syncMessage.read.count > 0) {
DDLogInfo(@"%@ Received %ld read receipt(s)", self.tag, (u_long)syncMessage.read.count);
OWSReadReceiptsProcessor *readReceiptsProcessor =
[[OWSReadReceiptsProcessor alloc] initWithReadReceiptProtos:syncMessage.read
storageManager:self.storageManager];
[readReceiptsProcessor processWithTransaction:transaction];
[OWSReadReceiptManager.sharedManager processReadReceiptsFromLinkedDevice:syncMessage.read
transaction:transaction];
} else if (syncMessage.hasVerified) {
DDLogInfo(@"%@ Received verification state for %@", self.tag, syncMessage.verified.destination);
// TODO: Do this synchronously.
@ -900,14 +897,12 @@ NS_ASSUME_NONNULL_BEGIN
}
if (thread && incomingMessage) {
// In case we already have a read receipt for this new message (this happens sometimes).
[OWSReadReceiptManager.sharedManager updateIncomingMessage:incomingMessage
transaction:transaction];
// TODO: Do this synchronously.
dispatch_async(dispatch_get_main_queue(), ^{
// In case we already have a read receipt for this new message (happens sometimes).
OWSReadReceiptsProcessor *readReceiptsProcessor =
[[OWSReadReceiptsProcessor alloc] initWithIncomingMessage:incomingMessage
storageManager:self.storageManager];
[readReceiptsProcessor process];
[OWSDisappearingMessagesJob becomeConsistentWithConfigurationForMessage:incomingMessage
contactsManager:self.contactsManager];

View file

@ -6,11 +6,14 @@ NS_ASSUME_NONNULL_BEGIN
@class OWSSignalServiceProtosEnvelope;
@class OWSSignalServiceProtosReceiptMessage;
@class OWSSignalServiceProtosSyncMessageRead;
@class TSIncomingMessage;
@class TSOutgoingMessage;
@class TSThread;
@class YapDatabaseReadWriteTransaction;
extern NSString *const kMessageMarkedAsReadNotification;
// There are four kinds of read receipts:
//
// * Read receipts that this client sends to linked
@ -26,12 +29,14 @@ NS_ASSUME_NONNULL_BEGIN
// * These read receipts are saved so that they can be applied
// if they arrive before the corresponding message.
//
// TODO: Merge OWSReadReceiptsProcessor into this class.
// This manager is responsible for handling and emitting all four kinds.
@interface OWSReadReceiptManager : NSObject
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)sharedManager;
#pragma mark - Sender/Recipient Read Receipts
// This method should be called when we receive a read receipt
// from a user to whom we have sent a message.
//
@ -42,6 +47,16 @@ NS_ASSUME_NONNULL_BEGIN
- (void)updateOutgoingMessageFromLinkedDevice:(TSOutgoingMessage *)message
transaction:(YapDatabaseReadWriteTransaction *)transaction;
#pragma mark - Linked Device Read Receipts
- (void)processReadReceiptsFromLinkedDevice:(NSArray<OWSSignalServiceProtosSyncMessageRead *> *)readReceiptProtos
transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)updateIncomingMessage:(TSIncomingMessage *)message
transaction:(YapDatabaseReadWriteTransaction *)transaction;
#pragma mark - Locally Read
// This method cues this manager:
//
// * ...to inform the sender that this message was read (if read receipts

View file

@ -3,8 +3,9 @@
//
#import "OWSReadReceiptManager.h"
#import "NSNotificationCenter+OWS.h"
#import "OWSLinkedDeviceReadReceipt.h"
#import "OWSMessageSender.h"
#import "OWSReadReceipt.h"
#import "OWSReadReceiptsForLinkedDevicesMessage.h"
#import "OWSReadReceiptsForSenderMessage.h"
#import "OWSSignalServiceProtos.pb.h"
@ -17,6 +18,8 @@
NS_ASSUME_NONNULL_BEGIN
NSString *const kMessageMarkedAsReadNotification = @"kMessageMarkedAsReadNotification";
@interface TSRecipientReadReceipt : TSYapDatabaseObject
@property (nonatomic, readonly) uint64_t sentTimestamp;
@ -115,7 +118,7 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
// we will send to our linked devices.
//
// Should only be accessed while synchronized on the OWSReadReceiptManager.
@property (nonatomic, readonly) NSMutableDictionary<NSString *, OWSReadReceipt *> *toLinkedDevicesReadReceiptMap;
@property (nonatomic, readonly) NSMutableDictionary<NSString *, OWSLinkedDeviceReadReceipt *> *toLinkedDevicesReadReceiptMap;
// A map of "recipient id"-to-"timestamp list" for read receipts that
// we will send to senders.
@ -211,7 +214,7 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
// Process read receipts every N seconds.
//
// We want a value high enough to allow us to effectively deduplicate,
// We want a value high enough to allow us to effectively de-duplicate,
// read receipts without being so high that we risk not sending read
// receipts due to app exit.
const CGFloat kProcessingFrequencySeconds = 3.f;
@ -288,46 +291,10 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
NSMutableArray<id<OWSReadTracking>> *interactions = [NSMutableArray new];
[[TSDatabaseView unseenDatabaseViewExtension:transaction]
enumerateRowsInGroup:thread.uniqueId
usingBlock:^(NSString *collection,
NSString *key,
id object,
id metadata,
NSUInteger index,
BOOL *stop) {
if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {
OWSFail(
@"Expected to conform to OWSReadTracking: object with class: %@ collection: %@ "
@"key: %@",
[object class],
collection,
key);
return;
}
id<OWSReadTracking> possiblyRead = (id<OWSReadTracking>)object;
if (possiblyRead.timestampForSorting > timestamp) {
*stop = YES;
return;
}
OWSAssert(!possiblyRead.read);
if (!possiblyRead.read) {
[interactions addObject:possiblyRead];
}
}];
if (interactions.count < 1) {
return;
}
DDLogError(@"Marking %zd messages as read.", interactions.count);
for (id<OWSReadTracking> possiblyRead in interactions) {
[possiblyRead markAsReadWithTransaction:transaction sendReadReceipt:YES updateExpiration:YES];
}
[self markAsReadBeforeTimestamp:timestamp
thread:thread
wasLocal:YES
transaction:transaction];
}];
});
}
@ -343,10 +310,10 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
NSString *messageAuthorId = message.messageAuthorId;
OWSAssert(messageAuthorId.length > 0);
OWSReadReceipt *newReadReceipt =
[[OWSReadReceipt alloc] initWithSenderId:messageAuthorId timestamp:message.timestamp];
OWSLinkedDeviceReadReceipt *newReadReceipt =
[[OWSLinkedDeviceReadReceipt alloc] initWithSenderId:messageAuthorId timestamp:message.timestamp];
OWSReadReceipt *_Nullable oldReadReceipt = self.toLinkedDevicesReadReceiptMap[threadUniqueId];
OWSLinkedDeviceReadReceipt *_Nullable oldReadReceipt = self.toLinkedDevicesReadReceiptMap[threadUniqueId];
if (oldReadReceipt && oldReadReceipt.timestamp > newReadReceipt.timestamp) {
// If there's an existing read receipt for the same thread with
// a newer timestamp, discard the new read receipt.
@ -445,6 +412,148 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
[TSRecipientReadReceipt removeRecipientIdsForTimestamp:message.timestamp transaction:transaction];
}
#pragma mark - Linked Device Read Receipts
- (void)updateIncomingMessage:(TSIncomingMessage *)message
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(message);
OWSAssert(transaction);
NSString *senderId = message.messageAuthorId;
uint64_t timestamp = message.timestamp;
if (senderId.length < 1 || timestamp < 1) {
OWSFail(@"%@ Invalid incoming message: %@ %llu", self.tag, senderId, timestamp);
return;
}
OWSLinkedDeviceReadReceipt *_Nullable readReceipt = [OWSLinkedDeviceReadReceipt linkedDeviceReadReceiptWithSenderId:senderId
timestamp:timestamp];
if (!readReceipt) {
return;
}
[message markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:YES];
[readReceipt removeWithTransaction:transaction];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter]
postNotificationNameAsync:kMessageMarkedAsReadNotification
object:message];
});
}
- (void)processReadReceiptsFromLinkedDevice:(NSArray<OWSSignalServiceProtosSyncMessageRead *> *)readReceiptProtos
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(readReceiptProtos);
OWSAssert(transaction);
for (OWSSignalServiceProtosSyncMessageRead *readReceiptProto in readReceiptProtos) {
NSString *_Nullable senderId = readReceiptProto.sender;
uint64_t timestamp = readReceiptProto.timestamp;
BOOL isValid = senderId.length > 0 && timestamp > 0;
if (!isValid) {
continue;
}
NSArray<TSIncomingMessage *> *messages = (NSArray<TSIncomingMessage *> *) [TSInteraction interactionsWithTimestamp:timestamp
ofClass:[TSIncomingMessage class]
withTransaction:transaction];
if (messages.count > 0) {
for (TSIncomingMessage *message in messages) {
OWSAssert([message isKindOfClass:[TSIncomingMessage class]]);
[self markAsReadOnLinkedDevice:message
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];
[readReceipt saveWithTransaction:transaction];
}
}
}
- (void)markAsReadOnLinkedDevice:(TSIncomingMessage *)message
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(message);
OWSAssert(transaction);
[self markAsReadBeforeTimestamp:message.timestamp
thread:message.thread
wasLocal:NO
transaction:transaction];
}
#pragma mark - Mark As Read
- (void)markAsReadBeforeTimestamp:(uint64_t)timestamp
thread:(TSThread *)thread
wasLocal:(BOOL)wasLocal
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(timestamp > 0);
OWSAssert(thread);
OWSAssert(transaction);
NSMutableArray<id<OWSReadTracking>> *interactions = [NSMutableArray new];
[[TSDatabaseView unseenDatabaseViewExtension:transaction]
enumerateRowsInGroup:thread.uniqueId
usingBlock:^(NSString *collection,
NSString *key,
id object,
id metadata,
NSUInteger index,
BOOL *stop) {
if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {
OWSFail(
@"Expected to conform to OWSReadTracking: object with class: %@ collection: %@ "
@"key: %@",
[object class],
collection,
key);
return;
}
id<OWSReadTracking> possiblyRead = (id<OWSReadTracking>)object;
if (possiblyRead.timestampForSorting > timestamp) {
*stop = YES;
return;
}
OWSAssert(!possiblyRead.read);
if (!possiblyRead.read) {
[interactions addObject:possiblyRead];
}
}];
if (interactions.count < 1) {
return;
}
if (wasLocal) {
DDLogError(@"Marking %zd messages as read locally.", interactions.count);
} else {
DDLogError(@"Marking %zd messages as read by linked device.", interactions.count);
}
for (id<OWSReadTracking> possiblyRead in interactions) {
[possiblyRead markAsReadWithTransaction:transaction sendReadReceipt:wasLocal updateExpiration:YES];
if ([possiblyRead isKindOfClass:[TSIncomingMessage class]]) {
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)possiblyRead;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter]
postNotificationNameAsync:kMessageMarkedAsReadNotification
object:incomingMessage];
});
}
}
}
#pragma mark - Settings
- (void)prepareCachedValues

View file

@ -9,7 +9,6 @@
#import "OWSFailedAttachmentDownloadsJob.h"
#import "OWSFailedMessagesJob.h"
#import "OWSIncomingMessageFinder.h"
#import "OWSReadReceipt.h"
#import "SignalRecipient.h"
#import "TSAttachmentStream.h"
#import "TSDatabaseSecondaryIndexes.h"
@ -332,10 +331,9 @@ void setDatabaseInitialized()
[TSDatabaseView asyncRegisterThreadOutgoingMessagesDatabaseView];
[TSDatabaseView asyncRegisterThreadSpecialMessagesDatabaseView];
// Register extensions which aren't essential for rendering threads async
// Register extensions which aren't essential for rendering threads async.
[[OWSIncomingMessageFinder new] asyncRegisterExtension];
[TSDatabaseView asyncRegisterSecondaryDevicesDatabaseView];
[OWSReadReceipt asyncRegisterIndexOnSenderIdAndTimestampWithDatabase:self.database];
[OWSDisappearingMessagesFinder asyncRegisterDatabaseExtensions:self];
OWSFailedMessagesJob *failedMessagesJob = [[OWSFailedMessagesJob alloc] initWithStorageManager:self];
[failedMessagesJob asyncRegisterDatabaseExtensions];