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.
		
		
		
		
		
			
		
			
				
	
	
		
			442 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Objective-C
		
	
			
		
		
	
	
			442 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Objective-C
		
	
| //
 | |
| //  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| #import "TSThread.h"
 | |
| #import "OWSDisappearingMessagesConfiguration.h"
 | |
| #import <SignalCoreKit/Cryptography.h>
 | |
| #import <SignalCoreKit/NSDate+OWS.h>
 | |
| #import <SignalCoreKit/NSString+OWS.h>
 | |
| #import <YapDatabase/YapDatabase.h>
 | |
| #import <SessionMessagingKit/SessionMessagingKit-Swift.h>
 | |
| #import <Curve25519Kit/Curve25519.h>
 | |
| 
 | |
| NS_ASSUME_NONNULL_BEGIN
 | |
| 
 | |
| BOOL IsNoteToSelfEnabled(void)
 | |
| {
 | |
|     return YES;
 | |
| }
 | |
| 
 | |
| @interface TSThread ()
 | |
| 
 | |
| @property (nonatomic) NSDate *creationDate;
 | |
| @property (nonatomic, nullable) NSDate *lastInteractionDate;
 | |
| @property (nonatomic, nullable) NSNumber *archivedAsOfMessageSortId;
 | |
| @property (nonatomic, copy, nullable) NSString *messageDraft;
 | |
| @property (atomic, nullable) NSDate *mutedUntilDate;
 | |
| 
 | |
| @end
 | |
| 
 | |
| @implementation TSThread
 | |
| 
 | |
| #pragma mark Dependencies
 | |
| 
 | |
| - (TSAccountManager *)tsAccountManager
 | |
| {
 | |
|     return SSKEnvironment.shared.tsAccountManager;
 | |
| }
 | |
| 
 | |
| #pragma mark Initialization
 | |
| 
 | |
| + (NSString *)collection {
 | |
|     return @"TSThread";
 | |
| }
 | |
| 
 | |
| - (instancetype)initWithUniqueId:(NSString *_Nullable)uniqueId
 | |
| {
 | |
|     self = [super initWithUniqueId:uniqueId];
 | |
| 
 | |
|     if (self) {
 | |
|         _creationDate    = [NSDate date];
 | |
|         _messageDraft    = nil;
 | |
|     }
 | |
| 
 | |
|     return self;
 | |
| }
 | |
| 
 | |
| - (nullable instancetype)initWithCoder:(NSCoder *)coder
 | |
