Merge branch 'mkirk/contact-merging'

pull/1/head
Michael Kirk 7 years ago
commit efa3c81f68

@ -294,6 +294,7 @@
4523149E1F7E916B003A428C /* SlideOffAnimatedTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523149D1F7E916B003A428C /* SlideOffAnimatedTransition.swift */; };
452314A01F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */; };
4523D016206EDC2B00A2AB51 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523D015206EDC2B00A2AB51 /* LRUCache.swift */; };
452B999020A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452B998F20A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift */; };
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */; };
452C7CA72037628B003D51A5 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170D51E315310003FC1F2 /* Weak.swift */; };
452D1AF12081059C00A67F7F /* StringAdditionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452D1AF02081059C00A67F7F /* StringAdditionsTest.swift */; };
@ -926,6 +927,7 @@
4523149D1F7E916B003A428C /* SlideOffAnimatedTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlideOffAnimatedTransition.swift; path = UserInterface/SlideOffAnimatedTransition.swift; sourceTree = "<group>"; };
4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectionalPanGestureRecognizer.swift; sourceTree = "<group>"; };
4523D015206EDC2B00A2AB51 /* LRUCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
452B998F20A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactShareToExistingContactViewController.swift; sourceTree = "<group>"; };
452C468E1E427E200087B011 /* OutboundCallInitiator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutboundCallInitiator.swift; sourceTree = "<group>"; };
452D1AF02081059C00A67F7F /* StringAdditionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringAdditionsTest.swift; sourceTree = "<group>"; };
452D1AF220810B6F00A67F7F /* String+OWS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+OWS.swift"; sourceTree = "<group>"; };
@ -1681,6 +1683,7 @@
34B3F86E1E8DF1700035BE1A /* SignalsNavigationController.m */,
340FC897204DAC8D007AEB0F /* ThreadSettings */,
34D1F0BE1F8EC1760066283D /* Utils */,
452B998F20A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift */,
);
path = ViewControllers;
sourceTree = "<group>";
@ -3238,6 +3241,7 @@
340FC8BC204DAC8D007AEB0F /* FingerprintViewController.m in Sources */,
450DF2051E0D74AC003D14BE /* Platform.swift in Sources */,
340FC8B2204DAC8D007AEB0F /* AdvancedSettingsTableViewController.m in Sources */,
452B999020A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift in Sources */,
346129991FD1E4DA00532771 /* SignalApp.m in Sources */,
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
343A65951FC47D5E000477A1 /* DebugUISyncMessages.m in Sources */,

@ -0,0 +1,133 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import UIKit
import ContactsUI
class AddContactShareToExistingContactViewController: ContactsPicker, ContactsPickerDelegate, CNContactViewControllerDelegate {
// TODO - there are some hard coded assumptions in this VC that assume we are *pushed* onto a
// navigation controller. That seems fine for now, but if we need to be presented as a modal,
// or need to notify our presenter about our dismisall or other contact actions, a delegate
// would be helpful. It seems like this would require some broad changes to the ContactShareViewHelper,
// so I've left it as is for now, since it happens to work.
// weak var addToExistingContactDelegate: AddContactShareToExistingContactViewControllerDelegate?
let contactShare: ContactShareViewModel
required init(contactShare: ContactShareViewModel) {
self.contactShare = contactShare
super.init(allowsMultipleSelection: false, subtitleCellType: .none)
self.contactsPickerDelegate = self
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc required public init(allowsMultipleSelection: Bool, subtitleCellType: SubtitleCellValue) {
fatalError("init(allowsMultipleSelection:subtitleCellType:) has not been implemented")
}
// MARK: - ContactsPickerDelegate
func contactsPicker(_: ContactsPicker, contactFetchDidFail error: NSError) {
owsFail("\(logTag) in \(#function) with error: \(error)")
guard let navigationController = self.navigationController else {
owsFail("\(logTag) in \(#function) navigationController was unexpectedly nil")
return
}
navigationController.popViewController(animated: true)
}
func contactsPickerDidCancel(_: ContactsPicker) {
Logger.debug("\(self.logTag) in \(#function)")
guard let navigationController = self.navigationController else {
owsFail("\(logTag) in \(#function) navigationController was unexpectedly nil")
return
}
navigationController.popViewController(animated: true)
}
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact) {
Logger.debug("\(self.logTag) in \(#function)")
guard let mergedContact: CNContact = self.contactShare.cnContact(mergedWithExistingContact: contact) else {
owsFail("\(logTag) in \(#function) mergedContact was unexpectedly nil")
return
}
// 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.
let contactViewController: CNContactViewController = CNContactViewController(forNewContact: mergedContact)
// Default title is "New Contact". We could give a more descriptive title, but anything
// seems redundant - the context is sufficiently clear.
contactViewController.title = ""
contactViewController.allowsActions = false
contactViewController.allowsEditing = true
contactViewController.delegate = self
guard let navigationController = self.navigationController else {
owsFail("\(logTag) in \(#function) navigationController was unexpectedly nil")
return
}
navigationController.pushViewController(contactViewController, animated: true)
}
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact]) {
Logger.debug("\(self.logTag) in \(#function)")
owsFail("\(logTag) only supports single contact select")
guard let navigationController = self.navigationController else {
owsFail("\(logTag) in \(#function) navigationController was unexpectedly nil")
return
}
navigationController.popViewController(animated: true)
}
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool {
return true
}
// MARK: - CNContactViewControllerDelegate
public func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
Logger.debug("\(self.logTag) in \(#function)")
guard let navigationController = self.navigationController else {
owsFail("\(logTag) in \(#function) navigationController was unexpectedly nil")
return
}
// TODO this is weird - ideally we'd do something like
// self.delegate?.didFinishAddingContact
// and the delegate, which knows about our presentation context could do the right thing.
//
// As it is, we happen to always be *pushing* this view controller onto a navcontroller, so the
// following works in all current cases.
//
// If we ever wanted to do something different, like present this in a modal, we'd have to rethink.
// We want to pop *this* view *and* the still presented CNContactViewController in a single animation.
// Note this happens for *cancel* and for *done*. Unfortunately, I don't know of a way to detect the difference
// between the two, since both just call this method.
guard let myIndex = navigationController.viewControllers.index(of: self) else {
owsFail("\(logTag) in \(#function) myIndex was unexpectedly nil")
navigationController.popViewController(animated: true)
navigationController.popViewController(animated: true)
return
}
let previousViewControllerIndex = navigationController.viewControllers.index(before: myIndex)
let previousViewController = navigationController.viewControllers[previousViewControllerIndex]
navigationController.popToViewController(previousViewController, animated: true)
}
}

