Centralize attachment state in nav controller

pull/2/head
Michael Kirk 5 years ago
parent 6502d7d4a5
commit 7dbb9517af

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

@ -133,8 +133,7 @@ typedef enum : NSUInteger {
UIDocumentMenuDelegate,
UIDocumentPickerDelegate,
UIImagePickerControllerDelegate,
OWSImagePickerControllerDelegate,
OWSPhotoCaptureViewControllerDelegate,
SendMediaNavDelegate,
UINavigationControllerDelegate,
UITextViewDelegate,
ConversationCollectionViewDelegate,
@ -2837,24 +2836,6 @@ typedef enum : NSUInteger {
[self showApprovalDialogForAttachment:attachment];
}
#pragma mark - OWSPhotoCaptureViewControllerDelegate
- (void)photoCaptureViewController:(OWSPhotoCaptureViewController *)photoCaptureViewController
didFinishProcessingAttachment:(SignalAttachment *)attachment
{
OWSLogDebug(@"");
[self dismissViewControllerAnimated:YES
completion:^{
[self showApprovalDialogForAttachment:attachment];
}];
}
- (void)photoCaptureViewControllerDidCancel:(OWSPhotoCaptureViewController *)photoCaptureViewController
{
OWSLogDebug(@"");
[self dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - UIImagePickerController
/*
@ -2877,19 +2858,8 @@ typedef enum : NSUInteger {
UIViewController *pickerModal;
if (SSKFeatureFlags.useCustomPhotoCapture) {
OWSPhotoCaptureViewController *captureVC = [OWSPhotoCaptureViewController new];
captureVC.delegate = self;
OWSNavigationController *navController =
[[OWSNavigationController alloc] initWithRootViewController:captureVC];
UINavigationBar *navigationBar = navController.navigationBar;
if (![navigationBar isKindOfClass:[OWSNavigationBar class]]) {
OWSFailDebug(@"navigationBar was nil or unexpected class");
} else {
OWSNavigationBar *owsNavigationBar = (OWSNavigationBar *)navigationBar;
[owsNavigationBar overrideThemeWithType:NavigationBarThemeOverrideClear];
}
navController.ows_prefersStatusBarHidden = @(YES);
SendMediaNavigationController *navController = [SendMediaNavigationController showingCameraFirst];
navController.sendMediaNavDelegate = self;
pickerModal = navController;
} else {
UIImagePickerController *picker = [OWSImagePickerController new];
@ -2933,11 +2903,8 @@ typedef enum : NSUInteger {
return;
}
OWSImagePickerGridController *picker = [OWSImagePickerGridController new];
picker.delegate = self;
OWSNavigationController *pickerModal = [[OWSNavigationController alloc] initWithRootViewController:picker];
pickerModal.ows_prefersStatusBarHidden = @(YES);
SendMediaNavigationController *pickerModal = [SendMediaNavigationController showingMediaLibraryFirst];
pickerModal.sendMediaNavDelegate = self;
[self dismissKeyBoard];
[self presentViewController:pickerModal animated:YES completion:nil];
@ -2960,13 +2927,19 @@ typedef enum : NSUInteger {
self.view.frame = frame;
}
#pragma mark - OWSImagePickerControllerDelegate
#pragma mark - SendMediaNavDelegate
- (void)imagePicker:(OWSImagePickerGridController *)imagePicker
didPickImageAttachments:(NSArray<SignalAttachment *> *)attachments
messageText:(NSString *_Nullable)messageText
- (void)sendMediaNavDidCancel:(SendMediaNavigationController *)sendMediaNavigationController
{
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)sendMediaNav:(SendMediaNavigationController *)sendMediaNavigationController
didApproveAttachments:(NSArray<SignalAttachment *> *)attachments
messageText:(nullable NSString *)messageText
{
[self tryToSendAttachments:attachments messageText:messageText];
[self dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - UIImagePickerControllerDelegate
@ -3943,8 +3916,7 @@ typedef enum : NSUInteger {
[self scrollToBottomAnimated:NO];
}
- (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval
didCancelAttachments:(NSArray<SignalAttachment *> *)attachment
- (void)attachmentApprovalDidCancel:(AttachmentApprovalViewController *)attachmentApproval
{
[self dismissViewControllerAnimated:YES completion:nil];
}

@ -6,16 +6,19 @@ import Foundation
import Photos
import PromiseKit
@objc(OWSImagePickerControllerDelegate)
protocol ImagePickerControllerDelegate {
func imagePicker(_ imagePicker: ImagePickerGridController, didPickImageAttachments attachments: [SignalAttachment], messageText: String?)
protocol ImagePickerGridControllerDelegate: AnyObject {
func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController)
func imagePicker(_ imagePicker: ImagePickerGridController, isAssetSelected asset: PHAsset) -> Bool
func imagePicker(_ imagePicker: ImagePickerGridController, didSelectAsset asset: PHAsset, attachmentPromise: Promise<SignalAttachment>)
func imagePicker(_ imagePicker: ImagePickerGridController, didDeselectAsset asset: PHAsset)
func imagePickerCanSelectAdditionalItems(_ imagePicker: ImagePickerGridController) -> Bool
}
@objc(OWSImagePickerGridController)
class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate, PhotoCollectionPickerDelegate, AttachmentApprovalViewControllerDelegate {
class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate, PhotoCollectionPickerDelegate {
@objc
weak var delegate: ImagePickerControllerDelegate?
weak var delegate: ImagePickerGridControllerDelegate?
private let library: PhotoLibrary = PhotoLibrary()
private var photoCollection: PhotoCollection
@ -25,12 +28,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
var collectionViewFlowLayout: UICollectionViewFlowLayout
var titleView: TitleView!
// We use NSMutableOrderedSet so that we can honor selection order.
private let selectedIds = NSMutableOrderedSet()
// This variable should only be accessed on the main thread.
private var assetIdToCommentMap = [String: String]()
init() {
collectionViewFlowLayout = type(of: self).buildLayout()
photoCollection = library.defaultPhotoCollection()
@ -79,10 +76,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
navigationItem.titleView = titleView
self.titleView = titleView
let featureFlag_isMultiselectEnabled = true
if featureFlag_isMultiselectEnabled {
updateSelectButton()
}
updateSelectButton()
collectionView.backgroundColor = .ows_gray95
@ -109,6 +103,11 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
return
}
guard let delegate = delegate else {
owsFailDebug("delegate was unexpectedly nil")
return
}
switch selectionPanGesture.state {
case .possible:
break
@ -121,7 +120,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
return
}
let asset = photoCollectionContents.asset(at: indexPath.item)
if selectedIds.contains(asset.localIdentifier) {
if delegate.imagePicker(self, isAssetSelected: asset) {
selectionPanGestureMode = .deselect
} else {
selectionPanGestureMode = .select
@ -149,28 +148,30 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
return
}
guard let delegate = delegate else {
owsFailDebug("delegate was unexpectedly nil")
return
}
let asset = photoCollectionContents.asset(at: indexPath.item)
switch selectionPanGestureMode {
case .select:
guard canSelectAdditionalItems else {
guard delegate.imagePickerCanSelectAdditionalItems(self) else {
showTooManySelectedToast()
return
}
selectedIds.add(asset.localIdentifier)
let attachmentPromise: Promise<SignalAttachment> = photoCollectionContents.outgoingAttachment(for: asset)
delegate.imagePicker(self, didSelectAsset: asset, attachmentPromise: attachmentPromise)
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: [])
case .deselect:
selectedIds.remove(asset.localIdentifier)
delegate.imagePicker(self, didDeselectAsset: asset)
collectionView.deselectItem(at: indexPath, animated: true)
}
updateDoneButton()
}
var canSelectAdditionalItems: Bool {
return selectedIds.count <= SignalAttachment.maxAttachmentsAllowed
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
updateLayout()
@ -263,14 +264,18 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
return
}
guard let delegate = delegate else {
owsFailDebug("delegate was unexpectedly nil")
return
}
collectionView.reloadData()
collectionView.layoutIfNeeded()
let count = photoCollectionContents.assetCount
for index in 0..<count {
let asset = photoCollectionContents.asset(at: index)
let assetId = asset.localIdentifier
if selectedIds.contains(assetId) {
if delegate.imagePicker(self, isAssetSelected: asset) {
collectionView.selectItem(at: IndexPath(row: index, section: 0),
animated: false, scrollPosition: [])
}
@ -352,79 +357,15 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
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()
guard let delegate = delegate else {
owsFailDebug("delegate was unexpectedly nil")
return
}
}
private func didComplete(withAttachments attachments: [SignalAttachment]) {
AssertIsOnMainThread()
for attachment in attachments {
guard let assetId = attachment.assetId else {
owsFailDebug("Attachment is missing asset id.")
continue
}
// Link the attachment with its asset to ensure caption continuity.
attachment.assetId = assetId
// Restore any existing caption for this attachment.
attachment.captionText = assetIdToCommentMap[assetId]
}
hasPressedDoneSinceAppeared = true
updateDoneButton()
let vc = AttachmentApprovalViewController(mode: .sharedNavigation, attachments: attachments)
vc.approvalDelegate = self
navigationController?.pushViewController(vc, animated: true)
delegate.imagePickerDidCompleteSelection(self)
}
var hasPressedDoneSinceAppeared: Bool = false
@ -465,18 +406,13 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
self.doneButton.isEnabled = false
}
func deselectAnySelected() {
func clearCollectionViewSelection() {
guard let collectionView = self.collectionView else {
owsFailDebug("collectionView was unexpectedly nil")
return
}
selectedIds.removeAllObjects()
collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false)}
if isInBatchSelectMode {
updateDoneButton()
}
}
func showTooManySelectedToast() {
@ -577,7 +513,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
}
// Any selections are invalid as they refer to indices in a different collection
deselectAnySelected()
clearCollectionViewSelection()
photoCollection = collection
photoCollectionContents = photoCollection.contents()
@ -605,25 +541,33 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let asset = photoCollectionContents.asset(at: indexPath.item)
guard let delegate = delegate else {
owsFailDebug("delegate was unexpectedly nil")
return
}
let asset: PHAsset = photoCollectionContents.asset(at: indexPath.item)
let attachmentPromise: Promise<SignalAttachment> = photoCollectionContents.outgoingAttachment(for: asset)
delegate.imagePicker(self, didSelectAsset: asset, attachmentPromise: attachmentPromise)
if isInBatchSelectMode {
let assetId = asset.localIdentifier
selectedIds.add(assetId)
updateDoneButton()
} else {
// Don't show "selected" badge unless we're in batch mode
collectionView.deselectItem(at: indexPath, animated: false)
complete(withAssets: [asset])
delegate.imagePickerDidCompleteSelection(self)
}
}
public override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
Logger.debug("")
guard let delegate = delegate else {
owsFailDebug("delegate was unexpectedly nil")
return
}
let asset = photoCollectionContents.asset(at: indexPath.item)
let assetId = asset.localIdentifier
selectedIds.remove(assetId)
delegate.imagePicker(self, didDeselectAsset: asset)
if isInBatchSelectMode {
updateDoneButton()
@ -635,68 +579,26 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let delegate = delegate else {
return UICollectionViewCell(forAutoLayout: ())
}
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoGridViewCell.reuseIdentifier, for: indexPath) as? PhotoGridViewCell else {
owsFail("cell was unexpectedly nil")
}
cell.loadingColor = UIColor(white: 0.2, alpha: 1)
let assetItem = photoCollectionContents.assetItem(at: indexPath.item, photoMediaSize: photoMediaSize)
cell.configure(item: assetItem)
let assetId = assetItem.asset.localIdentifier
let isSelected = selectedIds.contains(assetId)
cell.isSelected = isSelected
return cell
}
// MARK: - AttachmentApprovalViewControllerDelegate
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) {
self.dismiss(animated: true) {
self.delegate?.imagePicker(self, didPickImageAttachments: attachments, messageText: messageText)
}
}
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didCancelAttachments attachments: [SignalAttachment]) {
navigationController?.popToViewController(self, animated: true)
}
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, addMoreToAttachments attachments: [SignalAttachment]) {
// If we re-enter image picking via "add more" button, do so in batch mode.
isInBatchSelectMode = true
// clear selection
deselectAnySelected()
// removing-and-readding accomplishes two things
// 1. respect items removed from the rail while in the approval view
// 2. in the case of the user adding more to what was a single item
// which was not selected in batch mode, ensure that item is now
// part of the "batch selection"
for previouslySelected in attachments {
guard let assetId = previouslySelected.assetId else {
owsFailDebug("assetId was unexpectedly nil")
continue
}
selectedIds.add(assetId as Any)
let isSelected = delegate.imagePicker(self, isAssetSelected: assetItem.asset)
if isSelected {
cell.isSelected = isSelected
} else {
cell.isSelected = isSelected
}
navigationController?.popToViewController(self, animated: true)
}
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, changedCaptionOfAttachment attachment: SignalAttachment) {
AssertIsOnMainThread()
guard let assetId = attachment.assetId else {
owsFailDebug("Attachment missing source id.")
return
}
guard let captionText = attachment.captionText, captionText.count > 0 else {
assetIdToCommentMap.removeValue(forKey: assetId)
return
}
assetIdToCommentMap[assetId] = captionText
return cell
}
}

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

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

@ -0,0 +1,295 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
import Photos
import PromiseKit
@objc
protocol SendMediaNavDelegate: AnyObject {
func sendMediaNavDidCancel(_ sendMediaNavigationController: SendMediaNavigationController)
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], messageText: String?)
}
@objc
class SendMediaNavigationController: OWSNavigationController {
// MARK: - Overrides
override var prefersStatusBarHidden: Bool { return true }
// MARK: -
@objc
public weak var sendMediaNavDelegate: SendMediaNavDelegate?
@objc
public class func showingCameraFirst() -> SendMediaNavigationController {
let navController = SendMediaNavigationController()
if let owsNavBar = navController.navigationBar as? OWSNavigationBar {
owsNavBar.overrideTheme(type: .clear)
} else {
owsFailDebug("unexpected navbar: \(navController.navigationBar)")
}
navController.setViewControllers([navController.captureViewController], animated: false)
return navController
}
@objc
public class func showingMediaLibraryFirst() -> SendMediaNavigationController {
let navController = SendMediaNavigationController()
if let owsNavBar = navController.navigationBar as? OWSNavigationBar {
owsNavBar.overrideTheme(type: .clear)
} else {
owsFailDebug("unexpected navbar: \(navController.navigationBar)")
}
navController.setViewControllers([navController.mediaLibraryViewController], animated: false)
return navController
}
// MARK:
private var attachmentDraftCollection: AttachmentDraftCollection = .empty
private var attachments: [SignalAttachment] {
return attachmentDraftCollection.attachmentDrafts.map { $0.attachment }
}
private let mediaLibrarySelections: OrderedDictionary<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)
}
}
extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate {
func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didFinishProcessingAttachment attachment: SignalAttachment) {
attachmentDraftCollection.append(.camera(attachment: attachment))
pushApprovalViewController()
}
func photoCaptureViewControllerDidCancel(_ photoCaptureViewController: PhotoCaptureViewController) {
// TODO
// sometimes we might want this to be a "back" to the approval view
// other times we might want this to be a "close" and take me back to the CVC
// seems like we should show the "back" and have a seprate "didTapBack" delegate method or something...
self.sendMediaNavDelegate?.sendMediaNavDidCancel(self)
}
}
extension SendMediaNavigationController: ImagePickerGridControllerDelegate {
func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController) {
let mediaLibrarySelections: [MediaLibrarySelection] = self.mediaLibrarySelections.orderedValues
let backgroundBlock: (ModalActivityIndicatorViewController) -> Void = { modal in
let attachmentPromises: [Promise<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)
}
func imagePicker(_ imagePicker: ImagePickerGridController, didDeselectAsset asset: PHAsset) {
if mediaLibrarySelections.hasValue(forKey: asset) {
mediaLibrarySelections.remove(key: asset)
}
}
func imagePickerCanSelectAdditionalItems(_ imagePicker: ImagePickerGridController) -> Bool {
return attachmentDraftCollection.count <= SignalAttachment.maxAttachmentsAllowed
}
}
extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegate {
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) {
guard let removedDraft = attachmentDraftCollection.attachmentDrafts.first(where: { $0.attachment == attachment}) else {
owsFailDebug("removedDraft was unexpectedly nil")
return
}
switch removedDraft.source {
case .picker(attachment: let pickerAttachment):
mediaLibrarySelections.remove(key: pickerAttachment.asset)
case .camera(attachment: _):
break
}
attachmentDraftCollection.remove(attachment: attachment)
}
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) {
sendMediaNavDelegate?.sendMediaNav(self, didApproveAttachments: attachments, messageText: messageText)
}
func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) {
sendMediaNavDelegate?.sendMediaNavDidCancel(self)
}
func attachmentApprovalDidTapAddMore(_ attachmentApproval: AttachmentApprovalViewController) {
// Current design dicates we'll go "back" to the single thing before us.
assert(viewControllers.count == 2)
// regardless of which VC we're going "back" to, we're in "batch" mode at this point.
mediaLibraryViewController.isInBatchSelectMode = true
mediaLibraryViewController.collectionView?.reloadData()
popViewController(animated: true)
}
}
enum AttachmentDraft {
case camera(attachment: SignalAttachment)
case picker(attachment: MediaLibraryAttachment)
}
extension AttachmentDraft {
var attachment: SignalAttachment {
switch self {
case .camera(let cameraAttachment):
return cameraAttachment
case .picker(let pickerAttachment):
return pickerAttachment.signalAttachment
}
}
var source: AttachmentDraft {
return self
}
}
struct AttachmentDraftCollection {
private(set) var attachmentDrafts: [AttachmentDraft]
static var empty: AttachmentDraftCollection {
return AttachmentDraftCollection(attachmentDrafts: [])
}
// MARK -
var count: Int {
return attachmentDrafts.count
}
var pickerAttachments: [MediaLibraryAttachment] {
return attachmentDrafts.compactMap { attachmentDraft in
switch attachmentDraft.source {
case .picker(let pickerAttachment):
return pickerAttachment
case .camera:
return nil
}
}
}
mutating func append(_ element: AttachmentDraft) {
attachmentDrafts.append(element)
}
mutating func remove(attachment: SignalAttachment) {
attachmentDrafts = attachmentDrafts.filter { $0.attachment != attachment }
}
mutating func selectedFromPicker(attachments: [MediaLibraryAttachment]) {
let pickedAttachments: Set<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))
}
}
}
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
}
}
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
}
}

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

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

@ -30,6 +30,10 @@ public class OrderedDictionary<KeyType: Hashable, ValueType> {
return keyValueMap[key]
}
public func hasValue(forKey key: KeyType) -> Bool {
return keyValueMap[key] != nil
}
public func append(key: KeyType, value: ValueType) {
if keyValueMap[key] != nil {
owsFailDebug("Unexpected duplicate key in key map: \(key)")

@ -160,10 +160,6 @@ public class SignalAttachment: NSObject {
@objc
public let dataUTI: String
// Can be used by views to link this SignalAttachment with an Photos framework asset.
@objc
public var assetId: String?
var error: SignalAttachmentError? {
didSet {
AssertIsOnMainThread()

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

Loading…
Cancel
Save