Rework attachment captioning.

This commit is contained in:
Matthew Chen 2019-03-13 16:15:33 -04:00
parent 625656deb9
commit d80f086f31
6 changed files with 449 additions and 186 deletions

View File

@ -20,10 +20,11 @@
340872C92239563500CB25B0 /* AttachmentItemCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340872C42239563500CB25B0 /* AttachmentItemCollection.swift */; };
340872CA2239563500CB25B0 /* AttachmentApprovalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340872C52239563500CB25B0 /* AttachmentApprovalViewController.swift */; };
340872CB2239563500CB25B0 /* AttachmentPrepViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340872C62239563500CB25B0 /* AttachmentPrepViewController.swift */; };
340872CC2239563500CB25B0 /* AttachmentCaptionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340872C72239563500CB25B0 /* AttachmentCaptionViewController.swift */; };
340872CE2239596100CB25B0 /* AttachmentApprovalInputAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340872CD2239596000CB25B0 /* AttachmentApprovalInputAccessoryView.swift */; };
340872DD22399F9100CB25B0 /* AttachmentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340872DB22399F9100CB25B0 /* AttachmentTextView.swift */; };
340872DE22399F9100CB25B0 /* AttachmentTextToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340872DC22399F9100CB25B0 /* AttachmentTextToolbar.swift */; };
340872D02239787F00CB25B0 /* AttachmentTextToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340872CF2239787F00CB25B0 /* AttachmentTextToolbar.swift */; };
340872D622397E6800CB25B0 /* AttachmentCaptionToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340872D522397E6800CB25B0 /* AttachmentCaptionToolbar.swift */; };
340872D822397F4600CB25B0 /* AttachmentCaptionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340872D722397F4500CB25B0 /* AttachmentCaptionViewController.swift */; };
340872DA22397FEB00CB25B0 /* AttachmentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340872D922397FEB00CB25B0 /* AttachmentTextView.swift */; };
340B02BA1FA0D6C700F9CFEC /* ConversationViewItemTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 340B02B91FA0D6C700F9CFEC /* ConversationViewItemTest.m */; };
340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC87B204DAC8C007AEB0F /* NotificationSettingsOptionsViewController.m */; };
340FC8AA204DAC8D007AEB0F /* NotificationSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC87C204DAC8C007AEB0F /* NotificationSettingsViewController.m */; };
@ -657,10 +658,11 @@
340872C42239563500CB25B0 /* AttachmentItemCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentItemCollection.swift; sourceTree = "<group>"; };
340872C52239563500CB25B0 /* AttachmentApprovalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentApprovalViewController.swift; sourceTree = "<group>"; };
340872C62239563500CB25B0 /* AttachmentPrepViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentPrepViewController.swift; sourceTree = "<group>"; };
340872C72239563500CB25B0 /* AttachmentCaptionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentCaptionViewController.swift; sourceTree = "<group>"; };
340872CD2239596000CB25B0 /* AttachmentApprovalInputAccessoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentApprovalInputAccessoryView.swift; sourceTree = "<group>"; };
340872DB22399F9100CB25B0 /* AttachmentTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentTextView.swift; sourceTree = "<group>"; };
340872DC22399F9100CB25B0 /* AttachmentTextToolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentTextToolbar.swift; sourceTree = "<group>"; };
340872CF2239787F00CB25B0 /* AttachmentTextToolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentTextToolbar.swift; sourceTree = "<group>"; };
340872D522397E6800CB25B0 /* AttachmentCaptionToolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentCaptionToolbar.swift; sourceTree = "<group>"; };
340872D722397F4500CB25B0 /* AttachmentCaptionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentCaptionViewController.swift; sourceTree = "<group>"; };
340872D922397FEB00CB25B0 /* AttachmentTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentTextView.swift; sourceTree = "<group>"; };
340B02B61F9FD31800F9CFEC /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = translations/he.lproj/Localizable.strings; sourceTree = "<group>"; };
340B02B91FA0D6C700F9CFEC /* ConversationViewItemTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationViewItemTest.m; sourceTree = "<group>"; };
340FC87B204DAC8C007AEB0F /* NotificationSettingsOptionsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NotificationSettingsOptionsViewController.m; sourceTree = "<group>"; };
@ -1492,11 +1494,12 @@
340872C32239563500CB25B0 /* ApprovalRailCellView.swift */,
340872CD2239596000CB25B0 /* AttachmentApprovalInputAccessoryView.swift */,
340872C52239563500CB25B0 /* AttachmentApprovalViewController.swift */,
340872C72239563500CB25B0 /* AttachmentCaptionViewController.swift */,
340872D522397E6800CB25B0 /* AttachmentCaptionToolbar.swift */,
340872D722397F4500CB25B0 /* AttachmentCaptionViewController.swift */,
340872C42239563500CB25B0 /* AttachmentItemCollection.swift */,
340872C62239563500CB25B0 /* AttachmentPrepViewController.swift */,
340872DC22399F9100CB25B0 /* AttachmentTextToolbar.swift */,
340872DB22399F9100CB25B0 /* AttachmentTextView.swift */,
340872CF2239787F00CB25B0 /* AttachmentTextToolbar.swift */,
340872D922397FEB00CB25B0 /* AttachmentTextView.swift */,
);
path = AttachmentApproval;
sourceTree = "<group>";
@ -3399,6 +3402,7 @@
34BBC857220C7ADA00857249 /* ImageEditorItem.swift in Sources */,
34480B641FD0A98800BC14EF /* UIView+OWS.m in Sources */,
34AC0A1C211B39EA00997B47 /* OWSFlatButton.swift in Sources */,
340872D822397F4600CB25B0 /* AttachmentCaptionViewController.swift in Sources */,
34C3C7932040B0DD0000134C /* OWSAudioPlayer.m in Sources */,
34AC09E5211B39B100997B47 /* ScreenLockViewController.m in Sources */,
34AC09F7211B39B100997B47 /* MediaMessageView.swift in Sources */,
@ -3406,10 +3410,8 @@
3461293A1FD1B47300532771 /* OWSPreferences.m in Sources */,
34AC09E6211B39B100997B47 /* SelectRecipientViewController.m in Sources */,
4C858A52212DC5E1001B45D3 /* UIImage+OWS.swift in Sources */,
340872CC2239563500CB25B0 /* AttachmentCaptionViewController.swift in Sources */,
34480B671FD0AA9400BC14EF /* UIFont+OWS.m in Sources */,
346129E61FD5C0C600532771 /* OWSDatabaseMigrationRunner.m in Sources */,
340872DD22399F9100CB25B0 /* AttachmentTextView.swift in Sources */,
34AC0A11211B39EA00997B47 /* OWSLayerView.swift in Sources */,
34AC0A1B211B39EA00997B47 /* GradientView.swift in Sources */,
34AC09E2211B39B100997B47 /* ReturnToCallViewController.swift in Sources */,
@ -3430,6 +3432,7 @@
346129AD1FD1F34E00532771 /* ImageCache.swift in Sources */,
452C7CA72037628B003D51A5 /* Weak.swift in Sources */,
34D5872F208E2C4200D2255A /* OWS109OutgoingMessageState.m in Sources */,
340872D02239787F00CB25B0 /* AttachmentTextToolbar.swift in Sources */,
34AC09F8211B39B100997B47 /* CountryCodeViewController.m in Sources */,
451F8A341FD710C3005CB9DA /* FullTextSearcher.swift in Sources */,
34080F04222858DC0087E99F /* OWSViewController+ImageEditor.swift in Sources */,
@ -3466,6 +3469,7 @@
34BEDB0B21C2FA3D007B0EAE /* OWS114RemoveDynamicInteractions.swift in Sources */,
34AC0A1A211B39EA00997B47 /* CommonStrings.swift in Sources */,
34AC0A19211B39EA00997B47 /* OWSAlerts.swift in Sources */,
340872DA22397FEB00CB25B0 /* AttachmentTextView.swift in Sources */,
34FDB29221FF986600A01202 /* UIView+OWS.swift in Sources */,
34BBC859220C7ADA00857249 /* ImageEditorStrokeItem.swift in Sources */,
451F8A351FD710DE005CB9DA /* Searcher.swift in Sources */,
@ -3482,7 +3486,6 @@
34BBC84B220B2CB200857249 /* ImageEditorTextViewController.swift in Sources */,
34AC09FA211B39B100997B47 /* SharingThreadPickerViewController.m in Sources */,
45F59A082028E4FB00E8D2B0 /* OWSAudioSession.swift in Sources */,
340872DE22399F9100CB25B0 /* AttachmentTextToolbar.swift in Sources */,
34612A071FD7238600532771 /* OWSSyncManager.m in Sources */,
450C801220AD1D5B00F3A091 /* UIDevice+featureSupport.swift in Sources */,
451F8A471FD715BA005CB9DA /* OWSAvatarBuilder.m in Sources */,
@ -3497,6 +3500,7 @@
349EA07C2162AEA800F7B17F /* OWS111UDAttributesMigration.swift in Sources */,
34480B561FD0A7A400BC14EF /* DebugLogger.m in Sources */,
459B775C207BA46C0071D0AB /* OWSQuotedReplyModel.m in Sources */,
340872D622397E6800CB25B0 /* AttachmentCaptionToolbar.swift in Sources */,
34ABB2C42090C59700C727A6 /* OWSResaveCollectionDBMigration.m in Sources */,
4C948FF72146EB4800349F0D /* BlockListCache.swift in Sources */,
4551DB5A205C562300C8AE75 /* Collection+OWS.swift in Sources */,

