Merge branch 'mkirk/separate-caption-and-body'

pull/1/head
Michael Kirk 6 years ago
commit 31637656fb

@ -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<SignalAttachment *> *)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<SignalAttachment *> *)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<SignalAttachment *> *)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<SignalAttachment *> *)attachments
{
[self tryToSendAttachmentsIfApproved:attachments skipApprovalDialog:NO];
[self presentViewController:modal animated:YES completion:nil];
}
- (void)tryToSendAttachmentsIfApproved:(NSArray<SignalAttachment *> *)attachments
skipApprovalDialog:(BOOL)skipApprovalDialog
- (void)tryToSendAttachments:(NSArray<SignalAttachment *> *)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<SignalAttachment *> *)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

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

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

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

@ -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: %@";

@ -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<UIImage> {
@ -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()
}
}

Loading…
Cancel
Save