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.
		
		
		
		
		
			
		
			
				
	
	
		
			369 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Objective-C
		
	
			
		
		
	
	
			369 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Objective-C
		
	
| //
 | |
| //  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| #import "ThreadUtil.h"
 | |
| #import "OWSQuotedReplyModel.h"
 | |
| #import "OWSUnreadIndicator.h"
 | |
| #import "TSUnreadIndicatorInteraction.h"
 | |
| #import <SignalUtilitiesKit/OWSProfileManager.h>
 | |
| #import <SessionMessagingKit/SSKEnvironment.h>
 | |
| #import <SessionMessagingKit/OWSBlockingManager.h>
 | |
| #import <SessionMessagingKit/OWSDisappearingMessagesConfiguration.h>
 | |
| #import <SessionMessagingKit/TSAccountManager.h>
 | |
| #import <SessionMessagingKit/TSContactThread.h>
 | |
| #import <SessionMessagingKit/TSDatabaseView.h>
 | |
| #import <SessionMessagingKit/TSIncomingMessage.h>
 | |
| #import <SessionMessagingKit/TSOutgoingMessage.h>
 | |
| #import <SessionMessagingKit/TSThread.h>
 | |
| #import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
 | |
| 
 | |
| 
 | |
| NS_ASSUME_NONNULL_BEGIN
 | |
| 
 | |
| @interface ThreadDynamicInteractions ()
 | |
| 
 | |
| @property (nonatomic, nullable) NSNumber *focusMessagePosition;
 | |
| 
 | |
| @property (nonatomic, nullable) OWSUnreadIndicator *unreadIndicator;
 | |
| 
 | |
| @end
 | |
| 
 | |
| #pragma mark -
 | |
| 
 | |
| @implementation ThreadDynamicInteractions
 | |
| 
 | |
| - (void)clearUnreadIndicatorState
 | |
| {
 | |
|     self.unreadIndicator = nil;
 | |
| }
 | |
| 
 | |
| - (BOOL)isEqual:(id)object
 | |
| {
 | |
|     if (self == object) {
 | |
|         return YES;
 | |
|     }
 | |
| 
 | |
|     if (![object isKindOfClass:[ThreadDynamicInteractions class]]) {
 | |
|         return NO;
 | |
|     }
 | |
| 
 | |
|     ThreadDynamicInteractions *other = (ThreadDynamicInteractions *)object;
 | |
|     return ([NSObject isNullableObject:self.focusMessagePosition equalTo:other.focusMessagePosition] &&
 | |
|         [NSObject isNullableObject:self.unreadIndicator equalTo:other.unreadIndicator]);
 | |
| }
 | |
| 
 | |
| @end
 | |
| 
 | |
| @implementation ThreadUtil
 | |
| 
 | |
| #pragma mark - Dependencies
 | |
| 
 | |
| + (YapDatabaseConnection *)dbConnection
 | |
| {
 | |
|     return SSKEnvironment.shared.primaryStorage.dbReadWriteConnection;
 | |
| }
 | |
| 
 | |
| #pragma mark - Dynamic Interactions
 | |
| 
 | |
| + (ThreadDynamicInteractions *)ensureDynamicInteractionsForThread:(TSThread *)thread
 | |
|                                                   blockingManager:(OWSBlockingManager *)blockingManager
 | |
|                                                      dbConnection:(YapDatabaseConnection *)dbConnection
 | |
|                                       hideUnreadMessagesIndicator:(BOOL)hideUnreadMessagesIndicator
 | |
|                                               lastUnreadIndicator:(nullable OWSUnreadIndicator *)lastUnreadIndicator
 | |
|                                                    focusMessageId:(nullable NSString *)focusMessageId
 | |
|                                                      maxRangeSize:(int)maxRangeSize
 | |
