From bc63389d238b334cb740ac7ee9e38d6596565bef Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 19 May 2017 15:19:51 -0400 Subject: [PATCH] =?UTF-8?q?=E2=80=9CAdd=20to=20contacts=E2=80=9D=20offer.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit // FREEBIE --- .../ViewControllers/MessagesViewController.m | 39 +++- Signal/src/util/ThreadUtil.h | 29 ++- Signal/src/util/ThreadUtil.m | 190 +++++++++--------- 3 files changed, 143 insertions(+), 115 deletions(-) diff --git a/Signal/src/ViewControllers/MessagesViewController.m b/Signal/src/ViewControllers/MessagesViewController.m index fdde6c44f..a4ed0415f 100644 --- a/Signal/src/ViewControllers/MessagesViewController.m +++ b/Signal/src/ViewControllers/MessagesViewController.m @@ -646,6 +646,9 @@ typedef enum : NSUInteger { @property (nonatomic) NSTimer *scrollLaterTimer; @property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper; +@property (nonatomic, nullable) ThreadOffersAndIndicators *offersAndIndicators; +@property (nonatomic) BOOL hasClearedUnreadMessagesIndicator; + @end @@ -744,8 +747,6 @@ typedef enum : NSUInteger { _composeOnOpen = keyboardOnViewAppearing; _callOnOpen = callOnViewAppearing; - [ThreadUtil createUnreadMessagesIndicatorIfNecessary:thread storageManager:self.storageManager]; - [self markAllMessagesAsRead]; [self.uiDatabaseConnection beginLongLivedReadTransaction]; @@ -1000,11 +1001,6 @@ typedef enum : NSUInteger { repeats:NO]; } -- (void)clearUnreadMessagesIndicator -{ - [ThreadUtil clearUnreadMessagesIndicator:self.thread storageManager:self.storageManager]; -} - - (NSIndexPath *_Nullable)indexPathOfUnreadMessagesIndicator { int numberOfMessages = (int)[self.messageMappings numberOfItemsInGroup:self.thread.uniqueId]; @@ -1625,6 +1621,7 @@ typedef enum : NSUInteger { } else { [ThreadUtil sendMessageWithText:text inThread:self.thread messageSender:self.messageSender]; } + self.lastMessageSentDate = [NSDate new]; [self clearUnreadMessagesIndicator]; @@ -2739,13 +2736,33 @@ typedef enum : NSUInteger { { OWSAssert([NSThread isMainThread]); - if ([self.thread isKindOfClass:[TSContactThread class]]) { - TSContactThread *contactThread = (TSContactThread *)self.thread; - [ThreadUtil ensureThreadOffersAndIndicators:contactThread + self.offersAndIndicators = + [ThreadUtil ensureThreadOffersAndIndicators:self.thread storageManager:self.storageManager contactsManager:self.contactsManager - blockingManager:self.blockingManager]; + blockingManager:self.blockingManager + hideUnreadMessagesIndicator:self.hasClearedUnreadMessagesIndicator + fixedUnreadIndicatorTimestamp:(self.offersAndIndicators.unreadIndicator + ? @(self.offersAndIndicators.unreadIndicator.timestamp) + : nil)]; +} + +- (void)clearUnreadMessagesIndicator +{ + OWSAssert([NSThread isMainThread]); + + if (self.hasClearedUnreadMessagesIndicator) { + // ensureThreadOffersAndIndicators is slightly + // expensive, so make sure we don't call it + // unneccesarily. + return; } + + // Once we've cleared the unread messages indicator, + // make sure we don't show it again. + self.hasClearedUnreadMessagesIndicator = YES; + + [self ensureThreadOffersAndIndicators]; } #pragma mark - Attachment Picking: Documents diff --git a/Signal/src/util/ThreadUtil.h b/Signal/src/util/ThreadUtil.h index 77cd3e64a..f64de412e 100644 --- a/Signal/src/util/ThreadUtil.h +++ b/Signal/src/util/ThreadUtil.h @@ -12,6 +12,16 @@ NS_ASSUME_NONNULL_BEGIN +@class TSUnreadIndicatorInteraction; + +@interface ThreadOffersAndIndicators : NSObject + +@property (nonatomic, nullable) TSUnreadIndicatorInteraction *unreadIndicator; + +@end + +#pragma mark - + @interface ThreadUtil : NSObject + (void)sendMessageWithText:(NSString *)text @@ -22,13 +32,18 @@ NS_ASSUME_NONNULL_BEGIN inThread:(TSThread *)thread messageSender:(OWSMessageSender *)messageSender; -+ (void)ensureThreadOffersAndIndicators:(TSContactThread *)contactThread - storageManager:(TSStorageManager *)storageManager - contactsManager:(OWSContactsManager *)contactsManager - blockingManager:(OWSBlockingManager *)blockingManager; - -+ (void)createUnreadMessagesIndicatorIfNecessary:(TSThread *)thread storageManager:(TSStorageManager *)storageManager; -+ (void)clearUnreadMessagesIndicator:(TSThread *)thread storageManager:(TSStorageManager *)storageManager; +// This method will create and/or remove any offers and indicators +// necessary for this thread. +// +// * If hideUnreadMessagesIndicator is YES, there will be no "unread indicator". +// * Otherwise, if fixedUnreadIndicatorTimestamp is non-null, there will be a "unread indicator". +// * Otherwise, there will be a "unread indicator" if there is one unread message. ++ (ThreadOffersAndIndicators *)ensureThreadOffersAndIndicators:(TSThread *)thread + storageManager:(TSStorageManager *)storageManager + contactsManager:(OWSContactsManager *)contactsManager + blockingManager:(OWSBlockingManager *)blockingManager + hideUnreadMessagesIndicator:(BOOL)hideUnreadMessagesIndicator + fixedUnreadIndicatorTimestamp:(NSNumber *_Nullable)fixedUnreadIndicatorTimestamp; @end diff --git a/Signal/src/util/ThreadUtil.m b/Signal/src/util/ThreadUtil.m index b29955617..bedb84a4b 100644 --- a/Signal/src/util/ThreadUtil.m +++ b/Signal/src/util/ThreadUtil.m @@ -18,6 +18,12 @@ NS_ASSUME_NONNULL_BEGIN +@implementation ThreadOffersAndIndicators + +@end + +#pragma mark - + @implementation ThreadUtil + (void)sendMessageWithText:(NSString *)text inThread:(TSThread *)thread messageSender:(OWSMessageSender *)messageSender @@ -75,37 +81,45 @@ NS_ASSUME_NONNULL_BEGIN }]; } -+ (void)ensureThreadOffersAndIndicators:(TSContactThread *)contactThread - storageManager:(TSStorageManager *)storageManager - contactsManager:(OWSContactsManager *)contactsManager - blockingManager:(OWSBlockingManager *)blockingManager ++ (ThreadOffersAndIndicators *)ensureThreadOffersAndIndicators:(TSThread *)thread + storageManager:(TSStorageManager *)storageManager + contactsManager:(OWSContactsManager *)contactsManager + blockingManager:(OWSBlockingManager *)blockingManager + hideUnreadMessagesIndicator:(BOOL)hideUnreadMessagesIndicator + fixedUnreadIndicatorTimestamp:(NSNumber *_Nullable)fixedUnreadIndicatorTimestamp { - OWSAssert(contactThread); + OWSAssert(thread); OWSAssert(storageManager); OWSAssert(contactsManager); OWSAssert(blockingManager); + ThreadOffersAndIndicators *result = [ThreadOffersAndIndicators new]; + [storageManager.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { const int kMaxBlockOfferOutgoingMessageCount = 10; __block OWSAddToContactsOfferMessage *addToContactsOffer = nil; __block OWSUnknownContactBlockOfferMessage *blockOffer = nil; + __block TSUnreadIndicatorInteraction *unreadIndicator = nil; __block TSIncomingMessage *firstIncomingMessage = nil; __block TSOutgoingMessage *firstOutgoingMessage = nil; + __block TSMessage *firstUnreadMessage = nil; __block long outgoingMessageCount = 0; [[transaction ext:TSMessageDatabaseViewExtensionName] - enumerateRowsInGroup:contactThread.uniqueId + enumerateRowsInGroup:thread.uniqueId usingBlock:^( NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { - if ([object isKindOfClass:[OWSUnknownContactBlockOfferMessage class]]) { OWSAssert(!blockOffer); blockOffer = (OWSUnknownContactBlockOfferMessage *)object; } else if ([object isKindOfClass:[OWSAddToContactsOfferMessage class]]) { OWSAssert(!addToContactsOffer); addToContactsOffer = (OWSAddToContactsOfferMessage *)object; + } else if ([object isKindOfClass:[TSUnreadIndicatorInteraction class]]) { + OWSAssert(!unreadIndicator); + unreadIndicator = (TSUnreadIndicatorInteraction *)object; } else if ([object isKindOfClass:[TSIncomingMessage class]]) { TSIncomingMessage *incomingMessage = (TSIncomingMessage *)object; if (!firstIncomingMessage) { @@ -115,6 +129,16 @@ NS_ASSUME_NONNULL_BEGIN compare:[incomingMessage receiptDateForSorting]] == NSOrderedAscending); } + + if (!incomingMessage.wasRead) { + if (!firstUnreadMessage) { + firstUnreadMessage = incomingMessage; + } else { + OWSAssert([[firstUnreadMessage receiptDateForSorting] + compare:[incomingMessage receiptDateForSorting]] + == NSOrderedAscending); + } + } } else if ([object isKindOfClass:[TSOutgoingMessage class]]) { TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)object; if (!firstOutgoingMessage) { @@ -138,19 +162,30 @@ NS_ASSUME_NONNULL_BEGIN BOOL shouldHaveBlockOffer = YES; BOOL shouldHaveAddToContactsOffer = YES; - if ([[blockingManager blockedPhoneNumbers] containsObject:contactThread.contactIdentifier]) { - // Only create offers for users which are not already blocked. - shouldHaveAddToContactsOffer = NO; - // Only create block offers for users which are not already blocked. - shouldHaveBlockOffer = NO; - } - SignalAccount *signalAccount = contactsManager.signalAccountMap[contactThread.contactIdentifier]; - if (signalAccount) { - // Only create offers for non-contacts. + BOOL isContactThread = [thread isKindOfClass:[TSContactThread class]]; + if (!isContactThread) { + // Only create "add to contacts" offers in 1:1 conversations. shouldHaveAddToContactsOffer = NO; - // Only create block offers for non-contacts. + // Only create block offers in 1:1 conversations. shouldHaveBlockOffer = NO; + } else { + NSString *recipientId = ((TSContactThread *)thread).contactIdentifier; + + if ([[blockingManager blockedPhoneNumbers] containsObject:recipientId]) { + // Only create "add to contacts" offers for users which are not already blocked. + shouldHaveAddToContactsOffer = NO; + // Only create block offers for users which are not already blocked. + shouldHaveBlockOffer = NO; + } + + SignalAccount *signalAccount = contactsManager.signalAccountMap[recipientId]; + if (signalAccount) { + // Only create "add to contacts" offers for non-contacts. + shouldHaveAddToContactsOffer = NO; + // Only create block offers for non-contacts. + shouldHaveBlockOffer = NO; + } } if (!firstMessage) { @@ -176,8 +211,7 @@ NS_ASSUME_NONNULL_BEGIN // We use these offset to control the ordering of the offers and indicators. const int kBlockOfferOffset = -3; const int kAddToContactsOfferOffset = -2; - // TODO: - // const int kUnseenIndicatorOfferOffset = -1; + const int kUnreadIndicatorOfferOffset = -1; if (blockOffer && !shouldHaveBlockOffer) { [blockOffer removeWithTransaction:transaction]; @@ -188,11 +222,12 @@ NS_ASSUME_NONNULL_BEGIN // conversation's timeline, so we back-date it to slightly before // the first incoming message (which we know is the first message). uint64_t blockOfferTimestamp = (uint64_t)((long long)firstMessage.timestamp + kBlockOfferOffset); + NSString *recipientId = ((TSContactThread *)thread).contactIdentifier; TSMessage *offerMessage = [OWSUnknownContactBlockOfferMessage unknownContactBlockOfferMessage:blockOfferTimestamp - thread:contactThread - contactId:contactThread.contactIdentifier]; + thread:thread + contactId:recipientId]; [offerMessage saveWithTransaction:transaction]; } @@ -206,89 +241,50 @@ NS_ASSUME_NONNULL_BEGIN // conversation's timeline, so we back-date it to slightly before // the first incoming message (which we know is the first message). uint64_t offerTimestamp = (uint64_t)((long long)firstMessage.timestamp + kAddToContactsOfferOffset); + NSString *recipientId = ((TSContactThread *)thread).contactIdentifier; - TSMessage *offerMessage = - [OWSAddToContactsOfferMessage addToContactsOfferMessage:offerTimestamp - thread:contactThread - contactId:contactThread.contactIdentifier]; + TSMessage *offerMessage = [OWSAddToContactsOfferMessage addToContactsOfferMessage:offerTimestamp + thread:thread + contactId:recipientId]; [offerMessage saveWithTransaction:transaction]; } - }]; -} - -+ (void)createUnreadMessagesIndicatorIfNecessary:(TSThread *)thread storageManager:(TSStorageManager *)storageManager -{ - OWSAssert(thread); - OWSAssert(storageManager); - - [storageManager.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - - NSMutableArray *indicators = [NSMutableArray new]; - __block TSMessage *firstUnreadMessage = nil; - [[transaction ext:TSMessageDatabaseViewExtensionName] - enumerateRowsInGroup:thread.uniqueId - usingBlock:^( - NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { - if ([object isKindOfClass:[TSUnreadIndicatorInteraction class]]) { - [indicators addObject:object]; - } else if ([object isKindOfClass:[TSIncomingMessage class]]) { - TSIncomingMessage *incomingMessage = (TSIncomingMessage *)object; - if (!incomingMessage.wasRead) { - if (!firstUnreadMessage) { - firstUnreadMessage = incomingMessage; - } else { - OWSAssert([[firstUnreadMessage receiptDateForSorting] - compare:[incomingMessage receiptDateForSorting]] - == NSOrderedAscending); - } - } - } - }]; - - for (TSUnreadIndicatorInteraction *indicator in indicators) { - [indicator removeWithTransaction:transaction]; + BOOL shouldHaveUnreadIndicator + = ((firstUnreadMessage != nil || fixedUnreadIndicatorTimestamp != nil) && !hideUnreadMessagesIndicator); + if (!shouldHaveUnreadIndicator) { + if (unreadIndicator) { + [unreadIndicator removeWithTransaction:transaction]; + } + } else { + // We want the block offer to appear just before the first unread incoming + // message in the conversation timeline... + // + // ...unless we have a fixed timestamp for the unread indicator. + uint64_t indicatorTimestamp = (uint64_t)(fixedUnreadIndicatorTimestamp + ? [fixedUnreadIndicatorTimestamp longLongValue] + : ((long long)firstUnreadMessage.timestamp + kUnreadIndicatorOfferOffset)); + + if (indicatorTimestamp && unreadIndicator.timestamp == indicatorTimestamp) { + // Keep the existing indicator; it is in the correct position. + + result.unreadIndicator = unreadIndicator; + } else { + if (unreadIndicator) { + [unreadIndicator removeWithTransaction:transaction]; + } + + DDLogInfo(@"%@ Creating TSUnreadIndicatorInteraction", self.tag); + + TSUnreadIndicatorInteraction *indicator = + [[TSUnreadIndicatorInteraction alloc] initWithTimestamp:indicatorTimestamp thread:thread]; + [indicator saveWithTransaction:transaction]; + + result.unreadIndicator = indicator; + } } - - BOOL shouldHaveIndicator = firstUnreadMessage != nil; - if (!shouldHaveIndicator) { - return; - } - - DDLogInfo(@"%@ Creating TSUnreadIndicatorInteraction", self.tag); - - // We want the block offer to appear just before the first unread incoming - // message in the conversation timeline. - uint64_t indicatorTimestamp = firstUnreadMessage.timestamp - 1; - - TSUnreadIndicatorInteraction *indicator = - [[TSUnreadIndicatorInteraction alloc] initWithTimestamp:indicatorTimestamp thread:thread]; - [indicator saveWithTransaction:transaction]; }]; -} - -+ (void)clearUnreadMessagesIndicator:(TSThread *)thread storageManager:(TSStorageManager *)storageManager -{ - OWSAssert(thread); - OWSAssert(storageManager); - - [storageManager.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - - NSMutableArray *indicators = [NSMutableArray new]; - [[transaction ext:TSMessageDatabaseViewExtensionName] - enumerateRowsInGroup:thread.uniqueId - usingBlock:^( - NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { - if ([object isKindOfClass:[TSUnreadIndicatorInteraction class]]) { - [indicators addObject:object]; - } - }]; - - for (TSUnreadIndicatorInteraction *indicator in indicators) { - [indicator removeWithTransaction:transaction]; - } - }]; + return result; } #pragma mark - Logging