From 7dbb9517af973364f3edf8139e92aa0230fa8e5b Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Tue, 26 Mar 2019 18:18:13 -0600 Subject: [PATCH] Centralize attachment state in nav controller --- Signal.xcodeproj/project.pbxproj | 4 + .../ConversationViewController.m | 60 +--- .../Photos/ImagePickerController.swift | 228 ++++---------- .../Photos/PhotoCaptureViewController.swift | 1 + .../ViewControllers/Photos/PhotoLibrary.swift | 9 +- .../SendMediaNavigationController.swift | 295 ++++++++++++++++++ .../AttachmentApprovalViewController.swift | 18 +- .../SharingThreadPickerViewController.m | 3 +- .../Views/ImageEditor/OrderedDictionary.swift | 4 + .../attachments/SignalAttachment.swift | 4 - SignalServiceKit/src/Util/FeatureFlags.swift | 2 +- 11 files changed, 402 insertions(+), 226 deletions(-) create mode 100644 Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 3d13edd0f..c05e80e06 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -483,6 +483,7 @@ 4C3E245D21F2B395000AE092 /* DirectionalPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */; }; 4C3EF7FD2107DDEE0007EBF7 /* ParamParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EF7FC2107DDEE0007EBF7 /* ParamParserTest.swift */; }; 4C3EF802210918740007EBF7 /* SSKProtoEnvelopeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EF801210918740007EBF7 /* SSKProtoEnvelopeTest.swift */; }; + 4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */; }; 4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */; }; 4C4BC6C32102D697004040C9 /* ContactDiscoveryOperationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */; }; 4C5250D221E7BD7D00CE3D95 /* PhoneNumberValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5250D121E7BD7D00CE3D95 /* PhoneNumberValidator.swift */; }; @@ -1233,6 +1234,7 @@ 4C2F454E214C00E1004871FF /* AvatarTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarTableViewCell.swift; sourceTree = ""; }; 4C3EF7FC2107DDEE0007EBF7 /* ParamParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParamParserTest.swift; sourceTree = ""; }; 4C3EF801210918740007EBF7 /* SSKProtoEnvelopeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKProtoEnvelopeTest.swift; sourceTree = ""; }; + 4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMediaNavigationController.swift; sourceTree = ""; }; 4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissableTextField.swift; sourceTree = ""; }; 4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ContactDiscoveryOperationTest.swift; path = contact/ContactDiscoveryOperationTest.swift; sourceTree = ""; }; 4C5250D121E7BD7D00CE3D95 /* PhoneNumberValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberValidator.swift; sourceTree = ""; }; @@ -1857,6 +1859,7 @@ 34969558219B605E00DCFE74 /* Photos */ = { isa = PBXGroup; children = ( + 4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */, 34969559219B605E00DCFE74 /* ImagePickerController.swift */, 3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */, 3496955B219B605E00DCFE74 /* PhotoLibrary.swift */, @@ -3659,6 +3662,7 @@ 340FC8AB204DAC8D007AEB0F /* DomainFrontingCountryViewController.m in Sources */, 3496744D2076768700080B5F /* OWSMessageBubbleView.m in Sources */, 34B3F8751E8DF1700035BE1A /* CallViewController.swift in Sources */, + 4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */, 34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */, 45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */, 34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */, diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 79c3744e1..252ad9923 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -133,8 +133,7 @@ typedef enum : NSUInteger { UIDocumentMenuDelegate, UIDocumentPickerDelegate, UIImagePickerControllerDelegate, - OWSImagePickerControllerDelegate, - OWSPhotoCaptureViewControllerDelegate, + SendMediaNavDelegate, UINavigationControllerDelegate, UITextViewDelegate, ConversationCollectionViewDelegate, @@ -2837,24 +2836,6 @@ typedef enum : NSUInteger { [self showApprovalDialogForAttachment:attachment]; } -#pragma mark - OWSPhotoCaptureViewControllerDelegate - -- (void)photoCaptureViewController:(OWSPhotoCaptureViewController *)photoCaptureViewController - didFinishProcessingAttachment:(SignalAttachment *)attachment -{ - OWSLogDebug(@""); - [self dismissViewControllerAnimated:YES - completion:^{ - [self showApprovalDialogForAttachment:attachment]; - }]; -} - -- (void)photoCaptureViewControllerDidCancel:(OWSPhotoCaptureViewController *)photoCaptureViewController -{ - OWSLogDebug(@""); - [self dismissViewControllerAnimated:YES completion:nil]; -} - #pragma mark - UIImagePickerController /* @@ -2877,19 +2858,8 @@ typedef enum : NSUInteger { UIViewController *pickerModal; if (SSKFeatureFlags.useCustomPhotoCapture) { - OWSPhotoCaptureViewController *captureVC = [OWSPhotoCaptureViewController new]; - captureVC.delegate = self; - OWSNavigationController *navController = - [[OWSNavigationController alloc] initWithRootViewController:captureVC]; - UINavigationBar *navigationBar = navController.navigationBar; - if (![navigationBar isKindOfClass:[OWSNavigationBar class]]) { - OWSFailDebug(@"navigationBar was nil or unexpected class"); - } else { - OWSNavigationBar *owsNavigationBar = (OWSNavigationBar *)navigationBar; - [owsNavigationBar overrideThemeWithType:NavigationBarThemeOverrideClear]; - } - navController.ows_prefersStatusBarHidden = @(YES); - + SendMediaNavigationController *navController = [SendMediaNavigationController showingCameraFirst]; + navController.sendMediaNavDelegate = self; pickerModal = navController; } else { UIImagePickerController *picker = [OWSImagePickerController new]; @@ -2933,11 +2903,8 @@ typedef enum : NSUInteger { return; } - OWSImagePickerGridController *picker = [OWSImagePickerGridController new]; - picker.delegate = self; - - OWSNavigationController *pickerModal = [[OWSNavigationController alloc] initWithRootViewController:picker]; - pickerModal.ows_prefersStatusBarHidden = @(YES); + SendMediaNavigationController *pickerModal = [SendMediaNavigationController showingMediaLibraryFirst]; + pickerModal.sendMediaNavDelegate = self; [self dismissKeyBoard]; [self presentViewController:pickerModal animated:YES completion:nil]; @@ -2960,13 +2927,19 @@ typedef enum : NSUInteger { self.view.frame = frame; } -#pragma mark - OWSImagePickerControllerDelegate +#pragma mark - SendMediaNavDelegate -- (void)imagePicker:(OWSImagePickerGridController *)imagePicker - didPickImageAttachments:(NSArray *)attachments - messageText:(NSString *_Nullable)messageText +- (void)sendMediaNavDidCancel:(SendMediaNavigationController *)sendMediaNavigationController +{ + [self dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)sendMediaNav:(SendMediaNavigationController *)sendMediaNavigationController + didApproveAttachments:(NSArray *)attachments + messageText:(nullable NSString *)messageText { [self tryToSendAttachments:attachments messageText:messageText]; + [self dismissViewControllerAnimated:YES completion:nil]; } #pragma mark - UIImagePickerControllerDelegate @@ -3943,8 +3916,7 @@ typedef enum : NSUInteger { [self scrollToBottomAnimated:NO]; } -- (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval - didCancelAttachments:(NSArray *)attachment +- (void)attachmentApprovalDidCancel:(AttachmentApprovalViewController *)attachmentApproval { [self dismissViewControllerAnimated:YES completion:nil]; } diff --git a/Signal/src/ViewControllers/Photos/ImagePickerController.swift b/Signal/src/ViewControllers/Photos/ImagePickerController.swift index 54ac6aa07..82f93c855 100644 --- a/Signal/src/ViewControllers/Photos/ImagePickerController.swift +++ b/Signal/src/ViewControllers/Photos/ImagePickerController.swift @@ -6,16 +6,19 @@ import Foundation import Photos import PromiseKit -@objc(OWSImagePickerControllerDelegate) -protocol ImagePickerControllerDelegate { - func imagePicker(_ imagePicker: ImagePickerGridController, didPickImageAttachments attachments: [SignalAttachment], messageText: String?) +protocol ImagePickerGridControllerDelegate: AnyObject { + func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController) + + func imagePicker(_ imagePicker: ImagePickerGridController, isAssetSelected asset: PHAsset) -> Bool + func imagePicker(_ imagePicker: ImagePickerGridController, didSelectAsset asset: PHAsset, attachmentPromise: Promise) + func imagePicker(_ imagePicker: ImagePickerGridController, didDeselectAsset asset: PHAsset) + + func imagePickerCanSelectAdditionalItems(_ imagePicker: ImagePickerGridController) -> Bool } -@objc(OWSImagePickerGridController) -class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate, PhotoCollectionPickerDelegate, AttachmentApprovalViewControllerDelegate { +class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate, PhotoCollectionPickerDelegate { - @objc - weak var delegate: ImagePickerControllerDelegate? + weak var delegate: ImagePickerGridControllerDelegate? private let library: PhotoLibrary = PhotoLibrary() private var photoCollection: PhotoCollection @@ -25,12 +28,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat var collectionViewFlowLayout: UICollectionViewFlowLayout var titleView: TitleView! - // We use NSMutableOrderedSet so that we can honor selection order. - private let selectedIds = NSMutableOrderedSet() - - // This variable should only be accessed on the main thread. - private var assetIdToCommentMap = [String: String]() - init() { collectionViewFlowLayout = type(of: self).buildLayout() photoCollection = library.defaultPhotoCollection() @@ -79,10 +76,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat navigationItem.titleView = titleView self.titleView = titleView - let featureFlag_isMultiselectEnabled = true - if featureFlag_isMultiselectEnabled { - updateSelectButton() - } + updateSelectButton() collectionView.backgroundColor = .ows_gray95 @@ -109,6 +103,11 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } + guard let delegate = delegate else { + owsFailDebug("delegate was unexpectedly nil") + return + } + switch selectionPanGesture.state { case .possible: break @@ -121,7 +120,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } let asset = photoCollectionContents.asset(at: indexPath.item) - if selectedIds.contains(asset.localIdentifier) { + if delegate.imagePicker(self, isAssetSelected: asset) { selectionPanGestureMode = .deselect } else { selectionPanGestureMode = .select @@ -149,28 +148,30 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } + guard let delegate = delegate else { + owsFailDebug("delegate was unexpectedly nil") + return + } + let asset = photoCollectionContents.asset(at: indexPath.item) switch selectionPanGestureMode { case .select: - guard canSelectAdditionalItems else { + guard delegate.imagePickerCanSelectAdditionalItems(self) else { showTooManySelectedToast() return } - selectedIds.add(asset.localIdentifier) + let attachmentPromise: Promise = photoCollectionContents.outgoingAttachment(for: asset) + delegate.imagePicker(self, didSelectAsset: asset, attachmentPromise: attachmentPromise) collectionView.selectItem(at: indexPath, animated: true, scrollPosition: []) case .deselect: - selectedIds.remove(asset.localIdentifier) + delegate.imagePicker(self, didDeselectAsset: asset) collectionView.deselectItem(at: indexPath, animated: true) } updateDoneButton() } - var canSelectAdditionalItems: Bool { - return selectedIds.count <= SignalAttachment.maxAttachmentsAllowed - } - override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() updateLayout() @@ -263,14 +264,18 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } + guard let delegate = delegate else { + owsFailDebug("delegate was unexpectedly nil") + return + } + collectionView.reloadData() collectionView.layoutIfNeeded() let count = photoCollectionContents.assetCount for index in 0..] = assets.map({ - return self.photoCollectionContents.outgoingAttachment(for: $0) - }) - - firstly { - when(fulfilled: attachmentPromises) - }.map { attachments in - Logger.debug("built all attachments") - - DispatchQueue.main.async { - modal.dismiss(completion: { - self.didComplete(withAttachments: attachments) - }) - } - }.catch { error in - Logger.error("failed to prepare attachments. error: \(error)") - DispatchQueue.main.async { - modal.dismiss(completion: { - OWSAlerts.showAlert(title: NSLocalizedString("IMAGE_PICKER_FAILED_TO_PROCESS_ATTACHMENTS", comment: "alert title")) - }) - } - }.retainUntilComplete() + guard let delegate = delegate else { + owsFailDebug("delegate was unexpectedly nil") + return } - } - private func didComplete(withAttachments attachments: [SignalAttachment]) { - AssertIsOnMainThread() - - for attachment in attachments { - guard let assetId = attachment.assetId else { - owsFailDebug("Attachment is missing asset id.") - continue - } - // Link the attachment with its asset to ensure caption continuity. - attachment.assetId = assetId - // Restore any existing caption for this attachment. - attachment.captionText = assetIdToCommentMap[assetId] - } + hasPressedDoneSinceAppeared = true + updateDoneButton() - let vc = AttachmentApprovalViewController(mode: .sharedNavigation, attachments: attachments) - vc.approvalDelegate = self - navigationController?.pushViewController(vc, animated: true) + delegate.imagePickerDidCompleteSelection(self) } var hasPressedDoneSinceAppeared: Bool = false @@ -465,18 +406,13 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat self.doneButton.isEnabled = false } - func deselectAnySelected() { + func clearCollectionViewSelection() { guard let collectionView = self.collectionView else { owsFailDebug("collectionView was unexpectedly nil") return } - selectedIds.removeAllObjects() collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false)} - - if isInBatchSelectMode { - updateDoneButton() - } } func showTooManySelectedToast() { @@ -577,7 +513,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } // Any selections are invalid as they refer to indices in a different collection - deselectAnySelected() + clearCollectionViewSelection() photoCollection = collection photoCollectionContents = photoCollection.contents() @@ -605,25 +541,33 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let asset = photoCollectionContents.asset(at: indexPath.item) + guard let delegate = delegate else { + owsFailDebug("delegate was unexpectedly nil") + return + } + + let asset: PHAsset = photoCollectionContents.asset(at: indexPath.item) + let attachmentPromise: Promise = photoCollectionContents.outgoingAttachment(for: asset) + delegate.imagePicker(self, didSelectAsset: asset, attachmentPromise: attachmentPromise) if isInBatchSelectMode { - let assetId = asset.localIdentifier - selectedIds.add(assetId) updateDoneButton() } else { // Don't show "selected" badge unless we're in batch mode collectionView.deselectItem(at: indexPath, animated: false) - complete(withAssets: [asset]) + delegate.imagePickerDidCompleteSelection(self) } } public override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { Logger.debug("") + guard let delegate = delegate else { + owsFailDebug("delegate was unexpectedly nil") + return + } let asset = photoCollectionContents.asset(at: indexPath.item) - let assetId = asset.localIdentifier - selectedIds.remove(assetId) + delegate.imagePicker(self, didDeselectAsset: asset) if isInBatchSelectMode { updateDoneButton() @@ -635,68 +579,26 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let delegate = delegate else { + return UICollectionViewCell(forAutoLayout: ()) + } + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoGridViewCell.reuseIdentifier, for: indexPath) as? PhotoGridViewCell else { owsFail("cell was unexpectedly nil") } + cell.loadingColor = UIColor(white: 0.2, alpha: 1) let assetItem = photoCollectionContents.assetItem(at: indexPath.item, photoMediaSize: photoMediaSize) cell.configure(item: assetItem) - let assetId = assetItem.asset.localIdentifier - let isSelected = selectedIds.contains(assetId) - cell.isSelected = isSelected - - return cell - } - - // MARK: - AttachmentApprovalViewControllerDelegate - - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) { - self.dismiss(animated: true) { - self.delegate?.imagePicker(self, didPickImageAttachments: attachments, messageText: messageText) - } - } - - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didCancelAttachments attachments: [SignalAttachment]) { - navigationController?.popToViewController(self, animated: true) - } - - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, addMoreToAttachments attachments: [SignalAttachment]) { - // If we re-enter image picking via "add more" button, do so in batch mode. - isInBatchSelectMode = true - - // clear selection - deselectAnySelected() - - // removing-and-readding accomplishes two things - // 1. respect items removed from the rail while in the approval view - // 2. in the case of the user adding more to what was a single item - // which was not selected in batch mode, ensure that item is now - // part of the "batch selection" - for previouslySelected in attachments { - guard let assetId = previouslySelected.assetId else { - owsFailDebug("assetId was unexpectedly nil") - continue - } - - selectedIds.add(assetId as Any) + let isSelected = delegate.imagePicker(self, isAssetSelected: assetItem.asset) + if isSelected { + cell.isSelected = isSelected + } else { + cell.isSelected = isSelected } - navigationController?.popToViewController(self, animated: true) - } - - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, changedCaptionOfAttachment attachment: SignalAttachment) { - AssertIsOnMainThread() - - guard let assetId = attachment.assetId else { - owsFailDebug("Attachment missing source id.") - return - } - guard let captionText = attachment.captionText, captionText.count > 0 else { - assetIdToCommentMap.removeValue(forKey: assetId) - return - } - assetIdToCommentMap[assetId] = captionText + return cell } } diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 20271cb02..f12129566 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -148,6 +148,7 @@ class PhotoCaptureViewController: OWSViewController { button.setImage(imageName: imageName) } } + private lazy var dismissControl: PhotoControl = { return PhotoControl(imageName: "ic_x_with_shadow") { [weak self] in self?.didTapClose() diff --git a/Signal/src/ViewControllers/Photos/PhotoLibrary.swift b/Signal/src/ViewControllers/Photos/PhotoLibrary.swift index 077934954..76a59d6cb 100644 --- a/Signal/src/ViewControllers/Photos/PhotoLibrary.swift +++ b/Signal/src/ViewControllers/Photos/PhotoLibrary.swift @@ -79,7 +79,6 @@ class PhotoCollectionContents { enum PhotoLibraryError: Error { case assertionError(description: String) case unsupportedMediaType - } init(fetchResult: PHFetchResult, localizedTitle: String?) { @@ -207,15 +206,11 @@ class PhotoCollectionContents { switch asset.mediaType { case .image: return requestImageDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in - let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .medium) - attachment.assetId = asset.localIdentifier - return attachment + return SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .medium) } case .video: return requestVideoDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in - let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI) - attachment.assetId = asset.localIdentifier - return attachment + return SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI) } default: return Promise(error: PhotoLibraryError.unsupportedMediaType) diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift new file mode 100644 index 000000000..11eac7a05 --- /dev/null +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -0,0 +1,295 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation +import Photos +import PromiseKit + +@objc +protocol SendMediaNavDelegate: AnyObject { + func sendMediaNavDidCancel(_ sendMediaNavigationController: SendMediaNavigationController) + func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) +} + +@objc +class SendMediaNavigationController: OWSNavigationController { + + // MARK: - Overrides + + override var prefersStatusBarHidden: Bool { return true } + + // MARK: - + + @objc + public weak var sendMediaNavDelegate: SendMediaNavDelegate? + + @objc + public class func showingCameraFirst() -> SendMediaNavigationController { + let navController = SendMediaNavigationController() + + if let owsNavBar = navController.navigationBar as? OWSNavigationBar { + owsNavBar.overrideTheme(type: .clear) + } else { + owsFailDebug("unexpected navbar: \(navController.navigationBar)") + } + navController.setViewControllers([navController.captureViewController], animated: false) + + return navController + } + + @objc + public class func showingMediaLibraryFirst() -> SendMediaNavigationController { + let navController = SendMediaNavigationController() + + if let owsNavBar = navController.navigationBar as? OWSNavigationBar { + owsNavBar.overrideTheme(type: .clear) + } else { + owsFailDebug("unexpected navbar: \(navController.navigationBar)") + } + navController.setViewControllers([navController.mediaLibraryViewController], animated: false) + + return navController + } + + // MARK: + + private var attachmentDraftCollection: AttachmentDraftCollection = .empty + + private var attachments: [SignalAttachment] { + return attachmentDraftCollection.attachmentDrafts.map { $0.attachment } + } + + private let mediaLibrarySelections: OrderedDictionary = OrderedDictionary() + + // MARK: Child VC's + + private lazy var captureViewController: PhotoCaptureViewController = { + let vc = PhotoCaptureViewController() + vc.delegate = self + + return vc + }() + + private lazy var mediaLibraryViewController: ImagePickerGridController = { + let vc = ImagePickerGridController() + vc.delegate = self + + return vc + }() + + private func pushApprovalViewController() { + let approvalViewController = AttachmentApprovalViewController(mode: .sharedNavigation, attachments: self.attachments) + approvalViewController.approvalDelegate = self + + pushViewController(approvalViewController, animated: true) + } +} + +extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate { + func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didFinishProcessingAttachment attachment: SignalAttachment) { + attachmentDraftCollection.append(.camera(attachment: attachment)) + + pushApprovalViewController() + } + + func photoCaptureViewControllerDidCancel(_ photoCaptureViewController: PhotoCaptureViewController) { + // TODO + // sometimes we might want this to be a "back" to the approval view + // other times we might want this to be a "close" and take me back to the CVC + // seems like we should show the "back" and have a seprate "didTapBack" delegate method or something... + + self.sendMediaNavDelegate?.sendMediaNavDidCancel(self) + } +} + +extension SendMediaNavigationController: ImagePickerGridControllerDelegate { + + func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController) { + let mediaLibrarySelections: [MediaLibrarySelection] = self.mediaLibrarySelections.orderedValues + + let backgroundBlock: (ModalActivityIndicatorViewController) -> Void = { modal in + let attachmentPromises: [Promise] = mediaLibrarySelections.map { $0.promise } + + when(fulfilled: attachmentPromises).map { attachments in + Logger.debug("built all attachments") + modal.dismiss { + self.attachmentDraftCollection.selectedFromPicker(attachments: attachments) + self.pushApprovalViewController() + } + }.catch { error in + Logger.error("failed to prepare attachments. error: \(error)") + modal.dismiss { + OWSAlerts.showAlert(title: NSLocalizedString("IMAGE_PICKER_FAILED_TO_PROCESS_ATTACHMENTS", comment: "alert title")) + } + }.retainUntilComplete() + } + + ModalActivityIndicatorViewController.present(fromViewController: self, + canCancel: false, + backgroundBlock: backgroundBlock) + } + + func imagePicker(_ imagePicker: ImagePickerGridController, isAssetSelected asset: PHAsset) -> Bool { + return mediaLibrarySelections.hasValue(forKey: asset) + } + + func imagePicker(_ imagePicker: ImagePickerGridController, didSelectAsset asset: PHAsset, attachmentPromise: Promise) { + guard !mediaLibrarySelections.hasValue(forKey: asset) else { + return + } + + let libraryMedia = MediaLibrarySelection(asset: asset, signalAttachmentPromise: attachmentPromise) + mediaLibrarySelections.append(key: asset, value: libraryMedia) + } + + func imagePicker(_ imagePicker: ImagePickerGridController, didDeselectAsset asset: PHAsset) { + if mediaLibrarySelections.hasValue(forKey: asset) { + mediaLibrarySelections.remove(key: asset) + } + } + + func imagePickerCanSelectAdditionalItems(_ imagePicker: ImagePickerGridController) -> Bool { + return attachmentDraftCollection.count <= SignalAttachment.maxAttachmentsAllowed + } +} + +extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegate { + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) { + guard let removedDraft = attachmentDraftCollection.attachmentDrafts.first(where: { $0.attachment == attachment}) else { + owsFailDebug("removedDraft was unexpectedly nil") + return + } + + switch removedDraft.source { + case .picker(attachment: let pickerAttachment): + mediaLibrarySelections.remove(key: pickerAttachment.asset) + case .camera(attachment: _): + break + } + + attachmentDraftCollection.remove(attachment: attachment) + } + + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) { + sendMediaNavDelegate?.sendMediaNav(self, didApproveAttachments: attachments, messageText: messageText) + } + + func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) { + sendMediaNavDelegate?.sendMediaNavDidCancel(self) + } + + func attachmentApprovalDidTapAddMore(_ attachmentApproval: AttachmentApprovalViewController) { + // Current design dicates we'll go "back" to the single thing before us. + assert(viewControllers.count == 2) + + // regardless of which VC we're going "back" to, we're in "batch" mode at this point. + mediaLibraryViewController.isInBatchSelectMode = true + mediaLibraryViewController.collectionView?.reloadData() + + popViewController(animated: true) + } +} + +enum AttachmentDraft { + case camera(attachment: SignalAttachment) + case picker(attachment: MediaLibraryAttachment) +} + +extension AttachmentDraft { + var attachment: SignalAttachment { + switch self { + case .camera(let cameraAttachment): + return cameraAttachment + case .picker(let pickerAttachment): + return pickerAttachment.signalAttachment + } + } + + var source: AttachmentDraft { + return self + } +} + +struct AttachmentDraftCollection { + private(set) var attachmentDrafts: [AttachmentDraft] + + static var empty: AttachmentDraftCollection { + return AttachmentDraftCollection(attachmentDrafts: []) + } + + // MARK - + + var count: Int { + return attachmentDrafts.count + } + + var pickerAttachments: [MediaLibraryAttachment] { + return attachmentDrafts.compactMap { attachmentDraft in + switch attachmentDraft.source { + case .picker(let pickerAttachment): + return pickerAttachment + case .camera: + return nil + } + } + } + + mutating func append(_ element: AttachmentDraft) { + attachmentDrafts.append(element) + } + + mutating func remove(attachment: SignalAttachment) { + attachmentDrafts = attachmentDrafts.filter { $0.attachment != attachment } + } + + mutating func selectedFromPicker(attachments: [MediaLibraryAttachment]) { + let pickedAttachments: Set = Set(attachments) + let oldPickerAttachments: Set = Set(self.pickerAttachments) + + for removedAttachment in oldPickerAttachments.subtracting(pickedAttachments) { + remove(attachment: removedAttachment.signalAttachment) + } + + // enumerate over new attachments to maintain order from picker + for attachment in attachments { + guard !oldPickerAttachments.contains(attachment) else { + continue + } + append(.picker(attachment: attachment)) + } + } +} + +struct MediaLibrarySelection: Hashable, Equatable { + let asset: PHAsset + let signalAttachmentPromise: Promise + + var hashValue: Int { + return asset.hashValue + } + + var promise: Promise { + let asset = self.asset + return signalAttachmentPromise.map { signalAttachment in + return MediaLibraryAttachment(asset: asset, signalAttachment: signalAttachment) + } + } + + static func ==(lhs: MediaLibrarySelection, rhs: MediaLibrarySelection) -> Bool { + return lhs.asset == rhs.asset + } +} + +struct MediaLibraryAttachment: Hashable, Equatable { + let asset: PHAsset + let signalAttachment: SignalAttachment + + public var hashValue: Int { + return asset.hashValue + } + + public static func == (lhs: MediaLibraryAttachment, rhs: MediaLibraryAttachment) -> Bool { + return lhs.asset == rhs.asset + } +} diff --git a/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentApprovalViewController.swift b/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentApprovalViewController.swift index 6c674f1b7..7017c122e 100644 --- a/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentApprovalViewController.swift +++ b/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentApprovalViewController.swift @@ -10,9 +10,16 @@ import PromiseKit @objc public protocol AttachmentApprovalViewControllerDelegate: class { func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didCancelAttachments attachments: [SignalAttachment]) - @objc optional func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, addMoreToAttachments attachments: [SignalAttachment]) - @objc optional func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, changedCaptionOfAttachment attachment: SignalAttachment) + func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) + + @objc + optional func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) + + @objc + optional func attachmentApprovalDidTapAddMore(_ attachmentApproval: AttachmentApprovalViewController) + + @objc + optional func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, changedCaptionOfAttachment attachment: SignalAttachment) } // MARK: - @@ -363,6 +370,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC }, completion: { _ in self.attachmentItemCollection.remove(item: attachmentItem) + self.approvalDelegate?.attachmentApproval?(self, didRemoveAttachment: attachmentItem.attachment) self.updateMediaRail() }) } @@ -629,7 +637,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } private func cancelPressed() { - self.approvalDelegate?.attachmentApproval(self, didCancelAttachments: attachments) + self.approvalDelegate?.attachmentApprovalDidCancel(self) } @objc func didTapCaption(sender: UIButton) { @@ -668,7 +676,7 @@ extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate { } func attachmentTextToolbarDidAddMore(_ attachmentTextToolbar: AttachmentTextToolbar) { - self.approvalDelegate?.attachmentApproval?(self, addMoreToAttachments: attachments) + self.approvalDelegate?.attachmentApprovalDidTapAddMore?(self) } } diff --git a/SignalMessaging/ViewControllers/SharingThreadPickerViewController.m b/SignalMessaging/ViewControllers/SharingThreadPickerViewController.m index b704057ff..0097efa37 100644 --- a/SignalMessaging/ViewControllers/SharingThreadPickerViewController.m +++ b/SignalMessaging/ViewControllers/SharingThreadPickerViewController.m @@ -305,8 +305,7 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion); fromViewController:attachmentApproval]; } -- (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval - didCancelAttachments:(NSArray *)attachment +- (void)attachmentApprovalDidCancel:(AttachmentApprovalViewController *)attachmentApproval { [self cancelShareExperience]; } diff --git a/SignalMessaging/Views/ImageEditor/OrderedDictionary.swift b/SignalMessaging/Views/ImageEditor/OrderedDictionary.swift index 7aa6d7d28..5cdd820c1 100644 --- a/SignalMessaging/Views/ImageEditor/OrderedDictionary.swift +++ b/SignalMessaging/Views/ImageEditor/OrderedDictionary.swift @@ -30,6 +30,10 @@ public class OrderedDictionary { return keyValueMap[key] } + public func hasValue(forKey key: KeyType) -> Bool { + return keyValueMap[key] != nil + } + public func append(key: KeyType, value: ValueType) { if keyValueMap[key] != nil { owsFailDebug("Unexpected duplicate key in key map: \(key)") diff --git a/SignalMessaging/attachments/SignalAttachment.swift b/SignalMessaging/attachments/SignalAttachment.swift index f872f3b08..4a78de696 100644 --- a/SignalMessaging/attachments/SignalAttachment.swift +++ b/SignalMessaging/attachments/SignalAttachment.swift @@ -160,10 +160,6 @@ public class SignalAttachment: NSObject { @objc public let dataUTI: String - // Can be used by views to link this SignalAttachment with an Photos framework asset. - @objc - public var assetId: String? - var error: SignalAttachmentError? { didSet { AssertIsOnMainThread() diff --git a/SignalServiceKit/src/Util/FeatureFlags.swift b/SignalServiceKit/src/Util/FeatureFlags.swift index 4d58938c0..afe797338 100644 --- a/SignalServiceKit/src/Util/FeatureFlags.swift +++ b/SignalServiceKit/src/Util/FeatureFlags.swift @@ -22,6 +22,6 @@ public class FeatureFlags: NSObject { @objc public static var useCustomPhotoCapture: Bool { - return false + return true } }