| {
 | |
|     OWSAssertDebug(thread);
 | |
|     OWSAssertDebug(dbConnection);
 | |
|     OWSAssertDebug(blockingManager);
 | |
|     OWSAssertDebug(maxRangeSize > 0);
 | |
| 
 | |
|     ThreadDynamicInteractions *result = [ThreadDynamicInteractions new];
 | |
| 
 | |
|     [dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
 | |
|         // Find any "dynamic" interactions and safety number changes.
 | |
|         //
 | |
|         // We use different views for performance reasons.
 | |
|         NSMutableArray<TSInteraction *> *nonBlockingSafetyNumberChanges = [NSMutableArray new];
 | |
|         [[TSDatabaseView threadSpecialMessagesDatabaseView:transaction]
 | |
|             enumerateKeysAndObjectsInGroup:thread.uniqueId
 | |
|                                 usingBlock:^(
 | |
|                                     NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
 | |
|                                     if ([object isKindOfClass:[TSErrorMessage class]]) {
 | |
|                                         TSErrorMessage *errorMessage = (TSErrorMessage *)object;
 | |
|                                         OWSAssertDebug(
 | |
|                                             errorMessage.errorType == TSErrorMessageNonBlockingIdentityChange);
 | |
|                                         [nonBlockingSafetyNumberChanges addObject:errorMessage];
 | |
|                                     } else {
 | |
|                                         OWSFailDebug(@"Unexpected dynamic interaction type: %@", [object class]);
 | |
|                                     }
 | |
|                                 }];
 | |
| 
 | |
|         // 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.
 | |
|         NSNumber *_Nullable firstUnseenSortId = nil;
 | |
|         if (lastUnreadIndicator) {
 | |
|             firstUnseenSortId = @(lastUnreadIndicator.firstUnseenSortId);
 | |
|         } else {
 | |
|             TSInteraction *_Nullable firstUnseenInteraction =
 | |
|                 [[TSDatabaseView unseenDatabaseViewExtension:transaction] firstObjectInGroup:thread.uniqueId];
 | |
|             if (firstUnseenInteraction && firstUnseenInteraction.sortId != NULL) {
 | |
|                 firstUnseenSortId = @(firstUnseenInteraction.sortId);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         [self ensureUnreadIndicator:result
 | |
|                                     thread:thread
 | |
|                                transaction:transaction
 | |
|                               maxRangeSize:maxRangeSize
 | |
|             nonBlockingSafetyNumberChanges:nonBlockingSafetyNumberChanges
 | |
|                hideUnreadMessagesIndicator:hideUnreadMessagesIndicator
 | |
|                          firstUnseenSortId:firstUnseenSortId];
 | |
| 
 | |
|         // Determine the position of the focus message _after_ performing any mutations
 | |
|         // around dynamic interactions.
 | |
|         if (focusMessageId != nil) {
 | |
|             result.focusMessagePosition =
 | |
|                 [self focusMessagePositionForThread:thread transaction:transaction focusMessageId:focusMessageId];
 | |
|         }
 | |
|     }];
 | |
| 
 | |
|     return result;
 | |
| }
 | |
| 
 | |
| + (void)ensureUnreadIndicator:(ThreadDynamicInteractions *)dynamicInteractions
 | |
|                             thread:(TSThread *)thread
 | |
|                        transaction:(YapDatabaseReadTransaction *)transaction
 | |
|                       maxRangeSize:(int)maxRangeSize
 | |
|     nonBlockingSafetyNumberChanges:(NSArray<TSInteraction *> *)nonBlockingSafetyNumberChanges
 | |
|        hideUnreadMessagesIndicator:(BOOL)hideUnreadMessagesIndicator
 | |
|                  firstUnseenSortId:(nullable NSNumber *)firstUnseenSortId
 | |
| {
 | |
|     OWSAssertDebug(dynamicInteractions);
 | |
|     OWSAssertDebug(thread);
 | |
|     OWSAssertDebug(transaction);
 | |
|     OWSAssertDebug(nonBlockingSafetyNumberChanges);
 | |
| 
 | |
|     if (hideUnreadMessagesIndicator) {
 | |
|         return;
 | |
|     }
 | |
|     if (!firstUnseenSortId) {
 | |
|         // If there are no unseen interactions, don't show an unread indicator.
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     YapDatabaseViewTransaction *threadMessagesTransaction = [transaction ext:TSMessageDatabaseViewExtensionName];
 | |
|     OWSAssertDebug([threadMessagesTransaction isKindOfClass:[YapDatabaseViewTransaction class]]);
 | |
| 
 | |
|     // Determine unread indicator position, if necessary.
 | |
|     //
 | |
|     // 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;
 | |
|     __block BOOL hasMoreUnseenMessages = NO;
 | |
|     [threadMessagesTransaction
 | |
|         enumerateKeysAndObjectsInGroup:thread.uniqueId
 | |
|                            withOptions:NSEnumerationReverse
 | |
|                             usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
 | |
|                                 if (![object isKindOfClass:[TSInteraction class]]) {
 | |
|                                     OWSFailDebug(@"Expected a TSInteraction: %@", [object class]);
 | |
|                                     return;
 | |
|                                 }
 | |
| 
 | |
|                                 TSInteraction *interaction = (TSInteraction *)object;
 | |
| 
 | |
|                                 if (interaction.isDynamicInteraction) {
 | |
|                                     // Ignore dynamic interactions, if any.
 | |
|                                     return;
 | |
|                                 }
 | |
| 
 | |
|                                 if (interaction.sortId < firstUnseenSortId.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;
 | |
|                                     hasMoreUnseenMessages = YES;
 | |
|                                 }
 | |
|                             }];
 | |
| 
 | |
|     if (!interactionAfterUnreadIndicator) {
 | |
|         // If we can't find an interaction after the unread indicator,
 | |
|         // don't show it.  All unread messages may have been deleted or
 | |
|         // expired.
 | |
|         return;
 | |
|     }
 | |
|     OWSAssertDebug(visibleUnseenMessageCount > 0);
 | |
| 
 | |
|     NSInteger unreadIndicatorPosition = visibleUnseenMessageCount;
 | |
| 
 | |
|     dynamicInteractions.unreadIndicator =
 | |
|         [[OWSUnreadIndicator alloc] initWithFirstUnseenSortId:firstUnseenSortId.unsignedLongLongValue
 | |
|                                         hasMoreUnseenMessages:hasMoreUnseenMessages
 | |
|                          missingUnseenSafetyNumberChangeCount:nonBlockingSafetyNumberChanges.count
 | |
|                                       unreadIndicatorPosition:unreadIndicatorPosition];
 | |
|     OWSLogInfo(@"Creating Unread Indicator: %llu", dynamicInteractions.unreadIndicator.firstUnseenSortId);
 | |
| }
 | |
| 
 | |
| + (nullable NSNumber *)focusMessagePositionForThread:(TSThread *)thread
 | |
|                                          transaction:(YapDatabaseReadTransaction *)transaction
 | |
|                                       focusMessageId:(NSString *)focusMessageId
 | |
| {
 | |
|     OWSAssertDebug(thread);
 | |
|     OWSAssertDebug(transaction);
 | |
|     OWSAssertDebug(focusMessageId);
 | |
| 
 | |
|     YapDatabaseViewTransaction *databaseView = [transaction ext:TSMessageDatabaseViewExtensionName];
 | |
| 
 | |
|     NSString *_Nullable group = nil;
 | |
|     NSUInteger index;
 | |
|     BOOL success =
 | |
|         [databaseView getGroup:&group index:&index forKey:focusMessageId inCollection:TSInteraction.collection];
 | |
|     if (!success) {
 | |
|         // This might happen if the focus message has disappeared
 | |
|         // before this view could appear.
 | |
|         OWSFailDebug(@"failed to find focus message index.");
 | |
|         return nil;
 | |
|     }
 | |
|     if (![group isEqualToString:thread.uniqueId]) {
 | |
|         OWSFailDebug(@"focus message has invalid group.");
 | |
|         return nil;
 | |
|     }
 | |
|     NSUInteger count = [databaseView numberOfItemsInGroup:thread.uniqueId];
 | |
|     if (index >= count) {
 | |
|         OWSFailDebug(@"focus message has invalid index.");
 | |
|         return nil;
 | |
|     }
 | |
|     NSUInteger position = (count - index) - 1;
 | |
|     return @(position);
 | |
| }
 | |
| 
 | |
| #pragma mark - Delete Content
 | |
| 
 | |
| + (void)deleteAllContent
 | |
| {
 | |
|     OWSLogInfo(@"");
 | |
| 
 | |
|     [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
 | |
|         [self removeAllObjectsInCollection:[TSThread collection]
 | |
|                                      class:[TSThread class]
 | |
|                                transaction:transaction];
 | |
|         [self removeAllObjectsInCollection:[TSInteraction collection]
 | |
|                                      class:[TSInteraction class]
 | |
|                                transaction:transaction];
 | |
|         [self removeAllObjectsInCollection:[TSAttachment collection]
 | |
|                                      class:[TSAttachment class]
 | |
|                                transaction:transaction];
 | |
|         @try {
 | |
|             [self removeAllObjectsInCollection:[SignalRecipient collection]
 | |
|                                          class:[SignalRecipient class]
 | |
|                                    transaction:transaction];
 | |
|         } @catch (NSException *exception) {
 | |
|             // Do nothing
 | |
|         }
 | |
|     }];
 | |
|     [TSAttachmentStream deleteAttachments];
 | |
| }
 | |
