diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index ac8d0d71b..f76adcc04 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -2681,9 +2681,7 @@ typedef enum : NSUInteger { - (void)imagePicker:(OWSImagePickerGridController *)imagePicker didPickImageAttachments:(NSArray *)attachments { - // TODO support approving multiple attachments. - SignalAttachment *firstAttachment = attachments.firstObject; - [self tryToSendAttachmentIfApproved:firstAttachment]; + [self tryToSendAttachmentsIfApproved:attachments]; } /* @@ -2770,22 +2768,21 @@ typedef enum : NSUInteger { } } -- (void)sendMessageAttachment:(SignalAttachment *)attachment +- (void)sendMessageAttachments:(NSArray *)attachments { OWSAssertIsOnMainThread(); - // TODO: Should we assume non-nil or should we check for non-nil? - OWSAssertDebug(attachment != nil); - OWSAssertDebug(![attachment hasError]); - OWSAssertDebug([attachment mimeType].length > 0); - - OWSLogVerbose(@"Sending attachment. Size in bytes: %lu, contentType: %@", - (unsigned long)[attachment dataLength], - [attachment mimeType]); + for (SignalAttachment *attachment in attachments) { + // TODO: Should we assume non-nil or should we check for non-nil? + OWSAssertDebug(attachment != nil); + OWSAssertDebug(![attachment hasError]); + OWSAssertDebug([attachment mimeType].length > 0); + } BOOL didAddToProfileWhitelist = [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread]; - TSOutgoingMessage *message = [ThreadUtil enqueueMessageWithAttachment:attachment - inThread:self.thread - quotedReplyModel:self.inputToolbar.quotedReply]; + TSOutgoingMessage *message = [ThreadUtil enqueueMessageWithAttachments:attachments + messageBody:nil + inThread:self.thread + quotedReplyModel:self.inputToolbar.quotedReply]; [self messageWasSent:message]; @@ -3447,11 +3444,32 @@ typedef enum : NSUInteger { - (void)tryToSendAttachmentIfApproved:(SignalAttachment *_Nullable)attachment { - [self tryToSendAttachmentIfApproved:attachment skipApprovalDialog:NO]; + if (attachment == nil) { + OWSLogWarn(@"Missing attachment"); + [self showErrorAlertForAttachment:nil]; + return; + } + [self tryToSendAttachmentsIfApproved:@[ attachment ]]; } - (void)tryToSendAttachmentIfApproved:(SignalAttachment *_Nullable)attachment skipApprovalDialog:(BOOL)skipApprovalDialog +{ + if (attachment == nil) { + OWSLogWarn(@"Missing attachment"); + [self showErrorAlertForAttachment:nil]; + return; + } + [self tryToSendAttachmentsIfApproved:@[ attachment ] skipApprovalDialog:skipApprovalDialog]; +} + +- (void)tryToSendAttachmentsIfApproved:(NSArray *)attachments +{ + [self tryToSendAttachmentsIfApproved:attachments skipApprovalDialog:NO]; +} + +- (void)tryToSendAttachmentsIfApproved:(NSArray *)attachments + skipApprovalDialog:(BOOL)skipApprovalDialog { OWSLogError(@""); @@ -3460,7 +3478,7 @@ typedef enum : NSUInteger { if ([self isBlockedConversation]) { [self showUnblockConversationUI:^(BOOL isBlocked) { if (!isBlocked) { - [weakSelf tryToSendAttachmentIfApproved:attachment]; + [weakSelf tryToSendAttachmentsIfApproved:attachments]; } }]; return; @@ -3471,21 +3489,27 @@ typedef enum : NSUInteger { completion:^(BOOL didConfirmIdentity) { if (didConfirmIdentity) { [weakSelf - tryToSendAttachmentIfApproved:attachment]; + tryToSendAttachmentsIfApproved:attachments]; } }]; if (didShowSNAlert) { return; } - if (attachment == nil || [attachment hasError]) { - OWSLogWarn(@"Invalid attachment: %@.", attachment ? [attachment errorName] : @"Missing data"); - [self showErrorAlertForAttachment:attachment]; - } else if (skipApprovalDialog) { - [self sendMessageAttachment:attachment]; + for (SignalAttachment *attachment in attachments) { + if ([attachment hasError]) { + OWSLogWarn(@"Invalid attachment: %@.", attachment ? [attachment errorName] : @"Missing data"); + [self showErrorAlertForAttachment:attachment]; + return; + } + } + + if (skipApprovalDialog) { + [self sendMessageAttachments:attachments]; } else { OWSNavigationController *modal = - [AttachmentApprovalViewController wrappedInNavControllerWithAttachment:attachment delegate:self]; + [AttachmentApprovalViewController wrappedInNavControllerWithAttachments:attachments + approvalDelegate:self]; [self presentViewController:modal animated:YES completion:nil]; } }); @@ -3595,9 +3619,10 @@ typedef enum : NSUInteger { [self updateNavigationBarSubtitleLabel]; } -- (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval didApproveAttachment:(SignalAttachment * _Nonnull)attachment +- (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval + didApproveAttachments:(NSArray *)attachments { - [self sendMessageAttachment:attachment]; + [self sendMessageAttachments:attachments]; [self dismissViewControllerAnimated:YES completion:nil]; // We always want to scroll to the bottom of the conversation after the local user // sends a message. Normally, this is taken care of in yapDatabaseModified:, but @@ -3606,7 +3631,8 @@ typedef enum : NSUInteger { [self scrollToBottomAnimated:YES]; } -- (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval didCancelAttachment:(SignalAttachment * _Nonnull)attachment +- (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval + didCancelAttachments:(NSArray *)attachment { [self dismissViewControllerAnimated:YES completion:nil]; } diff --git a/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift b/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift index 036241a5d..07178ee95 100644 --- a/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift +++ b/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift @@ -8,31 +8,22 @@ import MediaPlayer @objc public protocol AttachmentApprovalViewControllerDelegate: class { - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachment attachment: SignalAttachment) - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didCancelAttachment attachment: SignalAttachment) + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment]) + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didCancelAttachments attachments: [SignalAttachment]) } -@objc -public class AttachmentApprovalViewController: OWSViewController, CaptioningToolbarDelegate, PlayerProgressBarDelegate, OWSVideoPlayerDelegate { - - weak var delegate: AttachmentApprovalViewControllerDelegate? +struct SignalAttachmentItem: Hashable { + let attachment: SignalAttachment +} - // We sometimes shrink the attachment view so that it remains somewhat visible - // when the keyboard is presented. - enum AttachmentViewScale { - case fullsize, compact - } +@objc +public class AttachmentApprovalViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, CaptioningToolbarDelegate { // MARK: Properties - let attachment: SignalAttachment - private var videoPlayer: OWSVideoPlayer? + weak var approvalDelegate: AttachmentApprovalViewControllerDelegate? - private(set) var bottomToolbar: UIView! - private(set) var mediaMessageView: MediaMessageView! - private(set) var scrollView: UIScrollView! - private(set) var contentContainer: UIView! - private(set) var playVideoButton: UIView? + private(set) var captioningToolbar: CaptioningToolbar! // MARK: Initializers @@ -41,29 +32,23 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool notImplemented() } - @objc - required public init(attachment: SignalAttachment, delegate: AttachmentApprovalViewControllerDelegate) { - assert(!attachment.hasError) - self.attachment = attachment - self.delegate = delegate - - super.init(nibName: nil, bundle: nil) - } - - // MARK: View Lifecycle - - override public func viewDidLoad() { - super.viewDidLoad() - self.navigationItem.title = nil + let kSpacingBetweenItems: CGFloat = 20 - let cancelButton = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(cancelPressed)) - cancelButton.tintColor = .white - self.navigationItem.leftBarButtonItem = cancelButton + @objc + required public init(attachments: [SignalAttachment]) { + assert(attachments.count > 0) + self.attachmentItems = attachments.map { SignalAttachmentItem(attachment: $0 )} + super.init(transitionStyle: .scroll, + navigationOrientation: .horizontal, + options: [UIPageViewControllerOptionInterPageSpacingKey: kSpacingBetweenItems]) + self.dataSource = self + self.delegate = self } @objc - public class func wrappedInNavController(attachment: SignalAttachment, delegate: AttachmentApprovalViewControllerDelegate) -> OWSNavigationController { - let vc = AttachmentApprovalViewController(attachment: attachment, delegate: delegate) + public class func wrappedInNavController(attachments: [SignalAttachment], approvalDelegate: AttachmentApprovalViewControllerDelegate) -> OWSNavigationController { + let vc = AttachmentApprovalViewController(attachments: attachments) + vc.approvalDelegate = approvalDelegate let navController = OWSNavigationController(rootViewController: vc) guard let navigationBar = navController.navigationBar as? OWSNavigationBar else { @@ -75,12 +60,35 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool return navController } - override public func viewWillLayoutSubviews() { - Logger.debug("") - super.viewWillLayoutSubviews() + // MARK: View Lifecycle - // e.g. if flipping to/from landscape - updateMinZoomScaleForSize(view.bounds.size) + override public func viewDidLoad() { + super.viewDidLoad() + + self.view.backgroundColor = .black + + disablePagingIfNecessary() + + // Bottom Toolbar + + let captioningToolbar = CaptioningToolbar() + captioningToolbar.captioningToolbarDelegate = self + self.captioningToolbar = captioningToolbar + + // Navigation + + self.navigationItem.title = nil + + let cancelButton = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(cancelPressed)) + cancelButton.tintColor = .white + self.navigationItem.leftBarButtonItem = cancelButton + + guard let firstItem = attachmentItems.first else { + owsFailDebug("firstItem was unexpectedly nil") + return + } + + self.setCurrentItem(firstItem, direction: .forward, animated: false) } override public func viewWillAppear(_ animated: Bool) { @@ -105,10 +113,277 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool CurrentAppContext().setStatusBarHidden(false, animated: false) } - // MARK: - Create Views + override public var inputAccessoryView: UIView? { + self.captioningToolbar.layoutIfNeeded() + return self.captioningToolbar + } + + override public var canBecomeFirstResponder: Bool { + return true + } + + // MARK: - View Helpers - public override func loadView() { + var pagerScrollView: UIScrollView? + // This is kind of a hack. Since we don't have first class access to the superview's `scrollView` + // we traverse the view hierarchy until we find it, then disable scrolling if there's only one + // item. This avoids an unpleasant "bounce" which doesn't make sense in the context of a single item. + fileprivate func disablePagingIfNecessary() { + for view in self.view.subviews { + if let pagerScrollView = view as? UIScrollView { + self.pagerScrollView = pagerScrollView + break + } + } + guard let pagerScrollView = self.pagerScrollView else { + owsFailDebug("pagerScrollView was unexpectedly nil") + return + } + + pagerScrollView.isScrollEnabled = attachmentItems.count > 1 + } + + private func makeClearToolbar() -> UIToolbar { + let toolbar = UIToolbar() + + toolbar.backgroundColor = UIColor.clear + + // Making a toolbar transparent requires setting an empty uiimage + toolbar.setBackgroundImage(UIImage(), forToolbarPosition: .any, barMetrics: .default) + + // hide 1px top-border + toolbar.clipsToBounds = true + + return toolbar + } + + // MARK: UIPageViewControllerDelegate + + public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { + Logger.debug("") + + assert(pendingViewControllers.count == 1) + pendingViewControllers.forEach { viewController in + guard let pendingPage = viewController as? AttachmentPrepViewController else { + owsFailDebug("unexpected viewController: \(viewController)") + return + } + + // use compact scale when keyboard is popped. + let scale: AttachmentPrepViewController.AttachmentViewScale = self.isFirstResponder ? .fullsize : .compact + pendingPage.setAttachmentViewScale(scale, animated: false) + } + } + + public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted: Bool) { + Logger.debug("") + + assert(previousViewControllers.count == 1) + previousViewControllers.forEach { viewController in + guard let previousPage = viewController as? AttachmentPrepViewController else { + owsFailDebug("unexpected viewController: \(viewController)") + return + } + + if transitionCompleted { + UIView.transition(with: self.captioningToolbar, + duration: 0.1, + options: .transitionCrossDissolve, + animations: { + self.captioningToolbar.captionText = self.currentViewController.attachment.captionText + }, + completion: nil) + previousPage.zoomOut(animated: false) + } + } + } + + // MARK: UIPageViewControllerDataSource + + public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { + guard let currentViewController = viewController as? AttachmentPrepViewController else { + owsFailDebug("unexpected viewController: \(viewController)") + return nil + } + + let currentItem = currentViewController.attachmentItem + guard let previousItem = attachmentItem(before: currentItem) else { + return nil + } + + guard let previousPage: AttachmentPrepViewController = buildPage(item: previousItem) else { + return nil + } + + return previousPage + } + + public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { + Logger.debug("") + + guard let currentViewController = viewController as? AttachmentPrepViewController else { + owsFailDebug("unexpected viewController: \(viewController)") + return nil + } + + let currentItem = currentViewController.attachmentItem + guard let nextItem = attachmentItem(after: currentItem) else { + return nil + } + + guard let nextPage: AttachmentPrepViewController = buildPage(item: nextItem) else { + return nil + } + + return nextPage + } + + public var currentViewController: AttachmentPrepViewController { + return viewControllers!.first as! AttachmentPrepViewController + } + + var currentItem: SignalAttachmentItem! { + get { + return currentViewController.attachmentItem + } + set { + setCurrentItem(newValue, direction: .forward, animated: false) + } + } + + private var cachedPages: [SignalAttachmentItem: AttachmentPrepViewController] = [:] + private func buildPage(item: SignalAttachmentItem) -> AttachmentPrepViewController? { + + if let cachedPage = cachedPages[item] { + Logger.debug("cache hit.") + return cachedPage + } + + Logger.debug("cache miss.") + let viewController = AttachmentPrepViewController(attachmentItem: item) + cachedPages[item] = viewController + + return viewController + } + + private func setCurrentItem(_ item: SignalAttachmentItem, direction: UIPageViewControllerNavigationDirection, animated isAnimated: Bool) { + guard let page = self.buildPage(item: item) else { + owsFailDebug("unexpetedly unable to build new page") + return + } + + self.setViewControllers([page], direction: direction, animated: isAnimated, completion: nil) + // TODO update rail + } + + let attachmentItems: [SignalAttachmentItem] + var attachments: [SignalAttachment] { + return attachmentItems.map { $0.attachment } + } + + func attachmentItem(before currentItem: SignalAttachmentItem) -> SignalAttachmentItem? { + guard let currentIndex = attachmentItems.index(of: currentItem) else { + owsFailDebug("currentIndex was unexpectedly nil") + return nil + } + + let index: Int = attachmentItems.index(before: currentIndex) + guard let previousItem = attachmentItems[safe: index] else { + // already at first item + return nil + } + + return previousItem + } + + func attachmentItem(after currentItem: SignalAttachmentItem) -> SignalAttachmentItem? { + guard let currentIndex = attachmentItems.index(of: currentItem) else { + owsFailDebug("currentIndex was unexpectedly nil") + return nil + } + + let index: Int = attachmentItems.index(after: currentIndex) + guard let nextItem = attachmentItems[safe: index] else { + // already at last item + return nil + } + + return nextItem + } + + // MARK: - Event Handlers + + @objc func cancelPressed(sender: UIButton) { + self.approvalDelegate?.attachmentApproval(self, didCancelAttachments: attachments) + } + + // MARK: CaptioningToolbarDelegate + + var currentPageController: AttachmentPrepViewController { + return viewControllers!.first as! AttachmentPrepViewController + } + + func captioningToolbarDidBeginEditing(_ captioningToolbar: CaptioningToolbar) { + currentPageController.setAttachmentViewScale(.compact, animated: true) + } + + func captioningToolbarDidEndEditing(_ captioningToolbar: CaptioningToolbar) { + currentPageController.setAttachmentViewScale(.fullsize, animated: true) + } + + func captioningToolbarDidTapSend(_ captioningToolbar: CaptioningToolbar) { + // Toolbar flickers in and out if there are errors + // and remains visible momentarily after share extension is dismissed. + // It's easiest to just hide it at this point since we're done with it. + currentViewController.shouldAllowAttachmentViewResizing = false + captioningToolbar.isUserInteractionEnabled = false + captioningToolbar.isHidden = true + + approvalDelegate?.attachmentApproval(self, didApproveAttachments: attachments) + } + + func captioningToolbar(_ captioningToolbar: CaptioningToolbar, textViewDidChange textView: UITextView) { + currentItem.attachment.captionText = textView.text + } +} + +public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarDelegate, OWSVideoPlayerDelegate { + // We sometimes shrink the attachment view so that it remains somewhat visible + // when the keyboard is presented. + enum AttachmentViewScale { + case fullsize, compact + } + + // MARK: Properties + + let attachmentItem: SignalAttachmentItem + var attachment: SignalAttachment { + return attachmentItem.attachment + } + + private var videoPlayer: OWSVideoPlayer? + + private(set) var mediaMessageView: MediaMessageView! + private(set) var scrollView: UIScrollView! + private(set) var contentContainer: UIView! + private(set) var playVideoButton: UIView? + + // MARK: Initializers + + init(attachmentItem: SignalAttachmentItem) { + self.attachmentItem = attachmentItem + super.init(nibName: nil, bundle: nil) + assert(!attachment.hasError) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: View Lifecycle + + override public func loadView() { self.view = UIView() self.mediaMessageView = MediaMessageView(attachment: attachment, mode: .attachmentApproval) @@ -160,11 +435,6 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool topGradient.autoSetDimension(.height, toSize: ScaleFromIPhone5(60)) } - // Bottom Toolbar - let captioningToolbar = CaptioningToolbar() - captioningToolbar.captioningToolbarDelegate = self - self.bottomToolbar = captioningToolbar - // Hide the play button embedded in the MediaView and replace it with our own. // This allows us to zoom in on the media view without zooming in on the button if attachment.isVideo { @@ -215,32 +485,21 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool } } - @objc public func didTapPlayerView(_ gestureRecognizer: UIGestureRecognizer) { - assert(self.videoPlayer != nil) - self.pauseVideo() - } + override public func viewWillLayoutSubviews() { + Logger.debug("") + super.viewWillLayoutSubviews() - override public var inputAccessoryView: UIView? { - self.bottomToolbar.layoutIfNeeded() - return self.bottomToolbar - } + // e.g. if flipping to/from landscape + updateMinZoomScaleForSize(view.bounds.size) - override public var canBecomeFirstResponder: Bool { - return true + ensureAttachmentViewScale(animated: false) } - private func makeClearToolbar() -> UIToolbar { - let toolbar = UIToolbar() - - toolbar.backgroundColor = UIColor.clear - - // Making a toolbar transparent requires setting an empty uiimage - toolbar.setBackgroundImage(UIImage(), forToolbarPosition: .any, barMetrics: .default) - - // hide 1px top-border - toolbar.clipsToBounds = true + // MARK: - return toolbar + @objc public func didTapPlayerView(_ gestureRecognizer: UIGestureRecognizer) { + assert(self.videoPlayer != nil) + self.pauseVideo() } // MARK: - Event Handlers @@ -250,24 +509,6 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool self.playVideo() } - @objc func cancelPressed(sender: UIButton) { - self.delegate?.attachmentApproval(self, didCancelAttachment: attachment) - } - - // MARK: CaptioningToolbarDelegate - - func captioningToolbarDidBeginEditing(_ captioningToolbar: CaptioningToolbar) { - self.scaleAttachmentView(.compact) - } - - func captioningToolbarDidEndEditing(_ captioningToolbar: CaptioningToolbar) { - self.scaleAttachmentView(.fullsize) - } - - func captioningToolbarDidTapSend(_ captioningToolbar: CaptioningToolbar, captionText: String?) { - self.approveAttachment(captionText: captionText) - } - // MARK: Video private func playVideo() { @@ -351,39 +592,46 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool return attachment.isImage || attachment.isVideo } - private func approveAttachment(captionText: String?) { - // Toolbar flickers in and out if there are errors - // and remains visible momentarily after share extension is dismissed. - // It's easiest to just hide it at this point since we're done with it. - shouldAllowAttachmentViewResizing = false - bottomToolbar.isUserInteractionEnabled = false - bottomToolbar.isHidden = true - - attachment.captionText = captionText - delegate?.attachmentApproval(self, didApproveAttachment: attachment) + func zoomOut(animated: Bool) { + if self.scrollView.zoomScale != self.scrollView.minimumZoomScale { + self.scrollView.setZoomScale(self.scrollView.minimumZoomScale, animated: animated) + } } // When the keyboard is popped, it can obscure the attachment view. // so we sometimes allow resizing the attachment. - private var shouldAllowAttachmentViewResizing: Bool = true + var shouldAllowAttachmentViewResizing: Bool = true + + var attachmentViewScale: AttachmentViewScale = .fullsize + fileprivate func setAttachmentViewScale(_ attachmentViewScale: AttachmentViewScale, animated: Bool) { + self.attachmentViewScale = attachmentViewScale + ensureAttachmentViewScale(animated: animated) + } - private func scaleAttachmentView(_ fit: AttachmentViewScale) { + func ensureAttachmentViewScale(animated: Bool) { + let animationDuration = animated ? 0.2 : 0 guard shouldAllowAttachmentViewResizing else { if self.contentContainer.transform != CGAffineTransform.identity { - UIView.animate(withDuration: 0.2) { + UIView.animate(withDuration: animationDuration) { self.contentContainer.transform = CGAffineTransform.identity } } return } - switch fit { + switch attachmentViewScale { case .fullsize: - UIView.animate(withDuration: 0.2) { + guard self.contentContainer.transform != .identity else { + return + } + UIView.animate(withDuration: animationDuration) { self.contentContainer.transform = CGAffineTransform.identity } case .compact: - UIView.animate(withDuration: 0.2) { + guard self.contentContainer.transform == .identity else { + return + } + UIView.animate(withDuration: animationDuration) { let kScaleFactor: CGFloat = 0.7 let scale = CGAffineTransform(scaleX: kScaleFactor, y: kScaleFactor) @@ -400,7 +648,7 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool } } -extension AttachmentApprovalViewController: UIScrollViewDelegate { +extension AttachmentPrepViewController: UIScrollViewDelegate { public func viewForZooming(in scrollView: UIScrollView) -> UIView? { if isZoomable { @@ -469,9 +717,10 @@ extension AttachmentApprovalViewController: UIScrollViewDelegate { } protocol CaptioningToolbarDelegate: class { - func captioningToolbarDidTapSend(_ captioningToolbar: CaptioningToolbar, captionText: String?) + func captioningToolbarDidTapSend(_ captioningToolbar: CaptioningToolbar) func captioningToolbarDidBeginEditing(_ captioningToolbar: CaptioningToolbar) func captioningToolbarDidEndEditing(_ captioningToolbar: CaptioningToolbar) + func captioningToolbar(_ captioningToolbar: CaptioningToolbar, textViewDidChange: UITextView) } class CaptioningToolbar: UIView, UITextViewDelegate { @@ -479,6 +728,12 @@ class CaptioningToolbar: UIView, UITextViewDelegate { weak var captioningToolbarDelegate: CaptioningToolbarDelegate? private let sendButton: UIButton private let textView: UITextView + + var captionText: String? { + get { return self.textView.text } + set { self.textView.text = newValue } + } + private let bottomGradient: GradientView private let lengthLimitLabel: UILabel @@ -493,10 +748,6 @@ class CaptioningToolbar: UIView, UITextViewDelegate { var textViewHeightConstraint: NSLayoutConstraint! var textViewHeight: CGFloat - required init?(coder aDecoder: NSCoder) { - notImplemented() - } - class MessageTextView: UITextView { // When creating new lines, contentOffset is animated, but because because // we are simultaneously resizing the text view, this can cause the @@ -515,6 +766,8 @@ class CaptioningToolbar: UIView, UITextViewDelegate { } } + // MARK: Initializers + init() { self.sendButton = UIButton(type: .system) self.bottomGradient = GradientView(from: UIColor.clear, to: UIColor.black) @@ -626,14 +879,21 @@ class CaptioningToolbar: UIView, UITextViewDelegate { bottomGradient.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top) } + required init?(coder aDecoder: NSCoder) { + notImplemented() + } + + // MARK: + @objc func didTapSend() { - self.captioningToolbarDelegate?.captioningToolbarDidTapSend(self, captionText: self.textView.text) + self.captioningToolbarDelegate?.captioningToolbarDidTapSend(self) } // MARK: - UITextViewDelegate public func textViewDidChange(_ textView: UITextView) { updateHeight(textView: textView) + self.captioningToolbarDelegate?.captioningToolbar(self, textViewDidChange: textView) } public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { diff --git a/SignalMessaging/ViewControllers/SharingThreadPickerViewController.m b/SignalMessaging/ViewControllers/SharingThreadPickerViewController.m index f4fcb51a6..07cf5b808 100644 --- a/SignalMessaging/ViewControllers/SharingThreadPickerViewController.m +++ b/SignalMessaging/ViewControllers/SharingThreadPickerViewController.m @@ -163,8 +163,10 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion); [self.navigationController pushViewController:approvalVC animated:YES]; } else { + // TODO ALBUMS - send album via SAE OWSNavigationController *approvalModal = - [AttachmentApprovalViewController wrappedInNavControllerWithAttachment:self.attachment delegate:self]; + [AttachmentApprovalViewController wrappedInNavControllerWithAttachments:@[ self.attachment ] + approvalDelegate:self]; [self presentViewController:approvalModal animated:YES completion:nil]; } } @@ -229,7 +231,7 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion); #pragma mark - AttachmentApprovalViewControllerDelegate - (void)attachmentApproval:(AttachmentApprovalViewController *)approvalViewController - didApproveAttachment:(SignalAttachment *)attachment + didApproveAttachments:(NSArray *)attachments { [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread]; [self tryToSendMessageWithBlock:^(SendCompletionBlock sendCompletion) { @@ -239,7 +241,8 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion); // DURABLE CLEANUP - SAE uses non-durable sending to make sure the app is running long enough to complete // the sending operation. Alternatively, we could use a durable send, but do more to make sure the // SAE runs as long as it needs. - outgoingMessage = [ThreadUtil sendMessageNonDurablyWithAttachment:attachment + // TODO ALBUMS - send album via SAE + outgoingMessage = [ThreadUtil sendMessageNonDurablyWithAttachment:attachments.firstObject inThread:self.thread quotedReplyModel:nil messageSender:self.messageSender @@ -254,7 +257,7 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion); } - (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval - didCancelAttachment:(SignalAttachment *)attachment + didCancelAttachments:(NSArray *)attachment { [self cancelShareExperience]; }