mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			323 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Objective-C
		
	
			
		
		
	
	
			323 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Objective-C
		
	
| //
 | |
| //  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| #import "OWSReadReceiptManager.h"
 | |
| #import "AppReadiness.h"
 | |
| #import "OWSOutgoingReceiptManager.h"
 | |
| #import "OWSPrimaryStorage.h"
 | |
| #import "OWSStorage.h"
 | |
| #import "SSKEnvironment.h"
 | |
| #import "TSAccountManager.h"
 | |
| #import "TSContactThread.h"
 | |
| #import "TSOutgoingMessage.h"
 | |
| #import "TSDatabaseView.h"
 | |
| #import "TSIncomingMessage.h"
 | |
| #import "YapDatabaseConnection+OWS.h"
 | |
| #import <SessionUtilitiesKit/SessionUtilitiesKit.h>
 | |
| #import <YapDatabase/YapDatabase.h>
 | |
| 
 | |
| NS_ASSUME_NONNULL_BEGIN
 | |
| 
 | |
| NSString *const kIncomingMessageMarkedAsReadNotification = @"kIncomingMessageMarkedAsReadNotification";
 | |
| 
 | |
| @interface TSRecipientReadReceipt : TSYapDatabaseObject
 | |
| 
 | |
| @property (nonatomic, readonly) uint64_t sentTimestamp;
 | |
| // Map of "recipient id"-to-"read timestamp".
 | |
| @property (nonatomic, readonly) NSDictionary<NSString *, NSNumber *> *recipientMap;
 | |
| 
 | |
| @end
 | |
| 
 | |
| #pragma mark -
 | |
| 
 | |
| @implementation TSRecipientReadReceipt
 | |
| 
 | |
| + (NSString *)collection
 | |
| {
 | |
|     return @"TSRecipientReadReceipt2";
 | |
| }
 | |
| 
 | |
| - (instancetype)initWithSentTimestamp:(uint64_t)sentTimestamp
 | |
| {
 | |
|     self = [super initWithUniqueId:[TSRecipientReadReceipt uniqueIdForSentTimestamp:sentTimestamp]];
 | |
| 
 | |
|     if (self) {
 | |
|         _sentTimestamp = sentTimestamp;
 | |
|         _recipientMap = [NSDictionary new];
 | |
|     }
 | |
| 
 | |
|     return self;
 | |
| }
 | |
| 
 | |
| + (NSString *)uniqueIdForSentTimestamp:(uint64_t)timestamp
 | |
| {
 | |
|     return [NSString stringWithFormat:@"%llu", timestamp];
 | |
| }
 | |
| 
 | |
| - (void)addRecipientId:(NSString *)recipientId timestamp:(uint64_t)timestamp
 | |
| {
 | |
|     NSMutableDictionary<NSString *, NSNumber *> *recipientMapCopy = [self.recipientMap mutableCopy];
 | |
|     recipientMapCopy[recipientId] = @(timestamp);
 | |
|     _recipientMap = [recipientMapCopy copy];
 | |
| }
 | |
| 
 | |
| + (void)addRecipientId:(NSString *)recipientId
 | |
|          sentTimestamp:(uint64_t)sentTimestamp
 | |
|          readTimestamp:(uint64_t)readTimestamp
 | |
|            transaction:(YapDatabaseReadWriteTransaction *)transaction
 | |
| {
 | |
|     TSRecipientReadReceipt *_Nullable recipientReadReceipt =
 | |
|         [transaction objectForKey:[self uniqueIdForSentTimestamp:sentTimestamp] inCollection:[self collection]];
 | |
|     if (!recipientReadReceipt) {
 | |
|         recipientReadReceipt = [[TSRecipientReadReceipt alloc] initWithSentTimestamp:sentTimestamp];
 | |
|     }
 | |
|     [recipientReadReceipt addRecipientId:recipientId timestamp:readTimestamp];
 | |
|     [recipientReadReceipt saveWithTransaction:transaction];
 | |
| }
 | |
| 
 | |
| + (nullable NSDictionary<NSString *, NSNumber *> *)recipientMapForSentTimestamp:(uint64_t)sentTimestamp
 | |
|                                                                     transaction:
 | |
|                                                                         (YapDatabaseReadWriteTransaction *)transaction
 | |
| {
 | |
|     TSRecipientReadReceipt *_Nullable recipientReadReceipt =
 | |
|         [transaction objectForKey:[self uniqueIdForSentTimestamp:sentTimestamp] inCollection:[self collection]];
 | |
|     return recipientReadReceipt.recipientMap;
 | |
| }
 | |
| 
 | |
| + (void)removeRecipientIdsForTimestamp:(uint64_t)sentTimestamp
 | |
|                            transaction:(YapDatabaseReadWriteTransaction *)transaction
 | |
