From bd116f89389d9313ed0287747e91c0426949a98d Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 4 May 2018 11:40:23 -0400 Subject: [PATCH 1/2] Share contacts from share extension. --- .../ConversationViewController.m | 16 ++--- .../ApproveContactShareViewController.swift | 49 ++++++++++++-- .../SharingThreadPickerViewController.m | 65 ++++++++++++++++++- .../attachments/SignalAttachment.swift | 20 +++--- .../src/Messages/Interactions/OWSContact.m | 2 + SignalShareExtension/Info.plist | 3 - .../ShareViewController.swift | 15 ++++- 7 files changed, 141 insertions(+), 29 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index d76ab631a..278a7986a 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -4958,19 +4958,19 @@ interactionControllerForAnimationController:(id { // MARK: - +protocol ContactShareFieldViewDelegate: class { + func contactShareFieldViewDidChangeSelectedState() +} + +// MARK: - + class ContactShareFieldView: UIStackView { + weak var delegate: ContactShareFieldViewDelegate? + let field: ContactShareField let previewViewBlock : (() -> UIView) @@ -112,9 +120,10 @@ class ContactShareFieldView: UIStackView { fatalError("Unimplemented") } - required init(field: ContactShareField, previewViewBlock : @escaping (() -> UIView)) { + required init(field: ContactShareField, previewViewBlock : @escaping (() -> UIView), delegate: ContactShareFieldViewDelegate) { self.field = field self.previewViewBlock = previewViewBlock + self.delegate = delegate super.init(frame: CGRect.zero) @@ -161,6 +170,10 @@ class ContactShareFieldView: UIStackView { } field.setIsIncluded(!field.isIncluded()) checkbox.isSelected = field.isIncluded() + + if let delegate = delegate { + delegate.contactShareFieldViewDidChangeSelectedState() + } } } @@ -168,7 +181,8 @@ class ContactShareFieldView: UIStackView { // TODO: Rename to ContactShareApprovalViewController @objc -public class ApproveContactShareViewController: OWSViewController, EditContactShareNameViewControllerDelegate { +public class ApproveContactShareViewController: OWSViewController, EditContactShareNameViewControllerDelegate, ContactShareFieldViewDelegate { + weak var delegate: ApproveContactShareViewControllerDelegate? let contactsManager: OWSContactsManager @@ -208,21 +222,26 @@ public class ApproveContactShareViewController: OWSViewController, EditContactSh let field = ContactSharePhoneNumber(phoneNumber) let fieldView = ContactShareFieldView(field: field, previewViewBlock: { return ContactFieldView.contactFieldView(forPhoneNumber: phoneNumber, layoutMargins: previewInsets, actionBlock: nil) - }) + }, + delegate: self) fieldViews.append(fieldView) } + for email in contactShare.emails { let field = ContactShareEmail(email) let fieldView = ContactShareFieldView(field: field, previewViewBlock: { return ContactFieldView.contactFieldView(forEmail: email, layoutMargins: previewInsets, actionBlock: nil) - }) + }, + delegate: self) fieldViews.append(fieldView) } + for address in contactShare.addresses { let field = ContactShareAddress(address) let fieldView = ContactShareFieldView(field: field, previewViewBlock: { return ContactFieldView.contactFieldView(forAddress: address, layoutMargins: previewInsets, actionBlock: nil) - }) + }, + delegate: self) fieldViews.append(fieldView) } @@ -264,7 +283,16 @@ public class ApproveContactShareViewController: OWSViewController, EditContactSh // TODO: Surface error with resolution to user if not. func canShareContact() -> Bool { - return contactShare.ows_isValid + return contactShare.ows_isValid && isAtLeastOneFieldSelected() + } + + func isAtLeastOneFieldSelected() -> Bool { + for fieldView in fieldViews { + if fieldView.field.isIncluded() { + return true + } + } + return false } func updateNavigationBar() { @@ -384,6 +412,9 @@ public class ApproveContactShareViewController: OWSViewController, EditContactSh // MARK: - func didPressSendButton() { + SwiftAssertIsOnMainThread(#function) + assert(canShareContact()) + Logger.info("\(logTag) \(#function)") guard let delegate = self.delegate else { @@ -424,4 +455,10 @@ public class ApproveContactShareViewController: OWSViewController, EditContactSh self.updateNavigationBar() } + + // MARK: - ContactShareFieldViewDelegate + + public func contactShareFieldViewDidChangeSelectedState() { + self.updateNavigationBar() + } } diff --git a/SignalMessaging/attachments/SharingThreadPickerViewController.m b/SignalMessaging/attachments/SharingThreadPickerViewController.m index 05aa26c63..2ae811df9 100644 --- a/SignalMessaging/attachments/SharingThreadPickerViewController.m +++ b/SignalMessaging/attachments/SharingThreadPickerViewController.m @@ -23,7 +23,8 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion); @interface SharingThreadPickerViewController () + MessageApprovalViewControllerDelegate, + ApproveContactShareViewControllerDelegate> @property (nonatomic, readonly) OWSContactsManager *contactsManager; @property (nonatomic, readonly) OWSMessageSender *messageSender; @@ -123,8 +124,6 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion); #pragma mark - SelectThreadViewControllerDelegate -// If the attachment is textual (e.g. text or URL), returns the message text -// for the attachment. Returns nil otherwise. - (nullable NSString *)convertAttachmentToMessageTextIfPossible { if (!self.attachment.isConvertibleToTextMessage) { @@ -144,8 +143,36 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion); { OWSAssert(self.attachment); OWSAssert(thread); + self.thread = thread; + if (self.attachment.isConvertibleToContactShare) { + NSData *data = self.attachment.data; + OWSContact *_Nullable contact = [OWSContacts contactForVCardData:data]; + + if (!contact) { + // This should never happen since we verify that the contact can be parsed when building the attachment. + OWSFail(@"%@ could not parse contact share.", self.logTag); + [self.shareViewDelegate shareViewWasCancelled]; + return; + } + + // TODO: Populate avatar image. + BOOL isProfileAvatar = NO; + UIImage *_Nullable avatarImage = nil; + contact.isProfileAvatar = isProfileAvatar; + + ContactShareViewModel *contactShare = + [[ContactShareViewModel alloc] initWithContactShareRecord:contact avatarImage:avatarImage]; + + ApproveContactShareViewController *approvalVC = + [[ApproveContactShareViewController alloc] initWithContactShare:contactShare + contactsManager:self.contactsManager + delegate:self]; + [self.navigationController pushViewController:approvalVC animated:YES]; + return; + } + NSString *_Nullable messageText = [self convertAttachmentToMessageTextIfPossible]; // Hide the navigation bar before presenting the approval view. @@ -252,6 +279,38 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion); [self cancelShareExperience]; } +#pragma mark - ApproveContactShareViewControllerDelegate + +- (void)approveContactShare:(ApproveContactShareViewController *)approvalViewController + didApproveContactShare:(OWSContact *)contactShare +{ + DDLogInfo(@"%@ in %s", self.logTag, __PRETTY_FUNCTION__); + + [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread]; + [self tryToSendMessageWithBlock:^(SendCompletionBlock sendCompletion) { + OWSAssertIsOnMainThread(); + + __block TSOutgoingMessage *outgoingMessage = nil; + outgoingMessage = [ThreadUtil sendMessageWithContactShare:contactShare + inThread:self.thread + messageSender:self.messageSender + completion:^(NSError *_Nullable error) { + sendCompletion(error, outgoingMessage); + }]; + // This is necessary to show progress. + self.outgoingMessage = outgoingMessage; + } + fromViewController:approvalViewController]; +} + +- (void)approveContactShare:(ApproveContactShareViewController *)approvalViewController + didCancelContactShare:(OWSContact *)contactShare +{ + DDLogInfo(@"%@ in %s", self.logTag, __PRETTY_FUNCTION__); + + [self cancelShareExperience]; +} + #pragma mark - Helpers - (void)tryToSendMessageWithBlock:(SendMessageBlock)sendMessageBlock diff --git a/SignalMessaging/attachments/SignalAttachment.swift b/SignalMessaging/attachments/SignalAttachment.swift index c51a83d18..32da0bbb9 100644 --- a/SignalMessaging/attachments/SignalAttachment.swift +++ b/SignalMessaging/attachments/SignalAttachment.swift @@ -142,6 +142,10 @@ public class SignalAttachment: NSObject { @objc public var isConvertibleToTextMessage = false + // This flag should be set for attachments that can be sent as contact shares. + @objc + public var isConvertibleToContactShare = false + // Attachment types are identified using UTIs. // // See: https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html @@ -826,14 +830,14 @@ public class SignalAttachment: NSObject { let removeMetadataProperties: [String: AnyObject] = [ - kCGImagePropertyExifDictionary as String : kCFNull, - kCGImagePropertyExifAuxDictionary as String : kCFNull, - kCGImagePropertyGPSDictionary as String : kCFNull, - kCGImagePropertyTIFFDictionary as String : kCFNull, - kCGImagePropertyJFIFDictionary as String : kCFNull, - kCGImagePropertyPNGDictionary as String : kCFNull, - kCGImagePropertyIPTCDictionary as String : kCFNull, - kCGImagePropertyMakerAppleDictionary as String : kCFNull + kCGImagePropertyExifDictionary as String: kCFNull, + kCGImagePropertyExifAuxDictionary as String: kCFNull, + kCGImagePropertyGPSDictionary as String: kCFNull, + kCGImagePropertyTIFFDictionary as String: kCFNull, + kCGImagePropertyJFIFDictionary as String: kCFNull, + kCGImagePropertyPNGDictionary as String: kCFNull, + kCGImagePropertyIPTCDictionary as String: kCFNull, + kCGImagePropertyMakerAppleDictionary as String: kCFNull ] for index in 0...count-1 { diff --git a/SignalServiceKit/src/Messages/Interactions/OWSContact.m b/SignalServiceKit/src/Messages/Interactions/OWSContact.m index fe979a008..97902357b 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSContact.m +++ b/SignalServiceKit/src/Messages/Interactions/OWSContact.m @@ -17,6 +17,8 @@ NS_ASSUME_NONNULL_BEGIN +// NOTE: When changing the value of this feature flag, you also need +// to update the filtering in the SAE's info.plist. BOOL kIsSendingContactSharesEnabled = YES; NSString *NSStringForContactPhoneType(OWSContactPhoneType value) diff --git a/SignalShareExtension/Info.plist b/SignalShareExtension/Info.plist index e497d9379..00c1f0a22 100644 --- a/SignalShareExtension/Info.plist +++ b/SignalShareExtension/Info.plist @@ -60,11 +60,8 @@ SUBQUERY ( $extensionItem.attachments, $attachment, - ( ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.data" || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" - ) - AND NOT (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.vcard") ).@count >= 1 ).@count == 1 diff --git a/SignalShareExtension/ShareViewController.swift b/SignalShareExtension/ShareViewController.swift index 2c6edbd9d..0ee30616a 100644 --- a/SignalShareExtension/ShareViewController.swift +++ b/SignalShareExtension/ShareViewController.swift @@ -694,6 +694,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed var customFileName: String? var isConvertibleToTextMessage = false + var isConvertibleToContactShare = false let loadCompletion: NSItemProvider.CompletionHandler = { [weak self] (value, error) in @@ -719,6 +720,15 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed if ShareViewController.itemMatchesSpecificUtiType(itemProvider: itemProvider, utiType: kUTTypeVCard as String) { customFileName = "Contact.vcf" + + if let contactShare = OWSContacts.contact(forVCardData: data) { + isConvertibleToContactShare = true + } else { + Logger.error("\(strongSelf.logTag) could not parse vcard.") + let writeError = ShareViewControllerError.assertionError(description: "Could not parse vcard data.") + reject(writeError) + return + } } let customFileExtension = MIMETypeUtil.fileExtension(forUTIType: srcUtiType) @@ -844,7 +854,10 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed } let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType, imageQuality: .medium) - if isConvertibleToTextMessage { + if isConvertibleToContactShare { + Logger.info("\(strongSelf.logTag) isConvertibleToContactShare") + attachment.isConvertibleToContactShare = isConvertibleToContactShare + } else if isConvertibleToTextMessage { Logger.info("\(strongSelf.logTag) isConvertibleToTextMessage") attachment.isConvertibleToTextMessage = isConvertibleToTextMessage } From 41f4b0866c9d4dc413e294db35fa67e87cd02a12 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 7 May 2018 17:05:44 -0400 Subject: [PATCH 2/2] Respond to CR. --- .../attachments/ApproveContactShareViewController.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/SignalMessaging/attachments/ApproveContactShareViewController.swift b/SignalMessaging/attachments/ApproveContactShareViewController.swift index 236b5a608..2a014978b 100644 --- a/SignalMessaging/attachments/ApproveContactShareViewController.swift +++ b/SignalMessaging/attachments/ApproveContactShareViewController.swift @@ -171,9 +171,7 @@ class ContactShareFieldView: UIStackView { field.setIsIncluded(!field.isIncluded()) checkbox.isSelected = field.isIncluded() - if let delegate = delegate { - delegate.contactShareFieldViewDidChangeSelectedState() - } + delegate?.contactShareFieldViewDidChangeSelectedState() } }