From 66c239bbbb54f246fbd54beece778dcdb3e8987c Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 23 Oct 2019 11:51:29 -0300 Subject: [PATCH] Message forwarding. --- Signal.xcodeproj/project.pbxproj | 10 +- .../CameraFirstNavigationController.swift | 75 +---- .../ConversationViewController.m | 9 +- .../ForwardMessageNavigationController.swift | 274 ++++++++++++++++-- ...dMediaNavigationController+Multisend.swift | 86 ++++++ .../SendMediaNavigationController.swift | 75 ++++- 6 files changed, 427 insertions(+), 102 deletions(-) create mode 100644 Signal/src/ViewControllers/Photos/SendMediaNavigationController+Multisend.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index b8de4514ca..39e52103a2 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 340CCB722305962B005243B3 /* YDBToGRDBMigrationModelTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340CCB702305962B005243B3 /* YDBToGRDBMigrationModelTest.swift */; }; 340E9AC0235F876800FA362C /* ForwardMessageNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340E9ABF235F876800FA362C /* ForwardMessageNavigationController.swift */; }; 340E9AC2235F878900FA362C /* TextApprovalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340E9AC1235F878800FA362C /* TextApprovalViewController.swift */; }; + 340E9AC4236095CD00FA362C /* SendMediaNavigationController+Multisend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340E9AC3236095CC00FA362C /* SendMediaNavigationController+Multisend.swift */; }; 340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC87B204DAC8C007AEB0F /* NotificationSettingsOptionsViewController.m */; }; 340FC8AA204DAC8D007AEB0F /* NotificationSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC87C204DAC8C007AEB0F /* NotificationSettingsViewController.m */; }; 340FC8AB204DAC8D007AEB0F /* DomainFrontingCountryViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC87D204DAC8C007AEB0F /* DomainFrontingCountryViewController.m */; }; @@ -755,6 +756,7 @@ 340CCB702305962B005243B3 /* YDBToGRDBMigrationModelTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YDBToGRDBMigrationModelTest.swift; sourceTree = ""; }; 340E9ABF235F876800FA362C /* ForwardMessageNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForwardMessageNavigationController.swift; sourceTree = ""; }; 340E9AC1235F878800FA362C /* TextApprovalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextApprovalViewController.swift; sourceTree = ""; }; + 340E9AC3236095CC00FA362C /* SendMediaNavigationController+Multisend.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SendMediaNavigationController+Multisend.swift"; sourceTree = ""; }; 340FC87B204DAC8C007AEB0F /* NotificationSettingsOptionsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NotificationSettingsOptionsViewController.m; sourceTree = ""; }; 340FC87C204DAC8C007AEB0F /* NotificationSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NotificationSettingsViewController.m; sourceTree = ""; }; 340FC87D204DAC8C007AEB0F /* DomainFrontingCountryViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DomainFrontingCountryViewController.m; sourceTree = ""; }; @@ -2030,13 +2032,14 @@ 34969558219B605E00DCFE74 /* Photos */ = { isa = PBXGroup; children = ( - 4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */, 34969559219B605E00DCFE74 /* ImagePickerController.swift */, + 4C21D5D7223AC60F00EF8A77 /* PhotoCapture.swift */, + 4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */, 3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */, 3496955B219B605E00DCFE74 /* PhotoLibrary.swift */, - 4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */, - 4C21D5D7223AC60F00EF8A77 /* PhotoCapture.swift */, 4C4F5EBB22711EEB00F3DD01 /* SendMediaBottomButton.swift */, + 4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */, + 340E9AC3236095CC00FA362C /* SendMediaNavigationController+Multisend.swift */, ); path = Photos; sourceTree = ""; @@ -4044,6 +4047,7 @@ 340CCB6A23032762005243B3 /* YDBLegacyMigration.m in Sources */, 3496955D219B605E00DCFE74 /* PhotoCollectionPickerController.swift in Sources */, 8835DDFD230CEE8900DC6B66 /* RecipientPickerDelegate.swift in Sources */, + 340E9AC4236095CD00FA362C /* SendMediaNavigationController+Multisend.swift in Sources */, 34CE88E71F2FB9A10098030F /* ProfileViewController.m in Sources */, 3403B95D20EA9527001A1F44 /* OWSContactShareButtonsView.m in Sources */, 34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */, diff --git a/Signal/src/ViewControllers/CameraFirstNavigationController.swift b/Signal/src/ViewControllers/CameraFirstNavigationController.swift index 268e666cea..58541a1b3d 100644 --- a/Signal/src/ViewControllers/CameraFirstNavigationController.swift +++ b/Signal/src/ViewControllers/CameraFirstNavigationController.swift @@ -20,16 +20,6 @@ public class CameraFirstCaptureSendFlow: NSObject { var approvalMessageText: String? var selectedConversations: [ConversationItem] = [] - - // MARK: Dependencies - - var databaseStorage: SDSDatabaseStorage { - return SSKEnvironment.shared.databaseStorage - } - - var broadcastMediaMessageJobQueue: BroadcastMediaMessageJobQueue { - return AppEnvironment.shared.broadcastMediaMessageJobQueue - } } extension CameraFirstCaptureSendFlow: SendMediaNavDelegate { @@ -92,64 +82,11 @@ extension CameraFirstCaptureSendFlow: ConversationPickerDelegate { } let conversations = selectedConversationsForConversationPicker - DispatchQueue.global().async(.promise) { - // Duplicate attachments per conversation - let conversationAttachments: [(ConversationItem, [SignalAttachment])] = - try conversations.map { conversation in - return (conversation, try approvedAttachments.map { try $0.cloneAttachment() }) - } - - // We only upload one set of attachments, and then copy the upload details into - // each conversation before sending. - let attachmentsToUpload: [OutgoingAttachmentInfo] = approvedAttachments.map { attachment in - return OutgoingAttachmentInfo(dataSource: attachment.dataSource, - contentType: attachment.mimeType, - sourceFilename: attachment.filenameOrDefault, - caption: attachment.captionText, - albumMessageId: nil) - } - - self.databaseStorage.write { transaction in - var messages: [TSOutgoingMessage] = [] - - for (conversation, attachments) in conversationAttachments { - let thread: TSThread - switch conversation.messageRecipient { - case .contact(let address): - thread = TSContactThread.getOrCreateThread(withContactAddress: address, - transaction: transaction) - case .group(let groupThread): - thread = groupThread - } - - let message = try! ThreadUtil.createUnsentMessage(withText: self.approvalMessageText, - mediaAttachments: attachments, - in: thread, - quotedReplyModel: nil, - linkPreviewDraft: nil, - transaction: transaction) - messages.append(message) - } - - // map of attachments we'll upload to their copies in each recipient thread - var attachmentIdMap: [String: [String]] = [:] - let correspondingAttachmentIds = transpose(messages.map { $0.attachmentIds }) - for (index, attachmentInfo) in attachmentsToUpload.enumerated() { - do { - let attachmentToUpload = try attachmentInfo.asStreamConsumingDataSource(withIsVoiceMessage: false) - attachmentToUpload.anyInsert(transaction: transaction) - - attachmentIdMap[attachmentToUpload.uniqueId] = correspondingAttachmentIds[index] - } catch { - owsFailDebug("error: \(error)") - } - } - - self.broadcastMediaMessageJobQueue.add(attachmentIdMap: attachmentIdMap, - transaction: transaction) - } - }.done { _ in - self.delegate?.cameraFirstCaptureSendFlowDidComplete(self) - }.retainUntilComplete() + SendMediaNavigationController.sendApprovedMedia(conversations: conversations, + approvalMessageText: self.approvalMessageText, + approvedAttachments: approvedAttachments) + .done { _ in + self.delegate?.cameraFirstCaptureSendFlowDidComplete(self) + }.retainUntilComplete() } } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 88a31c7bc0..9641b6c824 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -2052,11 +2052,18 @@ typedef enum : NSUInteger { { OWSAssertDebug(conversationViewItem); + // if (conversationViewItem.messageCellType == OWSMessageCellType_MediaMessage) { ForwardMessageNavigationController *modal = [[ForwardMessageNavigationController alloc] initWithConversationViewItem:conversationViewItem]; modal.forwardMessageDelegate = self; - [self presentFullScreenViewController:modal animated:YES completion:nil]; + // } else { + // NSMutableArray *attachments = [NSMutableArray new]; + // ForwardMediaNavigationController *modal = [ForwardMediaNavigationController + // forwardMediaModalWithAttachments:<#(NSArray * _Nonnull)#>]; + // modal.forwardMediaSendFlow.delegate = self; + // [self presentFullScreenViewController:modal animated:YES completion:nil]; + // } } #pragma mark - MessageDetailViewDelegate diff --git a/Signal/src/ViewControllers/ForwardMessageNavigationController.swift b/Signal/src/ViewControllers/ForwardMessageNavigationController.swift index 79fada7dcf..c366abcf34 100644 --- a/Signal/src/ViewControllers/ForwardMessageNavigationController.swift +++ b/Signal/src/ViewControllers/ForwardMessageNavigationController.swift @@ -35,7 +35,7 @@ extension ForwardMessageNavigationController { let approvalView = TextApprovalViewController(messageText: body) approvalView.delegate = self - self.pushViewController(approvalView, animated: true) + pushViewController(approvalView, animated: true) case .contactShare: guard let oldContactShare = conversationViewItem.contactShare else { throw OWSAssertionError("Missing contactShareViewModel.") @@ -43,14 +43,67 @@ extension ForwardMessageNavigationController { let newContactShare = oldContactShare.copyForResending() let approvalView = ContactShareApprovalViewController(contactShare: newContactShare) approvalView.delegate = self - self.pushViewController(approvalView, animated: true) + pushViewController(approvalView, animated: true) case .audio, .genericAttachment, .stickerMessage: // Skip approval for these message types. send() case .mediaMessage: - throw OWSAssertionError("Invalid message type.") +// fileprivate func pushApprovalViewController( +// attachmentApprovalItems: [AttachmentApprovalItem], +// options: AttachmentApprovalViewControllerOptions = .canAddMore, +// animated: Bool +// ) { +// guard let sendMediaNavDelegate = self.sendMediaNavDelegate else { +// owsFailDebug("sendMediaNavDelegate was unexpectedly nil") +// return +// } + +// public static let canAddMore = AttachmentApprovalViewControllerOptions(rawValue: 1 << 0) +// public static let hasCancel = AttachmentApprovalViewControllerOptions(rawValue: 1 << 1) +// public static let canToggleViewOnce = AttachmentApprovalViewControllerOptions(rawValue: 1 << 2) + let options: AttachmentApprovalViewControllerOptions = .hasCancel + let sendButtonImageName = "send-solid-24" + + var attachmentApprovalItems = [AttachmentApprovalItem]() + guard let mediaAlbumItems = conversationViewItem.mediaAlbumItems else { + throw OWSAssertionError("Missing mediaAlbumItems.") + } + for mediaAlbumItem in mediaAlbumItems { + guard let attachmentStream = mediaAlbumItem.attachmentStream else { + continue + } + let signalAttachment = try attachmentStream.asSignalAttachmentForSending() +// @interface ConversationMediaAlbumItem : NSObject +// +// @property (nonatomic, readonly) TSAttachment *attachment; +// +// // This property will only be set if the attachment is downloaded. +// @property (nonatomic, readonly, nullable) TSAttachmentStream *attachmentStream; +// +// // This property will be non-zero if the attachment is valid. +// @property (nonatomic, readonly) CGSize mediaSize; +// +// @property (nonatomic, readonly, nullable) NSString *caption; +// +// @property (nonatomic, readonly) BOOL isFailedDownload; + +// for attachment in attachments { + let attachmentApprovalItem = AttachmentApprovalItem(attachment: signalAttachment, canSave: false) + attachmentApprovalItems.append(attachmentApprovalItem) +// let cameraCaptureAttachment = CameraCaptureAttachment(signalAttachment: attachment, canSave: false) +// navController.attachmentDraftCollection.append(.camera(attachment: cameraCaptureAttachment)) +// attachmentApprovalItems.append(cameraCaptureAttachment.attachmentApprovalItem) + } + // let approvalItem = Attachmen + let approvalViewController = AttachmentApprovalViewController(options: options, + sendButtonImageName: sendButtonImageName, + attachmentApprovalItems: attachmentApprovalItems) + approvalViewController.approvalDelegate = self + approvalViewController.messageText = approvalMessageText + + pushViewController(approvalViewController, animated: true) case .unknown, .oversizeTextDownloading, .viewOnce: @@ -107,29 +160,21 @@ extension ForwardMessageNavigationController { } send { (thread, transaction) in - guard let sourceUrl = attachmentStream.originalMediaURL else { - throw OWSAssertionError("Missing originalMediaURL.") - } - guard let dataUTI = MIMETypeUtil.utiType(forMIMEType: attachmentStream.contentType) else { - throw OWSAssertionError("Missing dataUTI.") - } - let newUrl = OWSFileSystem.temporaryFileUrl(fileExtension: sourceUrl.pathExtension) - try FileManager.default.copyItem(at: sourceUrl, to: newUrl) - - let clonedDataSource = try DataSourcePath.dataSource(with: newUrl, - shouldDeleteOnDeallocation: true) - clonedDataSource.sourceFilename = attachmentStream.sourceFilename - - var attachment: SignalAttachment - if attachmentStream.isVoiceMessage { - attachment = SignalAttachment.voiceMessageAttachment(dataSource: clonedDataSource, dataUTI: dataUTI) - } else { - attachment = SignalAttachment.attachment(dataSource: clonedDataSource, dataUTI: dataUTI, imageQuality: .original) - } + let attachment = try attachmentStream.asSignalAttachmentForSending() self.send(body: "", attachment: attachment, thread: thread, transaction: transaction) } case .mediaMessage: - throw OWSAssertionError("Invalid message type.") + guard let approvedAttachments = approvedAttachments else { + throw OWSAssertionError("Missing approvedAttachments.") + } + + let conversations = selectedConversationsForConversationPicker + SendMediaNavigationController.sendApprovedMedia(conversations: conversations, + approvalMessageText: self.approvalMessageText, + approvedAttachments: approvedAttachments) + .done { threads in + self.forwardMessageDelegate?.forwardMessageFlowDidComplete(threads: threads) + }.retainUntilComplete() case .unknown, .oversizeTextDownloading, .viewOnce: @@ -211,10 +256,6 @@ class ForwardMessageNavigationController: OWSNavigationController { return SSKEnvironment.shared.databaseStorage } - var broadcastMediaMessageJobQueue: BroadcastMediaMessageJobQueue { - return AppEnvironment.shared.broadcastMediaMessageJobQueue - } - var messageSenderJobQueue: MessageSenderJobQueue { return SSKEnvironment.shared.messageSenderJobQueue } @@ -310,3 +351,182 @@ extension ForwardMessageNavigationController: ContactShareApprovalViewController forwardMessageDelegate?.forwardMessageFlowDidCancel() } } + +// MARK: - + +extension ForwardMessageNavigationController: AttachmentApprovalViewControllerDelegate { + + func attachmentApprovalDidAppear(_ attachmentApproval: AttachmentApprovalViewController) { + // TODO: +// updateViewState(topViewController: attachmentApproval) + } + + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeMessageText newMessageText: String?) { + self.approvalMessageText = newMessageText +// sendMediaNavDelegate?.sendMediaNav(self, didChangeMessageText: newMessageText) + } + + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) { +// guard let removedDraft = attachmentDraftCollection.attachmentDraft(forAttachment: attachment) else { +// owsFailDebug("removedDraft was unexpectedly nil") +// return +// } +// +// attachmentDraftCollection.remove(removedDraft) + } + + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) { + self.approvedAttachments = attachments + self.approvalMessageText = messageText + + send() + } + + func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) { + forwardMessageDelegate?.forwardMessageFlowDidCancel() + } + + func attachmentApprovalDidTapAddMore(_ attachmentApproval: AttachmentApprovalViewController) { + owsFailDebug("Cannot add more to message forwards.") + } + + var attachmentApprovalTextInputContextIdentifier: String? { + return nil + } +} + +//extension ForwardMediaNavigationController: SendMediaNavDelegate { +// func approveContactShare(_ approveContactShare: ContactShareApprovalViewController, +// didApproveContactShare contactShare: ContactShareViewModel) { +// approvedContactShare = contactShare +// +// send() +// } +// +// func approveContactShare(_ approveContactShare: ContactShareApprovalViewController, +// didCancelContactShare contactShare: ContactShareViewModel) { +// forwardMessageDelegate?.forwardMessageFlowDidCancel() +// } +//} + +//@objc +//public protocol CameraFirstCaptureDelegate: AnyObject { +// func cameraFirstCaptureSendFlowDidComplete(_ cameraFirstCaptureSendFlow: CameraFirstCaptureSendFlow) +// func cameraFirstCaptureSendFlowDidCancel(_ cameraFirstCaptureSendFlow: CameraFirstCaptureSendFlow) +//} + +//@objc +//public class ForwardMediaSendFlow: NSObject { +// @objc +// public weak var delegate: ForwardMessageDelegate? +// +// var approvedAttachments: [SignalAttachment]? +// var approvalMessageText: String? +// +// var selectedConversations: [ConversationItem] = [] +// +// // MARK: Dependencies +// +// var databaseStorage: SDSDatabaseStorage { +// return SSKEnvironment.shared.databaseStorage +// } +//} +////func forwardMessageFlowDidComplete(threads: [TSThread]) +////func forwardMessageFlowDidCancel() +// +//extension ForwardMediaSendFlow: SendMediaNavDelegate { +// func sendMediaNavDidCancel(_ sendMediaNavigationController: SendMediaNavigationController) { +// delegate?.forwardMessageFlowDidCancel() +// } +// +// func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) { +// self.approvedAttachments = attachments +// self.approvalMessageText = messageText +// +// let pickerVC = ConversationPickerViewController() +// pickerVC.delegate = self +// sendMediaNavigationController.pushViewController(pickerVC, animated: true) +// } +// +// func sendMediaNavInitialMessageText(_ sendMediaNavigationController: SendMediaNavigationController) -> String? { +// return approvalMessageText +// } +// +// func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didChangeMessageText newMessageText: String?) { +// self.approvalMessageText = newMessageText +// } +// +// var sendMediaNavApprovalButtonImageName: String { +// return "arrow-right-24" +// } +// +// var sendMediaNavCanSaveAttachments: Bool { +// return true +// } +// +// var sendMediaNavTextInputContextIdentifier: String? { +// return nil +// } +//} +// +//extension ForwardMediaSendFlow: ConversationPickerDelegate { +// var selectedConversationsForConversationPicker: [ConversationItem] { +// return selectedConversations +// } +// +// func conversationPicker(_ conversationPickerViewController: ConversationPickerViewController, +// didSelectConversation conversation: ConversationItem) { +// self.selectedConversations.append(conversation) +// } +// +// func conversationPicker(_ conversationPickerViewController: ConversationPickerViewController, +// didDeselectConversation conversation: ConversationItem) { +// self.selectedConversations = self.selectedConversations.filter { +// $0.messageRecipient != conversation.messageRecipient +// } +// } +// +// func conversationPickerDidCompleteSelection(_ conversationPickerViewController: ConversationPickerViewController) { +// guard let approvedAttachments = self.approvedAttachments else { +// owsFailDebug("approvedAttachments was unexpectedly nil") +// delegate?.forwardMessageFlowDidCancel() +// return +// } +// +// let conversations = selectedConversationsForConversationPicker +// SendMediaNavigationController.sendApprovedMedia(conversations: conversations, +// approvalMessageText: self.approvalMessageText, +// approvedAttachments: approvedAttachments) +// .done { threads in +// self.delegate?.forwardMessageFlowDidComplete(threads: threads) +// }.retainUntilComplete() +// } +//} + +// MARK: - + +extension TSAttachmentStream { + func asSignalAttachmentForSending() throws -> SignalAttachment { + guard let sourceUrl = originalMediaURL else { + throw OWSAssertionError("Missing originalMediaURL.") + } + guard let dataUTI = MIMETypeUtil.utiType(forMIMEType: contentType) else { + throw OWSAssertionError("Missing dataUTI.") + } + let newUrl = OWSFileSystem.temporaryFileUrl(fileExtension: sourceUrl.pathExtension) + try FileManager.default.copyItem(at: sourceUrl, to: newUrl) + + let clonedDataSource = try DataSourcePath.dataSource(with: newUrl, + shouldDeleteOnDeallocation: true) + clonedDataSource.sourceFilename = sourceFilename + + var signalAttachment: SignalAttachment + if isVoiceMessage { + signalAttachment = SignalAttachment.voiceMessageAttachment(dataSource: clonedDataSource, dataUTI: dataUTI) + } else { + signalAttachment = SignalAttachment.attachment(dataSource: clonedDataSource, dataUTI: dataUTI, imageQuality: .original) + } + signalAttachment.captionText = caption + return signalAttachment + } +} diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController+Multisend.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController+Multisend.swift new file mode 100644 index 0000000000..c5564143bd --- /dev/null +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController+Multisend.swift @@ -0,0 +1,86 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation +import PromiseKit + +extension SendMediaNavigationController { + + // MARK: Dependencies + + class var databaseStorage: SDSDatabaseStorage { + return SSKEnvironment.shared.databaseStorage + } + + class var broadcastMediaMessageJobQueue: BroadcastMediaMessageJobQueue { + return AppEnvironment.shared.broadcastMediaMessageJobQueue + } + + // MARK: - + + class func sendApprovedMedia(conversations: [ConversationItem], + approvalMessageText: String?, + approvedAttachments: [SignalAttachment]) -> Promise<[TSThread]> { + return DispatchQueue.global().async(.promise) { + // Duplicate attachments per conversation + let conversationAttachments: [(ConversationItem, [SignalAttachment])] = + try conversations.map { conversation in + return (conversation, try approvedAttachments.map { try $0.cloneAttachment() }) + } + + // We only upload one set of attachments, and then copy the upload details into + // each conversation before sending. + let attachmentsToUpload: [OutgoingAttachmentInfo] = approvedAttachments.map { attachment in + return OutgoingAttachmentInfo(dataSource: attachment.dataSource, + contentType: attachment.mimeType, + sourceFilename: attachment.filenameOrDefault, + caption: attachment.captionText, + albumMessageId: nil) + } + + var threads: [TSThread] = [] + self.databaseStorage.write { transaction in + var messages: [TSOutgoingMessage] = [] + + for (conversation, attachments) in conversationAttachments { + let thread: TSThread + switch conversation.messageRecipient { + case .contact(let address): + thread = TSContactThread.getOrCreateThread(withContactAddress: address, + transaction: transaction) + case .group(let groupThread): + thread = groupThread + } + + let message = try! ThreadUtil.createUnsentMessage(withText: approvalMessageText, + mediaAttachments: attachments, + in: thread, + quotedReplyModel: nil, + linkPreviewDraft: nil, + transaction: transaction) + messages.append(message) + threads.append(thread) + } + + // map of attachments we'll upload to their copies in each recipient thread + var attachmentIdMap: [String: [String]] = [:] + let correspondingAttachmentIds = transpose(messages.map { $0.attachmentIds }) + for (index, attachmentInfo) in attachmentsToUpload.enumerated() { + do { + let attachmentToUpload = try attachmentInfo.asStreamConsumingDataSource(withIsVoiceMessage: false) + attachmentToUpload.anyInsert(transaction: transaction) + + attachmentIdMap[attachmentToUpload.uniqueId] = correspondingAttachmentIds[index] + } catch { + owsFailDebug("error: \(error)") + } + } + + self.broadcastMediaMessageJobQueue.add(attachmentIdMap: attachmentIdMap, + transaction: transaction) + } + return threads + } + } +} diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift index 2f28af8209..2ac2ad9499 100644 --- a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -18,6 +18,8 @@ protocol SendMediaNavDelegate: AnyObject { var sendMediaNavTextInputContextIdentifier: String? { get } } +// MARK: - + @objc class SendMediaNavigationController: OWSNavigationController { @@ -289,7 +291,7 @@ class SendMediaNavigationController: OWSNavigationController { // MARK: State - private var attachmentDraftCollection: AttachmentDraftCollection = .empty + fileprivate var attachmentDraftCollection: AttachmentDraftCollection = .empty private var attachmentApprovalItemPromises: [Promise] { return attachmentDraftCollection.attachmentApprovalItemPromises @@ -311,7 +313,7 @@ class SendMediaNavigationController: OWSNavigationController { return vc }() - private func pushApprovalViewController( + fileprivate func pushApprovalViewController( attachmentApprovalItems: [AttachmentApprovalItem], options: AttachmentApprovalViewControllerOptions = .canAddMore, animated: Bool @@ -586,6 +588,7 @@ extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegat private enum AttachmentDraft { case camera(attachment: CameraCaptureAttachment) case picker(attachment: MediaLibraryAttachment) +// case forward(attachment: MediaForwardAttachment) } private extension AttachmentDraft { @@ -708,6 +711,26 @@ private struct MediaLibraryAttachment: Hashable, Equatable { } } +//private struct MediaForwardAttachment: Hashable, Equatable { +// let signalAttachment: SignalAttachment +// let attachmentApprovalItem: AttachmentApprovalItem +// let attachmentApprovalItemPromise: Promise +// +// init(signalAttachment: SignalAttachment, canSave: Bool) { +// self.signalAttachment = signalAttachment +// self.attachmentApprovalItem = AttachmentApprovalItem(attachment: signalAttachment, canSave: canSave) +// self.attachmentApprovalItemPromise = Promise.value(attachmentApprovalItem) +// } +// +// func hash(into hasher: inout Hasher) { +// hasher.combine(signalAttachment) +// } +// +// static func ==(lhs: MediaForwardAttachment, rhs: MediaForwardAttachment) -> Bool { +// return lhs.signalAttachment == rhs.signalAttachment +// } +//} + extension SendMediaNavigationController: DoneButtonDelegate { var doneButtonCount: Int { return attachmentCount @@ -830,3 +853,51 @@ class CaptureFirstCaptureNavigationController: SendMediaNavigationController { return navController } } + +// MARK: - + +//@objc +//class ForwardMediaNavigationController: SendMediaNavigationController { +//// delegate: SendMediaNavDelegate +//// @objc +//// private(set) var cameraFirstCaptureSendFlow: CameraFirstCaptureSendFlow! +// @objc +// public let forwardMediaSendFlow = ForwardMediaSendFlow() +// +// @objc +// public class func forwardMediaModal(attachments: [SignalAttachment]) -> ForwardMediaNavigationController { +// let navController = ForwardMediaNavigationController() +// +// navController.sendMediaNavDelegate = navController.forwardMediaSendFlow +// +// var attachmentApprovalItems = [AttachmentApprovalItem]() +// for attachment in attachments { +// let cameraCaptureAttachment = CameraCaptureAttachment(signalAttachment: attachment, canSave: false) +// navController.attachmentDraftCollection.append(.camera(attachment: cameraCaptureAttachment)) +// attachmentApprovalItems.append(cameraCaptureAttachment.attachmentApprovalItem) +// } +//// let approvalItem = AttachmentApprovalItem(attachment: attachment, canSave: false) +//// let libraryMedia = MediaLibraryAttachment(asset: asset, +//// attachmentApprovalItemPromise: .value(approvalItem)) +//// navController.attachmentDraftCollection.append(.picker(attachment: libraryMedia)) +// +//// navController.setViewControllers([navController.mediaLibraryViewController], animated: false) +// +// // Since we're starting on the approval view, include cancel to allow the user to immediately dismiss. +// // If they choose to add more, `hasCancel` will go away and they'll enter the normal gallery flow. +// navController.pushApprovalViewController( +// attachmentApprovalItems: attachmentApprovalItems, +// options: [.canAddMore, .hasCancel], +// animated: false +// ) +// +// +//// navController.setViewControllers([navController.captureViewController], animated: false) +// +//// let cameraFirstCaptureSendFlow = CameraFirstCaptureSendFlow() +//// navController.cameraFirstCaptureSendFlow = cameraFirstCaptureSendFlow +//// navController.sendMediaNavDelegate = cameraFirstCaptureSendFlow +// +// return navController +// } +//}