TM-SGNL-iOS/SignalServiceKit/Contacts/TSThread.m
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

520 lines
22 KiB
Objective-C

//
// Copyright 2017 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
#import "TSThread.h"
#import "OWSDisappearingMessagesConfiguration.h"
#import "OWSReadTracking.h"
#import "TSIncomingMessage.h"
#import "TSInfoMessage.h"
#import "TSInteraction.h"
#import "TSInvalidIdentityKeyReceivingErrorMessage.h"
#import "TSOutgoingMessage.h"
#import <SignalServiceKit/NSDate+OWS.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
@import Intents;
NS_ASSUME_NONNULL_BEGIN
@interface TSThread ()
@property (nonatomic, nullable) NSDate *creationDate;
@property (nonatomic) BOOL isArchivedObsolete;
@property (nonatomic) BOOL isMarkedUnreadObsolete;
@property (atomic) uint64_t mutedUntilTimestampObsolete;
@property (nonatomic, nullable) NSDate *mutedUntilDateObsolete;
@property (nonatomic) uint64_t lastVisibleSortIdObsolete;
@property (nonatomic) double lastVisibleSortIdOnScreenPercentageObsolete;
@end
#pragma mark -
@implementation TSThread
- (instancetype)initWithUniqueId:(NSString *)uniqueId
{
self = [super initWithUniqueId:uniqueId];
if (self) {
_creationDate = [NSDate date];
_messageDraft = nil;
_conversationColorNameObsolete = @"Obsolete";
}
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
conversationColorNameObsolete:(NSString *)conversationColorNameObsolete
creationDate:(nullable NSDate *)creationDate
editTargetTimestamp:(nullable NSNumber *)editTargetTimestamp
isArchivedObsolete:(BOOL)isArchivedObsolete
isMarkedUnreadObsolete:(BOOL)isMarkedUnreadObsolete
lastInteractionRowId:(uint64_t)lastInteractionRowId
lastSentStoryTimestamp:(nullable NSNumber *)lastSentStoryTimestamp
lastVisibleSortIdObsolete:(uint64_t)lastVisibleSortIdObsolete
lastVisibleSortIdOnScreenPercentageObsolete:(double)lastVisibleSortIdOnScreenPercentageObsolete
mentionNotificationMode:(TSThreadMentionNotificationMode)mentionNotificationMode
messageDraft:(nullable NSString *)messageDraft
messageDraftBodyRanges:(nullable MessageBodyRanges *)messageDraftBodyRanges
mutedUntilDateObsolete:(nullable NSDate *)mutedUntilDateObsolete
mutedUntilTimestampObsolete:(uint64_t)mutedUntilTimestampObsolete
shouldThreadBeVisible:(BOOL)shouldThreadBeVisible
storyViewMode:(TSThreadStoryViewMode)storyViewMode
{
self = [super initWithGrdbId:grdbId
uniqueId:uniqueId];
if (!self) {
return self;
}
_conversationColorNameObsolete = conversationColorNameObsolete;
_creationDate = creationDate;
_editTargetTimestamp = editTargetTimestamp;
_isArchivedObsolete = isArchivedObsolete;
_isMarkedUnreadObsolete = isMarkedUnreadObsolete;
_lastInteractionRowId = lastInteractionRowId;
_lastSentStoryTimestamp = lastSentStoryTimestamp;
_lastVisibleSortIdObsolete = lastVisibleSortIdObsolete;
_lastVisibleSortIdOnScreenPercentageObsolete = lastVisibleSortIdOnScreenPercentageObsolete;
_mentionNotificationMode = mentionNotificationMode;
_messageDraft = messageDraft;
_messageDraftBodyRanges = messageDraftBodyRanges;
_mutedUntilDateObsolete = mutedUntilDateObsolete;
_mutedUntilTimestampObsolete = mutedUntilTimestampObsolete;
_shouldThreadBeVisible = shouldThreadBeVisible;
_storyViewMode = storyViewMode;
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 (_conversationColorNameObsolete.length == 0) {
_conversationColorNameObsolete = @"Obsolete";
}
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(!_isArchivedObsolete);
_isArchivedObsolete = YES;
}
return self;
}
- (void)anyDidInsertWithTransaction:(SDSAnyWriteTransaction *)transaction
{
[super anyDidInsertWithTransaction:transaction];
[ThreadAssociatedData createFor:self.uniqueId transaction:transaction];
if (self.shouldThreadBeVisible && ![SSKPreferences hasSavedThreadWithTransaction:transaction]) {
[SSKPreferences setHasSavedThread:YES transaction:transaction];
}
[self _anyDidInsertWithTx:transaction];
[SSKEnvironment.shared.modelReadCachesRef.threadReadCache didInsertOrUpdateThread:self transaction:transaction];
}
- (void)anyDidUpdateWithTransaction:(SDSAnyWriteTransaction *)transaction
{
[super anyDidUpdateWithTransaction:transaction];
if (self.shouldThreadBeVisible && ![SSKPreferences hasSavedThreadWithTransaction:transaction]) {
[SSKPreferences setHasSavedThread:YES transaction:transaction];
}
[SSKEnvironment.shared.modelReadCachesRef.threadReadCache didInsertOrUpdateThread:self transaction:transaction];
[PinnedThreadManagerObjcBridge handleUpdatedThread:self transaction:transaction];
}
- (BOOL)isNoteToSelf
{
return NO;
}
- (NSString *)colorSeed
{
return self.uniqueId;
}
#pragma mark - To be subclassed.
- (NSArray<SignalServiceAddress *> *)recipientAddressesWithSneakyTransaction
{
__block NSArray<SignalServiceAddress *> *recipientAddresses;
[SSKEnvironment.shared.databaseStorageRef readWithBlock:^(SDSAnyReadTransaction *transaction) {
recipientAddresses = [self recipientAddressesWithTransaction:transaction];
}];
return recipientAddresses;
}
- (NSArray<SignalServiceAddress *> *)recipientAddressesWithTransaction:(SDSAnyReadTransaction *)transaction
{
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 enumerateRecentInteractionsForConversationViewWithTransaction:transaction
error:&error
block:^BOOL(TSInteraction *interaction) {
block(interaction);
return YES;
}];
if (error != nil) {
OWSFailDebug(@"Error during enumeration: %@", error);
}
}
- (NSArray<TSInvalidIdentityKeyReceivingErrorMessage *> *)receivedMessagesForInvalidKey:(NSData *)key
tx:(SDSAnyReadTransaction *)tx
{
NSMutableArray *errorMessages = [NSMutableArray new];
[self enumerateRecentInteractionsWithTransaction:tx
usingBlock:^(TSInteraction *interaction) {
if ([interaction isKindOfClass:[TSInvalidIdentityKeyReceivingErrorMessage
class]]) {
TSInvalidIdentityKeyReceivingErrorMessage *errorMessage
= (TSInvalidIdentityKeyReceivingErrorMessage *)interaction;
NSError *error;
NSData *newIdentityKey = [errorMessage newIdentityKey:&error];
if (newIdentityKey != nil) {
if ([newIdentityKey isEqualToData:key]) {
[errorMessages addObject:errorMessage];
}
} else {
OWSFailDebug(@"error: %@", error);
}
}
}];
return errorMessages;
}
- (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];
}
- (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];
}
- (uint64_t)messageSortIdForMessage:(TSInteraction *)message
{
if (message.grdbId == nil) {
OWSFailDebug(@"Missing messageSortId.");
} else if (message.grdbId.unsignedLongLongValue == 0) {
OWSFailDebug(@"Invalid messageSortId.");
} else {
return message.grdbId.unsignedLongLongValue;
}
return 0;
}
- (void)updateWithMessage:(TSInteraction *)message
wasMessageInserted:(BOOL)wasMessageInserted
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(message != nil);
OWSAssertDebug(transaction != nil);
BOOL hasLastVisibleInteraction = [self hasLastVisibleInteractionWithTransaction:transaction];
BOOL needsToClearLastVisibleSortId = hasLastVisibleInteraction && wasMessageInserted;
if (![message shouldAppearInInboxWithTransaction:transaction]) {
// 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 clearLastVisibleInteractionWithTransaction:transaction];
}
[self scheduleTouchFinalizationWithTransaction:transaction];
return;
}
uint64_t messageSortId = [self messageSortIdForMessage:message];
BOOL needsToMarkAsVisible = !self.shouldThreadBeVisible;
ThreadAssociatedData *associatedData = [ThreadAssociatedData fetchOrDefaultForThread:self transaction:transaction];
BOOL needsToClearArchived = [self shouldClearArchivedStatusWhenUpdatingWithMessage:message
wasMessageInserted:wasMessageInserted
threadAssociatedData:associatedData
transaction:transaction];
BOOL needsToUpdateLastInteractionRowId = messageSortId > self.lastInteractionRowId;
BOOL needsToClearIsMarkedUnread = associatedData.isMarkedUnread && wasMessageInserted;
if (needsToMarkAsVisible || needsToClearArchived || needsToUpdateLastInteractionRowId
|| needsToClearLastVisibleSortId || needsToClearIsMarkedUnread) {
[self anyUpdateWithTransaction:transaction
block:^(TSThread *thread) {
thread.shouldThreadBeVisible = YES;
thread.lastInteractionRowId = MAX(thread.lastInteractionRowId, messageSortId);
}];
[associatedData clearIsArchived:needsToClearArchived
clearIsMarkedUnread:needsToClearIsMarkedUnread
updateStorageService:YES
transaction:transaction];
if (needsToMarkAsVisible) {
// Non-visible threads don't get indexed, so if we're becoming visible for the first time...
[SSKEnvironment.shared.databaseStorageRef touchThread:self shouldReindex:YES transaction:transaction];
}
if (needsToClearLastVisibleSortId) {
[self clearLastVisibleInteractionWithTransaction:transaction];
}
} else {
[self scheduleTouchFinalizationWithTransaction:transaction];
}
}
- (BOOL)shouldClearArchivedStatusWhenUpdatingWithMessage:(TSInteraction *)message
wasMessageInserted:(BOOL)wasMessageInserted
threadAssociatedData:(ThreadAssociatedData *)threadAssociatedData
transaction:(SDSAnyReadTransaction *)transaction
{
BOOL needsToClearArchived = threadAssociatedData.isArchived && wasMessageInserted;
// Shouldn't clear archived during migrations.
if (!AppContextObjCBridge.shared.isRunningTests && !AppReadinessObjcBridge.isAppReady) {
needsToClearArchived = NO;
}
if ([message isKindOfClass:TSInfoMessage.class]) {
switch (((TSInfoMessage *)message).messageType) {
case TSInfoMessageSyncedThread: // Shouldn't clear archived during thread import.
case TSInfoMessageThreadMerge:
needsToClearArchived = NO;
break;
case TSInfoMessageTypeLocalUserEndedSession:
case TSInfoMessageTypeRemoteUserEndedSession:
case TSInfoMessageUserNotRegistered:
case TSInfoMessageTypeUnsupportedMessage:
case TSInfoMessageTypeGroupUpdate:
case TSInfoMessageTypeGroupQuit:
case TSInfoMessageTypeDisappearingMessagesUpdate:
case TSInfoMessageAddToContactsOffer:
case TSInfoMessageVerificationStateChange:
case TSInfoMessageAddUserToProfileWhitelistOffer:
case TSInfoMessageAddGroupToProfileWhitelistOffer:
case TSInfoMessageUnknownProtocolVersion:
case TSInfoMessageUserJoinedSignal:
case TSInfoMessageProfileUpdate:
case TSInfoMessagePhoneNumberChange:
case TSInfoMessageRecipientHidden:
case TSInfoMessagePaymentsActivationRequest:
case TSInfoMessagePaymentsActivated:
case TSInfoMessageSessionSwitchover:
case TSInfoMessageReportedSpam:
case TSInfoMessageLearnedProfileName:
case TSInfoMessageBlockedOtherUser:
case TSInfoMessageBlockedGroup:
case TSInfoMessageUnblockedOtherUser:
case TSInfoMessageUnblockedGroup:
case TSInfoMessageAcceptedMessageRequest:
break;
}
}
// Shouldn't clear archived if:
// - The thread is muted.
// - The user has requested we keep muted chats archived.
// - The message was sent by someone other than the current user. (If the
// current user sent the message, we should clear archived.)
{
BOOL threadIsMuted = threadAssociatedData.isMuted;
BOOL shouldKeepMutedChatsArchived = [SSKPreferences shouldKeepMutedChatsArchivedWithTransaction:transaction];
BOOL wasMessageSentByUs = [message isKindOfClass:[TSOutgoingMessage class]];
if (threadIsMuted && shouldKeepMutedChatsArchived && !wasMessageSentByUs) {
needsToClearArchived = NO;
}
}
return needsToClearArchived;
}
- (void)updateWithRemovedMessage:(TSInteraction *)message transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(message != nil);
OWSAssertDebug(transaction != nil);
uint64_t messageSortId = [self messageSortIdForMessage:message];
BOOL needsToUpdateLastInteractionRowId = messageSortId == self.lastInteractionRowId;
NSNumber *_Nullable lastVisibleSortId = [self lastVisibleSortIdWithTransaction:transaction];
BOOL needsToUpdateLastVisibleSortId
= (lastVisibleSortId != nil && lastVisibleSortId.unsignedLongLongValue == messageSortId);
[self updateOnInteractionsRemovedWithNeedsToUpdateLastInteractionRowId:needsToUpdateLastInteractionRowId
needsToUpdateLastVisibleSortId:needsToUpdateLastVisibleSortId
lastVisibleSortId:lastVisibleSortId
transaction:transaction];
}
- (void)updateOnInteractionsRemovedWithNeedsToUpdateLastInteractionRowId:(BOOL)needsToUpdateLastInteractionRowId
needsToUpdateLastVisibleSortId:(BOOL)needsToUpdateLastVisibleSortId
transaction:(SDSAnyWriteTransaction *)transaction
{
NSNumber *_Nullable lastVisibleSortId = [self lastVisibleSortIdWithTransaction:transaction];
[self updateOnInteractionsRemovedWithNeedsToUpdateLastInteractionRowId:needsToUpdateLastInteractionRowId
needsToUpdateLastVisibleSortId:needsToUpdateLastVisibleSortId
lastVisibleSortId:lastVisibleSortId
transaction:transaction];
}
- (void)updateOnInteractionsRemovedWithNeedsToUpdateLastInteractionRowId:(BOOL)needsToUpdateLastInteractionRowId
needsToUpdateLastVisibleSortId:(BOOL)needsToUpdateLastVisibleSortId
lastVisibleSortId:(nullable NSNumber *)lastVisibleSortId
transaction:(SDSAnyWriteTransaction *)transaction
{
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 =
[self firstInteractionAtOrAroundSortId:lastVisibleSortId.unsignedLongLongValue transaction:transaction];
if (messageBeforeDeletedMessage != nil) {
[self setLastVisibleInteractionWithSortId:messageBeforeDeletedMessage.sortId
onScreenPercentage:1
transaction:transaction];
} else {
[self clearLastVisibleInteractionWithTransaction:transaction];
}
}
} 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) {
[SSKEnvironment.shared.databaseStorageRef
touchThread:self
shouldReindex:NO
transaction:transactionForBlock];
}];
}
#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;
}
#pragma mark - Merging
- (void)mergeFrom:(TSThread *)otherThread
{
self.shouldThreadBeVisible = self.shouldThreadBeVisible || otherThread.shouldThreadBeVisible;
self.lastInteractionRowId = MAX(self.lastInteractionRowId, otherThread.lastInteractionRowId);
// Copy the draft if this thread doesn't have one. We always assign both
// values if we assign one of them since they're related.
if (self.messageDraft == nil) {
self.messageDraft = otherThread.messageDraft;
self.messageDraftBodyRanges = otherThread.messageDraftBodyRanges;
}
}
@end
NS_ASSUME_NONNULL_END