View File

@ -95,6 +95,9 @@
/* placeholder text for an empty captioning field */
"ATTACHMENT_APPROVAL_CAPTION_PLACEHOLDER" = "Add a caption…";
/* Title for 'caption' mode of the attachment approval view. */
"ATTACHMENT_APPROVAL_CAPTION_TITLE" = "Caption";
/* Format string for file extension label in call interstitial view */
"ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT" = "File type: %@";
@ -305,15 +308,9 @@
/* Label for the 'next' button. */
"BUTTON_NEXT" = "Next";
/* Label for redo button. */
"BUTTON_REDO" = "Redo";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Select";
/* Label for undo button. */
"BUTTON_UNDO" = "Undo";
/* Label for button that lets users call a contact again. */
"CALL_AGAIN_BUTTON_TITLE" = "Call Again";
@ -1068,13 +1065,13 @@
/* Placeholder text for search bar which filters conversations. */
"HOME_VIEW_CONVERSATION_SEARCHBAR_PLACEHOLDER" = "Search";
/* Format string for a label offering to start a new conversation with your contacts, if you have 1 Signal contact. Embeds: {{The name of 1 of your Signal contacts}}. */
/* Format string for a label offering to start a new conversation with your contacts, if you have 1 Signal contact. Embeds {{The name of 1 of your Signal contacts}}. */
"HOME_VIEW_FIRST_CONVERSATION_OFFER_1_CONTACT_FORMAT" = "Some of your contacts are already on Signal, including %@.";
/* Format string for a label offering to start a new conversation with your contacts, if you have 2 Signal contacts. Embeds: {{The names of 2 of your Signal contacts}}. */
/* Format string for a label offering to start a new conversation with your contacts, if you have 2 Signal contacts. Embeds {{The names of 2 of your Signal contacts}}. */
"HOME_VIEW_FIRST_CONVERSATION_OFFER_2_CONTACTS_FORMAT" = "Some of your contacts are already on Signal, including %@ and %@";
/* Format string for a label offering to start a new conversation with your contacts, if you have at least 3 Signal contacts. Embeds: {{The names of 3 of your Signal contacts}}. */
/* Format string for a label offering to start a new conversation with your contacts, if you have at least 3 Signal contacts. Embeds {{The names of 3 of your Signal contacts}}. */
"HOME_VIEW_FIRST_CONVERSATION_OFFER_3_CONTACTS_FORMAT" = "Some of your contacts are already on Signal, including %@, %@ and %@";
/* A label offering to start a new conversation with your contacts, if you have no Signal contacts. */
@ -1089,21 +1086,6 @@
/* Title for the home view's default mode. */
"HOME_VIEW_TITLE_INBOX" = "Signal";
/* Label for brush button in image editor. */
"IMAGE_EDITOR_BRUSH_BUTTON" = "Brush";
/* Label for crop button in image editor. */
"IMAGE_EDITOR_CROP_BUTTON" = "Crop";
/* Label for button that resets crop & rotation state. */
"IMAGE_EDITOR_RESET_BUTTON" = "Reset";
/* Label for button that rotates image 45 degrees. */
"IMAGE_EDITOR_ROTATE_45_BUTTON" = "Rotate 45°";
/* Label for button that rotates image 90 degrees. */
"IMAGE_EDITOR_ROTATE_90_BUTTON" = "Rotate 90°";
/* Momentarily shown to the user when attempting to select more images than is allowed. Embeds {{max number of items}} that can be shared. */
"IMAGE_PICKER_CAN_SELECT_NO_MORE_TOAST_FORMAT" = "You can't share more than %@ items.";
@ -1811,12 +1793,6 @@
/* Alert message explaining what happens if you forget your 'two-factor auth pin'. */
"REGISTER_2FA_FORGOT_PIN_ALERT_MESSAGE" = "Registration of this phone number will be possible without your Registration Lock PIN after 7 days have passed since the phone number was last active on Signal.";
/* Instructions to enter the 'two-factor auth pin' in the 2FA registration view. */
"REGISTER_2FA_INSTRUCTIONS" = "This phone number has Registration Lock enabled. Please enter the Registration Lock PIN.\n\nYour Registration Lock PIN is separate from the automated verification code that was sent to your phone during the last step.";
/* Title for alert indicating that attempt to register with 'two-factor auth' failed. */
"REGISTER_2FA_REGISTRATION_FAILED_ALERT_TITLE" = "Registration Failed";
/* Label for 'submit' button in the 2FA registration view. */
"REGISTER_2FA_SUBMIT_BUTTON" = "Submit";
@ -1838,9 +1814,6 @@
/* Label for the country code field */
"REGISTRATION_DEFAULT_COUNTRY_NAME" = "Country Code";
/* Navigation title shown when user is re-registering after having enabled registration lock */
"REGISTRATION_ENTER_LOCK_PIN_NAV_TITLE" = "Registration Lock";
/* Placeholder text for the phone number textfield */
"REGISTRATION_ENTERNUMBER_DEFAULT_TEXT" = "Enter Number";
@ -1859,15 +1832,6 @@
/* alert title when registering an iPad */
"REGISTRATION_IPAD_CONFIRM_TITLE" = "Already have a Signal account?";
/* one line label below submit button on registration screen, which links to an external webpage. */
"REGISTRATION_LEGAL_TERMS_LINK" = "Terms & Privacy Policy";
/* legal disclaimer, embeds a tappable {{link title}} which is styled as a hyperlink */
"REGISTRATION_LEGAL_TOP_MATTER_FORMAT" = "By registering this device, you agree to Signal's %@";
/* embedded in legal topmatter, styled as a link */
"REGISTRATION_LEGAL_TOP_MATTER_LINK_TITLE" = "terms";
/* No comment provided by engineer. */
"REGISTRATION_NON_VALID_NUMBER" = "This phone number format is not supported, please contact support.";
@ -1877,9 +1841,6 @@
/* No comment provided by engineer. */
"REGISTRATION_RESTRICTED_MESSAGE" = "You need to register before you can send a message.";
/* No comment provided by engineer. */
"REGISTRATION_TITLE_LABEL" = "Your Phone Number";
/* Alert view title */
"REGISTRATION_VERIFICATION_FAILED_TITLE" = "Verification Failed";
@ -1889,9 +1850,6 @@
/* Error message indicating that registration failed due to a missing or incorrect 2FA PIN. */
"REGISTRATION_VERIFICATION_FAILED_WRONG_PIN" = "Incorrect Registration Lock PIN.";
/* No comment provided by engineer. */
"REGISTRATION_VERIFY_DEVICE" = "Register";
/* Message of alert indicating that users needs to enter a valid phone number to register. */
"REGISTRATION_VIEW_INVALID_PHONE_NUMBER_ALERT_MESSAGE" = "Please enter a valid phone number to register.";
@ -2525,24 +2483,6 @@
/* Title for the alert indicating that user should upgrade iOS. */
"UPGRADE_IOS_ALERT_TITLE" = "Upgrade iOS";
/* button text for back button on verification view */
"VERIFICATION_BACK_BUTTON" = "Back";
/* Text field placeholder for SMS verification code during registration */
"VERIFICATION_CHALLENGE_DEFAULT_TEXT" = "Verification Code";
/* button text during registration to request phone number verification be done via phone call */
"VERIFICATION_CHALLENGE_SEND_VIA_VOICE" = "Call Me Instead";
/* button text during registration to request another SMS code be sent */
"VERIFICATION_CHALLENGE_SUBMIT_AGAIN" = "Resend Code by SMS";
/* button text during registration to submit your SMS verification code. */
"VERIFICATION_CHALLENGE_SUBMIT_CODE" = "Submit";
/* Label indicating the phone number currently being verified. */
"VERIFICATION_PHONE_NUMBER_FORMAT" = "Enter the verification code we sent to %@.";
/* Format for info message indicating that the verification state was unverified on this device. Embeds {{user's name or phone number}}. */
"VERIFICATION_STATE_CHANGE_FORMAT_NOT_VERIFIED_LOCAL" = "You marked %@ as not verified.";

