diff --git a/Podfile.lock b/Podfile.lock index ab74e12f7..5803ee8c7 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -134,7 +134,7 @@ CHECKOUT OPTIONS: :commit: 7054e4b13ee5bcd6d524adb6dc9a726e8c466308 :git: https://github.com/WhisperSystems/JSQMessagesViewController.git SignalServiceKit: - :commit: 485af7e8170dc1bb55245d94d991c10eda4979e0 + :commit: cbeafac20ebb0437baf8982381c1980db276681f :git: https://github.com/WhisperSystems/SignalServiceKit.git SocketRocket: :commit: 877ac7438be3ad0b45ef5ca3969574e4b97112bf diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index a915fd577..8fa0d640b 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -1066,23 +1066,23 @@ 457F3AC01D14A0F700C51351 /* Models */ = { isa = PBXGroup; children = ( - B62D53F41A23CC8B009AAF82 /* TSMessageAdapters */, - 453D28B51D32BA5F00D523F0 /* OWSDisplayedMessage.h */, - 453D28B61D32BA5F00D523F0 /* OWSDisplayedMessage.m */, + 45CD81EE1DC030E7004C9430 /* AccountManager.swift */, + 45DF5DF11DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift */, + 45666EC41D99483D008FE134 /* OWSAvatarBuilder.h */, + 45666EC51D99483D008FE134 /* OWSAvatarBuilder.m */, 45C681B51D305A580050903A /* OWSCall.h */, 45C681B61D305A580050903A /* OWSCall.m */, - 453D28B81D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.h */, - 453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */, - 458E38351D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.h */, - 458E38361D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m */, 45855F351D9498A40084F340 /* OWSContactAvatarBuilder.h */, 45855F361D9498A40084F340 /* OWSContactAvatarBuilder.m */, - 45666EC41D99483D008FE134 /* OWSAvatarBuilder.h */, - 45666EC51D99483D008FE134 /* OWSAvatarBuilder.m */, + 458E38351D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.h */, + 458E38361D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m */, + 453D28B51D32BA5F00D523F0 /* OWSDisplayedMessage.h */, + 453D28B61D32BA5F00D523F0 /* OWSDisplayedMessage.m */, 45666EC71D994C0D008FE134 /* OWSGroupAvatarBuilder.h */, 45666EC81D994C0D008FE134 /* OWSGroupAvatarBuilder.m */, - 45CD81EE1DC030E7004C9430 /* AccountManager.swift */, - 45DF5DF11DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift */, + 453D28B81D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.h */, + 453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */, + B62D53F41A23CC8B009AAF82 /* TSMessageAdapters */, ); path = Models; sourceTree = ""; diff --git a/Signal/src/ViewControllers/MessagesViewController.m b/Signal/src/ViewControllers/MessagesViewController.m index f3a310477..ab8802159 100644 --- a/Signal/src/ViewControllers/MessagesViewController.m +++ b/Signal/src/ViewControllers/MessagesViewController.m @@ -7,6 +7,7 @@ #import "AttachmentSharing.h" #import "BlockListUIUtils.h" #import "BlockListViewController.h" +#import "ContactsViewHelper.h" #import "DebugUITableViewController.h" #import "Environment.h" #import "FingerprintViewController.h" @@ -26,7 +27,6 @@ #import "OWSMessageCollectionViewCell.h" #import "OWSMessagesBubblesSizeCalculator.h" #import "OWSOutgoingMessageCollectionViewCell.h" -#import "OWSUnknownContactBlockOfferMessage.h" #import "OWSUnreadIndicatorCell.h" #import "PropertyListPreferences.h" #import "Signal-Swift.h" @@ -63,12 +63,14 @@ #import #import #import +#import #import #import #import #import #import #import +#import #import #import #import @@ -586,7 +588,10 @@ typedef enum : NSUInteger { OWSMessagesToolbarContentDelegate, OWSConversationSettingsViewDelegate, UIDocumentMenuDelegate, - UIDocumentPickerDelegate> { + UIDocumentPickerDelegate, + ContactsViewHelperDelegate, + ContactEditingDelegate, + CNContactViewControllerDelegate> { UIImage *tappedImage; BOOL isGroupConversation; } @@ -640,6 +645,11 @@ typedef enum : NSUInteger { @property (nonatomic) NSDate *lastMessageSentDate; @property (nonatomic) NSTimer *scrollLaterTimer; +@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper; +@property (nonatomic, nullable) ThreadOffersAndIndicators *offersAndIndicators; +@property (nonatomic) BOOL hasClearedUnreadMessagesIndicator; + + @end @implementation MessagesViewController @@ -694,6 +704,7 @@ typedef enum : NSUInteger { _messagesManager = [TSMessagesManager sharedManager]; _networkManager = [TSNetworkManager sharedManager]; _blockingManager = [OWSBlockingManager sharedManager]; + _contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self]; [self addNotificationListeners]; } @@ -736,7 +747,9 @@ typedef enum : NSUInteger { _composeOnOpen = keyboardOnViewAppearing; _callOnOpen = callOnViewAppearing; - [ThreadUtil createUnreadMessagesIndicatorIfNecessary:thread storageManager:self.storageManager]; + // We need to create the "unread indicator" before we mark + // all messages as read. + [self ensureThreadOffersAndIndicators]; [self markAllMessagesAsRead]; @@ -813,14 +826,6 @@ typedef enum : NSUInteger { self.automaticallyScrollsToMostRecentMessage = NO; [self initializeToolbars]; - - if ([self.thread isKindOfClass:[TSContactThread class]]) { - TSContactThread *contactThread = (TSContactThread *)self.thread; - [ThreadUtil createBlockOfferIfNecessary:contactThread - storageManager:self.storageManager - contactsManager:self.contactsManager - blockingManager:self.blockingManager]; - } } - (void)viewDidLayoutSubviews @@ -940,6 +945,9 @@ typedef enum : NSUInteger { { [super viewWillAppear:animated]; + // In case we're dismissing a CNContactViewController which requires default system appearance + [UIUtil applySignalAppearence]; + // Since we're using a custom back button, we have to do some extra work to manage the interactivePopGestureRecognizer self.navigationController.interactivePopGestureRecognizer.delegate = self; @@ -949,6 +957,8 @@ typedef enum : NSUInteger { [self toggleObservers:YES]; + [self ensureThreadOffersAndIndicators]; + // Triggering modified notification renders "call notification" when leaving full screen call view [self.thread touch]; @@ -995,11 +1005,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]; @@ -1620,6 +1625,7 @@ typedef enum : NSUInteger { } else { [ThreadUtil sendMessageWithText:text inThread:self.thread messageSender:self.messageSender]; } + self.lastMessageSentDate = [NSDate new]; [self clearUnreadMessagesIndicator]; @@ -2311,6 +2317,9 @@ typedef enum : NSUInteger { case TSErrorMessageAdapter: [self handleErrorMessageTap:(TSErrorMessage *)interaction]; break; + case TSInfoMessageAdapter: + [self handleInfoMessageTap:(TSInfoMessage *)interaction]; + break; case TSCallAdapter: case TSUnreadIndicatorAdapter: break; @@ -2549,6 +2558,15 @@ typedef enum : NSUInteger { } } +- (void)handleInfoMessageTap:(TSInfoMessage *)message +{ + if ([message isKindOfClass:[OWSAddToContactsOfferMessage class]]) { + [self tappedAddToContactsOfferMessage:(OWSAddToContactsOfferMessage *)message]; + } else { + DDLogInfo(@"%@ Unhandled tap for info message:%@", self.tag, message); + } +} + - (void)tappedCorruptedMessage:(TSErrorMessage *)message { @@ -2666,6 +2684,89 @@ typedef enum : NSUInteger { [self presentViewController:actionSheetController animated:YES completion:nil]; } +- (void)tappedAddToContactsOfferMessage:(OWSAddToContactsOfferMessage *)errorMessage +{ + if (!self.contactsManager.supportsContactEditing) { + DDLogError(@"%@ Contact editing not supported", self.tag); + OWSAssert(NO); + return; + } + if (![self.thread isKindOfClass:[TSContactThread class]]) { + DDLogError(@"%@ unexpected thread: %@ in %s", self.tag, self.thread, __PRETTY_FUNCTION__); + OWSAssert(NO); + return; + } + + TSContactThread *contactThread = (TSContactThread *)self.thread; + [self.contactsViewHelper presentContactViewControllerForRecipientId:contactThread.contactIdentifier + fromViewController:self + editImmediately:YES]; +} + +#pragma mark - ContactEditingDelegate + +- (void)didFinishEditingContact +{ + DDLogDebug(@"%@ %s", self.tag, __PRETTY_FUNCTION__); + + [self dismissViewControllerAnimated:NO completion:nil]; +} + +#pragma mark - CNContactViewControllerDelegate + +- (void)contactViewController:(CNContactViewController *)viewController + didCompleteWithContact:(nullable CNContact *)contact +{ + if (contact) { + // Saving normally returns you to the "Show Contact" view + // which we're not interested in, so we skip it here. There is + // an unfortunate blip of the "Show Contact" view on slower devices. + DDLogDebug(@"%@ completed editing contact.", self.tag); + [self dismissViewControllerAnimated:NO completion:nil]; + } else { + DDLogDebug(@"%@ canceled editing contact.", self.tag); + [self dismissViewControllerAnimated:YES completion:nil]; + } +} + +#pragma mark - ContactsViewHelperDelegate + +- (void)contactsViewHelperDidUpdateContacts +{ + [self ensureThreadOffersAndIndicators]; +} + +- (void)ensureThreadOffersAndIndicators +{ + OWSAssert([NSThread isMainThread]); + + self.offersAndIndicators = + [ThreadUtil ensureThreadOffersAndIndicators:self.thread + storageManager:self.storageManager + contactsManager:self.contactsManager + 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 somewhat expensive + // so we don't want to call it unnecessarily. + 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 cb1fcceaa..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)createBlockOfferIfNecessary:(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 09d246b8e..b1af6cda6 100644 --- a/Signal/src/util/ThreadUtil.m +++ b/Signal/src/util/ThreadUtil.m @@ -7,6 +7,7 @@ #import "Signal-Swift.h" #import "TSUnreadIndicatorInteraction.h" #import +#import #import #import #import @@ -17,6 +18,12 @@ NS_ASSUME_NONNULL_BEGIN +@implementation ThreadOffersAndIndicators + +@end + +#pragma mark - + @implementation ThreadUtil + (void)sendMessageWithText:(NSString *)text inThread:(TSThread *)thread messageSender:(OWSMessageSender *)messageSender @@ -74,44 +81,45 @@ NS_ASSUME_NONNULL_BEGIN }]; } -+ (void)createBlockOfferIfNecessary:(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); - if ([[blockingManager blockedPhoneNumbers] containsObject:contactThread.contactIdentifier]) { - // Only create block offers for users which are not already blocked. - return; - } - - SignalAccount *signalAccount = contactsManager.signalAccountMap[contactThread.contactIdentifier]; - if (signalAccount) { - // Only create block offers for non-contacts. - return; - } + ThreadOffersAndIndicators *result = [ThreadOffersAndIndicators new]; [storageManager.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - const int kMaxOutgoingMessageCount = 10; + const int kMaxBlockOfferOutgoingMessageCount = 10; + __block OWSAddToContactsOfferMessage *existingAddToContactsOffer = nil; + __block OWSUnknownContactBlockOfferMessage *existingBlockOffer = nil; + __block TSUnreadIndicatorInteraction *existingUnreadIndicator = nil; __block TSIncomingMessage *firstIncomingMessage = nil; __block TSOutgoingMessage *firstOutgoingMessage = nil; + __block TSIncomingMessage *firstUnreadMessage = nil; __block long outgoingMessageCount = 0; - __block BOOL hasUnknownContactBlockOffer = NO; [[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]]) { - hasUnknownContactBlockOffer = YES; - // If there already is a block offer, abort. - *stop = YES; + OWSAssert(!existingBlockOffer); + existingBlockOffer = (OWSUnknownContactBlockOfferMessage *)object; + } else if ([object isKindOfClass:[OWSAddToContactsOfferMessage class]]) { + OWSAssert(!existingAddToContactsOffer); + existingAddToContactsOffer = (OWSAddToContactsOfferMessage *)object; + } else if ([object isKindOfClass:[TSUnreadIndicatorInteraction class]]) { + OWSAssert(!existingUnreadIndicator); + existingUnreadIndicator = (TSUnreadIndicatorInteraction *)object; } else if ([object isKindOfClass:[TSIncomingMessage class]]) { TSIncomingMessage *incomingMessage = (TSIncomingMessage *)object; if (!firstIncomingMessage) { @@ -121,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) { @@ -131,26 +149,53 @@ NS_ASSUME_NONNULL_BEGIN == NSOrderedAscending); } outgoingMessageCount++; - if (outgoingMessageCount > kMaxOutgoingMessageCount) { - // If the user has sent more than N interactions, abort. - *stop = YES; - } } }]; - if (!firstIncomingMessage && !firstOutgoingMessage) { - // If the thread has no interactions, abort. - return; + TSMessage *firstMessage = firstIncomingMessage; + if (!firstMessage + || (firstOutgoingMessage && + [[firstOutgoingMessage receiptDateForSorting] compare:[firstMessage receiptDateForSorting]] + == NSOrderedAscending)) { + firstMessage = firstOutgoingMessage; + } + + BOOL shouldHaveBlockOffer = YES; + BOOL shouldHaveAddToContactsOffer = YES; + + BOOL isContactThread = [thread isKindOfClass:[TSContactThread class]]; + if (!isContactThread) { + // Only create "add to contacts" offers in 1:1 conversations. + shouldHaveAddToContactsOffer = NO; + // 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 (outgoingMessageCount > kMaxOutgoingMessageCount) { - // If the user has sent more than N messages, abort. - return; + if (!firstMessage) { + shouldHaveAddToContactsOffer = NO; + shouldHaveBlockOffer = NO; } - if (hasUnknownContactBlockOffer) { - // If there already is a block offer, abort. - return; + if (outgoingMessageCount > kMaxBlockOfferOutgoingMessageCount) { + // If the user has sent more than N messages, don't show a block offer. + shouldHaveBlockOffer = NO; } BOOL hasOutgoingBeforeIncomingInteraction = (firstOutgoingMessage @@ -159,99 +204,87 @@ NS_ASSUME_NONNULL_BEGIN == NSOrderedAscending)); if (hasOutgoingBeforeIncomingInteraction) { // If there is an outgoing message before an incoming message - // the local user initiated this conversation, abort. - return; + // the local user initiated this conversation, don't show a block offer. + shouldHaveBlockOffer = NO; } - DDLogInfo(@"Creating block offer for unknown contact"); + // We use these offset to control the ordering of the offers and indicators. + const int kBlockOfferOffset = -3; + const int kAddToContactsOfferOffset = -2; + const int kUnreadIndicatorOfferOffset = -1; + + if (existingBlockOffer && !shouldHaveBlockOffer) { + [existingBlockOffer removeWithTransaction:transaction]; + } else if (!existingBlockOffer && shouldHaveBlockOffer) { + DDLogInfo(@"Creating block offer for unknown contact"); + + // We want the block offer to be the first interaction in their + // 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:thread + contactId:recipientId]; + [offerMessage saveWithTransaction:transaction]; + } - // We want the block offer to be the first interaction in their - // conversation's timeline, so we back-date it to slightly before - // the first incoming message (which we know is the first message). - TSIncomingMessage *firstMessage = firstIncomingMessage; - uint64_t blockOfferTimestamp = firstMessage.timestamp - 1; + if (existingAddToContactsOffer && !shouldHaveAddToContactsOffer) { + [existingAddToContactsOffer removeWithTransaction:transaction]; + } else if (!existingAddToContactsOffer && shouldHaveAddToContactsOffer) { - TSErrorMessage *errorMessage = - [OWSUnknownContactBlockOfferMessage unknownContactBlockOfferMessage:blockOfferTimestamp - thread:contactThread - contactId:contactThread.contactIdentifier]; - [errorMessage saveWithTransaction:transaction]; - }]; -} + DDLogInfo(@"Creating 'add to contacts' offer for unknown contact"); -+ (void)createUnreadMessagesIndicatorIfNecessary:(TSThread *)thread storageManager:(TSStorageManager *)storageManager -{ - OWSAssert(thread); - OWSAssert(storageManager); + // We want the offer to be the first interaction in their + // 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; - [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]; + TSMessage *offerMessage = [OWSAddToContactsOfferMessage addToContactsOfferMessage:offerTimestamp + thread:thread + contactId:recipientId]; + [offerMessage saveWithTransaction:transaction]; } - BOOL shouldHaveIndicator = firstUnreadMessage != nil; - if (!shouldHaveIndicator) { - return; + BOOL shouldHaveUnreadIndicator + = ((firstUnreadMessage != nil || fixedUnreadIndicatorTimestamp != nil) && !hideUnreadMessagesIndicator); + if (!shouldHaveUnreadIndicator) { + if (existingUnreadIndicator) { + [existingUnreadIndicator 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 && existingUnreadIndicator.timestamp == indicatorTimestamp) { + // Keep the existing indicator; it is in the correct position. + + result.unreadIndicator = existingUnreadIndicator; + } else { + if (existingUnreadIndicator) { + [existingUnreadIndicator removeWithTransaction:transaction]; + } + + DDLogInfo(@"%@ Creating TSUnreadIndicatorInteraction", self.tag); + + TSUnreadIndicatorInteraction *indicator = + [[TSUnreadIndicatorInteraction alloc] initWithTimestamp:indicatorTimestamp thread:thread]; + [indicator saveWithTransaction:transaction]; + + result.unreadIndicator = indicator; + } } - - 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 diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 9db7db35d..74dfd8acd 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -16,6 +16,9 @@ /* Title for the 'add group member' view. */ "ADD_GROUP_MEMBER_VIEW_TITLE" = "Add Member"; +/* No comment provided by engineer. */ +"ADD_TO_CONTACTS_OFFER" = "Would you like to add this user to your contacts?"; + /* The label for the 'discard' button in alerts and action sheets. */ "ALERT_DISCARD_BUTTON" = "Discard";