989 lines
37 KiB
Objective-C
989 lines
37 KiB
Objective-C
//
|
|
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
#import "TSThread.h"
|
|
#import "OWSDisappearingMessagesConfiguration.h"
|
|
#import "OWSReadTracking.h"
|
|
#import "SSKEnvironment.h"
|
|
#import "TSAccountManager.h"
|
|
#import "TSIncomingMessage.h"
|
|
#import "TSInfoMessage.h"
|
|
#import "TSInteraction.h"
|
|
#import "TSInvalidIdentityKeyReceivingErrorMessage.h"
|
|
#import "TSOutgoingMessage.h"
|
|
#import <SignalCoreKit/Cryptography.h>
|
|
#import <SignalCoreKit/NSDate+OWS.h>
|
|
#import <SignalCoreKit/NSString+OWS.h>
|
|
#import <SignalServiceKit/AppReadiness.h>
|
|
#import <SignalServiceKit/SignalServiceKit-Swift.h>
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
BOOL IsNoteToSelfEnabled(void)
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
ConversationColorName const ConversationColorNameCrimson = @"red";
|
|
ConversationColorName const ConversationColorNameVermilion = @"orange";
|
|
ConversationColorName const ConversationColorNameBurlap = @"brown";
|
|
ConversationColorName const ConversationColorNameForest = @"green";
|
|
ConversationColorName const ConversationColorNameWintergreen = @"light_green";
|
|
ConversationColorName const ConversationColorNameTeal = @"teal";
|
|
ConversationColorName const ConversationColorNameBlue = @"blue";
|
|
ConversationColorName const ConversationColorNameIndigo = @"indigo";
|
|
ConversationColorName const ConversationColorNameViolet = @"purple";
|
|
ConversationColorName const ConversationColorNamePlum = @"pink";
|
|
ConversationColorName const ConversationColorNameTaupe = @"blue_grey";
|
|
ConversationColorName const ConversationColorNameSteel = @"grey";
|
|
|
|
ConversationColorName const ConversationColorNameDefault = ConversationColorNameSteel;
|
|
|
|
@interface TSThread ()
|
|
|
|
@property (nonatomic, nullable) NSDate *creationDate;
|
|
@property (nonatomic) BOOL isArchived;
|
|
@property (nonatomic) BOOL isMarkedUnread;
|
|
|
|
@property (nonatomic, copy, nullable) NSString *messageDraft;
|
|
@property (nonatomic, nullable) MessageBodyRanges *messageDraftBodyRanges;
|
|
|
|
@property (atomic, nullable) NSDate *mutedUntilDate;
|
|
@property (nonatomic) int64_t lastInteractionRowId;
|
|
|
|
@property (nonatomic) uint64_t lastVisibleSortId;
|
|
@property (nonatomic) double lastVisibleSortIdOnScreenPercentage;
|
|
|
|
@property (nonatomic) TSThreadMentionNotificationMode mentionNotificationMode;
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@implementation TSThread
|
|
|
|
#pragma mark - Dependencies
|
|
|
|
- (TSAccountManager *)tsAccountManager
|
|
{
|
|
OWSAssertDebug(SSKEnvironment.shared.tsAccountManager);
|
|
|
|
return SSKEnvironment.shared.tsAccountManager;
|
|
}
|
|
|
|
- (ThreadReadCache *)threadReadCache
|
|
{
|
|
return SSKEnvironment.shared.modelReadCaches.threadReadCache;
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
+ (NSString *)collection {
|
|
return @"TSThread";
|
|
}
|
|
|
|
+ (BOOL)shouldBeIndexedForFTS
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
return [super init];
|
|
}
|
|
|
|
- (instancetype)initWithUniqueId:(NSString *)uniqueId
|
|
{
|
|
self = [super initWithUniqueId:uniqueId];
|
|
|
|
if (self) {
|
|
_creationDate = [NSDate date];
|
|
_messageDraft = nil;
|
|
|
|
// This is overriden in TSContactThread to use the phone number when available
|
|
// We can't use self.colorSeed here because the subclass hasn't done its
|
|
// initializing work yet to set it up.
|
|
_conversationColorName = [self.class stableColorNameForNewConversationWithString:uniqueId];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
// --- CODE GENERATION MARKER
|
|
|
|
// This snippet is generated by /Scripts/sds_codegen/sds_generate.py. Do not manually edit it, instead run `sds_codegen.sh`.
|
|
|
|
// clang-format off
|
|
|
|
- (instancetype)initWithGrdbId:(int64_t)grdbId
|
|
uniqueId:(NSString *)uniqueId
|
|
conversationColorName:(ConversationColorName)conversationColorName
|
|
creationDate:(nullable NSDate *)creationDate
|
|
isArchived:(BOOL)isArchived
|
|
isMarkedUnread:(BOOL)isMarkedUnread
|
|
lastInteractionRowId:(int64_t)lastInteractionRowId
|
|
lastVisibleSortId:(uint64_t)lastVisibleSortId
|
|
lastVisibleSortIdOnScreenPercentage:(double)lastVisibleSortIdOnScreenPercentage
|
|
mentionNotificationMode:(TSThreadMentionNotificationMode)mentionNotificationMode
|
|
messageDraft:(nullable NSString *)messageDraft
|
|
messageDraftBodyRanges:(nullable MessageBodyRanges *)messageDraftBodyRanges
|
|
mutedUntilDate:(nullable NSDate *)mutedUntilDate
|
|
shouldThreadBeVisible:(BOOL)shouldThreadBeVisible
|
|
{
|
|
self = [super initWithGrdbId:grdbId
|
|
uniqueId:uniqueId];
|
|
|
|
if (!self) {
|
|
return self;
|
|
}
|
|
|
|
_conversationColorName = conversationColorName;
|
|
_creationDate = creationDate;
|
|
_isArchived = isArchived;
|
|
_isMarkedUnread = isMarkedUnread;
|
|
_lastInteractionRowId = lastInteractionRowId;
|
|
_lastVisibleSortId = lastVisibleSortId;
|
|
_lastVisibleSortIdOnScreenPercentage = lastVisibleSortIdOnScreenPercentage;
|
|
_mentionNotificationMode = mentionNotificationMode;
|
|
_messageDraft = messageDraft;
|
|
_messageDraftBodyRanges = messageDraftBodyRanges;
|
|
_mutedUntilDate = mutedUntilDate;
|
|
_shouldThreadBeVisible = shouldThreadBeVisible;
|
|
|
|
return self;
|
|
}
|
|
|
|
// clang-format on
|
|
|
|
// --- CODE GENERATION MARKER
|
|
|
|
- (nullable instancetype)initWithCoder:(NSCoder *)coder
|
|
{
|
|
self = [super initWithCoder:coder];
|
|
if (!self) {
|
|
return self;
|
|
}
|
|
|
|
// renamed `hasEverHadMessage` -> `shouldThreadBeVisible`
|
|
if (!_shouldThreadBeVisible) {
|
|
NSNumber *_Nullable legacy_hasEverHadMessage = [coder decodeObjectForKey:@"hasEverHadMessage"];
|
|
|
|
if (legacy_hasEverHadMessage != nil) {
|
|
_shouldThreadBeVisible = legacy_hasEverHadMessage.boolValue;
|
|
}
|
|
}
|
|
|
|
if (_conversationColorName.length == 0) {
|
|
ConversationColorName colorName = [self.class stableColorNameForLegacyConversationWithString:self.colorSeed];
|
|
OWSAssertDebug(colorName);
|
|
|
|
_conversationColorName = colorName;
|
|
} else if (![[[self class] conversationColorNames] containsObject:_conversationColorName]) {
|
|
// If we'd persisted a non-mapped color name
|
|
ConversationColorName _Nullable mappedColorName = self.class.legacyConversationColorMap[_conversationColorName];
|
|
|
|
if (!mappedColorName) {
|
|
// We previously used the wrong values for the new colors, it's possible we persited them.
|
|
// map them to the proper value
|
|
mappedColorName = self.class.legacyFixupConversationColorMap[_conversationColorName];
|
|
}
|
|
|
|
if (!mappedColorName) {
|
|
OWSFailDebug(@"failure: unexpected unmappable conversationColorName: %@", _conversationColorName);
|
|
mappedColorName = ConversationColorNameDefault;
|
|
}
|
|
|
|
_conversationColorName = mappedColorName;
|
|
}
|
|
|
|
NSDate *_Nullable lastMessageDate = [coder decodeObjectOfClass:NSDate.class forKey:@"lastMessageDate"];
|
|
NSDate *_Nullable archivalDate = [coder decodeObjectOfClass:NSDate.class forKey:@"archivalDate"];
|
|
_isArchivedByLegacyTimestampForSorting =
|
|
[self.class legacyIsArchivedWithLastMessageDate:lastMessageDate archivalDate:archivalDate];
|
|
|
|
if ([coder decodeObjectForKey:@"archivedAsOfMessageSortId"] != nil) {
|
|
OWSAssertDebug(!_isArchived);
|
|
_isArchived = YES;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)anyDidInsertWithTransaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[super anyDidInsertWithTransaction:transaction];
|
|
|
|
if (self.shouldThreadBeVisible && ![SSKPreferences hasSavedThreadWithTransaction:transaction]) {
|
|
[SSKPreferences setHasSavedThread:YES transaction:transaction];
|
|
}
|
|
|
|
[self.threadReadCache didInsertOrUpdateThread:self transaction:transaction];
|
|
}
|
|
|
|
- (void)anyDidUpdateWithTransaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[super anyDidUpdateWithTransaction:transaction];
|
|
|
|
if (self.shouldThreadBeVisible && ![SSKPreferences hasSavedThreadWithTransaction:transaction]) {
|
|
[SSKPreferences setHasSavedThread:YES transaction:transaction];
|
|
}
|
|
|
|
[self.threadReadCache didInsertOrUpdateThread:self transaction:transaction];
|
|
|
|
[PinnedThreadManager handleUpdatedThread:self transaction:transaction];
|
|
}
|
|
|
|
- (void)anyDidRemoveWithTransaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[super anyDidRemoveWithTransaction:transaction];
|
|
|
|
[self.threadReadCache didRemoveThread:self transaction:transaction];
|
|
}
|
|
|
|
- (void)anyWillRemoveWithTransaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[super anyWillRemoveWithTransaction:transaction];
|
|
|
|
[self removeAllThreadInteractionsWithTransaction:transaction];
|
|
|
|
// TODO: If we ever use transaction finalizations for more than
|
|
// de-bouncing thread touches, we should promote this to TSYapDatabaseObject
|
|
// (or at least include it in the "will remove" hook for any relevant models.
|
|
[transaction addRemovedFinalizationKey:self.transactionFinalizationKey];
|
|
}
|
|
|
|
- (void)removeAllThreadInteractionsWithTransaction:(SDSAnyWriteTransaction *)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];
|
|
NSError *error;
|
|
InteractionFinder *interactionFinder = [[InteractionFinder alloc] initWithThreadUniqueId:self.uniqueId];
|
|
[interactionFinder enumerateInteractionIdsWithTransaction:transaction
|
|
error:&error
|
|
block:^(NSString *key, BOOL *stop) {
|
|
[interactionIds addObject:key];
|
|
}];
|
|
if (error != nil) {
|
|
OWSFailDebug(@"Error during enumeration: %@", error);
|
|
}
|
|
|
|
[transaction ignoreInteractionUpdatesForThreadUniqueId:self.uniqueId];
|
|
|
|
for (NSString *interactionId in interactionIds) {
|
|
// We need to fetch each interaction, since [TSInteraction removeWithTransaction:] does important work.
|
|
TSInteraction *_Nullable interaction =
|
|
[TSInteraction anyFetchWithUniqueId:interactionId transaction:transaction];
|
|
if (!interaction) {
|
|
OWSFailDebug(@"couldn't load thread's interaction for deletion.");
|
|
continue;
|
|
}
|
|
[interaction anyRemoveWithTransaction:transaction];
|
|
}
|
|
|
|
// As an optimization, we called `ignoreInteractionUpdatesForThreadUniqueId` so as not
|
|
// to re-save the thread after *each* interaction deletion. However, we still need to resave
|
|
// the thread just once, after all the interactions are deleted.
|
|
[self anyUpdateWithTransaction:transaction
|
|
block:^(TSThread *thread) {
|
|
thread.lastInteractionRowId = 0;
|
|
}];
|
|
}
|
|
|
|
- (BOOL)isNoteToSelf
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (NSString *)colorSeed
|
|
{
|
|
return self.uniqueId;
|
|
}
|
|
|
|
#pragma mark - To be subclassed.
|
|
|
|
- (BOOL)isGroupThread {
|
|
OWSAbstractMethod();
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)isGroupV1Thread
|
|
{
|
|
OWSAbstractMethod();
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)isGroupV2Thread
|
|
{
|
|
OWSAbstractMethod();
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (NSArray<SignalServiceAddress *> *)recipientAddresses
|
|
{
|
|
OWSAbstractMethod();
|
|
|
|
return @[];
|
|
}
|
|
|
|
- (BOOL)hasSafetyNumbers
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
#pragma mark - Interactions
|
|
|
|
/**
|
|
* Iterate over this thread's interactions
|
|
*/
|
|
- (void)enumerateRecentInteractionsWithTransaction:(SDSAnyReadTransaction *)transaction
|
|
usingBlock:(void (^)(TSInteraction *interaction))block
|
|
{
|
|
NSError *error;
|
|
InteractionFinder *interactionFinder = [[InteractionFinder alloc] initWithThreadUniqueId:self.uniqueId];
|
|
[interactionFinder enumerateRecentInteractionsWithTransaction:transaction
|
|
error:&error
|
|
block:^(TSInteraction *interaction, BOOL *stop) {
|
|
block(interaction);
|
|
}];
|
|
if (error != nil) {
|
|
OWSFailDebug(@"Error during enumeration: %@", error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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)enumerateRecentInteractionsUsingBlock:(void (^)(TSInteraction *interaction))block
|
|
{
|
|
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
|
|
[self enumerateRecentInteractionsWithTransaction:transaction
|
|
usingBlock:^(TSInteraction *interaction) {
|
|
block(interaction);
|
|
}];
|
|
}];
|
|
}
|
|
|
|
/**
|
|
* Useful for tests and debugging. In production use an enumeration method.
|
|
*/
|
|
- (NSArray<TSInteraction *> *)allInteractions
|
|
{
|
|
NSMutableArray<TSInteraction *> *interactions = [NSMutableArray new];
|
|
[self enumerateRecentInteractionsUsingBlock:^(TSInteraction *interaction) {
|
|
[interactions addObject:interaction];
|
|
}];
|
|
|
|
return [interactions copy];
|
|
}
|
|
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
- (NSArray<TSInvalidIdentityKeyReceivingErrorMessage *> *)receivedMessagesForInvalidKey:(NSData *)key
|
|
{
|
|
NSMutableArray *errorMessages = [NSMutableArray new];
|
|
[self enumerateRecentInteractionsUsingBlock:^(TSInteraction *interaction) {
|
|
if ([interaction isKindOfClass:[TSInvalidIdentityKeyReceivingErrorMessage class]]) {
|
|
TSInvalidIdentityKeyReceivingErrorMessage *error = (TSInvalidIdentityKeyReceivingErrorMessage *)interaction;
|
|
@try {
|
|
if ([[error throws_newIdentityKey] isEqualToData:key]) {
|
|
[errorMessages addObject:(TSInvalidIdentityKeyReceivingErrorMessage *)interaction];
|
|
}
|
|
} @catch (NSException *exception) {
|
|
OWSFailDebug(@"exception: %@", exception);
|
|
}
|
|
}
|
|
}];
|
|
|
|
return [errorMessages copy];
|
|
}
|
|
#pragma clang diagnostic pop
|
|
|
|
- (NSUInteger)numberOfInteractionsWithTransaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(transaction);
|
|
return [[[InteractionFinder alloc] initWithThreadUniqueId:self.uniqueId] countWithTransaction:transaction];
|
|
}
|
|
|
|
- (void)markAllAsReadAndUpdateStorageService:(BOOL)updateStorageService
|
|
transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
BOOL hasPendingMessageRequest = [self hasPendingMessageRequestWithTransaction:transaction.unwrapGrdbWrite];
|
|
OWSReadCircumstance circumstance = hasPendingMessageRequest
|
|
? OWSReadCircumstanceReadOnThisDeviceWhilePendingMessageRequest
|
|
: OWSReadCircumstanceReadOnThisDevice;
|
|
|
|
InteractionFinder *interactionFinder = [[InteractionFinder alloc] initWithThreadUniqueId:self.uniqueId];
|
|
|
|
for (id<OWSReadTracking> message in
|
|
[interactionFinder allUnreadMessagesWithTransaction:transaction.unwrapGrdbRead]) {
|
|
[message markAsReadAtTimestamp:[NSDate ows_millisecondTimeStamp]
|
|
thread:self
|
|
circumstance:circumstance
|
|
transaction:transaction];
|
|
}
|
|
|
|
[self clearMarkedAsUnreadAndUpdateStorageService:updateStorageService transaction:transaction];
|
|
|
|
// Just to be defensive, we'll also check for unread messages.
|
|
OWSAssertDebug([interactionFinder allUnreadMessagesWithTransaction:transaction.unwrapGrdbRead].count < 1);
|
|
}
|
|
|
|
- (void)clearMarkedAsUnreadAndUpdateStorageService:(BOOL)updateStorageService
|
|
transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
__block BOOL wasMarkedUnread;
|
|
[self anyUpdateWithTransaction:transaction
|
|
block:^(TSThread *thread) {
|
|
wasMarkedUnread = thread.isMarkedUnread;
|
|
thread.isMarkedUnread = NO;
|
|
}];
|
|
|
|
if (updateStorageService && wasMarkedUnread) {
|
|
[self recordPendingStorageServiceUpdates];
|
|
}
|
|
}
|
|
|
|
- (void)markAsUnreadAndUpdateStorageService:(BOOL)updateStorageService transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
__block BOOL wasMarkedUnread;
|
|
[self anyUpdateWithTransaction:transaction
|
|
block:^(TSThread *thread) {
|
|
wasMarkedUnread = thread.isMarkedUnread;
|
|
thread.isMarkedUnread = YES;
|
|
}];
|
|
|
|
if (updateStorageService && !wasMarkedUnread) {
|
|
[self recordPendingStorageServiceUpdates];
|
|
}
|
|
}
|
|
|
|
- (nullable TSInteraction *)lastInteractionForInboxWithTransaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(transaction);
|
|
return [[[InteractionFinder alloc] initWithThreadUniqueId:self.uniqueId]
|
|
mostRecentInteractionForInboxWithTransaction:transaction];
|
|
}
|
|
|
|
- (nullable TSInteraction *)firstInteractionAtOrAroundSortId:(uint64_t)sortId
|
|
transaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(transaction);
|
|
return
|
|
[[[InteractionFinder alloc] initWithThreadUniqueId:self.uniqueId] firstInteractionAtOrAroundSortId:sortId
|
|
transaction:transaction];
|
|
}
|
|
|
|
// Returns YES IFF the interaction should show up in the inbox as the last message.
|
|
+ (BOOL)shouldInteractionAppearInInbox:(TSInteraction *)interaction
|
|
{
|
|
OWSAssertDebug(interaction);
|
|
|
|
if (!interaction.shouldBeSaved) {
|
|
OWSFailDebug(@"Unexpected interaction type: %@", interaction.class);
|
|
return NO;
|
|
}
|
|
if (interaction.isDynamicInteraction) {
|
|
OWSFailDebug(@"Unexpected interaction type: %@", interaction.class);
|
|
return NO;
|
|
}
|
|
if ([interaction isKindOfClass:[OWSOutgoingSyncMessage class]]) {
|
|
OWSFailDebug(@"Unexpected interaction type: %@", interaction.class);
|
|
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
|
|
|| infoMessage.messageType == TSInfoMessageProfileUpdate) {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)updateWithInsertedMessage:(TSInteraction *)message transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[self updateWithMessage:message wasMessageInserted:YES transaction:transaction];
|
|
}
|
|
|
|
- (void)updateWithUpdatedMessage:(TSInteraction *)message transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[self updateWithMessage:message wasMessageInserted:NO transaction:transaction];
|
|
}
|
|
|
|
- (int64_t)messageSortIdForMessage:(TSInteraction *)message transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
if (transaction.transitional_yapWriteTransaction) {
|
|
return message.sortId;
|
|
} else {
|
|
if (message.grdbId == nil) {
|
|
OWSFailDebug(@"Missing messageSortId.");
|
|
} else if (message.grdbId.unsignedLongLongValue == 0) {
|
|
OWSFailDebug(@"Invalid messageSortId.");
|
|
} else {
|
|
return message.grdbId.longLongValue;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
- (void)updateWithMessage:(TSInteraction *)message
|
|
wasMessageInserted:(BOOL)wasMessageInserted
|
|
transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(message != nil);
|
|
OWSAssertDebug(transaction != nil);
|
|
|
|
BOOL needsToClearLastVisibleSortId = self.lastVisibleSortId > 0 && wasMessageInserted;
|
|
|
|
if (![self.class shouldInteractionAppearInInbox:message]) {
|
|
|
|
// We want to clear the last visible sort ID on any new message,
|
|
// even if the message doesn't appear in the inbox view.
|
|
if (needsToClearLastVisibleSortId) {
|
|
[self anyUpdateWithTransaction:transaction
|
|
block:^(TSThread *thread) {
|
|
thread.lastVisibleSortId = 0;
|
|
thread.lastVisibleSortIdOnScreenPercentage = 0;
|
|
}];
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
int64_t messageSortId = [self messageSortIdForMessage:message transaction:transaction];
|
|
BOOL needsToMarkAsVisible = !self.shouldThreadBeVisible;
|
|
|
|
BOOL needsToClearArchived = self.isArchived && wasMessageInserted;
|
|
|
|
// Don't clear archived during migrations.
|
|
if (!CurrentAppContext().isRunningTests && !AppReadiness.isAppReady) {
|
|
needsToClearArchived = NO;
|
|
}
|
|
|
|
// Don't clear archived during thread import
|
|
if ([message isKindOfClass:TSInfoMessage.class]
|
|
&& ((TSInfoMessage *)message).messageType == TSInfoMessageSyncedThread) {
|
|
needsToClearArchived = NO;
|
|
}
|
|
|
|
BOOL needsToUpdateLastInteractionRowId = messageSortId > self.lastInteractionRowId;
|
|
|
|
BOOL needsToClearIsMarkedUnread = self.isMarkedUnread && wasMessageInserted;
|
|
|
|
if (needsToMarkAsVisible || needsToClearArchived || needsToUpdateLastInteractionRowId
|
|
|| needsToClearLastVisibleSortId || needsToClearIsMarkedUnread) {
|
|
[self anyUpdateWithTransaction:transaction
|
|
block:^(TSThread *thread) {
|
|
thread.shouldThreadBeVisible = YES;
|
|
thread.lastInteractionRowId = MAX(thread.lastInteractionRowId, messageSortId);
|
|
if (needsToClearArchived) {
|
|
thread.isArchived = NO;
|
|
}
|
|
if (needsToClearLastVisibleSortId) {
|
|
thread.lastVisibleSortId = 0;
|
|
thread.lastVisibleSortIdOnScreenPercentage = 0;
|
|
}
|
|
if (needsToClearIsMarkedUnread) {
|
|
thread.isMarkedUnread = NO;
|
|
[self recordPendingStorageServiceUpdates];
|
|
}
|
|
}];
|
|
} else {
|
|
[self scheduleTouchFinalizationWithTransaction:transaction];
|
|
}
|
|
}
|
|
|
|
- (void)updateWithRemovedMessage:(TSInteraction *)message transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(message != nil);
|
|
OWSAssertDebug(transaction != nil);
|
|
|
|
int64_t messageSortId = [self messageSortIdForMessage:message transaction:transaction];
|
|
BOOL needsToUpdateLastInteractionRowId = messageSortId == self.lastInteractionRowId;
|
|
|
|
BOOL needsToUpdateLastVisibleSortId = self.lastVisibleSortId == messageSortId;
|
|
|
|
if (needsToUpdateLastInteractionRowId || needsToUpdateLastVisibleSortId) {
|
|
[self anyUpdateWithTransaction:transaction
|
|
block:^(TSThread *thread) {
|
|
if (needsToUpdateLastInteractionRowId) {
|
|
TSInteraction *_Nullable latestInteraction =
|
|
[thread lastInteractionForInboxWithTransaction:transaction];
|
|
thread.lastInteractionRowId = latestInteraction ? latestInteraction.sortId : 0;
|
|
}
|
|
|
|
if (needsToUpdateLastVisibleSortId) {
|
|
TSInteraction *_Nullable messageBeforeDeletedMessage =
|
|
[thread firstInteractionAtOrAroundSortId:thread.lastVisibleSortId
|
|
transaction:transaction];
|
|
thread.lastVisibleSortId
|
|
= messageBeforeDeletedMessage ? messageBeforeDeletedMessage.sortId : 0;
|
|
thread.lastVisibleSortIdOnScreenPercentage = 1;
|
|
}
|
|
}];
|
|
} else {
|
|
[self scheduleTouchFinalizationWithTransaction:transaction];
|
|
}
|
|
}
|
|
|
|
- (void)scheduleTouchFinalizationWithTransaction:(SDSAnyWriteTransaction *)transactionForMethod
|
|
{
|
|
OWSAssertDebug(transactionForMethod != nil);
|
|
|
|
// If we insert, update or remove N interactions in a given
|
|
// transactions, we don't need to touch the same thread more
|
|
// than once.
|
|
[transactionForMethod addTransactionFinalizationBlockForKey:self.transactionFinalizationKey
|
|
block:^(SDSAnyWriteTransaction *transactionForBlock) {
|
|
[self.databaseStorage touchThread:self
|
|
shouldReindex:NO
|
|
transaction:transactionForBlock];
|
|
}];
|
|
}
|
|
|
|
- (void)softDeleteThreadWithTransaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[self removeAllThreadInteractionsWithTransaction:transaction];
|
|
[self anyUpdateWithTransaction:transaction
|
|
block:^(TSThread *thread) {
|
|
thread.messageDraft = nil;
|
|
thread.shouldThreadBeVisible = NO;
|
|
}];
|
|
}
|
|
|
|
- (BOOL)hasPendingMessageRequestWithTransaction:(GRDBReadTransaction *)transaction
|
|
{
|
|
return [GRDBThreadFinder hasPendingMessageRequestWithThread:self transaction:transaction];
|
|
}
|
|
|
|
#pragma mark - Disappearing Messages
|
|
|
|
- (OWSDisappearingMessagesConfiguration *)disappearingMessagesConfigurationWithTransaction:
|
|
(SDSAnyReadTransaction *)transaction
|
|
{
|
|
return [OWSDisappearingMessagesConfiguration fetchOrBuildDefaultWithThread:self transaction:transaction];
|
|
}
|
|
|
|
- (uint32_t)disappearingMessagesDurationWithTransaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
|
|
OWSDisappearingMessagesConfiguration *config = [self disappearingMessagesConfigurationWithTransaction:transaction];
|
|
|
|
if (!config.isEnabled) {
|
|
return 0;
|
|
} else {
|
|
return config.durationSeconds;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Archival
|
|
|
|
+ (BOOL)legacyIsArchivedWithLastMessageDate:(nullable NSDate *)lastMessageDate
|
|
archivalDate:(nullable NSDate *)archivalDate
|
|
{
|
|
if (!archivalDate) {
|
|
return NO;
|
|
}
|
|
|
|
if (!lastMessageDate) {
|
|
return YES;
|
|
}
|
|
|
|
return [archivalDate compare:lastMessageDate] != NSOrderedAscending;
|
|
}
|
|
|
|
- (void)archiveThreadAndUpdateStorageService:(BOOL)updateStorageService
|
|
transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[self anyUpdateWithTransaction:transaction
|
|
block:^(TSThread *thread) {
|
|
thread.isArchived = YES;
|
|
}];
|
|
|
|
// We already update storage service below, so we don't need to here.
|
|
[self markAllAsReadAndUpdateStorageService:NO transaction:transaction];
|
|
|
|
if (updateStorageService) {
|
|
[self recordPendingStorageServiceUpdates];
|
|
}
|
|
}
|
|
|
|
- (void)unarchiveThreadAndUpdateStorageService:(BOOL)updateStorageService
|
|
transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[self anyUpdateWithTransaction:transaction
|
|
block:^(TSThread *thread) {
|
|
thread.isArchived = NO;
|
|
}];
|
|
|
|
if (updateStorageService) {
|
|
[self recordPendingStorageServiceUpdates];
|
|
}
|
|
}
|
|
|
|
- (void)unarchiveAndMarkVisibleThreadWithUpdateStorageService:(BOOL)updateStorageService
|
|
transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[self anyUpdateWithTransaction:transaction
|
|
block:^(TSThread *thread) {
|
|
thread.isArchived = NO;
|
|
thread.shouldThreadBeVisible = YES;
|
|
}];
|
|
|
|
if (updateStorageService) {
|
|
[self recordPendingStorageServiceUpdates];
|
|
}
|
|
}
|
|
|
|
- (void)recordPendingStorageServiceUpdates
|
|
{
|
|
if ([self isKindOfClass:[TSGroupThread class]]) {
|
|
TSGroupThread *groupThread = (TSGroupThread *)self;
|
|
[SSKEnvironment.shared.storageServiceManager recordPendingUpdatesWithGroupModel:groupThread.groupModel];
|
|
} else if ([self isKindOfClass:[TSContactThread class]]) {
|
|
TSContactThread *contactThread = (TSContactThread *)self;
|
|
[SSKEnvironment.shared.storageServiceManager
|
|
recordPendingUpdatesWithUpdatedAddresses:@[ contactThread.contactAddress ]];
|
|
} else {
|
|
OWSFailDebug(@"unexpected thread type");
|
|
}
|
|
}
|
|
|
|
#pragma mark - Drafts
|
|
|
|
- (nullable MessageBody *)currentDraftWithTransaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
TSThread *_Nullable thread = [TSThread anyFetchWithUniqueId:self.uniqueId transaction:transaction];
|
|
if (thread.messageDraft != nil) {
|
|
return [[MessageBody alloc] initWithText:thread.messageDraft
|
|
ranges:thread.messageDraftBodyRanges ?: MessageBodyRanges.empty];
|
|
} else {
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
- (void)updateWithDraft:(nullable MessageBody *)draftMessageBody transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[self anyUpdateWithTransaction:transaction
|
|
block:^(TSThread *thread) {
|
|
thread.messageDraft = draftMessageBody.text;
|
|
thread.messageDraftBodyRanges = draftMessageBody.ranges;
|
|
}];
|
|
}
|
|
|
|
#pragma mark - Last Visible
|
|
|
|
- (void)updateWithLastVisibleSortId:(uint64_t)lastVisibleSortId
|
|
onScreenPercentage:(double)onScreenPercentage
|
|
transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(onScreenPercentage >= 0);
|
|
OWSAssertDebug(onScreenPercentage <= 1);
|
|
OWSAssertDebug(transaction);
|
|
|
|
[self anyUpdateWithTransaction:transaction
|
|
block:^(TSThread *thread) {
|
|
thread.lastVisibleSortId = lastVisibleSortId;
|
|
thread.lastVisibleSortIdOnScreenPercentage = onScreenPercentage;
|
|
}];
|
|
}
|
|
|
|
#pragma mark - Muted
|
|
|
|
- (BOOL)isMuted
|
|
{
|
|
NSDate *mutedUntilDate = self.mutedUntilDate;
|
|
NSDate *now = [NSDate date];
|
|
return (mutedUntilDate != nil &&
|
|
[mutedUntilDate timeIntervalSinceDate:now] > 0);
|
|
}
|
|
|
|
- (void)updateWithMutedUntilDate:(nullable NSDate *)mutedUntilDate transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[self anyUpdateWithTransaction:transaction
|
|
block:^(TSThread *thread) {
|
|
[thread setMutedUntilDate:mutedUntilDate];
|
|
}];
|
|
}
|
|
|
|
- (void)updateWithMentionNotificationMode:(TSThreadMentionNotificationMode)mentionNotificationMode
|
|
transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[self anyUpdateWithTransaction:transaction
|
|
block:^(TSThread *thread) {
|
|
thread.mentionNotificationMode = mentionNotificationMode;
|
|
}];
|
|
}
|
|
|
|
#pragma mark - Conversation Color
|
|
|
|
- (ConversationColorName)conversationColorName
|
|
{
|
|
OWSAssertDebug([self.class.conversationColorNames containsObject:_conversationColorName]);
|
|
return _conversationColorName;
|
|
}
|
|
|
|
+ (NSArray<ConversationColorName> *)colorNamesForNewConversation
|
|
{
|
|
// all conversation colors except "steel"
|
|
return @[
|
|
ConversationColorNameCrimson,
|
|
ConversationColorNameVermilion,
|
|
ConversationColorNameBurlap,
|
|
ConversationColorNameForest,
|
|
ConversationColorNameWintergreen,
|
|
ConversationColorNameTeal,
|
|
ConversationColorNameBlue,
|
|
ConversationColorNameIndigo,
|
|
ConversationColorNameViolet,
|
|
ConversationColorNamePlum,
|
|
ConversationColorNameTaupe,
|
|
];
|
|
}
|
|
|
|
+ (NSArray<ConversationColorName> *)conversationColorNames
|
|
{
|
|
return [self.colorNamesForNewConversation arrayByAddingObject:ConversationColorNameDefault];
|
|
}
|
|
|
|
+ (ConversationColorName)stableConversationColorNameForString:(NSString *)colorSeed
|
|
colorNames:(NSArray<ConversationColorName> *)colorNames
|
|
{
|
|
NSData *contactData = [colorSeed dataUsingEncoding:NSUTF8StringEncoding];
|
|
|
|
unsigned long long hash = 0;
|
|
NSUInteger hashingLength = sizeof(hash);
|
|
NSData *_Nullable hashData = [Cryptography computeSHA256Digest:contactData truncatedToBytes:hashingLength];
|
|
if (hashData) {
|
|
[hashData getBytes:&hash length:hashingLength];
|
|
} else {
|
|
OWSFailDebug(@"could not compute hash for color seed.");
|
|
}
|
|
|
|
NSUInteger index = (hash % colorNames.count);
|
|
return [colorNames objectAtIndex:index];
|
|
}
|
|
|
|
+ (ConversationColorName)stableColorNameForNewConversationWithString:(NSString *)colorSeed
|
|
{
|
|
return [self stableConversationColorNameForString:colorSeed colorNames:self.colorNamesForNewConversation];
|
|
}
|
|
|
|
// After introducing new conversation colors, we want to try to maintain as close as possible to the old color for an
|
|
// existing thread.
|
|
+ (ConversationColorName)stableColorNameForLegacyConversationWithString:(NSString *)colorSeed
|
|
{
|
|
NSString *legacyColorName =
|
|
[self stableConversationColorNameForString:colorSeed colorNames:self.legacyConversationColorNames];
|
|
ConversationColorName _Nullable mappedColorName = self.class.legacyConversationColorMap[legacyColorName];
|
|
|
|
if (!mappedColorName) {
|
|
OWSFailDebug(@"failure: unexpected unmappable legacyColorName: %@", legacyColorName);
|
|
return ConversationColorNameDefault;
|
|
}
|
|
|
|
return mappedColorName;
|
|
}
|
|
|
|
+ (NSArray<NSString *> *)legacyConversationColorNames
|
|
{
|
|
return @[
|
|
@"red",
|
|
@"pink",
|
|
@"purple",
|
|
@"indigo",
|
|
@"blue",
|
|
@"cyan",
|
|
@"teal",
|
|
@"green",
|
|
@"deep_orange",
|
|
@"grey"
|
|
];
|
|
}
|
|
|
|
+ (NSDictionary<NSString *, ConversationColorName> *)legacyConversationColorMap
|
|
{
|
|
static NSDictionary<NSString *, ConversationColorName> *colorMap;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
colorMap = @{
|
|
@"red" : ConversationColorNameCrimson,
|
|
@"deep_orange" : ConversationColorNameCrimson,
|
|
@"orange" : ConversationColorNameVermilion,
|
|
@"amber" : ConversationColorNameVermilion,
|
|
@"brown" : ConversationColorNameBurlap,
|
|
@"yellow" : ConversationColorNameBurlap,
|
|
@"pink" : ConversationColorNamePlum,
|
|
@"purple" : ConversationColorNameViolet,
|
|
@"deep_purple" : ConversationColorNameViolet,
|
|
@"indigo" : ConversationColorNameIndigo,
|
|
@"blue" : ConversationColorNameBlue,
|
|
@"light_blue" : ConversationColorNameBlue,
|
|
@"cyan" : ConversationColorNameTeal,
|
|
@"teal" : ConversationColorNameTeal,
|
|
@"green" : ConversationColorNameForest,
|
|
@"light_green" : ConversationColorNameWintergreen,
|
|
@"lime" : ConversationColorNameWintergreen,
|
|
@"blue_grey" : ConversationColorNameTaupe,
|
|
@"grey" : ConversationColorNameSteel,
|
|
};
|
|
});
|
|
|
|
return colorMap;
|
|
}
|
|
|
|
// we temporarily used the wrong value for the new color names.
|
|
+ (NSDictionary<NSString *, ConversationColorName> *)legacyFixupConversationColorMap
|
|
{
|
|
static NSDictionary<NSString *, ConversationColorName> *colorMap;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
colorMap = @{
|
|
@"crimson" : ConversationColorNameCrimson,
|
|
@"vermilion" : ConversationColorNameVermilion,
|
|
@"burlap" : ConversationColorNameBurlap,
|
|
@"forest" : ConversationColorNameForest,
|
|
@"wintergreen" : ConversationColorNameWintergreen,
|
|
@"teal" : ConversationColorNameTeal,
|
|
@"blue" : ConversationColorNameBlue,
|
|
@"indigo" : ConversationColorNameIndigo,
|
|
@"violet" : ConversationColorNameViolet,
|
|
@"plum" : ConversationColorNamePlum,
|
|
@"taupe" : ConversationColorNameTaupe,
|
|
@"steel" : ConversationColorNameSteel,
|
|
};
|
|
});
|
|
|
|
return colorMap;
|
|
}
|
|
|
|
- (void)updateConversationColorName:(ConversationColorName)colorName transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[self anyUpdateWithTransaction:transaction
|
|
block:^(TSThread *thread) {
|
|
thread.conversationColorName = colorName;
|
|
}];
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|