| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- //
- // FloatChatInputController.swift
- // TUIRoomKit
- //
- // Created by CY zhao on 2024/5/11.
- // Copyright © 2024 Tencent. All rights reserved.
- //
- import UIKit
- import SnapKit
- import Foundation
- import TUICore
- import Factory
- class FloatChatInputController: UIViewController {
- @Injected(\.floatChatService) private var store: FloatChatStoreProvider
- @Injected(\.conferenceStore) private var operation
-
- private var textViewBottomConstraint: Constraint?
- private var textViewHeightConstraint: Constraint?
- private var emojiPanelTopConstraint: Constraint?
- private let maxNumberOfLines = 3
- private let emojiPanelHeight = 274.0
-
- private let inputBarView: UIView = {
- let view = UIView()
- view.backgroundColor = UIColor.tui_color(withHex: "#22262E")
- return view
- }()
-
- private let emojiButton: LargeTapAreaButton = {
- let button = LargeTapAreaButton()
- let img = UIImage(named: "room_emoji_icon", in: tuiRoomKitBundle(), compatibleWith: nil)
- button.setImage(img, for: .normal)
- return button
- }()
-
- private let inputTextView: UITextView = {
- let view = UITextView(frame: .zero)
- view.font = UIFont.systemFont(ofSize: 17.5)
- view.returnKeyType = UIReturnKeyType.send
- view.enablesReturnKeyAutomatically = true
- view.textContainer.lineBreakMode = .byCharWrapping
- view.textContainerInset = UIEdgeInsets(top: view.textContainerInset.top, left: 10, bottom: view.textContainerInset.bottom, right: 10)
- view.textContainer.lineFragmentPadding = 0
- view.layer.cornerRadius = view.sizeThatFits(.zero).height / 2
- view.layer.masksToBounds = true
- view.isHidden = true
- view.textColor = UIColor.tui_color(withHex: "#D5F4F2", alpha: 0.6)
- view.backgroundColor = UIColor.tui_color(withHex: "#4F586B", alpha: 0.3)
- return view
- }()
-
- private let sendButton: UIButton = {
- let button = UIButton()
- button.setTitle(.sendText, for: .normal)
- button.layer.cornerRadius = 18
- button.backgroundColor = UIColor.tui_color(withHex: "#006CFF")
- return button
- }()
-
- private let backgroundView: UIView = {
- let view = UITextView(frame: .zero)
- view.backgroundColor = UIColor.tui_color(withHex: "#22262E")
- return view
- }()
-
- private lazy var emojiPanel: EmotionBoardView = {
- let emotionBoardView = EmotionBoardView()
- let emotionHelper = EmotionHelper.shared
- emotionBoardView.emotions = emotionHelper.emotions
- emotionBoardView.delegate = self
- emotionBoardView.backgroundColor = UIColor.tui_color(withHex: "#22262E")
- emotionBoardView.isHidden = true
- return emotionBoardView
- }()
-
- private lazy var maxHeightOfTextView: CGFloat = {
- let lineHeight = inputTextView.font?.lineHeight ?? 0
- return ceil(lineHeight * CGFloat(maxNumberOfLines) + inputTextView.textContainerInset.top + inputTextView.textContainerInset.bottom)
- }()
-
- override func viewDidLoad() {
- super.viewDidLoad()
- constructViewHierarchy()
- activateConstraints()
- bindInteraction()
- }
-
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- showInputView()
- }
-
- private func constructViewHierarchy() {
- inputBarView.addSubview(emojiButton)
- inputBarView.addSubview(inputTextView)
- inputBarView.addSubview(sendButton)
- view.addSubview(backgroundView)
- view.addSubview(inputBarView)
- view.addSubview(emojiPanel)
- }
-
- private func activateConstraints() {
- backgroundView.snp.makeConstraints { make in
- make.leading.trailing.equalToSuperview()
- make.bottom.equalToSuperview()
- make.top.equalTo(inputBarView.snp.top)
- }
-
- inputBarView.snp.makeConstraints { make in
- make.leading.trailing.equalToSuperview()
- make.height.equalTo(inputTextView).offset(2 * 12)
- textViewBottomConstraint = make.bottom.equalTo(view).constraint
- }
- emojiButton.snp.makeConstraints { make in
- make.width.height.equalTo(24)
- make.centerY.equalToSuperview()
- make.leading.equalTo(view.safeAreaLayoutGuide.snp.leading).offset(10)
- }
- sendButton.snp.makeConstraints { make in
- make.width.equalTo(64)
- make.height.equalTo(36)
- make.centerY.equalToSuperview()
- make.trailing.equalTo(view.safeAreaLayoutGuide.snp.trailing).offset(-10)
- }
- inputTextView.snp.makeConstraints { make in
- make.leading.equalTo(emojiButton.snp.trailing).offset(10)
- make.trailing.equalTo(sendButton.snp.leading).offset(-10)
- let size = inputTextView.sizeThatFits(.zero)
- textViewHeightConstraint = make.height.equalTo(size.height).constraint
- make.centerY.equalToSuperview()
- }
- emojiPanel.snp.makeConstraints { make in
- make.leading.trailing.equalTo(view.safeAreaLayoutGuide)
- make.height.equalTo(emojiPanelHeight)
- emojiPanelTopConstraint = make.top.equalTo(view.snp.bottom).constraint
- }
- }
-
- private func bindInteraction() {
- inputTextView.delegate = self
- emojiButton.addTarget(self, action: #selector(onEmojiButtonTapped), for: .touchUpInside)
- sendButton.addTarget(self, action: #selector(onSendButtonTapped), for: .touchUpInside)
- let tapGesture = UITapGestureRecognizer(target: self, action: #selector(hideInputView))
- view.addGestureRecognizer(tapGesture)
-
- NotificationCenter.default.addObserver(self,
- selector: #selector(keyboardWillShow),
- name: UIResponder.keyboardWillShowNotification,
- object: nil)
- }
-
- @objc private func keyboardWillShow(notification: NSNotification) {
- guard let keyboardRect: CGRect = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
- let curve: UInt = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt,
- let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
- else {
- return
- }
- let intersection = CGRectIntersection(keyboardRect, self.view.frame)
- UIView.animate(withDuration: duration, delay: 0.0, options: UIView.AnimationOptions(rawValue: curve)) { [weak self] in
- guard let self = self else { return }
- self.textViewBottomConstraint?.update(offset: -CGRectGetHeight(intersection))
- }
- }
-
- @objc private func onSendButtonTapped(sender: UIButton) {
- if inputTextView.normalText.isEmpty {
- operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .inputCannotBeEmpty, position: .center)))
- } else {
- store.dispatch(action: FloatChatActions.sendMessage(payload: inputTextView.normalText))
- store.dispatch(action: FloatChatActions.reportData(payload: .metricsBarrageSendMessage))
- }
- hideInputView()
- }
- private func showInputView() {
- inputTextView.isHidden = false
- inputTextView.becomeFirstResponder()
- }
-
- @objc private func hideInputView() {
- inputBarView.isHidden = true
- view.endEditing(true)
- store.dispatch(action: FloatViewActions.showFloatInputView(payload: false))
- }
-
- @objc private func onEmojiButtonTapped(sender: UIButton) {
- sender.isSelected = !sender.isSelected
- if sender.isSelected {
- showEmojiPanel()
- } else {
- hideEmojiPanel()
- }
- }
-
- private func showEmojiPanel() {
- inputTextView.resignFirstResponder()
- emojiPanel.isHidden = false
- UIView.animate(withDuration: 0.2, delay: 0, options: UIView.AnimationOptions.curveEaseInOut) { [weak self] in
- guard let self = self else { return }
- self.emojiPanelTopConstraint?.update(offset: -self.emojiPanelHeight)
- self.textViewBottomConstraint?.update(offset: -self.emojiPanelHeight)
- }
- }
-
- private func hideEmojiPanel() {
- UIView.animate(withDuration: 0.2, delay: 0, options: UIView.AnimationOptions.curveEaseInOut) { [weak self] in
- guard let self = self else { return }
- self.emojiPanelTopConstraint?.update(offset: self.emojiPanelHeight)
- } completion: {[weak self] _ in
- guard let self = self else { return }
- self.emojiPanel.isHidden = true
- self.inputTextView.becomeFirstResponder()
- }
- }
-
- private func updateTextViewHeight() {
- let currentHeight = ceil(inputTextView.sizeThatFits(CGSize(width: inputTextView.bounds.size.width, height: CGFloat.greatestFiniteMagnitude)).height)
- inputTextView.isScrollEnabled = currentHeight > maxHeightOfTextView
- if currentHeight <= maxHeightOfTextView {
- textViewHeightConstraint?.update(offset: currentHeight)
- }
- }
- }
- extension FloatChatInputController: UITextViewDelegate {
- func textViewDidBeginEditing(_ textView: UITextView) {
- inputTextView.becomeFirstResponder()
- }
- func textViewDidChange(_ textView: UITextView) {
- updateTextViewHeight()
- }
-
- func textViewDidEndEditing(_ textView: UITextView) {
- inputTextView.resignFirstResponder()
- }
- func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
- if text == "\n" {
- store.dispatch(action: FloatChatActions.sendMessage(payload: textView.normalText))
- hideInputView()
- return false
- }
- return true
- }
- }
- extension FloatChatInputController: EmotionBoardViewDelegate {
- func emotionView(emotionBoardView: EmotionBoardView, didSelectEmotion emotion: Emotion, atIndex index: Int) {
- let attributedString = EmotionHelper.shared.obtainImageAttributedString(byImageKey: emotion.displayName,
- font: inputTextView.font ?? UIFont(), useCache: false)
- inputTextView.insertEmotionAttributedString(emotionAttributedString: attributedString)
- }
-
- func emotionViewDidSelectDeleteButton(emotionBoardView: EmotionBoardView) {
- if !inputTextView.deleteEmotion() {
- inputTextView.deleteBackward()
- }
- }
- }
- class LargeTapAreaButton: UIButton {
- var tapAreaPadding: CGFloat = 20
- override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
- let largerBounds = bounds.insetBy(dx: -tapAreaPadding, dy: -tapAreaPadding)
- return largerBounds.contains(point)
- }
- }
- private extension String {
- static var sendText: String {
- localized("Send")
- }
- static let inputCannotBeEmpty = localized("Input can't be empty!")
- }
|