@ -70,7 +70,7 @@ public class ContactShareViewHelper: NSObject, CNContactViewControllerDelegate {
}
@objc
public func inviteContact(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
public func showInviteContact(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
Logger.info("\(logTag) \(#function)")
guard MFMessageComposeViewController.canSendText() else {
@ -89,7 +89,7 @@ public class ContactShareViewHelper: NSObject, CNContactViewControllerDelegate {
inviteFlow.sendSMSTo(phoneNumbers: phoneNumbers)
}
func addToContacts(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
func showAddToContacts(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
Logger.info("\(logTag) \(#function)")
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
@ -144,7 +144,7 @@ public class ContactShareViewHelper: NSObject, CNContactViewControllerDelegate {
return
}
guard let systemContact = OWSContacts.systemContact(for: contactShare.dbRecord) else {
guard let systemContact = OWSContacts.systemContact(for: contactShare.dbRecord, imageData: contactShare.avatarImageData) else {
owsFail("\(logTag) Could not derive system contact.")
return
}
@ -188,22 +188,13 @@ public class ContactShareViewHelper: NSObject, CNContactViewControllerDelegate {
return
}
// TODO: Revisit this.
guard let firstPhoneNumber = contactShare.e164PhoneNumbers().first else {
owsFail("\(logTag) Missing phone number.")
return
}
// TODO: We need to modify OWSAddToContactViewController to take a OWSContact
// and merge it with an existing CNContact.
let viewController = OWSAddToContactViewController()
viewController.configure(withRecipientId: firstPhoneNumber)
guard let navigationController = fromViewController.navigationController else {
owsFail("\(logTag) missing navigationController")
return
}
let viewController = AddContactShareToExistingContactViewController(contactShare: contactShare)
navigationController.pushViewController(viewController, animated: true)
}

@ -501,13 +501,13 @@ class ContactViewController: OWSViewController, ContactShareViewHelperDelegate {
func didPressInvite() {
Logger.info("\(logTag) \(#function)")
self.contactShareViewHelper.inviteContact(contactShare: self.contactShare, fromViewController: self)
self.contactShareViewHelper.showInviteContact(contactShare: self.contactShare, fromViewController: self)
}
func didPressAddToContacts() {
Logger.info("\(logTag) \(#function)")
self.contactShareViewHelper.addToContacts(contactShare: self.contactShare, fromViewController: self)
self.contactShareViewHelper.showAddToContacts(contactShare: self.contactShare, fromViewController: self)
}
func didPressDismiss() {

@ -20,10 +20,6 @@ public protocol ContactsPickerDelegate: class {
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool
}
public extension ContactsPickerDelegate {
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool { return true }
}
@objc
public enum SubtitleCellValue: Int {
case phoneNumber, email, none
@ -72,15 +68,22 @@ public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableView
// Configuration
public weak var contactsPickerDelegate: ContactsPickerDelegate?
private let subtitleCellValue: SubtitleCellValue
private let multiSelectEnabled: Bool
private let allowedContactKeys: [CNKeyDescriptor] = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
CNContactThumbnailImageDataKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor,
CNContactPostalAddressesKey as CNKeyDescriptor
]
private let subtitleCellType: SubtitleCellValue
private let allowsMultipleSelection: Bool
private let allowedContactKeys: [CNKeyDescriptor] = ContactsFrameworkContactStoreAdaptee.allowedContactKeys
// MARK: - Initializers
@objc
required public init(allowsMultipleSelection: Bool, subtitleCellType: SubtitleCellValue) {
self.allowsMultipleSelection = allowsMultipleSelection
self.subtitleCellType = subtitleCellType
super.init(nibName: "ContactsPicker", bundle: nil)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Lifecycle Methods
@ -95,7 +98,7 @@ public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableView
tableView.estimatedRowHeight = 60.0
tableView.rowHeight = UITableViewAutomaticDimension
tableView.allowsMultipleSelection = multiSelectEnabled
tableView.allowsMultipleSelection = allowsMultipleSelection
tableView.separatorInset = UIEdgeInsets(top: 0, left: ContactCell.kSeparatorHInset, bottom: 0, right: 16)
@ -116,7 +119,7 @@ public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableView
let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(onTouchCancelButton))
self.navigationItem.leftBarButtonItem = cancelButton
if multiSelectEnabled {
if allowsMultipleSelection {
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(onTouchDoneButton))
self.navigationItem.rightBarButtonItem = doneButton
}
@ -126,20 +129,6 @@ public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableView
tableView.register(ContactCell.self, forCellReuseIdentifier: contactCellReuseIdentifier)
}
// MARK: - Initializers
@objc
required public init(delegate: ContactsPickerDelegate?, multiSelection: Bool, subtitleCellType: SubtitleCellValue) {
multiSelectEnabled = multiSelection
subtitleCellValue = subtitleCellType
super.init(nibName: nil, bundle: nil)
contactsPickerDelegate = delegate
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Contact Operations
private func reloadContacts() {
@ -162,7 +151,6 @@ public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableView
let error = NSError(domain: "contactsPickerErrorDomain", code: 1, userInfo: [NSLocalizedDescriptionKey: "No Contacts Access"])
self.contactsPickerDelegate?.contactsPicker(self, contactFetchDidFail: error)
errorHandler(error)
self.dismiss(animated: true, completion: nil)
})
alert.addAction(cancelAction)
@ -231,13 +219,16 @@ public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableView
// MARK: - Table View Delegates
open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: contactCellReuseIdentifier, for: indexPath) as! ContactCell
guard let cell = tableView.dequeueReusableCell(withIdentifier: contactCellReuseIdentifier, for: indexPath) as? ContactCell else {
owsFail("\(logTag) in \(#function) cell had unexpected type")
return UITableViewCell()
}
let dataSource = filteredSections
let cnContact = dataSource[indexPath.section][indexPath.row]
let contact = Contact(systemContact: cnContact)
cell.configure(contact: contact, subtitleType: subtitleCellValue, contactsManager: self.contactsManager)
cell.configure(contact: contact, subtitleType: subtitleCellType, showsWhenSelected: self.allowsMultipleSelection, contactsManager: self.contactsManager)
let isSelected = selectedContacts.contains(where: { $0.uniqueId == contact.uniqueId })
cell.isSelected = isSelected
@ -274,11 +265,9 @@ public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableView
selectedContacts.append(selectedContact)
if !multiSelectEnabled {
//Single selection code
self.dismiss(animated: true) {
self.contactsPickerDelegate?.contactsPicker(self, didSelectContact: selectedContact)
}
if !allowsMultipleSelection {
// Single selection code
self.contactsPickerDelegate?.contactsPicker(self, didSelectContact: selectedContact)
}
}
@ -313,12 +302,10 @@ public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableView
func onTouchCancelButton() {
contactsPickerDelegate?.contactsPickerDidCancel(self)
dismiss(animated: true, completion: nil)
}
func onTouchDoneButton() {
contactsPickerDelegate?.contactsPicker(self, didSelectMultipleContacts: selectedContacts)
dismiss(animated: true, completion: nil)
}
// MARK: - Search Actions

@ -2124,7 +2124,7 @@ typedef enum : NSUInteger {
OWSAssertIsOnMainThread();
OWSAssert(contactShare);
[self.contactShareViewHelper inviteContactWithContactShare:contactShare fromViewController:self];
[self.contactShareViewHelper showInviteContactWithContactShare:contactShare fromViewController:self];
}
- (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare
@ -2132,7 +2132,7 @@ typedef enum : NSUInteger {
OWSAssertIsOnMainThread();
OWSAssert(contactShare);
[self.contactShareViewHelper addToContactsWithContactShare:contactShare fromViewController:self];
[self.contactShareViewHelper showAddToContactsWithContactShare:contactShare fromViewController:self];
}
- (void)didTapFailedIncomingAttachment:(ConversationViewItem *)viewItem
@ -2571,7 +2571,8 @@ typedef enum : NSUInteger {
- (void)chooseContactForSending
{
ContactsPicker *contactsPicker =
[[ContactsPicker alloc] initWithDelegate:self multiSelection:NO subtitleCellType:SubtitleCellValueNone];
[[ContactsPicker alloc] initWithAllowsMultipleSelection:NO subtitleCellType:SubtitleCellValueNone];
contactsPicker.contactsPickerDelegate = self;
contactsPicker.title
= NSLocalizedString(@"CONTACT_PICKER_TITLE", @"navbar title for contact picker when sharing a contact");
@ -4978,11 +4979,13 @@ interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransiti
- (void)contactsPickerDidCancel:(ContactsPicker *)contactsPicker
{
DDLogDebug(@"%@ in %s", self.logTag, __PRETTY_FUNCTION__);
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)contactsPicker:(ContactsPicker *)contactsPicker contactFetchDidFail:(NSError *)error
{
DDLogDebug(@"%@ in %s with error %@", self.logTag, __PRETTY_FUNCTION__, error);
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)contactsPicker:(ContactsPicker *)contactsPicker didSelectContact:(Contact *)contact
@ -4992,6 +4995,8 @@ interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransiti
DDLogDebug(@"%@ in %s with contact: %@", self.logTag, __PRETTY_FUNCTION__, contact);
[self dismissViewControllerAnimated:YES completion:nil];
OWSContact *_Nullable contactShareRecord = [OWSContacts contactForSystemContact:contact.cnContact];
if (!contactShareRecord) {
DDLogError(@"%@ Could not convert system contact.", self.logTag);
@ -4999,20 +5004,20 @@ interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransiti
}
BOOL isProfileAvatar = NO;
UIImage *_Nullable avatarImage = contact.image;
NSData *_Nullable avatarImageData = contact.imageData;
for (NSString *recipientId in contact.textSecureIdentifiers) {
if (avatarImage) {
if (avatarImageData) {
break;
}
avatarImage = [self.contactsManager profileImageForPhoneIdentifier:recipientId];
if (avatarImage) {
avatarImageData = [self.contactsManager profileImageDataForPhoneIdentifier:recipientId];
if (avatarImageData) {
isProfileAvatar = YES;
}
}
contactShareRecord.isProfileAvatar = isProfileAvatar;
ContactShareViewModel *contactShare =
[[ContactShareViewModel alloc] initWithContactShareRecord:contactShareRecord avatarImage:avatarImage];
[[ContactShareViewModel alloc] initWithContactShareRecord:contactShareRecord avatarImageData:avatarImageData];
// TODO: We should probably show this in the same navigation view controller.
ApproveContactShareViewController *approveContactShare =
@ -5029,6 +5034,7 @@ interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransiti
- (void)contactsPicker:(ContactsPicker *)contactsPicker didSelectMultipleContacts:(NSArray<Contact *> *)contacts
{
OWSFail(@"%@ in %s with contacts: %@", self.logTag, __PRETTY_FUNCTION__, contacts);
[self dismissViewControllerAnimated:YES completion:nil];
}
- (BOOL)contactsPicker:(ContactsPicker *)contactsPicker shouldSelectContact:(Contact *)contact

@ -88,6 +88,8 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact]) {
Logger.debug("\(TAG) didSelectContacts:\(contacts)")
self.presentingViewController.dismiss(animated: true)
guard let inviteChannel = channel else {
Logger.error("\(TAG) unexpected nil channel after returning from contact picker.")
return
@ -124,14 +126,17 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
func contactsPicker(_: ContactsPicker, contactFetchDidFail error: NSError) {
Logger.error("\(self.logTag) in \(#function) with error: \(error)")
self.presentingViewController.dismiss(animated: true)
}
func contactsPickerDidCancel(_: ContactsPicker) {
Logger.debug("\(self.logTag) in \(#function)")
self.presentingViewController.dismiss(animated: true)
}
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact) {
owsFail("\(logTag) in \(#function) InviteFlow only supports multi-select")
self.presentingViewController.dismiss(animated: true)
}
// MARK: SMS
@ -146,7 +151,8 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
return UIAlertAction(title: messageTitle, style: .default) { _ in
Logger.debug("\(self.TAG) Chose message.")
self.channel = .message
let picker = ContactsPicker(delegate: self, multiSelection: true, subtitleCellType: .phoneNumber)
let picker = ContactsPicker(allowsMultipleSelection: true, subtitleCellType: .phoneNumber)
picker.contactsPickerDelegate = self
picker.title = NSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
let navigationController = UINavigationController(rootViewController: picker)
self.presentingViewController.present(navigationController, animated: true)
@ -209,7 +215,8 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
Logger.debug("\(self.TAG) Chose mail.")
self.channel = .mail
let picker = ContactsPicker(delegate: self, multiSelection: true, subtitleCellType: .email)
let picker = ContactsPicker(allowsMultipleSelection: true, subtitleCellType: .email)
picker.contactsPickerDelegate = self
picker.title = NSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
let navigationController = UINavigationController(rootViewController: picker)
self.presentingViewController.present(navigationController, animated: true)

@ -630,11 +630,11 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
}
func didTapSendInvite(toContactShare contactShare: ContactShareViewModel) {
contactShareViewHelper.inviteContact(contactShare: contactShare, fromViewController: self)
contactShareViewHelper.showInviteContact(contactShare: contactShare, fromViewController: self)
}
func didTapShowAddToContactUI(forContactShare contactShare: ContactShareViewModel) {
contactShareViewHelper.addToContacts(contactShare: contactShare, fromViewController: self)
contactShareViewHelper.showAddToContacts(contactShare: contactShare, fromViewController: self)
}
var audioAttachmentPlayer: OWSAudioPlayer?

@ -19,6 +19,7 @@ class ContactCell: UITableViewCell {
var subtitleLabel: UILabel
var contact: Contact?
var showsWhenSelected: Bool = false
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
self.contactImageView = AvatarImageView()
@ -59,7 +60,9 @@ class ContactCell: UITableViewCell {
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
accessoryType = selected ? .checkmark : .none
if showsWhenSelected {
accessoryType = selected ? .checkmark : .none
}
}
func didChangePreferredContentSize() {
@ -67,8 +70,9 @@ class ContactCell: UITableViewCell {
self.subtitleLabel.font = UIFont.ows_dynamicTypeSubheadline
}
func configure(contact: Contact, subtitleType: SubtitleCellValue, contactsManager: OWSContactsManager) {
func configure(contact: Contact, subtitleType: SubtitleCellValue, showsWhenSelected: Bool, contactsManager: OWSContactsManager) {
self.contact = contact
self.showsWhenSelected = showsWhenSelected
titleLabel.attributedText = contact.cnContact?.formattedFullName(font: titleLabel.font)
updateSubtitle(subtitleType: subtitleType, contact: contact)

@ -8,18 +8,26 @@ import Foundation
public class ContactShareViewModel: NSObject {
public let dbRecord: OWSContact
public let avatarImage: UIImage?
public required init(contactShareRecord: OWSContact, avatarImage: UIImage?) {
public let avatarImageData: Data?
lazy var avatarImage: UIImage? = {
guard let avatarImageData = self.avatarImageData else {
return nil
}
return UIImage(data: avatarImageData)
}()
public required init(contactShareRecord: OWSContact, avatarImageData: Data?) {
self.dbRecord = contactShareRecord
self.avatarImage = avatarImage
self.avatarImageData = avatarImageData
}
public convenience init(contactShareRecord: OWSContact, transaction: YapDatabaseReadTransaction) {
if let avatarAttachment = contactShareRecord.avatarAttachment(with: transaction) as? TSAttachmentStream {
self.init(contactShareRecord: contactShareRecord, avatarImage: avatarAttachment.image())
self.init(contactShareRecord: contactShareRecord, avatarImageData: avatarAttachment.validStillImageData())
} else {
self.init(contactShareRecord: contactShareRecord, avatarImage: nil)
self.init(contactShareRecord: contactShareRecord, avatarImageData: nil)
}
}
@ -109,6 +117,16 @@ public class ContactShareViewModel: NSObject {
return dbRecord.isProfileAvatar
}
public func cnContact(mergedWithExistingContact existingContact: Contact) -> CNContact? {
guard let newCNContact = OWSContacts.systemContact(for: self.dbRecord, imageData: self.avatarImageData) else {
owsFail("\(logTag) in \(#function) newCNContact was unexpectedly nil")
return nil
}
return existingContact.buildCNContact(mergedWithNewContact: newCNContact)
}
public func copy(withNamePrefix namePrefix: String?,
givenName: String?,
middleName: String?,
@ -118,7 +136,7 @@ public class ContactShareViewModel: NSObject {
// TODO move the `copy` logic into the view model?
let newDbRecord = dbRecord.copy(withNamePrefix: namePrefix, givenName: givenName, middleName: middleName, familyName: familyName, nameSuffix: nameSuffix)
return ContactShareViewModel(contactShareRecord: newDbRecord, avatarImage: self.avatarImage)
return ContactShareViewModel(contactShareRecord: newDbRecord, avatarImageData: self.avatarImageData)
}
public func newContact(withNamePrefix namePrefix: String?,
@ -134,7 +152,7 @@ public class ContactShareViewModel: NSObject {
familyName: familyName,
nameSuffix: nameSuffix)
return ContactShareViewModel(contactShareRecord: newDbRecord, avatarImage: self.avatarImage)
return ContactShareViewModel(contactShareRecord: newDbRecord, avatarImageData: self.avatarImageData)
}
}

@ -308,7 +308,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)presentContactViewControllerForRecipientId:(NSString *)recipientId
fromViewController:(UIViewController<ContactEditingDelegate> *)fromViewController
editImmediately:(BOOL)shouldEditImmediately
addToExistingCnContact:(CNContact *_Nullable)addToExistingCnContact
addToExistingCnContact:(CNContact *_Nullable)existingContact
{
SignalAccount *signalAccount = [self signalAccountForRecipientId:recipientId];
@ -325,8 +325,8 @@ NS_ASSUME_NONNULL_BEGIN
CNContactViewController *_Nullable contactViewController;
CNContact *_Nullable cnContact = nil;
if (addToExistingCnContact) {
CNMutableContact *updatedContact = [addToExistingCnContact mutableCopy];
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

@ -159,11 +159,11 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion);
// TODO: Populate avatar image.
BOOL isProfileAvatar = NO;
UIImage *_Nullable avatarImage = nil;
NSData *_Nullable avatarImageData = nil;
contact.isProfileAvatar = isProfileAvatar;
ContactShareViewModel *contactShare =
[[ContactShareViewModel alloc] initWithContactShareRecord:contact avatarImage:avatarImage];
[[ContactShareViewModel alloc] initWithContactShareRecord:contact avatarImageData:avatarImageData];
ApproveContactShareViewController *approvalVC =
[[ApproveContactShareViewController alloc] initWithContactShare:contactShare

@ -81,6 +81,7 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification;
- (nullable UIImage *)systemContactImageForPhoneIdentifier:(nullable NSString *)identifier;
- (nullable UIImage *)profileImageForPhoneIdentifier:(nullable NSString *)identifier;
- (nullable NSData *)profileImageDataForPhoneIdentifier:(nullable NSString *)identifier;
- (nullable UIImage *)imageForPhoneIdentifier:(nullable NSString *)identifier;
- (NSAttributedString *)formattedDisplayNameForSignalAccount:(SignalAccount *)signalAccount font:(UIFont *_Nonnull)font;

@ -739,6 +739,11 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
return [self.profileManager profileAvatarForRecipientId:identifier];
}
- (nullable NSData *)profileImageDataForPhoneIdentifier:(nullable NSString *)identifier
{
return [self.profileManager profileAvatarDataForRecipientId:identifier];
}
- (UIImage *_Nullable)imageForPhoneIdentifier:(NSString *_Nullable)identifier
{
// Prefer the contact image from the local address book if available

@ -20,6 +20,7 @@ protocol ContactStoreAdaptee {
func startObservingChanges(changeHandler: @escaping () -> Void)
}
public
class ContactsFrameworkContactStoreAdaptee: ContactStoreAdaptee {
let TAG = "[ContactsFrameworkContactStoreAdaptee]"
private let contactStore = CNContactStore()
@ -29,7 +30,7 @@ class ContactsFrameworkContactStoreAdaptee: ContactStoreAdaptee {
let supportsContactEditing = true
private let allowedContactKeys: [CNKeyDescriptor] = [
public static let allowedContactKeys: [CNKeyDescriptor] = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
CNContactThumbnailImageDataKey as CNKeyDescriptor, // TODO full image instead of thumbnail?
CNContactPhoneNumbersKey as CNKeyDescriptor,
@ -92,7 +93,7 @@ class ContactsFrameworkContactStoreAdaptee: ContactStoreAdaptee {
func fetchContacts() -> Result<[Contact], Error> {
var systemContacts = [CNContact]()
do {
let contactFetchRequest = CNContactFetchRequest(keysToFetch: self.allowedContactKeys)
let contactFetchRequest = CNContactFetchRequest(keysToFetch: ContactsFrameworkContactStoreAdaptee.allowedContactKeys)
contactFetchRequest.sortOrder = .userDefault
try self.contactStore.enumerateContacts(with: contactFetchRequest) { (contact, _) -> Void in
systemContacts.append(contact)

@ -75,6 +75,7 @@ extern const NSUInteger kOWSProfileManager_MaxAvatarDiameter;
- (nullable NSString *)profileNameForRecipientId:(NSString *)recipientId;
- (nullable UIImage *)profileAvatarForRecipientId:(NSString *)recipientId;
- (nullable NSData *)profileAvatarDataForRecipientId:(NSString *)recipientId;
- (void)updateProfileForRecipientId:(NSString *)recipientId
profileNameEncrypted:(nullable NSData *)profileNameEncrypted

@ -783,6 +783,20 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640;
return nil;
}
- (nullable NSData *)profileAvatarDataForRecipientId:(NSString *)recipientId
{
OWSAssert(recipientId.length > 0);
OWSUserProfile *userProfile =
[OWSUserProfile getOrBuildUserProfileForRecipientId:recipientId dbConnection:self.dbConnection];
if (userProfile.avatarFileName.length > 0) {
return [self loadProfileDataWithFilename:userProfile.avatarFileName];
}
return nil;
}
- (void)downloadAvatarForUserProfile:(OWSUserProfile *)userProfile
{
OWSAssert(userProfile);

@ -7,10 +7,7 @@
NS_ASSUME_NONNULL_BEGIN
/**
*
* Contact represents relevant information related to a contact from the user's
* contact list.
*
* An adapter for the system contacts
*/
@class CNContact;
@ -33,6 +30,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) BOOL isSignalContact;
#if TARGET_OS_IOS
@property (nullable, readonly, nonatomic) UIImage *image;
@property (nullable, readonly, nonatomic) NSData *imageData;
@property (nullable, nonatomic, readonly) CNContact *cnContact;
#endif // TARGET_OS_IOS
@ -49,6 +47,9 @@ NS_ASSUME_NONNULL_BEGIN
#endif // TARGET_OS_IOS
+ (NSComparator)comparatorSortingNamesByFirstThenLast:(BOOL)firstNameOrdering;
+ (NSString *)formattedFullNameWithCNContact:(CNContact *)cnContact NS_SWIFT_NAME(formattedFullName(cnContact:));
- (CNContact *)buildCNContactMergedWithNewContact:(CNContact *)newCNContact NS_SWIFT_NAME(buildCNContact(mergedWithNewContact:));
@end

@ -4,6 +4,7 @@
#import "Contact.h"
#import "Cryptography.h"
#import "NSString+SSK.h"
#import "OWSPrimaryStorage.h"
#import "PhoneNumber.h"
#import "SignalRecipient.h"
@ -16,7 +17,6 @@ NS_ASSUME_NONNULL_BEGIN
@interface Contact ()
@property (readonly, nonatomic) NSMutableDictionary<NSString *, NSString *> *phoneNumberNameMap;
@property (readonly, nonatomic) NSData *imageData;
@end
@ -38,9 +38,9 @@ NS_ASSUME_NONNULL_BEGIN
}
_cnContact = contact;
_firstName = [self trimName:contact.givenName];
_lastName = [self trimName:contact.familyName];
_fullName = [CNContactFormatter stringFromContact:contact style:CNContactFormatterStyleFullName];
_firstName = contact.givenName.ows_stripped;
_lastName = contact.familyName.ows_stripped;
_fullName = [Contact formattedFullNameWithCNContact:contact];
_uniqueId = contact.identifier;
NSMutableArray<NSString *> *phoneNumbers = [NSMutableArray new];
@ -125,11 +125,6 @@ NS_ASSUME_NONNULL_BEGIN
return _image;
}
- (NSString *)trimName:(NSString *)name
{
return [name stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
+ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey
{
if ([propertyKey isEqualToString:@"cnContact"] || [propertyKey isEqualToString:@"image"]) {
@ -250,6 +245,11 @@ NS_ASSUME_NONNULL_BEGIN
};
}
+ (NSString *)formattedFullNameWithCNContact:(CNContact *)cnContact
{
return [CNContactFormatter stringFromContact:cnContact style:CNContactFormatterStyleFullName].ows_stripped;
}
- (NSString *)nameForPhoneNumber:(NSString *)recipientId
{
OWSAssert(recipientId.length > 0);
@ -290,6 +290,63 @@ NS_ASSUME_NONNULL_BEGIN
return hash;
}
- (CNContact *)buildCNContactMergedWithNewContact:(CNContact *)newCNContact
{
CNMutableContact *_Nullable mergedCNContact = [self.cnContact mutableCopy];
if (!mergedCNContact) {
OWSFail(@"%@ in %s mergedCNContact was unexpectedly nil", self.logTag, __PRETTY_FUNCTION__);
return [CNContact new];
}
// Name
NSString *formattedFullName = [self.class formattedFullNameWithCNContact:mergedCNContact];
// merged all or nothing - do not try to piece-meal merge.
if (formattedFullName.length == 0) {
mergedCNContact.namePrefix = newCNContact.namePrefix.ows_stripped;
mergedCNContact.givenName = newCNContact.givenName.ows_stripped;
mergedCNContact.middleName = newCNContact.middleName.ows_stripped;
mergedCNContact.familyName = newCNContact.familyName.ows_stripped;
mergedCNContact.nameSuffix = newCNContact.nameSuffix.ows_stripped;
}
// Phone Numbers
NSSet<PhoneNumber *> *existingPhoneNumberSet = [NSSet setWithArray:self.parsedPhoneNumbers];
NSMutableArray<CNLabeledValue<CNPhoneNumber *> *> *mergedPhoneNumbers = [mergedCNContact.phoneNumbers mutableCopy];
for (CNLabeledValue<CNPhoneNumber *> *labeledPhoneNumber in newCNContact.phoneNumbers) {
PhoneNumber *_Nullable parsedPhoneNumber = [PhoneNumber tryParsePhoneNumberFromUserSpecifiedText:labeledPhoneNumber.value.stringValue];
if (parsedPhoneNumber && ![existingPhoneNumberSet containsObject:parsedPhoneNumber]) {
[mergedPhoneNumbers addObject:labeledPhoneNumber];
}
}
mergedCNContact.phoneNumbers = mergedPhoneNumbers;
// Emails
NSSet<NSString *> *existingEmailSet = [NSSet setWithArray:self.emails];
NSMutableArray<CNLabeledValue<NSString *> *> *mergedEmailAddresses = [mergedCNContact.emailAddresses mutableCopy];
for (CNLabeledValue<NSString *> *labeledEmail in newCNContact.emailAddresses) {
NSString *normalizedValue = labeledEmail.value.ows_stripped;
if (![existingEmailSet containsObject:normalizedValue]) {
[mergedEmailAddresses addObject:labeledEmail];
}
}
mergedCNContact.emailAddresses = mergedEmailAddresses;
// Address
// merged all or nothing - do not try to piece-meal merge.
if (mergedCNContact.postalAddresses.count == 0) {
mergedCNContact.postalAddresses = newCNContact.postalAddresses;
}
// Avatar
if (!mergedCNContact.imageData) {
mergedCNContact.imageData = newCNContact.imageData;
}
return [mergedCNContact copy];
}
@end
NS_ASSUME_NONNULL_END

@ -40,6 +40,7 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable UIImage *)image;
- (nullable UIImage *)thumbnailImage;
- (nullable NSData *)thumbnailData;
- (nullable NSData *)validStillImageData;
#endif
- (BOOL)isAnimated;

@ -331,6 +331,30 @@ NS_ASSUME_NONNULL_BEGIN
}
}
- (nullable NSData *)validStillImageData
{
if ([self isVideo]) {
OWSFail(@"%@ in %s isVideo was unexpectedly true", self.logTag, __PRETTY_FUNCTION__);
return nil;
}
if ([self isAnimated]) {
OWSFail(@"%@ in %s isAnimated was unexpectedly true", self.logTag, __PRETTY_FUNCTION__);
return nil;
}
NSURL *_Nullable mediaUrl = [self mediaURL];
if (!mediaUrl) {
return nil;
}
NSData *data = [NSData dataWithContentsOfURL:mediaUrl];
if (![data ows_isValidImage]) {
return nil;
}
return data;
}
+ (BOOL)hasThumbnailForMimeType:(NSString *)contentType
{
return ([MIMETypeUtil isVideo:contentType] || [MIMETypeUtil isImage:contentType] ||

@ -166,7 +166,7 @@ NSString *NSStringForContactAddressType(OWSContactAddressType value);
#pragma mark - System Contact Conversion
+ (nullable OWSContact *)contactForSystemContact:(CNContact *)systemContact;
+ (nullable CNContact *)systemContactForContact:(OWSContact *)contact;
+ (nullable CNContact *)systemContactForContact:(OWSContact *)contact imageData:(nullable NSData *)imageData;
#pragma mark -

@ -3,6 +3,7 @@
//
#import "OWSContact.h"
#import "Contact.h"
#import "MimeTypeUtil.h"
#import "NSString+SSK.h"
#import "OWSContact+Private.h"
@ -371,8 +372,8 @@ NSString *NSStringForContactAddressType(OWSContactAddressType value)
- (void)ensureDisplayName
{
if (_displayName.length < 1) {
CNContact *_Nullable systemContact = [OWSContacts systemContactForContact:self];
_displayName = [CNContactFormatter stringFromContact:systemContact style:CNContactFormatterStyleFullName];
CNContact *_Nullable cnContact = [OWSContacts systemContactForContact:self imageData:nil];
_displayName = [Contact formattedFullNameWithCNContact:cnContact];
}
if (_displayName.length < 1) {
// Fall back to using the organization name.
@ -692,7 +693,7 @@ NSString *NSStringForContactAddressType(OWSContactAddressType value)
return contact;
}
+ (nullable CNContact *)systemContactForContact:(OWSContact *)contact
+ (nullable CNContact *)systemContactForContact:(OWSContact *)contact imageData:(nullable NSData *)imageData
{
if (!contact) {
OWSProdLogAndFail(@"%@ Missing contact.", self.logTag);
@ -789,11 +790,7 @@ NSString *NSStringForContactAddressType(OWSContactAddressType value)
}
}
systemContact.postalAddresses = systemAddresses;
// TODO: Avatar
// @property (readonly, copy, nullable, NS_NONATOMIC_IOSONLY) NSData *imageData;
// @property (readonly, copy, nullable, NS_NONATOMIC_IOSONLY) NSData *thumbnailImageData;
systemContact.imageData = imageData;
return systemContact;
}
@ -815,7 +812,8 @@ NSString *NSStringForContactAddressType(OWSContactAddressType value)
{
OWSAssert(contact);
CNContact *_Nullable systemContact = [self systemContactForContact:contact];
// TODO pass in image for vcard
CNContact *_Nullable systemContact = [self systemContactForContact:contact imageData:nil];
if (!systemContact) {
return nil;
}

Loading…
Cancel
Save