| {
 | |
|     [transaction removeObjectForKey:[self uniqueIdForSentTimestamp:sentTimestamp] inCollection:[self collection]];
 | |
| }
 | |
| 
 | |
| @end
 | |
| 
 | |
| #pragma mark -
 | |
| 
 | |
| NSString *const OWSReadReceiptManagerCollection = @"OWSReadReceiptManagerCollection";
 | |
| NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsEnabled";
 | |
| 
 | |
| @interface OWSReadReceiptManager ()
 | |
| 
 | |
| @property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
 | |
| 
 | |
| // A map of "thread unique id"-to-"read receipt" for read receipts that
 | |
| // we will send to our linked devices.
 | |
| //
 | |
| // Should only be accessed while synchronized on the OWSReadReceiptManager.
 | |
| // @property (nonatomic, readonly) NSMutableDictionary<NSString *, OWSLinkedDeviceReadReceipt *> *toLinkedDevicesReadReceiptMap;
 | |
| 
 | |
| // Should only be accessed while synchronized on the OWSReadReceiptManager.
 | |
| @property (nonatomic) BOOL isProcessing;
 | |
| 
 | |
| @property (atomic) NSNumber *areReadReceiptsEnabledCached;
 | |
| 
 | |
| @end
 | |
| 
 | |
| #pragma mark -
 | |
| 
 | |
| @implementation OWSReadReceiptManager
 | |
| 
 | |
| + (instancetype)sharedManager
 | |
| {
 | |
|     return SSKEnvironment.shared.readReceiptManager;
 | |
| }
 | |
| 
 | |
| - (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage
 | |
| {
 | |
|     self = [super init];
 | |
| 
 | |
|     if (!self) {
 | |
|         return self;
 | |
|     }
 | |
| 
 | |
|     _dbConnection = primaryStorage.newDatabaseConnection;
 | |
| 
 | |
|     // Start processing.
 | |
|     [AppReadiness runNowOrWhenAppDidBecomeReady:^{
 | |
|         [self scheduleProcessing];
 | |
|     }];
 | |
| 
 | |
|     return self;
 | |
| }
 | |
| 
 | |
| - (void)dealloc
 | |
| {
 | |
|     [[NSNotificationCenter defaultCenter] removeObserver:self];
 | |
| }
 | |
| 
 | |
| #pragma mark - Dependencies
 | |
| 
 | |
| - (OWSOutgoingReceiptManager *)outgoingReceiptManager
 | |
| {
 | |
|     return SSKEnvironment.shared.outgoingReceiptManager;
 | |
| }
 | |
| 
 | |
| #pragma mark -
 | |
| 
 | |
| // Schedules a processing pass, unless one is already scheduled.
 | |
| - (void)scheduleProcessing
 | |
| {
 | |
|     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 | |
|         @synchronized(self)
 | |
|         {
 | |
|             if (self.isProcessing) {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             self.isProcessing = YES;
 | |
| 
 | |
|             [self process];
 | |
|         }
 | |
|     });
 | |
| }
 | |
| 
 | |
| - (void)process
 | |
| {
 | |
|     
 | |
| }
 | |
| 
 | |
| #pragma mark - Mark as Read Locally
 | |
| 
 | |
| - (void)markAsReadLocallyBeforeSortId:(uint64_t)sortId thread:(TSThread *)thread
 | |
| {
 | |
|     [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
 | |
|         [self markAsReadBeforeSortId:sortId
 | |
|                               thread:thread
 | |
|                        readTimestamp:[NSDate millisecondTimestamp]
 | |
|                             wasLocal:YES
 | |
|                          transaction:transaction];
 | |
|     }];
 | |
| }
 | |
| 
 | |
| - (void)messageWasReadLocally:(TSIncomingMessage *)message
 | |
| {
 | |
|     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 | |
|         @synchronized(self)
 | |
|         {
 | |
|             NSString *messageAuthorId = message.authorId;
 | |
| 
 | |
|             if (message.thread.isGroupThread) { return; } // Don't send read receipts in group threads
 | |
|             
 | |
|             if ([self areReadReceiptsEnabled]) {
 | |
|                 [self.outgoingReceiptManager enqueueReadReceiptForEnvelope:messageAuthorId timestamp:message.timestamp];
 | |
|             }
 | |
| 
 | |
|             [self scheduleProcessing];
 | |
|         }
 | |
|     });
 | |
| }
 | |
| 
 | |
| #pragma mark - Read Receipts From Recipient
 | |
| 
 | |
| - (void)processReadReceiptsFromRecipientId:(NSString *)recipientId
 | |
|                             sentTimestamps:(NSArray<NSNumber *> *)sentTimestamps
 | |
|                              readTimestamp:(uint64_t)readTimestamp
 | |