| {
 | |
|     self = [super initWithCoder:coder];
 | |
|     if (!self) {
 | |
|         return self;
 | |
|     }
 | |
| 
 | |
|     // renamed `hasEverHadMessage` -> `shouldBeVisible`
 | |
|     if (!_shouldBeVisible) {
 | |
|         NSNumber *_Nullable legacy_hasEverHadMessage = [coder decodeObjectForKey:@"hasEverHadMessage"];
 | |
| 
 | |
|         if (legacy_hasEverHadMessage != nil) {
 | |
|             _shouldBeVisible = legacy_hasEverHadMessage.boolValue;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     NSDate *_Nullable lastMessageDate = [coder decodeObjectOfClass:NSDate.class forKey:@"lastMessageDate"];
 | |
|     NSDate *_Nullable archivalDate = [coder decodeObjectOfClass:NSDate.class forKey:@"archivalDate"];
 | |
| 
 | |
|     return self;
 | |
| }
 | |
| 
 | |
| - (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
 | |
| {
 | |
|     [super saveWithTransaction:transaction];
 | |
|     
 | |
|     [SSKPreferences setHasSavedThreadWithValue:YES transaction:transaction];
 | |
| }
 | |
| 
 | |
| - (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
 | |
| {
 | |
|     [self removeAllThreadInteractionsWithTransaction:transaction];
 | |
| 
 | |
|     [super removeWithTransaction:transaction];
 | |
| }
 | |
| 
 | |
| - (void)removeAllThreadInteractionsWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
 | |
| {
 | |
|     // We can't safely delete interactions while enumerating them, so
 | |
|     // we collect and delete separately.
 | |
|     //
 | |
|     // We don't want to instantiate the interactions when collecting them
 | |
|     // or when deleting them.
 | |
|     NSMutableArray<NSString *> *interactionIds = [NSMutableArray new];
 | |
|     YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName];
 | |
|     __block BOOL didDetectCorruption = NO;
 | |
|     [interactionsByThread enumerateKeysInGroup:self.uniqueId
 | |
|                                     usingBlock:^(NSString *collection, NSString *key, NSUInteger index, BOOL *stop) {
 | |
|                                         if (![key isKindOfClass:[NSString class]] || key.length < 1) {
 | |
|                                             didDetectCorruption = YES;
 | |
|                                             return;
 | |
|                                         }
 | |
|                                         [interactionIds addObject:key];
 | |
|                                     }];
 | |
| 
 | |
|     if (didDetectCorruption) {
 | |
|         [OWSPrimaryStorage incrementVersionOfDatabaseExtension:TSMessageDatabaseViewExtensionName];
 | |
|     }
 | |
| 
 | |
|     for (NSString *interactionId in interactionIds) {
 | |
|         // We need to fetch each interaction, since [TSInteraction removeWithTransaction:] does important work.
 | |
|         TSInteraction *_Nullable interaction =
 | |
|             [TSInteraction fetchObjectWithUniqueID:interactionId transaction:transaction];
 | |
|         if (!interaction) {
 | |
|             continue;
 | |
|         }
 | |
|         [interaction removeWithTransaction:transaction];
 | |
|     }
 | |
| }
 | |
| 
 | |
| - (BOOL)isNoteToSelf
 | |
| {
 | |
|     if (!IsNoteToSelfEnabled()) { return NO; }
 | |
|     if (![self isKindOfClass:TSContactThread.class]) { return NO; }
 | |
|     return [self.contactSessionID isEqual:[SNGeneralUtilities getUserPublicKey]];
 | |
| }
 | |
| 
 | |
| // Override in ContactThread
 | |
| - (BOOL)isMessageRequest {
 | |
|     return NO;
 | |
| }
 | |
| 
 | |
| // Override in ContactThread
 | |
| - (BOOL)isMessageRequestUsingTransaction:(YapDatabaseReadTransaction *)transaction {
 | |
|     return NO;
 | |
| }
 | |
| 
 | |
| #pragma mark To be subclassed.
 | |
| 
 | |
| - (BOOL)isGroupThread {
 | |
|     return NO;
 | |
| }
 | |
| 
 | |
| // Override in ContactThread
 | |
| - (nullable NSString *)contactSessionID
 | |
| {
 | |
|     return nil;
 | |
| }
 | |
| 
 | |
| - (NSString *)name {
 | |
|     return nil;
 | |
| }
 | |
| 
 | |
| - (NSString *)nameWithTransaction:(YapDatabaseReadTransaction *)transaction
 | |
| {
 | |
|     return nil;
 | |
| }
 | |
| 
 | |
| - (NSArray<NSString *> *)recipientIdentifiers
 | |
| {
 | |
|     return @[];
 | |
| }
 | |
| 
 | |
| #pragma mark Interactions
 | |
| 
 | |
| /**
 | |
|  * Iterate over this thread's interactions
 | |
|  */
 | |
| - (void)enumerateInteractionsWithTransaction:(YapDatabaseReadTransaction *)transaction
 | |
|                                   usingBlock:(void (^)(TSInteraction *interaction, BOOL *stop))block
 | |
| {
 | |
|     YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName];
 | |
|     [interactionsByThread
 | |
|         enumerateKeysAndObjectsInGroup:self.uniqueId
 | |
|                             usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
 | |
|                                 TSInteraction *interaction = object;
 | |
|                                 block(interaction, stop);
 | |
|                             }];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Enumerates all the threads interactions. Note this will explode if you try to create a transaction in the block.
 | |
|  * If you need a transaction, use the sister method: `enumerateInteractionsWithTransaction:usingBlock`
 | |
|  */
 | |
| - (void)enumerateInteractionsUsingBlock:(void (^)(TSInteraction *interaction))block
 | |
| {
 | |
|     [self.dbReadWriteConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
 | |
|         [self enumerateInteractionsWithTransaction:transaction
 | |
|                                         usingBlock:^(
 | |
|                                             TSInteraction *interaction, BOOL *stop) {
 | |
| 
 | |
|                                             block(interaction);
 | |
|                                         }];
 | |
|     }];
 | |
| }
 | |
| 
 | |
| - (TSInteraction *)lastInteraction
 | |
| {
 | |
|     __block TSInteraction *interaction;
 | |
|     [self.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
 | |
|         interaction = [self getLastInteractionWithTransaction:transaction];
 | |
|     }];
 | |
|     return interaction;
 | |
| }
 | |