View File

@ -5,42 +5,148 @@
import Foundation
import UIKit
protocol AttachmentApprovalInputAccessoryViewDelegate: class {
func attachmentApprovalInputUpdateMediaRail()
func attachmentApprovalInputEditCaptions()
}
// MARK: -
class AttachmentApprovalInputAccessoryView: UIView {
weak var delegate: AttachmentApprovalInputAccessoryViewDelegate?
let attachmentTextToolbar: AttachmentTextToolbar
let attachmentCaptionToolbar: AttachmentCaptionToolbar
let galleryRailView: GalleryRailView
let currentCaptionLabel = UILabel()
let currentCaptionWrapper = UIView()
var isEditingMediaMessage: Bool {
return attachmentTextToolbar.textView.isFirstResponder
}
private var isEditingCaptions: Bool = false
private var currentAttachmentItem: SignalAttachmentItem?
let kGalleryRailViewHeight: CGFloat = 72
required init(isAddMoreVisible: Bool) {
attachmentTextToolbar = AttachmentTextToolbar(isAddMoreVisible: isAddMoreVisible)
attachmentCaptionToolbar = AttachmentCaptionToolbar()
galleryRailView = GalleryRailView()
galleryRailView.scrollFocusMode = .keepWithinBounds
galleryRailView.autoSetDimension(.height, toSize: kGalleryRailViewHeight)
super.init(frame: .zero)
createContents()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func createContents() {
// Specifying auto-resizing mask and an intrinsic content size allows proper
// sizing when used as an input accessory view.
self.autoresizingMask = .flexibleHeight
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = .clear
backgroundColor = UIColor.black.withAlphaComponent(0.6)
preservesSuperviewLayoutMargins = true
let stackView = UIStackView(arrangedSubviews: [self.galleryRailView, self.attachmentTextToolbar])
// Use a background view that extends below the keyboard to avoid animation glitches.
let backgroundView = UIView()
backgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.6)
addSubview(backgroundView)
backgroundView.autoPinEdge(toSuperviewEdge: .top)
backgroundView.autoPinEdge(toSuperviewEdge: .leading)
backgroundView.autoPinEdge(toSuperviewEdge: .trailing)
backgroundView.autoPinEdge(toSuperviewEdge: .bottom, withInset: -200)
currentCaptionLabel.textColor = UIColor(white: 1, alpha: 0.8)
currentCaptionLabel.font = UIFont.ows_dynamicTypeBody
currentCaptionLabel.numberOfLines = 5
currentCaptionLabel.lineBreakMode = .byWordWrapping
currentCaptionWrapper.isUserInteractionEnabled = true
currentCaptionWrapper.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(captionTapped)))
currentCaptionWrapper.addSubview(currentCaptionLabel)
currentCaptionLabel.autoPinEdgesToSuperviewMargins()
attachmentCaptionToolbar.attachmentCaptionToolbarDelegate = self
let stackView = UIStackView(arrangedSubviews: [currentCaptionWrapper, attachmentCaptionToolbar, galleryRailView, attachmentTextToolbar])
stackView.axis = .vertical
addSubview(stackView)
stackView.autoPinEdgesToSuperviewEdges()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
// MARK: - Events
@objc func captionTapped(sender: UIGestureRecognizer) {
guard sender.state == .recognized else {
return
}
delegate?.attachmentApprovalInputEditCaptions()
}
// MARK:
public var shouldHideControls = false {
didSet {
updateContents()
}
}
private func updateContents() {
var hasCurrentCaption = false
if let currentAttachmentItem = currentAttachmentItem,
let captionText = currentAttachmentItem.captionText {
hasCurrentCaption = captionText.count > 0
attachmentCaptionToolbar.textView.text = captionText
currentCaptionLabel.text = captionText
} else {
attachmentCaptionToolbar.textView.text = nil
currentCaptionLabel.text = nil
}
attachmentCaptionToolbar.isHidden = !isEditingCaptions
currentCaptionWrapper.isHidden = isEditingCaptions || !hasCurrentCaption
attachmentTextToolbar.isHidden = isEditingCaptions
if (shouldHideControls) {
if attachmentCaptionToolbar.textView.isFirstResponder {
attachmentCaptionToolbar.textView.resignFirstResponder()
} else if attachmentTextToolbar.textView.isFirstResponder {
attachmentTextToolbar.textView.resignFirstResponder()
}
} else if (isEditingCaptions) {
if !attachmentCaptionToolbar.textView.isFirstResponder {
attachmentCaptionToolbar.textView.becomeFirstResponder()
}
} else {
if !attachmentTextToolbar.textView.isFirstResponder {
attachmentTextToolbar.textView.becomeFirstResponder()
}
}
invalidateIntrinsicContentSize()
layoutSubviews()
}
public func update(isEditingCaptions: Bool,
currentAttachmentItem: SignalAttachmentItem?) {
self.isEditingCaptions = isEditingCaptions
self.currentAttachmentItem = currentAttachmentItem
updateContents()
}
// MARK:
@ -53,3 +159,18 @@ class AttachmentApprovalInputAccessoryView: UIView {
}
}
}
// MARK: -
extension AttachmentApprovalInputAccessoryView: AttachmentCaptionToolbarDelegate {
public func attachmentCaptionToolbarDidEdit(_ attachmentCaptionToolbar: AttachmentCaptionToolbar) {
guard let currentAttachmentItem = currentAttachmentItem else {
owsFailDebug("Missing currentAttachmentItem.")
return
}
currentAttachmentItem.attachment.captionText = attachmentCaptionToolbar.textView.text
delegate?.attachmentApprovalInputUpdateMediaRail()
}
}

