Merge branch 'mkirk/media-send-flow'

This commit is contained in:
Michael Kirk 2019-03-28 10:32:36 -06:00
commit 3c5510438f
21 changed files with 788 additions and 360 deletions

View file

@ -483,6 +483,7 @@
4C3E245D21F2B395000AE092 /* DirectionalPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */; }; 4C3E245D21F2B395000AE092 /* DirectionalPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */; };
4C3EF7FD2107DDEE0007EBF7 /* ParamParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EF7FC2107DDEE0007EBF7 /* ParamParserTest.swift */; }; 4C3EF7FD2107DDEE0007EBF7 /* ParamParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EF7FC2107DDEE0007EBF7 /* ParamParserTest.swift */; };
4C3EF802210918740007EBF7 /* SSKProtoEnvelopeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EF801210918740007EBF7 /* SSKProtoEnvelopeTest.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 */; }; 4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */; };
4C4BC6C32102D697004040C9 /* ContactDiscoveryOperationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */; }; 4C4BC6C32102D697004040C9 /* ContactDiscoveryOperationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */; };
4C5250D221E7BD7D00CE3D95 /* PhoneNumberValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5250D121E7BD7D00CE3D95 /* PhoneNumberValidator.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 = "<group>"; }; 4C2F454E214C00E1004871FF /* AvatarTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarTableViewCell.swift; sourceTree = "<group>"; };
4C3EF7FC2107DDEE0007EBF7 /* ParamParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParamParserTest.swift; sourceTree = "<group>"; }; 4C3EF7FC2107DDEE0007EBF7 /* ParamParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParamParserTest.swift; sourceTree = "<group>"; };
4C3EF801210918740007EBF7 /* SSKProtoEnvelopeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKProtoEnvelopeTest.swift; sourceTree = "<group>"; }; 4C3EF801210918740007EBF7 /* SSKProtoEnvelopeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKProtoEnvelopeTest.swift; sourceTree = "<group>"; };
4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMediaNavigationController.swift; sourceTree = "<group>"; };
4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissableTextField.swift; sourceTree = "<group>"; }; 4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissableTextField.swift; sourceTree = "<group>"; };
4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ContactDiscoveryOperationTest.swift; path = contact/ContactDiscoveryOperationTest.swift; sourceTree = "<group>"; }; 4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ContactDiscoveryOperationTest.swift; path = contact/ContactDiscoveryOperationTest.swift; sourceTree = "<group>"; };
4C5250D121E7BD7D00CE3D95 /* PhoneNumberValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberValidator.swift; sourceTree = "<group>"; }; 4C5250D121E7BD7D00CE3D95 /* PhoneNumberValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberValidator.swift; sourceTree = "<group>"; };
@ -1857,6 +1859,7 @@
34969558219B605E00DCFE74 /* Photos */ = { 34969558219B605E00DCFE74 /* Photos */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */,
34969559219B605E00DCFE74 /* ImagePickerController.swift */, 34969559219B605E00DCFE74 /* ImagePickerController.swift */,
3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */, 3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */,
3496955B219B605E00DCFE74 /* PhotoLibrary.swift */, 3496955B219B605E00DCFE74 /* PhotoLibrary.swift */,
@ -3659,6 +3662,7 @@
340FC8AB204DAC8D007AEB0F /* DomainFrontingCountryViewController.m in Sources */, 340FC8AB204DAC8D007AEB0F /* DomainFrontingCountryViewController.m in Sources */,
3496744D2076768700080B5F /* OWSMessageBubbleView.m in Sources */, 3496744D2076768700080B5F /* OWSMessageBubbleView.m in Sources */,
34B3F8751E8DF1700035BE1A /* CallViewController.swift in Sources */, 34B3F8751E8DF1700035BE1A /* CallViewController.swift in Sources */,
4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */,
34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */, 34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */,
45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */, 45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */,
34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */, 34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */,

View file

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -133,8 +133,7 @@ typedef enum : NSUInteger {
UIDocumentMenuDelegate, UIDocumentMenuDelegate,
UIDocumentPickerDelegate, UIDocumentPickerDelegate,
UIImagePickerControllerDelegate, UIImagePickerControllerDelegate,
OWSImagePickerControllerDelegate, SendMediaNavDelegate,
OWSPhotoCaptureViewControllerDelegate,
UINavigationControllerDelegate, UINavigationControllerDelegate,
UITextViewDelegate, UITextViewDelegate,
ConversationCollectionViewDelegate, ConversationCollectionViewDelegate,
@ -2837,24 +2836,6 @@ typedef enum : NSUInteger {
[self showApprovalDialogForAttachment:attachment]; [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 #pragma mark - UIImagePickerController
/* /*
@ -2877,19 +2858,8 @@ typedef enum : NSUInteger {
UIViewController *pickerModal; UIViewController *pickerModal;
if (SSKFeatureFlags.useCustomPhotoCapture) { if (SSKFeatureFlags.useCustomPhotoCapture) {
OWSPhotoCaptureViewController *captureVC = [OWSPhotoCaptureViewController new]; SendMediaNavigationController *navController = [SendMediaNavigationController showingCameraFirst];
captureVC.delegate = self; navController.sendMediaNavDelegate = 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);
pickerModal = navController; pickerModal = navController;
} else { } else {
UIImagePickerController *picker = [OWSImagePickerController new]; UIImagePickerController *picker = [OWSImagePickerController new];
@ -2933,22 +2903,8 @@ typedef enum : NSUInteger {
return; return;
} }
UIViewController *pickerModal; SendMediaNavigationController *pickerModal = [SendMediaNavigationController showingMediaLibraryFirst];
if (SignalAttachment.isMultiSendEnabled) { pickerModal.sendMediaNavDelegate = self;
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 ];
pickerModal = picker;
}
[self dismissKeyBoard]; [self dismissKeyBoard];
[self presentViewController:pickerModal animated:YES completion:nil]; [self presentViewController:pickerModal animated:YES completion:nil];
@ -2971,13 +2927,19 @@ typedef enum : NSUInteger {
self.view.frame = frame; self.view.frame = frame;
} }
#pragma mark - OWSImagePickerControllerDelegate #pragma mark - SendMediaNavDelegate
- (void)imagePicker:(OWSImagePickerGridController *)imagePicker - (void)sendMediaNavDidCancel:(SendMediaNavigationController *)sendMediaNavigationController
didPickImageAttachments:(NSArray<SignalAttachment *> *)attachments {
messageText:(NSString *_Nullable)messageText [self dismissViewControllerAnimated:YES completion:nil];
}
- (void)sendMediaNav:(SendMediaNavigationController *)sendMediaNavigationController
didApproveAttachments:(NSArray<SignalAttachment *> *)attachments
messageText:(nullable NSString *)messageText
{ {
[self tryToSendAttachments:attachments messageText:messageText]; [self tryToSendAttachments:attachments messageText:messageText];
[self dismissViewControllerAnimated:YES completion:nil];
} }
#pragma mark - UIImagePickerControllerDelegate #pragma mark - UIImagePickerControllerDelegate
@ -3060,11 +3022,8 @@ typedef enum : NSUInteger {
}]; }];
} else { } else {
// Non-Video image picked from library // Non-Video image picked from library
if (SignalAttachment.isMultiSendEnabled) { OWSFailDebug(
OWSFailDebug(@"Only use UIImagePicker for camera/video capture. Picking media from UIImagePicker is not " @"Only use UIImagePicker for camera/video capture. Picking media from UIImagePicker is not supported. ");
@"supported. ");
}
// To avoid re-encoding GIF and PNG's as JPEG we have to get the raw data of // 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 // the selected item vs. using the UIImagePickerControllerOriginalImage
@ -3957,8 +3916,7 @@ typedef enum : NSUInteger {
[self scrollToBottomAnimated:NO]; [self scrollToBottomAnimated:NO];
} }
- (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval - (void)attachmentApprovalDidCancel:(AttachmentApprovalViewController *)attachmentApproval
didCancelAttachments:(NSArray<SignalAttachment *> *)attachment
{ {
[self dismissViewControllerAnimated:YES completion:nil]; [self dismissViewControllerAnimated:YES completion:nil];
} }

View file

@ -6,16 +6,21 @@ import Foundation
import Photos import Photos
import PromiseKit import PromiseKit
@objc(OWSImagePickerControllerDelegate) protocol ImagePickerGridControllerDelegate: AnyObject {
protocol ImagePickerControllerDelegate { func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController)
func imagePicker(_ imagePicker: ImagePickerGridController, didPickImageAttachments attachments: [SignalAttachment], messageText: String?) func imagePickerDidCancel(_ imagePicker: ImagePickerGridController)
func imagePicker(_ imagePicker: ImagePickerGridController, isAssetSelected asset: PHAsset) -> Bool
func imagePicker(_ imagePicker: ImagePickerGridController, didSelectAsset asset: PHAsset, attachmentPromise: Promise<SignalAttachment>)
func imagePicker(_ imagePicker: ImagePickerGridController, didDeselectAsset asset: PHAsset)
var isInBatchSelectMode: Bool { get }
func imagePickerCanSelectAdditionalItems(_ imagePicker: ImagePickerGridController) -> Bool
} }
@objc(OWSImagePickerGridController) class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate, PhotoCollectionPickerDelegate {
class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate, PhotoCollectionPickerDelegate, AttachmentApprovalViewControllerDelegate {
@objc weak var delegate: ImagePickerGridControllerDelegate?
weak var delegate: ImagePickerControllerDelegate?
private let library: PhotoLibrary = PhotoLibrary() private let library: PhotoLibrary = PhotoLibrary()
private var photoCollection: PhotoCollection private var photoCollection: PhotoCollection
@ -25,12 +30,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
var collectionViewFlowLayout: UICollectionViewFlowLayout var collectionViewFlowLayout: UICollectionViewFlowLayout
var titleView: TitleView! 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() { init() {
collectionViewFlowLayout = type(of: self).buildLayout() collectionViewFlowLayout = type(of: self).buildLayout()
photoCollection = library.defaultPhotoCollection() photoCollection = library.defaultPhotoCollection()
@ -79,11 +78,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
navigationItem.titleView = titleView navigationItem.titleView = titleView
self.titleView = titleView self.titleView = titleView
let featureFlag_isMultiselectEnabled = true
if featureFlag_isMultiselectEnabled {
updateSelectButton()
}
collectionView.backgroundColor = .ows_gray95 collectionView.backgroundColor = .ows_gray95
let selectionPanGesture = DirectionalPanGestureRecognizer(direction: [.horizontal], target: self, action: #selector(didPanSelection)) let selectionPanGesture = DirectionalPanGestureRecognizer(direction: [.horizontal], target: self, action: #selector(didPanSelection))
@ -100,12 +94,17 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
@objc @objc
func didPanSelection(_ selectionPanGesture: UIPanGestureRecognizer) { func didPanSelection(_ selectionPanGesture: UIPanGestureRecognizer) {
guard isInBatchSelectMode else { guard let collectionView = collectionView else {
owsFailDebug("collectionView was unexpectedly nil")
return return
} }
guard let collectionView = collectionView else { guard let delegate = delegate else {
owsFailDebug("collectionView was unexpectedly nil") owsFailDebug("delegate was unexpectedly nil")
return
}
guard delegate.isInBatchSelectMode else {
return return
} }
@ -121,7 +120,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
return return
} }
let asset = photoCollectionContents.asset(at: indexPath.item) let asset = photoCollectionContents.asset(at: indexPath.item)
if selectedIds.contains(asset.localIdentifier) { if delegate.imagePicker(self, isAssetSelected: asset) {
selectionPanGestureMode = .deselect selectionPanGestureMode = .deselect
} else { } else {
selectionPanGestureMode = .select selectionPanGestureMode = .select
@ -139,36 +138,36 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
} }
func tryToToggleBatchSelect(at indexPath: IndexPath) { func tryToToggleBatchSelect(at indexPath: IndexPath) {
guard isInBatchSelectMode else { guard let collectionView = collectionView else {
owsFailDebug("isInBatchSelectMode was unexpectedly false") owsFailDebug("collectionView was unexpectedly nil")
return return
} }
guard let collectionView = collectionView else { guard let delegate = delegate else {
owsFailDebug("collectionView was unexpectedly nil") owsFailDebug("delegate was unexpectedly nil")
return
}
guard delegate.isInBatchSelectMode else {
owsFailDebug("isInBatchSelectMode was unexpectedly false")
return return
} }
let asset = photoCollectionContents.asset(at: indexPath.item) let asset = photoCollectionContents.asset(at: indexPath.item)
switch selectionPanGestureMode { switch selectionPanGestureMode {
case .select: case .select:
guard canSelectAdditionalItems else { guard delegate.imagePickerCanSelectAdditionalItems(self) else {
showTooManySelectedToast() showTooManySelectedToast()
return return
} }
selectedIds.add(asset.localIdentifier) let attachmentPromise: Promise<SignalAttachment> = photoCollectionContents.outgoingAttachment(for: asset)
delegate.imagePicker(self, didSelectAsset: asset, attachmentPromise: attachmentPromise)
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: []) collectionView.selectItem(at: indexPath, animated: true, scrollPosition: [])
case .deselect: case .deselect:
selectedIds.remove(asset.localIdentifier) delegate.imagePicker(self, didDeselectAsset: asset)
collectionView.deselectItem(at: indexPath, animated: true) collectionView.deselectItem(at: indexPath, animated: true)
} }
updateDoneButton()
}
var canSelectAdditionalItems: Bool {
return selectedIds.count <= SignalAttachment.maxAttachmentsAllowed
} }
override func viewWillLayoutSubviews() { override func viewWillLayoutSubviews() {
@ -180,12 +179,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) 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 // Determine the size of the thumbnails to request
let scale = UIScreen.main.scale let scale = UIScreen.main.scale
let cellSize = collectionViewFlowLayout.itemSize let cellSize = collectionViewFlowLayout.itemSize
@ -216,10 +209,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
super.viewDidAppear(animated) super.viewDidAppear(animated)
hasEverAppeared = true 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`. // Since we're presenting *over* the ConversationVC, we need to `becomeFirstResponder`.
// //
@ -263,14 +252,18 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
return return
} }
guard let delegate = delegate else {
owsFailDebug("delegate was unexpectedly nil")
return
}
collectionView.reloadData() collectionView.reloadData()
collectionView.layoutIfNeeded() collectionView.layoutIfNeeded()
let count = photoCollectionContents.assetCount let count = photoCollectionContents.assetCount
for index in 0..<count { for index in 0..<count {
let asset = photoCollectionContents.asset(at: index) let asset = photoCollectionContents.asset(at: index)
let assetId = asset.localIdentifier if delegate.imagePicker(self, isAssetSelected: asset) {
if selectedIds.contains(assetId) {
collectionView.selectItem(at: IndexPath(row: index, section: 0), collectionView.selectItem(at: IndexPath(row: index, section: 0),
animated: false, scrollPosition: []) animated: false, scrollPosition: [])
} }
@ -281,7 +274,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
@objc @objc
func didPressCancel(sender: UIBarButtonItem) { func didPressCancel(sender: UIBarButtonItem) {
self.dismiss(animated: true) self.delegate?.imagePickerDidCancel(self)
} }
// MARK: - Layout // MARK: - Layout
@ -327,156 +320,27 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
// MARK: - Batch Selection // MARK: - Batch Selection
lazy var doneButton: UIBarButtonItem = { func batchSelectModeDidChange() {
return UIBarButtonItem(barButtonSystemItem: .done, guard let delegate = delegate else {
target: self, return
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()
} }
guard let collectionView = collectionView else {
owsFailDebug("collectionView was unexpectedly nil")
return
}
collectionView.allowsMultipleSelection = delegate.isInBatchSelectMode
collectionView.reloadData()
} }
@objc func clearCollectionViewSelection() {
func didPressDone(_ sender: Any) {
Logger.debug("")
hasPressedDoneSinceAppeared = true
updateDoneButton()
// Honor selection order.
var assetIdToAssetIndexMap = [String: Int]()
let assetCount = photoCollectionContents.assetCount
for index in 0..<assetCount {
let asset = photoCollectionContents.asset(at: index)
let assetId = asset.localIdentifier
assetIdToAssetIndexMap[assetId] = index
}
var assets = [PHAsset]()
for selectedIdAny in selectedIds.array {
guard let selectedId = selectedIdAny as? String else {
owsFailDebug("Invalid asset id: \(selectedIdAny)")
continue
}
guard let assetIndex = assetIdToAssetIndexMap[selectedId] else {
owsFailDebug("Missing asset id: \(selectedId)")
continue
}
assets.append(photoCollectionContents.asset(at: assetIndex))
}
complete(withAssets: assets)
}
func complete(withAssets assets: [PHAsset]) {
ModalActivityIndicatorViewController.present(fromViewController: self,
canCancel: false) { (modal) in
let attachmentPromises: [Promise<SignalAttachment>] = 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()
}
}
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]
}
let vc = AttachmentApprovalViewController(mode: .sharedNavigation, attachments: attachments)
vc.approvalDelegate = self
navigationController?.pushViewController(vc, animated: true)
}
var hasPressedDoneSinceAppeared: Bool = false
func updateDoneButton() {
guard let collectionView = self.collectionView else { guard let collectionView = self.collectionView else {
owsFailDebug("collectionView was unexpectedly nil") owsFailDebug("collectionView was unexpectedly nil")
return 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
}
func deselectAnySelected() {
guard let collectionView = self.collectionView else {
owsFailDebug("collectionView was unexpectedly nil")
return
}
selectedIds.removeAllObjects()
collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false)} collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false)}
if isInBatchSelectMode {
updateDoneButton()
}
} }
func showTooManySelectedToast() { func showTooManySelectedToast() {
@ -541,9 +405,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
UIView.animate(.promise, duration: 0.25, delay: 0, options: .curveEaseInOut) { UIView.animate(.promise, duration: 0.25, delay: 0, options: .curveEaseInOut) {
collectionPickerView.superview?.layoutIfNeeded() collectionPickerView.superview?.layoutIfNeeded()
self.updateSelectButton()
self.titleView.rotateIcon(.up) self.titleView.rotateIcon(.up)
}.retainUntilComplete() }.retainUntilComplete()
} }
@ -558,9 +419,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
UIView.animate(.promise, duration: 0.25, delay: 0, options: .curveEaseInOut) { UIView.animate(.promise, duration: 0.25, delay: 0, options: .curveEaseInOut) {
collectionPickerController.view.frame = self.view.frame.offsetBy(dx: 0, dy: self.view.frame.height) collectionPickerController.view.frame = self.view.frame.offsetBy(dx: 0, dy: self.view.frame.height)
self.updateSelectButton()
self.titleView.rotateIcon(.down) self.titleView.rotateIcon(.down)
}.done { _ in }.done { _ in
collectionPickerController.view.removeFromSuperview() collectionPickerController.view.removeFromSuperview()
@ -577,7 +435,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
} }
// Any selections are invalid as they refer to indices in a different collection // Any selections are invalid as they refer to indices in a different collection
deselectAnySelected() clearCollectionViewSelection()
photoCollection = collection photoCollection = collection
photoCollectionContents = photoCollection.contents() photoCollectionContents = photoCollection.contents()
@ -605,29 +463,31 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
} }
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 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
}
if isInBatchSelectMode { let asset: PHAsset = photoCollectionContents.asset(at: indexPath.item)
let assetId = asset.localIdentifier let attachmentPromise: Promise<SignalAttachment> = photoCollectionContents.outgoingAttachment(for: asset)
selectedIds.add(assetId) delegate.imagePicker(self, didSelectAsset: asset, attachmentPromise: attachmentPromise)
updateDoneButton()
} else { if !delegate.isInBatchSelectMode {
// Don't show "selected" badge unless we're in batch mode // Don't show "selected" badge unless we're in batch mode
collectionView.deselectItem(at: indexPath, animated: false) collectionView.deselectItem(at: indexPath, animated: false)
complete(withAssets: [asset]) delegate.imagePickerDidCompleteSelection(self)
} }
} }
public override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { public override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
Logger.debug("") Logger.debug("")
guard let delegate = delegate else {
owsFailDebug("delegate was unexpectedly nil")
return
}
let asset = photoCollectionContents.asset(at: indexPath.item) let asset = photoCollectionContents.asset(at: indexPath.item)
let assetId = asset.localIdentifier delegate.imagePicker(self, didDeselectAsset: asset)
selectedIds.remove(assetId)
if isInBatchSelectMode {
updateDoneButton()
}
} }
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
@ -635,69 +495,27 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
} }
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 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 { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoGridViewCell.reuseIdentifier, for: indexPath) as? PhotoGridViewCell else {
owsFail("cell was unexpectedly nil") owsFail("cell was unexpectedly nil")
} }
cell.loadingColor = UIColor(white: 0.2, alpha: 1) cell.loadingColor = UIColor(white: 0.2, alpha: 1)
let assetItem = photoCollectionContents.assetItem(at: indexPath.item, photoMediaSize: photoMediaSize) let assetItem = photoCollectionContents.assetItem(at: indexPath.item, photoMediaSize: photoMediaSize)
cell.configure(item: assetItem) cell.configure(item: assetItem)
let assetId = assetItem.asset.localIdentifier let isSelected = delegate.imagePicker(self, isAssetSelected: assetItem.asset)
let isSelected = selectedIds.contains(assetId) if isSelected {
cell.isSelected = isSelected cell.isSelected = isSelected
} else {
cell.isSelected = isSelected
}
return cell 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)
}
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
}
} }
extension ImagePickerGridController: UIGestureRecognizerDelegate { extension ImagePickerGridController: UIGestureRecognizerDelegate {

View file

@ -148,6 +148,7 @@ class PhotoCaptureViewController: OWSViewController {
button.setImage(imageName: imageName) button.setImage(imageName: imageName)
} }
} }
private lazy var dismissControl: PhotoControl = { private lazy var dismissControl: PhotoControl = {
return PhotoControl(imageName: "ic_x_with_shadow") { [weak self] in return PhotoControl(imageName: "ic_x_with_shadow") { [weak self] in
self?.didTapClose() self?.didTapClose()

View file

@ -79,7 +79,6 @@ class PhotoCollectionContents {
enum PhotoLibraryError: Error { enum PhotoLibraryError: Error {
case assertionError(description: String) case assertionError(description: String)
case unsupportedMediaType case unsupportedMediaType
} }
init(fetchResult: PHFetchResult<PHAsset>, localizedTitle: String?) { init(fetchResult: PHFetchResult<PHAsset>, localizedTitle: String?) {
@ -207,15 +206,11 @@ class PhotoCollectionContents {
switch asset.mediaType { switch asset.mediaType {
case .image: case .image:
return requestImageDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in return requestImageDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .medium) return SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .medium)
attachment.assetId = asset.localIdentifier
return attachment
} }
case .video: case .video:
return requestVideoDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in return requestVideoDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI) return SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI)
attachment.assetId = asset.localIdentifier
return attachment
} }
default: default:
return Promise(error: PhotoLibraryError.unsupportedMediaType) return Promise(error: PhotoLibraryError.unsupportedMediaType)

View file

@ -0,0 +1,605 @@
//
// 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 }
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
public weak var sendMediaNavDelegate: SendMediaNavDelegate?
@objc
public class func showingCameraFirst() -> SendMediaNavigationController {
let navController = SendMediaNavigationController()
navController.setViewControllers([navController.captureViewController], animated: false)
navController.updateButtons()
return navController
}
@objc
public class func showingMediaLibraryFirst() -> SendMediaNavigationController {
let navController = SendMediaNavigationController()
navController.setViewControllers([navController.mediaLibraryViewController], animated: false)
navController.updateButtons()
return navController
}
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
private var attachments: [SignalAttachment] {
return attachmentDraftCollection.attachmentDrafts.map { $0.attachment }
}
private let mediaLibrarySelections: OrderedDictionary<PHAsset, MediaLibrarySelection> = 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)
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 {
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))
if isInBatchSelectMode {
updateButtons()
} else {
pushApprovalViewController()
}
}
func photoCaptureViewControllerDidCancel(_ photoCaptureViewController: PhotoCaptureViewController) {
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)
}
}
extension SendMediaNavigationController: ImagePickerGridControllerDelegate {
func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController) {
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
let backgroundBlock: (ModalActivityIndicatorViewController) -> Void = { modal in
let attachmentPromises: [Promise<MediaLibraryAttachment>] = 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<SignalAttachment>) {
guard !mediaLibrarySelections.hasValue(forKey: asset) else {
return
}
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()
}
}
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.
isInBatchSelectMode = true
mediaLibraryViewController.batchSelectModeDidChange()
popViewController(animated: true) {
self.updateButtons()
}
}
}
private enum AttachmentDraft {
case camera(attachment: SignalAttachment)
case picker(attachment: MediaLibraryAttachment)
}
private 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
}
}
private 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<MediaLibraryAttachment> = Set(attachments)
let oldPickerAttachments: Set<MediaLibraryAttachment> = 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))
}
}
}
private struct MediaLibrarySelection: Hashable, Equatable {
let asset: PHAsset
let signalAttachmentPromise: Promise<SignalAttachment>
var hashValue: Int {
return asset.hashValue
}
var promise: Promise<MediaLibraryAttachment> {
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
}
}
private 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
}
}
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)
}
}

View file

@ -1991,6 +1991,18 @@
/* Text for button to send a Signal invite via SMS. %@ is placeholder for the recipient's phone number. */ /* 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: %@"; "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. */ /* No comment provided by engineer. */
"SEND_SMS_CONFIRM_TITLE" = "Invite a friend via insecure SMS?"; "SEND_SMS_CONFIRM_TITLE" = "Invite a friend via insecure SMS?";

View file

@ -10,9 +10,16 @@ import PromiseKit
@objc @objc
public protocol AttachmentApprovalViewControllerDelegate: class { public protocol AttachmentApprovalViewControllerDelegate: class {
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?)
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didCancelAttachments attachments: [SignalAttachment]) func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController)
@objc optional func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, addMoreToAttachments attachments: [SignalAttachment])
@objc optional func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, changedCaptionOfAttachment attachment: SignalAttachment) @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: - // MARK: -
@ -363,6 +370,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
}, },
completion: { _ in completion: { _ in
self.attachmentItemCollection.remove(item: attachmentItem) self.attachmentItemCollection.remove(item: attachmentItem)
self.approvalDelegate?.attachmentApproval?(self, didRemoveAttachment: attachmentItem.attachment)
self.updateMediaRail() self.updateMediaRail()
}) })
} }
@ -629,7 +637,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
} }
private func cancelPressed() { private func cancelPressed() {
self.approvalDelegate?.attachmentApproval(self, didCancelAttachments: attachments) self.approvalDelegate?.attachmentApprovalDidCancel(self)
} }
@objc func didTapCaption(sender: UIButton) { @objc func didTapCaption(sender: UIButton) {
@ -668,7 +676,7 @@ extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate {
} }
func attachmentTextToolbarDidAddMore(_ attachmentTextToolbar: AttachmentTextToolbar) { func attachmentTextToolbarDidAddMore(_ attachmentTextToolbar: AttachmentTextToolbar) {
self.approvalDelegate?.attachmentApproval?(self, addMoreToAttachments: attachments) self.approvalDelegate?.attachmentApprovalDidTapAddMore?(self)
} }
} }

