diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 93bec0d0a..7e6aefde9 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -2481,7 +2481,7 @@ typedef enum : NSUInteger { { OWSAssertDebug(attachment); - [self tryToSendAttachmentIfApproved:attachment]; + [self showApprovalDialogForAttachment:attachment]; [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread]; [self.conversationViewModel ensureDynamicInteractions]; @@ -2580,14 +2580,14 @@ typedef enum : NSUInteger { // Although we want to be able to send higher quality attachments through the document picker // it's more imporant that we ensure the sent format is one all clients can accept (e.g. *not* quicktime .mov) if ([SignalAttachment isInvalidVideoWithDataSource:dataSource dataUTI:type]) { - [self sendQualityAdjustedAttachmentForVideo:url filename:filename skipApprovalDialog:NO]; + [self showApprovalDialogAfterProcessingVideoURL:url filename:filename]; return; } // "Document picker" attachments _SHOULD NOT_ be resized, if possible. SignalAttachment *attachment = [SignalAttachment attachmentWithDataSource:dataSource dataUTI:type imageQuality:TSImageQualityOriginal]; - [self tryToSendAttachmentIfApproved:attachment]; + [self showApprovalDialogForAttachment:attachment]; } #pragma mark - UIImagePickerController @@ -2673,12 +2673,17 @@ typedef enum : NSUInteger { self.view.frame = frame; } +#pragma mark - OWSImagePickerControllerDelegate + - (void)imagePicker:(OWSImagePickerGridController *)imagePicker didPickImageAttachments:(NSArray *)attachments + messageText:(NSString *_Nullable)messageText { - [self tryToSendAttachmentsIfApproved:attachments skipApprovalDialog:YES]; + [self tryToSendAttachments:attachments messageText:messageText]; } +#pragma mark - UIImagePickerControllerDelegate + /* * Fetching data from UIImagePickerController */ @@ -2725,9 +2730,7 @@ typedef enum : NSUInteger { NSURL *videoURL = info[UIImagePickerControllerMediaURL]; [self dismissViewControllerAnimated:YES completion:^{ - [self sendQualityAdjustedAttachmentForVideo:videoURL - filename:filename - skipApprovalDialog:NO]; + [self showApprovalDialogAfterProcessingVideoURL:videoURL filename:filename]; }]; } else if (picker.sourceType == UIImagePickerControllerSourceTypeCamera) { // Static Image captured from camera @@ -2751,7 +2754,7 @@ typedef enum : NSUInteger { [self showErrorAlertForAttachment:attachment]; failedToPickAttachment(nil); } else { - [self tryToSendAttachmentIfApproved:attachment skipApprovalDialog:NO]; + [self showApprovalDialogForAttachment:attachment]; } } else { failedToPickAttachment(nil); @@ -2808,34 +2811,13 @@ typedef enum : NSUInteger { [self showErrorAlertForAttachment:attachment]; failedToPickAttachment(nil); } else { - [self tryToSendAttachmentIfApproved:attachment]; + [self showApprovalDialogForAttachment:attachment]; } }]; }]; } } -- (void)sendMessageAttachments:(NSArray *)attachments -{ - OWSAssertIsOnMainThread(); - for (SignalAttachment *attachment in attachments) { - OWSAssertDebug(![attachment hasError]); - OWSAssertDebug([attachment mimeType].length > 0); - } - - BOOL didAddToProfileWhitelist = [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread]; - TSOutgoingMessage *message = [ThreadUtil enqueueMessageWithAttachments:attachments - messageBody:nil - inThread:self.thread - quotedReplyModel:self.inputToolbar.quotedReply]; - - [self messageWasSent:message]; - - if (didAddToProfileWhitelist) { - [self.conversationViewModel ensureDynamicInteractions]; - } -} - - (void)sendContactShare:(ContactShareViewModel *)contactShare { OWSAssertIsOnMainThread(); @@ -2864,9 +2846,7 @@ typedef enum : NSUInteger { }]; } -- (void)sendQualityAdjustedAttachmentForVideo:(NSURL *)movieURL - filename:(nullable NSString *)filename - skipApprovalDialog:(BOOL)skipApprovalDialog +- (void)showApprovalDialogAfterProcessingVideoURL:(NSURL *)movieURL filename:(nullable NSString *)filename { OWSAssertIsOnMainThread(); @@ -2895,7 +2875,7 @@ typedef enum : NSUInteger { attachment ? [attachment errorName] : @"Missing data"); [self showErrorAlertForAttachment:attachment]; } else { - [self tryToSendAttachmentIfApproved:attachment skipApprovalDialog:skipApprovalDialog]; + [self showApprovalDialogForAttachment:attachment]; } }]; }) retainUntilComplete]; @@ -3086,7 +3066,7 @@ typedef enum : NSUInteger { OWSLogWarn(@"Invalid attachment: %@.", attachment ? [attachment errorName] : @"Missing data"); [self showErrorAlertForAttachment:attachment]; } else { - [self tryToSendAttachmentIfApproved:attachment skipApprovalDialog:YES]; + [self tryToSendAttachments:@[ attachment ] messageText:nil]; } } @@ -3480,37 +3460,28 @@ typedef enum : NSUInteger { { OWSLogError(@""); - [self tryToSendAttachmentIfApproved:attachment]; + [self showApprovalDialogForAttachment:attachment]; } -- (void)tryToSendAttachmentIfApproved:(SignalAttachment *_Nullable)attachment +- (void)showApprovalDialogForAttachment:(SignalAttachment *_Nullable)attachment { if (attachment == nil) { - OWSLogWarn(@"Missing attachment"); + OWSFailDebug(@"attachment was unexpectedly nil"); [self showErrorAlertForAttachment:nil]; return; } - [self tryToSendAttachmentsIfApproved:@[ attachment ]]; + [self showApprovalDialogForAttachments:@[ attachment ]]; } -- (void)tryToSendAttachmentIfApproved:(SignalAttachment *_Nullable)attachment - skipApprovalDialog:(BOOL)skipApprovalDialog +- (void)showApprovalDialogForAttachments:(NSArray *)attachments { - if (attachment == nil) { - OWSLogWarn(@"Missing attachment"); - [self showErrorAlertForAttachment:nil]; - return; - } - [self tryToSendAttachmentsIfApproved:@[ attachment ] skipApprovalDialog:skipApprovalDialog]; -} + OWSNavigationController *modal = + [AttachmentApprovalViewController wrappedInNavControllerWithAttachments:attachments approvalDelegate:self]; -- (void)tryToSendAttachmentsIfApproved:(NSArray *)attachments -{ - [self tryToSendAttachmentsIfApproved:attachments skipApprovalDialog:NO]; + [self presentViewController:modal animated:YES completion:nil]; } -- (void)tryToSendAttachmentsIfApproved:(NSArray *)attachments - skipApprovalDialog:(BOOL)skipApprovalDialog +- (void)tryToSendAttachments:(NSArray *)attachments messageText:(NSString *_Nullable)messageText { OWSLogError(@""); @@ -3519,7 +3490,7 @@ typedef enum : NSUInteger { if ([self isBlockedConversation]) { [self showUnblockConversationUI:^(BOOL isBlocked) { if (!isBlocked) { - [weakSelf tryToSendAttachmentsIfApproved:attachments]; + [weakSelf tryToSendAttachments:attachments messageText:messageText]; } }]; return; @@ -3529,8 +3500,8 @@ typedef enum : NSUInteger { showSafetyNumberConfirmationIfNecessaryWithConfirmationText:[SafetyNumberStrings confirmSendButton] completion:^(BOOL didConfirmIdentity) { if (didConfirmIdentity) { - [weakSelf - tryToSendAttachmentsIfApproved:attachments]; + [weakSelf tryToSendAttachments:attachments + messageText:messageText]; } }]; if (didShowSNAlert) { @@ -3545,13 +3516,16 @@ typedef enum : NSUInteger { } } - if (skipApprovalDialog) { - [self sendMessageAttachments:attachments]; - } else { - OWSNavigationController *modal = - [AttachmentApprovalViewController wrappedInNavControllerWithAttachments:attachments - approvalDelegate:self]; - [self presentViewController:modal animated:YES completion:nil]; + BOOL didAddToProfileWhitelist = [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread]; + TSOutgoingMessage *message = [ThreadUtil enqueueMessageWithAttachments:attachments + messageBody:messageText + inThread:self.thread + quotedReplyModel:self.inputToolbar.quotedReply]; + + [self messageWasSent:message]; + + if (didAddToProfileWhitelist) { + [self.conversationViewModel ensureDynamicInteractions]; } }); } @@ -3660,10 +3634,13 @@ typedef enum : NSUInteger { [self updateNavigationBarSubtitleLabel]; } +#pragma mark - AttachmentApprovalViewControllerDelegate + - (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval didApproveAttachments:(NSArray *)attachments + messageText:(NSString *_Nullable)messageText { - [self sendMessageAttachments:attachments]; + [self tryToSendAttachments:attachments messageText:messageText]; [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 diff --git a/Signal/src/ViewControllers/MediaPageViewController.swift b/Signal/src/ViewControllers/MediaPageViewController.swift index d238a5a4d..02ac04b05 100644 --- a/Signal/src/ViewControllers/MediaPageViewController.swift +++ b/Signal/src/ViewControllers/MediaPageViewController.swift @@ -50,7 +50,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou public func setCurrentItem(_ item: MediaGalleryItem, direction: UIPageViewControllerNavigationDirection, animated isAnimated: Bool) { guard let galleryPage = self.buildGalleryPage(galleryItem: item) else { - owsFailDebug("unexpetedly unable to build new gallery page") + owsFailDebug("unexpectedly unable to build new gallery page") return } @@ -83,7 +83,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou self.delegate = self guard let initialPage = self.buildGalleryPage(galleryItem: initialItem) else { - owsFailDebug("unexpetedly unable to build initial gallery item") + owsFailDebug("unexpectedly unable to build initial gallery item") return } self.initialPage = initialPage diff --git a/Signal/src/ViewControllers/MediaTileViewController.swift b/Signal/src/ViewControllers/MediaTileViewController.swift index 2f9acb261..76db73209 100644 --- a/Signal/src/ViewControllers/MediaTileViewController.swift +++ b/Signal/src/ViewControllers/MediaTileViewController.swift @@ -616,7 +616,7 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryDa Logger.debug("with deletedSections: \(deletedSections) deletedItems: \(deletedItems)") guard let collectionView = self.collectionView else { - owsFailDebug("collectionView was unexpetedly nil") + owsFailDebug("collectionView was unexpectedly nil") return } diff --git a/Signal/src/ViewControllers/PhotoLibrary/ImagePickerController.swift b/Signal/src/ViewControllers/PhotoLibrary/ImagePickerController.swift index f48ea7c4a..f999fb84b 100644 --- a/Signal/src/ViewControllers/PhotoLibrary/ImagePickerController.swift +++ b/Signal/src/ViewControllers/PhotoLibrary/ImagePickerController.swift @@ -8,7 +8,7 @@ import PromiseKit @objc(OWSImagePickerControllerDelegate) protocol ImagePickerControllerDelegate { - func imagePicker(_ imagePicker: ImagePickerGridController, didPickImageAttachments attachments: [SignalAttachment]) + func imagePicker(_ imagePicker: ImagePickerGridController, didPickImageAttachments attachments: [SignalAttachment], messageText: String?) } @objc(OWSImagePickerGridController) @@ -386,9 +386,9 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat // MARK: - AttachmentApprovalViewControllerDelegate - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment]) { + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) { self.dismiss(animated: true) { - self.delegate?.imagePicker(self, didPickImageAttachments: attachments) + self.delegate?.imagePicker(self, didPickImageAttachments: attachments, messageText: messageText) } } diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index ad3d5bd83..69677fcf4 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -101,6 +101,9 @@ /* One-line label indicating the user can add no more text to the attachment caption. */ "ATTACHMENT_APPROVAL_CAPTION_LENGTH_LIMIT_REACHED" = "Message limit reached."; +/* placeholder text for an empty captioning field */ +"ATTACHMENT_APPROVAL_CAPTION_PLACEHOLDER" = "Add a caption…"; + /* Format string for file extension label in call interstitial view */ "ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT" = "File type: %@"; diff --git a/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift b/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift index b748ebeb8..daf4bba9b 100644 --- a/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift +++ b/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift @@ -9,7 +9,7 @@ import PromiseKit @objc public protocol AttachmentApprovalViewControllerDelegate: class { - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment]) + 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) @@ -62,6 +62,10 @@ class SignalAttachmentItem: Hashable { // MARK: + var captionText: String? { + return attachment.captionText + } + var imageSize: CGSize = .zero func getThumbnailImage() -> Promise { @@ -100,7 +104,7 @@ public enum AttachmentApprovalViewControllerMode: UInt { } @objc -public class AttachmentApprovalViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, CaptioningToolbarDelegate { +public class AttachmentApprovalViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate { // MARK: - Properties @@ -108,8 +112,6 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC public weak var approvalDelegate: AttachmentApprovalViewControllerDelegate? - private(set) var captioningToolbar: CaptioningToolbar! - // MARK: - Initializers @available(*, unavailable, message:"use attachment: constructor instead.") @@ -148,45 +150,35 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC return navController } - // MARK: - View Lifecycle + // MARK: - Subviews - let galleryRailView = GalleryRailView() - let railContainerView = UIView() + var galleryRailView: GalleryRailView { + return bottomToolView.galleryRailView + } - override public func viewDidLoad() { - super.viewDidLoad() + var mediaMessageTextToolbar: MediaMessageTextToolbar { + return bottomToolView.mediaMessageTextToolbar + } - self.view.backgroundColor = .black + lazy var bottomToolView: BottomToolView = { + let isAddMoreVisible = mode == .sharedNavigation + let bottomToolView = BottomToolView(isAddMoreVisible: isAddMoreVisible) - disablePagingIfNecessary() + return bottomToolView + }() - railContainerView.backgroundColor = UIColor.black.withAlphaComponent(0.6) - view.addSubview(railContainerView) - railContainerView.preservesSuperviewLayoutMargins = true - railContainerView.layoutMargins.bottom = 50 - railContainerView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top) + // MARK: - View Lifecycle - let footerGradientView = GradientView(from: .clear, to: .black) - railContainerView.addSubview(footerGradientView) - footerGradientView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top) - footerGradientView.autoSetDimension(.height, toSize: ScaleFromIPhone5(100)) + override public func viewDidLoad() { + super.viewDidLoad() - railContainerView.addSubview(galleryRailView) - galleryRailView.delegate = self - galleryRailView.scrollFocusMode = .keepWithinBounds + self.view.backgroundColor = .black - galleryRailView.autoPinEdge(toSuperviewEdge: .leading) - galleryRailView.autoPinEdge(toSuperviewEdge: .trailing) - galleryRailView.autoPinEdge(toSuperviewMargin: .top) - galleryRailView.autoPinEdge(toSuperviewMargin: .bottom) - galleryRailView.autoSetDimension(.height, toSize: 72) + disablePagingIfNecessary() // Bottom Toolbar - - let isAddMoreVisible = mode == .sharedNavigation - let captioningToolbar = CaptioningToolbar(isAddMoreVisible: isAddMoreVisible) - captioningToolbar.captioningToolbarDelegate = self - self.captioningToolbar = captioningToolbar + galleryRailView.delegate = self + mediaMessageTextToolbar.mediaMessageTextToolbarDelegate = self // Navigation @@ -205,8 +197,6 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } self.setCurrentItem(firstItem, direction: .forward, animated: false) - - captioningToolbar.captionText = currentViewController.attachment.captionText } override public func viewWillAppear(_ animated: Bool) { @@ -232,8 +222,8 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } override public var inputAccessoryView: UIView? { - self.captioningToolbar.layoutIfNeeded() - return self.captioningToolbar + bottomToolView.layoutIfNeeded() + return bottomToolView } override public var canBecomeFirstResponder: Bool { @@ -347,13 +337,6 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } 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) updateMediaRail() } @@ -423,6 +406,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC Logger.debug("cache miss.") let viewController = AttachmentPrepViewController(attachmentItem: item) + viewController.prepDelegate = self cachedPages[item] = viewController return viewController @@ -430,7 +414,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC 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") + owsFailDebug("unexpectedly unable to build new page") return } @@ -447,8 +431,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC galleryRailView.configureCellViews(itemProvider: attachmentItemCollection, focusedItem: currentItem) addDeleteIcon(cellViews: galleryRailView.cellViews) - railContainerView.isHidden = attachmentItemCollection.attachmentItems.count < 2 - captioningToolbar.alwaysShowGradient = railContainerView.isHidden + galleryRailView.isHidden = attachmentItemCollection.attachmentItems.count < 2 } let attachmentItemCollection: AttachmentItemCollection @@ -496,40 +479,40 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC @objc func cancelPressed(sender: UIButton) { self.approvalDelegate?.attachmentApproval(self, didCancelAttachments: attachments) } +} - // MARK: - CaptioningToolbarDelegate - +extension AttachmentApprovalViewController: MediaMessageTextToolbarDelegate { var currentPageController: AttachmentPrepViewController { return viewControllers!.first as! AttachmentPrepViewController } - func captioningToolbarDidBeginEditing(_ captioningToolbar: CaptioningToolbar) { + func mediaMessageTextToolbarDidBeginEditing(_ mediaMessageTextToolbar: MediaMessageTextToolbar) { currentPageController.setAttachmentViewScale(.compact, animated: true) } - func captioningToolbarDidEndEditing(_ captioningToolbar: CaptioningToolbar) { + func mediaMessageTextToolbarDidEndEditing(_ mediaMessageTextToolbar: MediaMessageTextToolbar) { currentPageController.setAttachmentViewScale(.fullsize, animated: true) } - func captioningToolbarDidTapSend(_ captioningToolbar: CaptioningToolbar) { + func mediaMessageTextToolbarDidTapSend(_ mediaMessageTextToolbar: MediaMessageTextToolbar) { // 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 + mediaMessageTextToolbar.isUserInteractionEnabled = false + mediaMessageTextToolbar.isHidden = true - approvalDelegate?.attachmentApproval(self, didApproveAttachments: attachments) + approvalDelegate?.attachmentApproval(self, didApproveAttachments: attachments, messageText: mediaMessageTextToolbar.messageText) } - func captioningToolbar(_ captioningToolbar: CaptioningToolbar, textViewDidChange textView: UITextView) { - currentItem.attachment.captionText = textView.text - - self.approvalDelegate?.attachmentApproval?(self, changedCaptionOfAttachment: currentItem.attachment) + func mediaMessageTextToolbarDidAddMore(_ mediaMessageTextToolbar: MediaMessageTextToolbar) { + self.approvalDelegate?.attachmentApproval?(self, addMoreToAttachments: attachments) } +} - func captioningToolbarDidAddMore(_ captioningToolbar: CaptioningToolbar) { - self.approvalDelegate?.attachmentApproval?(self, addMoreToAttachments: attachments) +extension AttachmentApprovalViewController: AttachmentPrepViewControllerDelegate { + func prepViewController(_ prepViewController: AttachmentPrepViewController, didUpdateCaptionForAttachmentItem attachmentItem: SignalAttachmentItem) { + self.approvalDelegate?.attachmentApproval?(self, changedCaptionOfAttachment: attachmentItem.attachment) } } @@ -576,6 +559,10 @@ extension AttachmentApprovalViewController: GalleryRailViewDelegate { // MARK: - Individual Page +protocol AttachmentPrepViewControllerDelegate: class { + func prepViewController(_ prepViewController: AttachmentPrepViewController, didUpdateCaptionForAttachmentItem attachmentItem: SignalAttachmentItem) +} + public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarDelegate, OWSVideoPlayerDelegate { // We sometimes shrink the attachment view so that it remains somewhat visible // when the keyboard is presented. @@ -585,6 +572,8 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD // MARK: - Properties + weak var prepDelegate: AttachmentPrepViewControllerDelegate? + let attachmentItem: SignalAttachmentItem var attachment: SignalAttachment { return attachmentItem.attachment @@ -611,6 +600,10 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD // MARK: - View Lifecycle + lazy var captionView: CaptionView = { + return CaptionView(attachmentItem: attachmentItem) + }() + override public func loadView() { self.view = UIView() @@ -711,8 +704,23 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD playButton.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside) playButton.autoCenterInSuperview() } + + // Caption + + view.addSubview(captionView) + captionView.delegate = self + + captionView.autoPinWidthToSuperview() + + // MJK TODO ideal CaptionView placement + // 1. when no keyboard is popped (e.g. initially) to be *just* above the rail + // 2. when the CaptionTextView is first responder, to be *just* above the keyboard + // 3. when the MessageTextView is first responder, to be behind the keyboard + captionView.autoPinEdge(toSuperviewMargin: .bottom, withInset: 136) } + var captionViewBottomConstraint: NSLayoutConstraint! + override public func viewWillLayoutSubviews() { Logger.debug("") super.viewWillLayoutSubviews() @@ -876,6 +884,14 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD } } +extension AttachmentPrepViewController: CaptionViewDelegate { + func captionView(_ captionView: CaptionView, didChangeCaptionText captionText: String?, attachmentItem: SignalAttachmentItem) { + let attachment = attachmentItem.attachment + attachment.captionText = captionText + prepDelegate?.prepViewController(self, didUpdateCaptionForAttachmentItem: attachmentItem) + } +} + extension AttachmentPrepViewController: UIScrollViewDelegate { public func viewForZooming(in scrollView: UIScrollView) -> UIView? { @@ -944,27 +960,227 @@ extension AttachmentPrepViewController: UIScrollViewDelegate { } } -protocol CaptioningToolbarDelegate: class { - func captioningToolbarDidTapSend(_ captioningToolbar: CaptioningToolbar) - func captioningToolbarDidBeginEditing(_ captioningToolbar: CaptioningToolbar) - func captioningToolbarDidEndEditing(_ captioningToolbar: CaptioningToolbar) - func captioningToolbar(_ captioningToolbar: CaptioningToolbar, textViewDidChange: UITextView) - func captioningToolbarDidAddMore(_ captioningToolbar: CaptioningToolbar) +class BottomToolView: UIView { + let mediaMessageTextToolbar: MediaMessageTextToolbar + let galleryRailView: GalleryRailView + + let kGalleryRailViewHeight: CGFloat = 72 + + required init(isAddMoreVisible: Bool) { + mediaMessageTextToolbar = MediaMessageTextToolbar(isAddMoreVisible: isAddMoreVisible) + + galleryRailView = GalleryRailView() + galleryRailView.scrollFocusMode = .keepWithinBounds + galleryRailView.autoSetDimension(.height, toSize: kGalleryRailViewHeight) + + super.init(frame: .zero) + + // Specifying autorsizing mask and an intrinsic content size allows proper + // sizing when used as an input accessory view. + self.autoresizingMask = .flexibleHeight + self.translatesAutoresizingMaskIntoConstraints = false + + backgroundColor = UIColor.black.withAlphaComponent(0.6) + preservesSuperviewLayoutMargins = true + + let stackView = UIStackView(arrangedSubviews: [self.galleryRailView, self.mediaMessageTextToolbar]) + stackView.axis = .vertical + + addSubview(stackView) + stackView.autoPinEdge(toSuperviewEdge: .leading) + stackView.autoPinEdge(toSuperviewEdge: .trailing) + stackView.autoPinEdge(toSuperviewEdge: .top) + stackView.autoPinEdge(toSuperviewMargin: .bottom) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: + + override var intrinsicContentSize: CGSize { + get { + // Since we have `self.autoresizingMask = UIViewAutoresizingFlexibleHeight`, we must specify + // an intrinsicContentSize. Specifying CGSize.zero causes the height to be determined by autolayout. + return CGSize.zero + } + } +} + +protocol CaptionViewDelegate: class { + func captionView(_ captionView: CaptionView, didChangeCaptionText captionText: String?, attachmentItem: SignalAttachmentItem) } -class CaptioningToolbar: UIView, UITextViewDelegate { +class CaptionView: UIView { + + var captionText: String? { + get { return textView.text } + set { + textView.text = newValue + updatePlaceholderTextViewVisibility() + } + } + + let attachmentItem: SignalAttachmentItem + var attachment: SignalAttachment { + return attachmentItem.attachment + } + + weak var delegate: CaptionViewDelegate? + + private let kMinTextViewHeight: CGFloat = 38 + private var textViewHeightConstraint: NSLayoutConstraint! + + // TODO show length limit label + private let lengthLimitLabel: UILabel = UILabel() + + // MARK: Initializers + + init(attachmentItem: SignalAttachmentItem) { + self.attachmentItem = attachmentItem + + super.init(frame: .zero) + + self.captionText = attachmentItem.captionText + + addSubview(placeholderTextView) + placeholderTextView.autoPinEdgesToSuperviewMargins() - weak var captioningToolbarDelegate: CaptioningToolbarDelegate? + backgroundColor = UIColor.black.withAlphaComponent(0.6) + addSubview(textView) + textView.autoPinEdgesToSuperviewMargins() + textView.delegate = self + + self.textViewHeightConstraint = textView.autoSetDimension(.height, toSize: kMinTextViewHeight) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: + + override var inputAccessoryView: UIView? { + return nil + } + + // MARK: Subviews + + func updatePlaceholderTextViewVisibility() { + let isHidden: Bool = { + guard !self.textView.isFirstResponder else { + return true + } + + guard let captionText = self.textView.text else { + return false + } + + guard captionText.count > 0 else { + return false + } + + return true + }() + + placeholderTextView.isHidden = isHidden + } + + private lazy var placeholderTextView: UITextView = { + let placeholderTextView = UITextView() + placeholderTextView.text = NSLocalizedString("ATTACHMENT_APPROVAL_CAPTION_PLACEHOLDER", comment: "placeholder text for an empty captioning field") + placeholderTextView.isEditable = false + + placeholderTextView.backgroundColor = .clear + placeholderTextView.keyboardAppearance = Theme.keyboardAppearance + placeholderTextView.font = UIFont.ows_dynamicTypeBody + // MJK FIXME always dark theme + placeholderTextView.textColor = Theme.placeholderColor + placeholderTextView.returnKeyType = .done + + return placeholderTextView + }() + + private lazy var textView: UITextView = { + let textView = UITextView() + textView.backgroundColor = .clear + textView.keyboardAppearance = Theme.keyboardAppearance + textView.font = UIFont.ows_dynamicTypeBody + textView.textColor = Theme.darkThemePrimaryColor + textView.returnKeyType = .done + + return textView + }() +} + +extension CaptionView: UITextViewDelegate { + public func textViewDidBeginEditing(_ textView: UITextView) { + updatePlaceholderTextViewVisibility() + } + + public func textViewDidEndEditing(_ textView: UITextView) { + updatePlaceholderTextViewVisibility() + } + + public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + let existingText: String = textView.text ?? "" + let proposedText: String = (existingText as NSString).replacingCharacters(in: range, with: text) + + guard proposedText.utf8.count <= kOversizeTextMessageSizeThreshold else { + Logger.debug("long text was truncated") + self.lengthLimitLabel.isHidden = false + + // `range` represents the section of the existing text we will replace. We can re-use that space. + // Range is in units of NSStrings's standard UTF-16 characters. Since some of those chars could be + // represented as single bytes in utf-8, while others may be 8 or more, the only way to be sure is + // to just measure the utf8 encoded bytes of the replaced substring. + let bytesAfterDelete: Int = (existingText as NSString).replacingCharacters(in: range, with: "").utf8.count + + // Accept as much of the input as we can + let byteBudget: Int = Int(kOversizeTextMessageSizeThreshold) - bytesAfterDelete + if byteBudget >= 0, let acceptableNewText = text.truncated(toByteCount: UInt(byteBudget)) { + textView.text = (existingText as NSString).replacingCharacters(in: range, with: acceptableNewText) + } + + return false + } + self.lengthLimitLabel.isHidden = true + + // Though we can wrap the text, we don't want to encourage multline captions, plus a "done" button + // allows the user to get the keyboard out of the way while in the attachment approval view. + if text == "\n" { + textView.resignFirstResponder() + return false + } else { + return true + } + } + + public func textViewDidChange(_ textView: UITextView) { + self.delegate?.captionView(self, didChangeCaptionText: textView.text, attachmentItem: attachmentItem) + } +} + +protocol MediaMessageTextToolbarDelegate: class { + func mediaMessageTextToolbarDidTapSend(_ mediaMessageTextToolbar: MediaMessageTextToolbar) + func mediaMessageTextToolbarDidBeginEditing(_ mediaMessageTextToolbar: MediaMessageTextToolbar) + func mediaMessageTextToolbarDidEndEditing(_ mediaMessageTextToolbar: MediaMessageTextToolbar) + func mediaMessageTextToolbarDidAddMore(_ mediaMessageTextToolbar: MediaMessageTextToolbar) +} + +class MediaMessageTextToolbar: UIView, UITextViewDelegate { + + weak var mediaMessageTextToolbarDelegate: MediaMessageTextToolbarDelegate? private let addMoreButton: UIButton private let sendButton: UIButton private let textView: UITextView - var captionText: String? { + var messageText: String? { get { return self.textView.text } set { self.textView.text = newValue } } - private let bottomGradient: GradientView = GradientView(from: .clear, to: .black) private let lengthLimitLabel: UILabel = UILabel() // Layout Constants @@ -1051,7 +1267,6 @@ class CaptioningToolbar: UIView, UITextViewDelegate { self.lengthLimitLabel.isHidden = true let contentView = UIView() - contentView.addSubview(bottomGradient) contentView.addSubview(sendButton) contentView.addSubview(textView) contentView.addSubview(lengthLimitLabel) @@ -1103,11 +1318,6 @@ class CaptioningToolbar: UIView, UITextViewDelegate { lengthLimitLabel.autoPinEdge(.bottom, to: .top, of: textView, withOffset: -6) lengthLimitLabel.setContentHuggingHigh() lengthLimitLabel.setCompressionResistanceHigh() - - bottomGradient.isHidden = true - let bottomGradientHeight = ScaleFromIPhone5(100) - bottomGradient.autoSetDimension(.height, toSize: bottomGradientHeight) - bottomGradient.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top) } required init?(coder aDecoder: NSCoder) { @@ -1117,18 +1327,17 @@ class CaptioningToolbar: UIView, UITextViewDelegate { // MARK: - @objc func didTapSend() { - self.captioningToolbarDelegate?.captioningToolbarDidTapSend(self) + mediaMessageTextToolbarDelegate?.mediaMessageTextToolbarDidTapSend(self) } @objc func didTapAddMore() { - self.captioningToolbarDelegate?.captioningToolbarDidAddMore(self) + mediaMessageTextToolbarDelegate?.mediaMessageTextToolbarDidAddMore(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 { @@ -1166,30 +1375,12 @@ class CaptioningToolbar: UIView, UITextViewDelegate { } } - var alwaysShowGradient: Bool = false { - didSet { - if alwaysShowGradient { - bottomGradient.isHidden = false - } - } - } - public func textViewDidBeginEditing(_ textView: UITextView) { - self.captioningToolbarDelegate?.captioningToolbarDidBeginEditing(self) - if !alwaysShowGradient { - UIView.animate(withDuration: 0.2) { - self.bottomGradient.isHidden = false - } - } + mediaMessageTextToolbarDelegate?.mediaMessageTextToolbarDidBeginEditing(self) } public func textViewDidEndEditing(_ textView: UITextView) { - self.captioningToolbarDelegate?.captioningToolbarDidEndEditing(self) - if !alwaysShowGradient { - UIView.animate(withDuration: 0.2) { - self.bottomGradient.isHidden = true - } - } + mediaMessageTextToolbarDelegate?.mediaMessageTextToolbarDidEndEditing(self) } // MARK: - Helpers @@ -1199,11 +1390,11 @@ class CaptioningToolbar: UIView, UITextViewDelegate { let currentSize = textView.frame.size let newHeight = clampedTextViewHeight(fixedWidth: currentSize.width) - if newHeight != self.textViewHeight { - Logger.debug("TextView height changed: \(self.textViewHeight) -> \(newHeight)") - self.textViewHeight = newHeight - self.textViewHeightConstraint?.constant = textViewHeight - self.invalidateIntrinsicContentSize() + if newHeight != textViewHeight { + Logger.debug("TextView height changed: \(textViewHeight) -> \(newHeight)") + textViewHeight = newHeight + textViewHeightConstraint?.constant = textViewHeight + invalidateIntrinsicContentSize() } }