| 
 | |
| + (void)removeAllObjectsInCollection:(NSString *)collection
 | |
|                                class:(Class) class
 | |
|                          transaction:(YapDatabaseReadWriteTransaction *)transaction {
 | |
|     OWSAssertDebug(collection.length > 0);
 | |
|     OWSAssertDebug(class);
 | |
|     OWSAssertDebug(transaction);
 | |
| 
 | |
|     NSArray<NSString *> *_Nullable uniqueIds = [transaction allKeysInCollection:collection];
 | |
|     if (!uniqueIds) {
 | |
|         OWSFailDebug(@"couldn't load uniqueIds for collection: %@.", collection);
 | |
|         return;
 | |
|     }
 | |
|     OWSLogInfo(@"Deleting %lu objects from: %@", (unsigned long)uniqueIds.count, collection);
 | |
|     NSUInteger count = 0;
 | |
|     for (NSString *uniqueId in uniqueIds) {
 | |
|         // We need to fetch each object, since [TSYapDatabaseObject removeWithTransaction:] sometimes does important
 | |
|         // work.
 | |
|         TSYapDatabaseObject *_Nullable object = [class fetchObjectWithUniqueID:uniqueId transaction:transaction];
 | |
|         if (!object) {
 | |
|             OWSFailDebug(@"couldn't load object for deletion: %@.", collection);
 | |
|             continue;
 | |
|         }
 | |
|         [object removeWithTransaction:transaction];
 | |
|         count++;
 | |
|     };
 | |
|     OWSLogInfo(@"Deleted %lu/%lu objects from: %@", (unsigned long)count, (unsigned long)uniqueIds.count, collection);
 | |
| }
 | |
