diff --git a/Signal/src/view controllers/MessageComposeTableViewController.m b/Signal/src/view controllers/MessageComposeTableViewController.m index d2ff1bf04..ca277354e 100644 --- a/Signal/src/view controllers/MessageComposeTableViewController.m +++ b/Signal/src/view controllers/MessageComposeTableViewController.m @@ -22,9 +22,9 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) IBOutlet UITableViewCell *inviteCell; @property (nonatomic) UITableViewCell *conversationForNonContactCell; +@property (nonatomic) UITableViewCell *inviteViaSMSCell; @property (nonatomic) IBOutlet OWSNoSignalContactsView *noSignalContactsView; -@property (nonatomic) UIButton *sendTextButton; @property (nonatomic) UISearchController *searchController; @property (nonatomic) UIActivityIndicatorView *activityIndicator; @property (nonatomic) UIBarButtonItem *addGroup; @@ -35,15 +35,29 @@ NS_ASSUME_NONNULL_BEGIN @property (copy) NSArray *searchResults; @property (nonatomic, readonly) OWSContactsManager *contactsManager; -@property (nonatomic) BOOL showNewConversationForNonContactButton; -// This property should be set IFF showNewConversationForNonContactButton is YES. -@property (nonatomic) NSString *nonContactPhoneNumber; +// This property should be set IFF the current search text can +// be parsed as a phone number. If set, it contains a E164 value. +@property (nonatomic) NSString *searchPhoneNumber; +// This dictionary is used to cache the set of phone numbers +// which are known to correspond to Signal accounts. +@property (nonatomic) NSMutableSet *phoneNumberAccountSet; + +@property (nonatomic) BOOL isBackgroundViewHidden; @end -NSInteger const MessageComposeTableViewControllerSectionInvite = 0; -NSInteger const MessageComposeTableViewControllerSectionContacts = 1; -NSInteger const MessageComposeTableViewControllerSectionNewConversationForNonContact = 2; +// The "special" sections are used to display (at most) one of three cells: +// +// * "New conversation for non-contact" if user has entered a phone +// number which corresponds to a signal account, or: +// * "Send invite via SMS" if user has entered a phone number +// which is not known to correspond to a signal account, or: +// * "Invite contacts" if the invite flow is available, or: +// * Nothing, otherwise. +NSInteger const MessageComposeTableViewControllerSectionInviteNonContactConversation = 0; +NSInteger const MessageComposeTableViewControllerSectionInviteViaSMS = 1; +NSInteger const MessageComposeTableViewControllerSectionInviteFlow = 2; +NSInteger const MessageComposeTableViewControllerSectionContacts = 3; NSString *const MessageComposeTableViewControllerCellInvite = @"ContactTableInviteCell"; NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableViewCell"; @@ -91,6 +105,7 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie @"INVITE_FRIENDS_CONTACT_TABLE_BUTTON", @"Text for button at the top of the contact picker"); self.conversationForNonContactCell = [UITableViewCell new]; + self.inviteViaSMSCell = [UITableViewCell new]; self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero]; [self createLoadingAndBackgroundViews]; @@ -160,9 +175,36 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie [_loadingBackgroundView addSubview:loadingProgressView]; [_loadingBackgroundView addSubview:loadingLabel]; - [self.noSignalContactsView.inviteButton addTarget:self - action:@selector(presentInviteFlow) - forControlEvents:UIControlEventTouchUpInside]; + UIButton *inviteButton = self.noSignalContactsView.inviteButton; + [inviteButton addTarget:self + action:@selector(presentInviteFlow) + forControlEvents:UIControlEventTouchUpInside]; + [inviteButton setTitleColor:[UIColor ows_materialBlueColor] + forState:UIControlStateNormal]; + [inviteButton.titleLabel setFont:[UIFont ows_regularFontWithSize:17.f]]; + + UIButton *searchByPhoneNumberButton = [UIButton buttonWithType:UIButtonTypeCustom]; + [searchByPhoneNumberButton setTitle:NSLocalizedString(@"NO_CONTACTS_SEARCH_BY_PHONE_NUMBER", + @"Label for a button that lets users search for contacts by phone number") + forState:UIControlStateNormal]; + [searchByPhoneNumberButton setTitleColor:[UIColor ows_materialBlueColor] + forState:UIControlStateNormal]; + [searchByPhoneNumberButton.titleLabel setFont:[UIFont ows_regularFontWithSize:17.f]]; + [inviteButton.superview addSubview:searchByPhoneNumberButton]; + [searchByPhoneNumberButton autoHCenterInSuperview]; + [searchByPhoneNumberButton autoPinEdge:ALEdgeTop + toEdge:ALEdgeBottom + ofView:inviteButton + withOffset:20]; + [searchByPhoneNumberButton addTarget:self + action:@selector(hideBackgroundView) + forControlEvents:UIControlEventTouchUpInside]; +} + +- (void)hideBackgroundView { + self.isBackgroundViewHidden = YES; + + [self showEmptyBackgroundView:NO]; } - (void)presentInviteFlow @@ -172,9 +214,8 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie [self presentViewController:inviteFlow.actionSheetController animated:YES completion:nil]; } - - (void)showLoadingBackgroundView:(BOOL)show { - if (show) { + if (show && !self.isBackgroundViewHidden) { _addGroup = self.navigationItem.rightBarButtonItem != nil ? _addGroup : self.navigationItem.rightBarButtonItem; self.navigationItem.rightBarButtonItem = nil; self.searchController.searchBar.hidden = YES; @@ -206,6 +247,7 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie self.inviteCell.hidden = YES; self.conversationForNonContactCell.hidden = YES; + self.inviteViaSMSCell.hidden = YES; self.searchController.searchBar.hidden = YES; self.tableView.backgroundView = self.noSignalContactsView; self.tableView.backgroundView.opaque = YES; @@ -218,6 +260,7 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie self.tableView.backgroundView = nil; self.inviteCell.hidden = NO; self.conversationForNonContactCell.hidden = NO; + self.inviteViaSMSCell.hidden = NO; } } @@ -244,17 +287,6 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie self.searchController.searchBar.delegate = self; self.searchController.searchBar.placeholder = NSLocalizedString(@"SEARCH_BYNAMEORNUMBER_PLACEHOLDER_TEXT", @""); - self.sendTextButton = [UIButton buttonWithType:UIButtonTypeRoundedRect]; - [self.sendTextButton setBackgroundColor:[UIColor ows_materialBlueColor]]; - [self.sendTextButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; - self.sendTextButton.frame = CGRectMake(self.searchController.searchBar.frame.origin.x, - self.searchController.searchBar.frame.origin.y + 44.0f, - self.searchController.searchBar.frame.size.width, - 44.0); - [self.view addSubview:self.sendTextButton]; - self.sendTextButton.hidden = YES; - - [self.sendTextButton addTarget:self action:@selector(sendText) forControlEvents:UIControlEventTouchUpInside]; [self initializeRefreshControl]; } @@ -283,10 +315,8 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie } - (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar { - self.sendTextButton.hidden = YES; } - #pragma mark - Filter - (void)filterContentForSearchText:(NSString *)searchText @@ -297,26 +327,58 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie NSString *formattedNumber = [PhoneNumber tryParsePhoneNumberFromUserSpecifiedText:searchText].toE164; // text to a non-signal number if we have no results and a valid phone # if (self.searchResults.count == 0 && searchText.length > 8 && formattedNumber) { - NSString *sendTextTo = NSLocalizedString(@"SEND_SMS_BUTTON", @""); - sendTextTo = [sendTextTo stringByAppendingString:formattedNumber]; - [self.sendTextButton setTitle:sendTextTo forState:UIControlStateNormal]; - self.sendTextButton.hidden = NO; self.currentSearchTerm = formattedNumber; - self.showNewConversationForNonContactButton = YES; - self.nonContactPhoneNumber = formattedNumber; + self.searchPhoneNumber = formattedNumber; + // Kick off account lookup if necessary. + [self checkIsNonContactPhoneNumberSignalUser:formattedNumber]; } else { - self.sendTextButton.hidden = YES; - self.showNewConversationForNonContactButton = NO; - _nonContactPhoneNumber = nil; + _searchPhoneNumber = nil; } } -- (void)setShowNewConversationForNonContactButton:(BOOL)showNewConversationForNonContactButton { - if (_showNewConversationForNonContactButton == showNewConversationForNonContactButton) { - return; +- (BOOL)checkIsNonContactPhoneNumberSignalUser:(NSString *)phoneNumber +{ + DDLogWarn(@"isNonContactPhoneNumberSignalUser: %@", phoneNumber); + + if ([self.phoneNumberAccountSet containsObject:phoneNumber]) { + return YES; } - _showNewConversationForNonContactButton = showNewConversationForNonContactButton; + __weak MessageComposeTableViewController *weakSelf = self; + [[ContactsUpdater sharedUpdater] lookupIdentifier:phoneNumber + success:^(SignalRecipient *recipient) { + DDLogWarn(@"Lookup contact with recipient: %@ %@", recipient, phoneNumber); + + MessageComposeTableViewController *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + if (!strongSelf.phoneNumberAccountSet) { + strongSelf.phoneNumberAccountSet = [NSMutableSet set]; + } + if (![strongSelf.phoneNumberAccountSet containsObject:phoneNumber]) { + [strongSelf.phoneNumberAccountSet addObject:phoneNumber]; + [strongSelf.tableView reloadData]; + } + } + failure:^(NSError *error) { + DDLogWarn(@"Failed to lookup contact with error: %@ %@", error, phoneNumber); + }]; + + return NO; + +// // This dictionary is used to cache the set of known Signal +// // accounts that correspond to "non-contact phone numbers." +// @property (nonatomic) NSMutableSet *phoneNumberAccountSet; + +} + +- (void)setSearchPhoneNumber:(NSString *)searchPhoneNumber { + if ([_searchPhoneNumber isEqualToString:searchPhoneNumber]) { + return; + } + + _searchPhoneNumber = searchPhoneNumber; [self.tableView reloadData]; } @@ -371,7 +433,6 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie [alertController addAction:cancelAction]; [alertController addAction:okAction]; - self.sendTextButton.hidden = YES; self.searchController.searchBar.text = @""; //must dismiss search controller before presenting alert. @@ -430,15 +491,30 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - if (section == MessageComposeTableViewControllerSectionInvite) { - if (floor(NSFoundationVersionNumber) < NSFoundationVersionNumber_iOS_9_0) { - // Invite flow not supported on iOS8 - return 0; - } - return 1; - } else if (section == MessageComposeTableViewControllerSectionNewConversationForNonContact) { - return _showNewConversationForNonContactButton ? 1 : 0; + BOOL showNonContactConversation = NO; + BOOL showInviteViaSMS = NO; + BOOL showInviteFlow = NO; + + BOOL hasPhoneNumber = self.searchPhoneNumber.length > 0; + BOOL isKnownSignalUser = self.searchPhoneNumber && [self.phoneNumberAccountSet containsObject:self.searchPhoneNumber]; + BOOL isInviteFlowSupported = floor(NSFoundationVersionNumber) >= NSFoundationVersionNumber_iOS_9_0; + if (hasPhoneNumber && isKnownSignalUser) { + showNonContactConversation = YES; + } else if (hasPhoneNumber) { + showInviteViaSMS = YES; + } else if (isInviteFlowSupported) { + showInviteFlow = YES; + } + + if (section == MessageComposeTableViewControllerSectionInviteNonContactConversation) { + return showNonContactConversation ? 1 : 0; + } else if (section == MessageComposeTableViewControllerSectionInviteViaSMS) { + return showInviteViaSMS ? 1 : 0; + } else if (section == MessageComposeTableViewControllerSectionInviteFlow) { + return showInviteFlow ? 1 : 0; } else { + OWSAssert(section == MessageComposeTableViewControllerSectionContacts) + if (self.searchController.active) { return (NSInteger)[self.searchResults count]; } else { @@ -449,16 +525,23 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - if (indexPath.section == MessageComposeTableViewControllerSectionInvite) { - return self.inviteCell; - } else if (indexPath.section == MessageComposeTableViewControllerSectionNewConversationForNonContact) { - OWSAssert(self.nonContactPhoneNumber.length > 0); - + if (indexPath.section == MessageComposeTableViewControllerSectionInviteNonContactConversation) { self.conversationForNonContactCell.textLabel.text = [NSString stringWithFormat:NSLocalizedString(@"NEW_CONVERSATION_FOR_NON_CONTACT_FORMAT", @"Text for button to start a new conversation with a non-contact"), - self.nonContactPhoneNumber]; + self.searchPhoneNumber]; return self.conversationForNonContactCell; + } else if (indexPath.section == MessageComposeTableViewControllerSectionInviteViaSMS) { + // TODO: We should rework this string to be a format, to account for languages where the + // phone number should not appear at the end of the copy. + self.inviteViaSMSCell.textLabel.text = [NSLocalizedString(@"SEND_SMS_BUTTON", + @"Text for button to send a Signal invite via SMS") + stringByAppendingString:self.searchPhoneNumber]; + return self.inviteViaSMSCell; + } else if (indexPath.section == MessageComposeTableViewControllerSectionInviteFlow) { + return self.inviteCell; } else { + OWSAssert(indexPath.section == MessageComposeTableViewControllerSectionContacts) + ContactTableViewCell *cell = (ContactTableViewCell *)[tableView dequeueReusableCellWithIdentifier:MessageComposeTableViewControllerCellContact]; @@ -472,7 +555,18 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - if (indexPath.section == MessageComposeTableViewControllerSectionInvite) { + if (indexPath.section == MessageComposeTableViewControllerSectionInviteNonContactConversation) { + OWSAssert(self.searchPhoneNumber.length > 0); + + if (self.searchPhoneNumber.length > 0) { + [self dismissViewControllerAnimated:YES + completion:^() { + [Environment messageIdentifier:self.searchPhoneNumber withCompose:YES]; + }]; + } + } else if (indexPath.section == MessageComposeTableViewControllerSectionInviteViaSMS) { + [self sendText]; + } else if (indexPath.section == MessageComposeTableViewControllerSectionInviteFlow) { void (^showInvite)() = ^{ OWSInviteFlow *inviteFlow = [[OWSInviteFlow alloc] initWithPresentingViewController:self contactsManager:self.contactsManager]; @@ -489,15 +583,6 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie } else { showInvite(); } - } else if (indexPath.section == MessageComposeTableViewControllerSectionNewConversationForNonContact) { - OWSAssert(self.nonContactPhoneNumber.length > 0); - - if (self.nonContactPhoneNumber.length > 0) { - [self dismissViewControllerAnimated:YES - completion:^() { - [Environment messageIdentifier:self.nonContactPhoneNumber withCompose:YES]; - }]; - } } else { NSString *identifier = [[[self contactForIndexPath:indexPath] textSecureIdentifiers] firstObject]; diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 8fe4c9cd6..030ad11c0 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -244,9 +244,6 @@ /* Generic server error */ "ERROR_DESCRIPTION_SERVER_FAILURE" = "Server Error. Please try again later."; -/* Worst case generic error message */ -"ERROR_DESCRIPTION_UNKNOWN_ERROR" = "An unkown error occurred."; - /* Error message when attempting to send message */ "ERROR_DESCRIPTION_UNREGISTERED_RECIPIENT" = "Contact is not a Signal user."; @@ -504,6 +501,9 @@ /* No comment provided by engineer. */ "NEW_GROUP_REQUEST_ADDPEOPLE" = "Add people"; +/* Label for a button that lets users search for contacts by phone number */ +"NO_CONTACTS_SEARCH_BY_PHONE_NUMBER" = "Find Contacts by Phone Number"; + /* No comment provided by engineer. */ "NOTIFICATION_SEND_FAILED" = "Your message failed to send to %@.";