mirror of https://github.com/oxen-io/session-ios
mirror of https://github.com/oxen-io/session-ios
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
368 lines
16 KiB
368 lines
16 KiB
// |
|
// 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
|
|
|