session-ios/Signal/src/contact/ContactsManager.m

524 lines
19 KiB
Mathematica
Raw Normal View History

#import "ContactsManager.h"
#import "ContactsUpdater.h"
2014-05-06 19:41:08 +02:00
#import "Environment.h"
#import "Util.h"
#define ADDRESSBOOK_QUEUE dispatch_get_main_queue()
typedef BOOL (^ContactSearchBlock)(id, NSUInteger, BOOL *);
2014-05-06 19:41:08 +02:00
@interface ContactsManager () {
id addressBookReference;
}
@end
@implementation ContactsManager
- (id)init {
self = [super init];
if (self) {
life = [TOCCancelTokenSource new];
observableContactsController = [ObservableValueController observableValueControllerWithInitialValue:nil];
2014-05-06 19:41:08 +02:00
}
return self;
}
- (void)doAfterEnvironmentInitSetup {
2015-10-31 13:27:07 +01:00
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(_iOS_9)) {
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.
}
}];
2015-10-31 13:27:07 +01:00
}
2014-05-06 19:41:08 +02:00
[self setupAddressBook];
2014-05-06 19:41:08 +02:00
[observableContactsController watchLatestValueOnArbitraryThread:^(NSArray *latestContacts) {
@synchronized(self) {
[self setupLatestContacts:latestContacts];
}
}
untilCancelled:life.token];
2014-05-06 19:41:08 +02:00
}
- (void)dealloc {
2014-05-06 19:41:08 +02:00
[life cancel];
}
- (void)verifyABPermission {
if (!addressBookReference) {
[self setupAddressBook];
}
}
2014-05-06 19:41:08 +02:00
#pragma mark - Address Book callbacks
void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef info, void *context);
void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef info, void *context) {
ContactsManager *contactsManager = (__bridge ContactsManager *)context;
2014-05-06 19:41:08 +02:00
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[contactsManager pullLatestAddressBook];
[contactsManager intersectContacts];
2014-05-06 19:41:08 +02:00
});
}
#pragma mark - Setup
- (void)setupAddressBook {
2014-05-06 19:41:08 +02:00
dispatch_async(ADDRESSBOOK_QUEUE, ^{
[[ContactsManager asyncGetAddressBook] thenDo:^(id addressBook) {
addressBookReference = addressBook;
ABAddressBookRef cfAddressBook = (__bridge ABAddressBookRef)addressBook;
ABAddressBookRegisterExternalChangeCallback(cfAddressBook, onAddressBookChanged, (__bridge void *)self);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self pullLatestAddressBook];
[self intersectContacts];
});
}];
2014-05-06 19:41:08 +02:00
});
}
- (void)intersectContacts {
[[ContactsUpdater sharedUpdater] updateSignalContactIntersectionWithABContacts:self.allContacts
success:^{
}
failure:^(NSError *error) {
[NSTimer scheduledTimerWithTimeInterval:60
target:self
selector:@selector(intersectContacts)
userInfo:nil
repeats:NO];
}];
}
- (void)pullLatestAddressBook {
CFErrorRef creationError = nil;
2014-05-06 19:41:08 +02:00
ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, &creationError);
checkOperationDescribe(nil == creationError, [((__bridge NSError *)creationError)localizedDescription]);
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef error) {
if (!granted) {
[ContactsManager blockingContactDialog];
}
});
2014-05-06 19:41:08 +02:00
[observableContactsController updateValue:[self getContactsFromAddressBook:addressBookRef]];
}
- (void)setupLatestContacts:(NSArray *)contacts {
if (contacts) {
latestContactsById = [ContactsManager keyContactsById:contacts];
}
}
+ (void)blockingContactDialog {
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
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) {
exit(0);
}]];
[[UIApplication sharedApplication]
.keyWindow.rootViewController presentViewController:controller
animated:YES
completion:nil];
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
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];
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
break;
}
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
case kABAuthorizationStatusNotDetermined: {
DDLogInfo(@"AddressBook access not granted but status undetermined.");
[[Environment getCurrent].contactsManager pullLatestAddressBook];
break;
}
case kABAuthorizationStatusAuthorized: {
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
DDLogInfo(@"AddressBook access not granted but status authorized.");
break;
}
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
default:
break;
}
}
- (void)setupLatestRedPhoneUsers:(NSArray *)users {
2014-05-06 19:41:08 +02:00
if (users) {
latestWhisperUsersById = [ContactsManager keyContactsById:users];
}
}
#pragma mark - Observables
- (ObservableValue *)getObservableContacts {
2014-05-06 19:41:08 +02:00
return observableContactsController;
}
#pragma mark - Address Book utils
+ (TOCFuture *)asyncGetAddressBook {
CFErrorRef creationError = nil;
2014-05-06 19:41:08 +02:00
ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, &creationError);
assert((addressBookRef == nil) == (creationError != nil));
if (creationError != nil) {
[self blockingContactDialog];
return [TOCFuture futureWithFailure:(__bridge_transfer id)creationError];
2014-05-06 19:41:08 +02:00
}
TOCFutureSource *futureAddressBookSource = [TOCFutureSource new];
2014-05-06 19:41:08 +02:00
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];
}
2014-05-06 19:41:08 +02:00
});
return futureAddressBookSource.future;
2014-05-06 19:41:08 +02:00
}
- (NSArray *)getContactsFromAddressBook:(ABAddressBookRef)addressBook {
2014-07-31 15:50:24 +02:00
CFArrayRef allPeople = ABAddressBookCopyArrayOfAllPeople(addressBook);
CFMutableArrayRef allPeopleMutable =
CFArrayCreateMutableCopy(kCFAllocatorDefault, CFArrayGetCount(allPeople), allPeople);
CFArraySortValues(allPeopleMutable,
CFRangeMake(0, CFArrayGetCount(allPeopleMutable)),
2014-07-31 15:50:24 +02:00
(CFComparatorFunction)ABPersonComparePeopleByName,
(void *)(unsigned long)ABPersonGetSortOrdering());
2014-07-31 15:50:24 +02:00
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;
}];
2014-07-31 15:50:24 +02:00
CFRelease(allPeople);
NSArray *filteredContacts = [sortedPeople filteredArrayUsingPredicate:predicate];
return [filteredContacts map:^id(id item) {
return [self contactForRecord:(__bridge ABRecordRef)item];
2014-05-06 19:41:08 +02:00
}];
}
- (NSArray *)latestContactsWithSearchString:(NSString *)searchString {
return [latestContactsById.allValues filter:^int(Contact *contact) {
return searchString.length == 0 || [ContactsManager name:contact.fullName matchesQuery:searchString];
2014-05-06 19:41:08 +02:00
}];
}
#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);
2014-05-06 19:41:08 +02:00
NSArray *phoneNumbers = [self phoneNumbersForRecord:record];
2014-05-06 19:41:08 +02:00
if (!firstName && !lastName) {
NSString *companyName = (__bridge_transfer NSString *)ABRecordCopyValue(record, kABPersonOrganizationProperty);
2014-05-06 19:41:08 +02:00
if (companyName) {
firstName = companyName;
2014-08-14 03:13:24 +02:00
} else if (phoneNumbers.count) {
firstName = phoneNumbers.firstObject;
2014-05-06 19:41:08 +02:00
}
}
// NSString *notes = (__bridge_transfer NSString *)ABRecordCopyValue(record, kABPersonNoteProperty);
// NSArray *emails = [ContactsManager emailsForRecord:record];
// NSData *image = (__bridge_transfer NSData *)ABPersonCopyImageDataWithFormat(record,
// kABPersonImageFormatThumbnail);
// UIImage *img = [UIImage imageWithData:image];
return [[Contact alloc] initWithContactWithFirstName:firstName
andLastName:lastName
andUserTextPhoneNumbers:phoneNumbers
andContactID:recordID];
}
- (Contact *)latestContactForPhoneNumber:(PhoneNumber *)phoneNumber {
NSArray *allContacts = [self allContacts];
2014-05-06 19:41:08 +02:00
ContactSearchBlock searchBlock = ^BOOL(Contact *contact, NSUInteger idx, BOOL *stop) {
for (PhoneNumber *number in contact.parsedPhoneNumbers) {
if ([self phoneNumber:number matchesNumber:phoneNumber]) {
*stop = YES;
return YES;
}
}
return NO;
2014-05-06 19:41:08 +02:00
};
2014-05-06 19:41:08 +02:00
NSUInteger contactIndex = [allContacts indexOfObjectPassingTest:searchBlock];
2014-05-06 19:41:08 +02:00
if (contactIndex != NSNotFound) {
return allContacts[contactIndex];
} else {
return nil;
}
}
- (BOOL)phoneNumber:(PhoneNumber *)phoneNumber1 matchesNumber:(PhoneNumber *)phoneNumber2 {
return [phoneNumber1.toE164 isEqualToString:phoneNumber2.toE164];
2014-05-06 19:41:08 +02:00
}
- (NSArray *)phoneNumbersForRecord:(ABRecordRef)record {
ABMultiValueRef numberRefs = ABRecordCopyValue(record, kABPersonPhoneProperty);
2014-05-06 19:41:08 +02:00
@try {
NSArray *phoneNumbers = (__bridge_transfer NSArray *)ABMultiValueCopyArrayOfAllValues(numberRefs);
if (phoneNumbers == nil)
phoneNumbers = @[];
2014-05-06 19:41:08 +02:00
NSMutableArray *numbers = [NSMutableArray array];
2014-08-14 03:13:24 +02:00
for (NSUInteger i = 0; i < phoneNumbers.count; i++) {
NSString *phoneNumber = phoneNumbers[i];
2014-05-06 19:41:08 +02:00
[numbers addObject:phoneNumber];
}
2014-05-06 19:41:08 +02:00
return numbers;
2014-05-06 19:41:08 +02:00
} @finally {
if (numberRefs) {
CFRelease(numberRefs);
}
}
}
+ (NSArray *)emailsForRecord:(ABRecordRef)record {
2014-05-06 19:41:08 +02:00
ABMultiValueRef emailRefs = ABRecordCopyValue(record, kABPersonEmailProperty);
2014-05-06 19:41:08 +02:00
@try {
NSArray *emails = (__bridge_transfer NSArray *)ABMultiValueCopyArrayOfAllValues(emailRefs);
if (emails == nil)
emails = @[];
2014-05-06 19:41:08 +02:00
return emails;
2014-05-06 19:41:08 +02:00
} @finally {
if (emailRefs) {
CFRelease(emailRefs);
}
}
}
+ (NSDictionary *)groupContactsByFirstLetter:(NSArray *)contacts matchingSearchString:(NSString *)optionalSearchString {
assert(contacts != nil);
2014-05-06 19:41:08 +02:00
NSArray *matchingContacts = [contacts filter:^int(Contact *contact) {
return optionalSearchString.length == 0 || [self name:contact.fullName matchesQuery:optionalSearchString];
2014-05-06 19:41:08 +02:00
}];
2014-05-06 19:41:08 +02:00
return [matchingContacts groupBy:^id(Contact *contact) {
NSString *nameToUse = @"";
BOOL firstNameOrdering = ABPersonGetSortOrdering() == kABPersonCompositeNameFormatFirstNameFirst ? YES : NO;
if (firstNameOrdering && contact.firstName != nil && contact.firstName.length > 0) {
nameToUse = contact.firstName;
} else if (!firstNameOrdering && contact.lastName != nil && contact.lastName.length > 0) {
nameToUse = contact.lastName;
} else if (contact.lastName == nil) {
if (contact.fullName.length > 0) {
nameToUse = contact.fullName;
} else {
return nameToUse;
}
} else {
nameToUse = contact.lastName;
}
if (nameToUse.length >= 1) {
return [[[nameToUse substringToIndex:1] uppercaseString] decomposedStringWithCompatibilityMapping];
} else {
return @" ";
}
2014-05-06 19:41:08 +02:00
}];
}
+ (NSDictionary *)keyContactsById:(NSArray *)contacts {
return [contacts keyedBy:^id(Contact *contact) {
return @((int)contact.recordID);
2014-05-06 19:41:08 +02:00
}];
}
- (Contact *)latestContactWithRecordId:(ABRecordID)recordId {
2014-05-06 19:41:08 +02:00
@synchronized(self) {
return latestContactsById[@(recordId)];
2014-05-06 19:41:08 +02:00
}
}
- (NSArray<Contact *> *)allContacts {
NSMutableArray *allContacts = [NSMutableArray array];
for (NSString *key in latestContactsById.allKeys) {
Contact *contact = [latestContactsById objectForKey:key];
if ([contact isKindOfClass:[Contact class]]) {
[allContacts addObject:contact];
}
}
return allContacts;
}
- (NSArray *)recordsForContacts:(NSArray *)contacts {
2014-05-06 19:41:08 +02:00
return [contacts map:^id(Contact *contact) {
return @([contact recordID]);
2014-05-06 19:41:08 +02:00
}];
}
+ (BOOL)name:(NSString *)nameString matchesQuery:(NSString *)queryString {
NSCharacterSet *whitespaceSet = NSCharacterSet.whitespaceCharacterSet;
NSArray *queryStrings = [queryString componentsSeparatedByCharactersInSet:whitespaceSet];
NSArray *nameStrings = [nameString componentsSeparatedByCharactersInSet:whitespaceSet];
return [queryStrings all:^int(NSString *query) {
if (query.length == 0)
return YES;
return [nameStrings any:^int(NSString *nameWord) {
NSStringCompareOptions searchOpts = NSCaseInsensitiveSearch | NSAnchoredSearch;
return [nameWord rangeOfString:query options:searchOpts].location != NSNotFound;
}];
2014-05-06 19:41:08 +02:00
}];
}
+ (BOOL)phoneNumber:(PhoneNumber *)phoneNumber matchesQuery:(NSString *)queryString {
NSString *phoneNumberString = phoneNumber.localizedDescriptionForUser;
NSString *searchString = phoneNumberString.digitsOnly;
if (queryString.length == 0)
return YES;
2014-05-06 19:41:08 +02:00
NSStringCompareOptions searchOpts = NSCaseInsensitiveSearch | NSAnchoredSearch;
return [searchString rangeOfString:queryString options:searchOpts].location != NSNotFound;
}
- (NSArray *)contactsForContactIds:(NSArray *)contactIds {
2014-05-06 19:41:08 +02:00
NSMutableArray *contacts = [NSMutableArray array];
for (NSNumber *favouriteId in contactIds) {
Contact *contact = [self latestContactWithRecordId:favouriteId.intValue];
2014-05-06 19:41:08 +02:00
if (contact) {
[contacts addObject:contact];
}
}
return [contacts copy];
}
#pragma mark - Whisper User Management
- (NSArray *)getSignalUsersFromContactsArray:(NSArray *)contacts {
return [[contacts filter:^int(Contact *contact) {
return contact.isRedPhoneContact || contact.isTextSecureContact;
}] sortedArrayUsingComparator:[[self class] contactComparator]];
}
+ (NSComparator)contactComparator {
return ^NSComparisonResult(id obj1, id obj2) {
Contact *contact1 = (Contact *)obj1;
Contact *contact2 = (Contact *)obj2;
BOOL firstNameOrdering = ABPersonGetSortOrdering() == kABPersonCompositeNameFormatFirstNameFirst ? YES : NO;
if (firstNameOrdering) {
return [contact1.firstName caseInsensitiveCompare:contact2.firstName];
} else {
return [contact1.lastName caseInsensitiveCompare:contact2.lastName];
};
};
2014-05-06 19:41:08 +02:00
}
- (NSArray *)signalContacts {
return [self getSignalUsersFromContactsArray:self.allContacts];
}
- (NSArray *)textSecureContacts {
return [[self.allContacts filter:^int(Contact *contact) {
return [contact isTextSecureContact];
}] sortedArrayUsingComparator:[[self class] contactComparator]];
2014-05-06 19:41:08 +02:00
}
- (NSArray *)getNewItemsFrom:(NSArray *)newArray comparedTo:(NSArray *)oldArray {
NSMutableSet *newSet = [NSMutableSet setWithArray:newArray];
NSSet *oldSet = [NSSet setWithArray:oldArray];
[newSet minusSet:oldSet];
return newSet.allObjects;
2014-05-06 19:41:08 +02:00
}
- (NSString *)nameStringForPhoneIdentifier:(NSString *)identifier {
for (Contact *contact in self.allContacts) {
for (PhoneNumber *phoneNumber in contact.parsedPhoneNumbers) {
if ([phoneNumber.toE164 isEqualToString:identifier]) {
return contact.fullName;
}
}
}
return nil;
}
- (UIImage *)imageForPhoneIdentifier:(NSString *)identifier {
for (Contact *contact in self.allContacts) {
2014-11-25 19:06:09 +01:00
for (PhoneNumber *phoneNumber in contact.parsedPhoneNumbers) {
if ([phoneNumber.toE164 isEqualToString:identifier]) {
return contact.image;
}
}
}
return nil;
}
2014-05-06 19:41:08 +02:00
@end