ChatViewController2.swift 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import UIKit
  2. import DcCore
  3. class ChatViewController2: UIViewController {
  4. var dcContext: DcContext
  5. let chatId: Int
  6. var messageIds: [Int] = []
  7. var heightConstraint: NSLayoutConstraint?
  8. var bottomInset: CGFloat {
  9. get {
  10. logger.debug("bottomInset - get: \(heightConstraint?.constant ?? 0)")
  11. return heightConstraint?.constant ?? 0
  12. }
  13. set {
  14. logger.debug("bottomInset - set: \(newValue)")
  15. heightConstraint?.constant = newValue
  16. }
  17. }
  18. lazy var isGroupChat: Bool = {
  19. return dcContext.getChat(chatId: chatId).isGroup
  20. }()
  21. lazy var draft: DraftModel = {
  22. let draft = DraftModel(dcContext: dcContext, chatId: chatId)
  23. return draft
  24. }()
  25. lazy var tableView: UITableView = {
  26. let tableView: UITableView = UITableView(frame: .zero)
  27. tableView.translatesAutoresizingMaskIntoConstraints = false
  28. tableView.delegate = self
  29. tableView.dataSource = self
  30. return tableView
  31. }()
  32. lazy var textView: ChatInputTextView = {
  33. let textView = ChatInputTextView()
  34. textView.translatesAutoresizingMaskIntoConstraints = false
  35. textView.font = UIFont.preferredFont(forTextStyle: .body)
  36. textView.backgroundColor = DcColors.inputFieldColor
  37. textView.isEditable = true
  38. return textView
  39. }()
  40. lazy var dummyView: UIView = {
  41. let view = UIView(frame: .zero)
  42. view.translatesAutoresizingMaskIntoConstraints = false
  43. view.backgroundColor = .yellow
  44. view.addSubview(textView)
  45. return view
  46. }()
  47. private lazy var keyboardManager: KeyboardManager? = {
  48. let manager = KeyboardManager()
  49. return manager
  50. }()
  51. public lazy var backgroundContainer: UIImageView = {
  52. let view = UIImageView()
  53. view.contentMode = .scaleAspectFill
  54. view.backgroundColor = .blue
  55. if let backgroundImageName = UserDefaults.standard.string(forKey: Constants.Keys.backgroundImageName) {
  56. view.sd_setImage(with: Utils.getBackgroundImageURL(name: backgroundImageName),
  57. placeholderImage: nil,
  58. options: [.retryFailed]) { [weak self] (_, error, _, _) in
  59. if let error = error {
  60. logger.error("Error loading background image: \(error.localizedDescription)" )
  61. DispatchQueue.main.async { [weak self] in
  62. self?.setDefaultBackgroundImage(view: view)
  63. }
  64. }
  65. }
  66. } else {
  67. setDefaultBackgroundImage(view: view)
  68. }
  69. return view
  70. }()
  71. init(dcContext: DcContext, chatId: Int, highlightedMsg: Int? = nil) {
  72. self.dcContext = dcContext
  73. self.chatId = chatId
  74. super.init(nibName: nil, bundle: nil)
  75. hidesBottomBarWhenPushed = true
  76. }
  77. required init?(coder aDecoder: NSCoder) {
  78. fatalError("init(coder:) has not been implemented")
  79. }
  80. override func viewDidLoad() {
  81. super.viewDidLoad()
  82. setupSubviews()
  83. tableView.backgroundView = backgroundContainer
  84. tableView.register(TextMessageCell.self, forCellReuseIdentifier: "text")
  85. tableView.rowHeight = UITableView.automaticDimension
  86. tableView.separatorStyle = .none
  87. tableView.keyboardDismissMode = .interactive
  88. navigationController?.setNavigationBarHidden(false, animated: false)
  89. if #available(iOS 13.0, *) {
  90. navigationController?.navigationBar.scrollEdgeAppearance = navigationController?.navigationBar.standardAppearance
  91. }
  92. navigationItem.backButtonTitle = String.localized("chat")
  93. definesPresentationContext = true
  94. if !dcContext.isConfigured() {
  95. // TODO: display message about nothing being configured
  96. return
  97. }
  98. // Binding to the tableView will enable interactive dismissal
  99. keyboardManager?.bind(to: tableView)
  100. keyboardManager?.on(event: .willChangeFrame) { [weak self] event in
  101. guard let self = self else { return }
  102. if self.keyboardManager?.isKeyboardHidden ?? true {
  103. return
  104. }
  105. logger.debug("willChangeFrame \(event)")
  106. let keyboardScreenEndFrame = event.endFrame
  107. self.bottomInset = self.getInputTextHeight() + self.dummyView.convert(keyboardScreenEndFrame, from: self.view.window).height
  108. }.on(event: .willHide) { [weak self] event in
  109. guard let self = self else { return }
  110. logger.debug("willHide \(event)")
  111. self.bottomInset = self.getInputTextHeight()
  112. }.on(event: .didHide) { [weak self] event in
  113. guard let self = self else { return }
  114. logger.debug("didHide \(event)")
  115. self.bottomInset = self.getInputTextHeight()
  116. }.on(event: .willShow) { [weak self] event in
  117. guard let self = self else { return }
  118. logger.debug("willShow \(event)")
  119. self.bottomInset = self.getInputTextHeight() + self.dummyView.convert(event.endFrame, from: self.view.window).height
  120. UIView.animate(withDuration: event.timeInterval, delay: 0, options: event.animationOptions, animations: {
  121. self.dummyView.layoutIfNeeded()
  122. })
  123. }
  124. loadMessages()
  125. }
  126. private func getInputTextHeight() -> CGFloat {
  127. return 70
  128. }
  129. private func loadMessages() {
  130. // update message ids
  131. var msgIds = dcContext.getChatMsgs(chatId: chatId)
  132. let freshMsgsCount = self.dcContext.getUnreadMessages(chatId: self.chatId)
  133. if freshMsgsCount > 0 && msgIds.count >= freshMsgsCount {
  134. let index = msgIds.count - freshMsgsCount
  135. msgIds.insert(Int(DC_MSG_ID_MARKER1), at: index)
  136. }
  137. self.messageIds = msgIds
  138. self.reloadData()
  139. }
  140. private func reloadData() {
  141. let selectredRows = tableView.indexPathsForSelectedRows
  142. tableView.reloadData()
  143. // There's an iOS bug, filling up the console output but which can be ignored: https://developer.apple.com/forums/thread/668295
  144. // [Assert] Attempted to call -cellForRowAtIndexPath: on the table view while it was in the process of updating its visible cells, which is not allowed.
  145. selectredRows?.forEach({ (selectedRow) in
  146. tableView.selectRow(at: selectedRow, animated: false, scrollPosition: .none)
  147. })
  148. }
  149. func setupSubviews() {
  150. view.addSubview(tableView)
  151. view.addSubview(dummyView)
  152. view.addConstraints([
  153. tableView.constraintAlignTopToAnchor(view.topAnchor),
  154. tableView.constraintAlignLeadingToAnchor(view.safeAreaLayoutGuide.leadingAnchor),
  155. tableView.constraintAlignTrailingToAnchor(view.safeAreaLayoutGuide.trailingAnchor),
  156. tableView.constraintAlignBottomToAnchor(dummyView.topAnchor),
  157. dummyView.constraintAlignLeadingToAnchor(view.safeAreaLayoutGuide.leadingAnchor),
  158. dummyView.constraintAlignTrailingToAnchor(view.safeAreaLayoutGuide.trailingAnchor),
  159. dummyView.constraintAlignBottomToAnchor(view.bottomAnchor),
  160. textView.constraintAlignTopTo(dummyView),
  161. textView.constraintAlignLeadingTo(dummyView),
  162. textView.constraintAlignTrailingTo(dummyView),
  163. textView.constraintAlignBottomTo(dummyView),
  164. ])
  165. heightConstraint = dummyView.constraintMinHeightTo(bottomInset)
  166. bottomInset = getInputTextHeight()
  167. heightConstraint?.isActive = true
  168. navigationItem.title = "new Chat UI"
  169. }
  170. private func configureUIForWriting() {
  171. tableView.allowsMultipleSelectionDuringEditing = true
  172. tableView.dragInteractionEnabled = true
  173. }
  174. private func setDefaultBackgroundImage(view: UIImageView) {
  175. if #available(iOS 12.0, *) {
  176. view.image = UIImage(named: traitCollection.userInterfaceStyle == .light ? "background_light" : "background_dark")
  177. } else {
  178. view.image = UIImage(named: "background_light")
  179. }
  180. }
  181. }
  182. extension ChatViewController2: UITableViewDelegate {
  183. }
  184. extension ChatViewController2: UITableViewDataSource {
  185. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  186. messageIds.count
  187. }
  188. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  189. return UITableViewCell()
  190. }
  191. }