@ -5,29 +5,176 @@
#import "OWSReadReceiptManager . h "
#import "OWSReadReceiptManager . h "
#import "OWSMessageSender . h "
#import "OWSMessageSender . h "
#import "OWSReadReceipt . h "
#import "OWSReadReceipt . h "
#import "OWSReadReceiptsMessage . h "
#import "OWSReadReceiptsForLinkedDevicesMessage . h "
#import "OWSReadReceiptsForSenderMessage . h "
#import "OWSSignalServiceProtos . pb . h "
#import "TSContactThread . h "
#import "TSContactThread . h "
#import "TSDatabaseView . h "
#import "TSDatabaseView . h "
#import "TSIncomingMessage . h "
#import "TSIncomingMessage . h "
#import "TSStorageManager . h "
#import "TextSecureKitEnv . h "
#import "TextSecureKitEnv . h "
#import "Threading . h "
#import "Threading . h "
#import < YapDatabase / YapDatabaseView . h >
NS_ASSUME_NONNULL_BEGIN
NS_ASSUME_NONNULL_BEGIN
/ / #pragma mark - Finder
NSString * const OWSOutgoingMessageFinderExtensionName = @ "OWSOutgoingMessageFinderExtensionName ";
@ interface OWSOutgoingMessageFinder : NSObject
@ end
#pragma mark -
@ interface OWSOutgoingMessageFinder ( )
@ property ( nonatomic , readonly ) YapDatabaseConnection * dbConnection ;
@ end
#pragma mark -
@ implementation OWSOutgoingMessageFinder
- ( instancetype ) initWithDBConnection : ( YapDatabaseConnection * ) dbConnection
{
OWSSingletonAssert ( ) ;
self = [ super init ] ;
if ( !self ) {
return self ;
}
_dbConnection = dbConnection ;
return self ;
}
- ( NSArray < TSOutgoingMessage * > * ) outgoingMessagesWithTimestamp : ( uint64_t ) timestamp
transaction : ( YapDatabaseReadTransaction * ) transaction
{
OWSAssert ( transaction ) ;
NSMutableArray < TSOutgoingMessage * > * result = [ NSMutableArray new ] ;
YapDatabaseViewTransaction * viewTransaction = [ transaction ext : OWSOutgoingMessageFinderExtensionName ] ;
OWSAssert ( viewTransaction ) ;
[ viewTransaction
enumerateKeysAndObjectsInGroup : [ OWSOutgoingMessageFinder groupForTimestamp : timestamp ]
usingBlock : ^( NSString * collection , NSString * key , id object , NSUInteger index , BOOL * stop ) {
OWSAssert ( [ object isKindOfClass : [ TSOutgoingMessage class ] ] ) ;
TSOutgoingMessage * message = ( TSOutgoingMessage * ) object ;
OWSAssert ( message . timestamp == timestamp ) ;
[ result addObject : object ] ;
} ] ;
return [ result copy ] ;
}
+ ( NSString * ) groupForTimestamp : ( uint64_t ) timestamp
{
return [ NSString stringWithFormat : @ "%llu", timestamp];
}
+ ( YapDatabaseView * ) databaseExtension
{
YapDatabaseViewSorting * sorting =
[ YapDatabaseViewSorting withObjectBlock : ^NSComparisonResult ( YapDatabaseReadTransaction * transaction ,
NSString * group ,
NSString * collection1 ,
NSString * key1 ,
id object1 ,
NSString * collection2 ,
NSString * key2 ,
id object2 ) {
/ / The ordering doesn ' t matter as long as its consistent .
return [ key1 compare : key2 ] ;
} ] ;
YapDatabaseViewGrouping * grouping = [ YapDatabaseViewGrouping withObjectBlock : ^NSString * _Nullable (
YapDatabaseReadTransaction * transaction , NSString * collection , NSString * key , id object ) {
if ( ![ object isKindOfClass : [ TSOutgoingMessage class ] ] ) {
return nil ;
}
TSOutgoingMessage * message = ( TSOutgoingMessage * ) object ;
/ / Arbitrary string - all in the same group . We ' re only using the view for sorting .
return [ OWSOutgoingMessageFinder groupForTimestamp : message . timestamp ] ;
} ] ;
YapDatabaseViewOptions * options = [ YapDatabaseViewOptions new ] ;
options . allowedCollections =
[ [ YapWhitelistBlacklist alloc ] initWithWhitelist : [ NSSet setWithObject : [ TSOutgoingMessage collection ] ] ] ;
return [ [ YapDatabaseView alloc ] initWithGrouping : grouping sorting : sorting versionTag : @ "1 " options : options ] ;
}
+ ( void ) asyncRegisterDatabaseExtension : ( YapDatabase * ) database
{
YapDatabaseView * existingView = [ database registeredExtension : OWSOutgoingMessageFinderExtensionName ] ;
if ( existingView ) {
OWSFail ( @ "%@ was already initialized.", OWSOutgoingMessageFinderExtensionName);
return ;
}
[ database
asyncRegisterExtension : [ self databaseExtension ]
withName : OWSOutgoingMessageFinderExtensionName
completionBlock : ^( BOOL ready ) {
OWSCAssert ( ready ) ;
DDLogInfo (
@ "%@ asyncRegisterExtension: %@ -> %d", self.tag, OWSOutgoingMessageFinderExtensionName, ready);
} ] ;
}
#pragma mark - Logging
+ ( NSString * ) tag
{
return [ NSString stringWithFormat : @ "[ %@]", self.class];
}
- ( NSString * ) tag
{
return self . class . tag ;
}
@ end
#pragma mark -
NSString * const OWSReadReceiptManagerCollection = @ "OWSReadReceiptManagerCollection ";
NSString * const OWSReadReceiptManagerAreReadReceiptsEnabled = @ "areReadReceiptsEnabled ";
NSString * const OWSRecipientReadReceiptCollection = @ "OWSRecipientReadReceiptCollection ";
@ interface OWSReadReceiptManager ( )
@ interface OWSReadReceiptManager ( )
@ property ( nonatomic , readonly ) TSStorageManager * storageManager ;
@ property ( nonatomic , readonly ) OWSMessageSender * messageSender ;
@ property ( nonatomic , readonly ) OWSMessageSender * messageSender ;
@ property ( nonatomic , readonly ) YapDatabaseConnection * dbConnection ;
@ property ( nonatomic , readonly ) OWSOutgoingMessageFinder * outgoingMessageFinder ;
/ / A map of "thread unique id "- to - "read receipt " for read receipts that
/ / A map of "thread unique id "- to - "read receipt " for read receipts that
/ / we will send to our linked devices .
/ / we will send to our linked devices .
/ /
/ /
/ / Should only be accessed while synchronized on the OWSReadReceiptManager .
/ / Should only be accessed while synchronized on the OWSReadReceiptManager .
@ property ( nonatomic , readonly ) NSMutableDictionary < NSString * , OWSReadReceipt * > * toLinkedDevicesReadReceiptMap ;
@ property ( nonatomic , readonly ) NSMutableDictionary < NSString * , OWSReadReceipt * > * toLinkedDevicesReadReceiptMap ;
/ / A map of "recipient id "- to - "timestamp list " for read receipts that
/ / we will send to senders .
/ /
/ / Should only be accessed while synchronized on the OWSReadReceiptManager .
@ property ( nonatomic , readonly ) NSMutableDictionary < NSString * , NSMutableArray < NSNumber * > * > * toSenderReadReceiptMap ;
/ / Should only be accessed while synchronized on the OWSReadReceiptManager .
/ / Should only be accessed while synchronized on the OWSReadReceiptManager .
@ property ( nonatomic ) BOOL isProcessing ;
@ property ( nonatomic ) BOOL isProcessing ;
/ / Should only be accessed while synchronized on the OWSReadReceiptManager .
@ property ( nonatomic ) NSNumber * areReadReceiptsEnabledCached ;
@ end
@ end
#pragma mark -
#pragma mark -
@ -47,11 +194,13 @@ NS_ASSUME_NONNULL_BEGIN
- ( instancetype ) initDefault
- ( instancetype ) initDefault
{
{
OWSMessageSender * messageSender = [ TextSecureKitEnv sharedEnv ] . messageSender ;
OWSMessageSender * messageSender = [ TextSecureKitEnv sharedEnv ] . messageSender ;
TSStorageManager * storageManager = [ TSStorageManager sharedManager ] ;
return [ self initWithMessageSender : messageSender ] ;
return [ self initWithMessageSender : messageSender storageManager : storageManager ] ;
}
}
- ( instancetype ) initWithMessageSender : ( OWSMessageSender * ) messageSender
- ( instancetype ) initWithMessageSender : ( OWSMessageSender * ) messageSender
storageManager : ( TSStorageManager * ) storageManager
{
{
self = [ super init ] ;
self = [ super init ] ;
@ -60,8 +209,12 @@ NS_ASSUME_NONNULL_BEGIN
}
}
_messageSender = messageSender ;
_messageSender = messageSender ;
_dbConnection = storageManager . newDatabaseConnection ;
_outgoingMessageFinder = [ [ OWSOutgoingMessageFinder alloc ] initWithDBConnection : self . dbConnection ] ;
_toLinkedDevicesReadReceiptMap = [ NSMutableDictionary new ] ;
_toLinkedDevicesReadReceiptMap = [ NSMutableDictionary new ] ;
_toSenderReadReceiptMap = [ NSMutableDictionary new ] ;
OWSSingletonAssert ( ) ;
OWSSingletonAssert ( ) ;
@ -95,6 +248,8 @@ NS_ASSUME_NONNULL_BEGIN
@ synchronized ( self )
@ synchronized ( self )
{
{
if ( [ TSDatabaseView hasPendingViewRegistrations ] ) {
if ( [ TSDatabaseView hasPendingViewRegistrations ] ) {
DDLogInfo (
@ "%@ Deferring read receipt processing due to pending database view registrations.", self.tag);
return ;
return ;
}
}
if ( self . isProcessing ) {
if ( self . isProcessing ) {
@ -122,30 +277,112 @@ NS_ASSUME_NONNULL_BEGIN
{
{
@ synchronized ( self )
@ synchronized ( self )
{
{
DDLogVerbose ( @ "%@ Processing read receipts.", self.tag);
self . isProcessing = NO ;
self . isProcessing = NO ;
NSArray < OWSReadReceipt * > * readReceipts ToSend = [ self . toLinkedDevicesReadReceiptMap allValues ] ;
NSArray < OWSReadReceipt * > * readReceipts ForLinkedDevices = [ self . toLinkedDevicesReadReceiptMap allValues ] ;
[ self . toLinkedDevicesReadReceiptMap removeAllObjects ] ;
[ self . toLinkedDevicesReadReceiptMap removeAllObjects ] ;
if ( readReceiptsToSend . count > 0 ) {
if ( readReceiptsForLinkedDevices . count > 0 ) {
OWSReadReceiptsMessage * message = [ [ OWSReadReceiptsMessage alloc ] initWithReadReceipts : readReceiptsToSend ] ;
OWSReadReceiptsForLinkedDevicesMessage * message =
[ [ OWSReadReceiptsForLinkedDevicesMessage alloc ] initWithReadReceipts : readReceiptsForLinkedDevices ] ;
dispatch_async ( dispatch_get_main_queue ( ) , ^{
dispatch_async ( dispatch_get_main_queue ( ) , ^{
[ self . messageSender sendMessage : message
[ self . messageSender sendMessage : message
success : ^{
success : ^{
DDLogInfo ( @ "%@ Successfully sent %zd read receipt to linked devices.",
DDLogInfo ( @ "%@ Successfully sent %zd read receipt to linked devices.",
self . tag ,
self . tag ,
readReceipts ToSend . count ) ;
readReceipts ForLinkedDevices . count ) ;
}
}
failure : ^( NSError * error ) {
failure : ^( NSError * error ) {
DDLogError ( @ "%@ Failed to send read receipt to linked devices with error: %@", self.tag, error);
DDLogError ( @ "%@ Failed to send read receipt to linked devices with error: %@", self.tag, error);
} ] ;
} ] ;
} ) ;
} ) ;
}
}
NSArray < OWSReadReceipt * > * readReceiptsToSend = [ [ self . toLinkedDevicesReadReceiptMap allValues ] copy ] ;
[ self . toLinkedDevicesReadReceiptMap removeAllObjects ] ;
if ( self . toSenderReadReceiptMap . count > 0 ) {
for ( NSString * recipientId in self . toSenderReadReceiptMap ) {
NSArray < NSNumber * > * timestamps = self . toSenderReadReceiptMap [ recipientId ] ;
OWSAssert ( timestamps . count > 0 ) ;
TSThread * thread = [ TSContactThread getOrCreateThreadWithContactId : recipientId ] ;
OWSReadReceiptsForSenderMessage * message =
[ [ OWSReadReceiptsForSenderMessage alloc ] initWithThread : thread messageTimestamps : timestamps ] ;
dispatch_async ( dispatch_get_main_queue ( ) , ^{
[ self . messageSender sendMessage : message
success : ^{
DDLogInfo ( @ "%@ Successfully sent %zd read receipts to sender.",
self . tag ,
readReceiptsToSend . count ) ;
}
failure : ^( NSError * error ) {
DDLogError ( @ "%@ Failed to send read receipts to sender with error: %@", self.tag, error);
} ] ;
} ) ;
}
[ self . toSenderReadReceiptMap removeAllObjects ] ;
}
}
}
#pragma mark - Mark as Read Locally
- ( void ) markAsReadLocallyBeforeTimestamp : ( uint64_t ) timestamp thread : ( TSThread * ) thread
{
OWSAssert ( thread ) ;
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 ] ;
}
} ] ;
} ) ;
}
}
- ( void ) messageWasReadLocally : ( TSIncomingMessage * ) message ;
- ( void ) messageWasReadLocally : ( TSIncomingMessage * ) message
{
{
dispatch_async ( dispatch_get_global_queue ( DISPATCH_QUEUE_PRIORITY_DEFAULT , 0 ) , ^{
@ synchronized ( self )
@ synchronized ( self )
{
{
NSString * threadUniqueId = message . uniqueThreadId ;
NSString * threadUniqueId = message . uniqueThreadId ;
@ -161,13 +398,131 @@ NS_ASSUME_NONNULL_BEGIN
if ( oldReadReceipt && oldReadReceipt . timestamp > newReadReceipt . timestamp ) {
if ( oldReadReceipt && oldReadReceipt . timestamp > newReadReceipt . timestamp ) {
/ / If there ' s an existing read receipt for the same thread with
/ / If there ' s an existing read receipt for the same thread with
/ / a newer timestamp , discard the new read receipt .
/ / a newer timestamp , discard the new read receipt .
return ;
DDLogVerbose ( @ "%@ Ignoring redundant read receipt for linked devices.", self.tag);
} else {
DDLogVerbose ( @ "%@ Enqueuing read receipt for linked devices.", self.tag);
self . toLinkedDevicesReadReceiptMap [ threadUniqueId ] = newReadReceipt ;
}
}
self . toLinkedDevicesReadReceiptMap [ threadUniqueId ] = newReadReceipt ;
if ( [ self areReadReceiptsEnabled ] ) {
DDLogVerbose ( @ "%@ Enqueuing read receipt for sender.", self.tag);
NSMutableArray < NSNumber * > * _Nullable timestamps = self . toSenderReadReceiptMap [ messageAuthorId ] ;
if ( !timestamps ) {
timestamps = [ NSMutableArray new ] ;
self . toSenderReadReceiptMap [ messageAuthorId ] = timestamps ;
}
[ timestamps addObject : @ ( message . timestamp ) ] ;
}
[ self scheduleProcessing ] ;
[ self scheduleProcessing ] ;
}
}
} ) ;
}
#pragma mark - Read Receipts From Recipient
- ( void ) processReadReceiptsFromRecipient : ( OWSSignalServiceProtosReceiptMessage * ) receiptMessage
envelope : ( OWSSignalServiceProtosEnvelope * ) envelope
{
OWSAssert ( receiptMessage ) ;
OWSAssert ( envelope ) ;
OWSAssert ( receiptMessage . type == OWSSignalServiceProtosReceiptMessageTypeRead ) ;
if ( ![ self areReadReceiptsEnabled ] ) {
DDLogInfo ( @ "%@ Ignoring incoming receipt message as read receipts are disabled.", self.tag);
return ;
}
NSString * recipientId = envelope . source ;
OWSAssert ( recipientId . length > 0 ) ;
PBArray * timestamps = receiptMessage . timestamp ;
dispatch_async ( dispatch_get_global_queue ( DISPATCH_QUEUE_PRIORITY_DEFAULT , 0 ) , ^{
[ self . dbConnection readWriteWithBlock : ^( YapDatabaseReadWriteTransaction * transaction ) {
for ( int i = 0 ; i < timestamps . count ; i + + ) {
UInt64 timestamp = [ timestamps uint64AtIndex : i ] ;
NSArray < TSOutgoingMessage * > * messages =
[ self . outgoingMessageFinder outgoingMessagesWithTimestamp : timestamp transaction : transaction ] ;
OWSAssert ( messages . count < = 1 ) ;
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 updateWithReadRecipient : recipientId transaction : transaction ] ;
}
} else {
/ / Persist the read receipts so that we can apply them to outgoing messages
/ / that we learn about later through sync messages .
NSString * storageKey = [ NSString stringWithFormat : @ "%llu", timestamp];
NSSet < NSString * > * recipientIds =
[ transaction objectForKey : storageKey inCollection : OWSRecipientReadReceiptCollection ] ;
NSMutableSet < NSString * > * recipientIdsCopy
= ( recipientIds ? [ recipientIds mutableCopy ] : [ NSMutableSet new ] ) ;
[ recipientIdsCopy addObject : recipientId ] ;
[ transaction setObject : recipientIdsCopy
forKey : storageKey
inCollection : OWSRecipientReadReceiptCollection ] ;
}
}
} ] ;
} ) ;
}
- ( void ) outgoingMessageFromLinkedDevice : ( TSOutgoingMessage * ) message
transaction : ( YapDatabaseReadWriteTransaction * ) transaction
{
OWSAssert ( message ) ;
OWSAssert ( transaction ) ;
NSString * storageKey = [ NSString stringWithFormat : @ "%llu", message.timestamp];
NSSet < NSString * > * recipientIds =
[ transaction objectForKey : storageKey inCollection : OWSRecipientReadReceiptCollection ] ;
if ( recipientIds ) {
OWSAssert ( recipientIds . count > 0 ) ;
for ( NSString * recipientId in recipientIds ) {
[ message updateWithReadRecipient : recipientId transaction : transaction ] ;
}
}
[ transaction removeObjectForKey : storageKey inCollection : OWSRecipientReadReceiptCollection ] ;
}
#pragma mark - Settings
- ( BOOL ) areReadReceiptsEnabled
{
@ synchronized ( self )
{
if ( !self . areReadReceiptsEnabledCached ) {
/ / Default to NO .
self . areReadReceiptsEnabledCached =
@ ( [ self . dbConnection boolForKey : OWSReadReceiptManagerAreReadReceiptsEnabled
inCollection : OWSReadReceiptManagerCollection ] ) ;
}
return [ self . areReadReceiptsEnabledCached boolValue ] ;
}
}
- ( void ) setAreReadReceiptsEnabled : ( BOOL ) value
{
DDLogInfo ( @ "%@ areReadReceiptsEnabled: %d.", self.tag, value);
@ synchronized ( self )
{
[ self . dbConnection setBool : value
forKey : OWSReadReceiptManagerAreReadReceiptsEnabled
inCollection : OWSReadReceiptManagerCollection ] ;
self . areReadReceiptsEnabledCached = @ ( value ) ;
}
}
#pragma mark - Database Extension
+ ( void ) asyncRegisterDatabaseExtension : ( YapDatabase * ) database
{
[ OWSOutgoingMessageFinder asyncRegisterDatabaseExtension : database ] ;
}
}
#pragma mark - Logging
#pragma mark - Logging