From 7b70fe674adfc87791404a7293bace2e497a7986 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 19 May 2017 13:23:46 -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 --- Podfile.lock | 2 +- Signal.xcodeproj/project.pbxproj | 22 +-- .../ViewControllers/MessagesViewController.m | 101 ++++++++++++-- Signal/src/util/ThreadUtil.h | 8 +- Signal/src/util/ThreadUtil.m | 129 +++++++++++------- .../translations/en.lproj/Localizable.strings | 3 + 6 files changed, 193 insertions(+), 72 deletions(-) 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..fdde6c44f 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,8 @@ typedef enum : NSUInteger { @property (nonatomic) NSDate *lastMessageSentDate; @property (nonatomic) NSTimer *scrollLaterTimer; +@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper; + @end @implementation MessagesViewController @@ -694,6 +701,7 @@ typedef enum : NSUInteger { _messagesManager = [TSMessagesManager sharedManager]; _networkManager = [TSNetworkManager sharedManager]; _blockingManager = [OWSBlockingManager sharedManager]; + _contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self]; [self addNotificationListeners]; } @@ -813,14 +821,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 +940,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 +952,8 @@ typedef enum : NSUInteger { [self toggleObservers:YES]; + [self ensureThreadOffersAndIndicators]; + // Triggering modified notification renders "call notification" when leaving full screen call view [self.thread touch]; @@ -2311,6 +2316,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 +2557,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 +2683,70 @@ 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]); + + if ([self.thread isKindOfClass:[TSContactThread class]]) { + TSContactThread *contactThread = (TSContactThread *)self.thread; + [ThreadUtil ensureThreadOffersAndIndicators:contactThread + storageManager:self.storageManager + contactsManager:self.contactsManager + blockingManager:self.blockingManager]; + } +} #pragma mark - Attachment Picking: Documents diff --git a/Signal/src/util/ThreadUtil.h b/Signal/src/util/ThreadUtil.h index cb1fcceaa..77cd3e64a 100644 --- a/Signal/src/util/ThreadUtil.h +++ b/Signal/src/util/ThreadUtil.h @@ -22,10 +22,10 @@ NS_ASSUME_NONNULL_BEGIN inThread:(TSThread *)thread messageSender:(OWSMessageSender *)messageSender; -+ (void)createBlockOfferIfNecessary:(TSContactThread *)contactThread - storageManager:(TSStorageManager *)storageManager - contactsManager:(OWSContactsManager *)contactsManager - blockingManager:(OWSBlockingManager *)blockingManager; ++ (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; diff --git a/Signal/src/util/ThreadUtil.m b/Signal/src/util/ThreadUtil.m index 09d246b8e..b29955617 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 @@ -74,44 +75,37 @@ NS_ASSUME_NONNULL_BEGIN }]; } -+ (void)createBlockOfferIfNecessary:(TSContactThread *)contactThread - storageManager:(TSStorageManager *)storageManager - contactsManager:(OWSContactsManager *)contactsManager - blockingManager:(OWSBlockingManager *)blockingManager ++ (void)ensureThreadOffersAndIndicators:(TSContactThread *)contactThread + storageManager:(TSStorageManager *)storageManager + contactsManager:(OWSContactsManager *)contactsManager + blockingManager:(OWSBlockingManager *)blockingManager { OWSAssert(contactThread); 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; - } - [storageManager.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - const int kMaxOutgoingMessageCount = 10; + const int kMaxBlockOfferOutgoingMessageCount = 10; + __block OWSAddToContactsOfferMessage *addToContactsOffer = nil; + __block OWSUnknownContactBlockOfferMessage *blockOffer = nil; __block TSIncomingMessage *firstIncomingMessage = nil; __block TSOutgoingMessage *firstOutgoingMessage = nil; __block long outgoingMessageCount = 0; - __block BOOL hasUnknownContactBlockOffer = NO; [[transaction ext:TSMessageDatabaseViewExtensionName] enumerateRowsInGroup:contactThread.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(!blockOffer); + blockOffer = (OWSUnknownContactBlockOfferMessage *)object; + } else if ([object isKindOfClass:[OWSAddToContactsOfferMessage class]]) { + OWSAssert(!addToContactsOffer); + addToContactsOffer = (OWSAddToContactsOfferMessage *)object; } else if ([object isKindOfClass:[TSIncomingMessage class]]) { TSIncomingMessage *incomingMessage = (TSIncomingMessage *)object; if (!firstIncomingMessage) { @@ -131,26 +125,42 @@ 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; } - if (outgoingMessageCount > kMaxOutgoingMessageCount) { - // If the user has sent more than N messages, abort. - return; + 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; } - if (hasUnknownContactBlockOffer) { - // If there already is a block offer, abort. - return; + SignalAccount *signalAccount = contactsManager.signalAccountMap[contactThread.contactIdentifier]; + if (signalAccount) { + // Only create offers for non-contacts. + shouldHaveAddToContactsOffer = NO; + // Only create block offers for non-contacts. + shouldHaveBlockOffer = NO; + } + + if (!firstMessage) { + shouldHaveAddToContactsOffer = NO; + shouldHaveBlockOffer = NO; + } + + if (outgoingMessageCount > kMaxBlockOfferOutgoingMessageCount) { + // If the user has sent more than N messages, don't show a block offer. + shouldHaveBlockOffer = NO; } BOOL hasOutgoingBeforeIncomingInteraction = (firstOutgoingMessage @@ -159,23 +169,50 @@ 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; + // TODO: + // const int kUnseenIndicatorOfferOffset = -1; + + if (blockOffer && !shouldHaveBlockOffer) { + [blockOffer removeWithTransaction:transaction]; + } else if (!blockOffer && 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); + + TSMessage *offerMessage = + [OWSUnknownContactBlockOfferMessage unknownContactBlockOfferMessage:blockOfferTimestamp + thread:contactThread + contactId:contactThread.contactIdentifier]; + [offerMessage saveWithTransaction:transaction]; + } + + if (addToContactsOffer && !shouldHaveAddToContactsOffer) { + [addToContactsOffer removeWithTransaction:transaction]; + } else if (!addToContactsOffer && shouldHaveAddToContactsOffer) { - // 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; + DDLogInfo(@"Creating 'add to contacts' offer for unknown contact"); - TSErrorMessage *errorMessage = - [OWSUnknownContactBlockOfferMessage unknownContactBlockOfferMessage:blockOfferTimestamp - thread:contactThread - contactId:contactThread.contactIdentifier]; - [errorMessage saveWithTransaction:transaction]; + // 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); + + TSMessage *offerMessage = + [OWSAddToContactsOfferMessage addToContactsOfferMessage:offerTimestamp + thread:contactThread + contactId:contactThread.contactIdentifier]; + [offerMessage saveWithTransaction:transaction]; + } }]; } 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";