View File

@ -34,6 +34,12 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
public weak var approvalDelegate: AttachmentApprovalViewControllerDelegate?
public var isEditingCaptions = false {
didSet {
updateContents()
}
}
// MARK: - Initializers
@available(*, unavailable, message:"use attachment: constructor instead.")
@ -86,6 +92,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
lazy var bottomToolView: AttachmentApprovalInputAccessoryView = {
let isAddMoreVisible = mode == .sharedNavigation
let bottomToolView = AttachmentApprovalInputAccessoryView(isAddMoreVisible: isAddMoreVisible)
bottomToolView.delegate = self
return bottomToolView
}()
@ -133,8 +140,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
}
navigationBar.overrideTheme(type: .clear)
updateNavigationBar()
updateControlVisibility()
updateContents()
}
override public func viewDidAppear(_ animated: Bool) {
@ -142,8 +148,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
super.viewDidAppear(animated)
updateNavigationBar()
updateControlVisibility()
updateContents()
}
override public func viewWillDisappear(_ animated: Bool) {
@ -151,6 +156,14 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
super.viewWillDisappear(animated)
}
private func updateContents() {
updateNavigationBar()
updateControlVisibility()
updateInputAccessory()
}
// MARK: - Input Accessory
override public var inputAccessoryView: UIView? {
bottomToolView.layoutIfNeeded()
return bottomToolView
@ -160,6 +173,15 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
return !shouldHideControls
}
public func updateInputAccessory() {
var currentPageViewController: AttachmentPrepViewController?
if pageViewControllers.count == 1 {
currentPageViewController = pageViewControllers.first
}
let currentAttachmentItem: SignalAttachmentItem? = currentPageViewController?.attachmentItem
bottomToolView.update(isEditingCaptions: isEditingCaptions, currentAttachmentItem: currentAttachmentItem)
}
// MARK: - Navigation Bar
public func updateNavigationBar() {
@ -169,21 +191,36 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
return
}
guard !isEditingCaptions else {
// Hide all navigation bar items while the caption view is open.
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("ATTACHMENT_APPROVAL_CAPTION_TITLE", comment: "Title for 'caption' mode of the attachment approval view."), style: .plain, target: nil, action: nil)
let doneButton = navigationBarButton(imageName: "image_editor_checkmark_full",
selector: #selector(didTapCaptionDone(sender:)))
let navigationBarItems = [doneButton]
updateNavigationBar(navigationBarItems: navigationBarItems)
return
}
var navigationBarItems = [UIView]()
var isShowingCaptionView = false
if let viewControllers = viewControllers,
viewControllers.count == 1,
let firstViewController = viewControllers.first as? AttachmentPrepViewController {
navigationBarItems = firstViewController.navigationBarItems()
isShowingCaptionView = firstViewController.isShowingCaptionView
}
guard !isShowingCaptionView else {
// Hide all navigation bar items while the caption view is open.
self.navigationItem.leftBarButtonItem = nil
self.navigationItem.rightBarButtonItem = nil
return
// Show the caption UI if there's more than one attachment
// OR if the attachment already has a caption.
let attachmentCount = attachmentItemCollection.count
var shouldShowCaptionUI = attachmentCount > 0
if let captionText = firstViewController.attachmentItem.captionText, captionText.count > 0 {
shouldShowCaptionUI = true
}
if shouldShowCaptionUI {
let captionButton = navigationBarButton(imageName: "image_editor_caption",
selector: #selector(didTapCaption(sender:)))
navigationBarItems.append(captionButton)
}
}
updateNavigationBar(navigationBarItems: navigationBarItems)
@ -264,15 +301,10 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
}
private func updateControlVisibility() {
if shouldHideControls {
if isFirstResponder {
resignFirstResponder()
}
} else {
if !isFirstResponder {
becomeFirstResponder()
}
if !shouldHideControls {
self.becomeFirstResponder()
}
bottomToolView.shouldHideControls = shouldHideControls
}
// MARK: - View Helpers
@ -351,8 +383,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
}
}
updateNavigationBar()
updateControlVisibility()
updateContents()
}
// MARK: - UIPageViewControllerDataSource
@ -564,8 +595,22 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
private func cancelPressed() {
self.approvalDelegate?.attachmentApproval(self, didCancelAttachments: attachments)
}
@objc func didTapCaption(sender: UIButton) {
Logger.verbose("")
isEditingCaptions = true
}
@objc func didTapCaptionDone(sender: UIButton) {
Logger.verbose("")
isEditingCaptions = false
}
}
// MARK: -
extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate {
func attachmentTextToolbarDidBeginEditing(_ attachmentTextToolbar: AttachmentTextToolbar) {
currentPageViewController.setAttachmentViewScale(.compact, animated: true)
@ -594,12 +639,6 @@ extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate {
// MARK: -
extension AttachmentApprovalViewController: AttachmentPrepViewControllerDelegate {
func prepViewController(_ prepViewController: AttachmentPrepViewController, didUpdateCaptionForAttachmentItem attachmentItem: SignalAttachmentItem) {
self.approvalDelegate?.attachmentApproval?(self, changedCaptionOfAttachment: attachmentItem.attachment)
updateMediaRail()
}
func prepViewControllerUpdateNavigationBar() {
updateNavigationBar()
}
@ -607,10 +646,6 @@ extension AttachmentApprovalViewController: AttachmentPrepViewControllerDelegate
func prepViewControllerUpdateControls() {
updateControlVisibility()
}
func prepViewControllerAttachmentCount() -> Int {
return attachmentItemCollection.count
}
}
// MARK: GalleryRail
@ -671,3 +706,15 @@ extension AttachmentApprovalViewController: ApprovalRailCellViewDelegate {
remove(attachmentItem: attachmentItem)
}
}
// MARK: -
extension AttachmentApprovalViewController: AttachmentApprovalInputAccessoryViewDelegate {
public func attachmentApprovalInputUpdateMediaRail() {
updateMediaRail()
}
public func attachmentApprovalInputEditCaptions() {
isEditingCaptions = true
}
}

View File

@ -0,0 +1,222 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
import UIKit
protocol AttachmentCaptionToolbarDelegate: class {
func attachmentCaptionToolbarDidEdit(_ attachmentCaptionToolbar: AttachmentCaptionToolbar)
}
// MARK: -
class AttachmentCaptionToolbar: UIView, UITextViewDelegate {
private let kMaxCaptionCharacterCount = 240
weak var attachmentCaptionToolbarDelegate: AttachmentCaptionToolbarDelegate?
var messageText: String? {
get { return textView.text }
set {
textView.text = newValue
}
}
// Layout Constants
let kMinTextViewHeight: CGFloat = 38
var maxTextViewHeight: CGFloat {
// About ~4 lines in portrait and ~3 lines in landscape.
// Otherwise we risk obscuring too much of the content.
return UIDevice.current.orientation.isPortrait ? 160 : 100
}
var textViewHeightConstraint: NSLayoutConstraint!
var textViewHeight: CGFloat
// MARK: - Initializers
init() {
self.textViewHeight = kMinTextViewHeight
super.init(frame: CGRect.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
self.backgroundColor = UIColor.clear
textView.delegate = self
// Layout
let kToolbarMargin: CGFloat = 8
self.textViewHeightConstraint = textView.autoSetDimension(.height, toSize: kMinTextViewHeight)
lengthLimitLabel.setContentHuggingHigh()
lengthLimitLabel.setCompressionResistanceHigh()
let contentView = UIStackView(arrangedSubviews: [textContainer, lengthLimitLabel])
// We have to wrap the toolbar items in a content view because iOS (at least on iOS10.3) assigns the inputAccessoryView.layoutMargins
// when resigning first responder (verified by auditing with `layoutMarginsDidChange`).
// The effect of this is that if we were to assign these margins to self.layoutMargins, they'd be blown away if the
// user dismisses the keyboard, giving the input accessory view a wonky layout.
contentView.layoutMargins = UIEdgeInsets(top: kToolbarMargin, left: kToolbarMargin, bottom: kToolbarMargin, right: kToolbarMargin)
contentView.axis = .vertical
addSubview(contentView)
contentView.autoPinEdgesToSuperviewEdges()
}
required init?(coder aDecoder: NSCoder) {
notImplemented()
}
// MARK: - UIView Overrides
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
}
}
// MARK: - Subviews
private lazy var lengthLimitLabel: UILabel = {
let lengthLimitLabel = UILabel()
// Length Limit Label shown when the user inputs too long of a message
lengthLimitLabel.textColor = .white
lengthLimitLabel.text = NSLocalizedString("ATTACHMENT_APPROVAL_MESSAGE_LENGTH_LIMIT_REACHED", comment: "One-line label indicating the user can add no more text to the media message field.")
lengthLimitLabel.textAlignment = .center
// Add shadow in case overlayed on white content
lengthLimitLabel.layer.shadowColor = UIColor.black.cgColor
lengthLimitLabel.layer.shadowOffset = .zero
lengthLimitLabel.layer.shadowOpacity = 0.8
lengthLimitLabel.layer.shadowRadius = 2.0
lengthLimitLabel.isHidden = true
return lengthLimitLabel
}()
lazy var textView: UITextView = {
let textView = buildTextView()
textView.returnKeyType = .done
textView.scrollIndicatorInsets = UIEdgeInsets(top: 5, left: 0, bottom: 5, right: 3)
return textView
}()
private lazy var textContainer: UIView = {
let textContainer = UIView()
textContainer.clipsToBounds = true
textContainer.addSubview(textView)
textView.autoPinEdgesToSuperviewEdges()
return textContainer
}()
private func buildTextView() -> UITextView {
let textView = AttachmentTextView()
textView.keyboardAppearance = Theme.darkThemeKeyboardAppearance
textView.backgroundColor = .clear
textView.tintColor = Theme.darkThemePrimaryColor
textView.font = UIFont.ows_dynamicTypeBody
textView.textColor = Theme.darkThemePrimaryColor
textView.textContainerInset = UIEdgeInsets(top: 7, left: 7, bottom: 7, right: 7)
return textView
}
// MARK: - UITextViewDelegate
public func textViewDidChange(_ textView: UITextView) {
updateHeight(textView: textView)
attachmentCaptionToolbarDelegate?.attachmentCaptionToolbarDidEdit(self)
}
public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if !FeatureFlags.sendingMediaWithOversizeText {
let existingText: String = textView.text ?? ""
let proposedText: String = (existingText as NSString).replacingCharacters(in: range, with: text)
// Don't complicate things by mixing media attachments with oversize text attachments
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
// After verifying the byte-length is sufficiently small, verify the character count is within bounds.
guard proposedText.count < kMaxCaptionCharacterCount else {
Logger.debug("hit attachment message body character count limit")
self.lengthLimitLabel.isHidden = false
// `range` represents the section of the existing text we will replace. We can re-use that space.
let charsAfterDelete: Int = (existingText as NSString).replacingCharacters(in: range, with: "").count
// Accept as much of the input as we can
let charBudget: Int = Int(kMaxCaptionCharacterCount) - charsAfterDelete
if charBudget >= 0 {
let acceptableNewText = String(text.prefix(charBudget))
textView.text = (existingText as NSString).replacingCharacters(in: range, with: acceptableNewText)
}
return false
}
}
// 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
}
}
// MARK: - Helpers
private func updateHeight(textView: UITextView) {
// compute new height assuming width is unchanged
let currentSize = textView.frame.size
let newHeight = clampedTextViewHeight(fixedWidth: currentSize.width)
if newHeight != textViewHeight {
Logger.debug("TextView height changed: \(textViewHeight) -> \(newHeight)")
textViewHeight = newHeight
textViewHeightConstraint?.constant = textViewHeight
invalidateIntrinsicContentSize()
}
}
private func clampedTextViewHeight(fixedWidth: CGFloat) -> CGFloat {
let contentSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
return CGFloatClamp(contentSize.height, kMinTextViewHeight, maxTextViewHeight)
}
}

