From 580781e3e4ad4f975d0208fc4fa76760e4144d01 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 1 Sep 2016 10:28:35 -0400 Subject: [PATCH] Incoming Read Receipts // FREEBIE --- src/Contacts/TSThread.m | 3 +- src/Contacts/Threads/TSContactThread.h | 2 + src/Devices/OWSReadReceipt.h | 16 +++++ src/Devices/OWSReadReceipt.m | 35 +++++++++++ src/Devices/OWSReadReceiptsProcessor.h | 15 +++++ src/Devices/OWSReadReceiptsProcessor.m | 62 +++++++++++++++++++ src/Messages/Interactions/TSIncomingMessage.h | 19 +++++- src/Messages/Interactions/TSIncomingMessage.m | 51 +++++++++++++++ src/Messages/TSMessagesManager.m | 10 ++- 9 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 src/Devices/OWSReadReceipt.h create mode 100644 src/Devices/OWSReadReceipt.m create mode 100644 src/Devices/OWSReadReceiptsProcessor.h create mode 100644 src/Devices/OWSReadReceiptsProcessor.m diff --git a/src/Contacts/TSThread.m b/src/Contacts/TSThread.m index 242bcd2b9..573ada0bd 100644 --- a/src/Contacts/TSThread.m +++ b/src/Contacts/TSThread.m @@ -178,8 +178,7 @@ NS_ASSUME_NONNULL_BEGIN }]; for (TSIncomingMessage *message in array) { - message.read = YES; - [message saveWithTransaction:transaction]; + [message markAsReadWithTransaction:transaction]; } } diff --git a/src/Contacts/Threads/TSContactThread.h b/src/Contacts/Threads/TSContactThread.h index edca70df0..71742ccb0 100644 --- a/src/Contacts/Threads/TSContactThread.h +++ b/src/Contacts/Threads/TSContactThread.h @@ -18,6 +18,8 @@ NS_ASSUME_NONNULL_BEGIN - (NSString *)contactIdentifier; ++ (NSString *)contactIdFromThreadId:(NSString *)threadId; + @end NS_ASSUME_NONNULL_END diff --git a/src/Devices/OWSReadReceipt.h b/src/Devices/OWSReadReceipt.h new file mode 100644 index 000000000..5aa504c16 --- /dev/null +++ b/src/Devices/OWSReadReceipt.h @@ -0,0 +1,16 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSReadReceipt : NSObject + +@property (nonatomic, readonly) NSString *senderId; +@property (nonatomic, readonly) uint64_t timestamp; +@property (nonatomic, readonly, getter=isValid) BOOL valid; +@property (nonatomic, readonly) NSArray *validationErrorMessages; + +- (instancetype)initWithSenderId:(NSString *)senderId timestamp:(uint64_t)timestamp; + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/src/Devices/OWSReadReceipt.m b/src/Devices/OWSReadReceipt.m new file mode 100644 index 000000000..b67abc8c1 --- /dev/null +++ b/src/Devices/OWSReadReceipt.m @@ -0,0 +1,35 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +#import "OWSReadReceipt.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSReadReceipt + +- (instancetype)initWithSenderId:(NSString *)senderId timestamp:(uint64_t)timestamp; +{ + self = [super init]; + if (!self) { + return self; + } + + NSMutableArray *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; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/Devices/OWSReadReceiptsProcessor.h b/src/Devices/OWSReadReceiptsProcessor.h new file mode 100644 index 000000000..6c5e0114c --- /dev/null +++ b/src/Devices/OWSReadReceiptsProcessor.h @@ -0,0 +1,15 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +NS_ASSUME_NONNULL_BEGIN + +@class OWSSignalServiceProtosSyncMessageRead; + +@interface OWSReadReceiptsProcessor : NSObject + +- (instancetype)initWithReadReceiptProtos:(NSArray *)readReceiptProtos + NS_DESIGNATED_INITIALIZER; +- (void)process; + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/src/Devices/OWSReadReceiptsProcessor.m b/src/Devices/OWSReadReceiptsProcessor.m new file mode 100644 index 000000000..68e8564a4 --- /dev/null +++ b/src/Devices/OWSReadReceiptsProcessor.m @@ -0,0 +1,62 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +#import "OWSReadReceiptsProcessor.h" +#import "OWSReadReceipt.h" +#import "OWSSignalServiceProtos.pb.h" +#import "TSIncomingMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSReadReceiptsProcessor () + +@property (nonatomic, readonly) NSArray *readReceipts; + +@end + +@implementation OWSReadReceiptsProcessor + +- (instancetype)init +{ + return [self initWithReadReceiptProtos:@[]]; +} + +- (instancetype)initWithReadReceiptProtos:(NSArray *)readReceiptProtos +{ + self = [super init]; + if (!self) { + return self; + } + + NSMutableArray *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: %@", readReceipt.validationErrorMessages); + } + } + + _readReceipts = [readReceipts copy]; + + return self; +} + +- (void)process +{ + DDLogInfo(@"Processing %ld read receipts.", self.readReceipts.count); + for (OWSReadReceipt *readReceipt in self.readReceipts) { + TSIncomingMessage *message = + [TSIncomingMessage findMessageWithAuthorId:readReceipt.senderId timestamp:readReceipt.timestamp]; + if (message) { + [message markAsRead]; + } else { + DDLogWarn(@"Couldn't find message for read receipt. Message not synced?"); + } + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/Messages/Interactions/TSIncomingMessage.h b/src/Messages/Interactions/TSIncomingMessage.h index 44b797923..8a173e1b7 100644 --- a/src/Messages/Interactions/TSIncomingMessage.h +++ b/src/Messages/Interactions/TSIncomingMessage.h @@ -88,10 +88,27 @@ NS_ASSUME_NONNULL_BEGIN messageBody:(nullable NSString *)body attachmentIds:(NSArray *)attachmentIds; +/* + * Find a message matching the senderId and timestamp, if any. + * + * @param authorId + * Signal ID (i.e. e164) of the user who sent the message + * @params timestamp + * When the message was created in milliseconds since epoch + * + */ ++ (nullable instancetype)findMessageWithAuthorId:(NSString *)authorId timestamp:(uint64_t)timestamp; + @property (nonatomic, readonly) NSString *authorId; -@property (nonatomic, getter=wasRead) BOOL read; +@property (nonatomic, readonly, getter=wasRead) BOOL read; @property (nonatomic, readonly) NSDate *receivedAt; +/* + * Marks a message as having been read and broadcasts a TSIncomingMessageWasReadNotification + */ +- (void)markAsRead; +- (void)markAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; + @end NS_ASSUME_NONNULL_END diff --git a/src/Messages/Interactions/TSIncomingMessage.m b/src/Messages/Interactions/TSIncomingMessage.m index 5827fff06..e5b397924 100644 --- a/src/Messages/Interactions/TSIncomingMessage.m +++ b/src/Messages/Interactions/TSIncomingMessage.m @@ -3,7 +3,9 @@ #import "TSIncomingMessage.h" #import "TSContactThread.h" +#import "TSDatabaseSecondaryIndexes.h" #import "TSGroupThread.h" +#import NS_ASSUME_NONNULL_BEGIN @@ -61,6 +63,55 @@ NS_ASSUME_NONNULL_BEGIN return self; } ++ (nullable instancetype)findMessageWithAuthorId:(NSString *)authorId timestamp:(uint64_t)timestamp +{ + __block TSIncomingMessage *foundMessage; + [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + // In theory we could build a new secondaryIndex for (authorId,timestamp), but in practice there should + // be *very* few (millisecond) timestamps with multiple authors. + [TSDatabaseSecondaryIndexes + enumerateMessagesWithTimestamp:timestamp + withBlock:^(NSString *collection, NSString *key, BOOL *stop) { + TSInteraction *interaction = + [TSInteraction fetchObjectWithUniqueID:key transaction:transaction]; + if ([interaction isKindOfClass:[TSIncomingMessage class]]) { + TSIncomingMessage *message = (TSIncomingMessage *)interaction; + + // Only groupthread sets authorId, thus this crappy code. + // TODO ALL incoming messages should have an authorId. + NSString *messageAuthorId; + if (message.authorId) { // Group Thread + messageAuthorId = message.authorId; + } else { // Contact Thread + messageAuthorId = + [TSContactThread contactIdFromThreadId:message.uniqueThreadId]; + } + + if ([messageAuthorId isEqualToString:authorId]) { + foundMessage = message; + } + } + } + usingTransaction:transaction]; + }]; + + return foundMessage; +} + +- (void)markAsRead +{ + [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [self markAsReadWithTransaction:transaction]; + }]; +} + +- (void)markAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + _read = YES; + [self saveWithTransaction:transaction]; + [transaction touchObjectForKey:self.uniqueThreadId inCollection:[TSThread collection]]; +} + @end NS_ASSUME_NONNULL_END diff --git a/src/Messages/TSMessagesManager.m b/src/Messages/TSMessagesManager.m index da7c9cd72..081823d25 100644 --- a/src/Messages/TSMessagesManager.m +++ b/src/Messages/TSMessagesManager.m @@ -6,6 +6,7 @@ #import "MimeTypeUtil.h" #import "NSData+messagePadding.h" #import "OWSIncomingSentMessageTranscript.h" +#import "OWSReadReceiptsProcessor.h" #import "OWSSyncContactsMessage.h" #import "OWSSyncGroupsMessage.h" #import "TSAccountManager.h" @@ -235,8 +236,7 @@ OWSIncomingSentMessageTranscript *transcript = [[OWSIncomingSentMessageTranscript alloc] initWithProto:syncMessage.sent relay:messageEnvelope.relay]; [transcript record]; - } - if (syncMessage.hasRequest) { + } else if (syncMessage.hasRequest) { if (syncMessage.request.type == OWSSignalServiceProtosSyncMessageRequestTypeContacts) { DDLogInfo(@"Received request `Contacts` syncMessage."); @@ -270,6 +270,12 @@ DDLogError(@"Failed to send Groups response syncMessage."); }]; } + } else if (syncMessage.read.count > 0) { + DDLogInfo(@"Received %ld read receipt(s)", (u_long)syncMessage.read.count); + + OWSReadReceiptsProcessor *readReceiptsProcessor = + [[OWSReadReceiptsProcessor alloc] initWithReadReceiptProtos:syncMessage.read]; + [readReceiptsProcessor process]; } else { DDLogWarn(@"Ignoring unsupported sync message."); }