diff --git a/Podfile.lock b/Podfile.lock index ee8a76967..0d31a30af 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -136,7 +136,7 @@ CHECKOUT OPTIONS: :commit: 7054e4b13ee5bcd6d524adb6dc9a726e8c466308 :git: https://github.com/WhisperSystems/JSQMessagesViewController.git SignalServiceKit: - :commit: d25a934039e3e14dcb128bd7b1648e3f514bbbf6 + :commit: e336e0b34a40178ad0d96767fe9dc7f37ba97dc0 :git: https://github.com/WhisperSystems/SignalServiceKit.git SocketRocket: :commit: 877ac7438be3ad0b45ef5ca3969574e4b97112bf diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 62444cd25..5a1ce3cfa 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -105,6 +105,7 @@ 453D28B71D32BA5F00D523F0 /* OWSDisplayedMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 453D28B61D32BA5F00D523F0 /* OWSDisplayedMessage.m */; }; 453D28BA1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = 453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */; }; 453D28BB1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = 453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */; }; + 4542F0941EB9372700C7EE92 /* SystemContactsFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542F0931EB9372700C7EE92 /* SystemContactsFetcher.swift */; }; 45464DBC1DFA041F001D3FD6 /* DataChannelMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45464DBB1DFA041F001D3FD6 /* DataChannelMessage.swift */; }; 45666EC61D99483D008FE134 /* OWSAvatarBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 45666EC51D99483D008FE134 /* OWSAvatarBuilder.m */; }; 45666EC91D994C0D008FE134 /* OWSGroupAvatarBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 45666EC81D994C0D008FE134 /* OWSGroupAvatarBuilder.m */; }; @@ -504,6 +505,7 @@ 453D28B61D32BA5F00D523F0 /* OWSDisplayedMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDisplayedMessage.m; sourceTree = ""; }; 453D28B81D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessagesBubblesSizeCalculator.h; sourceTree = ""; }; 453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessagesBubblesSizeCalculator.m; sourceTree = ""; }; + 4542F0931EB9372700C7EE92 /* SystemContactsFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemContactsFetcher.swift; sourceTree = ""; }; 45464DBB1DFA041F001D3FD6 /* DataChannelMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChannelMessage.swift; sourceTree = ""; }; 454B35071D08EED80026D658 /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = translations/mk.lproj/Localizable.strings; sourceTree = ""; }; 45666EC41D99483D008FE134 /* OWSAvatarBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAvatarBuilder.h; sourceTree = ""; }; @@ -1192,6 +1194,7 @@ 76EB040918170B33006006FC /* OWSContactsManager.m */, 45843D1D1D2236B30013E85A /* OWSContactsSearcher.h */, 45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */, + 4542F0931EB9372700C7EE92 /* SystemContactsFetcher.swift */, ); path = contact; sourceTree = ""; @@ -2010,6 +2013,7 @@ 450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */, 340CB2241EAC155C0001CAA1 /* ContactsViewHelper.m in Sources */, 45CD81F21DC03A22004C9430 /* OWSLogger.m in Sources */, + 4542F0941EB9372700C7EE92 /* SystemContactsFetcher.swift in Sources */, B60C16651988999D00E97A6C /* VersionMigrations.m in Sources */, B97940271832BD2400BD66CB /* UIUtil.m in Sources */, 34B3F8791E8DF1700035BE1A /* CountryCodeViewController.m in Sources */, @@ -2449,7 +2453,7 @@ "\"$(SRCROOT)/Libraries\"/**", ); INFOPLIST_FILE = "$(SRCROOT)/Signal/Signal-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -2509,7 +2513,7 @@ "\"$(SRCROOT)/Libraries\"/**", ); INFOPLIST_FILE = "$(SRCROOT)/Signal/Signal-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 6426ed931..3d1616423 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -113,11 +113,6 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; return YES; } - if ([TSAccountManager isRegistered]) { - [Environment.getCurrent.contactsManager doAfterEnvironmentInitSetup]; - } - - UIStoryboard *storyboard; if ([TSAccountManager isRegistered]) { storyboard = [UIStoryboard storyboardWithName:AppDelegateStoryboardMain bundle:[NSBundle mainBundle]]; @@ -171,7 +166,7 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; // sent before the app exited should be marked as failures. [[[OWSFailedMessagesJob alloc] initWithStorageManager:[TSStorageManager sharedManager]] run]; [[[OWSFailedAttachmentDownloadsJob alloc] initWithStorageManager:[TSStorageManager sharedManager]] run]; - + [AppStoreRating setupRatingLibrary]; }]; @@ -405,8 +400,12 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; // can't verify in production env due to code // signing. [TSSocketManager requestSocketOpen]; - [[Environment getCurrent].contactsManager verifyABPermission]; - + + dispatch_async(dispatch_get_main_queue(), ^{ + [[Environment getCurrent] + .contactsManager fetchSystemContactsIfAlreadyAuthorized]; + }); + // This will fetch new messages, if we're using domain // fronting. [[PushManager sharedManager] applicationDidBecomeActive]; diff --git a/Signal/src/ViewControllers/ContactsPicker.swift b/Signal/src/ViewControllers/ContactsPicker.swift index 2016c4047..092523947 100644 --- a/Signal/src/ViewControllers/ContactsPicker.swift +++ b/Signal/src/ViewControllers/ContactsPicker.swift @@ -1,10 +1,11 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + // Originally based on EPContacts // // Created by Prabaharan Elangovan on 12/10/15. -// Parts Copyright © 2015 Prabaharan Elangovan. All rights reserved. -// -// Modified for Signal by Michael Kirk on 11/25/2016 -// Parts Copyright © 2016 Open Whisper Systems. All rights reserved. +// Parts Copyright © 2015 Prabaharan Elangovan. All rights reserved import UIKit import Contacts @@ -27,7 +28,7 @@ public extension ContactsPickerDelegate { func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool { return true } } -public enum SubtitleCellValue{ +public enum SubtitleCellValue { case phoneNumber case email } @@ -89,17 +90,17 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat func didChangePreferredContentSize() { self.tableView.reloadData() } - + func initializeBarButtons() { let cancelButton = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(onTouchCancelButton)) self.navigationItem.leftBarButtonItem = cancelButton - + if multiSelectEnabled { let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(onTouchDoneButton)) self.navigationItem.rightBarButtonItem = doneButton } } - + fileprivate func registerContactCell() { tableView.register(ContactCell.nib, forCellReuseIdentifier: contactCellReuseIdentifier) } @@ -119,14 +120,14 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat convenience public init(delegate: ContactsPickerDelegate?) { self.init(delegate: delegate, multiSelection: false) } - - convenience public init(delegate: ContactsPickerDelegate?, multiSelection : Bool) { + + convenience public init(delegate: ContactsPickerDelegate?, multiSelection: Bool) { self.init() multiSelectEnabled = multiSelection contactsPickerDelegate = delegate } - convenience public init(delegate: ContactsPickerDelegate?, multiSelection : Bool, subtitleCellType: SubtitleCellValue) { + convenience public init(delegate: ContactsPickerDelegate?, multiSelection: Bool, subtitleCellType: SubtitleCellValue) { self.init() multiSelectEnabled = multiSelection contactsPickerDelegate = delegate @@ -134,24 +135,24 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat } // MARK: - Contact Operations - + open func reloadContacts() { getContacts( onError: { error in Logger.error("\(self.TAG) failed to reload contacts with error:\(error)") }) } - func getContacts(onError errorHandler: @escaping (_ error: Error) -> Void) { + func getContacts(onError errorHandler: @escaping (_ error: Error) -> Void) { switch CNContactStore.authorizationStatus(for: CNEntityType.contacts) { case CNAuthorizationStatus.denied, CNAuthorizationStatus.restricted: - + let title = NSLocalizedString("AB_PERMISSION_MISSING_TITLE", comment: "Alert title when contacts disabled") let body = NSLocalizedString("ADDRESSBOOK_RESTRICTED_ALERT_BODY", comment: "Alert body when contacts disabled") let alert = UIAlertController(title: title, message: body, preferredStyle: UIAlertControllerStyle.alert) let dismissText = NSLocalizedString("DISMISS_BUTTON_TEXT", comment:"") - let okAction = UIAlertAction(title: dismissText, style: UIAlertActionStyle.default, handler: { action in + let okAction = UIAlertAction(title: dismissText, style: UIAlertActionStyle.default, handler: { _ in let error = NSError(domain: "contactsPickerErrorDomain", code: 1, userInfo: [NSLocalizedDescriptionKey: "No Contacts Access"]) self.contactsPickerDelegate?.contactsPicker(self, didContactFetchFailed: error) errorHandler(error) @@ -159,7 +160,7 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat }) alert.addAction(okAction) self.present(alert, animated: true, completion: nil) - + case CNAuthorizationStatus.notDetermined: //This case means the user is prompted for the first time for allowing contacts contactStore.requestAccess(for: CNEntityType.contacts) { (granted, error) -> Void in @@ -170,14 +171,14 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat errorHandler(error!) } } - + case CNAuthorizationStatus.authorized: //Authorization granted by user for this app. var contacts = [CNContact]() do { let contactFetchRequest = CNContactFetchRequest(keysToFetch: allowedContactKeys) - try contactStore.enumerateContacts(with: contactFetchRequest) { (contact, stop) -> Void in + try contactStore.enumerateContacts(with: contactFetchRequest) { (contact, _) -> Void in contacts.append(contact) } self.sections = collatedContacts(contacts) @@ -198,13 +199,12 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat return collated } - // MARK: - Table View DataSource - + open func numberOfSections(in tableView: UITableView) -> Int { return self.collation.sectionTitles.count } - + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { let dataSource = filteredSections @@ -218,7 +218,7 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat let dataSource = filteredSections let cnContact = dataSource[indexPath.section][indexPath.row] - let contact = Contact(contact: cnContact) + let contact = Contact(systemContact: cnContact) cell.updateContactsinUI(contact, subtitleType: subtitleCellValue, contactsManager: self.contactsManager) let isSelected = selectedContacts.contains(where: { $0.uniqueId == contact.uniqueId }) @@ -239,7 +239,7 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat let cell = tableView.cellForRow(at: indexPath) as! ContactCell let deselectedContact = cell.contact! - selectedContacts = selectedContacts.filter() { + selectedContacts = selectedContacts.filter { return $0.uniqueId != deselectedContact.uniqueId } } @@ -262,11 +262,11 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat } } } - + open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { return collation.section(forSectionIndexTitle: index) } - + open func sectionIndexTitles(for tableView: UITableView) -> [String]? { return collation.sectionIndexTitles } @@ -280,24 +280,24 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat return nil } } - + // MARK: - Button Actions - + func onTouchCancelButton() { contactsPickerDelegate?.contactsPicker(self, didCancel: NSError(domain: "contactsPickerErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey: "User Canceled Selection"])) dismiss(animated: true, completion: nil) } - + func onTouchDoneButton() { contactsPickerDelegate?.contactsPicker(self, didSelectMultipleContacts: selectedContacts) dismiss(animated: true, completion: nil) } - + // MARK: - Search Actions open func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { updateSearchResults(searchText: searchText) } - + open func updateSearchResults(searchText: String) { let predicate: NSPredicate if searchText.characters.count == 0 { @@ -305,7 +305,7 @@ open class ContactsPicker: UIViewController, UITableViewDelegate, UITableViewDat } else { do { predicate = CNContact.predicateForContacts(matchingName: searchText) - let filteredContacts = try contactStore.unifiedContacts(matching: predicate,keysToFetch: allowedContactKeys) + let filteredContacts = try contactStore.unifiedContacts(matching: predicate, keysToFetch: allowedContactKeys) filteredSections = collatedContacts(filteredContacts) } catch let error as NSError { Logger.error("\(self.TAG) updating search results failed with error: \(error)") @@ -349,7 +349,7 @@ fileprivate extension CNContact { if self.familyName.isEmpty && self.givenName.isEmpty { return self.emailAddresses.first?.value as? String ?? "" } - + let compositeName: String if ContactSortOrder == .familyName { compositeName = "\(self.familyName) \(self.givenName)" diff --git a/Signal/src/ViewControllers/ContactsViewHelper.m b/Signal/src/ViewControllers/ContactsViewHelper.m index c553297dc..c4a137432 100644 --- a/Signal/src/ViewControllers/ContactsViewHelper.m +++ b/Signal/src/ViewControllers/ContactsViewHelper.m @@ -8,6 +8,7 @@ #import "SignalAccount.h" #import #import +#import #import NS_ASSUME_NONNULL_BEGIN diff --git a/Signal/src/ViewControllers/MessageComposeTableViewController.m b/Signal/src/ViewControllers/MessageComposeTableViewController.m index 947c2e0f8..0adc0bfb3 100644 --- a/Signal/src/ViewControllers/MessageComposeTableViewController.m +++ b/Signal/src/ViewControllers/MessageComposeTableViewController.m @@ -165,6 +165,11 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie { [super viewWillAppear:animated]; + // Make sure we have requested contact access at this point if, e.g. + // the user has no messages in their inbox and they choose to compose + // a message. + [self.contactsManager requestSystemContactsOnce]; + [self showEmptyBackgroundViewIfNecessary]; } @@ -729,6 +734,8 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie self.contacts = [self filteredContacts]; [self updateSearchResultsForSearchController:self.searchController]; [self.tableView reloadData]; + // TODO revisit this after https://github.com/WhisperSystems/Signal-iOS/pull/2058 is merged + [self showEmptyBackgroundViewIfNecessary]; } - (BOOL)isContactHidden:(Contact *)contact diff --git a/Signal/src/ViewControllers/MessagesViewController.m b/Signal/src/ViewControllers/MessagesViewController.m index f4bf894c0..0c673705e 100644 --- a/Signal/src/ViewControllers/MessagesViewController.m +++ b/Signal/src/ViewControllers/MessagesViewController.m @@ -528,6 +528,10 @@ typedef enum : NSUInteger { // restart any animations that were stopped e.g. while inspecting the contact info screens. [self startExpirationTimerAnimations]; + // We should have already requested contact access at this point, so this should be a no-op + // unless it ever becomes possible to to load this VC without going via the SignalsViewController + [self.contactsManager requestSystemContactsOnce]; + OWSDisappearingMessagesConfiguration *configuration = [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:self.thread.uniqueId]; [self setBarButtonItemsForDisappearingMessagesConfiguration:configuration]; diff --git a/Signal/src/ViewControllers/SelectThreadViewController.m b/Signal/src/ViewControllers/SelectThreadViewController.m index cc9b5fa88..4ee339ec7 100644 --- a/Signal/src/ViewControllers/SelectThreadViewController.m +++ b/Signal/src/ViewControllers/SelectThreadViewController.m @@ -16,6 +16,7 @@ #import "UIColor+OWS.h" #import "UIFont+OWS.h" #import "UIView+OWS.h" +#import #import #import #import diff --git a/Signal/src/ViewControllers/SignalsViewController.m b/Signal/src/ViewControllers/SignalsViewController.m index 5a93a6f0d..4d84ffab3 100644 --- a/Signal/src/ViewControllers/SignalsViewController.m +++ b/Signal/src/ViewControllers/SignalsViewController.m @@ -276,7 +276,9 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self checkIfEmptyView]; - + if ([TSThread numberOfKeysInCollection] > 0) { + [self.contactsManager requestSystemContactsOnce]; + } [self updateInboxCountLabel]; [[self tableView] reloadData]; } @@ -287,8 +289,7 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS [self.editingDbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) { [self.experienceUpgradeFinder markAllAsSeenWithTransaction:transaction]; }]; - - [self didAppearForNewlyRegisteredUser]; + [self ensureNotificationsUpToDate]; } else { [self displayAnyUnseenUpgradeExperience]; } @@ -296,39 +297,6 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS #pragma mark - startup -- (void)didAppearForNewlyRegisteredUser -{ - ABAuthorizationStatus status = ABAddressBookGetAuthorizationStatus(); - switch (status) { - case kABAuthorizationStatusNotDetermined: - case kABAuthorizationStatusRestricted: { - UIAlertController *controller = - [UIAlertController alertControllerWithTitle:NSLocalizedString(@"REGISTER_CONTACTS_WELCOME", nil) - message:NSLocalizedString(@"REGISTER_CONTACTS_BODY", nil) - preferredStyle:UIAlertControllerStyleAlert]; - - [controller - addAction:[UIAlertAction - actionWithTitle:NSLocalizedString(@"REGISTER_CONTACTS_CONTINUE", nil) - style:UIAlertActionStyleCancel - handler:^(UIAlertAction *action) { - [self ensureNotificationsUpToDate]; - [[Environment getCurrent].contactsManager doAfterEnvironmentInitSetup]; - }]]; - - [self presentViewController:controller animated:YES completion:nil]; - break; - } - default: { - DDLogError(@"%@ Unexpected for new user to have kABAuthorizationStatus:%ld", self.tag, status); - [self ensureNotificationsUpToDate]; - [[Environment getCurrent].contactsManager doAfterEnvironmentInitSetup]; - - break; - } - } -} - - (void)displayAnyUnseenUpgradeExperience { AssertIsOnMainThread(); @@ -683,6 +651,12 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS NSArray *sectionChanges = nil; NSArray *rowChanges = nil; + // If the user hasn't already granted contact access + // we don't want to request until they receive a message. + if ([TSThread numberOfKeysInCollection] > 0) { + [self.contactsManager requestSystemContactsOnce]; + } + [[self.uiDatabaseConnection ext:TSThreadDatabaseViewExtensionName] getSectionChanges:§ionChanges rowChanges:&rowChanges forNotifications:notifications diff --git a/Signal/src/contact/OWSContactsManager.h b/Signal/src/contact/OWSContactsManager.h index 4408a0eb8..315373abe 100644 --- a/Signal/src/contact/OWSContactsManager.h +++ b/Signal/src/contact/OWSContactsManager.h @@ -2,12 +2,8 @@ // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // -#import -#import -#import -#import -#import "CollapsingFutures.h" #import "Contact.h" +#import NS_ASSUME_NONNULL_BEGIN @@ -33,13 +29,14 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification; - (Contact *)getOrBuildContactForPhoneIdentifier:(NSString *)identifier; -- (void)verifyABPermission; +#pragma mark - System Contact Fetching + +- (void)requestSystemContactsOnce; +- (void)fetchSystemContactsIfAlreadyAuthorized; // TODO: Remove this method. - (NSArray *)signalContacts; -- (void)doAfterEnvironmentInitSetup; - - (NSString *)displayNameForPhoneIdentifier:(nullable NSString *)identifier; - (NSString *)displayNameForContact:(Contact *)contact; - (NSString *)displayNameForSignalAccount:(SignalAccount *)signalAccount; @@ -48,6 +45,7 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification; - (NSAttributedString *)formattedFullNameForContact:(Contact *)contact font:(UIFont *)font; - (NSAttributedString *)formattedFullNameForRecipientId:(NSString *)recipientId font:(UIFont *)font; +// TODO migrate to CNContact? - (BOOL)hasAddressBook; + (NSComparator _Nonnull)contactComparator; diff --git a/Signal/src/contact/OWSContactsManager.m b/Signal/src/contact/OWSContactsManager.m index 322d1c659..d2bbb303f 100644 --- a/Signal/src/contact/OWSContactsManager.m +++ b/Signal/src/contact/OWSContactsManager.m @@ -4,21 +4,19 @@ #import "OWSContactsManager.h" #import "Environment.h" +#import "Signal-Swift.h" #import "SignalAccount.h" #import "Util.h" #import #import -#define ADDRESSBOOK_QUEUE dispatch_get_main_queue() - -typedef BOOL (^ContactSearchBlock)(id, NSUInteger, BOOL *); +@import Contacts; NSString *const OWSContactsManagerSignalAccountsDidChangeNotification = @"OWSContactsManagerSignalAccountsDidChangeNotification"; -@interface OWSContactsManager () +@interface OWSContactsManager () -@property (atomic, nullable) CNContactStore *contactStore; @property (atomic) id addressBookReference; @property (atomic) TOCFuture *futureAddressBook; @property (nonatomic) BOOL isContactsUpdateInFlight; @@ -28,7 +26,7 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification = @property (atomic) NSDictionary *allContactsMap; @property (atomic) NSArray *signalAccounts; @property (atomic) NSDictionary *signalAccountMap; - +@property (nonatomic, readonly) SystemContactsFetcher *systemContactsFetcher; @end @implementation OWSContactsManager @@ -43,89 +41,37 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification = _allContacts = @[]; _signalAccountMap = @{}; _signalAccounts = @[]; + _systemContactsFetcher = [SystemContactsFetcher new]; + _systemContactsFetcher.delegate = self; OWSSingletonAssert(); return self; } -- (void)doAfterEnvironmentInitSetup { - if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(9, 0) && - !self.contactStore) { - OWSAssert(!self.contactStore); - self.contactStore = [[CNContactStore alloc] init]; - [self.contactStore requestAccessForEntityType:CNEntityTypeContacts - completionHandler:^(BOOL granted, NSError *_Nullable error) { - if (!granted) { - // We're still using the old addressbook API. - // User warned if permission not granted in that setup. - } - }]; - } - - [self setupAddressBookIfNecessary]; -} +#pragma mark - System Contact Fetching -- (void)verifyABPermission { - [self setupAddressBookIfNecessary]; -} - -#pragma mark - Address Book callbacks - -void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef info, void *context); -void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef info, void *context) { - OWSContactsManager *contactsManager = (__bridge OWSContactsManager *)context; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [contactsManager handleAddressBookChanged]; - }); +// Request contacts access if you haven't asked recently. +- (void)requestSystemContactsOnce +{ + [self.systemContactsFetcher requestOnce]; } -- (void)handleAddressBookChanged +- (void)fetchSystemContactsIfAlreadyAuthorized { - [self pullLatestAddressBook]; + [self.systemContactsFetcher fetchIfAlreadyAuthorized]; } -#pragma mark - Setup +#pragma mark SystemContactsFetcherDelegate -- (void)setupAddressBookIfNecessary +- (void)systemContactsFetcher:(SystemContactsFetcher *)systemsContactsFetcher + updatedContacts:(NSArray *)contacts { - dispatch_async(ADDRESSBOOK_QUEUE, ^{ - // De-bounce address book setup. - if (self.isContactsUpdateInFlight) { - return; - } - // We only need to set up our address book once; - // after that we only need to respond to onAddressBookChanged. - if (self.addressBookReference) { - return; - } - self.isContactsUpdateInFlight = YES; - - TOCFuture *future = [OWSContactsManager asyncGetAddressBook]; - [future thenDo:^(id addressBook) { - // Success. - OWSAssert(self.isContactsUpdateInFlight); - OWSAssert(!self.addressBookReference); - - self.addressBookReference = addressBook; - self.isContactsUpdateInFlight = NO; - - ABAddressBookRef cfAddressBook = (__bridge ABAddressBookRef)addressBook; - ABAddressBookRegisterExternalChangeCallback(cfAddressBook, onAddressBookChanged, (__bridge void *)self); - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self handleAddressBookChanged]; - }); - }]; - [future catchDo:^(id failure) { - // Failure. - OWSAssert(self.isContactsUpdateInFlight); - OWSAssert(!self.addressBookReference); - - self.isContactsUpdateInFlight = NO; - }]; - }); + [self updateWithContacts:contacts]; } +#pragma mark - Intersection + - (void)intersectContacts { [self intersectContactsWithRetryDelay:1]; @@ -159,21 +105,6 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in failure:failure]; } -- (void)pullLatestAddressBook { - dispatch_async(ADDRESSBOOK_QUEUE, ^{ - CFErrorRef creationError = nil; - ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, &creationError); - checkOperationDescribe(nil == creationError, [((__bridge NSError *)creationError)localizedDescription]); - ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef error) { - if (!granted) { - [OWSContactsManager blockingContactDialog]; - } - }); - NSArray *contacts = [self getContactsFromAddressBook:addressBookRef]; - [self updateWithContacts:contacts]; - }); -} - - (void)updateWithContacts:(NSArray *)contacts { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @@ -249,6 +180,8 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in }); } +#pragma mark - View Helpers +// TODO move into Contact class. + (NSString *)accountLabelForContact:(Contact *)contact recipientId:(NSString *)recipientId { OWSAssert(contact); @@ -324,207 +257,10 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in return phoneNumberLabel; } -+ (void)blockingContactDialog { - switch (ABAddressBookGetAuthorizationStatus()) { - case kABAuthorizationStatusRestricted: { - UIAlertController *controller = - [UIAlertController alertControllerWithTitle:NSLocalizedString(@"AB_PERMISSION_MISSING_TITLE", nil) - message:NSLocalizedString(@"ADDRESSBOOK_RESTRICTED_ALERT_BODY", nil) - preferredStyle:UIAlertControllerStyleAlert]; - - [controller - addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ADDRESSBOOK_RESTRICTED_ALERT_BUTTON", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - [DDLog flushLog]; - exit(0); - }]]; - - [[UIApplication sharedApplication] - .keyWindow.rootViewController presentViewController:controller - animated:YES - completion:nil]; - - break; - } - case kABAuthorizationStatusDenied: { - UIAlertController *controller = - [UIAlertController alertControllerWithTitle:NSLocalizedString(@"AB_PERMISSION_MISSING_TITLE", nil) - message:NSLocalizedString(@"AB_PERMISSION_MISSING_BODY", nil) - preferredStyle:UIAlertControllerStyleAlert]; - - [controller addAction:[UIAlertAction - actionWithTitle:NSLocalizedString(@"AB_PERMISSION_MISSING_ACTION", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - [[UIApplication sharedApplication] - openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]]; - }]]; - - [[[UIApplication sharedApplication] keyWindow] - .rootViewController presentViewController:controller - animated:YES - completion:nil]; - break; - } - - case kABAuthorizationStatusNotDetermined: { - DDLogInfo(@"AddressBook access not granted but status undetermined."); - [[Environment getCurrent].contactsManager pullLatestAddressBook]; - break; - } - - case kABAuthorizationStatusAuthorized: { - DDLogInfo(@"AddressBook access not granted but status authorized."); - break; - } - - default: - break; - } -} - -#pragma mark - Address Book utils - -+ (TOCFuture *)asyncGetAddressBook { - CFErrorRef creationError = nil; - ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, &creationError); - assert((addressBookRef == nil) == (creationError != nil)); - if (creationError != nil) { - [self blockingContactDialog]; - return [TOCFuture futureWithFailure:(__bridge_transfer id)creationError]; - } - - TOCFutureSource *futureAddressBookSource = [TOCFutureSource new]; - - id addressBook = (__bridge_transfer id)addressBookRef; - ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef requestAccessError) { - if (granted && ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized) { - dispatch_async(ADDRESSBOOK_QUEUE, ^{ - [futureAddressBookSource trySetResult:addressBook]; - }); - } else { - [self blockingContactDialog]; - [futureAddressBookSource trySetFailure:(__bridge id)requestAccessError]; - } - }); - - return futureAddressBookSource.future; -} - -- (NSArray *)getContactsFromAddressBook:(ABAddressBookRef _Nonnull)addressBook -{ - CFArrayRef allPeople = ABAddressBookCopyArrayOfAllPeople(addressBook); - - CFMutableArrayRef allPeopleMutable = - CFArrayCreateMutableCopy(kCFAllocatorDefault, CFArrayGetCount(allPeople), allPeople); - - CFArraySortValues(allPeopleMutable, - CFRangeMake(0, CFArrayGetCount(allPeopleMutable)), - (CFComparatorFunction)ABPersonComparePeopleByName, - (void *)(unsigned long)ABPersonGetSortOrdering()); - - NSArray *sortedPeople = (__bridge_transfer NSArray *)allPeopleMutable; - - // This predicate returns all contacts from the addressbook having at least one phone number - - NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id record, NSDictionary *bindings) { - ABMultiValueRef phoneNumbers = ABRecordCopyValue((__bridge ABRecordRef)record, kABPersonPhoneProperty); - BOOL result = NO; - - for (CFIndex i = 0; i < ABMultiValueGetCount(phoneNumbers); i++) { - NSString *phoneNumber = (__bridge_transfer NSString *)ABMultiValueCopyValueAtIndex(phoneNumbers, i); - if (phoneNumber.length > 0) { - result = YES; - break; - } - } - CFRelease(phoneNumbers); - return result; - }]; - CFRelease(allPeople); - NSArray *filteredContacts = [sortedPeople filteredArrayUsingPredicate:predicate]; - - return [filteredContacts map:^id(id item) { - Contact *contact = [self contactForRecord:(__bridge ABRecordRef)item]; - return contact; - }]; -} - -#pragma mark - Contact/Phone Number util - -- (Contact *)contactForRecord:(ABRecordRef)record { - ABRecordID recordID = ABRecordGetRecordID(record); - - NSString *firstName = (__bridge_transfer NSString *)ABRecordCopyValue(record, kABPersonFirstNameProperty); - NSString *lastName = (__bridge_transfer NSString *)ABRecordCopyValue(record, kABPersonLastNameProperty); - NSDictionary *phoneNumberTypeMap = [self phoneNumbersForRecord:record]; - NSArray *phoneNumbers = [phoneNumberTypeMap.allKeys sortedArrayUsingSelector:@selector(compare:)]; - - if (!firstName && !lastName) { - NSString *companyName = (__bridge_transfer NSString *)ABRecordCopyValue(record, kABPersonOrganizationProperty); - if (companyName) { - firstName = companyName; - } else if (phoneNumbers.count) { - firstName = phoneNumbers.firstObject; - } - } - - NSData *imageData - = (__bridge_transfer NSData *)ABPersonCopyImageDataWithFormat(record, kABPersonImageFormatThumbnail); - UIImage *img = [UIImage imageWithData:imageData]; - - return [[Contact alloc] initWithContactWithFirstName:firstName - andLastName:lastName - andUserTextPhoneNumbers:phoneNumbers - phoneNumberTypeMap:phoneNumberTypeMap - andImage:img - andContactID:recordID]; -} - - (BOOL)phoneNumber:(PhoneNumber *)phoneNumber1 matchesNumber:(PhoneNumber *)phoneNumber2 { return [phoneNumber1.toE164 isEqualToString:phoneNumber2.toE164]; } -- (NSDictionary *)phoneNumbersForRecord:(ABRecordRef)record -{ - ABMultiValueRef phoneNumberRefs = NULL; - - @try { - phoneNumberRefs = ABRecordCopyValue(record, kABPersonPhoneProperty); - - CFIndex phoneNumberCount = ABMultiValueGetCount(phoneNumberRefs); - NSMutableDictionary *result = [NSMutableDictionary new]; - for (int i = 0; i < phoneNumberCount; i++) { - NSString *phoneNumberLabel = (__bridge_transfer NSString *)ABMultiValueCopyLabelAtIndex(phoneNumberRefs, i); - NSString *phoneNumber = (__bridge_transfer NSString *)ABMultiValueCopyValueAtIndex(phoneNumberRefs, i); - - if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneMobileLabel]) { - result[phoneNumber] = @(OWSPhoneNumberTypeMobile); - } else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneIPhoneLabel]) { - result[phoneNumber] = @(OWSPhoneNumberTypeIPhone); - } else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneMainLabel]) { - result[phoneNumber] = @(OWSPhoneNumberTypeMain); - } else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneHomeFAXLabel]) { - result[phoneNumber] = @(OWSPhoneNumberTypeHomeFAX); - } else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneWorkFAXLabel]) { - result[phoneNumber] = @(OWSPhoneNumberTypeWorkFAX); - } else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneOtherFAXLabel]) { - result[phoneNumber] = @(OWSPhoneNumberTypeOtherFAX); - } else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhonePagerLabel]) { - result[phoneNumber] = @(OWSPhoneNumberTypePager); - } else { - result[phoneNumber] = @(OWSPhoneNumberTypeUnknown); - } - } - return [result copy]; - } @finally { - if (phoneNumberRefs) { - CFRelease(phoneNumberRefs); - } - } -} - #pragma mark - Whisper User Management - (NSArray *)getSignalUsersFromContactsArray:(NSArray *)contacts { @@ -567,6 +303,7 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in return displayName; } +// TODO move into Contact class. - (NSString *_Nonnull)displayNameForContact:(Contact *)contact { OWSAssert(contact); @@ -617,6 +354,7 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in } } +// TODO move into Contact class. - (NSAttributedString *_Nonnull)formattedFullNameForContact:(Contact *)contact font:(UIFont *_Nonnull)font { UIFont *boldFont = [UIFont ows_mediumFontWithSize:font.pointSize]; diff --git a/Signal/src/contact/SystemContactsFetcher.swift b/Signal/src/contact/SystemContactsFetcher.swift new file mode 100644 index 000000000..504927524 --- /dev/null +++ b/Signal/src/contact/SystemContactsFetcher.swift @@ -0,0 +1,110 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +import Foundation +import Contacts + +@objc protocol SystemContactsFetcherDelegate: class { + func systemContactsFetcher(_ systemContactsFetcher: SystemContactsFetcher, updatedContacts contacts: [Contact]) +} + +@objc +class SystemContactsFetcher: NSObject { + + private let TAG = "[SystemContactsFetcher]" + + public weak var delegate: SystemContactsFetcherDelegate? + + public var authorizationStatus: CNAuthorizationStatus { + return CNContactStore.authorizationStatus(for: CNEntityType.contacts) + } + + private let contactStore = CNContactStore() + private var systemContactsHaveBeenRequestedAtLeastOnce = false + private let allowedContactKeys: [CNKeyDescriptor] = [ + CNContactFormatter.descriptorForRequiredKeys(for: .fullName), + CNContactThumbnailImageDataKey as CNKeyDescriptor, // TODO full image instead of thumbnail? + CNContactPhoneNumbersKey as CNKeyDescriptor, + CNContactEmailAddressesKey as CNKeyDescriptor + ] + + public func requestOnce() { + AssertIsOnMainThread() + + guard !systemContactsHaveBeenRequestedAtLeastOnce else { + Logger.debug("\(TAG) already requested system contacts") + return + } + systemContactsHaveBeenRequestedAtLeastOnce = true + self.startObservingContactChanges() + + switch authorizationStatus { + case .notDetermined: + contactStore.requestAccess(for: .contacts, completionHandler: { (granted, error) in + if let error = error { + Logger.error("\(self.TAG) error fetching contacts: \(error)") + assertionFailure() + } + + if !granted { + // TODO, make this a one time dismissable admonishment + // e.g. remember across launches that the user has dismissed. + self.displayMissingContactsPermissionAlert() + } else { + self.updateContacts() + } + }) + case .authorized: + // TODO reset onetime admonishment reminder, so that we remind user again (once) if they've since toggled permissions. + self.updateContacts() + case .denied, .restricted: + Logger.debug("\(TAG) contacts were \(self.authorizationStatus)") + } + } + + public func fetchIfAlreadyAuthorized() { + AssertIsOnMainThread() + guard authorizationStatus == .authorized else { + return + } + + updateContacts() + } + + private func displayMissingContactsPermissionAlert() { + let foo = UIApplication.shared.frontmostViewController + Logger.error("TODO") + } + + private func updateContacts() { + systemContactsHaveBeenRequestedAtLeastOnce = true + + var systemContacts = [CNContact]() + do { + let contactFetchRequest = CNContactFetchRequest(keysToFetch: allowedContactKeys) + try contactStore.enumerateContacts(with: contactFetchRequest) { (contact, _) -> Void in + systemContacts.append(contact) + } + } catch let error as NSError { + Logger.error("\(self.TAG) Failed to fetch contacts with error:\(error)") + assertionFailure() + } + + let contacts = systemContacts.map { Contact(systemContact: $0) } + self.delegate?.systemContactsFetcher(self, updatedContacts: contacts) + } + + private func startObservingContactChanges() { + NotificationCenter.default.addObserver(self, + selector: #selector(contactStoreDidChange), + name: .CNContactStoreDidChange, + object: nil) + } + + @objc + private func contactStoreDidChange() { + updateContacts() + } + +} diff --git a/Signal/src/views/ContactCell.swift b/Signal/src/views/ContactCell.swift index 6fdb4db91..9a6f4ba6b 100644 --- a/Signal/src/views/ContactCell.swift +++ b/Signal/src/views/ContactCell.swift @@ -1,25 +1,22 @@ -// Originally based on EPContacts // -// Created by Prabaharan Elangovan on 13/10/15. -// Copyright © 2015 Prabaharan Elangovan. All rights reserved. +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // -// Modified for Signal by Michael Kirk on 11/25/2016 -// Parts Copyright © 2016 Open Whisper Systems. All rights reserved. import UIKit +import Contacts @available(iOS 9.0, *) class ContactCell: UITableViewCell { static let nib = UINib(nibName:"ContactCell", bundle: nil) - + @IBOutlet weak var contactTextLabel: UILabel! @IBOutlet weak var contactDetailTextLabel: UILabel! @IBOutlet weak var contactImageView: UIImageView! @IBOutlet weak var contactContainerView: UIView! - + var contact: Contact? - + override func awakeFromNib() { super.awakeFromNib() @@ -28,7 +25,7 @@ class ContactCell: UITableViewCell { contactContainerView.layer.masksToBounds = true contactContainerView.layer.cornerRadius = contactContainerView.frame.size.width/2 - + NotificationCenter.default.addObserver(self, selector: #selector(self.didChangePreferredContentSize), name: NSNotification.Name.UIContentSizeCategoryDidChange, object: nil) } @@ -52,7 +49,7 @@ class ContactCell: UITableViewCell { if contactTextLabel != nil { contactTextLabel.attributedText = contact.cnContact?.formattedFullName(font:contactTextLabel.font) } - + updateSubtitleBasedonType(subtitleType, contact: contact) if contact.image == nil { @@ -66,15 +63,15 @@ class ContactCell: UITableViewCell { let avatarBuilder = OWSContactAvatarBuilder(contactId:contactIdForDeterminingBackgroundColor, name:contact.fullName, contactsManager:contactsManager) - self.contactImageView?.image = avatarBuilder.buildDefaultImage(); + self.contactImageView?.image = avatarBuilder.buildDefaultImage() } else { self.contactImageView?.image = contact.image } } - - func updateSubtitleBasedonType(_ subtitleType: SubtitleCellValue , contact: Contact) { + + func updateSubtitleBasedonType(_ subtitleType: SubtitleCellValue, contact: Contact) { switch subtitleType { - + case SubtitleCellValue.phoneNumber: if contact.userTextPhoneNumbers.count > 0 { self.contactDetailTextLabel.text = "\(contact.userTextPhoneNumbers[0])" @@ -106,7 +103,7 @@ fileprivate extension CNContact { if let attributedName = CNContactFormatter.attributedString(from: self, style: .fullName, defaultAttributes: nil) { let highlightedName = attributedName.mutableCopy() as! NSMutableAttributedString - highlightedName.enumerateAttributes(in: NSMakeRange(0, highlightedName.length), options: [], using: { (attrs, range, stop) in + highlightedName.enumerateAttributes(in: NSMakeRange(0, highlightedName.length), options: [], using: { (attrs, range, _) in if let property = attrs[CNContactPropertyAttribute] as? String, property == keyToHighlight { highlightedName.addAttributes(boldAttributes, range: range) } diff --git a/Signal/translations/bin/auto-genstrings b/Signal/translations/bin/auto-genstrings index 83241223e..cf113242f 100755 --- a/Signal/translations/bin/auto-genstrings +++ b/Signal/translations/bin/auto-genstrings @@ -8,8 +8,13 @@ pushd $SSK_DIR CURRENT_SSK_BRANCH=$(git status|awk 'NR==1{print $3}') if [ $CURRENT_SSK_BRANCH != "master" ] then - echo "[!] Error - SSK must be on master to be sure we're generating up-to-date strings" - exit 1 + if [[ $* == *--non-master* ]] + then + echo "[!] Note - generating from non-master SSK." + else + echo "[!] Error - SSK must be on master to be sure we're generating up-to-date strings, or use '--non-master'." + exit 1 + fi fi popd diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 63c6feabc..552b7e82a 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1,5 +1,5 @@ -/* No comment provided by engineer. */ -"AB_PERMISSION_MISSING_ACTION" = "Give access"; +/* Button text to dismiss missing contacts permission alert */ +"AB_PERMISSION_MISSING_ACTION_NOT_NOW" = "Not Now"; /* No comment provided by engineer. */ "AB_PERMISSION_MISSING_BODY" = "Signal requires access to your contacts. We do not store your contacts on our servers."; @@ -28,9 +28,6 @@ /* Alert body when contacts disabled */ "ADDRESSBOOK_RESTRICTED_ALERT_BODY" = "Signal requires access to your contacts. Access to contacts is restricted. Signal will close. You can disable the restriction temporarily to let Signal access your contacts by going the Settings app >> General >> Restrictions >> Contacts >> Allow Changes."; -/* No comment provided by engineer. */ -"ADDRESSBOOK_RESTRICTED_ALERT_BUTTON" = "Close"; - /* The label for the 'discard' button in alerts and action sheets. */ "ALERT_DISCARD_BUTTON" = "Discard"; @@ -928,12 +925,6 @@ /* No comment provided by engineer. */ "REGISTER_CC_ERR_ALERT_VIEW_TITLE" = "Country Code Error"; -/* No comment provided by engineer. */ -"REGISTER_CONTACTS_BODY" = "Signal allows you to have private conversations with your existing contacts. To use Signal, please allow access to your contacts."; - -/* No comment provided by engineer. */ -"REGISTER_CONTACTS_CONTINUE" = "Continue"; - /* No comment provided by engineer. */ "REGISTER_CONTACTS_WELCOME" = "Welcome!";