mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Merge branch 'mkirk/media-send-flow'
This commit is contained in:
commit
3c5510438f
21 changed files with 788 additions and 360 deletions
|
@ -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 */,
|
||||||
|
|
23
Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/Contents.json
vendored
Normal file
23
Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/Contents.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
BIN
Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@1x.png
vendored
Normal file
BIN
Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@1x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 477 B |
BIN
Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@2x.png
vendored
Normal file
BIN
Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 813 B |
BIN
Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@3x.png
vendored
Normal file
BIN
Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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?";
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -22,6 +22,6 @@ public class FeatureFlags: NSObject {
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
public static var useCustomPhotoCapture: Bool {
|
public static var useCustomPhotoCapture: Bool {
|
||||||
return false
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue