|  |  |  | 
 | 
					
						
							|  |  |  | public final class InputTextView : UITextView, UITextViewDelegate { | 
					
						
							|  |  |  |     private weak var snDelegate: InputTextViewDelegate? | 
					
						
							|  |  |  |     private let maxWidth: CGFloat | 
					
						
							|  |  |  |     private lazy var heightConstraint = self.set(.height, to: minHeight) | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     public override var text: String! { didSet { handleTextChanged() } } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: UI Components | 
					
						
							|  |  |  |     private lazy var placeholderLabel: UILabel = { | 
					
						
							|  |  |  |         let result = UILabel() | 
					
						
							|  |  |  |         result.text = NSLocalizedString("vc_conversation_input_prompt", comment: "") | 
					
						
							|  |  |  |         result.font = .systemFont(ofSize: Values.mediumFontSize) | 
					
						
							|  |  |  |         result.textColor = Colors.text.withAlphaComponent(Values.mediumOpacity) | 
					
						
							|  |  |  |         return result | 
					
						
							|  |  |  |     }() | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: Settings | 
					
						
							|  |  |  |     private let minHeight: CGFloat = 22 | 
					
						
							|  |  |  |     private let maxHeight: CGFloat = 80 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // MARK: Lifecycle | 
					
						
							|  |  |  |     init(delegate: InputTextViewDelegate, maxWidth: CGFloat) { | 
					
						
							|  |  |  |         snDelegate = delegate | 
					
						
							|  |  |  |         self.maxWidth = maxWidth | 
					
						
							|  |  |  |         super.init(frame: CGRect.zero, textContainer: nil) | 
					
						
							|  |  |  |         setUpViewHierarchy() | 
					
						
							|  |  |  |         self.delegate = self | 
					
						
							|  |  |  |         self.isAccessibilityElement = true | 
					
						
							|  |  |  |         self.accessibilityLabel = NSLocalizedString("vc_conversation_input_prompt", comment: "") | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     public override init(frame: CGRect, textContainer: NSTextContainer?) { | 
					
						
							|  |  |  |         preconditionFailure("Use init(delegate:) instead.") | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public required init?(coder: NSCoder) { | 
					
						
							|  |  |  |         preconditionFailure("Use init(delegate:) instead.") | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { | 
					
						
							|  |  |  |         if action == #selector(paste(_:)) { | 
					
						
							|  |  |  |             if let _ = UIPasteboard.general.image { | 
					
						
							|  |  |  |                 return true | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         return super.canPerformAction(action, withSender: sender) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     public override func paste(_ sender: Any?) { | 
					
						
							|  |  |  |         if let image = UIPasteboard.general.image { | 
					
						
							|  |  |  |             snDelegate?.didPasteImageFromPasteboard(self, image: image) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         super.paste(sender) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private func setUpViewHierarchy() { | 
					
						
							|  |  |  |         showsHorizontalScrollIndicator = false | 
					
						
							|  |  |  |         showsVerticalScrollIndicator = false | 
					
						
							|  |  |  |         backgroundColor = .clear | 
					
						
							|  |  |  |         textColor = Colors.text | 
					
						
							|  |  |  |         font = .systemFont(ofSize: Values.mediumFontSize) | 
					
						
							|  |  |  |         tintColor = Colors.accent | 
					
						
							|  |  |  |         keyboardAppearance = isLightMode ? .light : .dark | 
					
						
							|  |  |  |         heightConstraint.isActive = true | 
					
						
							|  |  |  |         let horizontalInset: CGFloat = 2 | 
					
						
							|  |  |  |         textContainerInset = UIEdgeInsets(top: 0, left: horizontalInset, bottom: 0, right: horizontalInset) | 
					
						
							|  |  |  |         addSubview(placeholderLabel) | 
					
						
							|  |  |  |         placeholderLabel.pin(.leading, to: .leading, of: self, withInset: horizontalInset + 3) // Slight visual adjustment | 
					
						
							|  |  |  |         placeholderLabel.pin(.top, to: .top, of: self) | 
					
						
							|  |  |  |         pin(.trailing, to: .trailing, of: placeholderLabel, withInset: horizontalInset) | 
					
						
							|  |  |  |         pin(.bottom, to: .bottom, of: placeholderLabel) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // MARK: Updating | 
					
						
							|  |  |  |     public func textViewDidChange(_ textView: UITextView) { | 
					
						
							|  |  |  |         handleTextChanged() | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private func handleTextChanged() { | 
					
						
							|  |  |  |         defer { snDelegate?.inputTextViewDidChangeContent(self) } | 
					
						
							|  |  |  |         placeholderLabel.isHidden = !text.isEmpty | 
					
						
							|  |  |  |         let height = frame.height | 
					
						
							|  |  |  |         let size = sizeThatFits(CGSize(width: maxWidth, height: .greatestFiniteMagnitude)) | 
					
						
							|  |  |  |         // `textView.contentSize` isn't accurate when restoring a multiline draft, so we set it here manually | 
					
						
							|  |  |  |         self.contentSize = size | 
					
						
							|  |  |  |         let newHeight = size.height.clamp(minHeight, maxHeight) | 
					
						
							|  |  |  |         guard newHeight != height else { return } | 
					
						
							|  |  |  |         heightConstraint.constant = newHeight | 
					
						
							|  |  |  |         snDelegate?.inputTextViewDidChangeSize(self) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // MARK: Delegate | 
					
						
							|  |  |  | protocol InputTextViewDelegate : AnyObject { | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     func inputTextViewDidChangeSize(_ inputTextView: InputTextView) | 
					
						
							|  |  |  |     func inputTextViewDidChangeContent(_ inputTextView: InputTextView) | 
					
						
							|  |  |  |     func didPasteImageFromPasteboard(_ inputTextView: InputTextView, image: UIImage) | 
					
						
							|  |  |  | } |