// // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import "TSThread.h" #import "OWSReadTracking.h" #import "TSDatabaseView.h" #import "TSIncomingMessage.h" #import "TSInfoMessage.h" #import "TSInteraction.h" #import "TSInvalidIdentityKeyReceivingErrorMessage.h" #import "TSOutgoingMessage.h" #import "TSStorageManager.h" #import #import NS_ASSUME_NONNULL_BEGIN @interface TSThread () @property (nonatomic) NSDate *creationDate; @property (nonatomic, copy) NSDate *archivalDate; @property (nonatomic) NSDate *lastMessageDate; @property (nonatomic, copy) NSString *messageDraft; @property (atomic, nullable) NSDate *mutedUntilDate; - (TSInteraction *)lastInteraction; @end @implementation TSThread + (NSString *)collection { return @"TSThread"; } - (instancetype)initWithUniqueId:(NSString *)uniqueId { self = [super initWithUniqueId:uniqueId]; if (self) { _archivalDate = nil; _lastMessageDate = nil; _creationDate = [NSDate date]; _messageDraft = nil; } return self; } - (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction { [super removeWithTransaction: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 *interactionIds = [NSMutableArray new]; YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName]; OWSAssert(interactionsByThread); [interactionsByThread enumerateKeysInGroup:self.uniqueId usingBlock:^( NSString *_Nonnull collection, NSString *_Nonnull key, NSUInteger index, BOOL *_Nonnull stop) { [interactionIds addObject:key]; }]; for (NSString *interactionId in interactionIds) { [transaction removeObjectForKey:interactionId inCollection:[[TSInteraction class] collection]]; } } #pragma mark To be subclassed. - (BOOL)isGroupThread { NSAssert(false, @"An abstract method on TSThread was called."); return FALSE; } // Override in ContactThread - (nullable NSString *)contactIdentifier { return nil; } - (NSString *)name { NSAssert(FALSE, @"Should be implemented in subclasses"); return nil; } - (NSArray *)recipientIdentifiers { NSAssert(FALSE, @"Should be implemented in subclasses"); return @[]; } - (nullable UIImage *)image { return nil; } - (BOOL)hasSafetyNumbers { return NO; } #pragma mark Interactions /** * Iterate over this thread's interactions */ - (void)enumerateInteractionsWithTransaction:(YapDatabaseReadWriteTransaction *)transaction usingBlock:(void (^)(TSInteraction *interaction, YapDatabaseReadTransaction *transaction))block { void (^interactionBlock)(NSString *, NSString *, id, id, NSUInteger, BOOL *) = ^void(NSString *_Nonnull collection, NSString *_Nonnull key, id _Nonnull object, id _Nonnull metadata, NSUInteger index, BOOL *_Nonnull stop) { TSInteraction *interaction = object; block(interaction, transaction); }; YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName]; [interactionsByThread enumerateRowsInGroup:self.uniqueId usingBlock:interactionBlock]; } /** * 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 readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [self enumerateInteractionsWithTransaction:transaction usingBlock:^( TSInteraction *interaction, YapDatabaseReadTransaction *transaction) { block(interaction); }]; }]; } /** * Useful for tests and debugging. In production use an enumeration method. */ - (NSArray *)allInteractions { NSMutableArray *interactions = [NSMutableArray new]; [self enumerateInteractionsUsingBlock:^(TSInteraction *_Nonnull interaction) { [interactions addObject:interaction]; }]; return [interactions copy]; } - (NSArray *)receivedMessagesForInvalidKey:(NSData *)key { NSMutableArray *errorMessages = [NSMutableArray new]; [self enumerateInteractionsUsingBlock:^(TSInteraction *interaction) { if ([interaction isKindOfClass:[TSInvalidIdentityKeyReceivingErrorMessage class]]) { TSInvalidIdentityKeyReceivingErrorMessage *error = (TSInvalidIdentityKeyReceivingErrorMessage *)interaction; if ([[error newIdentityKey] isEqualToData:key]) { [errorMessages addObject:(TSInvalidIdentityKeyReceivingErrorMessage *)interaction]; } } }]; return [errorMessages copy]; } - (NSUInteger)numberOfInteractions { __block NSUInteger count; [[self dbReadConnection] readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName]; count = [interactionsByThread numberOfItemsInGroup:self.uniqueId]; }]; return count; } - (BOOL)hasUnreadMessages { TSInteraction *interaction = self.lastInteraction; BOOL hasUnread = NO; if ([interaction isKindOfClass:[TSIncomingMessage class]]) { hasUnread = ![(TSIncomingMessage *)interaction wasRead]; } return hasUnread; } - (NSArray> *)unseenMessagesWithTransaction:(YapDatabaseReadTransaction *)transaction { NSMutableArray> *messages = [NSMutableArray new]; [[TSDatabaseView unseenDatabaseViewExtension:transaction] enumerateRowsInGroup:self.uniqueId usingBlock:^( NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { if (![object conformsToProtocol:@protocol(OWSReadTracking)]) { OWSFail(@"%@ Unexpected object in unseen messages: %@", self.tag, object); return; } [messages addObject:(id)object]; }]; return [messages copy]; } - (NSArray > *)unreadMessagesWithTransaction:(YapDatabaseReadTransaction *)transaction { NSMutableArray > *messages = [NSMutableArray new]; [[transaction ext:TSUnreadDatabaseViewExtensionName] enumerateRowsInGroup:self.uniqueId usingBlock:^( NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { if (![object conformsToProtocol:@protocol(OWSReadTracking)]) { OWSFail(@"%@ Unexpected object in unread messages: %@", self.tag, object); return; } [messages addObject:(id)object]; }]; return [messages copy]; } - (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction { for (id message in [self unseenMessagesWithTransaction:transaction]) { [message markAsReadWithTransaction:transaction sendReadReceipt:YES updateExpiration:YES]; } // Just to be defensive, we'll also check for unread messages. OWSAssert([self unseenMessagesWithTransaction:transaction].count < 1); } - (TSInteraction *) lastInteraction { __block TSInteraction *last; [TSStorageManager.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { last = [[transaction ext:TSMessageDatabaseViewExtensionName] lastObjectInGroup:self.uniqueId]; }]; return last; } - (TSInteraction *)lastInteractionForInbox { __block TSInteraction *last = nil; [TSStorageManager.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { [[transaction ext:TSMessageDatabaseViewExtensionName] enumerateRowsInGroup:self.uniqueId withOptions:NSEnumerationReverse usingBlock:^( NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { OWSAssert([object isKindOfClass:[TSInteraction class]]); TSInteraction *interaction = (TSInteraction *)object; if ([TSThread shouldInteractionAppearInInbox:interaction]) { last = interaction; *stop = YES; } }]; }]; return last; } - (NSDate *)lastMessageDate { if (_lastMessageDate) { return _lastMessageDate; } else { return _creationDate; } } - (NSString *)lastMessageLabel { TSInteraction *interaction = self.lastInteractionForInbox; if (interaction == nil) { return @""; } else { return interaction.description; } } // Returns YES IFF the interaction should show up in the inbox as the last message. + (BOOL)shouldInteractionAppearInInbox:(TSInteraction *)interaction { OWSAssert(interaction); if (interaction.isDynamicInteraction) { return NO; } if ([interaction isKindOfClass:[TSErrorMessage class]]) { TSErrorMessage *errorMessage = (TSErrorMessage *)interaction; if (errorMessage.errorType == TSErrorMessageNonBlockingIdentityChange) { // Otherwise all group threads with the recipient will percolate to the top of the inbox, even though // there was no meaningful interaction. return NO; } } else if ([interaction isKindOfClass:[TSInfoMessage class]]) { TSInfoMessage *infoMessage = (TSInfoMessage *)interaction; if (infoMessage.messageType == TSInfoMessageVerificationStateChange) { return NO; } } return YES; } - (void)updateWithLastMessage:(TSInteraction *)lastMessage transaction:(YapDatabaseReadWriteTransaction *)transaction { OWSAssert(lastMessage); OWSAssert(transaction); if (![self.class shouldInteractionAppearInInbox:lastMessage]) { return; } self.hasEverHadMessage = YES; NSDate *lastMessageDate = [lastMessage dateForSorting]; if (!_lastMessageDate || [lastMessageDate timeIntervalSinceDate:self.lastMessageDate] > 0) { _lastMessageDate = lastMessageDate; [self saveWithTransaction:transaction]; } } #pragma mark Archival - (nullable NSDate *)archivalDate { return _archivalDate; } - (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction { [self archiveThreadWithTransaction:transaction referenceDate:[NSDate date]]; } - (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction referenceDate:(NSDate *)date { [self markAllAsReadWithTransaction:transaction]; _archivalDate = date; [self saveWithTransaction:transaction]; } - (void)unarchiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction { _archivalDate = nil; [self saveWithTransaction:transaction]; } #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 - Muted - (BOOL)isMuted { NSDate *mutedUntilDate = self.mutedUntilDate; NSDate *now = [NSDate date]; return (mutedUntilDate != nil && [mutedUntilDate timeIntervalSinceDate:now] > 0); } // This method does the work for the "updateWith..." methods. Please see // the header for a discussion of those methods. - (void)applyChangeToSelfAndLatestThread:(YapDatabaseReadWriteTransaction *)transaction changeBlock:(void (^)(TSThread *))changeBlock { OWSAssert(transaction); changeBlock(self); NSString *collection = [[self class] collection]; TSThread *latestInstance = [transaction objectForKey:self.uniqueId inCollection:collection]; if (latestInstance) { changeBlock(latestInstance); [latestInstance saveWithTransaction:transaction]; } else { // This message has not yet been saved. [self saveWithTransaction:transaction]; } } - (void)updateWithMutedUntilDate:(NSDate *)mutedUntilDate { [self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [self applyChangeToSelfAndLatestThread:transaction changeBlock:^(TSThread *thread) { [thread setMutedUntilDate:mutedUntilDate]; }]; }]; } #pragma mark - Logging + (NSString *)tag { return [NSString stringWithFormat:@"[%@]", self.class]; } - (NSString *)tag { return self.class.tag; } @end NS_ASSUME_NONNULL_END