don't request contacts until necessary
Most commonly this will be after hitting the "compose" button But also we'll do it in the SignalViewController once you've received a message. - get rid blocking contacts nag - use Contacts framework simplifies logic - remove dead AB code // FREEBIE
This commit is contained in:
parent
931b6b4200
commit
b24cf29189
|
@ -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
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
453D28B81D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessagesBubblesSizeCalculator.h; sourceTree = "<group>"; };
|
||||
453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessagesBubblesSizeCalculator.m; sourceTree = "<group>"; };
|
||||
4542F0931EB9372700C7EE92 /* SystemContactsFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemContactsFetcher.swift; sourceTree = "<group>"; };
|
||||
45464DBB1DFA041F001D3FD6 /* DataChannelMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChannelMessage.swift; sourceTree = "<group>"; };
|
||||
454B35071D08EED80026D658 /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = translations/mk.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
45666EC41D99483D008FE134 /* OWSAvatarBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAvatarBuilder.h; sourceTree = "<group>"; };
|
||||
|
@ -1192,6 +1194,7 @@
|
|||
76EB040918170B33006006FC /* OWSContactsManager.m */,
|
||||
45843D1D1D2236B30013E85A /* OWSContactsSearcher.h */,
|
||||
45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */,
|
||||
4542F0931EB9372700C7EE92 /* SystemContactsFetcher.swift */,
|
||||
);
|
||||
path = contact;
|
||||
sourceTree = "<group>";
|
||||
|
@ -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)",
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#import "SignalAccount.h"
|
||||
#import <SignalServiceKit/Contact.h>
|
||||
#import <SignalServiceKit/OWSBlockingManager.h>
|
||||
#import <SignalServiceKit/PhoneNumber.h>
|
||||
#import <SignalServiceKit/TSAccountManager.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalServiceKit/PhoneNumber.h>
|
||||
#import <SignalServiceKit/TSAccountManager.h>
|
||||
#import <SignalServiceKit/TSContactThread.h>
|
||||
#import <SignalServiceKit/TSThread.h>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,12 +2,8 @@
|
|||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Contacts/Contacts.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <SignalServiceKit/ContactsManagerProtocol.h>
|
||||
#import <SignalServiceKit/PhoneNumber.h>
|
||||
#import "CollapsingFutures.h"
|
||||
#import "Contact.h"
|
||||
#import <SignalServiceKit/ContactsManagerProtocol.h>
|
||||
|
||||
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<Contact *> *)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;
|
||||
|
|
|
@ -4,21 +4,19 @@
|
|||
|
||||
#import "OWSContactsManager.h"
|
||||
#import "Environment.h"
|
||||
#import "Signal-Swift.h"
|
||||
#import "SignalAccount.h"
|
||||
#import "Util.h"
|
||||
#import <SignalServiceKit/ContactsUpdater.h>
|
||||
#import <SignalServiceKit/OWSError.h>
|
||||
|
||||
#define ADDRESSBOOK_QUEUE dispatch_get_main_queue()
|
||||
|
||||
typedef BOOL (^ContactSearchBlock)(id, NSUInteger, BOOL *);
|
||||
@import Contacts;
|
||||
|
||||
NSString *const OWSContactsManagerSignalAccountsDidChangeNotification =
|
||||
@"OWSContactsManagerSignalAccountsDidChangeNotification";
|
||||
|
||||
@interface OWSContactsManager ()
|
||||
@interface OWSContactsManager () <SystemContactsFetcherDelegate>
|
||||
|
||||
@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<NSString *, Contact *> *allContactsMap;
|
||||
@property (atomic) NSArray<SignalAccount *> *signalAccounts;
|
||||
@property (atomic) NSDictionary<NSString *, SignalAccount *> *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.
|
||||
}
|
||||
}];
|
||||
}
|
||||
#pragma mark - System Contact Fetching
|
||||
|
||||
[self setupAddressBookIfNecessary];
|
||||
}
|
||||
|
||||
- (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];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)handleAddressBookChanged
|
||||
// Request contacts access if you haven't asked recently.
|
||||
- (void)requestSystemContactsOnce
|
||||
{
|
||||
[self pullLatestAddressBook];
|
||||
[self.systemContactsFetcher requestOnce];
|
||||
}
|
||||
|
||||
#pragma mark - Setup
|
||||
|
||||
- (void)setupAddressBookIfNecessary
|
||||
- (void)fetchSystemContactsIfAlreadyAuthorized
|
||||
{
|
||||
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.systemContactsFetcher fetchIfAlreadyAuthorized];
|
||||
}
|
||||
|
||||
#pragma mark SystemContactsFetcherDelegate
|
||||
|
||||
- (void)systemContactsFetcher:(SystemContactsFetcher *)systemsContactsFetcher
|
||||
updatedContacts:(NSArray<Contact *> *)contacts
|
||||
{
|
||||
[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<Contact *> *contacts = [self getContactsFromAddressBook:addressBookRef];
|
||||
[self updateWithContacts:contacts];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)updateWithContacts:(NSArray<Contact *> *)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<Contact *> *)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<NSString *, NSNumber *> *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<NSString *, NSNumber *> *)phoneNumbersForRecord:(ABRecordRef)record
|
||||
{
|
||||
ABMultiValueRef phoneNumberRefs = NULL;
|
||||
|
||||
@try {
|
||||
phoneNumberRefs = ABRecordCopyValue(record, kABPersonPhoneProperty);
|
||||
|
||||
CFIndex phoneNumberCount = ABMultiValueGetCount(phoneNumberRefs);
|
||||
NSMutableDictionary<NSString *, NSNumber *> *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];
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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!";
|
||||
|
||||
|
|
Loading…
Reference in New Issue