From cd88ef2bec6ae6ce8ac11c4879891b53e1256c53 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Tue, 20 Nov 2018 13:57:15 -0600 Subject: [PATCH] CaptionView text field per page --- .../translations/en.lproj/Localizable.strings | 3 + .../AttachmentApprovalViewController.swift | 186 ++++++++++++++++++ 2 files changed, 189 insertions(+) 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 bd477b8f5..c00777dad 100644 --- a/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift +++ b/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift @@ -598,6 +598,8 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD // MARK: - View Lifecycle + let captionView = CaptionView() + override public func loadView() { self.view = UIView() @@ -698,8 +700,23 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD playButton.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside) playButton.autoCenterInSuperview() } + + // Caption + + captionView.captionText = attachment.captionText + + view.addSubview(captionView) + 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() @@ -979,6 +996,175 @@ class BottomToolView: UIView { } } +protocol CaptionViewDelegate: class { + func captionViewDidChange(_ captionView: CaptionView) +} + +class CaptionView: UIView { + + var captionText: String? { + get { return textView.text } + set { + textView.text = newValue + updatePlaceholderTextViewVisibility() + } + } + + 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 + + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(placeholderTextView) + placeholderTextView.autoPinEdgesToSuperviewMargins() + + 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 { +// @available(iOS 2.0, *) +// optional public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool +// +// @available(iOS 2.0, *) +// optional public func textViewShouldEndEditing(_ textView: UITextView) -> Bool +// + 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?.captionViewDidChange(self) + } +// +// +// @available(iOS 2.0, *) +// optional public func textViewDidChangeSelection(_ textView: UITextView) +// +// +// @available(iOS 10.0, *) +// optional public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool +// +// @available(iOS 10.0, *) +// optional public func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool +// +// +// @available(iOS, introduced: 7.0, deprecated: 10.0, message: "Use textView:shouldInteractWithURL:inRange:forInteractionType: instead") +// optional public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool +// +// @available(iOS, introduced: 7.0, deprecated: 10.0, message: "Use textView:shouldInteractWithTextAttachment:inRange:forInteractionType: instead") +// optional public func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange) -> Bool +} + protocol MediaMessageTextToolbarDelegate: class { func mediaMessageTextToolbarDidTapSend(_ mediaMessageTextToolbar: MediaMessageTextToolbar) func mediaMessageTextToolbarDidBeginEditing(_ mediaMessageTextToolbar: MediaMessageTextToolbar)