2017-02-08 20:25:31 +01:00
|
|
|
//
|
2018-02-16 02:45:28 +01:00
|
|
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
2017-02-08 20:25:31 +01:00
|
|
|
//
|
|
|
|
|
2016-06-27 23:32:35 +02:00
|
|
|
#import "OWSContactsManager.h"
|
2014-05-06 19:41:08 +02:00
|
|
|
#import "Environment.h"
|
2018-07-02 15:42:48 +02:00
|
|
|
#import "NSAttributedString+OWS.h"
|
2017-12-01 23:10:14 +01:00
|
|
|
#import "OWSFormat.h"
|
2017-08-02 19:12:26 +02:00
|
|
|
#import "OWSProfileManager.h"
|
2017-12-08 19:45:24 +01:00
|
|
|
#import "OWSUserProfile.h"
|
2017-07-12 16:46:54 +02:00
|
|
|
#import "ViewControllerUtils.h"
|
2017-12-01 23:10:14 +01:00
|
|
|
#import <SignalMessaging/SignalMessaging-Swift.h>
|
2017-12-04 18:38:44 +01:00
|
|
|
#import <SignalMessaging/UIColor+OWS.h>
|
|
|
|
#import <SignalMessaging/UIFont+OWS.h>
|
2017-04-28 18:18:42 +02:00
|
|
|
#import <SignalServiceKit/ContactsUpdater.h>
|
2017-09-18 15:40:28 +02:00
|
|
|
#import <SignalServiceKit/NSNotificationCenter+OWS.h>
|
2018-02-16 02:45:28 +01:00
|
|
|
#import <SignalServiceKit/NSString+SSK.h>
|
2017-04-13 21:38:32 +02:00
|
|
|
#import <SignalServiceKit/OWSError.h>
|
2018-03-05 15:30:58 +01:00
|
|
|
#import <SignalServiceKit/OWSPrimaryStorage.h>
|
2017-12-01 23:10:14 +01:00
|
|
|
#import <SignalServiceKit/PhoneNumber.h>
|
2017-05-05 18:33:10 +02:00
|
|
|
#import <SignalServiceKit/SignalAccount.h>
|
2014-05-06 19:41:08 +02:00
|
|
|
|
2017-05-01 20:28:37 +02:00
|
|
|
@import Contacts;
|
2014-05-06 19:41:08 +02:00
|
|
|
|
2018-07-16 21:46:48 +02:00
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
2017-10-02 17:47:46 +02:00
|
|
|
NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
|
|
|
|
= @"OWSContactsManagerSignalAccountsDidChangeNotification";
|
2017-02-08 17:59:02 +01:00
|
|
|
|
2018-07-16 21:46:48 +02:00
|
|
|
NSString *const OWSContactsManagerCollection = @"OWSContactsManagerCollection";
|
|
|
|
NSString *const OWSContactsManagerKeyLastKnownContactPhoneNumbers
|
|
|
|
= @"OWSContactsManagerKeyLastKnownContactPhoneNumbers";
|
|
|
|
NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsManagerKeyNextFullIntersectionDate2";
|
|
|
|
|
2017-05-01 20:28:37 +02:00
|
|
|
@interface OWSContactsManager () <SystemContactsFetcherDelegate>
|
2016-06-16 22:54:33 +02:00
|
|
|
|
2017-04-13 15:43:37 +02:00
|
|
|
@property (nonatomic) BOOL isContactsUpdateInFlight;
|
2017-05-01 18:51:59 +02:00
|
|
|
// This reflects the contents of the device phone book and includes
|
|
|
|
// contacts that do not correspond to any signal account.
|
|
|
|
@property (atomic) NSArray<Contact *> *allContacts;
|
|
|
|
@property (atomic) NSDictionary<NSString *, Contact *> *allContactsMap;
|
|
|
|
@property (atomic) NSArray<SignalAccount *> *signalAccounts;
|
|
|
|
@property (atomic) NSDictionary<NSString *, SignalAccount *> *signalAccountMap;
|
2017-05-01 20:28:37 +02:00
|
|
|
@property (nonatomic, readonly) SystemContactsFetcher *systemContactsFetcher;
|
2017-12-14 01:08:47 +01:00
|
|
|
@property (nonatomic, readonly) YapDatabaseConnection *dbReadConnection;
|
|
|
|
@property (nonatomic, readonly) YapDatabaseConnection *dbWriteConnection;
|
2018-06-20 22:22:17 +02:00
|
|
|
@property (nonatomic, readonly) NSCache<NSString *, CNContact *> *cnContactCache;
|
|
|
|
@property (nonatomic, readonly) NSCache<NSString *, UIImage *> *cnContactAvatarCache;
|
2017-05-24 17:06:46 +02:00
|
|
|
|
2014-05-06 19:41:08 +02:00
|
|
|
@end
|
|
|
|
|
2018-07-16 21:46:48 +02:00
|
|
|
#pragma mark -
|
|
|
|
|
2016-06-27 23:32:35 +02:00
|
|
|
@implementation OWSContactsManager
|
2014-05-06 19:41:08 +02:00
|
|
|
|
2017-12-01 23:10:14 +01:00
|
|
|
- (id)init
|
|
|
|
{
|
2014-05-06 19:41:08 +02:00
|
|
|
self = [super init];
|
2016-11-18 23:11:56 +01:00
|
|
|
if (!self) {
|
|
|
|
return self;
|
2014-05-06 19:41:08 +02:00
|
|
|
}
|
2016-11-18 23:11:56 +01:00
|
|
|
|
2017-05-23 17:27:29 +02:00
|
|
|
// TODO: We need to configure the limits of this cache.
|
2017-08-25 00:47:11 +02:00
|
|
|
_avatarCache = [ImageCache new];
|
2017-12-14 17:43:27 +01:00
|
|
|
|
2018-03-05 15:30:58 +01:00
|
|
|
_dbReadConnection = [OWSPrimaryStorage sharedManager].newDatabaseConnection;
|
|
|
|
_dbWriteConnection = [OWSPrimaryStorage sharedManager].newDatabaseConnection;
|
2017-12-14 17:43:27 +01:00
|
|
|
|
2017-05-01 18:51:59 +02:00
|
|
|
_allContacts = @[];
|
2017-10-02 17:31:59 +02:00
|
|
|
_allContactsMap = @{};
|
2017-05-01 18:51:59 +02:00
|
|
|
_signalAccountMap = @{};
|
|
|
|
_signalAccounts = @[];
|
2017-05-01 20:28:37 +02:00
|
|
|
_systemContactsFetcher = [SystemContactsFetcher new];
|
|
|
|
_systemContactsFetcher.delegate = self;
|
2018-06-20 22:22:17 +02:00
|
|
|
_cnContactCache = [NSCache new];
|
|
|
|
_cnContactCache.countLimit = 50;
|
|
|
|
_cnContactAvatarCache = [NSCache new];
|
|
|
|
_cnContactAvatarCache.countLimit = 25;
|
2017-12-14 17:43:27 +01:00
|
|
|
|
2017-04-01 00:45:51 +02:00
|
|
|
OWSSingletonAssert();
|
|
|
|
|
2014-05-06 19:41:08 +02:00
|
|
|
return self;
|
|
|
|
}
|
2015-08-24 01:47:25 +02:00
|
|
|
|
2017-12-14 01:08:47 +01:00
|
|
|
- (void)loadSignalAccountsFromCache
|
2017-10-02 17:47:46 +02:00
|
|
|
{
|
2017-12-14 01:08:47 +01:00
|
|
|
__block NSMutableArray<SignalAccount *> *signalAccounts;
|
2017-12-14 17:43:27 +01:00
|
|
|
[self.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
|
2017-12-15 22:15:25 +01:00
|
|
|
NSUInteger signalAccountCount = [SignalAccount numberOfKeysInCollectionWithTransaction:transaction];
|
|
|
|
DDLogInfo(@"%@ loading %lu signal accounts from cache.", self.logTag, (unsigned long)signalAccountCount);
|
2017-12-14 17:43:27 +01:00
|
|
|
|
2017-12-15 22:15:25 +01:00
|
|
|
signalAccounts = [[NSMutableArray alloc] initWithCapacity:signalAccountCount];
|
|
|
|
|
2017-12-14 01:08:47 +01:00
|
|
|
[SignalAccount enumerateCollectionObjectsWithTransaction:transaction usingBlock:^(SignalAccount *signalAccount, BOOL * _Nonnull stop) {
|
|
|
|
[signalAccounts addObject:signalAccount];
|
|
|
|
}];
|
2017-10-02 17:47:46 +02:00
|
|
|
}];
|
2017-12-16 18:26:47 +01:00
|
|
|
[signalAccounts sortUsingComparator:self.signalAccountComparator];
|
2017-12-14 17:43:27 +01:00
|
|
|
|
2017-12-14 01:08:47 +01:00
|
|
|
[self updateSignalAccounts:signalAccounts];
|
2017-10-02 17:47:46 +02:00
|
|
|
}
|
|
|
|
|
2017-12-15 23:36:06 +01:00
|
|
|
- (dispatch_queue_t)serialQueue
|
|
|
|
{
|
|
|
|
static dispatch_queue_t _serialQueue;
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
_serialQueue = dispatch_queue_create("org.whispersystems.contacts.buildSignalAccount", DISPATCH_QUEUE_SERIAL);
|
|
|
|
});
|
|
|
|
|
|
|
|
return _serialQueue;
|
|
|
|
}
|
|
|
|
|
2017-05-01 20:28:37 +02:00
|
|
|
#pragma mark - System Contact Fetching
|
2014-05-06 19:41:08 +02:00
|
|
|
|
2017-05-01 20:28:37 +02:00
|
|
|
// Request contacts access if you haven't asked recently.
|
|
|
|
- (void)requestSystemContactsOnce
|
|
|
|
{
|
2017-05-08 22:28:01 +02:00
|
|
|
[self requestSystemContactsOnceWithCompletion:nil];
|
2014-05-06 19:41:08 +02:00
|
|
|
}
|
|
|
|
|
2018-07-17 17:36:21 +02:00
|
|
|
- (void)requestSystemContactsOnceWithCompletion:(void (^_Nullable)(NSError *_Nullable error))completion
|
2017-05-08 22:28:01 +02:00
|
|
|
{
|
|
|
|
[self.systemContactsFetcher requestOnceWithCompletion:completion];
|
|
|
|
}
|
|
|
|
|
2017-10-25 15:18:27 +02:00
|
|
|
- (void)fetchSystemContactsOnceIfAlreadyAuthorized
|
2016-11-18 23:11:56 +01:00
|
|
|
{
|
2017-10-25 15:18:27 +02:00
|
|
|
[self.systemContactsFetcher fetchOnceIfAlreadyAuthorized];
|
2017-09-11 16:53:28 +02:00
|
|
|
}
|
|
|
|
|
2018-07-17 17:36:21 +02:00
|
|
|
- (void)userRequestedSystemContactsRefreshWithCompletion:(void (^)(NSError *_Nullable error))completionHandler
|
2017-09-11 16:53:28 +02:00
|
|
|
{
|
2017-12-15 23:36:06 +01:00
|
|
|
[self.systemContactsFetcher userRequestedRefreshWithCompletion:completionHandler];
|
2016-11-18 23:11:56 +01:00
|
|
|
}
|
|
|
|
|
2017-05-03 23:03:26 +02:00
|
|
|
- (BOOL)isSystemContactsAuthorized
|
|
|
|
{
|
|
|
|
return self.systemContactsFetcher.isAuthorized;
|
|
|
|
}
|
|
|
|
|
2018-06-22 19:50:24 +02:00
|
|
|
- (BOOL)isSystemContactsDenied
|
|
|
|
{
|
|
|
|
return self.systemContactsFetcher.isDenied;
|
|
|
|
}
|
|
|
|
|
2017-11-17 16:20:00 +01:00
|
|
|
- (BOOL)systemContactsHaveBeenRequestedAtLeastOnce
|
|
|
|
{
|
|
|
|
return self.systemContactsFetcher.systemContactsHaveBeenRequestedAtLeastOnce;
|
|
|
|
}
|
|
|
|
|
2017-05-18 14:41:28 +02:00
|
|
|
- (BOOL)supportsContactEditing
|
|
|
|
{
|
|
|
|
return self.systemContactsFetcher.supportsContactEditing;
|
|
|
|
}
|
|
|
|
|
2018-06-20 17:41:14 +02:00
|
|
|
#pragma mark - CNContacts
|
|
|
|
|
2018-06-18 23:20:27 +02:00
|
|
|
- (nullable CNContact *)cnContactWithId:(nullable NSString *)contactId
|
2018-06-18 22:52:46 +02:00
|
|
|
{
|
2018-06-18 23:20:27 +02:00
|
|
|
OWSAssert(self.cnContactCache);
|
2018-06-18 22:52:46 +02:00
|
|
|
|
2018-06-18 23:20:27 +02:00
|
|
|
if (!contactId) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
2018-07-17 17:36:21 +02:00
|
|
|
CNContact *_Nullable cnContact;
|
2018-06-19 15:08:38 +02:00
|
|
|
@synchronized(self.cnContactCache) {
|
2018-06-20 22:22:17 +02:00
|
|
|
cnContact = [self.cnContactCache objectForKey:contactId];
|
2018-06-19 15:08:38 +02:00
|
|
|
if (!cnContact) {
|
|
|
|
cnContact = [self.systemContactsFetcher fetchCNContactWithContactId:contactId];
|
|
|
|
if (cnContact) {
|
2018-06-20 22:22:17 +02:00
|
|
|
[self.cnContactCache setObject:cnContact forKey:contactId];
|
2018-06-19 15:08:38 +02:00
|
|
|
}
|
2018-06-18 23:20:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return cnContact;
|
2018-06-18 22:52:46 +02:00
|
|
|
}
|
|
|
|
|
2018-06-20 17:41:14 +02:00
|
|
|
- (nullable NSData *)avatarDataForCNContactId:(nullable NSString *)contactId
|
|
|
|
{
|
|
|
|
// Don't bother to cache avatar data.
|
2018-07-17 17:36:21 +02:00
|
|
|
CNContact *_Nullable cnContact = [self cnContactWithId:contactId];
|
2018-06-20 17:41:14 +02:00
|
|
|
return [Contact avatarDataForCNContact:cnContact];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (nullable UIImage *)avatarImageForCNContactId:(nullable NSString *)contactId
|
|
|
|
{
|
|
|
|
OWSAssert(self.cnContactAvatarCache);
|
|
|
|
|
|
|
|
if (!contactId) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
2018-07-17 17:36:21 +02:00
|
|
|
UIImage *_Nullable avatarImage;
|
2018-06-20 17:41:14 +02:00
|
|
|
@synchronized(self.cnContactAvatarCache) {
|
2018-06-20 22:22:17 +02:00
|
|
|
avatarImage = [self.cnContactAvatarCache objectForKey:contactId];
|
2018-06-20 17:41:14 +02:00
|
|
|
if (!avatarImage) {
|
2018-07-17 17:36:21 +02:00
|
|
|
NSData *_Nullable avatarData = [self avatarDataForCNContactId:contactId];
|
2018-06-20 17:41:14 +02:00
|
|
|
if (avatarData) {
|
|
|
|
avatarImage = [UIImage imageWithData:avatarData];
|
|
|
|
}
|
|
|
|
if (avatarImage) {
|
2018-06-20 22:22:17 +02:00
|
|
|
[self.cnContactAvatarCache setObject:avatarImage forKey:contactId];
|
2018-06-20 17:41:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return avatarImage;
|
|
|
|
}
|
|
|
|
|
2018-05-29 22:05:36 +02:00
|
|
|
#pragma mark - SystemContactsFetcherDelegate
|
2014-05-06 19:41:08 +02:00
|
|
|
|
2017-05-01 20:28:37 +02:00
|
|
|
- (void)systemContactsFetcher:(SystemContactsFetcher *)systemsContactsFetcher
|
|
|
|
updatedContacts:(NSArray<Contact *> *)contacts
|
2017-12-16 19:21:34 +01:00
|
|
|
isUserRequested:(BOOL)isUserRequested
|
2017-04-13 15:43:37 +02:00
|
|
|
{
|
2018-05-30 16:52:53 +02:00
|
|
|
BOOL shouldClearStaleCache;
|
|
|
|
// On iOS 11.2, only clear the contacts cache if the fetch was initiated by the user.
|
|
|
|
// iOS 11.2 rarely returns partial fetches and we use the cache to prevent contacts from
|
|
|
|
// periodically disappearing from the UI.
|
|
|
|
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(11, 2) && !SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(11, 3)) {
|
|
|
|
shouldClearStaleCache = isUserRequested;
|
|
|
|
} else {
|
|
|
|
shouldClearStaleCache = YES;
|
|
|
|
}
|
2018-07-16 21:46:48 +02:00
|
|
|
[self updateWithContacts:contacts isUserRequested:isUserRequested shouldClearStaleCache:shouldClearStaleCache];
|
2014-05-06 19:41:08 +02:00
|
|
|
}
|
|
|
|
|
2018-05-29 22:05:36 +02:00
|
|
|
- (void)systemContactsFetcher:(SystemContactsFetcher *)systemContactsFetcher
|
|
|
|
hasAuthorizationStatus:(enum ContactStoreAuthorizationStatus)authorizationStatus
|
|
|
|
{
|
|
|
|
if (authorizationStatus == ContactStoreAuthorizationStatusRestricted
|
|
|
|
|| authorizationStatus == ContactStoreAuthorizationStatusDenied) {
|
|
|
|
// Clear the contacts cache if access to the system contacts is revoked.
|
2018-07-16 21:46:48 +02:00
|
|
|
[self updateWithContacts:@[] isUserRequested:NO shouldClearStaleCache:YES];
|
2018-05-29 22:05:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-01 20:28:37 +02:00
|
|
|
#pragma mark - Intersection
|
|
|
|
|
2018-07-16 21:46:48 +02:00
|
|
|
- (NSSet<NSString *> *)recipientIdsForIntersectionWithContacts:(NSArray<Contact *> *)contacts
|
|
|
|
{
|
|
|
|
OWSAssert(contacts);
|
|
|
|
|
|
|
|
NSMutableSet<NSString *> *recipientIds = [NSMutableSet set];
|
|
|
|
|
|
|
|
for (Contact *contact in contacts) {
|
|
|
|
for (PhoneNumber *phoneNumber in contact.parsedPhoneNumbers) {
|
|
|
|
[recipientIds addObject:phoneNumber.toE164];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return recipientIds;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)intersectContacts:(NSArray<Contact *> *)contacts
|
|
|
|
isUserRequested:(BOOL)isUserRequested
|
|
|
|
completion:(void (^)(NSError *_Nullable error))completion
|
|
|
|
{
|
|
|
|
OWSAssert(contacts);
|
|
|
|
OWSAssert(completion);
|
|
|
|
|
|
|
|
dispatch_async(self.serialQueue, ^{
|
|
|
|
__block BOOL isFullIntersection = YES;
|
|
|
|
__block NSSet<NSString *> *allContactRecipientIds;
|
|
|
|
__block NSSet<NSString *> *recipientIdsForIntersection;
|
|
|
|
[self.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
|
|
|
// Contact updates initiated by the user should always do a full intersection.
|
|
|
|
if (!isUserRequested) {
|
|
|
|
NSDate *_Nullable nextFullIntersectionDate =
|
|
|
|
[transaction dateForKey:OWSContactsManagerKeyNextFullIntersectionDate
|
|
|
|
inCollection:OWSContactsManagerCollection];
|
|
|
|
if (nextFullIntersectionDate && [nextFullIntersectionDate isAfterNow]) {
|
|
|
|
isFullIntersection = NO;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
allContactRecipientIds = [self recipientIdsForIntersectionWithContacts:contacts];
|
|
|
|
recipientIdsForIntersection = allContactRecipientIds;
|
|
|
|
|
|
|
|
if (!isFullIntersection) {
|
2018-07-17 17:40:34 +02:00
|
|
|
// Do a "delta" intersection instead of a "full" intersection:
|
|
|
|
// only intersect new contacts which were not in the last successful
|
|
|
|
// "full" intersection.
|
2018-07-16 21:46:48 +02:00
|
|
|
NSSet<NSString *> *_Nullable lastKnownContactPhoneNumbers =
|
|
|
|
[transaction objectForKey:OWSContactsManagerKeyLastKnownContactPhoneNumbers
|
|
|
|
inCollection:OWSContactsManagerCollection];
|
|
|
|
if (lastKnownContactPhoneNumbers) {
|
|
|
|
// Do a "delta" sync which only intersects recipient ids not included
|
|
|
|
// in the last full intersection.
|
|
|
|
NSMutableSet<NSString *> *newRecipientIds = [allContactRecipientIds mutableCopy];
|
|
|
|
[newRecipientIds minusSet:lastKnownContactPhoneNumbers];
|
|
|
|
recipientIdsForIntersection = newRecipientIds;
|
|
|
|
} else {
|
|
|
|
// Without a list of "last known" contact phone numbers, we'll have to do a full intersection.
|
|
|
|
isFullIntersection = YES;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
OWSAssert(recipientIdsForIntersection);
|
|
|
|
|
|
|
|
if (recipientIdsForIntersection.count < 1) {
|
|
|
|
DDLogInfo(@"%@ Skipping intersection; no contacts to intersect.", self.logTag);
|
|
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
|
|
completion(nil);
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
} else if (isFullIntersection) {
|
2018-07-19 17:31:04 +02:00
|
|
|
DDLogInfo(@"%@ Doing full intersection with %zu contacts.", self.logTag, recipientIdsForIntersection.count);
|
2018-07-16 21:46:48 +02:00
|
|
|
} else {
|
|
|
|
DDLogInfo(
|
2018-07-19 17:31:04 +02:00
|
|
|
@"%@ Doing delta intersection with %zu contacts.", self.logTag, recipientIdsForIntersection.count);
|
2018-07-16 21:46:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
[self intersectContacts:recipientIdsForIntersection
|
|
|
|
retryDelaySeconds:1.0
|
|
|
|
success:^(NSSet<SignalRecipient *> *registeredRecipients) {
|
|
|
|
[self markIntersectionAsComplete:allContactRecipientIds isFullIntersection:isFullIntersection];
|
|
|
|
|
|
|
|
completion(nil);
|
|
|
|
}
|
|
|
|
failure:^(NSError *error) {
|
|
|
|
completion(error);
|
|
|
|
}];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)markIntersectionAsComplete:(NSSet<NSString *> *)recipientIdsForIntersection
|
|
|
|
isFullIntersection:(BOOL)isFullIntersection
|
2017-04-13 21:38:32 +02:00
|
|
|
{
|
2018-07-16 21:46:48 +02:00
|
|
|
OWSAssert(recipientIdsForIntersection.count > 0);
|
|
|
|
|
|
|
|
dispatch_async(self.serialQueue, ^{
|
|
|
|
[self.dbReadConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
|
|
|
[transaction setObject:recipientIdsForIntersection
|
|
|
|
forKey:OWSContactsManagerKeyLastKnownContactPhoneNumbers
|
|
|
|
inCollection:OWSContactsManagerCollection];
|
|
|
|
|
|
|
|
if (isFullIntersection) {
|
|
|
|
// Don't do a full intersection more often than once every 6 hours.
|
2018-07-19 17:31:04 +02:00
|
|
|
const NSTimeInterval kMinFullIntersectionInterval = 6 * kHourInterval;
|
|
|
|
NSDate *nextFullIntersectionDate = [NSDate
|
|
|
|
dateWithTimeIntervalSince1970:[NSDate new].timeIntervalSince1970 + kMinFullIntersectionInterval];
|
2018-07-16 21:46:48 +02:00
|
|
|
[transaction setDate:nextFullIntersectionDate
|
|
|
|
forKey:OWSContactsManagerKeyNextFullIntersectionDate
|
|
|
|
inCollection:OWSContactsManagerCollection];
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
});
|
2017-04-13 21:38:32 +02:00
|
|
|
}
|
|
|
|
|
2018-07-16 21:46:48 +02:00
|
|
|
- (void)intersectContacts:(NSSet<NSString *> *)recipientIds
|
|
|
|
retryDelaySeconds:(double)retryDelaySeconds
|
|
|
|
success:(void (^)(NSSet<SignalRecipient *> *))successParameter
|
|
|
|
failure:(void (^)(NSError *))failureParameter
|
2017-04-13 15:43:37 +02:00
|
|
|
{
|
2018-07-16 21:46:48 +02:00
|
|
|
OWSAssert(recipientIds.count > 0);
|
|
|
|
OWSAssert(retryDelaySeconds > 0);
|
|
|
|
OWSAssert(successParameter);
|
|
|
|
OWSAssert(failureParameter);
|
|
|
|
|
|
|
|
void (^success)(NSArray<SignalRecipient *> *) = ^(NSArray<SignalRecipient *> *registeredRecipientIds) {
|
2017-11-08 20:04:51 +01:00
|
|
|
DDLogInfo(@"%@ Successfully intersected contacts.", self.logTag);
|
2018-07-16 21:46:48 +02:00
|
|
|
successParameter([NSSet setWithArray:registeredRecipientIds]);
|
2017-04-13 15:43:37 +02:00
|
|
|
};
|
2018-07-16 21:46:48 +02:00
|
|
|
void (^failure)(NSError *) = ^(NSError *error) {
|
2017-04-13 21:38:32 +02:00
|
|
|
if ([error.domain isEqualToString:OWSSignalServiceKitErrorDomain]
|
|
|
|
&& error.code == OWSErrorCodeContactsUpdaterRateLimit) {
|
2017-04-13 16:11:01 +02:00
|
|
|
DDLogError(@"Contact intersection hit rate limit with error: %@", error);
|
2018-07-16 21:46:48 +02:00
|
|
|
failureParameter(error);
|
2017-04-13 16:11:01 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-11-08 20:04:51 +01:00
|
|
|
DDLogWarn(@"%@ Failed to intersect contacts with error: %@. Rescheduling", self.logTag, error);
|
2017-04-13 15:43:37 +02:00
|
|
|
|
|
|
|
// Retry with exponential backoff.
|
|
|
|
//
|
2017-04-13 16:11:01 +02:00
|
|
|
// TODO: Abort if another contact intersection succeeds in the meantime.
|
2017-04-27 22:18:11 +02:00
|
|
|
dispatch_after(
|
|
|
|
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(retryDelaySeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
2018-07-16 21:46:48 +02:00
|
|
|
[self intersectContacts:recipientIds
|
|
|
|
retryDelaySeconds:retryDelaySeconds * 2.0
|
|
|
|
success:successParameter
|
|
|
|
failure:failureParameter];
|
2017-04-27 22:18:11 +02:00
|
|
|
});
|
2017-04-13 15:43:37 +02:00
|
|
|
};
|
2018-07-16 21:46:48 +02:00
|
|
|
[[ContactsUpdater sharedUpdater] lookupIdentifiers:recipientIds.allObjects success:success failure:failure];
|
2017-02-08 17:59:02 +01:00
|
|
|
}
|
|
|
|
|
2017-08-24 23:08:02 +02:00
|
|
|
- (void)startObserving
|
|
|
|
{
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
2017-08-25 18:39:27 +02:00
|
|
|
selector:@selector(otherUsersProfileWillChange:)
|
|
|
|
name:kNSNotificationName_OtherUsersProfileWillChange
|
2017-08-24 23:08:02 +02:00
|
|
|
object:nil];
|
|
|
|
}
|
|
|
|
|
2017-08-25 18:39:27 +02:00
|
|
|
- (void)otherUsersProfileWillChange:(NSNotification *)notification
|
2017-08-24 23:08:02 +02:00
|
|
|
{
|
2017-12-19 17:38:25 +01:00
|
|
|
OWSAssertIsOnMainThread();
|
2017-08-24 23:08:02 +02:00
|
|
|
|
|
|
|
NSString *recipientId = notification.userInfo[kNSNotificationKey_ProfileRecipientId];
|
|
|
|
OWSAssert(recipientId.length > 0);
|
|
|
|
|
2017-08-25 00:47:11 +02:00
|
|
|
[self.avatarCache removeAllImagesForKey:recipientId];
|
2017-08-24 23:08:02 +02:00
|
|
|
}
|
|
|
|
|
2018-07-16 21:46:48 +02:00
|
|
|
- (void)updateWithContacts:(NSArray<Contact *> *)contacts
|
|
|
|
isUserRequested:(BOOL)isUserRequested
|
|
|
|
shouldClearStaleCache:(BOOL)shouldClearStaleCache
|
2017-05-01 18:51:59 +02:00
|
|
|
{
|
2017-12-15 23:36:06 +01:00
|
|
|
dispatch_async(self.serialQueue, ^{
|
2017-05-01 20:10:53 +02:00
|
|
|
NSMutableDictionary<NSString *, Contact *> *allContactsMap = [NSMutableDictionary new];
|
|
|
|
for (Contact *contact in contacts) {
|
|
|
|
for (PhoneNumber *phoneNumber in contact.parsedPhoneNumbers) {
|
|
|
|
NSString *phoneNumberE164 = phoneNumber.toE164;
|
|
|
|
if (phoneNumberE164.length > 0) {
|
|
|
|
allContactsMap[phoneNumberE164] = contact;
|
|
|
|
}
|
2017-05-01 18:51:59 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-01 20:10:53 +02:00
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
self.allContacts = contacts;
|
|
|
|
self.allContactsMap = [allContactsMap copy];
|
2018-06-20 22:22:17 +02:00
|
|
|
[self.cnContactCache removeAllObjects];
|
|
|
|
[self.cnContactAvatarCache removeAllObjects];
|
2017-05-01 20:10:53 +02:00
|
|
|
|
2017-08-25 00:47:11 +02:00
|
|
|
[self.avatarCache removeAllImages];
|
2017-05-02 18:30:53 +02:00
|
|
|
|
2018-07-16 21:46:48 +02:00
|
|
|
[self intersectContacts:contacts
|
|
|
|
isUserRequested:isUserRequested
|
|
|
|
completion:^(NSError *_Nullable error) {
|
|
|
|
// TODO: Should we do this on error?
|
|
|
|
[self buildSignalAccountsAndClearStaleCache:shouldClearStaleCache];
|
|
|
|
}];
|
2017-05-01 20:10:53 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-03-02 05:30:28 +01:00
|
|
|
- (void)buildSignalAccountsAndClearStaleCache:(BOOL)shouldClearStaleCache
|
2017-05-01 20:10:53 +02:00
|
|
|
{
|
2017-12-15 23:36:06 +01:00
|
|
|
dispatch_async(self.serialQueue, ^{
|
2017-05-01 20:10:53 +02:00
|
|
|
NSMutableArray<SignalAccount *> *signalAccounts = [NSMutableArray new];
|
|
|
|
NSArray<Contact *> *contacts = self.allContacts;
|
2017-05-02 19:59:35 +02:00
|
|
|
|
|
|
|
// We use a transaction only to load the SignalRecipients for each contact,
|
|
|
|
// in order to avoid database deadlock.
|
|
|
|
NSMutableDictionary<NSString *, NSArray<SignalRecipient *> *> *contactIdToSignalRecipientsMap =
|
|
|
|
[NSMutableDictionary new];
|
2017-12-14 01:08:47 +01:00
|
|
|
[self.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
2017-05-01 20:10:53 +02:00
|
|
|
for (Contact *contact in contacts) {
|
|
|
|
NSArray<SignalRecipient *> *signalRecipients = [contact signalRecipientsWithTransaction:transaction];
|
2018-06-20 19:14:21 +02:00
|
|
|
contactIdToSignalRecipientsMap[contact.uniqueId] = signalRecipients;
|
2017-05-01 18:51:59 +02:00
|
|
|
}
|
2017-05-01 20:10:53 +02:00
|
|
|
}];
|
2017-05-01 18:51:59 +02:00
|
|
|
|
2017-12-15 22:15:25 +01:00
|
|
|
NSMutableSet<NSString *> *seenRecipientIds = [NSMutableSet new];
|
2017-05-02 19:59:35 +02:00
|
|
|
for (Contact *contact in contacts) {
|
2018-06-20 19:14:21 +02:00
|
|
|
NSArray<SignalRecipient *> *signalRecipients = contactIdToSignalRecipientsMap[contact.uniqueId];
|
2018-07-18 03:08:53 +02:00
|
|
|
for (SignalRecipient *signalRecipient in [signalRecipients sortedArrayUsingSelector:@selector((compare:))]) {
|
2017-12-15 22:15:25 +01:00
|
|
|
if ([seenRecipientIds containsObject:signalRecipient.recipientId]) {
|
|
|
|
DDLogDebug(@"Ignoring duplicate contact: %@, %@", signalRecipient.recipientId, contact.fullName);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
[seenRecipientIds addObject:signalRecipient.recipientId];
|
2017-12-17 20:02:03 +01:00
|
|
|
|
2017-05-02 19:59:35 +02:00
|
|
|
SignalAccount *signalAccount = [[SignalAccount alloc] initWithSignalRecipient:signalRecipient];
|
|
|
|
signalAccount.contact = contact;
|
|
|
|
if (signalRecipients.count > 1) {
|
|
|
|
signalAccount.hasMultipleAccountContact = YES;
|
|
|
|
signalAccount.multipleAccountLabelText =
|
|
|
|
[[self class] accountLabelForContact:contact recipientId:signalRecipient.recipientId];
|
|
|
|
}
|
|
|
|
[signalAccounts addObject:signalAccount];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-15 22:15:25 +01:00
|
|
|
NSMutableDictionary<NSString *, SignalAccount *> *oldSignalAccounts = [NSMutableDictionary new];
|
|
|
|
[self.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
|
|
|
|
[SignalAccount
|
|
|
|
enumerateCollectionObjectsWithTransaction:transaction
|
|
|
|
usingBlock:^(id _Nonnull object, BOOL *_Nonnull stop) {
|
2017-12-16 19:21:34 +01:00
|
|
|
OWSAssert([object isKindOfClass:[SignalAccount class]]);
|
2017-12-15 22:15:25 +01:00
|
|
|
SignalAccount *oldSignalAccount = (SignalAccount *)object;
|
|
|
|
|
|
|
|
oldSignalAccounts[oldSignalAccount.uniqueId] = oldSignalAccount;
|
|
|
|
}];
|
|
|
|
}];
|
|
|
|
|
|
|
|
NSMutableArray *accountsToSave = [NSMutableArray new];
|
|
|
|
for (SignalAccount *signalAccount in signalAccounts) {
|
2018-07-17 17:36:21 +02:00
|
|
|
SignalAccount *_Nullable oldSignalAccount = oldSignalAccounts[signalAccount.uniqueId];
|
2017-12-15 22:15:25 +01:00
|
|
|
|
|
|
|
// keep track of which accounts are still relevant, so we can clean up orphans
|
|
|
|
[oldSignalAccounts removeObjectForKey:signalAccount.uniqueId];
|
|
|
|
|
|
|
|
if (oldSignalAccount == nil) {
|
|
|
|
// new Signal Account
|
|
|
|
[accountsToSave addObject:signalAccount];
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ([oldSignalAccount isEqual:signalAccount]) {
|
|
|
|
// Same value, no need to save.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// value changed, save account
|
|
|
|
[accountsToSave addObject:signalAccount];
|
|
|
|
}
|
|
|
|
|
2017-12-14 01:08:47 +01:00
|
|
|
// Update cached SignalAccounts on disk
|
|
|
|
[self.dbWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
2017-12-15 23:36:06 +01:00
|
|
|
DDLogInfo(@"%@ Saving %lu SignalAccounts", self.logTag, (unsigned long)accountsToSave.count);
|
2017-12-15 22:15:25 +01:00
|
|
|
for (SignalAccount *signalAccount in accountsToSave) {
|
2017-12-15 23:36:06 +01:00
|
|
|
DDLogVerbose(@"%@ Saving SignalAccount: %@", self.logTag, signalAccount);
|
2017-12-14 01:08:47 +01:00
|
|
|
[signalAccount saveWithTransaction:transaction];
|
|
|
|
}
|
2017-12-14 17:43:27 +01:00
|
|
|
|
2017-12-16 19:21:34 +01:00
|
|
|
if (shouldClearStaleCache) {
|
2017-12-15 23:36:06 +01:00
|
|
|
DDLogInfo(@"%@ Removing %lu old SignalAccounts.", self.logTag, (unsigned long)oldSignalAccounts.count);
|
|
|
|
for (SignalAccount *signalAccount in oldSignalAccounts.allValues) {
|
|
|
|
DDLogVerbose(@"%@ Removing old SignalAccount: %@", self.logTag, signalAccount);
|
|
|
|
[signalAccount removeWithTransaction:transaction];
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// In theory we want to remove SignalAccounts if the user deletes the corresponding system contact.
|
|
|
|
// However, as of iOS11.2 CNContactStore occasionally gives us only a subset of the system contacts.
|
|
|
|
// Because of that, it's not safe to clear orphaned accounts.
|
|
|
|
// Because we still want to give users a way to clear their stale accounts, if they pull-to-refresh
|
|
|
|
// their contacts we'll clear the cached ones.
|
|
|
|
// RADAR: https://bugreport.apple.com/web/?problemID=36082946
|
|
|
|
if (oldSignalAccounts.allValues.count > 0) {
|
|
|
|
DDLogWarn(@"%@ NOT Removing %lu old SignalAccounts.",
|
|
|
|
self.logTag,
|
|
|
|
(unsigned long)oldSignalAccounts.count);
|
|
|
|
for (SignalAccount *signalAccount in oldSignalAccounts.allValues) {
|
|
|
|
DDLogVerbose(
|
|
|
|
@"%@ Ensuring old SignalAccount is not inadvertently lost: %@", self.logTag, signalAccount);
|
|
|
|
[signalAccounts addObject:signalAccount];
|
|
|
|
}
|
|
|
|
|
|
|
|
// re-sort signal accounts since we've appended some orphans
|
2017-12-16 18:26:47 +01:00
|
|
|
[signalAccounts sortUsingComparator:self.signalAccountComparator];
|
2017-12-15 23:36:06 +01:00
|
|
|
}
|
2017-12-14 01:08:47 +01:00
|
|
|
}
|
|
|
|
}];
|
2017-08-01 19:53:51 +02:00
|
|
|
|
2017-05-01 20:10:53 +02:00
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
2017-12-14 01:08:47 +01:00
|
|
|
[self updateSignalAccounts:signalAccounts];
|
2017-05-01 20:10:53 +02:00
|
|
|
});
|
2017-05-01 18:51:59 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-12-14 01:08:47 +01:00
|
|
|
- (void)updateSignalAccounts:(NSArray<SignalAccount *> *)signalAccounts
|
2017-08-07 21:46:18 +02:00
|
|
|
{
|
2017-12-19 17:38:25 +01:00
|
|
|
OWSAssertIsOnMainThread();
|
2017-12-14 17:43:27 +01:00
|
|
|
|
2017-12-16 18:31:56 +01:00
|
|
|
if ([signalAccounts isEqual:self.signalAccounts]) {
|
|
|
|
DDLogDebug(@"%@ SignalAccounts unchanged.", self.logTag);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-12-14 01:08:47 +01:00
|
|
|
NSMutableDictionary<NSString *, SignalAccount *> *signalAccountMap = [NSMutableDictionary new];
|
|
|
|
for (SignalAccount *signalAccount in signalAccounts) {
|
|
|
|
signalAccountMap[signalAccount.recipientId] = signalAccount;
|
2017-05-19 22:30:43 +02:00
|
|
|
}
|
2017-12-14 17:43:27 +01:00
|
|
|
|
2017-12-14 01:08:47 +01:00
|
|
|
self.signalAccountMap = [signalAccountMap copy];
|
|
|
|
self.signalAccounts = [signalAccounts copy];
|
|
|
|
[self.profileManager setContactRecipientIds:signalAccountMap.allKeys];
|
2017-12-14 19:18:51 +01:00
|
|
|
|
|
|
|
[[NSNotificationCenter defaultCenter]
|
|
|
|
postNotificationNameAsync:OWSContactsManagerSignalAccountsDidChangeNotification
|
|
|
|
object:nil];
|
2017-05-24 17:06:46 +02:00
|
|
|
}
|
|
|
|
|
2017-08-07 21:46:18 +02:00
|
|
|
// TODO dependency inject, avoid circular dependencies.
|
|
|
|
- (OWSProfileManager *)profileManager
|
2017-05-24 17:06:46 +02:00
|
|
|
{
|
2017-08-07 21:46:18 +02:00
|
|
|
return [OWSProfileManager sharedManager];
|
2017-05-19 22:30:43 +02:00
|
|
|
}
|
|
|
|
|
2018-04-12 00:59:52 +02:00
|
|
|
- (NSString *_Nullable)cachedContactNameForRecipientId:(NSString *)recipientId
|
2017-05-19 22:30:43 +02:00
|
|
|
{
|
2017-05-30 15:50:17 +02:00
|
|
|
OWSAssert(recipientId.length > 0);
|
2017-05-19 22:30:43 +02:00
|
|
|
|
2018-07-27 20:19:11 +02:00
|
|
|
SignalAccount *_Nullable signalAccount = [self fetchSignalAccountForRecipientId:recipientId];
|
2018-04-12 00:59:52 +02:00
|
|
|
if (!signalAccount) {
|
2018-04-21 21:22:49 +02:00
|
|
|
// search system contacts for no-longer-registered signal users, for which there will be no SignalAccount
|
|
|
|
DDLogDebug(@"%@ no signal account", self.logTag);
|
2018-07-17 17:36:21 +02:00
|
|
|
Contact *_Nullable nonSignalContact = self.allContactsMap[recipientId];
|
2018-04-21 21:22:49 +02:00
|
|
|
if (!nonSignalContact) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
return nonSignalContact.fullName;
|
2018-04-12 00:59:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
NSString *fullName = signalAccount.contactFullName;
|
|
|
|
if (fullName.length == 0) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
NSString *multipleAccountLabelText = signalAccount.multipleAccountLabelText;
|
|
|
|
if (multipleAccountLabelText.length == 0) {
|
|
|
|
return fullName;
|
|
|
|
}
|
|
|
|
|
|
|
|
return [NSString stringWithFormat:@"%@ (%@)", fullName, multipleAccountLabelText];
|
2017-05-24 17:06:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
- (NSString *_Nullable)cachedFirstNameForRecipientId:(NSString *)recipientId
|
|
|
|
{
|
2017-05-30 15:50:17 +02:00
|
|
|
OWSAssert(recipientId.length > 0);
|
2017-05-24 17:06:46 +02:00
|
|
|
|
2018-07-27 20:19:11 +02:00
|
|
|
SignalAccount *_Nullable signalAccount = [self fetchSignalAccountForRecipientId:recipientId];
|
2018-02-16 02:45:28 +01:00
|
|
|
return signalAccount.contact.firstName.filterStringForDisplay;
|
2017-05-24 17:06:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
- (NSString *_Nullable)cachedLastNameForRecipientId:(NSString *)recipientId
|
|
|
|
{
|
2017-05-30 15:50:17 +02:00
|
|
|
OWSAssert(recipientId.length > 0);
|
2017-05-24 17:06:46 +02:00
|
|
|
|
2018-07-27 20:19:11 +02:00
|
|
|
SignalAccount *_Nullable signalAccount = [self fetchSignalAccountForRecipientId:recipientId];
|
2018-02-16 02:45:28 +01:00
|
|
|
return signalAccount.contact.lastName.filterStringForDisplay;
|
2017-05-19 22:30:43 +02:00
|
|
|
}
|
|
|
|
|
2017-05-01 20:28:37 +02:00
|
|
|
#pragma mark - View Helpers
|
2017-05-19 22:30:43 +02:00
|
|
|
|
2017-05-01 20:28:37 +02:00
|
|
|
// TODO move into Contact class.
|
2017-05-01 18:51:59 +02:00
|
|
|
+ (NSString *)accountLabelForContact:(Contact *)contact recipientId:(NSString *)recipientId
|
|
|
|
{
|
|
|
|
OWSAssert(contact);
|
|
|
|
OWSAssert(recipientId.length > 0);
|
|
|
|
OWSAssert([contact.textSecureIdentifiers containsObject:recipientId]);
|
|
|
|
|
|
|
|
if (contact.textSecureIdentifiers.count <= 1) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 1. Find the phone number type of this account.
|
2017-05-04 17:08:35 +02:00
|
|
|
NSString *phoneNumberLabel = [contact nameForPhoneNumber:recipientId];
|
2017-05-01 18:51:59 +02:00
|
|
|
|
|
|
|
// 2. Find all phone numbers for this contact of the same type.
|
2017-05-04 17:08:35 +02:00
|
|
|
NSMutableArray *phoneNumbersWithTheSameName = [NSMutableArray new];
|
2017-05-01 18:51:59 +02:00
|
|
|
for (NSString *textSecureIdentifier in contact.textSecureIdentifiers) {
|
2017-05-04 17:08:35 +02:00
|
|
|
if ([phoneNumberLabel isEqualToString:[contact nameForPhoneNumber:textSecureIdentifier]]) {
|
|
|
|
[phoneNumbersWithTheSameName addObject:textSecureIdentifier];
|
2017-05-01 18:51:59 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-04 17:08:35 +02:00
|
|
|
OWSAssert([phoneNumbersWithTheSameName containsObject:recipientId]);
|
|
|
|
if (phoneNumbersWithTheSameName.count > 1) {
|
2017-05-01 18:51:59 +02:00
|
|
|
NSUInteger index =
|
2017-12-04 22:09:26 +01:00
|
|
|
[[phoneNumbersWithTheSameName sortedArrayUsingSelector:@selector((compare:))] indexOfObject:recipientId];
|
2017-12-01 23:10:14 +01:00
|
|
|
NSString *indexText = [OWSFormat formatInt:(int)index + 1];
|
2017-05-01 18:51:59 +02:00
|
|
|
phoneNumberLabel =
|
2017-07-12 16:46:54 +02:00
|
|
|
[NSString stringWithFormat:NSLocalizedString(@"PHONE_NUMBER_TYPE_AND_INDEX_NAME_FORMAT",
|
2017-05-01 18:51:59 +02:00
|
|
|
@"Format for phone number label with an index. Embeds {{Phone number label "
|
|
|
|
@"(e.g. 'home')}} and {{index, e.g. 2}}."),
|
|
|
|
phoneNumberLabel,
|
2017-07-12 16:46:54 +02:00
|
|
|
indexText];
|
2017-05-01 18:51:59 +02:00
|
|
|
}
|
|
|
|
|
2018-02-16 02:45:28 +01:00
|
|
|
return phoneNumberLabel.filterStringForDisplay;
|
2014-05-06 19:41:08 +02:00
|
|
|
}
|
|
|
|
|
2017-12-01 23:10:14 +01:00
|
|
|
- (BOOL)phoneNumber:(PhoneNumber *)phoneNumber1 matchesNumber:(PhoneNumber *)phoneNumber2
|
|
|
|
{
|
2014-09-07 20:43:53 +02:00
|
|
|
return [phoneNumber1.toE164 isEqualToString:phoneNumber2.toE164];
|
2014-05-06 19:41:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - Whisper User Management
|
|
|
|
|
2018-05-07 21:20:34 +02:00
|
|
|
- (BOOL)isSystemContact:(NSString *)recipientId
|
|
|
|
{
|
|
|
|
OWSAssert(recipientId.length > 0);
|
|
|
|
|
|
|
|
return self.allContactsMap[recipientId] != nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL)isSystemContactWithSignalAccount:(NSString *)recipientId
|
|
|
|
{
|
|
|
|
OWSAssert(recipientId.length > 0);
|
|
|
|
|
|
|
|
return [self hasSignalAccountForRecipientId:recipientId];
|
|
|
|
}
|
|
|
|
|
2017-08-16 21:14:52 +02:00
|
|
|
- (BOOL)hasNameInSystemContactsForRecipientId:(NSString *)recipientId
|
|
|
|
{
|
2018-04-12 00:59:52 +02:00
|
|
|
return [self cachedContactNameForRecipientId:recipientId].length > 0;
|
2017-08-16 21:14:52 +02:00
|
|
|
}
|
|
|
|
|
2016-11-12 18:22:29 +01:00
|
|
|
- (NSString *)unknownContactName
|
|
|
|
{
|
2017-12-01 23:10:14 +01:00
|
|
|
return NSLocalizedString(
|
|
|
|
@"UNKNOWN_CONTACT_NAME", @"Displayed if for some reason we can't determine a contacts phone number *or* name");
|
2016-11-12 18:22:29 +01:00
|
|
|
}
|
|
|
|
|
2017-08-16 21:14:52 +02:00
|
|
|
- (nullable NSString *)formattedProfileNameForRecipientId:(NSString *)recipientId
|
|
|
|
{
|
2018-07-17 17:36:21 +02:00
|
|
|
NSString *_Nullable profileName = [self.profileManager profileNameForRecipientId:recipientId];
|
2017-08-17 18:18:05 +02:00
|
|
|
if (profileName.length == 0) {
|
2017-08-16 21:14:52 +02:00
|
|
|
return nil;
|
|
|
|
}
|
2017-08-17 18:18:05 +02:00
|
|
|
|
|
|
|
NSString *profileNameFormatString = NSLocalizedString(@"PROFILE_NAME_LABEL_FORMAT",
|
|
|
|
@"Prepend a simple marker to differentiate the profile name, embeds the contact's {{profile name}}.");
|
|
|
|
|
|
|
|
return [NSString stringWithFormat:profileNameFormatString, profileName];
|
2017-08-16 21:14:52 +02:00
|
|
|
}
|
|
|
|
|
2017-08-24 23:08:02 +02:00
|
|
|
- (nullable NSString *)profileNameForRecipientId:(NSString *)recipientId
|
|
|
|
{
|
|
|
|
return [self.profileManager profileNameForRecipientId:recipientId];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (nullable NSString *)nameFromSystemContactsForRecipientId:(NSString *)recipientId
|
|
|
|
{
|
2018-04-12 00:59:52 +02:00
|
|
|
return [self cachedContactNameForRecipientId:recipientId];
|
2017-08-24 23:08:02 +02:00
|
|
|
}
|
|
|
|
|
2017-05-19 22:30:43 +02:00
|
|
|
- (NSString *_Nonnull)displayNameForPhoneIdentifier:(NSString *_Nullable)recipientId
|
|
|
|
{
|
|
|
|
if (!recipientId) {
|
2016-11-12 18:22:29 +01:00
|
|
|
return self.unknownContactName;
|
2016-09-11 22:53:12 +02:00
|
|
|
}
|
2017-05-01 18:51:59 +02:00
|
|
|
|
2018-07-17 17:36:21 +02:00
|
|
|
NSString *_Nullable displayName = [self nameFromSystemContactsForRecipientId:recipientId];
|
2017-08-07 21:46:18 +02:00
|
|
|
|
2017-08-24 23:08:02 +02:00
|
|
|
// Fall back to just using their recipientId
|
2017-05-19 22:30:43 +02:00
|
|
|
if (displayName.length < 1) {
|
|
|
|
displayName = recipientId;
|
|
|
|
}
|
2017-08-07 21:46:18 +02:00
|
|
|
|
2017-04-04 16:19:47 +02:00
|
|
|
return displayName;
|
|
|
|
}
|
|
|
|
|
2017-05-01 18:51:59 +02:00
|
|
|
- (NSString *_Nonnull)displayNameForSignalAccount:(SignalAccount *)signalAccount
|
2017-04-28 18:18:42 +02:00
|
|
|
{
|
2017-05-01 18:51:59 +02:00
|
|
|
OWSAssert(signalAccount);
|
2017-04-28 18:18:42 +02:00
|
|
|
|
2017-05-24 17:06:46 +02:00
|
|
|
return [self displayNameForPhoneIdentifier:signalAccount.recipientId];
|
2017-04-30 16:34:28 +02:00
|
|
|
}
|
|
|
|
|
2018-06-29 15:27:10 +02:00
|
|
|
- (NSAttributedString *_Nonnull)formattedDisplayNameForSignalAccount:(SignalAccount *)signalAccount font:(UIFont *)font
|
2017-04-30 16:34:28 +02:00
|
|
|
{
|
2017-05-01 18:51:59 +02:00
|
|
|
OWSAssert(signalAccount);
|
2017-04-30 16:34:28 +02:00
|
|
|
OWSAssert(font);
|
|
|
|
|
2017-05-24 17:06:46 +02:00
|
|
|
return [self formattedFullNameForRecipientId:signalAccount.recipientId font:font];
|
2017-04-28 18:18:42 +02:00
|
|
|
}
|
|
|
|
|
2017-05-24 17:06:46 +02:00
|
|
|
- (NSAttributedString *)formattedFullNameForRecipientId:(NSString *)recipientId font:(UIFont *)font
|
2016-12-05 04:51:31 +01:00
|
|
|
{
|
2017-05-24 17:06:46 +02:00
|
|
|
OWSAssert(recipientId.length > 0);
|
|
|
|
OWSAssert(font);
|
|
|
|
|
2016-12-05 04:51:31 +01:00
|
|
|
UIFont *boldFont = [UIFont ows_mediumFontWithSize:font.pointSize];
|
|
|
|
|
|
|
|
NSDictionary<NSString *, id> *boldFontAttributes =
|
2018-07-13 15:50:49 +02:00
|
|
|
@{ NSFontAttributeName : boldFont, NSForegroundColorAttributeName : [Theme boldColor] };
|
2016-12-05 04:51:31 +01:00
|
|
|
NSDictionary<NSString *, id> *normalFontAttributes =
|
2018-07-13 15:50:49 +02:00
|
|
|
@{ NSFontAttributeName : font, NSForegroundColorAttributeName : [Theme primaryColor] };
|
2017-05-24 17:06:46 +02:00
|
|
|
NSDictionary<NSString *, id> *firstNameAttributes
|
2018-05-11 16:50:11 +02:00
|
|
|
= (self.shouldSortByGivenName ? boldFontAttributes : normalFontAttributes);
|
2017-05-24 17:06:46 +02:00
|
|
|
NSDictionary<NSString *, id> *lastNameAttributes
|
2018-05-11 16:50:11 +02:00
|
|
|
= (self.shouldSortByGivenName ? normalFontAttributes : boldFontAttributes);
|
2017-05-24 17:06:46 +02:00
|
|
|
|
|
|
|
NSString *cachedFirstName = [self cachedFirstNameForRecipientId:recipientId];
|
|
|
|
NSString *cachedLastName = [self cachedLastNameForRecipientId:recipientId];
|
|
|
|
|
|
|
|
NSMutableAttributedString *formattedName = [NSMutableAttributedString new];
|
|
|
|
|
|
|
|
if (cachedFirstName.length > 0 && cachedLastName.length > 0) {
|
|
|
|
NSAttributedString *firstName =
|
|
|
|
[[NSAttributedString alloc] initWithString:cachedFirstName attributes:firstNameAttributes];
|
|
|
|
NSAttributedString *lastName =
|
|
|
|
[[NSAttributedString alloc] initWithString:cachedLastName attributes:lastNameAttributes];
|
|
|
|
|
2018-07-17 17:36:21 +02:00
|
|
|
NSString *_Nullable cnContactId = self.allContactsMap[recipientId].cnContactId;
|
|
|
|
CNContact *_Nullable cnContact = [self cnContactWithId:cnContactId];
|
2018-05-31 16:23:00 +02:00
|
|
|
if (!cnContact) {
|
|
|
|
// If we don't have a CNContact for this recipient id, make one.
|
|
|
|
// Presumably [CNContactFormatter nameOrderForContact:] tries
|
|
|
|
// to localizes its result based on the languages/scripts used
|
|
|
|
// in the contact's fields.
|
|
|
|
CNMutableContact *formatContact = [CNMutableContact new];
|
|
|
|
formatContact.givenName = firstName.string;
|
|
|
|
formatContact.familyName = lastName.string;
|
|
|
|
cnContact = formatContact;
|
|
|
|
}
|
|
|
|
CNContactDisplayNameOrder nameOrder = [CNContactFormatter nameOrderForContact:cnContact];
|
2018-07-17 17:36:21 +02:00
|
|
|
NSAttributedString *_Nullable leftName, *_Nullable rightName;
|
2018-05-11 16:50:11 +02:00
|
|
|
if (nameOrder == CNContactDisplayNameOrderGivenNameFirst) {
|
2017-05-24 17:06:46 +02:00
|
|
|
leftName = firstName;
|
|
|
|
rightName = lastName;
|
|
|
|
} else {
|
|
|
|
leftName = lastName;
|
|
|
|
rightName = firstName;
|
2016-12-05 04:51:31 +01:00
|
|
|
}
|
|
|
|
|
2017-05-24 17:06:46 +02:00
|
|
|
[formattedName appendAttributedString:leftName];
|
|
|
|
[formattedName
|
|
|
|
appendAttributedString:[[NSAttributedString alloc] initWithString:@" " attributes:normalFontAttributes]];
|
|
|
|
[formattedName appendAttributedString:rightName];
|
|
|
|
} else if (cachedFirstName.length > 0) {
|
|
|
|
[formattedName appendAttributedString:[[NSAttributedString alloc] initWithString:cachedFirstName
|
|
|
|
attributes:firstNameAttributes]];
|
|
|
|
} else if (cachedLastName.length > 0) {
|
|
|
|
[formattedName appendAttributedString:[[NSAttributedString alloc] initWithString:cachedLastName
|
|
|
|
attributes:lastNameAttributes]];
|
2016-12-05 04:51:31 +01:00
|
|
|
} else {
|
2017-08-16 21:14:52 +02:00
|
|
|
// Else, fall back to using just their recipientId
|
|
|
|
NSString *phoneString =
|
|
|
|
[PhoneNumber bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:recipientId];
|
|
|
|
return [[NSAttributedString alloc] initWithString:phoneString attributes:normalFontAttributes];
|
2016-12-05 04:51:31 +01:00
|
|
|
}
|
|
|
|
|
2017-08-16 21:14:52 +02:00
|
|
|
// Append unique label for contacts with multiple Signal accounts
|
2018-07-27 20:19:11 +02:00
|
|
|
SignalAccount *_Nullable signalAccount = [self fetchSignalAccountForRecipientId:recipientId];
|
2017-05-24 17:06:46 +02:00
|
|
|
if (signalAccount && signalAccount.multipleAccountLabelText) {
|
|
|
|
OWSAssert(signalAccount.multipleAccountLabelText.length > 0);
|
|
|
|
|
|
|
|
[formattedName
|
|
|
|
appendAttributedString:[[NSAttributedString alloc] initWithString:@" (" attributes:normalFontAttributes]];
|
|
|
|
[formattedName
|
|
|
|
appendAttributedString:[[NSAttributedString alloc] initWithString:signalAccount.multipleAccountLabelText
|
|
|
|
attributes:normalFontAttributes]];
|
|
|
|
[formattedName
|
|
|
|
appendAttributedString:[[NSAttributedString alloc] initWithString:@")" attributes:normalFontAttributes]];
|
2016-12-05 04:51:31 +01:00
|
|
|
}
|
2017-04-18 22:08:01 +02:00
|
|
|
|
2017-05-24 17:06:46 +02:00
|
|
|
return formattedName;
|
2017-04-18 22:08:01 +02:00
|
|
|
}
|
|
|
|
|
2017-09-19 16:36:23 +02:00
|
|
|
- (NSString *)contactOrProfileNameForPhoneIdentifier:(NSString *)recipientId
|
2017-08-16 21:14:52 +02:00
|
|
|
{
|
|
|
|
// Prefer a saved name from system contacts, if available
|
2018-07-17 17:36:21 +02:00
|
|
|
NSString *_Nullable savedContactName = [self cachedContactNameForRecipientId:recipientId];
|
2017-08-16 21:14:52 +02:00
|
|
|
if (savedContactName.length > 0) {
|
2017-09-19 16:36:23 +02:00
|
|
|
return savedContactName;
|
2017-08-16 21:14:52 +02:00
|
|
|
}
|
|
|
|
|
2018-07-17 17:36:21 +02:00
|
|
|
NSString *_Nullable profileName = [self.profileManager profileNameForRecipientId:recipientId];
|
2017-08-17 18:18:05 +02:00
|
|
|
if (profileName.length > 0) {
|
|
|
|
NSString *numberAndProfileNameFormat = NSLocalizedString(@"PROFILE_NAME_AND_PHONE_NUMBER_LABEL_FORMAT",
|
|
|
|
@"Label text combining the phone number and profile name separated by a simple demarcation character. "
|
|
|
|
@"Phone number should be most prominent. '%1$@' is replaced with {{phone number}} and '%2$@' is replaced "
|
|
|
|
@"with {{profile name}}");
|
|
|
|
|
|
|
|
NSString *numberAndProfileName =
|
|
|
|
[NSString stringWithFormat:numberAndProfileNameFormat, recipientId, profileName];
|
2017-09-19 16:36:23 +02:00
|
|
|
return numberAndProfileName;
|
2017-08-16 21:14:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// else fall back to recipient id
|
2017-09-19 16:36:23 +02:00
|
|
|
return recipientId;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSAttributedString *)attributedContactOrProfileNameForPhoneIdentifier:(NSString *)recipientId
|
|
|
|
{
|
|
|
|
return [[NSAttributedString alloc] initWithString:[self contactOrProfileNameForPhoneIdentifier:recipientId]];
|
2017-08-16 21:14:52 +02:00
|
|
|
}
|
|
|
|
|
2018-07-02 15:42:48 +02:00
|
|
|
- (NSAttributedString *)attributedContactOrProfileNameForPhoneIdentifier:(NSString *)recipientId
|
|
|
|
primaryFont:(UIFont *)primaryFont
|
|
|
|
secondaryFont:(UIFont *)secondaryFont
|
|
|
|
{
|
|
|
|
OWSAssert(primaryFont);
|
|
|
|
OWSAssert(secondaryFont);
|
|
|
|
|
|
|
|
return [self attributedContactOrProfileNameForPhoneIdentifier:(NSString *)recipientId
|
|
|
|
primaryAttributes:@{
|
|
|
|
NSFontAttributeName : primaryFont,
|
|
|
|
}
|
|
|
|
secondaryAttributes:@{
|
|
|
|
NSFontAttributeName : secondaryFont,
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSAttributedString *)attributedContactOrProfileNameForPhoneIdentifier:(NSString *)recipientId
|
|
|
|
primaryAttributes:(NSDictionary *)primaryAttributes
|
|
|
|
secondaryAttributes:(NSDictionary *)secondaryAttributes
|
2017-08-23 18:03:36 +02:00
|
|
|
{
|
2018-07-02 15:42:48 +02:00
|
|
|
OWSAssert(primaryAttributes.count > 0);
|
|
|
|
OWSAssert(secondaryAttributes.count > 0);
|
|
|
|
|
2017-08-23 18:03:36 +02:00
|
|
|
// Prefer a saved name from system contacts, if available
|
2018-07-17 17:36:21 +02:00
|
|
|
NSString *_Nullable savedContactName = [self cachedContactNameForRecipientId:recipientId];
|
2017-08-23 18:03:36 +02:00
|
|
|
if (savedContactName.length > 0) {
|
2018-07-02 15:42:48 +02:00
|
|
|
return [[NSAttributedString alloc] initWithString:savedContactName attributes:primaryAttributes];
|
2017-08-23 18:03:36 +02:00
|
|
|
}
|
2017-08-23 21:41:56 +02:00
|
|
|
|
2018-07-17 17:36:21 +02:00
|
|
|
NSString *_Nullable profileName = [self.profileManager profileNameForRecipientId:recipientId];
|
2017-08-23 18:03:36 +02:00
|
|
|
if (profileName.length > 0) {
|
2018-07-02 15:42:48 +02:00
|
|
|
NSAttributedString *result =
|
|
|
|
[[NSAttributedString alloc] initWithString:recipientId attributes:primaryAttributes];
|
2018-07-02 21:33:21 +02:00
|
|
|
result = [result rtlSafeAppend:[[NSAttributedString alloc] initWithString:@" "]];
|
2018-07-05 17:57:08 +02:00
|
|
|
result = [result rtlSafeAppend:[[NSAttributedString alloc] initWithString:@"~" attributes:secondaryAttributes]];
|
2018-07-02 21:33:21 +02:00
|
|
|
result = [result
|
|
|
|
rtlSafeAppend:[[NSAttributedString alloc] initWithString:profileName attributes:secondaryAttributes]];
|
2018-07-02 15:42:48 +02:00
|
|
|
return [result copy];
|
2017-08-23 18:03:36 +02:00
|
|
|
}
|
2017-08-23 21:41:56 +02:00
|
|
|
|
2017-08-23 18:03:36 +02:00
|
|
|
// else fall back to recipient id
|
2018-07-02 15:42:48 +02:00
|
|
|
return [[NSAttributedString alloc] initWithString:recipientId attributes:primaryAttributes];
|
2017-08-23 18:03:36 +02:00
|
|
|
}
|
|
|
|
|
2017-09-07 22:25:57 +02:00
|
|
|
// TODO refactor attributed counterparts to use this as a helper method?
|
|
|
|
- (NSString *)stringForConversationTitleWithPhoneIdentifier:(NSString *)recipientId
|
|
|
|
{
|
|
|
|
// Prefer a saved name from system contacts, if available
|
2018-07-17 17:36:21 +02:00
|
|
|
NSString *_Nullable savedContactName = [self cachedContactNameForRecipientId:recipientId];
|
2017-09-07 22:25:57 +02:00
|
|
|
if (savedContactName.length > 0) {
|
|
|
|
return savedContactName;
|
|
|
|
}
|
|
|
|
|
|
|
|
NSString *formattedPhoneNumber =
|
|
|
|
[PhoneNumber bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:recipientId];
|
2018-07-17 17:36:21 +02:00
|
|
|
NSString *_Nullable profileName = [self.profileManager profileNameForRecipientId:recipientId];
|
2017-09-07 22:25:57 +02:00
|
|
|
if (profileName.length > 0) {
|
|
|
|
NSString *numberAndProfileNameFormat = NSLocalizedString(@"PROFILE_NAME_AND_PHONE_NUMBER_LABEL_FORMAT",
|
|
|
|
@"Label text combining the phone number and profile name separated by a simple demarcation character. "
|
|
|
|
@"Phone number should be most prominent. '%1$@' is replaced with {{phone number}} and '%2$@' is replaced "
|
|
|
|
@"with {{profile name}}");
|
|
|
|
|
|
|
|
NSString *numberAndProfileName =
|
|
|
|
[NSString stringWithFormat:numberAndProfileNameFormat, formattedPhoneNumber, profileName];
|
|
|
|
|
|
|
|
return numberAndProfileName;
|
|
|
|
}
|
|
|
|
|
|
|
|
// else fall back phone number
|
|
|
|
return formattedPhoneNumber;
|
|
|
|
}
|
|
|
|
|
2018-07-27 20:19:11 +02:00
|
|
|
- (nullable SignalAccount *)fetchSignalAccountForRecipientId:(NSString *)recipientId
|
2017-05-01 18:51:59 +02:00
|
|
|
{
|
|
|
|
OWSAssert(recipientId.length > 0);
|
|
|
|
|
2017-12-14 01:08:47 +01:00
|
|
|
__block SignalAccount *signalAccount = self.signalAccountMap[recipientId];
|
2014-11-25 19:06:09 +01:00
|
|
|
|
2017-12-13 22:59:57 +01:00
|
|
|
// If contact intersection hasn't completed, it might exist on disk
|
|
|
|
// even if it doesn't exist in memory yet.
|
|
|
|
if (!signalAccount) {
|
2017-12-14 17:43:27 +01:00
|
|
|
[self.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
|
|
|
|
signalAccount = [SignalAccount fetchObjectWithUniqueID:recipientId transaction:transaction];
|
2017-12-14 01:08:47 +01:00
|
|
|
}];
|
2016-11-12 18:22:29 +01:00
|
|
|
}
|
2017-12-13 22:59:57 +01:00
|
|
|
|
|
|
|
return signalAccount;
|
2014-11-25 19:06:09 +01:00
|
|
|
}
|
|
|
|
|
2018-07-27 20:19:11 +02:00
|
|
|
- (SignalAccount *)fetchOrBuildSignalAccountForRecipientId:(NSString *)recipientId
|
2016-11-12 18:22:29 +01:00
|
|
|
{
|
2018-07-27 20:19:11 +02:00
|
|
|
OWSAssert(recipientId.length > 0);
|
|
|
|
|
|
|
|
SignalAccount *_Nullable signalAccount = [self fetchSignalAccountForRecipientId:recipientId];
|
|
|
|
return (signalAccount ?: [[SignalAccount alloc] initWithRecipientId:recipientId]);
|
2016-11-12 18:22:29 +01:00
|
|
|
}
|
|
|
|
|
2018-07-27 20:19:11 +02:00
|
|
|
- (BOOL)hasSignalAccountForRecipientId:(NSString *)recipientId
|
|
|
|
{
|
|
|
|
return [self fetchSignalAccountForRecipientId:recipientId] != nil;
|
|
|
|
}
|
2018-05-05 04:32:29 +02:00
|
|
|
|
|
|
|
- (UIImage *_Nullable)systemContactImageForPhoneIdentifier:(NSString *_Nullable)identifier
|
2017-12-01 23:10:14 +01:00
|
|
|
{
|
2018-05-17 14:44:07 +02:00
|
|
|
if (identifier.length == 0) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
2017-05-01 18:51:59 +02:00
|
|
|
Contact *contact = self.allContactsMap[identifier];
|
2017-12-13 22:59:57 +01:00
|
|
|
if (!contact) {
|
2018-05-05 04:32:29 +02:00
|
|
|
// If we haven't loaded system contacts yet, we may have a cached
|
|
|
|
// copy in the db
|
2018-07-27 20:19:11 +02:00
|
|
|
contact = [self fetchSignalAccountForRecipientId:identifier].contact;
|
2017-12-13 22:59:57 +01:00
|
|
|
}
|
2016-12-01 22:17:57 +01:00
|
|
|
|
2018-06-20 17:41:14 +02:00
|
|
|
return [self avatarImageForCNContactId:contact.cnContactId];
|
2018-05-05 04:32:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
- (nullable UIImage *)profileImageForPhoneIdentifier:(nullable NSString *)identifier
|
|
|
|
{
|
2018-05-17 14:44:07 +02:00
|
|
|
if (identifier.length == 0) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
2018-05-05 04:32:29 +02:00
|
|
|
return [self.profileManager profileAvatarForRecipientId:identifier];
|
|
|
|
}
|
|
|
|
|
2018-05-10 03:10:23 +02:00
|
|
|
- (nullable NSData *)profileImageDataForPhoneIdentifier:(nullable NSString *)identifier
|
|
|
|
{
|
2018-05-17 14:44:07 +02:00
|
|
|
if (identifier.length == 0) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
2018-05-10 03:10:23 +02:00
|
|
|
return [self.profileManager profileAvatarDataForRecipientId:identifier];
|
|
|
|
}
|
|
|
|
|
2018-05-05 04:32:29 +02:00
|
|
|
- (UIImage *_Nullable)imageForPhoneIdentifier:(NSString *_Nullable)identifier
|
|
|
|
{
|
2018-05-17 14:44:07 +02:00
|
|
|
if (identifier.length == 0) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
2017-08-07 21:46:18 +02:00
|
|
|
// Prefer the contact image from the local address book if available
|
2018-07-17 17:36:21 +02:00
|
|
|
UIImage *_Nullable image = [self systemContactImageForPhoneIdentifier:identifier];
|
2017-08-07 21:46:18 +02:00
|
|
|
|
|
|
|
// Else try to use the image from their profile
|
|
|
|
if (image == nil) {
|
2018-05-05 04:32:29 +02:00
|
|
|
image = [self profileImageForPhoneIdentifier:identifier];
|
2017-08-07 21:46:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return image;
|
2016-12-01 22:17:57 +01:00
|
|
|
}
|
|
|
|
|
2018-05-16 22:12:13 +02:00
|
|
|
- (NSComparisonResult)compareSignalAccount:(SignalAccount *)left withSignalAccount:(SignalAccount *)right
|
|
|
|
{
|
|
|
|
return self.signalAccountComparator(left, right);
|
|
|
|
}
|
|
|
|
|
2017-12-16 18:26:47 +01:00
|
|
|
- (NSComparisonResult (^)(SignalAccount *left, SignalAccount *right))signalAccountComparator
|
|
|
|
{
|
|
|
|
return ^NSComparisonResult(SignalAccount *left, SignalAccount *right) {
|
|
|
|
NSString *leftName = [self comparableNameForSignalAccount:left];
|
|
|
|
NSString *rightName = [self comparableNameForSignalAccount:right];
|
|
|
|
|
2018-05-14 17:21:22 +02:00
|
|
|
NSComparisonResult nameComparison = [leftName caseInsensitiveCompare:rightName];
|
2017-12-20 20:48:44 +01:00
|
|
|
if (nameComparison == NSOrderedSame) {
|
|
|
|
return [left.recipientId compare:right.recipientId];
|
|
|
|
}
|
|
|
|
|
|
|
|
return nameComparison;
|
2017-12-16 18:26:47 +01:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-05-11 16:50:11 +02:00
|
|
|
- (BOOL)shouldSortByGivenName
|
|
|
|
{
|
|
|
|
return [[CNContactsUserDefaults sharedDefaults] sortOrder] == CNContactSortOrderGivenName;
|
|
|
|
}
|
|
|
|
|
2017-10-14 19:20:46 +02:00
|
|
|
- (NSString *)comparableNameForSignalAccount:(SignalAccount *)signalAccount
|
|
|
|
{
|
2018-07-17 17:36:21 +02:00
|
|
|
NSString *_Nullable name;
|
2017-10-14 19:20:46 +02:00
|
|
|
if (signalAccount.contact) {
|
2018-05-11 16:50:11 +02:00
|
|
|
if (self.shouldSortByGivenName) {
|
2017-10-14 19:20:46 +02:00
|
|
|
name = signalAccount.contact.comparableNameFirstLast;
|
|
|
|
} else {
|
|
|
|
name = signalAccount.contact.comparableNameLastFirst;
|
|
|
|
}
|
|
|
|
}
|
2017-12-01 23:10:14 +01:00
|
|
|
|
2017-10-14 19:20:46 +02:00
|
|
|
if (name.length < 1) {
|
|
|
|
name = signalAccount.recipientId;
|
|
|
|
}
|
2017-12-01 23:10:14 +01:00
|
|
|
|
2017-10-14 19:20:46 +02:00
|
|
|
return name;
|
|
|
|
}
|
|
|
|
|
2018-07-16 21:46:48 +02:00
|
|
|
NS_ASSUME_NONNULL_END
|
|
|
|
|
2014-05-06 19:41:08 +02:00
|
|
|
@end
|