View file

@ -305,8 +305,7 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion);
fromViewController:attachmentApproval]; fromViewController:attachmentApproval];
} }
- (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval - (void)attachmentApprovalDidCancel:(AttachmentApprovalViewController *)attachmentApproval
didCancelAttachments:(NSArray<SignalAttachment *> *)attachment
{ {
[self cancelShareExperience]; [self cancelShareExperience];
} }

View file

@ -11,7 +11,7 @@ import UIKit
// as immutable, once configured. // as immutable, once configured.
public class ImageEditorContents: NSObject { public class ImageEditorContents: NSObject {
public typealias ItemMapType = OrderedDictionary<ImageEditorItem> public typealias ItemMapType = OrderedDictionary<String, ImageEditorItem>
// This represents the current state of each item, // This represents the current state of each item,
// a mapping of [itemId : item]. // a mapping of [itemId : item].
@ -72,7 +72,7 @@ public class ImageEditorContents: NSObject {
@objc @objc
public func items() -> [ImageEditorItem] { public func items() -> [ImageEditorItem] {
return itemMap.orderedValues() return itemMap.orderedValues
} }
@objc @objc

View file

@ -4,16 +4,13 @@
import Foundation import Foundation
public class OrderedDictionary<ValueType>: NSObject { public class OrderedDictionary<KeyType: Hashable, ValueType> {
public typealias KeyType = String private var keyValueMap = [KeyType: ValueType]()
var keyValueMap = [KeyType: ValueType]() public var orderedKeys = [KeyType]()
var orderedKeys = [KeyType]() public init() { }
public override init() {
}
// Used to clone copies of instances of this class. // Used to clone copies of instances of this class.
public init(keyValueMap: [KeyType: ValueType], public init(keyValueMap: [KeyType: ValueType],
@ -25,7 +22,7 @@ public class OrderedDictionary<ValueType>: NSObject {
// Since the contents are immutable, we only modify copies // Since the contents are immutable, we only modify copies
// made with this method. // made with this method.
public func clone() -> OrderedDictionary<ValueType> { public func clone() -> OrderedDictionary<KeyType, ValueType> {
return OrderedDictionary(keyValueMap: keyValueMap, orderedKeys: orderedKeys) return OrderedDictionary(keyValueMap: keyValueMap, orderedKeys: orderedKeys)
} }
@ -33,6 +30,10 @@ public class OrderedDictionary<ValueType>: NSObject {
return keyValueMap[key] return keyValueMap[key]
} }
public func hasValue(forKey key: KeyType) -> Bool {
return keyValueMap[key] != nil
}
public func append(key: KeyType, value: ValueType) { public func append(key: KeyType, value: ValueType) {
if keyValueMap[key] != nil { if keyValueMap[key] != nil {
owsFailDebug("Unexpected duplicate key in key map: \(key)") owsFailDebug("Unexpected duplicate key in key map: \(key)")
@ -90,7 +91,7 @@ public class OrderedDictionary<ValueType>: NSObject {
return orderedKeys.count return orderedKeys.count
} }
public func orderedValues() -> [ValueType] { public var orderedValues: [ValueType] {
var values = [ValueType]() var values = [ValueType]()
for key in orderedKeys { for key in orderedKeys {
guard let value = self.keyValueMap[key] else { guard let value = self.keyValueMap[key] else {

View file

@ -160,10 +160,6 @@ public class SignalAttachment: NSObject {
@objc @objc
public let dataUTI: String 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? { var error: SignalAttachmentError? {
didSet { didSet {
AssertIsOnMainThread() AssertIsOnMainThread()
@ -193,12 +189,7 @@ public class SignalAttachment: NSObject {
// MARK: // MARK:
@objc @objc
public static let isMultiSendEnabled = true public static let maxAttachmentsAllowed: Int = 32
@objc
public static var maxAttachmentsAllowed: Int {
return isMultiSendEnabled ? 32 : 1
}
// MARK: Constructor // MARK: Constructor

View file

@ -56,6 +56,7 @@ NS_ASSUME_NONNULL_BEGIN
- (UIFont *)ows_italic; - (UIFont *)ows_italic;
- (UIFont *)ows_bold; - (UIFont *)ows_bold;
- (UIFont *)ows_mediumWeight; - (UIFont *)ows_mediumWeight;
- (UIFont *)ows_monospaced;
@end @end

View file

@ -229,6 +229,12 @@ NS_ASSUME_NONNULL_BEGIN
return derivedFont; return derivedFont;
} }
- (UIFont *)ows_monospaced
{
return [self.class ows_monospacedDigitFontWithSize:self.pointSize];
}
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

View file

@ -43,6 +43,7 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value);
- (NSLayoutConstraint *)autoPinToSquareAspectRatio; - (NSLayoutConstraint *)autoPinToSquareAspectRatio;
- (NSLayoutConstraint *)autoPinToAspectRatioWithSize:(CGSize)size; - (NSLayoutConstraint *)autoPinToAspectRatioWithSize:(CGSize)size;
- (NSLayoutConstraint *)autoPinToAspectRatio:(CGFloat)ratio; - (NSLayoutConstraint *)autoPinToAspectRatio:(CGFloat)ratio;
- (NSLayoutConstraint *)autoPinToAspectRatio:(CGFloat)ratio relation:(NSLayoutRelation)relation;
#pragma mark - Content Hugging and Compression Resistance #pragma mark - Content Hugging and Compression Resistance

View file

@ -2,8 +2,8 @@
// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // Copyright (c) 2019 Open Whisper Systems. All rights reserved.
// //
#import "OWSMath.h"
#import "UIView+OWS.h" #import "UIView+OWS.h"
#import "OWSMath.h"
#import <SignalCoreKit/iOSVersions.h> #import <SignalCoreKit/iOSVersions.h>
#import <SignalMessaging/SignalMessaging-Swift.h> #import <SignalMessaging/SignalMessaging-Swift.h>
#import <SignalServiceKit/AppContext.h> #import <SignalServiceKit/AppContext.h>
@ -148,6 +148,11 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value)
} }
- (NSLayoutConstraint *)autoPinToAspectRatio:(CGFloat)ratio - (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. // Clamp to ensure view has reasonable aspect ratio.
CGFloat clampedRatio = CGFloatClamp(ratio, 0.05f, 95.0f); CGFloat clampedRatio = CGFloatClamp(ratio, 0.05f, 95.0f);
@ -158,7 +163,7 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value)
self.translatesAutoresizingMaskIntoConstraints = NO; self.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeWidth attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual relatedBy:relation
toItem:self toItem:self
attribute:NSLayoutAttributeHeight attribute:NSLayoutAttributeHeight
multiplier:clampedRatio multiplier:clampedRatio

View file

@ -22,6 +22,6 @@ public class FeatureFlags: NSObject {
@objc @objc
public static var useCustomPhotoCapture: Bool { public static var useCustomPhotoCapture: Bool {
return false return true
} }
} }