View File

@ -7,13 +7,9 @@ import UIKit
import AVFoundation
protocol AttachmentPrepViewControllerDelegate: class {
func prepViewController(_ prepViewController: AttachmentPrepViewController, didUpdateCaptionForAttachmentItem attachmentItem: SignalAttachmentItem)
func prepViewControllerUpdateNavigationBar()
func prepViewControllerUpdateControls()
func prepViewControllerAttachmentCount() -> Int
}
// MARK: -
@ -42,13 +38,6 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
private(set) var playVideoButton: UIView?
private var imageEditorView: ImageEditorView?
public var isShowingCaptionView = false {
didSet {
prepDelegate?.prepViewControllerUpdateNavigationBar()
prepDelegate?.prepViewControllerUpdateControls()
}
}
public var shouldHideControls: Bool {
guard let imageEditorView = imageEditorView else {
return false
@ -189,8 +178,6 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
playButton.autoCenterInSuperview()
}
// Caption
view.addSubview(touchInterceptorView)
touchInterceptorView.autoPinEdgesToSuperviewEdges()
touchInterceptorView.isHidden = true
@ -227,52 +214,10 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
// MARK: - Navigation Bar
public func navigationBarItems() -> [UIView] {
let captionButton = navigationBarButton(imageName: "image_editor_caption",
selector: #selector(didTapCaption(sender:)))
guard let imageEditorView = imageEditorView else {
// Show the "add caption" button for non-image attachments if
// there is more than one attachment.
if let prepDelegate = prepDelegate,
prepDelegate.prepViewControllerAttachmentCount() > 1 {
return [captionButton]
}
return []
}
var navigationBarItems = imageEditorView.navigationBarItems()
// Show the caption UI if there's more than one attachment
// OR if the attachment already has a caption.
var shouldShowCaptionUI = attachmentCount() > 0
if let captionText = attachmentItem.captionText, captionText.count > 0 {
shouldShowCaptionUI = true
}
if shouldShowCaptionUI {
navigationBarItems.append(captionButton)
}
return navigationBarItems
}
private func attachmentCount() -> Int {
guard let prepDelegate = prepDelegate else {
owsFailDebug("Missing prepDelegate.")
return 0
}
return prepDelegate.prepViewControllerAttachmentCount()
}
@objc func didTapCaption(sender: UIButton) {
Logger.verbose("")
presentCaptionView()
}
private func presentCaptionView() {
let view = AttachmentCaptionViewController(delegate: self, attachmentItem: attachmentItem)
self.imageEditor(presentFullScreenView: view, isTransparent: true)
isShowingCaptionView = true
return imageEditorView.navigationBarItems()
}
// MARK: - Event Handlers
@ -435,22 +380,6 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
// MARK: -
extension AttachmentPrepViewController: AttachmentCaptionDelegate {
func captionView(_ captionView: AttachmentCaptionViewController, didChangeCaptionText captionText: String?, attachmentItem: SignalAttachmentItem) {
let attachment = attachmentItem.attachment
attachment.captionText = captionText
prepDelegate?.prepViewController(self, didUpdateCaptionForAttachmentItem: attachmentItem)
isShowingCaptionView = false
}
func captionViewDidCancel() {
isShowingCaptionView = false
}
}
// MARK: -
extension AttachmentPrepViewController: UIScrollViewDelegate {
public func viewForZooming(in scrollView: UIScrollView) -> UIView? {