session-ios/Session/Conversations V2/Input View/InputView.swift

199 lines
9.6 KiB
Swift
Raw Normal View History

2021-01-29 01:46:32 +01:00
2021-02-15 04:45:46 +01:00
final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, QuoteViewDelegate, LinkPreviewViewV2Delegate {
2021-01-29 01:46:32 +01:00
private let delegate: InputViewDelegate
2021-02-10 04:43:57 +01:00
var quoteDraftInfo: (model: OWSQuotedReplyModel, isOutgoing: Bool)? { didSet { handleQuoteDraftChanged() } }
2021-02-15 04:45:46 +01:00
var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)?
2021-02-15 03:51:26 +01:00
private lazy var linkPreviewView: LinkPreviewViewV2 = {
let maxWidth = self.additionalContentContainer.bounds.width - InputView.linkPreviewViewInset
return LinkPreviewViewV2(for: nil, maxWidth: maxWidth, delegate: self)
}()
2021-01-29 01:46:32 +01:00
var text: String {
get { inputTextView.text }
set { inputTextView.text = newValue }
}
override var intrinsicContentSize: CGSize { CGSize.zero }
// MARK: UI Components
private lazy var cameraButton = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_camera_black"), delegate: self)
private lazy var libraryButton = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_camera_roll_black"), delegate: self)
private lazy var gifButton = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_gif_black"), delegate: self)
private lazy var documentButton = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_document_black"), delegate: self)
private lazy var sendButton = InputViewButton(icon: #imageLiteral(resourceName: "ArrowUp"), isSendButton: true, delegate: self)
private lazy var inputTextView = InputTextView(delegate: self)
2021-02-10 04:43:57 +01:00
2021-02-15 03:51:26 +01:00
private lazy var additionalContentContainer: UIView = {
2021-02-10 04:43:57 +01:00
let result = UIView()
2021-02-10 05:33:39 +01:00
result.heightAnchor.constraint(greaterThanOrEqualToConstant: 4).isActive = true
2021-02-10 04:43:57 +01:00
return result
}()
2021-02-15 03:51:26 +01:00
// MARK: Settings
private static let linkPreviewViewInset: CGFloat = 6
2021-01-29 01:46:32 +01:00
// MARK: Lifecycle
init(delegate: InputViewDelegate) {
self.delegate = delegate
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
override init(frame: CGRect) {
preconditionFailure("Use init(delegate:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(delegate:) instead.")
}
private func setUpViewHierarchy() {
autoresizingMask = .flexibleHeight
// Background & blur
let backgroundView = UIView()
backgroundView.backgroundColor = isLightMode ? .white : .black
backgroundView.alpha = Values.lowOpacity
addSubview(backgroundView)
backgroundView.pin(to: self)
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
addSubview(blurView)
blurView.pin(to: self)
// Separator
let separator = UIView()
separator.backgroundColor = Colors.text.withAlphaComponent(0.2)
separator.set(.height, to: 1 / UIScreen.main.scale)
addSubview(separator)
separator.pin([ UIView.HorizontalEdge.leading, UIView.VerticalEdge.top, UIView.HorizontalEdge.trailing ], to: self)
// Buttons
func container(for button: InputViewButton) -> UIView {
let result = UIView()
result.addSubview(button)
result.set(.width, to: InputViewButton.expandedSize)
result.set(.height, to: InputViewButton.expandedSize)
button.center(in: result)
return result
}
let (cameraButtonContainer, libraryButtonContainer, gifButtonContainer, documentButtonContainer) = (container(for: cameraButton), container(for: libraryButton), container(for: gifButton), container(for: documentButton))
let buttonStackView = UIStackView(arrangedSubviews: [ cameraButtonContainer, libraryButtonContainer, gifButtonContainer, documentButtonContainer, UIView.hStretchingSpacer() ])
buttonStackView.axis = .horizontal
buttonStackView.spacing = Values.smallSpacing
// Bottom stack view
let bottomStackView = UIStackView(arrangedSubviews: [ inputTextView, container(for: sendButton) ])
bottomStackView.axis = .horizontal
bottomStackView.spacing = Values.smallSpacing
2021-02-10 05:33:39 +01:00
bottomStackView.alignment = .center
2021-01-29 01:46:32 +01:00
// Main stack view
2021-02-15 03:51:26 +01:00
let mainStackView = UIStackView(arrangedSubviews: [ buttonStackView, additionalContentContainer, bottomStackView ])
2021-01-29 01:46:32 +01:00
mainStackView.axis = .vertical
mainStackView.isLayoutMarginsRelativeArrangement = true
let adjustment = (InputViewButton.expandedSize - InputViewButton.size) / 2
mainStackView.layoutMargins = UIEdgeInsets(top: Values.smallSpacing, leading: Values.largeSpacing, bottom: Values.smallSpacing, trailing: Values.largeSpacing - adjustment)
addSubview(mainStackView)
mainStackView.pin(.top, to: .bottom, of: separator)
mainStackView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: self)
2021-02-10 05:33:39 +01:00
mainStackView.pin(.bottom, to: .bottom, of: self, withInset: -2)
2021-01-29 01:46:32 +01:00
}
// MARK: Updating
func inputTextViewDidChangeSize(_ inputTextView: InputTextView) {
invalidateIntrinsicContentSize()
2021-02-15 03:51:26 +01:00
doLinkPreviewThingies()
2021-01-29 01:46:32 +01:00
}
2021-02-10 04:43:57 +01:00
private func handleQuoteDraftChanged() {
2021-02-15 03:51:26 +01:00
additionalContentContainer.subviews.forEach { $0.removeFromSuperview() }
2021-02-15 04:45:46 +01:00
linkPreviewInfo = nil
2021-02-10 04:43:57 +01:00
guard let quoteDraftInfo = quoteDraftInfo else { return }
let direction: QuoteView.Direction = quoteDraftInfo.isOutgoing ? .outgoing : .incoming
let hInset: CGFloat = 6
2021-02-15 03:51:26 +01:00
let maxWidth = additionalContentContainer.bounds.width
2021-02-10 07:04:26 +01:00
let quoteView = QuoteView(for: quoteDraftInfo.model, direction: direction, hInset: hInset, maxWidth: maxWidth, delegate: self)
2021-02-15 03:51:26 +01:00
additionalContentContainer.addSubview(quoteView)
quoteView.pin(.left, to: .left, of: additionalContentContainer, withInset: hInset)
quoteView.pin(.top, to: .top, of: additionalContentContainer, withInset: 12)
quoteView.pin(.right, to: .right, of: additionalContentContainer, withInset: -hInset)
quoteView.pin(.bottom, to: .bottom, of: additionalContentContainer, withInset: -6)
}
private func doLinkPreviewThingies() {
additionalContentContainer.subviews.forEach { $0.removeFromSuperview() }
2021-02-15 04:45:46 +01:00
quoteDraftInfo = nil
// Suggest that the user enable link previews if they haven't already, and we haven't
// told them about link previews yet
2021-02-15 03:51:26 +01:00
let text = inputTextView.text!
let userDefaults = UserDefaults.standard
if !OWSLinkPreview.allPreviewUrls(forMessageBodyText: text).isEmpty && !SSKPreferences.areLinkPreviewsEnabled
&& !userDefaults[.hasSeenLinkPreviewSuggestion] {
// TODO: Show suggestion
userDefaults[.hasSeenLinkPreviewSuggestion] = true
}
2021-02-15 04:45:46 +01:00
// Check that link previews are enabled
guard SSKPreferences.areLinkPreviewsEnabled else { return }
// Check that a valid URL is present
2021-02-15 03:51:26 +01:00
guard let linkPreviewURL = OWSLinkPreview.previewUrl(forRawBodyText: text, selectedRange: inputTextView.selectedRange) else {
return
}
2021-02-15 04:45:46 +01:00
// Guard against obsolete updates
guard linkPreviewURL != self.linkPreviewInfo?.url else { return }
// Set the state to loading
linkPreviewInfo = (url: linkPreviewURL, draft: nil)
2021-02-15 03:51:26 +01:00
linkPreviewView.linkPreviewState = LinkPreviewLoading()
2021-02-15 04:45:46 +01:00
// Add the link preview view
2021-02-15 03:51:26 +01:00
additionalContentContainer.addSubview(linkPreviewView)
linkPreviewView.pin(.left, to: .left, of: additionalContentContainer, withInset: InputView.linkPreviewViewInset)
linkPreviewView.pin(.top, to: .top, of: additionalContentContainer, withInset: 10)
linkPreviewView.pin(.right, to: .right, of: additionalContentContainer)
linkPreviewView.pin(.bottom, to: .bottom, of: additionalContentContainer, withInset: -4)
2021-02-15 04:45:46 +01:00
// Build the link preview
2021-02-15 03:51:26 +01:00
OWSLinkPreview.tryToBuildPreviewInfo(previewUrl: linkPreviewURL).done { [weak self] draft in
guard let self = self else { return }
2021-02-15 04:45:46 +01:00
guard self.linkPreviewInfo?.url == linkPreviewURL else { return } // Obsolete
self.linkPreviewInfo = (url: linkPreviewURL, draft: draft)
2021-02-15 03:51:26 +01:00
self.linkPreviewView.linkPreviewState = LinkPreviewDraft(linkPreviewDraft: draft)
}.catch { _ in
2021-02-15 04:45:46 +01:00
guard self.linkPreviewInfo?.url == linkPreviewURL else { return } // Obsolete
self.linkPreviewInfo = nil
self.additionalContentContainer.subviews.forEach { $0.removeFromSuperview() }
2021-02-15 03:51:26 +01:00
}.retainUntilComplete()
2021-02-10 04:43:57 +01:00
}
2021-01-29 01:46:32 +01:00
// MARK: Interaction
func handleInputViewButtonTapped(_ inputViewButton: InputViewButton) {
if inputViewButton == cameraButton { delegate.handleCameraButtonTapped() }
if inputViewButton == libraryButton { delegate.handleLibraryButtonTapped() }
if inputViewButton == gifButton { delegate.handleGIFButtonTapped() }
if inputViewButton == documentButton { delegate.handleDocumentButtonTapped() }
if inputViewButton == sendButton { delegate.handleSendButtonTapped() }
}
2021-02-10 07:04:26 +01:00
func handleQuoteViewCancelButtonTapped() {
delegate.handleQuoteViewCancelButtonTapped()
}
2021-01-29 01:46:32 +01:00
override func resignFirstResponder() -> Bool {
inputTextView.resignFirstResponder()
}
2021-02-15 03:51:26 +01:00
func handleLongPress() {
// Not relevant in this case
}
2021-02-15 04:45:46 +01:00
func handleLinkPreviewCanceled() {
linkPreviewInfo = nil
additionalContentContainer.subviews.forEach { $0.removeFromSuperview() }
}
2021-01-29 01:46:32 +01:00
}
// MARK: Delegate
protocol InputViewDelegate {
2021-02-15 03:51:26 +01:00
2021-01-29 01:46:32 +01:00
func handleCameraButtonTapped()
func handleLibraryButtonTapped()
func handleGIFButtonTapped()
func handleDocumentButtonTapped()
func handleSendButtonTapped()
2021-02-10 07:04:26 +01:00
func handleQuoteViewCancelButtonTapped()
2021-01-29 01:46:32 +01:00
}