| 
 | |
| #pragma mark - Find Content
 | |
| 
 | |
| + (nullable TSInteraction *)findInteractionInThreadByTimestamp:(uint64_t)timestamp
 | |
|                                                       authorId:(NSString *)authorId
 | |
|                                                 threadUniqueId:(NSString *)threadUniqueId
 | |
|                                                    transaction:(YapDatabaseReadTransaction *)transaction
 | |
| {
 | |
|     OWSAssertDebug(timestamp > 0);
 | |
|     OWSAssertDebug(authorId.length > 0);
 | |
| 
 | |
|     NSString *localNumber = [TSAccountManager localNumber];
 | |
|     if (localNumber.length < 1) {
 | |
|         OWSFailDebug(@"missing long number.");
 | |
|         return nil;
 | |
|     }
 | |
| 
 | |
|     NSArray<TSInteraction *> *interactions =
 | |
|         [TSInteraction interactionsWithTimestamp:timestamp
 | |
|                                           filter:^(TSInteraction *interaction) {
 | |
|                                               NSString *_Nullable messageAuthorId = nil;
 | |
|                                               if ([interaction isKindOfClass:[TSIncomingMessage class]]) {
 | |
|                                                   TSIncomingMessage *incomingMessage = (TSIncomingMessage *)interaction;
 | |
|                                                   messageAuthorId = incomingMessage.authorId;
 | |
|                                               } else if ([interaction isKindOfClass:[TSOutgoingMessage class]]) {
 | |
|                                                   messageAuthorId = localNumber;
 | |
|                                               }
 | |
|                                               if (messageAuthorId.length < 1) {
 | |
|                                                   return NO;
 | |
|                                               }
 | |
| 
 | |
|                                               if (![authorId isEqualToString:messageAuthorId]) {
 | |
|                                                   return NO;
 | |
|                                               }
 | |
|                                               if (![interaction.uniqueThreadId isEqualToString:threadUniqueId]) {
 | |
|                                                   return NO;
 | |
|                                               }
 | |
|                                               return YES;
 | |
|                                           }
 | |
|                                  withTransaction:transaction];
 | |
|     if (interactions.count < 1) {
 | |
|         return nil;
 | |
|     }
 | |
|     if (interactions.count > 1) {
 | |
|         // In case of collision, take the first.
 | |
|         OWSLogError(@"more than one matching interaction in thread.");
 | |
|     }
 | |
|     return interactions.firstObject;
 | |
| }
 | |
| 
 | |
| @end
 | |
| 
 | |
| NS_ASSUME_NONNULL_END
 |