|  |  |  | // | 
					
						
							|  |  |  | //  Copyright (c) 2017 Open Whisper Systems. All rights reserved. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #import "ThreadUtil.h" | 
					
						
							|  |  |  | #import "OWSContactOffersInteraction.h" | 
					
						
							|  |  |  | #import "OWSContactsManager.h" | 
					
						
							|  |  |  | #import "TSUnreadIndicatorInteraction.h" | 
					
						
							|  |  |  | #import <SignalMessaging/OWSProfileManager.h> | 
					
						
							|  |  |  | #import <SignalMessaging/SignalMessaging-Swift.h> | 
					
						
							|  |  |  | #import <SignalServiceKit/NSDate+OWS.h> | 
					
						
							|  |  |  | #import <SignalServiceKit/OWSAddToContactsOfferMessage.h> | 
					
						
							|  |  |  | #import <SignalServiceKit/OWSAddToProfileWhitelistOfferMessage.h> | 
					
						
							|  |  |  | #import <SignalServiceKit/OWSBlockingManager.h> | 
					
						
							|  |  |  | #import <SignalServiceKit/OWSDisappearingMessagesConfiguration.h> | 
					
						
							|  |  |  | #import <SignalServiceKit/OWSMessageSender.h> | 
					
						
							|  |  |  | #import <SignalServiceKit/OWSUnknownContactBlockOfferMessage.h> | 
					
						
							|  |  |  | #import <SignalServiceKit/TSAccountManager.h> | 
					
						
							|  |  |  | #import <SignalServiceKit/TSCall.h> | 
					
						
							|  |  |  | #import <SignalServiceKit/TSContactThread.h> | 
					
						
							|  |  |  | #import <SignalServiceKit/TSDatabaseView.h> | 
					
						
							|  |  |  | #import <SignalServiceKit/TSIncomingMessage.h> | 
					
						
							|  |  |  | #import <SignalServiceKit/TSInvalidIdentityKeyErrorMessage.h> | 
					
						
							|  |  |  | #import <SignalServiceKit/TSOutgoingMessage.h> | 
					
						
							|  |  |  | #import <SignalServiceKit/TSThread.h> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | NS_ASSUME_NONNULL_BEGIN | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @interface ThreadDynamicInteractions () | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @property (nonatomic, nullable) NSNumber *unreadIndicatorPosition; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @property (nonatomic, nullable) NSNumber *firstUnseenInteractionTimestamp; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @property (nonatomic) BOOL hasMoreUnseenMessages; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #pragma mark - | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @implementation ThreadDynamicInteractions | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - (void)clearUnreadIndicatorState | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     self.unreadIndicatorPosition = nil; | 
					
						
							|  |  |  |     self.firstUnseenInteractionTimestamp = nil; | 
					
						
							|  |  |  |     self.hasMoreUnseenMessages = NO; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #pragma mark - | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @implementation ThreadUtil | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (TSOutgoingMessage *)sendMessageWithText:(NSString *)text | 
					
						
							|  |  |  |                                   inThread:(TSThread *)thread | 
					
						
							|  |  |  |                              messageSender:(OWSMessageSender *)messageSender | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     return [self sendMessageWithText:text | 
					
						
							|  |  |  |         inThread:thread | 
					
						
							|  |  |  |         messageSender:messageSender | 
					
						
							|  |  |  |         success:^{ | 
					
						
							|  |  |  |             DDLogInfo(@"%@ Successfully sent message.", self.logTag); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         failure:^(NSError *error) { | 
					
						
							|  |  |  |             DDLogWarn(@"%@ Failed to deliver message with error: %@", self.logTag, error); | 
					
						
							|  |  |  |         }]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (TSOutgoingMessage *)sendMessageWithText:(NSString *)text | 
					
						
							|  |  |  |                                   inThread:(TSThread *)thread | 
					
						
							|  |  |  |                              messageSender:(OWSMessageSender *)messageSender | 
					
						
							|  |  |  |                                    success:(void (^)(void))successHandler | 
					
						
							|  |  |  |                                    failure:(void (^)(NSError *error))failureHandler | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSAssertIsOnMainThread(); | 
					
						
							|  |  |  |     OWSAssert(text.length > 0); | 
					
						
							|  |  |  |     OWSAssert(thread); | 
					
						
							|  |  |  |     OWSAssert(messageSender); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     OWSDisappearingMessagesConfiguration *configuration = | 
					
						
							|  |  |  |         [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:thread.uniqueId]; | 
					
						
							|  |  |  |     TSOutgoingMessage *message = | 
					
						
							|  |  |  |         [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] | 
					
						
							|  |  |  |                                             inThread:thread | 
					
						
							|  |  |  |                                          messageBody:text | 
					
						
							|  |  |  |                                        attachmentIds:[NSMutableArray new] | 
					
						
							|  |  |  |                                     expiresInSeconds:(configuration.isEnabled ? configuration.durationSeconds : 0)]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     [messageSender enqueueMessage:message success:successHandler failure:failureHandler]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return message; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (TSOutgoingMessage *)sendMessageWithAttachment:(SignalAttachment *)attachment | 
					
						
							|  |  |  |                                         inThread:(TSThread *)thread | 
					
						
							|  |  |  |                                    messageSender:(OWSMessageSender *)messageSender | 
					
						
							|  |  |  |                                       completion:(void (^_Nullable)(NSError *_Nullable error))completion | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     return [self sendMessageWithAttachment:attachment | 
					
						
							|  |  |  |                                   inThread:thread | 
					
						
							|  |  |  |                              messageSender:messageSender | 
					
						
							|  |  |  |                               ignoreErrors:NO | 
					
						
							|  |  |  |                                 completion:completion]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (TSOutgoingMessage *)sendMessageWithAttachment:(SignalAttachment *)attachment | 
					
						
							|  |  |  |                                         inThread:(TSThread *)thread | 
					
						
							|  |  |  |                                    messageSender:(OWSMessageSender *)messageSender | 
					
						
							|  |  |  |                                     ignoreErrors:(BOOL)ignoreErrors | 
					
						
							|  |  |  |                                       completion:(void (^_Nullable)(NSError *_Nullable error))completion | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSAssertIsOnMainThread(); | 
					
						
							|  |  |  |     OWSAssert(attachment); | 
					
						
							|  |  |  |     OWSAssert(ignoreErrors || ![attachment hasError]); | 
					
						
							|  |  |  |     OWSAssert([attachment mimeType].length > 0); | 
					
						
							|  |  |  |     OWSAssert(thread); | 
					
						
							|  |  |  |     OWSAssert(messageSender); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     OWSDisappearingMessagesConfiguration *configuration = | 
					
						
							|  |  |  |         [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:thread.uniqueId]; | 
					
						
							|  |  |  |     TSOutgoingMessage *message = | 
					
						
							|  |  |  |         [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] | 
					
						
							|  |  |  |                                             inThread:thread | 
					
						
							|  |  |  |                                          messageBody:attachment.captionText | 
					
						
							|  |  |  |                                       isVoiceMessage:[attachment isVoiceMessage] | 
					
						
							|  |  |  |                                     expiresInSeconds:(configuration.isEnabled ? configuration.durationSeconds : 0)]; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     [messageSender enqueueAttachment:attachment.dataSource | 
					
						
							|  |  |  |         contentType:attachment.mimeType | 
					
						
							|  |  |  |         sourceFilename:attachment.filenameOrDefault | 
					
						
							|  |  |  |         inMessage:message | 
					
						
							|  |  |  |         success:^{ | 
					
						
							|  |  |  |             DDLogDebug(@"%@ Successfully sent message attachment.", self.logTag); | 
					
						
							|  |  |  |             if (completion) { | 
					
						
							|  |  |  |                 dispatch_async(dispatch_get_main_queue(), ^(void) { | 
					
						
							|  |  |  |                     completion(nil); | 
					
						
							|  |  |  |                 }); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         failure:^(NSError *error) { | 
					
						
							|  |  |  |             DDLogError(@"%@ Failed to send message attachment with error: %@", self.logTag, error); | 
					
						
							|  |  |  |             if (completion) { | 
					
						
							|  |  |  |                 dispatch_async(dispatch_get_main_queue(), ^(void) { | 
					
						
							|  |  |  |                     completion(error); | 
					
						
							|  |  |  |                 }); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         }]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return message; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (ThreadDynamicInteractions *)ensureDynamicInteractionsForThread:(TSThread *)thread | 
					
						
							|  |  |  |                                                   contactsManager:(OWSContactsManager *)contactsManager | 
					
						
							|  |  |  |                                                   blockingManager:(OWSBlockingManager *)blockingManager | 
					
						
							|  |  |  |                                                      dbConnection:(YapDatabaseConnection *)dbConnection | 
					
						
							|  |  |  |                                       hideUnreadMessagesIndicator:(BOOL)hideUnreadMessagesIndicator | 
					
						
							|  |  |  |                                   firstUnseenInteractionTimestamp: | 
					
						
							|  |  |  |                                       (nullable NSNumber *)firstUnseenInteractionTimestampParameter | 
					
						
							|  |  |  |                                                      maxRangeSize:(int)maxRangeSize | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSAssert(thread); | 
					
						
							|  |  |  |     OWSAssert(dbConnection); | 
					
						
							|  |  |  |     OWSAssert(contactsManager); | 
					
						
							|  |  |  |     OWSAssert(blockingManager); | 
					
						
							|  |  |  |     OWSAssert(maxRangeSize > 0); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     NSString *localNumber = [TSAccountManager localNumber]; | 
					
						
							|  |  |  |     OWSAssert(localNumber.length > 0); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Many OWSProfileManager methods aren't safe to call from inside a database | 
					
						
							|  |  |  |     // transaction, so do this work now. | 
					
						
							|  |  |  |     OWSProfileManager *profileManager = OWSProfileManager.sharedManager; | 
					
						
							|  |  |  |     BOOL hasLocalProfile = [profileManager hasLocalProfile]; | 
					
						
							|  |  |  |     BOOL isThreadInProfileWhitelist = [profileManager isThreadInProfileWhitelist:thread]; | 
					
						
							|  |  |  |     BOOL hasUnwhitelistedMember = NO; | 
					
						
							|  |  |  |     for (NSString *recipientId in thread.recipientIdentifiers) { | 
					
						
							|  |  |  |         if (![profileManager isUserInProfileWhitelist:recipientId]) { | 
					
						
							|  |  |  |             hasUnwhitelistedMember = YES; | 
					
						
							|  |  |  |             break; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     ThreadDynamicInteractions *result = [ThreadDynamicInteractions new]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     [dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { | 
					
						
							|  |  |  |         const int kMaxBlockOfferOutgoingMessageCount = 10; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Find any "dynamic" interactions and safety number changes. | 
					
						
							|  |  |  |         // | 
					
						
							|  |  |  |         // We use different views for performance reasons. | 
					
						
							|  |  |  |         __block TSUnreadIndicatorInteraction *existingUnreadIndicator = nil; | 
					
						
							|  |  |  |         __block OWSContactOffersInteraction *existingContactOffers = nil; | 
					
						
							|  |  |  |         NSMutableArray<TSInvalidIdentityKeyErrorMessage *> *blockingSafetyNumberChanges = [NSMutableArray new]; | 
					
						
							|  |  |  |         NSMutableArray<TSInteraction *> *nonBlockingSafetyNumberChanges = [NSMutableArray new]; | 
					
						
							|  |  |  |         // We want to delete legacy and duplicate interactions. | 
					
						
							|  |  |  |         NSMutableArray<TSInteraction *> *interactionsToDelete = [NSMutableArray new]; | 
					
						
							|  |  |  |         [[TSDatabaseView threadSpecialMessagesDatabaseView:transaction] | 
					
						
							|  |  |  |             enumerateRowsInGroup:thread.uniqueId | 
					
						
							|  |  |  |                       usingBlock:^( | 
					
						
							|  |  |  |                           NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                           if ([object isKindOfClass:[OWSUnknownContactBlockOfferMessage class]]) { | 
					
						
							|  |  |  |                               // Delete this legacy interactions, which has been superseded by | 
					
						
							|  |  |  |                               // the OWSContactOffersInteraction. | 
					
						
							|  |  |  |                               [interactionsToDelete addObject:object]; | 
					
						
							|  |  |  |                           } else if ([object isKindOfClass:[OWSAddToContactsOfferMessage class]]) { | 
					
						
							|  |  |  |                               // Delete this legacy interactions, which has been superseded by | 
					
						
							|  |  |  |                               // the OWSContactOffersInteraction. | 
					
						
							|  |  |  |                               [interactionsToDelete addObject:object]; | 
					
						
							|  |  |  |                           } else if ([object isKindOfClass:[OWSAddToProfileWhitelistOfferMessage class]]) { | 
					
						
							|  |  |  |                               // Delete this legacy interactions, which has been superseded by | 
					
						
							|  |  |  |                               // the OWSContactOffersInteraction. | 
					
						
							|  |  |  |                               [interactionsToDelete addObject:object]; | 
					
						
							|  |  |  |                           } else if ([object isKindOfClass:[TSUnreadIndicatorInteraction class]]) { | 
					
						
							|  |  |  |                               OWSAssert(!existingUnreadIndicator); | 
					
						
							|  |  |  |                               if (existingUnreadIndicator) { | 
					
						
							|  |  |  |                                   // There should never be more than one unread indicator in | 
					
						
							|  |  |  |                                   // a given thread, but if there is, discard all but one. | 
					
						
							|  |  |  |                                   [interactionsToDelete addObject:existingUnreadIndicator]; | 
					
						
							|  |  |  |                               } | 
					
						
							|  |  |  |                               existingUnreadIndicator = (TSUnreadIndicatorInteraction *)object; | 
					
						
							|  |  |  |                           } else if ([object isKindOfClass:[OWSContactOffersInteraction class]]) { | 
					
						
							|  |  |  |                               OWSAssert(!existingContactOffers); | 
					
						
							|  |  |  |                               if (existingContactOffers) { | 
					
						
							|  |  |  |                                   // There should never be more than one "contact offers" in | 
					
						
							|  |  |  |                                   // a given thread, but if there is, discard all but one. | 
					
						
							|  |  |  |                                   [interactionsToDelete addObject:existingContactOffers]; | 
					
						
							|  |  |  |                               } | 
					
						
							|  |  |  |                               existingContactOffers = (OWSContactOffersInteraction *)object; | 
					
						
							|  |  |  |                           } else if ([object isKindOfClass:[TSInvalidIdentityKeyErrorMessage class]]) { | 
					
						
							|  |  |  |                               [blockingSafetyNumberChanges addObject:object]; | 
					
						
							|  |  |  |                           } else if ([object isKindOfClass:[TSErrorMessage class]]) { | 
					
						
							|  |  |  |                               TSErrorMessage *errorMessage = (TSErrorMessage *)object; | 
					
						
							|  |  |  |                               OWSAssert(errorMessage.errorType == TSErrorMessageNonBlockingIdentityChange); | 
					
						
							|  |  |  |                               [nonBlockingSafetyNumberChanges addObject:errorMessage]; | 
					
						
							|  |  |  |                           } else { | 
					
						
							|  |  |  |                               OWSFail(@"Unexpected dynamic interaction type: %@", [object class]); | 
					
						
							|  |  |  |                           } | 
					
						
							|  |  |  |                       }]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for (TSInteraction *interaction in interactionsToDelete) { | 
					
						
							|  |  |  |             DDLogDebug(@"Cleaning up interaction: %@", [interaction class]); | 
					
						
							|  |  |  |             [interaction removeWithTransaction:transaction]; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Determine if there are "unread" messages in this conversation. | 
					
						
							|  |  |  |         // If we've been passed a firstUnseenInteractionTimestampParameter, | 
					
						
							|  |  |  |         // just use that value in order to preserve continuity of the | 
					
						
							|  |  |  |         // unread messages indicator after all messages in the conversation | 
					
						
							|  |  |  |         // have been marked as read. | 
					
						
							|  |  |  |         // | 
					
						
							|  |  |  |         // IFF this variable is non-null, there are unseen messages in the thread. | 
					
						
							|  |  |  |         if (firstUnseenInteractionTimestampParameter) { | 
					
						
							|  |  |  |             result.firstUnseenInteractionTimestamp = firstUnseenInteractionTimestampParameter; | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |             TSInteraction *_Nullable firstUnseenInteraction = | 
					
						
							|  |  |  |                 [[TSDatabaseView unseenDatabaseViewExtension:transaction] firstObjectInGroup:thread.uniqueId]; | 
					
						
							|  |  |  |             if (firstUnseenInteraction) { | 
					
						
							|  |  |  |                 result.firstUnseenInteractionTimestamp = @(firstUnseenInteraction.timestampForSorting); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         __block TSInteraction *firstCallOrMessage = nil; | 
					
						
							|  |  |  |         [[transaction ext:TSMessageDatabaseViewExtensionName] | 
					
						
							|  |  |  |             enumerateRowsInGroup:thread.uniqueId | 
					
						
							|  |  |  |                       usingBlock:^( | 
					
						
							|  |  |  |                           NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                           OWSAssert([object isKindOfClass:[TSInteraction class]]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                           if ([object isKindOfClass:[TSIncomingMessage class]] || | 
					
						
							|  |  |  |                               [object isKindOfClass:[TSOutgoingMessage class]] || | 
					
						
							|  |  |  |                               [object isKindOfClass:[TSCall class]]) { | 
					
						
							|  |  |  |                               firstCallOrMessage = object; | 
					
						
							|  |  |  |                               *stop = YES; | 
					
						
							|  |  |  |                           } | 
					
						
							|  |  |  |                       }]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         NSUInteger outgoingMessageCount = | 
					
						
							|  |  |  |             [[TSDatabaseView threadOutgoingMessageDatabaseView:transaction] numberOfItemsInGroup:thread.uniqueId]; | 
					
						
							|  |  |  |         NSUInteger threadMessageCount = | 
					
						
							|  |  |  |             [[transaction ext:TSMessageDatabaseViewExtensionName] numberOfItemsInGroup:thread.uniqueId]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Enumerate in reverse to count the number of messages | 
					
						
							|  |  |  |         // after the unseen messages indicator.  Not all of | 
					
						
							|  |  |  |         // them are unnecessarily unread, but we need to tell | 
					
						
							|  |  |  |         // the messages view the position of the unread indicator, | 
					
						
							|  |  |  |         // so that it can widen its "load window" to always show | 
					
						
							|  |  |  |         // the unread indicator. | 
					
						
							|  |  |  |         __block long visibleUnseenMessageCount = 0; | 
					
						
							|  |  |  |         __block TSInteraction *interactionAfterUnreadIndicator = nil; | 
					
						
							|  |  |  |         NSUInteger missingUnseenSafetyNumberChangeCount = 0; | 
					
						
							|  |  |  |         if (result.firstUnseenInteractionTimestamp != nil) { | 
					
						
							|  |  |  |             [[transaction ext:TSMessageDatabaseViewExtensionName] | 
					
						
							|  |  |  |                 enumerateRowsInGroup:thread.uniqueId | 
					
						
							|  |  |  |                          withOptions:NSEnumerationReverse | 
					
						
							|  |  |  |                           usingBlock:^(NSString *collection, | 
					
						
							|  |  |  |                               NSString *key, | 
					
						
							|  |  |  |                               id object, | 
					
						
							|  |  |  |                               id metadata, | 
					
						
							|  |  |  |                               NSUInteger index, | 
					
						
							|  |  |  |                               BOOL *stop) { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                               if (![object isKindOfClass:[TSInteraction class]]) { | 
					
						
							|  |  |  |                                   OWSFail(@"Expected a TSInteraction: %@", [object class]); | 
					
						
							|  |  |  |                                   return; | 
					
						
							|  |  |  |                               } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                               TSInteraction *interaction = (TSInteraction *)object; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                               if (interaction.isDynamicInteraction) { | 
					
						
							|  |  |  |                                   // Ignore dynamic interactions, if any. | 
					
						
							|  |  |  |                                   return; | 
					
						
							|  |  |  |                               } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                               if (interaction.timestampForSorting | 
					
						
							|  |  |  |                                   < result.firstUnseenInteractionTimestamp.unsignedLongLongValue) { | 
					
						
							|  |  |  |                                   // By default we want the unread indicator to appear just before | 
					
						
							|  |  |  |                                   // the first unread message. | 
					
						
							|  |  |  |                                   *stop = YES; | 
					
						
							|  |  |  |                                   return; | 
					
						
							|  |  |  |                               } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                               visibleUnseenMessageCount++; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                               interactionAfterUnreadIndicator = interaction; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                               if (visibleUnseenMessageCount + 1 >= maxRangeSize) { | 
					
						
							|  |  |  |                                   // If there are more unseen messages than can be displayed in the | 
					
						
							|  |  |  |                                   // messages view, show the unread indicator at the top of the | 
					
						
							|  |  |  |                                   // displayed messages. | 
					
						
							|  |  |  |                                   *stop = YES; | 
					
						
							|  |  |  |                                   result.hasMoreUnseenMessages = YES; | 
					
						
							|  |  |  |                               } | 
					
						
							|  |  |  |                           }]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if (!interactionAfterUnreadIndicator) { | 
					
						
							|  |  |  |                 // If we can't find an interaction after the unread indicator, | 
					
						
							|  |  |  |                 // remove it.  All unread messages may have been deleted or | 
					
						
							|  |  |  |                 // expired. | 
					
						
							|  |  |  |                 result.firstUnseenInteractionTimestamp = nil; | 
					
						
							|  |  |  |             } else if (result.hasMoreUnseenMessages) { | 
					
						
							|  |  |  |                 NSMutableSet<NSData *> *missingUnseenSafetyNumberChanges = [NSMutableSet set]; | 
					
						
							|  |  |  |                 for (TSInvalidIdentityKeyErrorMessage *safetyNumberChange in blockingSafetyNumberChanges) { | 
					
						
							|  |  |  |                     BOOL isUnseen = safetyNumberChange.timestampForSorting | 
					
						
							|  |  |  |                         >= result.firstUnseenInteractionTimestamp.unsignedLongLongValue; | 
					
						
							|  |  |  |                     if (!isUnseen) { | 
					
						
							|  |  |  |                         continue; | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                     BOOL isMissing | 
					
						
							|  |  |  |                         = safetyNumberChange.timestampForSorting < interactionAfterUnreadIndicator.timestampForSorting; | 
					
						
							|  |  |  |                     if (!isMissing) { | 
					
						
							|  |  |  |                         continue; | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     NSData *_Nullable newIdentityKey = safetyNumberChange.newIdentityKey; | 
					
						
							|  |  |  |                     if (newIdentityKey == nil) { | 
					
						
							|  |  |  |                         OWSFail(@"Safety number change was missing it's new identity key."); | 
					
						
							|  |  |  |                         continue; | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     [missingUnseenSafetyNumberChanges addObject:newIdentityKey]; | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 // Count the de-duplicated "blocking" safety number changes and all | 
					
						
							|  |  |  |                 // of the "non-blocking" safety number changes. | 
					
						
							|  |  |  |                 missingUnseenSafetyNumberChangeCount | 
					
						
							|  |  |  |                     = (missingUnseenSafetyNumberChanges.count + nonBlockingSafetyNumberChanges.count); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         if (result.firstUnseenInteractionTimestamp) { | 
					
						
							|  |  |  |             // The unread indicator is _before_ the last visible unseen message. | 
					
						
							|  |  |  |             result.unreadIndicatorPosition = @(visibleUnseenMessageCount); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         OWSAssert((result.firstUnseenInteractionTimestamp != nil) == (result.unreadIndicatorPosition != nil)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         BOOL shouldHaveBlockOffer = YES; | 
					
						
							|  |  |  |         BOOL shouldHaveAddToContactsOffer = YES; | 
					
						
							|  |  |  |         BOOL shouldHaveAddToProfileWhitelistOffer = YES; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         BOOL isContactThread = [thread isKindOfClass:[TSContactThread class]]; | 
					
						
							|  |  |  |         if (!isContactThread) { | 
					
						
							|  |  |  |             // Only create "add to contacts" offers in 1:1 conversations. | 
					
						
							|  |  |  |             shouldHaveAddToContactsOffer = NO; | 
					
						
							|  |  |  |             // Only create block offers in 1:1 conversations. | 
					
						
							|  |  |  |             shouldHaveBlockOffer = NO; | 
					
						
							|  |  |  |             // Only create profile whitelist offers in 1:1 conversations. | 
					
						
							|  |  |  |             shouldHaveAddToProfileWhitelistOffer = NO; | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |             NSString *recipientId = ((TSContactThread *)thread).contactIdentifier; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if ([recipientId isEqualToString:localNumber]) { | 
					
						
							|  |  |  |                 // Don't add self to contacts. | 
					
						
							|  |  |  |                 shouldHaveAddToContactsOffer = NO; | 
					
						
							|  |  |  |                 // Don't bother to block self. | 
					
						
							|  |  |  |                 shouldHaveBlockOffer = NO; | 
					
						
							|  |  |  |                 // Don't bother adding self to profile whitelist. | 
					
						
							|  |  |  |                 shouldHaveAddToProfileWhitelistOffer = NO; | 
					
						
							|  |  |  |             } else { | 
					
						
							|  |  |  |                 if ([[blockingManager blockedPhoneNumbers] containsObject:recipientId]) { | 
					
						
							|  |  |  |                     // Only create "add to contacts" offers for users which are not already blocked. | 
					
						
							|  |  |  |                     shouldHaveAddToContactsOffer = NO; | 
					
						
							|  |  |  |                     // Only create block offers for users which are not already blocked. | 
					
						
							|  |  |  |                     shouldHaveBlockOffer = NO; | 
					
						
							|  |  |  |                     // Don't create profile whitelist offers for users which are not already blocked. | 
					
						
							|  |  |  |                     shouldHaveAddToProfileWhitelistOffer = NO; | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 if ([contactsManager hasSignalAccountForRecipientId:recipientId]) { | 
					
						
							|  |  |  |                     // Only create "add to contacts" offers for non-contacts. | 
					
						
							|  |  |  |                     shouldHaveAddToContactsOffer = NO; | 
					
						
							|  |  |  |                     // Only create block offers for non-contacts. | 
					
						
							|  |  |  |                     shouldHaveBlockOffer = NO; | 
					
						
							|  |  |  |                     // Don't create profile whitelist offers for non-contacts. | 
					
						
							|  |  |  |                     shouldHaveAddToProfileWhitelistOffer = NO; | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!firstCallOrMessage) { | 
					
						
							|  |  |  |             shouldHaveAddToContactsOffer = NO; | 
					
						
							|  |  |  |             shouldHaveBlockOffer = NO; | 
					
						
							|  |  |  |             shouldHaveAddToProfileWhitelistOffer = NO; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (outgoingMessageCount > kMaxBlockOfferOutgoingMessageCount) { | 
					
						
							|  |  |  |             // If the user has sent more than N messages, don't show a block offer. | 
					
						
							|  |  |  |             shouldHaveBlockOffer = NO; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         BOOL hasOutgoingBeforeIncomingInteraction = [firstCallOrMessage isKindOfClass:[TSOutgoingMessage class]]; | 
					
						
							|  |  |  |         if ([firstCallOrMessage isKindOfClass:[TSCall class]]) { | 
					
						
							|  |  |  |             TSCall *call = (TSCall *)firstCallOrMessage; | 
					
						
							|  |  |  |             hasOutgoingBeforeIncomingInteraction | 
					
						
							|  |  |  |                 = (call.callType == RPRecentCallTypeOutgoing || call.callType == RPRecentCallTypeOutgoingIncomplete); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         if (hasOutgoingBeforeIncomingInteraction) { | 
					
						
							|  |  |  |             // If there is an outgoing message before an incoming message | 
					
						
							|  |  |  |             // the local user initiated this conversation, don't show a block offer. | 
					
						
							|  |  |  |             shouldHaveBlockOffer = NO; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!hasLocalProfile || isThreadInProfileWhitelist) { | 
					
						
							|  |  |  |             // Don't show offer if thread is local user hasn't configured their profile. | 
					
						
							|  |  |  |             // Don't show offer if thread is already in profile whitelist. | 
					
						
							|  |  |  |             shouldHaveAddToProfileWhitelistOffer = NO; | 
					
						
							|  |  |  |         } else if (thread.isGroupThread && !hasUnwhitelistedMember) { | 
					
						
							|  |  |  |             // Don't show offer in group thread if all members are already individually | 
					
						
							|  |  |  |             // whitelisted. | 
					
						
							|  |  |  |             shouldHaveAddToProfileWhitelistOffer = NO; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         BOOL shouldHaveContactOffers | 
					
						
							|  |  |  |             = (shouldHaveBlockOffer || shouldHaveAddToContactsOffer || shouldHaveAddToProfileWhitelistOffer); | 
					
						
							|  |  |  |         if (isContactThread) { | 
					
						
							|  |  |  |             TSContactThread *contactThread = (TSContactThread *)thread; | 
					
						
							|  |  |  |             if (contactThread.hasDismissedOffers) { | 
					
						
							|  |  |  |                 shouldHaveContactOffers = NO; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // We use these offset to control the ordering of the offers and indicators. | 
					
						
							|  |  |  |         const int kUnreadIndicatorOffset = -1; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // We want the offers to be the first interactions in their | 
					
						
							|  |  |  |         // conversation's timeline, so we back-date them to slightly before | 
					
						
							|  |  |  |         // the first message - or at an aribtrary old timestamp if the | 
					
						
							|  |  |  |         // conversation has no messages. | 
					
						
							|  |  |  |         uint64_t contactOffersTimestamp = [NSDate ows_millisecondTimeStamp]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // If the contact offers' properties have changed, discard the current | 
					
						
							|  |  |  |         // one and create a new one. | 
					
						
							|  |  |  |         if (existingContactOffers) { | 
					
						
							|  |  |  |             if (existingContactOffers.hasBlockOffer != shouldHaveBlockOffer | 
					
						
							|  |  |  |                 || existingContactOffers.hasAddToContactsOffer != shouldHaveAddToContactsOffer | 
					
						
							|  |  |  |                 || existingContactOffers.hasAddToProfileWhitelistOffer != shouldHaveAddToProfileWhitelistOffer) { | 
					
						
							|  |  |  |                 DDLogInfo(@"%@ Removing stale contact offers: %@ (%llu)", | 
					
						
							|  |  |  |                     self.logTag, | 
					
						
							|  |  |  |                     existingContactOffers.uniqueId, | 
					
						
							|  |  |  |                     existingContactOffers.timestampForSorting); | 
					
						
							|  |  |  |                 // Preserve the timestamp of the existing "contact offers" so that | 
					
						
							|  |  |  |                 // we replace it in the same position in the timeline. | 
					
						
							|  |  |  |                 contactOffersTimestamp = existingContactOffers.timestamp; | 
					
						
							|  |  |  |                 [existingContactOffers removeWithTransaction:transaction]; | 
					
						
							|  |  |  |                 existingContactOffers = nil; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (existingContactOffers && !shouldHaveContactOffers) { | 
					
						
							|  |  |  |             DDLogInfo(@"%@ Removing contact offers: %@ (%llu)", | 
					
						
							|  |  |  |                 self.logTag, | 
					
						
							|  |  |  |                 existingContactOffers.uniqueId, | 
					
						
							|  |  |  |                 existingContactOffers.timestampForSorting); | 
					
						
							|  |  |  |             [existingContactOffers removeWithTransaction:transaction]; | 
					
						
							|  |  |  |         } else if (!existingContactOffers && shouldHaveContactOffers) { | 
					
						
							|  |  |  |             NSString *recipientId = ((TSContactThread *)thread).contactIdentifier; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             TSInteraction *offersMessage = | 
					
						
							|  |  |  |                 [[OWSContactOffersInteraction alloc] initWithTimestamp:contactOffersTimestamp | 
					
						
							|  |  |  |                                                                 thread:thread | 
					
						
							|  |  |  |                                                          hasBlockOffer:shouldHaveBlockOffer | 
					
						
							|  |  |  |                                                  hasAddToContactsOffer:shouldHaveAddToContactsOffer | 
					
						
							|  |  |  |                                          hasAddToProfileWhitelistOffer:shouldHaveAddToProfileWhitelistOffer | 
					
						
							|  |  |  |                                                            recipientId:recipientId]; | 
					
						
							|  |  |  |             [offersMessage saveWithTransaction:transaction]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             DDLogInfo(@"%@ Creating contact offers: %@ (%llu)", | 
					
						
							|  |  |  |                 self.logTag, | 
					
						
							|  |  |  |                 offersMessage.uniqueId, | 
					
						
							|  |  |  |                 offersMessage.timestampForSorting); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         BOOL shouldHaveUnreadIndicator | 
					
						
							|  |  |  |             = (interactionAfterUnreadIndicator && !hideUnreadMessagesIndicator && threadMessageCount > 1); | 
					
						
							|  |  |  |         if (!shouldHaveUnreadIndicator) { | 
					
						
							|  |  |  |             if (existingUnreadIndicator) { | 
					
						
							|  |  |  |                 DDLogInfo(@"%@ Removing obsolete TSUnreadIndicatorInteraction: %@", | 
					
						
							|  |  |  |                     self.logTag, | 
					
						
							|  |  |  |                     existingUnreadIndicator.uniqueId); | 
					
						
							|  |  |  |                 [existingUnreadIndicator removeWithTransaction:transaction]; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |             // We want the unread indicator to appear just before the first unread incoming | 
					
						
							|  |  |  |             // message in the conversation timeline... | 
					
						
							|  |  |  |             // | 
					
						
							|  |  |  |             // ...unless we have a fixed timestamp for the unread indicator. | 
					
						
							|  |  |  |             uint64_t indicatorTimestamp | 
					
						
							|  |  |  |                 = (uint64_t)((long long)interactionAfterUnreadIndicator.timestampForSorting + kUnreadIndicatorOffset); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if (indicatorTimestamp && existingUnreadIndicator.timestampForSorting == indicatorTimestamp) { | 
					
						
							|  |  |  |                 // Keep the existing indicator; it is in the correct position. | 
					
						
							|  |  |  |             } else { | 
					
						
							|  |  |  |                 if (existingUnreadIndicator) { | 
					
						
							|  |  |  |                     DDLogInfo(@"%@ Removing TSUnreadIndicatorInteraction due to changed timestamp: %@", | 
					
						
							|  |  |  |                         self.logTag, | 
					
						
							|  |  |  |                         existingUnreadIndicator.uniqueId); | 
					
						
							|  |  |  |                     [existingUnreadIndicator removeWithTransaction:transaction]; | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 TSUnreadIndicatorInteraction *indicator = | 
					
						
							|  |  |  |                     [[TSUnreadIndicatorInteraction alloc] initWithTimestamp:indicatorTimestamp | 
					
						
							|  |  |  |                                                                      thread:thread | 
					
						
							|  |  |  |                                                       hasMoreUnseenMessages:result.hasMoreUnseenMessages | 
					
						
							|  |  |  |                                        missingUnseenSafetyNumberChangeCount:missingUnseenSafetyNumberChangeCount]; | 
					
						
							|  |  |  |                 [indicator saveWithTransaction:transaction]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 DDLogInfo(@"%@ Creating TSUnreadIndicatorInteraction: %@ (%llu)", | 
					
						
							|  |  |  |                     self.logTag, | 
					
						
							|  |  |  |                     indicator.uniqueId, | 
					
						
							|  |  |  |                     indicator.timestampForSorting); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     }]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return result; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (BOOL)shouldShowGroupProfileBannerInThread:(TSThread *)thread blockingManager:(OWSBlockingManager *)blockingManager | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSAssert(thread); | 
					
						
							|  |  |  |     OWSAssert(blockingManager); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!thread.isGroupThread) { | 
					
						
							|  |  |  |         return NO; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if ([OWSProfileManager.sharedManager isThreadInProfileWhitelist:thread]) { | 
					
						
							|  |  |  |         return NO; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (![OWSProfileManager.sharedManager hasLocalProfile]) { | 
					
						
							|  |  |  |         return NO; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     BOOL hasUnwhitelistedMember = NO; | 
					
						
							|  |  |  |     NSArray<NSString *> *blockedPhoneNumbers = [blockingManager blockedPhoneNumbers]; | 
					
						
							|  |  |  |     for (NSString *recipientId in thread.recipientIdentifiers) { | 
					
						
							|  |  |  |         if (![blockedPhoneNumbers containsObject:recipientId] | 
					
						
							|  |  |  |             && ![OWSProfileManager.sharedManager isUserInProfileWhitelist:recipientId]) { | 
					
						
							|  |  |  |             hasUnwhitelistedMember = YES; | 
					
						
							|  |  |  |             break; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (!hasUnwhitelistedMember) { | 
					
						
							|  |  |  |         return NO; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return YES; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (BOOL)addThreadToProfileWhitelistIfEmptyContactThread:(TSThread *)thread | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSAssert(thread); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (thread.isGroupThread) { | 
					
						
							|  |  |  |         return NO; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if ([OWSProfileManager.sharedManager isThreadInProfileWhitelist:thread]) { | 
					
						
							|  |  |  |         return NO; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (!thread.hasEverHadMessage) { | 
					
						
							|  |  |  |         [OWSProfileManager.sharedManager addThreadToProfileWhitelist:thread]; | 
					
						
							|  |  |  |         return YES; | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |         return NO; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | NS_ASSUME_NONNULL_END |