| 
 | |
| - (TSInteraction *)getLastInteractionWithTransaction:(YapDatabaseReadTransaction *)transaction
 | |
| {
 | |
|     YapDatabaseViewTransaction *interactions = [transaction ext:TSMessageDatabaseViewExtensionName];
 | |
|     return [interactions lastObjectInGroup:self.uniqueId];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Useful for tests and debugging. In production use an enumeration method.
 | |
|  */
 | |
| - (NSArray<TSInteraction *> *)allInteractions
 | |
| {
 | |
|     NSMutableArray<TSInteraction *> *interactions = [NSMutableArray new];
 | |
|     [self enumerateInteractionsUsingBlock:^(TSInteraction *interaction) {
 | |
|         [interactions addObject:interaction];
 | |
|     }];
 | |
| 
 | |
|     return [interactions copy];
 | |
| }
 | |
| 
 | |
| - (NSUInteger)numberOfInteractions
 | |
| {
 | |
|     __block NSUInteger count;
 | |
|     [[self dbReadConnection] readWithBlock:^(YapDatabaseReadTransaction *transaction) {
 | |
|         count = [self numberOfInteractionsWithTransaction:transaction];
 | |
|     }];
 | |
|     return count;
 | |
| }
 | |
| 
 | |
| - (NSUInteger)numberOfInteractionsWithTransaction:(YapDatabaseReadTransaction *)transaction
 | |
| {
 | |
|     YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName];
 | |
|     return [interactionsByThread numberOfItemsInGroup:self.uniqueId];
 | |
| }
 | |
| 
 | |
| - (NSArray<id<OWSReadTracking>> *)unseenMessagesWithTransaction:(YapDatabaseReadTransaction *)transaction
 | |
| {
 | |
|     NSMutableArray<id<OWSReadTracking>> *messages = [NSMutableArray new];
 | |
|     [[TSDatabaseView unseenDatabaseViewExtension:transaction]
 | |
|         enumerateKeysAndObjectsInGroup:self.uniqueId
 | |
|                             usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
 | |
|                                 if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {
 | |
|                                     return;
 | |
|                                 }
 | |
|                                 id<OWSReadTracking> unread = (id<OWSReadTracking>)object;
 | |
|                                 if (unread.read) {
 | |
|                                     NSLog(@"Found an already read message in the * unseen * messages list.");
 | |
|                                     return;
 | |
|                                 }
 | |
|                                 [messages addObject:unread];
 | |
|                             }];
 | |
| 
 | |
|     return [messages copy];
 | |
| }
 | |
| 
 | |
| - (NSUInteger)unreadMessageCountWithTransaction:(YapDatabaseReadTransaction *)transaction
 | |
