From 1357449dcc9ea64da051d3ed53b100b996fa6bea Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 27 Mar 2019 16:11:28 -0600 Subject: [PATCH 1/5] Make OrderedDictionary keys generic --- .../Views/ImageEditor/ImageEditorContents.swift | 4 ++-- .../Views/ImageEditor/OrderedDictionary.swift | 15 ++++++--------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/SignalMessaging/Views/ImageEditor/ImageEditorContents.swift b/SignalMessaging/Views/ImageEditor/ImageEditorContents.swift index 8725ba433..e5ce28f6d 100644 --- a/SignalMessaging/Views/ImageEditor/ImageEditorContents.swift +++ b/SignalMessaging/Views/ImageEditor/ImageEditorContents.swift @@ -11,7 +11,7 @@ import UIKit // as immutable, once configured. public class ImageEditorContents: NSObject { - public typealias ItemMapType = OrderedDictionary + public typealias ItemMapType = OrderedDictionary // This represents the current state of each item, // a mapping of [itemId : item]. @@ -72,7 +72,7 @@ public class ImageEditorContents: NSObject { @objc public func items() -> [ImageEditorItem] { - return itemMap.orderedValues() + return itemMap.orderedValues } @objc diff --git a/SignalMessaging/Views/ImageEditor/OrderedDictionary.swift b/SignalMessaging/Views/ImageEditor/OrderedDictionary.swift index adec0c9fd..7aa6d7d28 100644 --- a/SignalMessaging/Views/ImageEditor/OrderedDictionary.swift +++ b/SignalMessaging/Views/ImageEditor/OrderedDictionary.swift @@ -4,16 +4,13 @@ import Foundation -public class OrderedDictionary: NSObject { +public class OrderedDictionary { - public typealias KeyType = String + private var keyValueMap = [KeyType: ValueType]() - var keyValueMap = [KeyType: ValueType]() + public var orderedKeys = [KeyType]() - var orderedKeys = [KeyType]() - - public override init() { - } + public init() { } // Used to clone copies of instances of this class. public init(keyValueMap: [KeyType: ValueType], @@ -25,7 +22,7 @@ public class OrderedDictionary: NSObject { // Since the contents are immutable, we only modify copies // made with this method. - public func clone() -> OrderedDictionary { + public func clone() -> OrderedDictionary { return OrderedDictionary(keyValueMap: keyValueMap, orderedKeys: orderedKeys) } @@ -90,7 +87,7 @@ public class OrderedDictionary: NSObject { return orderedKeys.count } - public func orderedValues() -> [ValueType] { + public var orderedValues: [ValueType] { var values = [ValueType]() for key in orderedKeys { guard let value = self.keyValueMap[key] else { From 6502d7d4a5983e49ca8b1eda334e64cbfdd6e18d Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Tue, 26 Mar 2019 19:05:09 -0600 Subject: [PATCH 2/5] remove `isMultiSendEnabled` feature flag --- .../ConversationViewController.m | 26 +++++-------------- .../attachments/SignalAttachment.swift | 7 +---- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 0002ac339..79c3744e1 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -2933,22 +2933,11 @@ typedef enum : NSUInteger { return; } - UIViewController *pickerModal; - if (SignalAttachment.isMultiSendEnabled) { - OWSImagePickerGridController *picker = [OWSImagePickerGridController new]; - picker.delegate = self; - - OWSNavigationController *modal = [[OWSNavigationController alloc] initWithRootViewController:picker]; - modal.ows_prefersStatusBarHidden = @(YES); - pickerModal = modal; - } else { - UIImagePickerController *picker = [OWSImagePickerController new]; - picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; - picker.delegate = self; - picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ]; + OWSImagePickerGridController *picker = [OWSImagePickerGridController new]; + picker.delegate = self; - pickerModal = picker; - } + OWSNavigationController *pickerModal = [[OWSNavigationController alloc] initWithRootViewController:picker]; + pickerModal.ows_prefersStatusBarHidden = @(YES); [self dismissKeyBoard]; [self presentViewController:pickerModal animated:YES completion:nil]; @@ -3060,11 +3049,8 @@ typedef enum : NSUInteger { }]; } else { // Non-Video image picked from library - if (SignalAttachment.isMultiSendEnabled) { - OWSFailDebug(@"Only use UIImagePicker for camera/video capture. Picking media from UIImagePicker is not " - @"supported. "); - } - + OWSFailDebug( + @"Only use UIImagePicker for camera/video capture. Picking media from UIImagePicker is not supported. "); // To avoid re-encoding GIF and PNG's as JPEG we have to get the raw data of // the selected item vs. using the UIImagePickerControllerOriginalImage diff --git a/SignalMessaging/attachments/SignalAttachment.swift b/SignalMessaging/attachments/SignalAttachment.swift index 48a711516..f872f3b08 100644 --- a/SignalMessaging/attachments/SignalAttachment.swift +++ b/SignalMessaging/attachments/SignalAttachment.swift @@ -193,12 +193,7 @@ public class SignalAttachment: NSObject { // MARK: @objc - public static let isMultiSendEnabled = true - - @objc - public static var maxAttachmentsAllowed: Int { - return isMultiSendEnabled ? 32 : 1 - } + public static let maxAttachmentsAllowed: Int = 32 // MARK: Constructor From 7dbb9517af973364f3edf8139e92aa0230fa8e5b Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Tue, 26 Mar 2019 18:18:13 -0600 Subject: [PATCH 3/5] 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 } } From 1a4062dd89a97dd4c5cdb8e06a9bd9406abc91a4 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 28 Mar 2019 06:37:23 -0600 Subject: [PATCH 4/5] Nav buttons: batch, camera/library switch, done --- .../Contents.json | 23 ++ .../create-album-outline-32@1x.png | Bin 0 -> 477 bytes .../create-album-outline-32@2x.png | Bin 0 -> 813 bytes .../create-album-outline-32@3x.png | Bin 0 -> 1208 bytes .../Photos/ImagePickerController.swift | 115 +----- .../SendMediaNavigationController.swift | 335 ++++++++++++++++-- SignalMessaging/categories/UIFont+OWS.h | 1 + SignalMessaging/categories/UIFont+OWS.m | 6 + SignalMessaging/categories/UIView+OWS.h | 1 + SignalMessaging/categories/UIView+OWS.m | 9 +- 10 files changed, 360 insertions(+), 130 deletions(-) create mode 100644 Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/Contents.json create mode 100644 Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@1x.png create mode 100644 Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@2x.png create mode 100644 Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@3x.png diff --git a/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/Contents.json b/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/Contents.json new file mode 100644 index 000000000..ae32da7a5 --- /dev/null +++ b/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "create-album-outline-32@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "create-album-outline-32@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "create-album-outline-32@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@1x.png b/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..d23c007d13f7c74ecb487527c2e2ffa568ca4936 GIT binary patch literal 477 zcmV<30V4j1P)ff2ev!v^UF zjF3*Co;W#YED;<+6MB7jCUN}r{3OeQm>F~8Vb;~}fdIq=X#ubSaFFH^r3J79Pqy1jxp66hFrd_W1)v^j zie7V^n1G0+vS;Rsh-C54NJd}6Jgfjby5>j}ke^!L!_Hql=?0pkPz8)R`Uo6-!JBXd z5!rbgf4dY1#>noWQB!pI`;kzbRh(Cyc6tJ1(Geh`a#jM&d@!vOU@>$F$JVRxB%+fB zX0FWyDK5l;+BwT!sCA1oHUzgSd>gC)fM-X6cK~mC&T}^1DKnTrDr+QrlO=hAy9`8y z`7$AaZ^>kcXl;5 zT?XUja#s*=Ga{3g2M|%Ak!z{W*%xHw9_hQx@^V6lb!-kd+}<=Ro&*2<&nbW(iTSP3 Tbda`p00000NkvXXu0mjfzh=!U literal 0 HcmV?d00001 diff --git a/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@2x.png b/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9ccabb7c69fce53efb9b3b2cb0d4a6d58bbf66a5 GIT binary patch literal 813 zcmV+|1JeA7P)h5!$HpY63(cTNF-&KQ-utRcXi9CHn| zTZlozkdpxa>6=fUhvjb$hn*3lbIR^g5|lzXwiSktMtR4MmpCyduMSEf96X2yH%9qr zbbNQKLu?}?XW6sa9ueJ>|L)a)XVOOQeCgHU$=QvF@zud%g`coFxo~H)%r(BU+PH_+ zK`9al@Z-J+=RMn#SBEJtL%q0uC!&L@u`r0ptzaL*ir0`u*S-xF=e)pHT(49qV+sM! z;yOIJEDt$xKZmTjb_C2l+h2kt+p0ehX#^W74lQ r?axuAsSJ$(rKt>!fC6k-4gl~E>TFL#pFtBG00000NkvXXu0mjfl8j{` literal 0 HcmV?d00001 diff --git a/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@3x.png b/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..b13aa24b3e1399da20c58955939905ac51468983 GIT binary patch literal 1208 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9FJ<{->y95KI&fq^B< z)5S5QV$R#S7YlD2@U%6?-Ji@miT4%nth@=luY3ghC*(}Z7W zfB6YIOjOczT=wzq!`4Z;LH3+NTUZnY8y6lI-~DaZ42DyfsAlcY&m9%a4DPQ#&7HgE z{OW*aU4}OuFU+ltMD>L93>kK?G_W(=*u6gc2;V;6=}WJxr!5u#zilJ;ee>(vq*uOu z#l&!6a=^o|T;14ascBm!+dK3bK1`P7`24K>YfwSn=i4WC-4VRVt*yJX=)C=}b5pzH?AqKr`Wq23F^e1`QmI4|)XdLG>&*!A2tOd;~XU!Gr1_irz?T2Ef){YTGDl4@l6hn zIpR^P4aoL*CE9#;4=Y(Ftye+pfUp@MM zOM2F8HgOG8mK$8pJ#LGHHcxBqQs`}IE?Jji`hQZ)#!39sub8-O`REuuom;-bZKY;| z$iuuy&)FV<8?PuzPX4Fq?@}FhLoLXCvX#o=cU>zSG}OEfvIHChqh-q&{{H=AUgmcD z%k9};{vVs7oZu;)T|D9Hy|fqsr98ojR%*Ju;_YV{(c|BR^@IOj3E_!GOq)A zg+;pV+@3-QmaA637me3_=~=P;v!~0Ajguu7CO>0%nfamG#pcPfc#lk{4^_`dSq@f z3NFuWxf1()!X}+5nns$-&9pVv^n^@Qe|e{kCBo;NMf;R3Tcp>Rw&q{odqGVzW9r0^ znF$t?)?Ycf|AFGA&zh<$e=dJNk&9bfEj8-Bn(1Ahr*q?exUQdPSw6`;>-R!Sv-*;? zcP>`j3weKE^sMq?{}QjI_k>Pq-CmWN>YvUkykqgghqsz!)#~dV7ILiJ=eqdRYd-av z%uX)%l#8e7|CtlY(44$|sYWsrCdy!9_|MQZLF) func imagePicker(_ imagePicker: ImagePickerGridController, didDeselectAsset asset: PHAsset) + var isInBatchSelectMode: Bool { get } func imagePickerCanSelectAdditionalItems(_ imagePicker: ImagePickerGridController) -> Bool } @@ -76,8 +77,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat navigationItem.titleView = titleView self.titleView = titleView - updateSelectButton() - collectionView.backgroundColor = .ows_gray95 let selectionPanGesture = DirectionalPanGestureRecognizer(direction: [.horizontal], target: self, action: #selector(didPanSelection)) @@ -94,10 +93,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat @objc func didPanSelection(_ selectionPanGesture: UIPanGestureRecognizer) { - guard isInBatchSelectMode else { - return - } - guard let collectionView = collectionView else { owsFailDebug("collectionView was unexpectedly nil") return @@ -108,6 +103,10 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } + guard delegate.isInBatchSelectMode else { + return + } + switch selectionPanGesture.state { case .possible: break @@ -138,11 +137,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } func tryToToggleBatchSelect(at indexPath: IndexPath) { - guard isInBatchSelectMode else { - owsFailDebug("isInBatchSelectMode was unexpectedly false") - return - } - guard let collectionView = collectionView else { owsFailDebug("collectionView was unexpectedly nil") return @@ -153,6 +147,11 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } + guard delegate.isInBatchSelectMode else { + owsFailDebug("isInBatchSelectMode was unexpectedly false") + return + } + let asset = photoCollectionContents.asset(at: indexPath.item) switch selectionPanGestureMode { case .select: @@ -168,8 +167,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat delegate.imagePicker(self, didDeselectAsset: asset) collectionView.deselectItem(at: indexPath, animated: true) } - - updateDoneButton() } override func viewWillLayoutSubviews() { @@ -181,12 +178,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - if let navBar = self.navigationController?.navigationBar as? OWSNavigationBar { - navBar.overrideTheme(type: .alwaysDark) - } else { - owsFailDebug("Invalid nav bar.") - } - // Determine the size of the thumbnails to request let scale = UIScreen.main.scale let cellSize = collectionViewFlowLayout.itemSize @@ -217,10 +208,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat super.viewDidAppear(animated) hasEverAppeared = true - // done button may have been disable from the last time we hit "Done" - // make sure to re-enable it if appropriate upon returning to the view - hasPressedDoneSinceAppeared = false - updateDoneButton() // Since we're presenting *over* the ConversationVC, we need to `becomeFirstResponder`. // @@ -332,78 +319,18 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat // MARK: - Batch Selection - lazy var doneButton: UIBarButtonItem = { - return UIBarButtonItem(barButtonSystemItem: .done, - target: self, - action: #selector(didPressDone)) - }() - - lazy var selectButton: UIBarButtonItem = { - return UIBarButtonItem(title: NSLocalizedString("BUTTON_SELECT", comment: "Button text to enable batch selection mode"), - style: .plain, - target: self, - action: #selector(didTapSelect)) - }() - - var isInBatchSelectMode = false { - didSet { - collectionView!.allowsMultipleSelection = isInBatchSelectMode - updateSelectButton() - updateDoneButton() - } - } - - @objc - func didPressDone(_ sender: Any) { - Logger.debug("") - + func batchSelectModeDidChange() { guard let delegate = delegate else { - owsFailDebug("delegate was unexpectedly nil") return } - hasPressedDoneSinceAppeared = true - updateDoneButton() - - delegate.imagePickerDidCompleteSelection(self) - } - - var hasPressedDoneSinceAppeared: Bool = false - func updateDoneButton() { - guard let collectionView = self.collectionView else { + guard let collectionView = collectionView else { owsFailDebug("collectionView was unexpectedly nil") return } - guard !hasPressedDoneSinceAppeared else { - doneButton.isEnabled = false - return - } - - if let count = collectionView.indexPathsForSelectedItems?.count, count > 0 { - doneButton.isEnabled = true - } else { - doneButton.isEnabled = false - } - } - - func updateSelectButton() { - guard !isShowingCollectionPickerController else { - navigationItem.rightBarButtonItem = nil - return - } - - let button = isInBatchSelectMode ? doneButton : selectButton - button.tintColor = .ows_gray05 - navigationItem.rightBarButtonItem = button - } - - @objc - func didTapSelect(_ sender: Any) { - isInBatchSelectMode = true - - // disabled until at least one item is selected - self.doneButton.isEnabled = false + collectionView.allowsMultipleSelection = delegate.isInBatchSelectMode + collectionView.reloadData() } func clearCollectionViewSelection() { @@ -477,9 +404,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat UIView.animate(.promise, duration: 0.25, delay: 0, options: .curveEaseInOut) { collectionPickerView.superview?.layoutIfNeeded() - - self.updateSelectButton() - self.titleView.rotateIcon(.up) }.retainUntilComplete() } @@ -494,9 +418,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat UIView.animate(.promise, duration: 0.25, delay: 0, options: .curveEaseInOut) { collectionPickerController.view.frame = self.view.frame.offsetBy(dx: 0, dy: self.view.frame.height) - - self.updateSelectButton() - self.titleView.rotateIcon(.down) }.done { _ in collectionPickerController.view.removeFromSuperview() @@ -550,9 +471,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat let attachmentPromise: Promise = photoCollectionContents.outgoingAttachment(for: asset) delegate.imagePicker(self, didSelectAsset: asset, attachmentPromise: attachmentPromise) - if isInBatchSelectMode { - updateDoneButton() - } else { + if !delegate.isInBatchSelectMode { // Don't show "selected" badge unless we're in batch mode collectionView.deselectItem(at: indexPath, animated: false) delegate.imagePickerDidCompleteSelection(self) @@ -568,10 +487,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat let asset = photoCollectionContents.asset(at: indexPath.item) delegate.imagePicker(self, didDeselectAsset: asset) - - if isInBatchSelectMode { - updateDoneButton() - } } override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift index 11eac7a05..e2e7a41b7 100644 --- a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -19,6 +19,32 @@ class SendMediaNavigationController: OWSNavigationController { override var prefersStatusBarHidden: Bool { return true } + override func viewDidLoad() { + super.viewDidLoad() + + self.delegate = self + + view.addSubview(batchModeButton) + batchModeButton.setCompressionResistanceHigh() + batchModeButton.autoPinEdge(toSuperviewMargin: .bottom) + batchModeButton.autoPinEdge(toSuperviewMargin: .trailing) + + view.addSubview(doneButton) + doneButton.setCompressionResistanceHigh() + doneButton.autoPinEdge(toSuperviewMargin: .bottom) + doneButton.autoPinEdge(toSuperviewMargin: .trailing) + + view.addSubview(cameraModeButton) + cameraModeButton.setCompressionResistanceHigh() + cameraModeButton.autoPinEdge(toSuperviewMargin: .bottom) + cameraModeButton.autoPinEdge(toSuperviewMargin: .leading) + + view.addSubview(mediaLibraryModeButton) + mediaLibraryModeButton.setCompressionResistanceHigh() + mediaLibraryModeButton.autoPinEdge(toSuperviewMargin: .bottom) + mediaLibraryModeButton.autoPinEdge(toSuperviewMargin: .leading) + } + // MARK: - @objc @@ -27,13 +53,8 @@ class SendMediaNavigationController: OWSNavigationController { @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) + navController.updateButtons() return navController } @@ -41,18 +62,126 @@ class SendMediaNavigationController: OWSNavigationController { @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) + navController.updateButtons() return navController } - // MARK: + var isInBatchSelectMode = false { + didSet { + if oldValue != isInBatchSelectMode { + updateButtons() + mediaLibraryViewController.batchSelectModeDidChange() + } + } + } + + func updateButtons() { + guard let topViewController = viewControllers.last else { + return + } + + switch topViewController { + case is AttachmentApprovalViewController: + batchModeButton.isHidden = true + doneButton.isHidden = true + cameraModeButton.isHidden = true + mediaLibraryModeButton.isHidden = true + case is ImagePickerGridController: + batchModeButton.isHidden = isInBatchSelectMode + doneButton.isHidden = !isInBatchSelectMode || (attachmentDraftCollection.count == 0 && mediaLibrarySelections.count == 0) + cameraModeButton.isHidden = false + mediaLibraryModeButton.isHidden = true + case is PhotoCaptureViewController: + batchModeButton.isHidden = isInBatchSelectMode + doneButton.isHidden = !isInBatchSelectMode || (attachmentDraftCollection.count == 0 && mediaLibrarySelections.count == 0) + cameraModeButton.isHidden = true + mediaLibraryModeButton.isHidden = false + default: + owsFailDebug("unexpected topViewController: \(topViewController)") + } + + doneButton.updateCount() + } + + func fadeTo(viewControllers: [UIViewController]) { + let transition: CATransition = CATransition() + transition.duration = 0.1 + transition.type = kCATransitionFade + view.layer.add(transition, forKey: nil) + setViewControllers(viewControllers, animated: false) + } + + // MARK: - Events + + private func didTapBatchModeButton() { + // There's no way to _disable_ batch mode. + isInBatchSelectMode = true + } + + private func didTapCameraModeButton() { + fadeTo(viewControllers: [captureViewController]) + updateButtons() + } + + private func didTapMediaLibraryModeButton() { + fadeTo(viewControllers: [mediaLibraryViewController]) + updateButtons() + } + + // MARK: Views + + private lazy var doneButton: DoneButton = { + let button = DoneButton() + button.delegate = self + + return button + }() + + private lazy var batchModeButton: UIButton = { + let button = OWSButton(imageName: "media_send_batch_mode_disabled", + tintColor: .ows_gray60, + block: { [weak self] in self?.didTapBatchModeButton() }) + + let width: CGFloat = 44 + button.autoSetDimensions(to: CGSize(width: width, height: width)) + button.layer.cornerRadius = width / 2 + button.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + button.backgroundColor = .ows_white + + return button + }() + + private lazy var cameraModeButton: UIButton = { + let button = OWSButton(imageName: "settings-avatar-camera-2", + tintColor: .ows_gray60, + block: { [weak self] in self?.didTapCameraModeButton() }) + + let width: CGFloat = 44 + button.autoSetDimensions(to: CGSize(width: width, height: width)) + button.layer.cornerRadius = width / 2 + button.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + button.backgroundColor = .ows_white + + return button + }() + + private lazy var mediaLibraryModeButton: UIButton = { + let button = OWSButton(imageName: "actionsheet_camera_roll_black", + tintColor: .ows_gray60, + block: { [weak self] in self?.didTapMediaLibraryModeButton() }) + + let width: CGFloat = 44 + button.autoSetDimensions(to: CGSize(width: width, height: width)) + button.layer.cornerRadius = width / 2 + button.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + button.backgroundColor = .ows_white + + return button + }() + + // MARK: State private var attachmentDraftCollection: AttachmentDraftCollection = .empty @@ -83,22 +212,60 @@ class SendMediaNavigationController: OWSNavigationController { approvalViewController.approvalDelegate = self pushViewController(approvalViewController, animated: true) + updateButtons() + } +} + +extension SendMediaNavigationController: UINavigationControllerDelegate { + func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { + if let navbarTheme = preferredNavbarTheme(viewController: viewController) { + if let owsNavBar = navigationBar as? OWSNavigationBar { + owsNavBar.overrideTheme(type: navbarTheme) + } else { + owsFailDebug("unexpected navigationBar: \(navigationBar)") + } + } + } + + // In case back navigation was canceled, we re-apply whatever is showing. + func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { + if let navbarTheme = preferredNavbarTheme(viewController: viewController) { + if let owsNavBar = navigationBar as? OWSNavigationBar { + owsNavBar.overrideTheme(type: navbarTheme) + } else { + owsFailDebug("unexpected navigationBar: \(navigationBar)") + } + } + } + + // MARK: - Helpers + + private func preferredNavbarTheme(viewController: UIViewController) -> OWSNavigationBar.NavigationBarThemeOverride? { + switch viewController { + case is AttachmentApprovalViewController: + return .clear + case is ImagePickerGridController: + return .alwaysDark + case is PhotoCaptureViewController: + return .clear + default: + owsFailDebug("unexpected viewController: \(viewController)") + return nil + } } } extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate { func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didFinishProcessingAttachment attachment: SignalAttachment) { attachmentDraftCollection.append(.camera(attachment: attachment)) - - pushApprovalViewController() + if isInBatchSelectMode { + updateButtons() + } else { + 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) } } @@ -106,6 +273,10 @@ extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate { extension SendMediaNavigationController: ImagePickerGridControllerDelegate { func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController) { + showApprovalAfterProcessingAnyMediaLibrarySelections() + } + + func showApprovalAfterProcessingAnyMediaLibrarySelections() { let mediaLibrarySelections: [MediaLibrarySelection] = self.mediaLibrarySelections.orderedValues let backgroundBlock: (ModalActivityIndicatorViewController) -> Void = { modal in @@ -141,11 +312,15 @@ extension SendMediaNavigationController: ImagePickerGridControllerDelegate { let libraryMedia = MediaLibrarySelection(asset: asset, signalAttachmentPromise: attachmentPromise) mediaLibrarySelections.append(key: asset, value: libraryMedia) + + updateButtons() } func imagePicker(_ imagePicker: ImagePickerGridController, didDeselectAsset asset: PHAsset) { if mediaLibrarySelections.hasValue(forKey: asset) { mediaLibrarySelections.remove(key: asset) + + updateButtons() } } @@ -184,19 +359,21 @@ extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegat 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() + isInBatchSelectMode = true + mediaLibraryViewController.batchSelectModeDidChange() - popViewController(animated: true) + popViewController(animated: true) { + self.updateButtons() + } } } -enum AttachmentDraft { +private enum AttachmentDraft { case camera(attachment: SignalAttachment) case picker(attachment: MediaLibraryAttachment) } -extension AttachmentDraft { +private extension AttachmentDraft { var attachment: SignalAttachment { switch self { case .camera(let cameraAttachment): @@ -211,7 +388,7 @@ extension AttachmentDraft { } } -struct AttachmentDraftCollection { +private struct AttachmentDraftCollection { private(set) var attachmentDrafts: [AttachmentDraft] static var empty: AttachmentDraftCollection { @@ -261,7 +438,7 @@ struct AttachmentDraftCollection { } } -struct MediaLibrarySelection: Hashable, Equatable { +private struct MediaLibrarySelection: Hashable, Equatable { let asset: PHAsset let signalAttachmentPromise: Promise @@ -281,7 +458,7 @@ struct MediaLibrarySelection: Hashable, Equatable { } } -struct MediaLibraryAttachment: Hashable, Equatable { +private struct MediaLibraryAttachment: Hashable, Equatable { let asset: PHAsset let signalAttachment: SignalAttachment @@ -293,3 +470,105 @@ struct MediaLibraryAttachment: Hashable, Equatable { return lhs.asset == rhs.asset } } + +extension SendMediaNavigationController: DoneButtonDelegate { + var doneButtonCount: Int { + return attachmentDraftCollection.count - attachmentDraftCollection.pickerAttachments.count + mediaLibrarySelections.count + } + + fileprivate func doneButtonWasTapped(_ doneButton: DoneButton) { + assert(attachmentDraftCollection.count > 0 || mediaLibrarySelections.count > 0) + showApprovalAfterProcessingAnyMediaLibrarySelections() + } +} + +private protocol DoneButtonDelegate: AnyObject { + func doneButtonWasTapped(_ doneButton: DoneButton) + var doneButtonCount: Int { get } +} + +private class DoneButton: UIView { + weak var delegate: DoneButtonDelegate? + + init() { + super.init(frame: .zero) + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap(tapGesture:))) + addGestureRecognizer(tapGesture) + + let container = UIView() + container.backgroundColor = .ows_white + container.layer.cornerRadius = 20 + container.layoutMargins = UIEdgeInsets(top: 7, leading: 8, bottom: 7, trailing: 8) + + addSubview(container) + container.autoPinEdgesToSuperviewMargins() + + let stackView = UIStackView(arrangedSubviews: [badge, chevron]) + stackView.axis = .horizontal + stackView.alignment = .center + stackView.spacing = 9 + + container.addSubview(stackView) + stackView.autoPinEdgesToSuperviewMargins() + } + + let numberFormatter: NumberFormatter = NumberFormatter() + + func updateCount() { + guard let delegate = delegate else { + return + } + + badgeLabel.text = numberFormatter.string(for: delegate.doneButtonCount) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Subviews + + private lazy var badge: UIView = { + let badge = CircleView() + badge.layoutMargins = UIEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4) + badge.backgroundColor = .ows_signalBlue + badge.addSubview(badgeLabel) + badgeLabel.autoPinEdgesToSuperviewMargins() + + // Constrain to be a pill that is at least a circle, and maybe wider. + badgeLabel.autoPin(toAspectRatio: 1.0, relation: .greaterThanOrEqual) + NSLayoutConstraint.autoSetPriority(.defaultLow) { + badgeLabel.autoPinToSquareAspectRatio() + } + + return badge + }() + + private lazy var badgeLabel: UILabel = { + let label = UILabel() + label.textColor = .ows_white + label.font = UIFont.ows_dynamicTypeSubheadline.ows_monospaced() + label.textAlignment = .center + return label + }() + + private lazy var chevron: UIView = { + let image: UIImage + if CurrentAppContext().isRTL { + image = #imageLiteral(resourceName: "small_chevron_left") + } else { + image = #imageLiteral(resourceName: "small_chevron_right") + } + let chevron = UIImageView(image: image.withRenderingMode(.alwaysTemplate)) + chevron.contentMode = .scaleAspectFit + chevron.tintColor = .ows_gray60 + chevron.autoSetDimensions(to: CGSize(width: 10, height: 18)) + + return chevron + }() + + @objc + func didTap(tapGesture: UITapGestureRecognizer) { + delegate?.doneButtonWasTapped(self) + } +} diff --git a/SignalMessaging/categories/UIFont+OWS.h b/SignalMessaging/categories/UIFont+OWS.h index 2e4dc57b1..741ed989b 100644 --- a/SignalMessaging/categories/UIFont+OWS.h +++ b/SignalMessaging/categories/UIFont+OWS.h @@ -56,6 +56,7 @@ NS_ASSUME_NONNULL_BEGIN - (UIFont *)ows_italic; - (UIFont *)ows_bold; - (UIFont *)ows_mediumWeight; +- (UIFont *)ows_monospaced; @end diff --git a/SignalMessaging/categories/UIFont+OWS.m b/SignalMessaging/categories/UIFont+OWS.m index 8f3675773..14b80f75c 100644 --- a/SignalMessaging/categories/UIFont+OWS.m +++ b/SignalMessaging/categories/UIFont+OWS.m @@ -229,6 +229,12 @@ NS_ASSUME_NONNULL_BEGIN return derivedFont; } +- (UIFont *)ows_monospaced +{ + return [self.class ows_monospacedDigitFontWithSize:self.pointSize]; +} + + @end NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/categories/UIView+OWS.h b/SignalMessaging/categories/UIView+OWS.h index 6ca66a471..64e24b4ce 100644 --- a/SignalMessaging/categories/UIView+OWS.h +++ b/SignalMessaging/categories/UIView+OWS.h @@ -43,6 +43,7 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value); - (NSLayoutConstraint *)autoPinToSquareAspectRatio; - (NSLayoutConstraint *)autoPinToAspectRatioWithSize:(CGSize)size; - (NSLayoutConstraint *)autoPinToAspectRatio:(CGFloat)ratio; +- (NSLayoutConstraint *)autoPinToAspectRatio:(CGFloat)ratio relation:(NSLayoutRelation)relation; #pragma mark - Content Hugging and Compression Resistance diff --git a/SignalMessaging/categories/UIView+OWS.m b/SignalMessaging/categories/UIView+OWS.m index e58f9354f..23263bd87 100644 --- a/SignalMessaging/categories/UIView+OWS.m +++ b/SignalMessaging/categories/UIView+OWS.m @@ -2,8 +2,8 @@ // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // -#import "OWSMath.h" #import "UIView+OWS.h" +#import "OWSMath.h" #import #import #import @@ -148,6 +148,11 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value) } - (NSLayoutConstraint *)autoPinToAspectRatio:(CGFloat)ratio +{ + return [self autoPinToAspectRatio:ratio relation:NSLayoutRelationEqual]; +} + +- (NSLayoutConstraint *)autoPinToAspectRatio:(CGFloat)ratio relation:(NSLayoutRelation)relation { // Clamp to ensure view has reasonable aspect ratio. CGFloat clampedRatio = CGFloatClamp(ratio, 0.05f, 95.0f); @@ -158,7 +163,7 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value) self.translatesAutoresizingMaskIntoConstraints = NO; NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeWidth - relatedBy:NSLayoutRelationEqual + relatedBy:relation toItem:self attribute:NSLayoutAttributeHeight multiplier:clampedRatio From 0a324484706f8825cc62a510fef0e34ca38ca97d Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 28 Mar 2019 08:48:23 -0600 Subject: [PATCH 5/5] cancel flow per design --- .../Photos/ImagePickerController.swift | 3 +- .../SendMediaNavigationController.swift | 33 ++++++++++++++++++- .../translations/en.lproj/Localizable.strings | 12 +++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/ImagePickerController.swift b/Signal/src/ViewControllers/Photos/ImagePickerController.swift index fdb0323f0..d1b784fd3 100644 --- a/Signal/src/ViewControllers/Photos/ImagePickerController.swift +++ b/Signal/src/ViewControllers/Photos/ImagePickerController.swift @@ -8,6 +8,7 @@ import PromiseKit protocol ImagePickerGridControllerDelegate: AnyObject { func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController) + func imagePickerDidCancel(_ imagePicker: ImagePickerGridController) func imagePicker(_ imagePicker: ImagePickerGridController, isAssetSelected asset: PHAsset) -> Bool func imagePicker(_ imagePicker: ImagePickerGridController, didSelectAsset asset: PHAsset, attachmentPromise: Promise) @@ -273,7 +274,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat @objc func didPressCancel(sender: UIBarButtonItem) { - self.dismiss(animated: true) + self.delegate?.imagePickerDidCancel(self) } // MARK: - Layout diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift index e2e7a41b7..cb234fe22 100644 --- a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -214,6 +214,31 @@ class SendMediaNavigationController: OWSNavigationController { pushViewController(approvalViewController, animated: true) updateButtons() } + + private func didRequestExit(dontAbandonText: String) { + if attachmentDraftCollection.count == 0 { + self.sendMediaNavDelegate?.sendMediaNavDidCancel(self) + } else { + let alertTitle = NSLocalizedString("SEND_MEDIA_ABANDON_TITLE", comment: "alert title when user attempts to leave the send media flow when they have an in-progress album") + + let alert = UIAlertController(title: alertTitle, message: nil, preferredStyle: .alert) + + let confirmAbandonText = NSLocalizedString("SEND_MEDIA_CONFIRM_ABANDON_ALBUM", comment: "alert action, confirming the user wants to exit the media flow and abandon any photos they've taken") + let confirmAbandonAction = UIAlertAction(title: confirmAbandonText, + style: .destructive, + handler: { [weak self] _ in + guard let self = self else { return } + self.sendMediaNavDelegate?.sendMediaNavDidCancel(self) + }) + alert.addAction(confirmAbandonAction) + let dontAbandonAction = UIAlertAction(title: dontAbandonText, + style: .default, + handler: { _ in }) + alert.addAction(dontAbandonAction) + + self.presentAlert(alert) + } + } } extension SendMediaNavigationController: UINavigationControllerDelegate { @@ -266,7 +291,8 @@ extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate { } func photoCaptureViewControllerDidCancel(_ photoCaptureViewController: PhotoCaptureViewController) { - self.sendMediaNavDelegate?.sendMediaNavDidCancel(self) + let dontAbandonText = NSLocalizedString("SEND_MEDIA_RETURN_TO_CAMERA", comment: "alert action when the user decides not to cancel the media flow after all.") + didRequestExit(dontAbandonText: dontAbandonText) } } @@ -276,6 +302,11 @@ extension SendMediaNavigationController: ImagePickerGridControllerDelegate { showApprovalAfterProcessingAnyMediaLibrarySelections() } + func imagePickerDidCancel(_ imagePicker: ImagePickerGridController) { + let dontAbandonText = NSLocalizedString("SEND_MEDIA_RETURN_TO_MEDIA_LIBRARY", comment: "alert action when the user decides not to cancel the media flow after all.") + didRequestExit(dontAbandonText: dontAbandonText) + } + func showApprovalAfterProcessingAnyMediaLibrarySelections() { let mediaLibrarySelections: [MediaLibrarySelection] = self.mediaLibrarySelections.orderedValues diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index c5585ead8..3066ce983 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1991,6 +1991,18 @@ /* Text for button to send a Signal invite via SMS. %@ is placeholder for the recipient's phone number. */ "SEND_INVITE_VIA_SMS_BUTTON_FORMAT" = "Invite via SMS: %@"; +/* alert title when user attempts to leave the send media flow when they have an in-progress album */ +"SEND_MEDIA_ABANDON_TITLE" = "Discard Media?"; + +/* alert action, confirming the user wants to exit the media flow and abandon any photos they've taken */ +"SEND_MEDIA_CONFIRM_ABANDON_ALBUM" = "Discard Media"; + +/* alert action when the user decides not to cancel the media flow after all. */ +"SEND_MEDIA_RETURN_TO_CAMERA" = "Return to Camera"; + +/* alert action when the user decides not to cancel the media flow after all. */ +"SEND_MEDIA_RETURN_TO_MEDIA_LIBRARY" = "Return to Media Library"; + /* No comment provided by engineer. */ "SEND_SMS_CONFIRM_TITLE" = "Invite a friend via insecure SMS?";