diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index ff258445f..ff09cf586 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -166,6 +166,7 @@ 348BB254209CD4B80047AEC2 /* ContactFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348BB253209CD4B80047AEC2 /* ContactFieldView.swift */; }; 348BB25A209CF8E50047AEC2 /* TappableStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348BB258209CF8E40047AEC2 /* TappableStackView.swift */; }; 348BB25B209CF8E50047AEC2 /* TappableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348BB259209CF8E50047AEC2 /* TappableView.swift */; }; + 348BB25D20A0C5530047AEC2 /* ContactShareViewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348BB25C20A0C5530047AEC2 /* ContactShareViewHelper.swift */; }; 3496744D2076768700080B5F /* OWSMessageBubbleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496744C2076768700080B5F /* OWSMessageBubbleView.m */; }; 3496744F2076ACD000080B5F /* LongTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496744E2076ACCE00080B5F /* LongTextViewController.swift */; }; 34A55F3720485465002CC6DE /* OWS2FARegistrationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A55F3520485464002CC6DE /* OWS2FARegistrationViewController.m */; }; @@ -766,6 +767,7 @@ 348BB253209CD4B80047AEC2 /* ContactFieldView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ContactFieldView.swift; path = SignalMessaging/attachments/ContactFieldView.swift; sourceTree = SOURCE_ROOT; }; 348BB258209CF8E40047AEC2 /* TappableStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TappableStackView.swift; path = SignalMessaging/Views/TappableStackView.swift; sourceTree = SOURCE_ROOT; }; 348BB259209CF8E50047AEC2 /* TappableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TappableView.swift; path = SignalMessaging/Views/TappableView.swift; sourceTree = SOURCE_ROOT; }; + 348BB25C20A0C5530047AEC2 /* ContactShareViewHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactShareViewHelper.swift; sourceTree = ""; }; 348F2EAD1F0D21BC00D4ECE0 /* DeviceSleepManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceSleepManager.swift; sourceTree = ""; }; 3495BC911F1426B800B478F5 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = translations/ar.lproj/Localizable.strings; sourceTree = ""; }; 3496744B2076768600080B5F /* OWSMessageBubbleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageBubbleView.h; sourceTree = ""; }; @@ -1641,6 +1643,7 @@ 34D5CCA71EAE3D30005515DB /* AvatarViewHelper.h */, 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */, 34B3F83B1E8DF1700035BE1A /* CallViewController.swift */, + 348BB25C20A0C5530047AEC2 /* ContactShareViewHelper.swift */, 34B3F83E1E8DF1700035BE1A /* ContactsPicker.swift */, 34B3F83F1E8DF1700035BE1A /* ContactsPicker.xib */, 34E88D252098C5AE00A608F4 /* ContactViewController.swift */, @@ -1651,6 +1654,7 @@ 34BECE2C1F7ABCE000D7438D /* GifPicker */, 34386A4C207D0C01009F5D9C /* HomeView */, 34B3F84C1E8DF1700035BE1A /* InviteFlow.swift */, + 4542DF53208D40AC007B4E76 /* LoadingViewController.swift */, 3496744E2076ACCE00080B5F /* LongTextViewController.swift */, 45B9EE9A200E91FB005D2F2D /* MediaDetailViewController.h */, 45B9EE9B200E91FB005D2F2D /* MediaDetailViewController.m */, @@ -1677,7 +1681,6 @@ 34B3F86E1E8DF1700035BE1A /* SignalsNavigationController.m */, 340FC897204DAC8D007AEB0F /* ThreadSettings */, 34D1F0BE1F8EC1760066283D /* Utils */, - 4542DF53208D40AC007B4E76 /* LoadingViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -3249,6 +3252,7 @@ 4542DF54208D40AC007B4E76 /* LoadingViewController.swift in Sources */, 34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */, 34D1F0B71F87F8850066283D /* OWSGenericAttachmentView.m in Sources */, + 348BB25D20A0C5530047AEC2 /* ContactShareViewHelper.swift in Sources */, 34B3F8801E8DF1700035BE1A /* InviteFlow.swift in Sources */, 457C87B82032645C008D52D6 /* DebugUINotifications.swift in Sources */, 340FC8D0205BF2FA007AEB0F /* OWSBackupIO.m in Sources */, diff --git a/Signal/src/ViewControllers/ContactShareViewHelper.swift b/Signal/src/ViewControllers/ContactShareViewHelper.swift new file mode 100644 index 000000000..eb717d337 --- /dev/null +++ b/Signal/src/ViewControllers/ContactShareViewHelper.swift @@ -0,0 +1,289 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import SignalServiceKit +import ContactsUI +import MessageUI + +@objc +public protocol ContactShareViewHelperDelegate: class { + func didCreateOrEditContact() +} + +@objc +public class ContactShareViewHelper: NSObject, CNContactViewControllerDelegate { + + weak var delegate: ContactShareViewHelperDelegate? + + let contactShare: ContactShareViewModel + let contactsManager: OWSContactsManager + weak var fromViewController: UIViewController? + + public required init(contactShare: ContactShareViewModel, contactsManager: OWSContactsManager, fromViewController: UIViewController, delegate: ContactShareViewHelperDelegate) { + SwiftAssertIsOnMainThread(#function) + + self.contactShare = contactShare + self.contactsManager = contactsManager + self.fromViewController = fromViewController + self.delegate = delegate + + super.init() + } + + // MARK: Actions + + @objc + public func sendMessageToContact() { + Logger.info("\(logTag) \(#function)") + + presentThreadAndPeform(action: .compose) + } + + @objc + public func audioCallToContact() { + Logger.info("\(logTag) \(#function)") + + presentThreadAndPeform(action: .audioCall) + } + + @objc + public func videoCallToContact() { + Logger.info("\(logTag) \(#function)") + + presentThreadAndPeform(action: .videoCall) + } + + private func presentThreadAndPeform(action: ConversationViewAction) { + // TODO: We're taking the first Signal account id. We might + // want to let the user select if there's more than one. + let phoneNumbers = contactShare.systemContactsWithSignalAccountPhoneNumbers(contactsManager) + guard phoneNumbers.count > 0 else { + owsFail("\(logTag) missing Signal recipient id.") + return + } + guard phoneNumbers.count > 1 else { + let recipientId = phoneNumbers.first! + SignalApp.shared().presentConversation(forRecipientId: recipientId, action: action) + return + } + + showPhoneNumberPicker(phoneNumbers: phoneNumbers, completion: { (recipientId) in + SignalApp.shared().presentConversation(forRecipientId: recipientId, action: action) + }) + } + + @objc + public func inviteContact() { + Logger.info("\(logTag) \(#function)") + + guard let fromViewController = fromViewController else { + owsFail("\(logTag) missing fromViewController") + return + } + + guard MFMessageComposeViewController.canSendText() else { + Logger.info("\(logTag) Device cannot send text") + OWSAlerts.showErrorAlert(message: NSLocalizedString("UNSUPPORTED_FEATURE_ERROR", comment: "")) + return + } + let phoneNumbers = contactShare.e164PhoneNumbers() + guard phoneNumbers.count > 0 else { + owsFail("\(logTag) no phone numbers.") + return + } + + let inviteFlow = + InviteFlow(presentingViewController: fromViewController, contactsManager: contactsManager) + inviteFlow.sendSMSTo(phoneNumbers: phoneNumbers) + } + + func addToContacts() { + Logger.info("\(logTag) \(#function)") + + guard let fromViewController = fromViewController else { + owsFail("\(logTag) missing fromViewController") + return + } + + let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + + actionSheet.addAction(UIAlertAction(title: NSLocalizedString("CONVERSATION_SETTINGS_NEW_CONTACT", + comment: "Label for 'new contact' button in conversation settings view."), + style: .default) { _ in + self.didPressCreateNewContact() + }) + actionSheet.addAction(UIAlertAction(title: NSLocalizedString("CONVERSATION_SETTINGS_ADD_TO_EXISTING_CONTACT", + comment: "Label for 'new contact' button in conversation settings view."), + style: .default) { _ in + self.didPressAddToExistingContact() + }) + actionSheet.addAction(OWSAlerts.cancelAction) + + fromViewController.present(actionSheet, animated: true) + } + + private func showPhoneNumberPicker(phoneNumbers: [String], completion :@escaping ((String) -> Void)) { + + guard let fromViewController = fromViewController else { + owsFail("\(logTag) missing fromViewController") + return + } + + let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + + for phoneNumber in phoneNumbers { + actionSheet.addAction(UIAlertAction(title: PhoneNumber.bestEffortLocalizedPhoneNumber(withE164: phoneNumber), + style: .default) { _ in + completion(phoneNumber) + }) + } + actionSheet.addAction(OWSAlerts.cancelAction) + + fromViewController.present(actionSheet, animated: true) + } + + func didPressCreateNewContact() { + Logger.info("\(logTag) \(#function)") + + presentNewContactView() + } + + func didPressAddToExistingContact() { + Logger.info("\(logTag) \(#function)") + + presentSelectAddToExistingContactView() + } + + // MARK: - + + private func presentNewContactView() { + + guard let fromViewController = fromViewController else { + owsFail("\(logTag) missing fromViewController") + return + } + + guard contactsManager.supportsContactEditing else { + owsFail("\(logTag) Contact editing not supported") + return + } + + guard let systemContact = OWSContacts.systemContact(for: contactShare.dbRecord) else { + owsFail("\(logTag) Could not derive system contact.") + return + } + + guard contactsManager.isSystemContactsAuthorized else { + ContactsViewHelper.presentMissingContactAccessAlertController(from: fromViewController) + return + } + + let contactViewController = CNContactViewController(forNewContact: systemContact) + contactViewController.delegate = self + contactViewController.allowsActions = false + contactViewController.allowsEditing = true + contactViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: CommonStrings.cancelButton, style: .plain, target: self, action: #selector(didFinishEditingContact)) + contactViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: CommonStrings.cancelButton, + style: .plain, + target: self, + action: #selector(didFinishEditingContact)) + + guard let navigationController = fromViewController.navigationController else { + owsFail("\(logTag) missing navigationController") + return + } + + navigationController.pushViewController(contactViewController, animated: true) + + // HACK otherwise CNContactViewController Navbar is shown as black. + // RADAR rdar://28433898 http://www.openradar.me/28433898 + // CNContactViewController incompatible with opaque navigation bar + UIUtil.applyDefaultSystemAppearence() + } + + private func presentSelectAddToExistingContactView() { + + guard let fromViewController = fromViewController else { + owsFail("\(logTag) missing fromViewController") + return + } + + guard contactsManager.supportsContactEditing else { + owsFail("\(logTag) Contact editing not supported") + return + } + + guard contactsManager.isSystemContactsAuthorized else { + ContactsViewHelper.presentMissingContactAccessAlertController(from: fromViewController) + 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 + } + + navigationController.pushViewController(viewController, animated: true) + } + + // MARK: - CNContactViewControllerDelegate + + @objc public func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) { + Logger.info("\(logTag) \(#function)") + + guard let fromViewController = fromViewController else { + owsFail("\(logTag) missing fromViewController") + return + } + + guard let navigationController = fromViewController.navigationController else { + owsFail("\(logTag) missing navigationController") + return + } + + guard let delegate = delegate else { + owsFail("\(logTag) missing delegate") + return + } + + navigationController.popToViewController(fromViewController, animated: true) + + delegate.didCreateOrEditContact() + } + + @objc public func didFinishEditingContact() { + Logger.info("\(logTag) \(#function)") + + guard let fromViewController = fromViewController else { + owsFail("\(logTag) missing fromViewController") + return + } + + guard let navigationController = fromViewController.navigationController else { + owsFail("\(logTag) missing navigationController") + return + } + + guard let delegate = delegate else { + owsFail("\(logTag) missing delegate") + return + } + + navigationController.popToViewController(fromViewController, animated: true) + + delegate.didCreateOrEditContact() + } +} diff --git a/Signal/src/ViewControllers/ContactViewController.swift b/Signal/src/ViewControllers/ContactViewController.swift index 9b12e5fc7..c8360d86b 100644 --- a/Signal/src/ViewControllers/ContactViewController.swift +++ b/Signal/src/ViewControllers/ContactViewController.swift @@ -9,7 +9,7 @@ import Reachability import ContactsUI import MessageUI -class ContactViewController: OWSViewController, CNContactViewControllerDelegate { +class ContactViewController: OWSViewController, ContactShareViewHelperDelegate { enum ContactViewMode { case systemContactWithSignal, @@ -37,6 +37,8 @@ class ContactViewController: OWSViewController, CNContactViewControllerDelegate private let contactShare: ContactShareViewModel + private var helper: ContactShareViewHelper! + // MARK: - Initializers @available(*, unavailable, message: "use init(call:) constructor instead.") @@ -50,6 +52,8 @@ class ContactViewController: OWSViewController, CNContactViewControllerDelegate super.init(nibName: nil, bundle: nil) + self.helper = ContactShareViewHelper(contactShare: contactShare, contactsManager: contactsManager, fromViewController: self, delegate: self) + updateMode() NotificationCenter.default.addObserver(forName: .OWSContactsManagerSignalAccountsDidChange, object: nil, queue: nil) { [weak self] _ in @@ -110,7 +114,7 @@ class ContactViewController: OWSViewController, CNContactViewControllerDelegate private func updateMode() { SwiftAssertIsOnMainThread(#function) - guard contactShare.phoneNumberStrings.count > 0 else { + guard contactShare.e164PhoneNumbers().count > 0 else { viewMode = .noPhoneNumber return } @@ -129,17 +133,19 @@ class ContactViewController: OWSViewController, CNContactViewControllerDelegate private func systemContactsWithSignalAccountsForContact() -> [String] { SwiftAssertIsOnMainThread(#function) - return contactShare.phoneNumberStrings.filter({ (phoneNumber) -> Bool in - return contactsManager.hasSignalAccount(forRecipientId: phoneNumber) - }) + return contactShare.systemContactsWithSignalAccountPhoneNumbers(contactsManager) } private func systemContactsForContact() -> [String] { SwiftAssertIsOnMainThread(#function) - return contactShare.phoneNumberStrings.filter({ (phoneNumber) -> Bool in - return contactsManager.allContactsMap[phoneNumber] != nil - }) + return contactShare.systemContactPhoneNumbers(contactsManager) + } + + private func phoneNumbersForContact() -> [String] { + SwiftAssertIsOnMainThread(#function) + + return contactShare.e164PhoneNumbers() } private func updateContent() { @@ -466,18 +472,6 @@ class ContactViewController: OWSViewController, CNContactViewControllerDelegate return button } - func didPressCreateNewContact() { - Logger.info("\(logTag) \(#function)") - - presentNewContactView() - } - - func didPressAddToExistingContact() { - Logger.info("\(logTag) \(#function)") - - presentSelectAddToExistingContactView() - } - func didPressShareContact(sender: UIGestureRecognizer) { Logger.info("\(logTag) \(#function)") @@ -490,92 +484,31 @@ class ContactViewController: OWSViewController, CNContactViewControllerDelegate func didPressSendMessage() { Logger.info("\(logTag) \(#function)") - presentThreadAndPeform(action: .compose) + self.helper.sendMessageToContact() } func didPressAudioCall() { Logger.info("\(logTag) \(#function)") - presentThreadAndPeform(action: .audioCall) + self.helper.audioCallToContact() } func didPressVideoCall() { Logger.info("\(logTag) \(#function)") - presentThreadAndPeform(action: .videoCall) - } - - func presentThreadAndPeform(action: ConversationViewAction) { - // TODO: We're taking the first Signal account id. We might - // want to let the user select if there's more than one. - let phoneNumbers = systemContactsWithSignalAccountsForContact() - guard phoneNumbers.count > 0 else { - owsFail("\(logTag) missing Signal recipient id.") - return - } - guard phoneNumbers.count > 1 else { - let recipientId = systemContactsWithSignalAccountsForContact().first! - SignalApp.shared().presentConversation(forRecipientId: recipientId, action: action) - return - } - - showPhoneNumberPicker(phoneNumbers: phoneNumbers, completion: { (recipientId) in - SignalApp.shared().presentConversation(forRecipientId: recipientId, action: action) - }) + self.helper.videoCallToContact() } func didPressInvite() { Logger.info("\(logTag) \(#function)") - guard MFMessageComposeViewController.canSendText() else { - Logger.info("\(logTag) Device cannot send text") - OWSAlerts.showErrorAlert(message: NSLocalizedString("UNSUPPORTED_FEATURE_ERROR", comment: "")) - return - } - let phoneNumbers = contactShare.phoneNumberStrings - guard phoneNumbers.count > 0 else { - owsFail("\(logTag) no phone numbers.") - return - } - - let inviteFlow = - InviteFlow(presentingViewController: self, contactsManager: contactsManager) - inviteFlow.sendSMSTo(phoneNumbers: phoneNumbers) + self.helper.inviteContact() } func didPressAddToContacts() { Logger.info("\(logTag) \(#function)") - let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - - actionSheet.addAction(UIAlertAction(title: NSLocalizedString("CONVERSATION_SETTINGS_NEW_CONTACT", - comment: "Label for 'new contact' button in conversation settings view."), - style: .default) { _ in - self.didPressCreateNewContact() - }) - actionSheet.addAction(UIAlertAction(title: NSLocalizedString("CONVERSATION_SETTINGS_ADD_TO_EXISTING_CONTACT", - comment: "Label for 'new contact' button in conversation settings view."), - style: .default) { _ in - self.didPressAddToExistingContact() - }) - actionSheet.addAction(OWSAlerts.cancelAction) - - self.present(actionSheet, animated: true) - } - - private func showPhoneNumberPicker(phoneNumbers: [String], completion :@escaping ((String) -> Void)) { - - let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - - for phoneNumber in phoneNumbers { - actionSheet.addAction(UIAlertAction(title: PhoneNumber.bestEffortLocalizedPhoneNumber(withE164: phoneNumber), - style: .default) { _ in - completion(phoneNumber) - }) - } - actionSheet.addAction(OWSAlerts.cancelAction) - - self.present(actionSheet, animated: true) + self.helper.addToContacts() } func didPressDismiss() { @@ -619,80 +552,11 @@ class ContactViewController: OWSViewController, CNContactViewControllerDelegate UIApplication.shared.openURL(url as URL) } - // MARK: - + // MARK: - ContactShareViewHelperDelegate - private func presentNewContactView() { - guard contactsManager.supportsContactEditing else { - owsFail("\(logTag) Contact editing not supported") - return - } - - guard let systemContact = OWSContacts.systemContact(for: contactShare.dbRecord) else { - owsFail("\(logTag) Could not derive system contactShare.") - return - } - - guard contactsManager.isSystemContactsAuthorized else { - ContactsViewHelper.presentMissingContactAccessAlertController(from: self) - return - } - - let contactViewController = CNContactViewController(forNewContact: systemContact) - contactViewController.delegate = self - contactViewController.allowsActions = false - contactViewController.allowsEditing = true - contactViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: CommonStrings.cancelButton, style: .plain, target: self, action: #selector(didFinishEditingContact)) - contactViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: CommonStrings.cancelButton, - style: .plain, - target: self, - action: #selector(didFinishEditingContact)) - - self.navigationController?.pushViewController(contactViewController, animated: true) - - // HACK otherwise CNContactViewController Navbar is shown as black. - // RADAR rdar://28433898 http://www.openradar.me/28433898 - // CNContactViewController incompatible with opaque navigation bar - UIUtil.applyDefaultSystemAppearence() - } - - private func presentSelectAddToExistingContactView() { - guard contactsManager.supportsContactEditing else { - owsFail("\(logTag) Contact editing not supported") - return - } - - guard contactsManager.isSystemContactsAuthorized else { - ContactsViewHelper.presentMissingContactAccessAlertController(from: self) - return - } - - guard let firstPhoneNumber = contactShare.phoneNumbers.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.phoneNumber) - self.navigationController?.pushViewController(viewController, animated: true) - } - - // MARK: - CNContactViewControllerDelegate - - @objc public func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) { + public func didCreateOrEditContact() { Logger.info("\(logTag) \(#function)") - self.navigationController?.popToViewController(self, animated: true) - - updateContent() - } - - @objc public func didFinishEditingContact() { - Logger.info("\(logTag) \(#function)") - - self.navigationController?.popToViewController(self, animated: true) - updateContent() } } diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.h index c374c9fcf..07e624e6b 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.h @@ -5,14 +5,31 @@ NS_ASSUME_NONNULL_BEGIN @class ContactShareViewModel; +@class OWSContact; +@class OWSContactsManager; + +@protocol OWSContactShareViewDelegate + +- (void)sendMessageToContactShare:(ContactShareViewModel *)contactShare; +- (void)sendInviteToContactShare:(ContactShareViewModel *)contactShare; +- (void)showAddToContactUIForContactShare:(ContactShareViewModel *)contactShare; + +@end + +#pragma mark - @interface OWSContactShareView : UIView -- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare isIncoming:(BOOL)isIncoming; +- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare + isIncoming:(BOOL)isIncoming + delegate:(id)delegate; - (void)createContents; -+ (CGFloat)bubbleHeight; ++ (CGFloat)bubbleHeightForContactShare:(ContactShareViewModel *)contactShare; + +// Returns YES IFF the tap was handled. +- (BOOL)handleTapGesture:(UITapGestureRecognizer *)sender; @end diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.m index 21615e390..1c2374c20 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.m @@ -18,7 +18,12 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSContactShareView () @property (nonatomic) ContactShareViewModel *contactShare; +@property (nonatomic, weak) id delegate; + @property (nonatomic) BOOL isIncoming; +@property (nonatomic) OWSContactsManager *contactsManager; + +@property (nonatomic, nullable) UIView *buttonView; @end @@ -26,13 +31,17 @@ NS_ASSUME_NONNULL_BEGIN @implementation OWSContactShareView -- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare isIncoming:(BOOL)isIncoming +- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare + isIncoming:(BOOL)isIncoming + delegate:(id)delegate { self = [super init]; if (self) { - _contactShare = contactShare; - _isIncoming = isIncoming; + self.delegate = delegate; + self.contactShare = contactShare; + self.isIncoming = isIncoming; + self.contactsManager = [Environment current].contactsManager; } return self; @@ -65,14 +74,60 @@ NS_ASSUME_NONNULL_BEGIN return [OWSContactShareView iconVMargin]; } -+ (CGFloat)bubbleHeight ++ (BOOL)hasSendTextButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager +{ + OWSAssert(contactShare); + OWSAssert(contactsManager); + + return [contactShare systemContactsWithSignalAccountPhoneNumbers:contactsManager].count > 0; +} + ++ (BOOL)hasInviteButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager +{ + OWSAssert(contactShare); + OWSAssert(contactsManager); + + return [contactShare systemContactPhoneNumbers:contactsManager].count > 0; +} + ++ (BOOL)hasAddToContactsButton:(ContactShareViewModel *)contactShare +{ + OWSAssert(contactShare); + + return [contactShare e164PhoneNumbers].count > 0; +} + + ++ (BOOL)hasAnyButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager +{ + OWSAssert(contactShare); + + return ([self hasSendTextButton:contactShare contactsManager:contactsManager] || + [self hasInviteButton:contactShare contactsManager:contactsManager] || + [self hasAddToContactsButton:contactShare]); +} + ++ (CGFloat)bubbleHeightForContactShare:(ContactShareViewModel *)contactShare +{ + OWSAssert(contactShare); + + OWSContactsManager *contactsManager = [Environment current].contactsManager; + + if ([self hasAnyButton:contactShare contactsManager:contactsManager]) { + return self.contentHeight + self.buttonHeight; + } else { + return self.contentHeight; + } +} + ++ (CGFloat)contentHeight { return self.iconSize + self.iconVMargin * 2; } -- (CGFloat)bubbleHeight ++ (CGFloat)buttonHeight { - return [OWSContactShareView bubbleHeight]; + return 44.f; } + (CGFloat)iconSize @@ -108,7 +163,6 @@ NS_ASSUME_NONNULL_BEGIN [contentView autoPinLeadingToSuperviewMarginWithInset:self.isIncoming ? kBubbleTailWidth : 0.f]; [contentView autoPinTrailingToSuperviewMarginWithInset:self.isIncoming ? 0.f : kBubbleTailWidth]; [contentView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.vMargin]; - [contentView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.vMargin]; AvatarImageView *avatarView = [AvatarImageView new]; avatarView.image = @@ -130,14 +184,11 @@ NS_ASSUME_NONNULL_BEGIN labelsView.spacing = 2; [labelsView addArrangedSubview:topLabel]; - // TODO: Should we just try to show the _first_ phone number? - // What about email? - // What if the second phone number is a signal account? - NSString *_Nullable firstPhoneNumber = self.contactShare.phoneNumbers.firstObject.phoneNumber; + NSString *_Nullable firstPhoneNumber = + [self.contactShare systemContactsWithSignalAccountPhoneNumbers:self.contactsManager].firstObject; if (firstPhoneNumber.length > 0) { UILabel *bottomLabel = [UILabel new]; bottomLabel.text = [PhoneNumber bestEffortLocalizedPhoneNumberWithE164:firstPhoneNumber]; - // TODO: bottomLabel.textColor = [UIColor ows_darkGrayColor]; bottomLabel.lineBreakMode = NSLineBreakByTruncatingTail; bottomLabel.font = [UIFont ows_dynamicTypeCaption1Font]; @@ -170,6 +221,64 @@ NS_ASSUME_NONNULL_BEGIN [stackView addArrangedSubview:avatarView]; [stackView addArrangedSubview:labelsView]; [stackView addArrangedSubview:disclosureImageView]; + + if ([OWSContactShareView hasAnyButton:self.contactShare contactsManager:self.contactsManager]) { + UIStackView *buttonView = [UIStackView new]; + self.buttonView = buttonView; + buttonView.layoutMargins = UIEdgeInsetsZero; + [buttonView addBackgroundViewWithBackgroundColor:[UIColor whiteColor]]; + buttonView.axis = UILayoutConstraintAxisHorizontal; + buttonView.alignment = UIStackViewAlignmentCenter; + [self addSubview:buttonView]; + [buttonView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:contentView withOffset:self.vMargin]; + [buttonView autoPinWidthToSuperview]; + [buttonView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + [buttonView autoSetDimension:ALDimensionHeight toSize:OWSContactShareView.buttonHeight]; + + UILabel *label = [UILabel new]; + if ([OWSContactShareView hasSendTextButton:self.contactShare contactsManager:self.contactsManager]) { + label.text = NSLocalizedString(@"ACTION_SEND_MESSAGE", @"Label for 'sent message' button in contact view."); + } else if ([OWSContactShareView hasInviteButton:self.contactShare contactsManager:self.contactsManager]) { + label.text = NSLocalizedString(@"ACTION_INVITE", @"Label for 'invite' button in contact view."); + } else if ([OWSContactShareView hasAddToContactsButton:self.contactShare]) { + label.text = NSLocalizedString(@"CONVERSATION_VIEW_ADD_TO_CONTACTS_OFFER", + @"Message shown in conversation view that offers to add an unknown user to your phone's contacts."); + } else { + OWSFail(@"%@ unexpected button state.", self.logTag); + } + label.font = [UIFont ows_dynamicTypeBodyFont]; + label.textColor = UIColor.ows_materialBlueColor; + label.textAlignment = NSTextAlignmentCenter; + [buttonView addArrangedSubview:label]; + + [buttonView logFrameLaterWithLabel:@"buttonView"]; + [label logFrameLaterWithLabel:@"label"]; + } else { + [contentView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.vMargin]; + } +} + +- (BOOL)handleTapGesture:(UITapGestureRecognizer *)sender +{ + if (!self.buttonView) { + return NO; + } + CGPoint location = [sender locationInView:self.buttonView]; + if (!CGRectContainsPoint(self.buttonView.bounds, location)) { + return NO; + } + + if ([OWSContactShareView hasSendTextButton:self.contactShare contactsManager:self.contactsManager]) { + [self.delegate sendMessageToContactShare:self.contactShare]; + } else if ([OWSContactShareView hasInviteButton:self.contactShare contactsManager:self.contactsManager]) { + [self.delegate sendInviteToContactShare:self.contactShare]; + } else if ([OWSContactShareView hasAddToContactsButton:self.contactShare]) { + [self.delegate showAddToContactUIForContactShare:self.contactShare]; + } else { + OWSFail(@"%@ unexpected button tap.", self.logTag); + } + + return YES; } @end diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h index fedb9a51e..779dad7c3 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h @@ -4,7 +4,9 @@ NS_ASSUME_NONNULL_BEGIN +@class ContactShareViewModel; @class ConversationViewItem; +@class OWSContact; @class OWSQuotedReplyModel; @class TSAttachmentPointer; @class TSAttachmentStream; @@ -42,6 +44,11 @@ typedef NS_ENUM(NSUInteger, OWSMessageGestureLocation) { - (void)didTapContactShareViewItem:(ConversationViewItem *)viewItem; +- (void)sendMessageToContactShare:(ContactShareViewModel *)contactShare NS_SWIFT_NAME(sendMessage(toContactShare:)); +- (void)sendInviteToContactShare:(ContactShareViewModel *)contactShare NS_SWIFT_NAME(sendInvite(toContactShare:)); +- (void)showAddToContactUIForContactShare:(ContactShareViewModel *)contactShare + NS_SWIFT_NAME(showAddToContactUI(forContactShare:)); + @end @interface OWSMessageBubbleView : UIView diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index 413133b60..2fd59a897 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -18,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN -@interface OWSMessageBubbleView () +@interface OWSMessageBubbleView () @property (nonatomic) OWSBubbleView *bubbleView; @@ -378,7 +378,8 @@ NS_ASSUME_NONNULL_BEGIN lastSubview = bodyMediaView; bottomMargin = 0; - BOOL shouldStrokeMediaView = [bodyMediaView isKindOfClass:[UIImageView class]]; + BOOL shouldStrokeMediaView = ([bodyMediaView isKindOfClass:[UIImageView class]] || + [bodyMediaView isKindOfClass:[OWSContactShareView class]]); if (shouldStrokeMediaView) { OWSBubbleStrokeView *bubbleStrokeView = [OWSBubbleStrokeView new]; bubbleStrokeView.strokeThickness = 1.f; @@ -796,9 +797,9 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(self.viewItem.contactShare); - OWSContactShareView *contactShareView = - [[OWSContactShareView alloc] initWithContactShare:self.viewItem.contactShare - isIncoming:self.isIncoming]; + OWSContactShareView *contactShareView = [[OWSContactShareView alloc] initWithContactShare:self.viewItem.contactShare + isIncoming:self.isIncoming + delegate:self]; [contactShareView createContents]; // TODO: Should we change appearance if contact avatar is uploading? @@ -931,7 +932,10 @@ NS_ASSUME_NONNULL_BEGIN case OWSMessageCellType_DownloadingAttachment: return CGSizeMake(200, 90); case OWSMessageCellType_ContactShare: - return CGSizeMake(maxMessageWidth, [OWSContactShareView bubbleHeight]); + OWSAssert(self.viewItem.contactShare); + + return CGSizeMake( + maxMessageWidth, [OWSContactShareView bubbleHeightForContactShare:self.viewItem.contactShare]); } } @@ -1127,6 +1131,13 @@ NS_ASSUME_NONNULL_BEGIN } } + if ([self.bodyMediaView isKindOfClass:[OWSContactShareView class]]) { + OWSContactShareView *contactShareView = (OWSContactShareView *)self.bodyMediaView; + if ([contactShareView handleTapGesture:sender]) { + return; + } + } + CGPoint locationInMessageBubble = [sender locationInView:self]; switch ([self gestureLocationForLocation:locationInMessageBubble]) { case OWSMessageGestureLocation_Default: @@ -1247,6 +1258,32 @@ NS_ASSUME_NONNULL_BEGIN failedThumbnailDownloadAttachmentPointer:attachmentPointer]; } +#pragma mark - OWSContactShareViewDelegate + +- (void)sendMessageToContactShare:(ContactShareViewModel *)contactShare +{ + OWSAssertIsOnMainThread(); + OWSAssert(contactShare); + + [self.delegate sendMessageToContactShare:contactShare]; +} + +- (void)sendInviteToContactShare:(ContactShareViewModel *)contactShare +{ + OWSAssertIsOnMainThread(); + OWSAssert(contactShare); + + [self.delegate sendInviteToContactShare:contactShare]; +} + +- (void)showAddToContactUIForContactShare:(ContactShareViewModel *)contactShare +{ + OWSAssertIsOnMainThread(); + OWSAssert(contactShare); + + [self.delegate showAddToContactUIForContactShare:contactShare]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 124b404e3..e0f2ed110 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -123,6 +123,7 @@ typedef enum : NSUInteger { CNContactViewControllerDelegate, ContactEditingDelegate, ContactsPickerDelegate, + ContactShareViewHelperDelegate, ContactsViewHelperDelegate, DisappearingTimerConfigurationViewDelegate, OWSConversationSettingsViewDelegate, @@ -231,6 +232,7 @@ typedef enum : NSUInteger { @property (nonatomic) BOOL isPickingMediaAsDocument; @property (nonatomic, nullable) NSNumber *previousLastTimestamp; @property (nonatomic, nullable) NSNumber *viewHorizonTimestamp; +@property (nonatomic) ContactShareViewHelper *contactShareViewHelper; @end @@ -2106,6 +2108,42 @@ typedef enum : NSUInteger { [self.navigationController pushViewController:view animated:YES]; } +- (void)sendMessageToContactShare:(ContactShareViewModel *)contactShare +{ + OWSAssertIsOnMainThread(); + OWSAssert(contactShare); + + self.contactShareViewHelper = [[ContactShareViewHelper alloc] initWithContactShare:contactShare + contactsManager:self.contactsManager + fromViewController:self + delegate:self]; + [self.contactShareViewHelper sendMessageToContact]; +} + +- (void)sendInviteToContactShare:(ContactShareViewModel *)contactShare +{ + OWSAssertIsOnMainThread(); + OWSAssert(contactShare); + + self.contactShareViewHelper = [[ContactShareViewHelper alloc] initWithContactShare:contactShare + contactsManager:self.contactsManager + fromViewController:self + delegate:self]; + [self.contactShareViewHelper inviteContact]; +} + +- (void)showAddToContactUIForContactShare:(ContactShareViewModel *)contactShare +{ + OWSAssertIsOnMainThread(); + OWSAssert(contactShare); + + self.contactShareViewHelper = [[ContactShareViewHelper alloc] initWithContactShare:contactShare + contactsManager:self.contactsManager + fromViewController:self + delegate:self]; + [self.contactShareViewHelper addToContacts]; +} + - (void)didTapFailedIncomingAttachment:(ConversationViewItem *)viewItem attachmentPointer:(TSAttachmentPointer *)attachmentPointer { @@ -5030,6 +5068,15 @@ interactionControllerForAnimationController:(id [String] { + return dbRecord.systemContactsWithSignalAccountPhoneNumbers(contactsManager) + } + + public func systemContactPhoneNumbers(_ contactsManager: ContactsManagerProtocol) -> [String] { + return dbRecord.systemContactPhoneNumbers(contactsManager) + } + + public func e164PhoneNumbers() -> [String] { + return dbRecord.e164PhoneNumbers() } public var displayName: String { diff --git a/SignalMessaging/Views/TappableStackView.swift b/SignalMessaging/Views/TappableStackView.swift index 8fa9e212e..237bd0ee5 100644 --- a/SignalMessaging/Views/TappableStackView.swift +++ b/SignalMessaging/Views/TappableStackView.swift @@ -4,6 +4,7 @@ import Foundation +@objc public class TappableStackView: UIStackView { let actionBlock : (() -> Void) diff --git a/SignalMessaging/categories/UIView+OWS.h b/SignalMessaging/categories/UIView+OWS.h index 165f60ca0..311e2d1bc 100644 --- a/SignalMessaging/categories/UIView+OWS.h +++ b/SignalMessaging/categories/UIView+OWS.h @@ -135,6 +135,14 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value); @end +#pragma mark - + +@interface UIStackView (OWS) + +- (void)addBackgroundViewWithBackgroundColor:(UIColor *)backgroundColor; + +@end + #pragma mark - Macros CG_INLINE CGSize CGSizeCeil(CGSize size) diff --git a/SignalMessaging/categories/UIView+OWS.m b/SignalMessaging/categories/UIView+OWS.m index 14e68d3da..d99cadd3a 100644 --- a/SignalMessaging/categories/UIView+OWS.m +++ b/SignalMessaging/categories/UIView+OWS.m @@ -516,4 +516,18 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value) @end +#pragma mark - + +@implementation UIStackView (OWS) + +- (void)addBackgroundViewWithBackgroundColor:(UIColor *)backgroundColor +{ + UIView *subview = [UIView new]; + subview.backgroundColor = backgroundColor; + [self addSubview:subview]; + [subview autoPinEdgesToSuperviewEdges]; +} + +@end + NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/contacts/OWSContactsManager.h b/SignalMessaging/contacts/OWSContactsManager.h index a26ca8b3d..92ce39844 100644 --- a/SignalMessaging/contacts/OWSContactsManager.h +++ b/SignalMessaging/contacts/OWSContactsManager.h @@ -60,6 +60,8 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification; #pragma mark - Util +- (BOOL)isSystemContact:(NSString *)recipientId; +- (BOOL)isSystemContactWithSignalAccount:(NSString *)recipientId; - (BOOL)hasNameInSystemContactsForRecipientId:(NSString *)recipientId; - (NSString *)displayNameForPhoneIdentifier:(nullable NSString *)identifier; - (NSString *)displayNameForSignalAccount:(SignalAccount *)signalAccount; diff --git a/SignalMessaging/contacts/OWSContactsManager.m b/SignalMessaging/contacts/OWSContactsManager.m index 27b998592..bde6d537f 100644 --- a/SignalMessaging/contacts/OWSContactsManager.m +++ b/SignalMessaging/contacts/OWSContactsManager.m @@ -459,6 +459,20 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification #pragma mark - Whisper User Management +- (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]; +} + - (BOOL)hasNameInSystemContactsForRecipientId:(NSString *)recipientId { return [self cachedContactNameForRecipientId:recipientId].length > 0; diff --git a/SignalServiceKit/src/Contacts/PhoneNumber.h b/SignalServiceKit/src/Contacts/PhoneNumber.h index 7d89ad304..d98b814cf 100644 --- a/SignalServiceKit/src/Contacts/PhoneNumber.h +++ b/SignalServiceKit/src/Contacts/PhoneNumber.h @@ -14,7 +14,6 @@ + (PhoneNumber *)phoneNumberFromE164:(NSString *)text; -+ (PhoneNumber *)tryParsePhoneNumberFromText:(NSString *)text fromRegion:(NSString *)regionCode; + (PhoneNumber *)tryParsePhoneNumberFromUserSpecifiedText:(NSString *)text; + (PhoneNumber *)tryParsePhoneNumberFromE164:(NSString *)text; diff --git a/SignalServiceKit/src/Contacts/PhoneNumber.m b/SignalServiceKit/src/Contacts/PhoneNumber.m index 9af598d86..83b1cf8ac 100644 --- a/SignalServiceKit/src/Contacts/PhoneNumber.m +++ b/SignalServiceKit/src/Contacts/PhoneNumber.m @@ -170,14 +170,6 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN return regionCode; } - -+ (PhoneNumber *)tryParsePhoneNumberFromText:(NSString *)text fromRegion:(NSString *)regionCode { - OWSAssert(text != nil); - OWSAssert(regionCode != nil); - - return [self phoneNumberFromText:text andRegion:regionCode]; -} - + (PhoneNumber *)tryParsePhoneNumberFromUserSpecifiedText:(NSString *)text { OWSAssert(text != nil); diff --git a/SignalServiceKit/src/Messages/Interactions/OWSContact.h b/SignalServiceKit/src/Messages/Interactions/OWSContact.h index 0d4109f7d..c8199d1c2 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSContact.h +++ b/SignalServiceKit/src/Messages/Interactions/OWSContact.h @@ -3,6 +3,7 @@ // #import +#import NS_ASSUME_NONNULL_BEGIN @@ -140,10 +141,19 @@ NSString *NSStringForContactAddressType(OWSContactAddressType value); familyName:(nullable NSString *)familyName nameSuffix:(nullable NSString *)nameSuffix; +#pragma mark - Phone Numbers and Recipient IDs + +- (NSArray *)systemContactsWithSignalAccountPhoneNumbers:(id)contactsManager + NS_SWIFT_NAME(systemContactsWithSignalAccountPhoneNumbers(_:)); +- (NSArray *)systemContactPhoneNumbers:(id)contactsManager + NS_SWIFT_NAME(systemContactPhoneNumbers(_:)); +- (NSArray *)e164PhoneNumbers NS_SWIFT_NAME(e164PhoneNumbers()); + @end #pragma mark - +// TODO: Move to separate source file, rename to OWSContactConversion. @interface OWSContacts : NSObject #pragma mark - VCard Serialization diff --git a/SignalServiceKit/src/Messages/Interactions/OWSContact.m b/SignalServiceKit/src/Messages/Interactions/OWSContact.m index a49ca0455..4cad201c3 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSContact.m +++ b/SignalServiceKit/src/Messages/Interactions/OWSContact.m @@ -279,6 +279,8 @@ NSString *NSStringForContactAddressType(OWSContactAddressType value) @property (nonatomic, nullable) NSString *avatarAttachmentId; @property (nonatomic) BOOL isProfileAvatar; +@property (nonatomic, nullable) NSArray *e164PhoneNumbersCached; + @end #pragma mark - @@ -483,6 +485,50 @@ NSString *NSStringForContactAddressType(OWSContactAddressType value) self.avatarAttachmentId = attachmentStream.uniqueId; } +#pragma mark - Phone Numbers and Recipient IDs + +- (NSArray *)systemContactsWithSignalAccountPhoneNumbers:(id)contactsManager +{ + OWSAssert(contactsManager); + + return [self.e164PhoneNumbers + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString *_Nullable recipientId, + NSDictionary *_Nullable bindings) { + return [contactsManager isSystemContactWithSignalAccount:recipientId]; + }]]; +} + +- (NSArray *)systemContactPhoneNumbers:(id)contactsManager +{ + OWSAssert(contactsManager); + + return [self.e164PhoneNumbers + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString *_Nullable recipientId, + NSDictionary *_Nullable bindings) { + return [contactsManager isSystemContact:recipientId]; + }]]; +} + +- (NSArray *)e164PhoneNumbers +{ + if (self.e164PhoneNumbersCached) { + return self.e164PhoneNumbersCached; + } + NSMutableArray *e164PhoneNumbers = [NSMutableArray new]; + for (OWSContactPhoneNumber *phoneNumber in self.phoneNumbers) { + PhoneNumber *_Nullable parsedPhoneNumber; + parsedPhoneNumber = [PhoneNumber tryParsePhoneNumberFromE164:phoneNumber.phoneNumber]; + if (!parsedPhoneNumber) { + parsedPhoneNumber = [PhoneNumber tryParsePhoneNumberFromUserSpecifiedText:phoneNumber.phoneNumber]; + } + if (parsedPhoneNumber) { + [e164PhoneNumbers addObject:parsedPhoneNumber.toE164]; + } + } + self.e164PhoneNumbersCached = e164PhoneNumbers; + return e164PhoneNumbers; +} + @end #pragma mark - @@ -553,7 +599,20 @@ NSString *NSStringForContactAddressType(OWSContactAddressType value) NSMutableArray *phoneNumbers = [NSMutableArray new]; for (CNLabeledValue *phoneNumberField in systemContact.phoneNumbers) { OWSContactPhoneNumber *phoneNumber = [OWSContactPhoneNumber new]; - phoneNumber.phoneNumber = phoneNumberField.value.stringValue; + + // Make a best effort to parse the phone number to e164. + NSString *unparsedPhoneNumber = phoneNumberField.value.stringValue; + PhoneNumber *_Nullable parsedPhoneNumber; + parsedPhoneNumber = [PhoneNumber tryParsePhoneNumberFromE164:unparsedPhoneNumber]; + if (!parsedPhoneNumber) { + parsedPhoneNumber = [PhoneNumber tryParsePhoneNumberFromUserSpecifiedText:unparsedPhoneNumber]; + } + if (parsedPhoneNumber) { + phoneNumber.phoneNumber = parsedPhoneNumber.toE164; + } else { + phoneNumber.phoneNumber = unparsedPhoneNumber; + } + if ([phoneNumberField.label isEqualToString:CNLabelHome]) { phoneNumber.phoneType = OWSContactPhoneType_Home; } else if ([phoneNumberField.label isEqualToString:CNLabelWork]) { diff --git a/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h b/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h index c20015d9f..122ddaead 100644 --- a/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h +++ b/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h @@ -12,4 +12,7 @@ - (NSString * _Nonnull)displayNameForPhoneIdentifier:(NSString * _Nullable)phoneNumber; - (NSArray * _Nonnull)signalAccounts; +- (BOOL)isSystemContact:(NSString *)recipientId; +- (BOOL)isSystemContactWithSignalAccount:(NSString *)recipientId; + @end