| {
 | |
|     YapDatabaseViewTransaction *unreadMessages = [transaction ext:TSUnreadDatabaseViewExtensionName];
 | |
|     return [unreadMessages numberOfItemsInGroup:self.uniqueId];
 | |
|     
 | |
|     
 | |
|     // FIXME: Why did we have to do as the following?
 | |
| //    __block NSUInteger count = 0;
 | |
| //
 | |
| //    YapDatabaseViewTransaction *unreadMessages = [transaction ext:TSUnreadDatabaseViewExtensionName];
 | |
| //    [unreadMessages enumerateKeysAndObjectsInGroup:self.uniqueId
 | |
| //                                        usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
 | |
| //        if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {
 | |
| //            return;
 | |
| //        }
 | |
| //        id<OWSReadTracking> unread = (id<OWSReadTracking>)object;
 | |
| //        if (unread.read) {
 | |
| //            NSLog(@"Found an already read message in the * unread * messages list.");
 | |
| //            return;
 | |
| //        }
 | |
| //        count += 1;
 | |
| //    }];
 | |
|     
 | |
| //    return count;
 | |
| }
 | |
| 
 | |
| - (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
 | |
| {
 | |
|     for (id<OWSReadTracking> message in [self unseenMessagesWithTransaction:transaction]) {
 | |
|         [message markAsReadAtTimestamp:[NSDate ows_millisecondTimeStamp] trySendReadReceipt:YES transaction:transaction];
 | |
|     }
 | |
|     
 | |
|     // Update unread mention.
 | |
|     self.hasUnreadMentionMessage = false;
 | |
|     [super saveWithTransaction:transaction];
 | |
| }
 | |
| 
 | |
| - (nullable TSInteraction *)lastInteractionForInboxWithTransaction:(YapDatabaseReadTransaction *)transaction
 | |
| {
 | |
|     __block NSUInteger missedCount = 0;
 | |
|     __block TSInteraction *last = nil;
 | |
|     [[transaction ext:TSMessageDatabaseViewExtensionName]
 | |
|         enumerateKeysAndObjectsInGroup:self.uniqueId
 | |
|                            withOptions:NSEnumerationReverse
 | |
|                             usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
 | |
|                                 missedCount++;
 | |
|                                 TSInteraction *interaction = (TSInteraction *)object;
 | |
| 
 | |
|                                 if ([TSThread shouldInteractionAppearInInbox:interaction]) {
 | |
|                                     last = interaction;
 | |
| 
 | |
|                                     // For long ignored threads, with lots of SN changes this can get really slow.
 | |
|                                     // I see this in development because I have a lot of long forgotten threads with
 | |
|                                     // members who's test devices are constantly reinstalled. We could add a
 | |
|                                     // purpose-built DB view, but I think in the real world this is rare to be a
 | |
|                                     // hotspot.
 | |
| 
 | |
|                                     *stop = YES;
 | |
|                                 }
 | |
|                             }];
 | |
|     return last;
 | |
| }
 | |
| 
 | |
| - (NSString *)lastMessageTextWithTransaction:(YapDatabaseReadTransaction *)transaction
 | |
| {
 | |
|     TSInteraction *interaction = [self lastInteractionForInboxWithTransaction:transaction];
 | |
|     if ([interaction conformsToProtocol:@protocol(OWSPreviewText)]) {
 | |
|         id<OWSPreviewText> previewable = (id<OWSPreviewText>)interaction;
 | |
|         return [previewable previewTextWithTransaction:transaction].filterStringForDisplay;
 | |
|     } else {
 | |
|         return @"";
 | |
|     }
 | |
| }
 | |
| 
 | |
| // Returns YES IFF the interaction should show up in the inbox as the last message.
 | |
| + (BOOL)shouldInteractionAppearInInbox:(TSInteraction *)interaction
 | |
