|  |  |  | // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import Foundation | 
					
						
							|  |  |  | import UIKit | 
					
						
							|  |  |  | import SessionUIKit | 
					
						
							|  |  |  | import SignalCoreKit | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | protocol AttachmentApprovalInputAccessoryViewDelegate: AnyObject { | 
					
						
							|  |  |  |     func attachmentApprovalInputUpdateMediaRail() | 
					
						
							|  |  |  |     func attachmentApprovalInputStartEditingCaptions() | 
					
						
							|  |  |  |     func attachmentApprovalInputStopEditingCaptions() | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // 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() { | 
					
						
							|  |  |  |         attachmentTextToolbar = AttachmentTextToolbar() | 
					
						
							|  |  |  |         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.themeBackgroundColor = .clear | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         preservesSuperviewLayoutMargins = true | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Use a background view that extends below the keyboard to avoid animation glitches. | 
					
						
							|  |  |  |         let backgroundView = UIView() | 
					
						
							|  |  |  |         backgroundView.themeBackgroundColor = .backgroundPrimary | 
					
						
							|  |  |  |         addSubview(backgroundView) | 
					
						
							|  |  |  |         backgroundView.pin(to: self) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Separator | 
					
						
							|  |  |  |         let separator = UIView.separator() | 
					
						
							|  |  |  |         addSubview(separator) | 
					
						
							|  |  |  |         separator.pin(.top, to: .top, of: self) | 
					
						
							|  |  |  |         separator.pin(.leading, to: .leading, of: self) | 
					
						
							|  |  |  |         separator.pin(.trailing, to: .trailing, of: self) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         currentCaptionLabel.themeTextColor = .white | 
					
						
							|  |  |  |         currentCaptionLabel.font = .systemFont(ofSize: Values.mediumFontSize) | 
					
						
							|  |  |  |         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.autoPinEdge(toSuperviewEdge: .top) | 
					
						
							|  |  |  |         stackView.autoPinEdge(toSuperviewEdge: .leading) | 
					
						
							|  |  |  |         stackView.autoPinEdge(toSuperviewEdge: .trailing) | 
					
						
							|  |  |  |         // We pin to the superview's _margin_.  Otherwise the notch breaks | 
					
						
							|  |  |  |         // the layout if you hide the keyboard in the simulator (or if the | 
					
						
							|  |  |  |         // user uses an external keyboard). | 
					
						
							|  |  |  |         stackView.autoPinEdge(toSuperviewMargin: .bottom) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         let galleryRailBlockingView: UIView = UIView() | 
					
						
							|  |  |  |         galleryRailBlockingView.themeBackgroundColor = .backgroundPrimary | 
					
						
							|  |  |  |         stackView.addSubview(galleryRailBlockingView) | 
					
						
							|  |  |  |         galleryRailBlockingView.pin(.top, to: .bottom, of: attachmentTextToolbar) | 
					
						
							|  |  |  |         galleryRailBlockingView.pin(.left, to: .left, of: stackView) | 
					
						
							|  |  |  |         galleryRailBlockingView.pin(.right, to: .right, of: stackView) | 
					
						
							|  |  |  |         galleryRailBlockingView.pin(.bottom, to: .bottom, of: stackView) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // MARK: - Events | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @objc func captionTapped(sender: UIGestureRecognizer) { | 
					
						
							|  |  |  |         guard sender.state == .recognized else { return } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         delegate?.attachmentApprovalInputStartEditingCaptions() | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // MARK:  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private var shouldHideControls = false | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     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 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         updateFirstResponder() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         layoutSubviews() | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private func updateFirstResponder() { | 
					
						
							|  |  |  |         if (shouldHideControls) { | 
					
						
							|  |  |  |             if attachmentCaptionToolbar.textView.isFirstResponder { | 
					
						
							|  |  |  |                 attachmentCaptionToolbar.textView.resignFirstResponder() | 
					
						
							|  |  |  |             } else if attachmentTextToolbar.textView.isFirstResponder { | 
					
						
							|  |  |  |                 attachmentTextToolbar.textView.resignFirstResponder() | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } else if (isEditingCaptions) { | 
					
						
							|  |  |  |             // While editing captions, the keyboard should always remain visible. | 
					
						
							|  |  |  |             if !attachmentCaptionToolbar.textView.isFirstResponder { | 
					
						
							|  |  |  |                 attachmentCaptionToolbar.textView.becomeFirstResponder() | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |             if attachmentCaptionToolbar.textView.isFirstResponder { | 
					
						
							|  |  |  |                 attachmentCaptionToolbar.textView.resignFirstResponder() | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         // NOTE: We don't automatically make attachmentTextToolbar.textView | 
					
						
							|  |  |  |         // first responder; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public func update(isEditingCaptions: Bool, | 
					
						
							|  |  |  |                        currentAttachmentItem: SignalAttachmentItem?, | 
					
						
							|  |  |  |                        shouldHideControls: Bool) { | 
					
						
							|  |  |  |         // De-bounce | 
					
						
							|  |  |  |         guard self.isEditingCaptions != isEditingCaptions || | 
					
						
							|  |  |  |             self.currentAttachmentItem != currentAttachmentItem || | 
					
						
							|  |  |  |             self.shouldHideControls != shouldHideControls else { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 updateFirstResponder() | 
					
						
							|  |  |  |                 return | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.isEditingCaptions = isEditingCaptions | 
					
						
							|  |  |  |         self.currentAttachmentItem = currentAttachmentItem | 
					
						
							|  |  |  |         self.shouldHideControls = shouldHideControls | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         updateContents() | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // 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 | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public var hasFirstResponder: Bool { | 
					
						
							|  |  |  |         return (isFirstResponder || | 
					
						
							|  |  |  |             attachmentCaptionToolbar.textView.isFirstResponder || | 
					
						
							|  |  |  |             attachmentTextToolbar.textView.isFirstResponder) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // MARK: - | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | extension AttachmentApprovalInputAccessoryView: AttachmentCaptionToolbarDelegate { | 
					
						
							|  |  |  |     public func attachmentCaptionToolbarDidEdit(_ attachmentCaptionToolbar: AttachmentCaptionToolbar) { | 
					
						
							|  |  |  |         guard let currentAttachmentItem = currentAttachmentItem else { | 
					
						
							|  |  |  |             owsFailDebug("Missing currentAttachmentItem.") | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // TODO: Look at refactoring this behaviour to consolidate attachment mutations | 
					
						
							|  |  |  |         currentAttachmentItem.attachment.captionText = attachmentCaptionToolbar.textView.text | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         delegate?.attachmentApprovalInputUpdateMediaRail() | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public func attachmentCaptionToolbarDidComplete() { | 
					
						
							|  |  |  |         delegate?.attachmentApprovalInputStopEditingCaptions() | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |