From acb89f0b0f4247e82bfd292d60da65fccefba93b Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 1 Sep 2016 15:42:51 -0400 Subject: [PATCH] Outgoing Read Receipts // FREEBIE --- src/Devices/OWSReadReceiptObserver.h | 14 +++ src/Devices/OWSReadReceiptObserver.m | 110 ++++++++++++++++++ src/Devices/OWSReadReceiptsMessage.h | 15 +++ src/Devices/OWSReadReceiptsMessage.m | 45 +++++++ src/Devices/OWSReadReceiptsProcessor.m | 6 +- .../DeviceSyncing/OWSOutgoingSyncMessage.m | 7 ++ .../DeviceSyncing/OWSSyncContactsMessage.h | 1 - .../DeviceSyncing/OWSSyncGroupsMessage.h | 1 - .../DeviceSyncing/OWSSyncGroupsMessage.m | 8 -- src/Messages/Interactions/TSIncomingMessage.h | 12 +- src/Messages/Interactions/TSIncomingMessage.m | 13 ++- src/Messages/TSMessagesManager.m | 2 +- 12 files changed, 217 insertions(+), 17 deletions(-) create mode 100644 src/Devices/OWSReadReceiptObserver.h create mode 100644 src/Devices/OWSReadReceiptObserver.m create mode 100644 src/Devices/OWSReadReceiptsMessage.h create mode 100644 src/Devices/OWSReadReceiptsMessage.m diff --git a/src/Devices/OWSReadReceiptObserver.h b/src/Devices/OWSReadReceiptObserver.h new file mode 100644 index 000000000..d1c9d82ba --- /dev/null +++ b/src/Devices/OWSReadReceiptObserver.h @@ -0,0 +1,14 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +NS_ASSUME_NONNULL_BEGIN + +@class TSMessagesManager; + +@interface OWSReadReceiptObserver : NSObject + +- (instancetype)initWithMessagesManager:(TSMessagesManager *)messagesManager; +- (void)startObserving; + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/src/Devices/OWSReadReceiptObserver.m b/src/Devices/OWSReadReceiptObserver.m new file mode 100644 index 000000000..a128e686b --- /dev/null +++ b/src/Devices/OWSReadReceiptObserver.m @@ -0,0 +1,110 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +#import "OWSReadReceiptObserver.h" +#import "OWSReadReceipt.h" +#import "OWSReadReceiptsMessage.h" +#import "TSContactThread.h" +#import "TSIncomingMessage.h" +#import "TSMessagesManager+sendMessages.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSReadReceiptObserver () + +@property (atomic) NSMutableArray *readReceiptsQueue; +@property (nonatomic, readonly) TSMessagesManager *messagesManager; +@property BOOL isObserving; + +@end + +@implementation OWSReadReceiptObserver + +- (instancetype)initWithMessagesManager:(TSMessagesManager *)messagesManager +{ + self = [super init]; + if (!self) { + return self; + } + + _readReceiptsQueue = [NSMutableArray new]; + _messagesManager = messagesManager; + _isObserving = NO; + + return self; +} + +- (void)startObserving +{ + if (self.isObserving) { + return; + } + + self.isObserving = true; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleReadNotification:) + name:TSIncomingMessageWasReadOnThisDeviceNotification + object:nil]; +} + +- (void)handleReadNotification:(NSNotification *)notification +{ + if (![notification.object isKindOfClass:[TSIncomingMessage class]]) { + DDLogError(@"Read receipt notifier got unexpected object: %@", notification.object); + return; + } + + TSIncomingMessage *message = (TSIncomingMessage *)notification.object; + + // 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]; + } + + OWSReadReceipt *readReceipt = [[OWSReadReceipt alloc] initWithSenderId:messageAuthorId timestamp:message.timestamp]; + [self.readReceiptsQueue addObject:readReceipt]; + + // Wait a bit to bundle up read receipts into one request. + __weak typeof(self) weakSelf = self; + [weakSelf performSelector:@selector(sendAllReadReceiptsInQueue) withObject:nil afterDelay:2.0]; +} + +- (void)sendAllReadReceiptsInQueue +{ + // Synchronized so we don't lose any read receipts while replacing the queue + __block NSArray *receiptsToSend; + @synchronized(self) + { + if (self.readReceiptsQueue.count > 0) { + receiptsToSend = [self.readReceiptsQueue copy]; + self.readReceiptsQueue = [NSMutableArray new]; + } + } + + if (receiptsToSend) { + [self sendReadReceipts:receiptsToSend]; + } else { + DDLogVerbose(@"Read receipts queue already drained."); + } +} + +- (void)sendReadReceipts:(NSArray *)readReceipts +{ + OWSReadReceiptsMessage *message = [[OWSReadReceiptsMessage alloc] initWithReadReceipts:readReceipts]; + + [self.messagesManager sendMessage:message + inThread:nil + success:^{ + DDLogInfo(@"Successfully sent %ld read receipt", (unsigned long)readReceipts.count); + } + failure:^{ + DDLogError(@"Failed to send read receipt"); + }]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/Devices/OWSReadReceiptsMessage.h b/src/Devices/OWSReadReceiptsMessage.h new file mode 100644 index 000000000..5c980c7c3 --- /dev/null +++ b/src/Devices/OWSReadReceiptsMessage.h @@ -0,0 +1,15 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +#import "OWSOutgoingSyncMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class OWSReadReceipt; + +@interface OWSReadReceiptsMessage : OWSOutgoingSyncMessage + +- (instancetype)initWithReadReceipts:(NSArray *)readReceipts; + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/src/Devices/OWSReadReceiptsMessage.m b/src/Devices/OWSReadReceiptsMessage.m new file mode 100644 index 000000000..4a4098a35 --- /dev/null +++ b/src/Devices/OWSReadReceiptsMessage.m @@ -0,0 +1,45 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +#import "OWSReadReceiptsMessage.h" +#import "OWSReadReceipt.h" +#import "OWSSignalServiceProtos.pb.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSReadReceiptsMessage () + +@property (nonatomic, readonly) NSArray *readReceipts; + +@end + +@implementation OWSReadReceiptsMessage + +- (instancetype)initWithReadReceipts:(NSArray *)readReceipts +{ + self = [super init]; + if (!self) { + return self; + } + + _readReceipts = [readReceipts copy]; + + return self; +} + +- (OWSSignalServiceProtosSyncMessage *)buildSyncMessage +{ + OWSSignalServiceProtosSyncMessageBuilder *syncMessageBuilder = [OWSSignalServiceProtosSyncMessageBuilder new]; + for (OWSReadReceipt *readReceipt in self.readReceipts) { + OWSSignalServiceProtosSyncMessageReadBuilder *readProtoBuilder = + [OWSSignalServiceProtosSyncMessageReadBuilder new]; + [readProtoBuilder setSender:readReceipt.senderId]; + [readProtoBuilder setTimestamp:readReceipt.timestamp]; + [syncMessageBuilder addRead:[readProtoBuilder build]]; + } + + return [syncMessageBuilder build]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/Devices/OWSReadReceiptsProcessor.m b/src/Devices/OWSReadReceiptsProcessor.m index 68e8564a4..4c90b5377 100644 --- a/src/Devices/OWSReadReceiptsProcessor.m +++ b/src/Devices/OWSReadReceiptsProcessor.m @@ -45,13 +45,15 @@ NS_ASSUME_NONNULL_BEGIN - (void)process { - DDLogInfo(@"Processing %ld read receipts.", self.readReceipts.count); + DDLogInfo(@"Processing %ld read receipts.", (unsigned long)self.readReceipts.count); for (OWSReadReceipt *readReceipt in self.readReceipts) { TSIncomingMessage *message = [TSIncomingMessage findMessageWithAuthorId:readReceipt.senderId timestamp:readReceipt.timestamp]; if (message) { - [message markAsRead]; + [message markAsReadFromReadReceipt]; } else { + // TODO keep read receipts around so that if we get the receipt before the message, + // we can immediately mark the message as read once we get it. DDLogWarn(@"Couldn't find message for read receipt. Message not synced?"); } } diff --git a/src/Messages/DeviceSyncing/OWSOutgoingSyncMessage.m b/src/Messages/DeviceSyncing/OWSOutgoingSyncMessage.m index f5cc5b9e0..463b5c49b 100644 --- a/src/Messages/DeviceSyncing/OWSOutgoingSyncMessage.m +++ b/src/Messages/DeviceSyncing/OWSOutgoingSyncMessage.m @@ -7,6 +7,13 @@ NS_ASSUME_NONNULL_BEGIN @implementation OWSOutgoingSyncMessage +- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + // override superclass with no-op. + // + // There's no need to save this message, since it's not displayed to the user. +} + - (BOOL)shouldSyncTranscript { return NO; diff --git a/src/Messages/DeviceSyncing/OWSSyncContactsMessage.h b/src/Messages/DeviceSyncing/OWSSyncContactsMessage.h index 5a3261f7a..848b45c3d 100644 --- a/src/Messages/DeviceSyncing/OWSSyncContactsMessage.h +++ b/src/Messages/DeviceSyncing/OWSSyncContactsMessage.h @@ -10,7 +10,6 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSSyncContactsMessage : OWSOutgoingSyncMessage - (instancetype)initWithContactsManager:(id)contactsManager; -- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; - (NSData *)buildPlainTextAttachmentData; @end diff --git a/src/Messages/DeviceSyncing/OWSSyncGroupsMessage.h b/src/Messages/DeviceSyncing/OWSSyncGroupsMessage.h index f86a2ada8..530997f53 100644 --- a/src/Messages/DeviceSyncing/OWSSyncGroupsMessage.h +++ b/src/Messages/DeviceSyncing/OWSSyncGroupsMessage.h @@ -6,7 +6,6 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSSyncGroupsMessage : OWSOutgoingSyncMessage -- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; - (NSData *)buildPlainTextAttachmentData; @end diff --git a/src/Messages/DeviceSyncing/OWSSyncGroupsMessage.m b/src/Messages/DeviceSyncing/OWSSyncGroupsMessage.m index 1ae57c7fe..235c99b32 100644 --- a/src/Messages/DeviceSyncing/OWSSyncGroupsMessage.m +++ b/src/Messages/DeviceSyncing/OWSSyncGroupsMessage.m @@ -17,14 +17,6 @@ NS_ASSUME_NONNULL_BEGIN return [super initWithTimestamp:[NSDate ows_millisecondTimeStamp]]; } -- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction -{ - // no-op - - // There's no need to save this message, since it's not displayed to the user. - // Furthermore if we did save it, we probably don't want to save the conctactsManager property. -} - - (OWSSignalServiceProtosSyncMessage *)buildSyncMessage { diff --git a/src/Messages/Interactions/TSIncomingMessage.h b/src/Messages/Interactions/TSIncomingMessage.h index 8a173e1b7..8de5023f3 100644 --- a/src/Messages/Interactions/TSIncomingMessage.h +++ b/src/Messages/Interactions/TSIncomingMessage.h @@ -8,6 +8,8 @@ NS_ASSUME_NONNULL_BEGIN @class TSContactThread; @class TSGroupThread; +extern NSString *const TSIncomingMessageWasReadOnThisDeviceNotification; + @interface TSIncomingMessage : TSMessage /** @@ -104,11 +106,17 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) NSDate *receivedAt; /* - * Marks a message as having been read and broadcasts a TSIncomingMessageWasReadNotification + * Marks a message as having been read on this device (as opposed to responding to a remote read receipt). + * */ -- (void)markAsRead; - (void)markAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; +/** + * Similar to markAsReadWithTransaction, but doesn't send out read receipts. + * Used for *responding* to a remote read receipt. + */ +- (void)markAsReadFromReadReceipt; + @end NS_ASSUME_NONNULL_END diff --git a/src/Messages/Interactions/TSIncomingMessage.m b/src/Messages/Interactions/TSIncomingMessage.m index e5b397924..ee3884776 100644 --- a/src/Messages/Interactions/TSIncomingMessage.m +++ b/src/Messages/Interactions/TSIncomingMessage.m @@ -9,6 +9,8 @@ NS_ASSUME_NONNULL_BEGIN +NSString *const TSIncomingMessageWasReadOnThisDeviceNotification = @"TSIncomingMessageWasReadOnThisDeviceNotification"; + @implementation TSIncomingMessage - (instancetype)initWithTimestamp:(uint64_t)timestamp @@ -98,14 +100,21 @@ NS_ASSUME_NONNULL_BEGIN return foundMessage; } -- (void)markAsRead +- (void)markAsReadFromReadReceipt { [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [self markAsReadWithTransaction:transaction]; + [self markAsReadWithoutNotificationWithTransaction:transaction]; }]; } - (void)markAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [self markAsReadWithoutNotificationWithTransaction:transaction]; + [[NSNotificationCenter defaultCenter] postNotificationName:TSIncomingMessageWasReadOnThisDeviceNotification + object:self]; +} + +- (void)markAsReadWithoutNotificationWithTransaction:(YapDatabaseReadWriteTransaction *)transaction { _read = YES; [self saveWithTransaction:transaction]; diff --git a/src/Messages/TSMessagesManager.m b/src/Messages/TSMessagesManager.m index 081823d25..5dac400d0 100644 --- a/src/Messages/TSMessagesManager.m +++ b/src/Messages/TSMessagesManager.m @@ -69,7 +69,7 @@ DDLogWarn(@"Received an unknown message type"); break; default: - DDLogWarn(@"Received unhandled envelope type: %d", envelope.type); + DDLogWarn(@"Received unhandled envelope type: %d", (int)envelope.type); break; } } @catch (NSException *exception) {