| {
 | |
|     if (interaction.isDynamicInteraction) {
 | |
|         return NO;
 | |
|     }
 | |
|     
 | |
|     if ([interaction isKindOfClass:[TSMessage class]]) {
 | |
|         TSMessage *message = (TSMessage *)interaction;
 | |
|         if (message.isDeleted) {
 | |
|             return NO;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return YES;
 | |
| }
 | |
| 
 | |
| - (void)updateWithLastMessage:(TSInteraction *)lastMessage transaction:(YapDatabaseReadWriteTransaction *)transaction {
 | |
|     if (![self.class shouldInteractionAppearInInbox:lastMessage]) {
 | |
|         return;
 | |
|     }
 | |
|     
 | |
|     if ([_lastInteractionDate compare: lastMessage.receivedAtDate] == NSOrderedAscending) {
 | |
|         _lastInteractionDate = lastMessage.receivedAtDate;
 | |
|         [super saveWithTransaction:transaction];
 | |
|     }
 | |
|     
 | |
|     // Update unread mention if there is a new incoming message.
 | |
|     if ([lastMessage isKindOfClass:[TSIncomingMessage class]] && ((TSIncomingMessage *)lastMessage).isUserMentioned) {
 | |
|         self.hasUnreadMentionMessage = true;
 | |
|         [super saveWithTransaction:transaction];
 | |
|     }
 | |
| 
 | |
|     if (!self.shouldBeVisible) {
 | |
|         self.shouldBeVisible = YES;
 | |
|         [self saveWithTransaction:transaction];
 | |
|     } else {
 | |
|         [self touchWithTransaction:transaction];
 | |
|     }
 | |
| }
 | |
| 
 | |
| #pragma mark Disappearing Messages
 | |
| 
 | |
| - (OWSDisappearingMessagesConfiguration *)disappearingMessagesConfigurationWithTransaction:
 | |
|     (YapDatabaseReadTransaction *)transaction
 | |
| {
 | |
|     return [OWSDisappearingMessagesConfiguration fetchOrBuildDefaultWithThreadId:self.uniqueId transaction:transaction];
 | |
| }
 | |
| 
 | |
| - (uint32_t)disappearingMessagesDurationWithTransaction:(YapDatabaseReadTransaction *)transaction
 | |
| {
 | |
|     OWSDisappearingMessagesConfiguration *config = [self disappearingMessagesConfigurationWithTransaction:transaction];
 | |
| 
 | |
|     if (!config.isEnabled) {
 | |
|         return 0;
 | |
|     } else {
 | |
|         return config.durationSeconds;
 | |
|     }
 | |
| }
 | |
| 
 | |
| #pragma mark Drafts
 | |
| 
 | |
| - (NSString *)currentDraftWithTransaction:(YapDatabaseReadTransaction *)transaction {
 | |
|     TSThread *thread = [TSThread fetchObjectWithUniqueID:self.uniqueId transaction:transaction];
 | |
|     if (thread.messageDraft) {
 | |
|         return thread.messageDraft;
 | |
|     } else {
 | |
|         return @"";
 | |
|     }
 | |
| }
 | |
| 
 | |
| - (void)setDraft:(NSString *)draftString transaction:(YapDatabaseReadWriteTransaction *)transaction {
 | |
|     TSThread *thread    = [TSThread fetchObjectWithUniqueID:self.uniqueId transaction:transaction];
 | |
|     thread.messageDraft = draftString;
 | |
|     [thread saveWithTransaction:transaction];
 | |
| }
 | |
| 
 | |
| #pragma mark Muting
 | |
| 
 | |
| - (BOOL)isMuted
 | |
| {
 | |
|     NSDate *mutedUntilDate = self.mutedUntilDate;
 | |
|     NSDate *now = [NSDate date];
 | |
|     return (mutedUntilDate != nil && [mutedUntilDate timeIntervalSinceDate:now] > 0);
 | |
| }
 | |
| 
 | |
| - (void)updateWithMutedUntilDate:(NSDate * _Nullable)mutedUntilDate transaction:(YapDatabaseReadWriteTransaction *)transaction
 | |
| {
 | |
|     [self applyChangeToSelfAndLatestCopy:transaction
 | |
|                              changeBlock:^(TSThread *thread) {
 | |
|                                  [thread setMutedUntilDate:mutedUntilDate];
 | |
|                              }];
 | |
| 
 | |
|     [transaction addCompletionQueue:dispatch_get_main_queue() completionBlock:^{
 | |
|         [NSNotificationCenter.defaultCenter postNotificationName:NSNotification.muteSettingUpdated object:self.uniqueId];
 | |
|     }];
 | |
| }
 | |
| 
 | |
| @end
 | |
| 
 | |
| NS_ASSUME_NONNULL_END
 |