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.
463 lines
16 KiB
Objective-C
463 lines
16 KiB
Objective-C
//
|
|
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
#import "TSThread.h"
|
|
#import "OWSDisappearingMessagesConfiguration.h"
|
|
#import <SignalCoreKit/Cryptography.h>
|
|
#import <SignalCoreKit/NSDate+OWS.h>
|
|
#import <SignalCoreKit/NSString+OWS.h>
|
|
#import <YapDatabase/YapDatabase.h>
|
|
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
|
|
#import <Curve25519Kit/Curve25519.h>
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
BOOL IsNoteToSelfEnabled(void)
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
@interface TSThread ()
|
|
|
|
@property (nonatomic) NSDate *creationDate;
|
|
@property (nonatomic, nullable) NSNumber *archivedAsOfMessageSortId;
|
|
@property (nonatomic, copy, nullable) NSString *messageDraft;
|
|
@property (atomic, nullable) NSDate *mutedUntilDate;
|
|
|
|
// DEPRECATED - not used since migrating to sortId
|
|
// but keeping these properties around to ease any pain in the back-forth
|
|
// migration while testing. Eventually we can safely delete these as they aren't used anywhere.
|
|
@property (nonatomic, nullable) NSDate *lastMessageDate DEPRECATED_ATTRIBUTE;
|
|
@property (nonatomic, nullable) NSDate *archivalDate DEPRECATED_ATTRIBUTE;
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@implementation TSThread
|
|
|
|
#pragma mark - Dependencies
|
|
|
|
- (TSAccountManager *)tsAccountManager
|
|
{
|
|
return SSKEnvironment.shared.tsAccountManager;
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
+ (NSString *)collection {
|
|
return @"TSThread";
|
|
}
|
|
|
|
- (instancetype)initWithUniqueId:(NSString *_Nullable)uniqueId
|
|
{
|
|
self = [super initWithUniqueId:uniqueId];
|
|
|
|
if (self) {
|
|
_creationDate = [NSDate date];
|
|
_messageDraft = nil;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (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;
|
|
}
|
|
}
|
|
|
|
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];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
|
|
{
|
|
[super saveWithTransaction:transaction];
|
|
|
|
[SSKPreferences setHasSavedThreadWithValue:YES transaction:transaction];
|
|
}
|
|
|
|
- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
|
|
{
|
|
[self removeAllThreadInteractionsWithTransaction:transaction];
|
|
|
|
[super removeWithTransaction:transaction];
|
|
}
|
|
|
|
- (void)removeAllThreadInteractionsWithTransaction:(YapDatabaseReadWriteTransaction *)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];
|
|
YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName];
|
|
__block BOOL didDetectCorruption = NO;
|
|
[interactionsByThread enumerateKeysInGroup:self.uniqueId
|
|
usingBlock:^(NSString *collection, NSString *key, NSUInteger index, BOOL *stop) {
|
|
if (![key isKindOfClass:[NSString class]] || key.length < 1) {
|
|
didDetectCorruption = YES;
|
|
return;
|
|
}
|
|
[interactionIds addObject:key];
|
|
}];
|
|
|
|
if (didDetectCorruption) {
|
|
[OWSPrimaryStorage incrementVersionOfDatabaseExtension:TSMessageDatabaseViewExtensionName];
|
|
}
|
|
|
|
for (NSString *interactionId in interactionIds) {
|
|
// We need to fetch each interaction, since [TSInteraction removeWithTransaction:] does important work.
|
|
TSInteraction *_Nullable interaction =
|
|
[TSInteraction fetchObjectWithUniqueID:interactionId transaction:transaction];
|
|
if (!interaction) {
|
|
continue;
|
|
}
|
|
[interaction removeWithTransaction:transaction];
|
|
}
|
|
}
|
|
|
|
- (BOOL)isNoteToSelf
|
|
{
|
|
if (!IsNoteToSelfEnabled()) { return NO; }
|
|
if (![self isKindOfClass:TSContactThread.class]) { return NO; }
|
|
return [self.contactIdentifier isEqual:[SNGeneralUtilities getUserPublicKey]];
|
|
}
|
|
|
|
#pragma mark - To be subclassed.
|
|
|
|
- (BOOL)isGroupThread {
|
|
return NO;
|
|
}
|
|
|
|
// Override in ContactThread
|
|
- (nullable NSString *)contactIdentifier
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
- (NSString *)name {
|
|
return nil;
|
|
}
|
|
|
|
- (NSArray<NSString *> *)recipientIdentifiers
|
|
{
|
|
return @[];
|
|
}
|
|
|
|
#pragma mark - Interactions
|
|
|
|
/**
|
|
* Iterate over this thread's interactions
|
|
*/
|
|
- (void)enumerateInteractionsWithTransaction:(YapDatabaseReadTransaction *)transaction
|
|
usingBlock:(void (^)(TSInteraction *interaction,
|
|
YapDatabaseReadTransaction *transaction))block
|
|
{
|
|
YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName];
|
|
[interactionsByThread
|
|
enumerateKeysAndObjectsInGroup:self.uniqueId
|
|
usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
|
|
TSInteraction *interaction = object;
|
|
block(interaction, transaction);
|
|
}];
|
|
}
|
|
|
|
/**
|
|
* 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 readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
|
[self enumerateInteractionsWithTransaction:transaction
|
|
usingBlock:^(
|
|
TSInteraction *interaction, YapDatabaseReadTransaction *t) {
|
|
|
|
block(interaction);
|
|
}];
|
|
}];
|
|
}
|
|
|
|
- (TSInteraction *)lastInteraction
|
|
{
|
|
__block TSInteraction *interaction;
|
|
[self.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
|
interaction = [self getLastInteractionWithTransaction:transaction];
|
|
}];
|
|
return interaction;
|
|
}
|
|
|
|
- (TSInteraction *)getLastInteractionWithTransaction:(YapDatabaseReadTransaction *)transaction
|
|
{
|
|
YapDatabaseViewTransaction *interactions = [transaction ext:TSMessageDatabaseViewExtensionName];
|
|
return [interactions lastObjectInGroup:self.uniqueId];
|
|
}
|
|
|
|
/**
|
|
* Useful for tests and debugging. In production use an enumeration method.
|
|
*/
|
|
- (NSArray<TSInteraction *> *)allInteractions
|
|
{
|
|
NSMutableArray<TSInteraction *> *interactions = [NSMutableArray new];
|
|
[self enumerateInteractionsUsingBlock:^(TSInteraction *interaction) {
|
|
[interactions addObject:interaction];
|
|
}];
|
|
|
|
return [interactions copy];
|
|
}
|
|
|
|
- (NSUInteger)numberOfInteractions
|
|
{
|
|
__block NSUInteger count;
|
|
[[self dbReadConnection] readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
|
YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName];
|
|
count = [interactionsByThread numberOfItemsInGroup:self.uniqueId];
|
|
}];
|
|
return count;
|
|
}
|
|
|
|
- (NSArray<id<OWSReadTracking>> *)unseenMessagesWithTransaction:(YapDatabaseReadTransaction *)transaction
|
|
{
|
|
NSMutableArray<id<OWSReadTracking>> *messages = [NSMutableArray new];
|
|
[[TSDatabaseView unseenDatabaseViewExtension:transaction]
|
|
enumerateKeysAndObjectsInGroup:self.uniqueId
|
|
usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
|
|
if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {
|
|
return;
|
|
}
|
|
id<OWSReadTracking> unread = (id<OWSReadTracking>)object;
|
|
if (unread.read) {
|
|
NSLog(@"Found an already read message in the * unseen * messages list.");
|
|
return;
|
|
}
|
|
[messages addObject:unread];
|
|
}];
|
|
|
|
return [messages copy];
|
|
}
|
|
|
|
- (NSUInteger)unreadMessageCountWithTransaction:(YapDatabaseReadTransaction *)transaction
|
|
{
|
|
__block NSUInteger count = 0;
|
|
|
|
YapDatabaseViewTransaction *unreadMessages = [transaction ext:TSUnreadDatabaseViewExtensionName];
|
|
[unreadMessages enumerateKeysAndObjectsInGroup:self.uniqueId
|
|
usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
|
|
if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {
|
|
return;
|
|
}
|
|
id<OWSReadTracking> unread = (id<OWSReadTracking>)object;
|
|
if (unread.read) {
|
|
NSLog(@"Found an already read message in the * unread * messages list.");
|
|
return;
|
|
}
|
|
count += 1;
|
|
}];
|
|
|
|
return count;
|
|
}
|
|
|
|
- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
|
|
{
|
|
for (id<OWSReadTracking> message in [self unseenMessagesWithTransaction:transaction]) {
|
|
[message markAsReadAtTimestamp:[NSDate ows_millisecondTimeStamp] sendReadReceipt:YES transaction:transaction];
|
|
}
|
|
}
|
|
|
|
- (nullable TSInteraction *)lastInteractionForInboxWithTransaction:(YapDatabaseReadTransaction *)transaction
|
|
{
|
|
__block NSUInteger missedCount = 0;
|
|
__block TSInteraction *last = nil;
|
|
[[transaction ext:TSMessageDatabaseViewExtensionName]
|
|
enumerateKeysAndObjectsInGroup:self.uniqueId
|
|
withOptions:NSEnumerationReverse
|
|
usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
|
|
missedCount++;
|
|
TSInteraction *interaction = (TSInteraction *)object;
|
|
|
|
if ([TSThread shouldInteractionAppearInInbox:interaction]) {
|
|
last = interaction;
|
|
|
|
// For long ignored threads, with lots of SN changes this can get really slow.
|
|
// I see this in development because I have a lot of long forgotten threads with
|
|
// members who's test devices are constantly reinstalled. We could add a
|
|
// purpose-built DB view, but I think in the real world this is rare to be a
|
|
// hotspot.
|
|
|
|
*stop = YES;
|
|
}
|
|
}];
|
|
return last;
|
|
}
|
|
|
|
- (NSString *)lastMessageTextWithTransaction:(YapDatabaseReadTransaction *)transaction
|
|
{
|
|
TSInteraction *interaction = [self lastInteractionForInboxWithTransaction:transaction];
|
|
if ([interaction conformsToProtocol:@protocol(OWSPreviewText)]) {
|
|
id<OWSPreviewText> previewable = (id<OWSPreviewText>)interaction;
|
|
return [previewable previewTextWithTransaction:transaction].filterStringForDisplay;
|
|
} else {
|
|
return @"";
|
|
}
|
|
}
|
|
|
|
// Returns YES IFF the interaction should show up in the inbox as the last message.
|
|
+ (BOOL)shouldInteractionAppearInInbox:(TSInteraction *)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;
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)updateWithLastMessage:(TSInteraction *)lastMessage transaction:(YapDatabaseReadWriteTransaction *)transaction {
|
|
if (![self.class shouldInteractionAppearInInbox:lastMessage]) {
|
|
return;
|
|
}
|
|
|
|
if (!self.shouldThreadBeVisible) {
|
|
self.shouldThreadBeVisible = YES;
|
|
[self saveWithTransaction:transaction];
|
|
} else {
|
|
[self touchWithTransaction:transaction];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Disappearing Messages
|
|
|
|
- (OWSDisappearingMessagesConfiguration *)disappearingMessagesConfigurationWithTransaction:
|
|
(YapDatabaseReadTransaction *)transaction
|
|
{
|
|
return [OWSDisappearingMessagesConfiguration fetchOrBuildDefaultWithThreadId:self.uniqueId transaction:transaction];
|
|
}
|
|
|
|
- (uint32_t)disappearingMessagesDurationWithTransaction:(YapDatabaseReadTransaction *)transaction
|
|
{
|
|
|
|
OWSDisappearingMessagesConfiguration *config = [self disappearingMessagesConfigurationWithTransaction:transaction];
|
|
|
|
if (!config.isEnabled) {
|
|
return 0;
|
|
} else {
|
|
return config.durationSeconds;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Archival
|
|
|
|
- (BOOL)isArchivedWithTransaction:(YapDatabaseReadTransaction *)transaction;
|
|
{
|
|
if (!self.archivedAsOfMessageSortId) {
|
|
return NO;
|
|
}
|
|
|
|
TSInteraction *_Nullable latestInteraction = [self lastInteractionForInboxWithTransaction:transaction];
|
|
uint64_t latestSortIdForInbox = latestInteraction ? latestInteraction.sortId : 0;
|
|
return self.archivedAsOfMessageSortId.unsignedLongLongValue >= latestSortIdForInbox;
|
|
}
|
|
|
|
+ (BOOL)legacyIsArchivedWithLastMessageDate:(nullable NSDate *)lastMessageDate
|
|
archivalDate:(nullable NSDate *)archivalDate
|
|
{
|
|
if (!archivalDate) {
|
|
return NO;
|
|
}
|
|
|
|
if (!lastMessageDate) {
|
|
return YES;
|
|
}
|
|
|
|
return [archivalDate compare:lastMessageDate] != NSOrderedAscending;
|
|
}
|
|
|
|
- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
|
|
{
|
|
[self applyChangeToSelfAndLatestCopy:transaction
|
|
changeBlock:^(TSThread *thread) {
|
|
uint64_t latestId = [SSKIncrementingIdFinder previousIdWithKey:TSInteraction.collection
|
|
transaction:transaction];
|
|
thread.archivedAsOfMessageSortId = @(latestId);
|
|
}];
|
|
|
|
[self markAllAsReadWithTransaction:transaction];
|
|
}
|
|
|
|
- (void)unarchiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
|
|
{
|
|
[self applyChangeToSelfAndLatestCopy:transaction
|
|
changeBlock:^(TSThread *thread) {
|
|
thread.archivedAsOfMessageSortId = nil;
|
|
}];
|
|
}
|
|
|
|
#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);
|
|
}
|
|
|
|
- (void)updateWithMutedUntilDate:(NSDate *)mutedUntilDate transaction:(YapDatabaseReadWriteTransaction *)transaction
|
|
{
|
|
[self applyChangeToSelfAndLatestCopy:transaction
|
|
changeBlock:^(TSThread *thread) {
|
|
[thread setMutedUntilDate:mutedUntilDate];
|
|
}];
|
|
|
|
[transaction addCompletionQueue:dispatch_get_main_queue() completionBlock:^{
|
|
[NSNotificationCenter.defaultCenter postNotificationName:NSNotification.muteSettingUpdated object:self.uniqueId];
|
|
}];
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|