From d80f086f3134562f73c0e701e51ba3be033d6f0b Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 13 Mar 2019 16:15:33 -0400 Subject: [PATCH] Rework attachment captioning. --- Signal.xcodeproj/project.pbxproj | 28 ++- .../translations/en.lproj/Localizable.strings | 72 +----- ...AttachmentApprovalInputAccessoryView.swift | 129 +++++++++- .../AttachmentApprovalViewController.swift | 111 ++++++--- .../AttachmentCaptionToolbar.swift | 222 ++++++++++++++++++ .../AttachmentPrepViewController.swift | 73 +----- 6 files changed, 449 insertions(+), 186 deletions(-) create mode 100644 SignalMessaging/ViewControllers/AttachmentApproval/AttachmentCaptionToolbar.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index cabb23b0a..2eeeffa51 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -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 = ""; }; 340872C52239563500CB25B0 /* AttachmentApprovalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentApprovalViewController.swift; sourceTree = ""; }; 340872C62239563500CB25B0 /* AttachmentPrepViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentPrepViewController.swift; sourceTree = ""; }; - 340872C72239563500CB25B0 /* AttachmentCaptionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentCaptionViewController.swift; sourceTree = ""; }; 340872CD2239596000CB25B0 /* AttachmentApprovalInputAccessoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentApprovalInputAccessoryView.swift; sourceTree = ""; }; - 340872DB22399F9100CB25B0 /* AttachmentTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentTextView.swift; sourceTree = ""; }; - 340872DC22399F9100CB25B0 /* AttachmentTextToolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentTextToolbar.swift; sourceTree = ""; }; + 340872CF2239787F00CB25B0 /* AttachmentTextToolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentTextToolbar.swift; sourceTree = ""; }; + 340872D522397E6800CB25B0 /* AttachmentCaptionToolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentCaptionToolbar.swift; sourceTree = ""; }; + 340872D722397F4500CB25B0 /* AttachmentCaptionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentCaptionViewController.swift; sourceTree = ""; }; + 340872D922397FEB00CB25B0 /* AttachmentTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentTextView.swift; sourceTree = ""; }; 340B02B61F9FD31800F9CFEC /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = translations/he.lproj/Localizable.strings; sourceTree = ""; }; 340B02B91FA0D6C700F9CFEC /* ConversationViewItemTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationViewItemTest.m; sourceTree = ""; }; 340FC87B204DAC8C007AEB0F /* NotificationSettingsOptionsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NotificationSettingsOptionsViewController.m; sourceTree = ""; }; @@ -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 = ""; @@ -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 */, diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index d206bb629..6ee9840fa 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -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."; diff --git a/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentApprovalInputAccessoryView.swift b/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentApprovalInputAccessoryView.swift index d520b307c..22c553b7f 100644 --- a/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentApprovalInputAccessoryView.swift +++ b/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentApprovalInputAccessoryView.swift @@ -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() + } +} diff --git a/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentApprovalViewController.swift b/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentApprovalViewController.swift index 759995258..5d5474b7d 100644 --- a/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentApprovalViewController.swift +++ b/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentApprovalViewController.swift @@ -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 + } +} diff --git a/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentCaptionToolbar.swift b/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentCaptionToolbar.swift new file mode 100644 index 000000000..56b068c89 --- /dev/null +++ b/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentCaptionToolbar.swift @@ -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) + } +} diff --git a/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentPrepViewController.swift b/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentPrepViewController.swift index 46527237a..878c3f767 100644 --- a/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentPrepViewController.swift +++ b/SignalMessaging/ViewControllers/AttachmentApproval/AttachmentPrepViewController.swift @@ -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? {