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.
361 lines
15 KiB
Objective-C
361 lines
15 KiB
Objective-C
//
|
|
// 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 (!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
|