Split it based on whether the purpose is “editing” or “sharing”. If we’re editing, it generally means we have full access to contacts, and that we’ve checked which of the contacts are registered on Signal. If we’re “sharing”, we don’t care whether or not they’re registered. If we’re “sharing”, we check whether or not we’ve prompted for the contacts permission yet. If not, we’ll show the prompt. If we’re “editing”, we assume we’ve already prompted (which is already the case today), so `.notDetermined` isn’t a possible state. Other things to note: - The `supportsContactEditing` was always true, so it’s been removed. - The error when contacts access isn’t allowed is now shown more consistently. Previously, it was sometimes shown in response to initializing a view controller. - Some RecipientPickerViewController code was moved from Obj-C to Swift. This is probably useful on its own, but it’ll also make it easier to build some new UI in subsequent commits.
369 lines
14 KiB
Objective-C
369 lines
14 KiB
Objective-C
//
|
|
// Copyright 2017 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
#import "ContactsViewHelper.h"
|
|
#import "UIUtil.h"
|
|
#import <ContactsUI/ContactsUI.h>
|
|
#import <SignalCoreKit/NSString+OWS.h>
|
|
#import <SignalMessaging/Environment.h>
|
|
#import <SignalMessaging/OWSProfileManager.h>
|
|
#import <SignalMessaging/SignalMessaging-Swift.h>
|
|
#import <SignalServiceKit/AppContext.h>
|
|
#import <SignalServiceKit/Contact.h>
|
|
#import <SignalServiceKit/PhoneNumber.h>
|
|
#import <SignalServiceKit/SignalAccount.h>
|
|
#import <SignalServiceKit/TSAccountManager.h>
|
|
#import <SignalUI/SignalUI-Swift.h>
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
@interface ContactsViewHelper ()
|
|
|
|
@property (nonatomic) NSHashTable<id<ContactsViewHelperObserver>> *observers;
|
|
|
|
@property (nonatomic) NSDictionary<NSString *, SignalAccount *> *phoneNumberSignalAccountMap;
|
|
@property (nonatomic) NSDictionary<NSUUID *, SignalAccount *> *uuidSignalAccountMap;
|
|
|
|
@property (nonatomic) NSArray<SignalAccount *> *signalAccounts;
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@implementation ContactsViewHelper
|
|
|
|
- (instancetype)init
|
|
{
|
|
self = [super init];
|
|
if (!self) {
|
|
return self;
|
|
}
|
|
|
|
_observers = [NSHashTable weakObjectsHashTable];
|
|
|
|
AppReadinessRunNowOrWhenUIDidBecomeReadySync(^{
|
|
// setup() - especially updateContacts() - can
|
|
// be expensive, so we don't want to run that
|
|
// directly in runNowOrWhenAppDidBecomeReadySync().
|
|
// That could cause 0x8badf00d crashes.
|
|
//
|
|
// On the other hand, the user might quickly
|
|
// open a view (like the compose view) that uses
|
|
// this helper. If the helper hasn't completed
|
|
// setup, that view won't be able to display a
|
|
// list of users to pick from. Therefore, we
|
|
// can't use runNowOrWhenAppDidBecomeReadyAsync()
|
|
// which might not run for many seconds after
|
|
// the app becomes ready.
|
|
//
|
|
// Therefore we dispatch async to the main queue.
|
|
// We'll run very soon after app UI becomes ready,
|
|
// without introducing the risk of a 0x8badf00d
|
|
// crash.
|
|
dispatch_async(dispatch_get_main_queue(), ^{ [self setup]; });
|
|
});
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)setup
|
|
{
|
|
if (CurrentAppContext().isNSE) {
|
|
return;
|
|
}
|
|
[self updateContacts];
|
|
[self observeNotifications];
|
|
}
|
|
|
|
- (void)observeNotifications
|
|
{
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(signalAccountsDidChange:)
|
|
name:OWSContactsManagerSignalAccountsDidChangeNotification
|
|
object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(profileWhitelistDidChange:)
|
|
name:kNSNotificationNameProfileWhitelistDidChange
|
|
object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(blockListDidChange:)
|
|
name:BlockingManager.blockListDidChange
|
|
object:nil];
|
|
}
|
|
|
|
- (void)signalAccountsDidChange:(NSNotification *)notification
|
|
{
|
|
OWSAssertIsOnMainThread();
|
|
|
|
[self updateContacts];
|
|
}
|
|
|
|
- (void)profileWhitelistDidChange:(NSNotification *)notification
|
|
{
|
|
OWSAssertIsOnMainThread();
|
|
|
|
[self updateContacts];
|
|
}
|
|
|
|
- (void)addObserver:(id<ContactsViewHelperObserver>)observer
|
|
{
|
|
OWSAssertIsOnMainThread();
|
|
|
|
[self.observers addObject:observer];
|
|
}
|
|
|
|
- (void)fireDidUpdateContacts
|
|
{
|
|
OWSAssertIsOnMainThread();
|
|
|
|
for (id<ContactsViewHelperObserver> delegate in self.observers) {
|
|
[delegate contactsViewHelperDidUpdateContacts];
|
|
}
|
|
}
|
|
|
|
- (void)blockListDidChange:(NSNotification *)notification
|
|
{
|
|
OWSAssertIsOnMainThread();
|
|
OWSAssertDebug(!CurrentAppContext().isNSE);
|
|
|
|
[self updateContacts];
|
|
}
|
|
|
|
#pragma mark - Contacts
|
|
|
|
- (nullable SignalAccount *)fetchSignalAccountForAddress:(SignalServiceAddress *)address
|
|
{
|
|
OWSAssertIsOnMainThread();
|
|
OWSAssertDebug(address);
|
|
OWSAssertDebug(!CurrentAppContext().isNSE);
|
|
|
|
SignalAccount *_Nullable signalAccount;
|
|
|
|
if (address.uuid) {
|
|
signalAccount = self.uuidSignalAccountMap[address.uuid];
|
|
}
|
|
|
|
if (!signalAccount && address.phoneNumber) {
|
|
signalAccount = self.phoneNumberSignalAccountMap[address.phoneNumber];
|
|
}
|
|
|
|
return signalAccount;
|
|
}
|
|
|
|
- (SignalAccount *)fetchOrBuildSignalAccountForAddress:(SignalServiceAddress *)address
|
|
{
|
|
OWSAssertDebug(address);
|
|
OWSAssertDebug(!CurrentAppContext().isNSE);
|
|
|
|
SignalAccount *_Nullable signalAccount = [self fetchSignalAccountForAddress:address];
|
|
return (signalAccount ?: [[SignalAccount alloc] initWithSignalServiceAddress:address]);
|
|
}
|
|
|
|
- (NSArray<SignalAccount *> *)allSignalAccounts
|
|
{
|
|
OWSAssertDebug(!CurrentAppContext().isNSE);
|
|
|
|
return self.signalAccounts;
|
|
}
|
|
|
|
- (SignalServiceAddress *)localAddress
|
|
{
|
|
OWSAssertDebug(!CurrentAppContext().isNSE);
|
|
|
|
return TSAccountManager.localAddress;
|
|
}
|
|
|
|
- (BOOL)hasUpdatedContactsAtLeastOnce
|
|
{
|
|
OWSAssertDebug(!CurrentAppContext().isNSE);
|
|
|
|
return self.contactsManagerImpl.hasLoadedSystemContacts;
|
|
}
|
|
|
|
- (void)updateContacts
|
|
{
|
|
OWSAssertIsOnMainThread();
|
|
OWSAssertDebug(!CurrentAppContext().isNSE);
|
|
|
|
NSMutableDictionary<NSString *, SignalAccount *> *phoneNumberSignalAccountMap = [NSMutableDictionary new];
|
|
NSMutableDictionary<NSUUID *, SignalAccount *> *uuidSignalAccountMap = [NSMutableDictionary new];
|
|
|
|
__block NSArray<SignalAccount *> *allSignalAccounts;
|
|
__block NSArray<SignalServiceAddress *> *whitelistedAddresses;
|
|
__block ContactsMaps *contactsMaps;
|
|
|
|
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
|
|
allSignalAccounts = [self.contactsManagerImpl unsortedSignalAccountsWithTransaction:transaction];
|
|
whitelistedAddresses = [self.profileManagerImpl allWhitelistedRegisteredAddressesWithTransaction:transaction];
|
|
contactsMaps = [self.contactsManagerImpl contactsMapsWithTransaction:transaction];
|
|
} file:__FILE__ function:__FUNCTION__ line:__LINE__];
|
|
|
|
NSMutableArray<SignalAccount *> *accountsToProcess = [allSignalAccounts mutableCopy];
|
|
for (SignalServiceAddress *address in whitelistedAddresses) {
|
|
if ([contactsMaps isSystemContactWithAddress:address]) {
|
|
continue;
|
|
}
|
|
[accountsToProcess addObject:[[SignalAccount alloc] initWithSignalServiceAddress:address]];
|
|
}
|
|
|
|
NSMutableSet<SignalServiceAddress *> *addressSet = [NSMutableSet new];
|
|
NSMutableArray<SignalAccount *> *signalAccounts = [NSMutableArray new];
|
|
for (SignalAccount *signalAccount in accountsToProcess) {
|
|
if ([addressSet containsObject:signalAccount.recipientAddress]) {
|
|
OWSLogVerbose(@"Ignoring duplicate: %@", signalAccount.recipientAddress);
|
|
// We prefer the copy from contactsManager which will appear first
|
|
// in accountsToProcess; don't overwrite it.
|
|
continue;
|
|
}
|
|
[addressSet addObject:signalAccount.recipientAddress];
|
|
if (signalAccount.recipientPhoneNumber) {
|
|
phoneNumberSignalAccountMap[signalAccount.recipientPhoneNumber] = signalAccount;
|
|
}
|
|
if (signalAccount.recipientUUID) {
|
|
uuidSignalAccountMap[signalAccount.recipientUUID] = signalAccount;
|
|
}
|
|
[signalAccounts addObject:signalAccount];
|
|
}
|
|
|
|
self.phoneNumberSignalAccountMap = [phoneNumberSignalAccountMap copy];
|
|
self.uuidSignalAccountMap = [uuidSignalAccountMap copy];
|
|
self.signalAccounts = [self.contactsManagerImpl sortSignalAccountsWithSneakyTransaction:signalAccounts];
|
|
|
|
[self fireDidUpdateContacts];
|
|
}
|
|
|
|
- (NSArray<SignalAccount *> *)signalAccountsMatchingSearchString:(NSString *)searchText
|
|
transaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(!CurrentAppContext().isNSE);
|
|
|
|
// Check for matches against "Note to Self".
|
|
NSMutableArray<SignalAccount *> *signalAccountsToSearch = [self.signalAccounts mutableCopy];
|
|
SignalAccount *selfAccount = [[SignalAccount alloc] initWithSignalServiceAddress:self.localAddress];
|
|
[signalAccountsToSearch addObject:selfAccount];
|
|
return [self.fullTextSearcher filterSignalAccounts:signalAccountsToSearch
|
|
withSearchText:searchText
|
|
transaction:transaction];
|
|
}
|
|
|
|
#pragma mark - Editing
|
|
|
|
- (CNContactViewController *)contactViewControllerForAddress:(SignalServiceAddress *)address
|
|
editImmediately:(BOOL)shouldEditImmediately
|
|
{
|
|
OWSAssertDebug(!CurrentAppContext().isNSE);
|
|
|
|
return [self contactViewControllerForAddress:address
|
|
editImmediately:shouldEditImmediately
|
|
addToExistingCnContact:nil
|
|
updatedNameComponents:nil];
|
|
}
|
|
|
|
- (CNContactViewController *)contactViewControllerForAddress:(SignalServiceAddress *)address
|
|
editImmediately:(BOOL)shouldEditImmediately
|
|
addToExistingCnContact:(nullable CNContact *)existingContact
|
|
updatedNameComponents:(nullable NSPersonNameComponents *)updatedNameComponents
|
|
{
|
|
OWSAssertIsOnMainThread();
|
|
OWSAssertDebug(!CurrentAppContext().isNSE);
|
|
OWSAssertDebug(self.contactsManagerImpl.editingAuthorization == ContactAuthorizationForEditingAuthorized);
|
|
|
|
SignalAccount *signalAccount = [self fetchSignalAccountForAddress:address];
|
|
|
|
CNContactViewController *_Nullable contactViewController;
|
|
CNContact *_Nullable cnContact = nil;
|
|
if (existingContact) {
|
|
CNMutableContact *updatedContact = [existingContact mutableCopy];
|
|
NSMutableArray<CNLabeledValue *> *phoneNumbers
|
|
= (updatedContact.phoneNumbers ? [updatedContact.phoneNumbers mutableCopy] : [NSMutableArray new]);
|
|
// Only add recipientId as a phone number for the existing contact
|
|
// if its not already present.
|
|
BOOL hasPhoneNumber = NO;
|
|
if (address.phoneNumber) {
|
|
for (CNLabeledValue *existingPhoneNumber in phoneNumbers) {
|
|
CNPhoneNumber *phoneNumber = existingPhoneNumber.value;
|
|
if ([phoneNumber.stringValue isEqualToString:address.phoneNumber]) {
|
|
OWSFailDebug(
|
|
@"We currently only should the 'add to existing contact' UI for phone numbers that don't "
|
|
@"correspond to an existing user.");
|
|
hasPhoneNumber = YES;
|
|
break;
|
|
}
|
|
}
|
|
if (!hasPhoneNumber) {
|
|
CNPhoneNumber *phoneNumber = [CNPhoneNumber phoneNumberWithStringValue:address.phoneNumber];
|
|
CNLabeledValue<CNPhoneNumber *> *labeledPhoneNumber =
|
|
[CNLabeledValue labeledValueWithLabel:CNLabelPhoneNumberMain value:phoneNumber];
|
|
[phoneNumbers addObject:labeledPhoneNumber];
|
|
updatedContact.phoneNumbers = phoneNumbers;
|
|
|
|
// When adding a phone number to an existing contact, immediately enter
|
|
// "edit" mode.
|
|
shouldEditImmediately = YES;
|
|
}
|
|
cnContact = updatedContact;
|
|
}
|
|
}
|
|
if (signalAccount && !cnContact && signalAccount.contact.cnContactId != nil) {
|
|
cnContact = [self.contactsManager cnContactWithId:signalAccount.contact.cnContactId];
|
|
}
|
|
if (cnContact) {
|
|
if (updatedNameComponents) {
|
|
CNMutableContact *updatedContact = [cnContact mutableCopy];
|
|
updatedContact.givenName = updatedNameComponents.givenName;
|
|
updatedContact.familyName = updatedNameComponents.familyName;
|
|
cnContact = updatedContact;
|
|
}
|
|
|
|
if (shouldEditImmediately) {
|
|
// Not actually a "new" contact, but this brings up the edit form rather than the "Read" form
|
|
// saving our users a tap in some cases when we already know they want to edit.
|
|
contactViewController = [CNContactViewController viewControllerForNewContact:cnContact];
|
|
|
|
// Default title is "New Contact". We could give a more descriptive title, but anything
|
|
// seems redundant - the context is sufficiently clear.
|
|
contactViewController.title = @"";
|
|
} else {
|
|
contactViewController = [CNContactViewController viewControllerForContact:cnContact];
|
|
}
|
|
}
|
|
|
|
if (!contactViewController) {
|
|
CNMutableContact *newContact = [CNMutableContact new];
|
|
if (address.phoneNumber) {
|
|
CNPhoneNumber *phoneNumber = [CNPhoneNumber phoneNumberWithStringValue:address.phoneNumber];
|
|
CNLabeledValue<CNPhoneNumber *> *labeledPhoneNumber =
|
|
[CNLabeledValue labeledValueWithLabel:CNLabelPhoneNumberMain value:phoneNumber];
|
|
newContact.phoneNumbers = @[ labeledPhoneNumber ];
|
|
}
|
|
|
|
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
|
|
newContact.givenName = [self.profileManagerImpl givenNameForAddress:address transaction:transaction];
|
|
newContact.familyName = [self.profileManagerImpl familyNameForAddress:address transaction:transaction];
|
|
newContact.imageData
|
|
= UIImagePNGRepresentation([self.profileManagerImpl profileAvatarForAddress:address
|
|
downloadIfMissing:YES
|
|
transaction:transaction]);
|
|
}];
|
|
|
|
if (updatedNameComponents) {
|
|
newContact.givenName = updatedNameComponents.givenName;
|
|
newContact.familyName = updatedNameComponents.familyName;
|
|
}
|
|
contactViewController = [CNContactViewController viewControllerForNewContact:newContact];
|
|
}
|
|
|
|
contactViewController.allowsActions = NO;
|
|
contactViewController.allowsEditing = YES;
|
|
contactViewController.edgesForExtendedLayout = UIRectEdgeNone;
|
|
contactViewController.view.backgroundColor = Theme.backgroundColor;
|
|
|
|
return contactViewController;
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|