From aeadea67e27b758c1891b4f4e9619c700294a3d3 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 12 Nov 2018 15:24:12 -0500 Subject: [PATCH] Send multiple attachments from the share extension. --- .../SharingThreadPickerViewController.h | 5 +- .../SharingThreadPickerViewController.m | 90 +++-- SignalMessaging/utils/ThreadUtil.h | 10 +- SignalMessaging/utils/ThreadUtil.m | 31 +- .../ShareViewController.swift | 311 ++++++++++++------ 5 files changed, 296 insertions(+), 151 deletions(-) diff --git a/SignalMessaging/ViewControllers/SharingThreadPickerViewController.h b/SignalMessaging/ViewControllers/SharingThreadPickerViewController.h index f221beaea..ce1ceadd2 100644 --- a/SignalMessaging/ViewControllers/SharingThreadPickerViewController.h +++ b/SignalMessaging/ViewControllers/SharingThreadPickerViewController.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "SelectThreadViewController.h" @@ -7,11 +7,12 @@ NS_ASSUME_NONNULL_BEGIN @class SignalAttachment; + @protocol ShareViewDelegate; @interface SharingThreadPickerViewController : SelectThreadViewController -@property (nonatomic) SignalAttachment *attachment; +@property (nonatomic) NSArray *attachments; - (instancetype)initWithShareViewDelegate:(id)shareViewDelegate; diff --git a/SignalMessaging/ViewControllers/SharingThreadPickerViewController.m b/SignalMessaging/ViewControllers/SharingThreadPickerViewController.m index 07cf5b808..ce97c94bc 100644 --- a/SignalMessaging/ViewControllers/SharingThreadPickerViewController.m +++ b/SignalMessaging/ViewControllers/SharingThreadPickerViewController.m @@ -127,13 +127,18 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion); - (nullable NSString *)convertAttachmentToMessageTextIfPossible { - if (!self.attachment.isConvertibleToTextMessage) { + if (self.attachments.count > 1) { return nil; } - if (self.attachment.dataLength >= kOversizeTextMessageSizeThreshold) { + OWSAssertDebug(self.attachments.count == 1); + SignalAttachment *attachment = self.attachments.firstObject; + if (!attachment.isConvertibleToTextMessage) { return nil; } - NSData *data = self.attachment.data; + if (attachment.dataLength >= kOversizeTextMessageSizeThreshold) { + return nil; + } + NSData *data = attachment.data; OWSAssertDebug(data.length < kOversizeTextMessageSizeThreshold); NSString *_Nullable messageText = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; OWSLogVerbose(@"messageTextForAttachment: %@", messageText); @@ -142,42 +147,67 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion); - (void)threadWasSelected:(TSThread *)thread { - OWSAssertDebug(self.attachment); + OWSAssertDebug(self.attachments.count > 0); OWSAssertDebug(thread); self.thread = thread; - if (self.attachment.isConvertibleToContactShare) { - [self showContactShareApproval]; + if ([self tryToShareAsMessageText]) { + return; + } + + if ([self tryToShareAsContactShare]) { return; } + OWSNavigationController *approvalModal = + [AttachmentApprovalViewController wrappedInNavControllerWithAttachments:self.attachments approvalDelegate:self]; + [self presentViewController:approvalModal animated:YES completion:nil]; +} + +- (BOOL)tryToShareAsMessageText +{ + OWSAssertDebug(self.attachments.count > 0); + NSString *_Nullable messageText = [self convertAttachmentToMessageTextIfPossible]; + if (!messageText) { + return NO; + } - if (messageText) { - MessageApprovalViewController *approvalVC = - [[MessageApprovalViewController alloc] initWithMessageText:messageText - thread:thread - contactsManager:self.contactsManager - delegate:self]; + MessageApprovalViewController *approvalVC = + [[MessageApprovalViewController alloc] initWithMessageText:messageText + thread:self.thread + contactsManager:self.contactsManager + delegate:self]; - [self.navigationController pushViewController:approvalVC animated:YES]; - } else { - // TODO ALBUMS - send album via SAE - OWSNavigationController *approvalModal = - [AttachmentApprovalViewController wrappedInNavControllerWithAttachments:@[ self.attachment ] - approvalDelegate:self]; - [self presentViewController:approvalModal animated:YES completion:nil]; + [self.navigationController pushViewController:approvalVC animated:YES]; + return YES; +} + +- (BOOL)tryToShareAsContactShare +{ + OWSAssertDebug(self.attachments.count > 0); + + if (self.attachments.count > 1) { + return nil; } + OWSAssertDebug(self.attachments.count == 1); + SignalAttachment *attachment = self.attachments.firstObject; + if (!attachment.isConvertibleToContactShare) { + return NO; + } + + [self showContactShareApproval:attachment]; + return YES; } -- (void)showContactShareApproval +- (void)showContactShareApproval:(SignalAttachment *)attachment { - OWSAssertDebug(self.attachment); + OWSAssertDebug(attachment); OWSAssertDebug(self.thread); - OWSAssertDebug(self.attachment.isConvertibleToContactShare); + OWSAssertDebug(attachment.isConvertibleToContactShare); - NSData *data = self.attachment.data; + NSData *data = attachment.data; CNContact *_Nullable cnContact = [Contact cnContactWithVCardData:data]; Contact *_Nullable contact = [[Contact alloc] initWithSystemContact:cnContact]; @@ -242,13 +272,13 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion); // the sending operation. Alternatively, we could use a durable send, but do more to make sure the // SAE runs as long as it needs. // TODO ALBUMS - send album via SAE - outgoingMessage = [ThreadUtil sendMessageNonDurablyWithAttachment:attachments.firstObject - inThread:self.thread - quotedReplyModel:nil - messageSender:self.messageSender - completion:^(NSError *_Nullable error) { - sendCompletion(error, outgoingMessage); - }]; + outgoingMessage = [ThreadUtil sendMessageNonDurablyWithAttachments:attachments + inThread:self.thread + quotedReplyModel:nil + messageSender:self.messageSender + completion:^(NSError *_Nullable error) { + sendCompletion(error, outgoingMessage); + }]; // This is necessary to show progress. self.outgoingMessage = outgoingMessage; diff --git a/SignalMessaging/utils/ThreadUtil.h b/SignalMessaging/utils/ThreadUtil.h index 2d7908058..7973ae0ea 100644 --- a/SignalMessaging/utils/ThreadUtil.h +++ b/SignalMessaging/utils/ThreadUtil.h @@ -72,11 +72,11 @@ NS_ASSUME_NONNULL_BEGIN failure:(void (^)(NSError *error))failureHandler; // Used by SAE, otherwise we should use the durable `enqueue` counterpart -+ (TSOutgoingMessage *)sendMessageNonDurablyWithAttachment:(SignalAttachment *)attachment - inThread:(TSThread *)thread - quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel - messageSender:(OWSMessageSender *)messageSender - completion:(void (^_Nullable)(NSError *_Nullable error))completion; ++ (TSOutgoingMessage *)sendMessageNonDurablyWithAttachments:(NSArray *)attachments + inThread:(TSThread *)thread + quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel + messageSender:(OWSMessageSender *)messageSender + completion:(void (^_Nullable)(NSError *_Nullable error))completion; // Used by SAE, otherwise we should use the durable `enqueue` counterpart + (TSOutgoingMessage *)sendMessageNonDurablyWithContactShare:(OWSContact *)contactShare diff --git a/SignalMessaging/utils/ThreadUtil.m b/SignalMessaging/utils/ThreadUtil.m index 9ebed697f..2ac2cff92 100644 --- a/SignalMessaging/utils/ThreadUtil.m +++ b/SignalMessaging/utils/ThreadUtil.m @@ -213,15 +213,14 @@ NS_ASSUME_NONNULL_BEGIN return message; } -+ (TSOutgoingMessage *)sendMessageNonDurablyWithAttachment:(SignalAttachment *)attachment - inThread:(TSThread *)thread - quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel - messageSender:(OWSMessageSender *)messageSender - completion:(void (^_Nullable)(NSError *_Nullable error))completion ++ (TSOutgoingMessage *)sendMessageNonDurablyWithAttachments:(NSArray *)attachments + inThread:(TSThread *)thread + quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel + messageSender:(OWSMessageSender *)messageSender + completion:(void (^_Nullable)(NSError *_Nullable error))completion { OWSAssertIsOnMainThread(); - OWSAssertDebug(attachment); - OWSAssertDebug([attachment mimeType].length > 0); + OWSAssertDebug(attachments.count > 0); OWSAssertDebug(thread); OWSAssertDebug(messageSender); @@ -229,22 +228,28 @@ NS_ASSUME_NONNULL_BEGIN [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:thread.uniqueId]; uint32_t expiresInSeconds = (configuration.isEnabled ? configuration.durationSeconds : 0); + BOOL isVoiceMessage = (attachments.count == 1 && attachments.firstObject.isVoiceMessage); + NSString *_Nullable messageBody = (attachments.count == 1 ? attachments.firstObject.captionText : nil); TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp] inThread:thread - messageBody:attachment.captionText + messageBody:messageBody attachmentIds:[NSMutableArray new] expiresInSeconds:expiresInSeconds expireStartedAt:0 - isVoiceMessage:[attachment isVoiceMessage] + isVoiceMessage:isVoiceMessage groupMetaMessage:TSGroupMetaMessageUnspecified quotedMessage:[quotedReplyModel buildQuotedMessageForSending] contactShare:nil]; - [messageSender sendAttachment:attachment.dataSource - contentType:attachment.mimeType - sourceFilename:attachment.filenameOrDefault - albumMessageId:message.uniqueId + NSMutableArray *attachmentInfos = [NSMutableArray new]; + for (SignalAttachment *attachment in attachments) { + OWSAssertDebug([attachment mimeType].length > 0); + + [attachmentInfos addObject:[attachment buildOutgoingAttachmentInfoWithMessage:message]]; + } + + [messageSender sendAttachments:attachmentInfos inMessage:message success:^{ OWSLogDebug(@"Successfully sent message attachment."); diff --git a/SignalShareExtension/ShareViewController.swift b/SignalShareExtension/ShareViewController.swift index 199a8a583..9904206d5 100644 --- a/SignalShareExtension/ShareViewController.swift +++ b/SignalShareExtension/ShareViewController.swift @@ -334,7 +334,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed } else { DispatchQueue.main.async { [weak self] in guard let strongSelf = self else { return } - strongSelf.buildAttachmentAndPresentConversationPicker() + strongSelf.buildAttachmentsAndPresentConversationPicker() } } @@ -534,10 +534,10 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed } } - private func buildAttachmentAndPresentConversationPicker() { + private func buildAttachmentsAndPresentConversationPicker() { AssertIsOnMainThread() - self.buildAttachment().map { [weak self] attachment in + self.buildAttachments().map { [weak self] attachments in AssertIsOnMainThread() guard let strongSelf = self else { return } @@ -546,9 +546,9 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed let conversationPicker = SharingThreadPickerViewController(shareViewDelegate: strongSelf) Logger.debug("presentConversationPicker: \(conversationPicker)") - conversationPicker.attachment = attachment + conversationPicker.attachments = attachments strongSelf.showPrimaryViewController(conversationPicker) - Logger.info("showing picker with attachment: \(attachment)") + Logger.info("showing picker with attachments: \(attachments)") }.catch { [weak self] error in AssertIsOnMainThread() guard let strongSelf = self else { return } @@ -585,6 +585,11 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed return firstUtiType == utiType } + private class func isVisualMediaItem(itemProvider: NSItemProvider) -> Bool { + return (itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) || + itemProvider.hasItemConformingToTypeIdentifier(kUTTypeMovie as String)) + } + private class func isUrlItem(itemProvider: NSItemProvider) -> Bool { return itemMatchesSpecificUtiType(itemProvider: itemProvider, utiType: kUTTypeURL as String) @@ -611,26 +616,6 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed return matchingUtiType } - private class func preferredItemProvider(inputItem: NSExtensionItem) -> NSItemProvider? { - guard let attachments = inputItem.attachments else { - return nil - } - - // Prefer a URL provider if available - // TODO: Support multi-image messages. - if let preferredAttachment = attachments.first(where: { (attachment: Any) -> Bool in - guard let itemProvider = attachment as? NSItemProvider else { - return false - } - return isUrlItem(itemProvider: itemProvider) - }) { - return preferredAttachment as? NSItemProvider - } - - // else return whatever is available - return inputItem.attachments?.first as? NSItemProvider - } - private class func createDataSource(utiType: String, url: URL, customFileName: String?) -> DataSource? { if utiType == (kUTTypeURL as String) { // Share URLs as oversize text messages whose text content is the URL. @@ -662,10 +647,23 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed } } - private func buildAttachment() -> Promise { - guard let inputItem: NSExtensionItem = self.extensionContext?.inputItems.first as? NSExtensionItem else { - let error = ShareViewControllerError.assertionError(description: "no input item") - return Promise(error: error) + private class func preferredItemProviders(inputItem: NSExtensionItem) -> [NSItemProvider]? { + guard let attachments = inputItem.attachments else { + return nil + } + + var visualMediaItemProviders = [NSItemProvider]() + for attachment in attachments { + guard let itemProvider = attachment as? NSItemProvider else { + owsFailDebug("Unexpected attachment type: \(String(describing: attachment))") + continue + } + if isVisualMediaItem(itemProvider: itemProvider) { + visualMediaItemProviders.append(itemProvider) + } + } + if visualMediaItemProviders.count > 0 { + return visualMediaItemProviders } // A single inputItem can have multiple attachments, e.g. sharing from Firefox gives @@ -675,10 +673,75 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed // FIXME: For now, we prefer the URL provider and discard the text provider, since it's more useful to share the URL than the caption // but we *should* include both. This will be a bigger change though since our share extension is currently heavily predicated // on one itemProvider per share. - guard let itemProvider: NSItemProvider = type(of: self).preferredItemProvider(inputItem: inputItem) else { - let error = ShareViewControllerError.assertionError(description: "No item provider in input item attachments") + + // Prefer a URL provider if available + if let preferredAttachment = attachments.first(where: { (attachment: Any) -> Bool in + guard let itemProvider = attachment as? NSItemProvider else { + return false + } + return isUrlItem(itemProvider: itemProvider) + }) { + if let itemProvider = preferredAttachment as? NSItemProvider { + return [itemProvider] + } else { + owsFailDebug("Unexpected attachment type: \(String(describing: preferredAttachment))") + } + } + + // else return whatever is available + if let itemProvider = inputItem.attachments?.first as? NSItemProvider { + return [itemProvider] + } else { + owsFailDebug("Missing attachment.") + } + return [] + } + + private func selectItemProviders() -> Promise<[NSItemProvider]> { + guard let inputItems = self.extensionContext?.inputItems else { + let error = ShareViewControllerError.assertionError(description: "no input item") return Promise(error: error) } + + for inputItemRaw in inputItems { + guard let inputItem = inputItemRaw as? NSExtensionItem else { + Logger.error("invalid inputItem \(inputItemRaw)") + continue + } + if let itemProviders = ShareViewController.preferredItemProviders(inputItem: inputItem) { + return Promise.value(itemProviders) + } + } + let error = ShareViewControllerError.assertionError(description: "no input item") + return Promise(error: error) + } + + private + struct LoadedItem { + let itemProvider: NSItemProvider + let itemUrl: URL + let utiType: String + + var customFileName: String? + var isConvertibleToTextMessage = false + var isConvertibleToContactShare = false + + init(itemProvider: NSItemProvider, + itemUrl: URL, + utiType: String, + customFileName: String? = nil, + isConvertibleToTextMessage: Bool = false, + isConvertibleToContactShare: Bool = false) { + self.itemProvider = itemProvider + self.itemUrl = itemUrl + self.utiType = utiType + self.customFileName = customFileName + self.isConvertibleToTextMessage = isConvertibleToTextMessage + self.isConvertibleToContactShare = isConvertibleToContactShare + } + } + + private func loadItemProvider(itemProvider: NSItemProvider) -> Promise { Logger.info("attachment: \(itemProvider)") // We need to be very careful about which UTI type we use. @@ -696,17 +759,12 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed } Logger.debug("matched utiType: \(srcUtiType)") - let (promise, resolver) = Promise<(itemUrl: URL, utiType: String)>.pending() - - var customFileName: String? - var isConvertibleToTextMessage = false - var isConvertibleToContactShare = false + let (promise, resolver) = Promise.pending() let loadCompletion: NSItemProvider.CompletionHandler = { [weak self] (value, error) in guard let _ = self else { return } - guard error == nil else { resolver.reject(error!) return @@ -721,11 +779,13 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed Logger.info("value type: \(type(of: value))") if let data = value as? Data { + let customFileName = "Contact.vcf" + var isConvertibleToContactShare = false + // Although we don't support contacts _yet_, when we do we'll want to make // sure they are shared with a reasonable filename. if ShareViewController.itemMatchesSpecificUtiType(itemProvider: itemProvider, utiType: kUTTypeVCard as String) { - customFileName = "Contact.vcf" if Contact(vCardData: data) != nil { isConvertibleToContactShare = true @@ -744,7 +804,11 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed return } let fileUrl = URL(fileURLWithPath: tempFilePath) - resolver.fulfill((itemUrl: fileUrl, utiType: srcUtiType)) + resolver.fulfill(LoadedItem(itemProvider: itemProvider, + itemUrl: fileUrl, + utiType: srcUtiType, + customFileName: customFileName, + isConvertibleToContactShare: isConvertibleToContactShare)) } else if let string = value as? String { Logger.debug("string provider: \(string)") guard let data = string.filterStringForDisplay().data(using: String.Encoding.utf8) else { @@ -760,21 +824,33 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed let fileUrl = URL(fileURLWithPath: tempFilePath) - isConvertibleToTextMessage = !itemProvider.registeredTypeIdentifiers.contains(kUTTypeFileURL as String) + let isConvertibleToTextMessage = !itemProvider.registeredTypeIdentifiers.contains(kUTTypeFileURL as String) if UTTypeConformsTo(srcUtiType as CFString, kUTTypeText) { - resolver.fulfill((itemUrl: fileUrl, utiType: srcUtiType)) + resolver.fulfill(LoadedItem(itemProvider: itemProvider, + itemUrl: fileUrl, + utiType: srcUtiType, + isConvertibleToTextMessage: isConvertibleToTextMessage)) } else { - resolver.fulfill((itemUrl: fileUrl, utiType: kUTTypeText as String)) + resolver.fulfill(LoadedItem(itemProvider: itemProvider, + itemUrl: fileUrl, + utiType: kUTTypeText as String, + isConvertibleToTextMessage: isConvertibleToTextMessage)) } } else if let url = value as? URL { // If the share itself is a URL (e.g. a link from Safari), try to send this as a text message. - isConvertibleToTextMessage = (itemProvider.registeredTypeIdentifiers.contains(kUTTypeURL as String) && + let isConvertibleToTextMessage = (itemProvider.registeredTypeIdentifiers.contains(kUTTypeURL as String) && !itemProvider.registeredTypeIdentifiers.contains(kUTTypeFileURL as String)) if isConvertibleToTextMessage { - resolver.fulfill((itemUrl: url, utiType: kUTTypeURL as String)) + resolver.fulfill(LoadedItem(itemProvider: itemProvider, + itemUrl: url, + utiType: kUTTypeURL as String, + isConvertibleToTextMessage: isConvertibleToTextMessage)) } else { - resolver.fulfill((itemUrl: url, utiType: srcUtiType)) + resolver.fulfill(LoadedItem(itemProvider: itemProvider, + itemUrl: url, + utiType: srcUtiType, + isConvertibleToTextMessage: isConvertibleToTextMessage)) } } else if let image = value as? UIImage { if let data = UIImagePNGRepresentation(image) { @@ -782,7 +858,8 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed do { let url = NSURL.fileURL(withPath: tempFilePath) try data.write(to: url) - resolver.fulfill((url, srcUtiType)) + resolver.fulfill(LoadedItem(itemProvider: itemProvider, itemUrl: url, + utiType: srcUtiType)) } catch { resolver.reject(ShareViewControllerError.assertionError(description: "couldn't write UIImage: \(String(describing: error))")) } @@ -799,76 +876,108 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed itemProvider.loadItem(forTypeIdentifier: srcUtiType, options: nil, completionHandler: loadCompletion) - return promise.then { [weak self] (itemUrl: URL, utiType: String) -> Promise in - guard let strongSelf = self else { - let error = ShareViewControllerError.obsoleteShare - return Promise(error: error) - } - - let url: URL = try { - if strongSelf.isVideoNeedingRelocation(itemProvider: itemProvider, itemUrl: itemUrl) { - return try SignalAttachment.copyToVideoTempDir(url: itemUrl) - } else { - return itemUrl - } - }() + return promise + } - Logger.debug("building DataSource with url: \(url), utiType: \(utiType)") + private func buildAttachment(forLoadedItem loadedItem: LoadedItem) -> Promise { + let itemProvider = loadedItem.itemProvider + let itemUrl = loadedItem.itemUrl + let utiType = loadedItem.utiType - guard let dataSource = ShareViewController.createDataSource(utiType: utiType, url: url, customFileName: customFileName) else { - throw ShareViewControllerError.assertionError(description: "Unable to read attachment data") + var url = itemUrl + do { + if isVideoNeedingRelocation(itemProvider: itemProvider, itemUrl: itemUrl) { + url = try SignalAttachment.copyToVideoTempDir(url: itemUrl) } + } catch { + let error = ShareViewControllerError.assertionError(description: "Could not copy video") + return Promise(error: error) + } - // start with base utiType, but it might be something generic like "image" - var specificUTIType = utiType - if utiType == (kUTTypeURL as String) { - // Use kUTTypeURL for URLs. - } else if UTTypeConformsTo(utiType as CFString, kUTTypeText) { - // Use kUTTypeText for text. - } else if url.pathExtension.count > 0 { - // Determine a more specific utiType based on file extension - if let typeExtension = MIMETypeUtil.utiType(forFileExtension: url.pathExtension) { - Logger.debug("utiType based on extension: \(typeExtension)") - specificUTIType = typeExtension - } + Logger.debug("building DataSource with url: \(url), utiType: \(utiType)") + + guard let dataSource = ShareViewController.createDataSource(utiType: utiType, url: url, customFileName: loadedItem.customFileName) else { + let error = ShareViewControllerError.assertionError(description: "Unable to read attachment data") + return Promise(error: error) + } + + // start with base utiType, but it might be something generic like "image" + var specificUTIType = utiType + if utiType == (kUTTypeURL as String) { + // Use kUTTypeURL for URLs. + } else if UTTypeConformsTo(utiType as CFString, kUTTypeText) { + // Use kUTTypeText for text. + } else if url.pathExtension.count > 0 { + // Determine a more specific utiType based on file extension + if let typeExtension = MIMETypeUtil.utiType(forFileExtension: url.pathExtension) { + Logger.debug("utiType based on extension: \(typeExtension)") + specificUTIType = typeExtension } + } - guard !SignalAttachment.isInvalidVideo(dataSource: dataSource, dataUTI: specificUTIType) else { - // This can happen, e.g. when sharing a quicktime-video from iCloud drive. + guard !SignalAttachment.isInvalidVideo(dataSource: dataSource, dataUTI: specificUTIType) else { + // This can happen, e.g. when sharing a quicktime-video from iCloud drive. - let (promise, exportSession) = SignalAttachment.compressVideoAsMp4(dataSource: dataSource, dataUTI: specificUTIType) + let (promise, exportSession) = SignalAttachment.compressVideoAsMp4(dataSource: dataSource, dataUTI: specificUTIType) - // TODO: How can we move waiting for this export to the end of the share flow rather than having to do it up front? - // Ideally we'd be able to start it here, and not block the UI on conversion unless there's still work to be done - // when the user hits "send". - if let exportSession = exportSession { - let progressPoller = ProgressPoller(timeInterval: 0.1, ratioCompleteBlock: { return exportSession.progress }) - strongSelf.progressPoller = progressPoller - progressPoller.startPolling() + // TODO: How can we move waiting for this export to the end of the share flow rather than having to do it up front? + // Ideally we'd be able to start it here, and not block the UI on conversion unless there's still work to be done + // when the user hits "send". + if let exportSession = exportSession { + let progressPoller = ProgressPoller(timeInterval: 0.1, ratioCompleteBlock: { return exportSession.progress }) + self.progressPoller = progressPoller + progressPoller.startPolling() - guard let loadViewController = strongSelf.loadViewController else { - owsFailDebug("load view controller was unexpectedly nil") - return promise - } + guard let loadViewController = self.loadViewController else { + owsFailDebug("load view controller was unexpectedly nil") + return promise + } - DispatchQueue.main.async { - loadViewController.progress = progressPoller.progress - } + DispatchQueue.main.async { + loadViewController.progress = progressPoller.progress } + } + + return promise + } + + let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType, imageQuality: .medium) + if loadedItem.isConvertibleToContactShare { + Logger.info("isConvertibleToContactShare") + attachment.isConvertibleToContactShare = true + } else if loadedItem.isConvertibleToTextMessage { + Logger.info("isConvertibleToTextMessage") + attachment.isConvertibleToTextMessage = true + } + return Promise.value(attachment) + } - return promise + private func buildAttachments() -> Promise<[SignalAttachment]> { + let promise = selectItemProviders().then { [weak self] (itemProviders) -> Promise<[SignalAttachment]> in + guard let strongSelf = self else { + let error = ShareViewControllerError.assertionError(description: "expired") + return Promise(error: error) } - let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType, imageQuality: .medium) - if isConvertibleToContactShare { - Logger.info("isConvertibleToContactShare") - attachment.isConvertibleToContactShare = isConvertibleToContactShare - } else if isConvertibleToTextMessage { - Logger.info("isConvertibleToTextMessage") - attachment.isConvertibleToTextMessage = isConvertibleToTextMessage + var loadPromises = [Promise]() + for itemProvider in itemProviders { + let loadPromise = strongSelf.loadItemProvider(itemProvider: itemProvider) + .then({ (loadedItem) -> Promise in + return strongSelf.buildAttachment(forLoadedItem: loadedItem) + }) + + loadPromises.append(loadPromise) } - return Promise.value(attachment) - } + return when(fulfilled: loadPromises) + }.then { (signalAttachments) -> Promise<[SignalAttachment]> in + guard signalAttachments.count > 0 else { + let error = ShareViewControllerError.assertionError(description: "no valid attachments") + return Promise(error: error) + } + return Promise.value(signalAttachments) + } + promise.retainUntilComplete() + return promise } // Some host apps (e.g. iOS Photos.app) sometimes auto-converts some video formats (e.g. com.apple.quicktime-movie)