| {
 | |
|     if (![self areReadReceiptsEnabled]) {
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 | |
|         [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
 | |
|             for (NSNumber *nsSentTimestamp in sentTimestamps) {
 | |
|                 UInt64 sentTimestamp = [nsSentTimestamp unsignedLongLongValue];
 | |
| 
 | |
|                 NSArray<TSOutgoingMessage *> *messages
 | |
|                     = (NSArray<TSOutgoingMessage *> *)[TSInteraction interactionsWithTimestamp:sentTimestamp
 | |
|                                                                                        ofClass:[TSOutgoingMessage class]
 | |
|                                                                                withTransaction:transaction];
 | |
|                 if (messages.count > 0) {
 | |
|                     // TODO: We might also need to "mark as read by recipient" any older messages
 | |
|                     // from us in that thread.  Or maybe this state should hang on the thread?
 | |
|                     for (TSOutgoingMessage *message in messages) {
 | |
|                         [message updateWithReadRecipientId:recipientId
 | |
|                                              readTimestamp:readTimestamp
 | |
|                                                transaction:transaction];
 | |
|                     }
 | |
|                 } else {
 | |
|                     // Persist the read receipts so that we can apply them to outgoing messages
 | |
|                     // that we learn about later through sync messages.
 | |
|                     [TSRecipientReadReceipt addRecipientId:recipientId
 | |
|                                              sentTimestamp:sentTimestamp
 | |
|                                              readTimestamp:readTimestamp
 | |
|                                                transaction:transaction];
 | |
|                 }
 | |
|             }
 | |
|         }];
 | |
|     });
 | |
| }
 | |
| 
 | |
| #pragma mark - Mark As Read
 | |
| 
 | |
| - (void)markAsReadBeforeSortId:(uint64_t)sortId
 | |
|                         thread:(TSThread *)thread
 | |
|                  readTimestamp:(uint64_t)readTimestamp
 | |
|                       wasLocal:(BOOL)wasLocal
 | |
|                    transaction:(YapDatabaseReadWriteTransaction *)transaction
 | |
| {
 | |
|     NSMutableArray<id<OWSReadTracking>> *newlyReadList = [NSMutableArray new];
 | |
| 
 | |
|     [[TSDatabaseView unseenDatabaseViewExtension:transaction]
 | |
|         enumerateKeysAndObjectsInGroup:thread.uniqueId
 | |
|                             usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
 | |
|                                 if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {
 | |
|                                     return;
 | |
|                                 }
 | |
|                                 id<OWSReadTracking> possiblyRead = (id<OWSReadTracking>)object;
 | |
|                                 if (possiblyRead.sortId > sortId) {
 | |
|                                     *stop = YES;
 | |
|                                     return;
 | |
|                                 }
 | |
| 
 | |
|                                 // Under normal circumstances !possiblyRead.read should always evaluate to true at this point, but
 | |
|                                 // there is a bug that can somehow cause it to be false leading to conversations permanently being
 | |
|                                 // stuck with "unread" messages.
 | |
| 
 | |
|                                 if (!possiblyRead.read) {
 | |
|                                     [newlyReadList addObject:possiblyRead];
 | |
|                                 }
 | |
|                             }];
 | |
| 
 | |
|     if (newlyReadList.count < 1) {
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     for (id<OWSReadTracking> readItem in newlyReadList) {
 | |
|         [readItem markAsReadAtTimestamp:readTimestamp sendReadReceipt:wasLocal transaction:transaction];
 | |
|     }
 | |
| }
 | |
| 
 | |
| #pragma mark - Settings
 | |
| 
 | |
| - (void)prepareCachedValues
 | |
| {
 | |
|     [self areReadReceiptsEnabled];
 | |
| }
 | |
| 
 | |
| - (BOOL)areReadReceiptsEnabled
 | |
| {
 | |
|     // We don't need to worry about races around this cached value.
 | |
|     if (!self.areReadReceiptsEnabledCached) {
 | |
|         self.areReadReceiptsEnabledCached = @([self.dbConnection boolForKey:OWSReadReceiptManagerAreReadReceiptsEnabled
 | |
|                                                                inCollection:OWSReadReceiptManagerCollection
 | |
|                                                                defaultValue:NO]);
 | |
|     }
 | |
| 
 | |
|     return [self.areReadReceiptsEnabledCached boolValue];
 | |
| }
 | |
| 
 | |
| - (void)setAreReadReceiptsEnabled:(BOOL)value
 | |
| {
 | |
|     [self.dbConnection setBool:value
 | |
|                         forKey:OWSReadReceiptManagerAreReadReceiptsEnabled
 | |
|                   inCollection:OWSReadReceiptManagerCollection];
 | |
| 
 | |
|     self.areReadReceiptsEnabledCached = @(value);
 | |
| }
 | |
| 
 | |
| @end
 | |
| 
 | |
| NS_ASSUME_NONNULL_END
 |