222 lines
7.6 KiB
Swift
222 lines
7.6 KiB
Swift
//
|
|
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
@objc protocol ConversationInputTextViewDelegate {
|
|
func didPasteAttachment(_ attachment: SignalAttachment?)
|
|
func inputTextViewSendMessagePressed()
|
|
func textViewDidChange(_ textView: UITextView)
|
|
}
|
|
|
|
@objc protocol ConversationTextViewToolbarDelegate {
|
|
func textViewDidChange(_ textView: UITextView)
|
|
func textViewDidChangeSelection(_ textView: UITextView)
|
|
func textViewDidBecomeFirstResponder(_ textView: UITextView)
|
|
}
|
|
|
|
@objcMembers
|
|
class ConversationInputTextView: MentionTextView {
|
|
|
|
private lazy var placeholderView = UILabel()
|
|
private var placeholderConstraints: [NSLayoutConstraint]?
|
|
|
|
weak var inputTextViewDelegate: ConversationInputTextViewDelegate?
|
|
weak var textViewToolbarDelegate: ConversationTextViewToolbarDelegate?
|
|
|
|
var trimmedText: String { text.ows_stripped() }
|
|
var untrimmedText: String { text }
|
|
|
|
required init() {
|
|
super.init()
|
|
|
|
backgroundColor = nil
|
|
scrollIndicatorInsets = UIEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
|
|
|
|
isScrollEnabled = true
|
|
scrollsToTop = false
|
|
isUserInteractionEnabled = true
|
|
|
|
contentMode = .redraw
|
|
dataDetectorTypes = []
|
|
|
|
placeholderView.text = NSLocalizedString("new_message", comment: "")
|
|
placeholderView.textColor = Theme.placeholderColor
|
|
placeholderView.isUserInteractionEnabled = false
|
|
addSubview(placeholderView)
|
|
|
|
// We need to do these steps _after_ placeholderView is configured.
|
|
font = .ows_dynamicTypeBody
|
|
textColor = Theme.primaryTextColor
|
|
textAlignment = .natural
|
|
textContainer.lineFragmentPadding = 0
|
|
contentInset = .zero
|
|
text = nil
|
|
|
|
updateTextContainerInset()
|
|
|
|
ensurePlaceholderConstraints()
|
|
updatePlaceholderVisibility()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private func updateTextContainerInset() {
|
|
let stickerButtonOffset: CGFloat = 30
|
|
var leftInset: CGFloat = 12
|
|
var rightInset: CGFloat = leftInset
|
|
|
|
// If the placeholder view is visible, we need to offset
|
|
// the input container to accomodate for the sticker button.
|
|
if !placeholderView.isHidden {
|
|
if CurrentAppContext().isRTL {
|
|
leftInset += stickerButtonOffset
|
|
} else {
|
|
rightInset += stickerButtonOffset
|
|
}
|
|
}
|
|
|
|
// Check the system font size and increase text inset accordingly
|
|
// to keep the text vertically centered
|
|
updateVerticalInsetsForDynamicBodyType(defaultInsets: 7)
|
|
textContainerInset.left = leftInset
|
|
textContainerInset.right = rightInset
|
|
}
|
|
|
|
private func ensurePlaceholderConstraints() {
|
|
if let placeholderConstraints = placeholderConstraints {
|
|
NSLayoutConstraint.deactivate(placeholderConstraints)
|
|
}
|
|
|
|
let topInset = textContainerInset.top
|
|
let leftInset = textContainerInset.left
|
|
let rightInset = textContainerInset.right
|
|
|
|
placeholderConstraints = [
|
|
placeholderView.autoMatch(.width, to: .width, of: self, withOffset: -(leftInset + rightInset)),
|
|
placeholderView.autoPinEdge(toSuperviewEdge: .left, withInset: leftInset),
|
|
placeholderView.autoPinEdge(toSuperviewEdge: .top, withInset: topInset)
|
|
]
|
|
}
|
|
|
|
private func updatePlaceholderVisibility() {
|
|
placeholderView.isHidden = !text.isEmpty
|
|
}
|
|
|
|
override var font: UIFont? {
|
|
didSet { placeholderView.font = font }
|
|
}
|
|
|
|
override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
|
|
// When creating new lines, contentOffset is animated, but because because
|
|
// we are simultaneously resizing the text view, this can cause the
|
|
// text in the textview to be "too high" in the text view.
|
|
// Solution is to disable animation for setting content offset.
|
|
super.setContentOffset(contentOffset, animated: false)
|
|
}
|
|
|
|
override var contentInset: UIEdgeInsets {
|
|
didSet { ensurePlaceholderConstraints() }
|
|
}
|
|
|
|
override var textContainerInset: UIEdgeInsets {
|
|
didSet { ensurePlaceholderConstraints() }
|
|
}
|
|
|
|
override var text: String! {
|
|
didSet {
|
|
updatePlaceholderVisibility()
|
|
updateTextContainerInset()
|
|
}
|
|
}
|
|
|
|
override func becomeFirstResponder() -> Bool {
|
|
let result = super.becomeFirstResponder()
|
|
if result { textViewToolbarDelegate?.textViewDidBecomeFirstResponder(self) }
|
|
return result
|
|
}
|
|
|
|
var pasteboardHasPossibleAttachment: Bool {
|
|
// We don't want to load/convert images more than once so we
|
|
// only do a cursory validation pass at this time.
|
|
SignalAttachment.pasteboardHasPossibleAttachment() && !SignalAttachment.pasteboardHasText()
|
|
}
|
|
|
|
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
|
if action == #selector(paste(_:)) {
|
|
if pasteboardHasPossibleAttachment {
|
|
return true
|
|
}
|
|
}
|
|
return super.canPerformAction(action, withSender: sender)
|
|
}
|
|
|
|
override func paste(_ sender: Any?) {
|
|
if pasteboardHasPossibleAttachment {
|
|
// Note: attachment might be nil or have an error at this point; that's fine.
|
|
let attachment = SignalAttachment.attachmentFromPasteboard()
|
|
inputTextViewDelegate?.didPasteAttachment(attachment)
|
|
return
|
|
}
|
|
|
|
super.paste(sender)
|
|
}
|
|
|
|
// MARK: - UITextViewDelegate
|
|
|
|
override func textViewDidChange(_ textView: UITextView) {
|
|
super.textViewDidChange(textView)
|
|
|
|
updatePlaceholderVisibility()
|
|
updateTextContainerInset()
|
|
|
|
inputTextViewDelegate?.textViewDidChange(self)
|
|
textViewToolbarDelegate?.textViewDidChange(self)
|
|
}
|
|
|
|
override func textViewDidChangeSelection(_ textView: UITextView) {
|
|
super.textViewDidChangeSelection(textView)
|
|
|
|
textViewToolbarDelegate?.textViewDidChangeSelection(self)
|
|
}
|
|
|
|
// MARK: - Key Commands
|
|
|
|
override var keyCommands: [UIKeyCommand]? {
|
|
let keyCommands = super.keyCommands ?? []
|
|
|
|
// We don't define discoverability title for these key commands as they're
|
|
// considered "default" functionality and shouldn't clutter the shortcut
|
|
// list that is rendered when you hold down the command key.
|
|
return keyCommands + [
|
|
// An unmodified return can only be sent by a hardware keyboard,
|
|
// return on the software keyboard will not trigger this command.
|
|
// Return, send message
|
|
UIKeyCommand(input: "\r", modifierFlags: [], action: #selector(unmodifiedReturnPressed(_:))),
|
|
// Alt + Return, inserts a new line
|
|
UIKeyCommand(input: "\r", modifierFlags: .alternate, action: #selector(modifiedReturnPressed(_:))),
|
|
// Shift + Return, inserts a new line
|
|
UIKeyCommand(input: "\r", modifierFlags: .shift, action: #selector(modifiedReturnPressed(_:)))
|
|
]
|
|
}
|
|
|
|
func unmodifiedReturnPressed(_ sender: UIKeyCommand) {
|
|
Logger.info("unmodifedReturnPressed: \(String(describing: sender.input))")
|
|
inputTextViewDelegate?.inputTextViewSendMessagePressed()
|
|
}
|
|
|
|
func modifiedReturnPressed(_ sender: UIKeyCommand) {
|
|
Logger.info("modifedReturnPressed: \(String(describing: sender.input))")
|
|
|
|
replace(selectedTextRange ?? UITextRange(), withText: "\n")
|
|
|
|
inputTextViewDelegate?.textViewDidChange(self)
|
|
textViewToolbarDelegate?.textViewDidChange(self)
|
|
}
|
|
}
|