NewChatViewController.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. import ALCameraViewController
  2. import Contacts
  3. import UIKit
  4. class NewChatViewController: UITableViewController {
  5. weak var coordinator: NewChatCoordinator?
  6. private lazy var searchController: UISearchController = {
  7. let searchController = UISearchController(searchResultsController: nil)
  8. searchController.searchResultsUpdater = self
  9. searchController.obscuresBackgroundDuringPresentation = false
  10. searchController.searchBar.placeholder = "Search Contact"
  11. return searchController
  12. }()
  13. var contactIds: [Int] = Utils.getContactIds() {
  14. didSet {
  15. tableView.reloadData()
  16. }
  17. }
  18. // contactWithSearchResults.indexesToHightLight empty by default
  19. var contacts: [ContactWithSearchResults] {
  20. return contactIds.map { ContactWithSearchResults(contact: MRContact(id: $0), indexesToHighlight: []) }
  21. }
  22. // used when seachbar is active
  23. var filteredContacts: [ContactWithSearchResults] = []
  24. // searchBar active?
  25. func isFiltering() -> Bool {
  26. return searchController.isActive && !searchBarIsEmpty()
  27. }
  28. // weak var chatDisplayer: ChatDisplayer?
  29. var syncObserver: Any?
  30. var hud: ProgressHud?
  31. lazy var deviceContactHandler: DeviceContactsHandler = {
  32. let handler = DeviceContactsHandler()
  33. handler.contactListDelegate = self
  34. return handler
  35. }()
  36. var deviceContactAccessGranted: Bool = false {
  37. didSet {
  38. tableView.reloadData()
  39. }
  40. }
  41. init() {
  42. super.init(style: .grouped)
  43. hidesBottomBarWhenPushed = true
  44. }
  45. required init?(coder _: NSCoder) {
  46. fatalError("init(coder:) has not been implemented")
  47. }
  48. override func viewDidLoad() {
  49. super.viewDidLoad()
  50. title = "New Chat"
  51. deviceContactHandler.importDeviceContacts()
  52. navigationItem.searchController = searchController
  53. definesPresentationContext = true // to make sure searchbar will only be shown in this viewController
  54. }
  55. override func viewWillAppear(_ animated: Bool) {
  56. super.viewWillAppear(animated)
  57. deviceContactAccessGranted = CNContactStore.authorizationStatus(for: .contacts) == .authorized
  58. contactIds = Utils.getContactIds()
  59. // this will show the searchbar on launch -> will be set back to true on viewDidAppear
  60. if #available(iOS 11.0, *) {
  61. navigationItem.hidesSearchBarWhenScrolling = false
  62. }
  63. }
  64. override func viewDidAppear(_ animated: Bool) {
  65. super.viewDidAppear(animated)
  66. if #available(iOS 11.0, *) {
  67. navigationItem.hidesSearchBarWhenScrolling = true
  68. }
  69. let nc = NotificationCenter.default
  70. syncObserver = nc.addObserver(
  71. forName: dcNotificationSecureJoinerProgress,
  72. object: nil,
  73. queue: nil
  74. ) {
  75. notification in
  76. if let ui = notification.userInfo {
  77. if ui["error"] as! Bool {
  78. self.hud?.error(ui["errorMessage"] as? String)
  79. } else if ui["done"] as! Bool {
  80. self.hud?.done()
  81. } else {
  82. self.hud?.progress(ui["progress"] as! Int)
  83. }
  84. }
  85. }
  86. }
  87. override func viewWillDisappear(_: Bool) {
  88. title = "Chats" /* hack: when navigating to chatView (removing this viewController), there was a delayed backButton update (showing 'New Chat' for a moment) */
  89. }
  90. override func viewDidDisappear(_ animated: Bool) {
  91. super.viewDidDisappear(animated)
  92. let nc = NotificationCenter.default
  93. if let syncObserver = self.syncObserver {
  94. nc.removeObserver(syncObserver)
  95. }
  96. }
  97. @objc func cancelButtonPressed() {
  98. dismiss(animated: true, completion: nil)
  99. }
  100. override func didReceiveMemoryWarning() {
  101. super.didReceiveMemoryWarning()
  102. // Dispose of any resources that can be recreated.
  103. }
  104. // MARK: - Table view data source
  105. override func numberOfSections(in _: UITableView) -> Int {
  106. return deviceContactAccessGranted ? 2 : 3
  107. }
  108. override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
  109. if section == 0 {
  110. return 3
  111. } else if section == 1 {
  112. if deviceContactAccessGranted {
  113. return isFiltering() ? filteredContacts.count : contacts.count
  114. } else {
  115. return 1
  116. }
  117. } else {
  118. return isFiltering() ? filteredContacts.count : contacts.count
  119. }
  120. }
  121. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  122. let section = indexPath.section
  123. let row = indexPath.row
  124. if section == 0 {
  125. if row == 0 {
  126. // new group row
  127. let cell: UITableViewCell
  128. if let c = tableView.dequeueReusableCell(withIdentifier: "newContactCell") {
  129. cell = c
  130. } else {
  131. cell = UITableViewCell(style: .default, reuseIdentifier: "newContactCell")
  132. }
  133. cell.textLabel?.text = "New Group"
  134. cell.textLabel?.textColor = view.tintColor
  135. return cell
  136. }
  137. if row == 1 {
  138. // new contact row
  139. let cell: UITableViewCell
  140. if let c = tableView.dequeueReusableCell(withIdentifier: "scanGroupCell") {
  141. cell = c
  142. } else {
  143. cell = UITableViewCell(style: .default, reuseIdentifier: "scanGroupCell")
  144. }
  145. cell.textLabel?.text = "Scan Group QR Code"
  146. cell.textLabel?.textColor = view.tintColor
  147. return cell
  148. }
  149. if row == 2 {
  150. // new contact row
  151. let cell: UITableViewCell
  152. if let c = tableView.dequeueReusableCell(withIdentifier: "newContactCell") {
  153. cell = c
  154. } else {
  155. cell = UITableViewCell(style: .default, reuseIdentifier: "newContactCell")
  156. }
  157. cell.textLabel?.text = "New Contact"
  158. cell.textLabel?.textColor = view.tintColor
  159. return cell
  160. }
  161. } else if section == 1 {
  162. if deviceContactAccessGranted {
  163. let cell: ContactCell
  164. if let c = tableView.dequeueReusableCell(withIdentifier: "contactCell") as? ContactCell {
  165. cell = c
  166. } else {
  167. cell = ContactCell(style: .default, reuseIdentifier: "contactCell")
  168. }
  169. let contact: ContactWithSearchResults = isFiltering() ? filteredContacts[row] : contacts[row]
  170. updateContactCell(cell: cell, contactWithHighlight: contact)
  171. return cell
  172. } else {
  173. let cell: ActionCell
  174. if let c = tableView.dequeueReusableCell(withIdentifier: "actionCell") as? ActionCell {
  175. cell = c
  176. } else {
  177. cell = ActionCell(style: .default, reuseIdentifier: "actionCell")
  178. }
  179. cell.actionTitle = "Import Device Contacts"
  180. return cell
  181. }
  182. } else {
  183. // section 2
  184. let cell: ContactCell
  185. if let c = tableView.dequeueReusableCell(withIdentifier: "contactCell") as? ContactCell {
  186. cell = c
  187. } else {
  188. cell = ContactCell(style: .default, reuseIdentifier: "contactCell")
  189. }
  190. let contact: ContactWithSearchResults = isFiltering() ? filteredContacts[row] : contacts[row]
  191. updateContactCell(cell: cell, contactWithHighlight: contact)
  192. return cell
  193. }
  194. // will actually never get here but compiler not happy
  195. return UITableViewCell(style: .default, reuseIdentifier: "cell")
  196. }
  197. override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
  198. let row = indexPath.row
  199. let section = indexPath.section
  200. if section == 0 {
  201. if row == 0 {
  202. coordinator?.showNewGroupController()
  203. }
  204. if row == 1 {
  205. if UIImagePickerController.isSourceTypeAvailable(.camera) {
  206. coordinator?.showQRCodeController()
  207. } else {
  208. let alert = UIAlertController(title: "Camera is not available", message: nil, preferredStyle: .alert)
  209. alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: { _ in
  210. self.dismiss(animated: true, completion: nil)
  211. }))
  212. present(alert, animated: true, completion: nil)
  213. }
  214. }
  215. if row == 2 {
  216. coordinator?.showNewContactController()
  217. }
  218. } else if section == 1 {
  219. if deviceContactAccessGranted {
  220. if searchController.isActive {
  221. // edge case: when searchController is active but searchBar is empty -> filteredContacts is empty, so we fallback to contactIds
  222. let contactId = isFiltering() ? filteredContacts[row].contact.id : contactIds[row]
  223. searchController.dismiss(animated: false, completion: {
  224. self.coordinator?.showNewChat(contactId: contactId)
  225. })
  226. } else {
  227. let contactId = contactIds[row]
  228. coordinator?.showNewChat(contactId: contactId)
  229. }
  230. } else {
  231. showSettingsAlert()
  232. }
  233. } else {
  234. let contactIndex = row
  235. let contactId = contactIds[contactIndex]
  236. coordinator?.showNewChat(contactId: contactId)
  237. }
  238. }
  239. private func updateContactCell(cell: ContactCell, contactWithHighlight: ContactWithSearchResults) {
  240. let contact = contactWithHighlight.contact
  241. if let nameHighlightedIndexes = contactWithHighlight.indexesToHighlight.filter({ $0.contactDetail == .NAME }).first,
  242. let emailHighlightedIndexes = contactWithHighlight.indexesToHighlight.filter({ $0.contactDetail == .EMAIL }).first {
  243. // gets here when contact is a result of current search -> highlights relevant indexes
  244. let nameLabelFontSize = cell.nameLabel.font.pointSize
  245. let emailLabelFontSize = cell.emailLabel.font.pointSize
  246. cell.nameLabel.attributedText = contact.name.boldAt(indexes: nameHighlightedIndexes.indexes, fontSize: nameLabelFontSize)
  247. cell.emailLabel.attributedText = contact.email.boldAt(indexes: emailHighlightedIndexes.indexes, fontSize: emailLabelFontSize)
  248. } else {
  249. cell.nameLabel.text = contact.name
  250. cell.emailLabel.text = contact.email
  251. }
  252. cell.initialsLabel.text = Utils.getInitials(inputName: contact.name)
  253. cell.setColor(contact.color)
  254. }
  255. private func searchBarIsEmpty() -> Bool {
  256. return searchController.searchBar.text?.isEmpty ?? true
  257. }
  258. private func filterContentForSearchText(_ searchText: String, scope _: String = "All") {
  259. let contactsWithHighlights: [ContactWithSearchResults] = contacts.map { contact in
  260. let indexes = contact.contact.contains(searchText: searchText)
  261. return ContactWithSearchResults(contact: contact.contact, indexesToHighlight: indexes)
  262. }
  263. filteredContacts = contactsWithHighlights.filter { !$0.indexesToHighlight.isEmpty }
  264. tableView.reloadData()
  265. }
  266. }
  267. extension NewChatViewController: QrCodeReaderDelegate {
  268. func handleQrCode(_ code: String) {
  269. logger.info("decoded: \(code)")
  270. let check = dc_check_qr(mailboxPointer, code)!
  271. logger.info("got ver: \(check)")
  272. if dc_lot_get_state(check) == DC_QR_ASK_VERIFYGROUP {
  273. hud = ProgressHud("Synchronizing Account", in: view)
  274. DispatchQueue.global(qos: .userInitiated).async {
  275. let id = dc_join_securejoin(mailboxPointer, code)
  276. DispatchQueue.main.async {
  277. self.dismiss(animated: true) {
  278. self.coordinator?.showChat(chatId: Int(id))
  279. // self.chatDisplayer?.displayChatForId(chatId: Int(id))
  280. }
  281. }
  282. }
  283. } else {
  284. let alert = UIAlertController(title: "Not a valid group QR Code", message: code, preferredStyle: .alert)
  285. alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: { _ in
  286. self.dismiss(animated: true, completion: nil)
  287. }))
  288. present(alert, animated: true, completion: nil)
  289. }
  290. dc_lot_unref(check)
  291. }
  292. }
  293. extension NewChatViewController: ContactListDelegate {
  294. func deviceContactsImported() {
  295. contactIds = Utils.getContactIds()
  296. // tableView.reloadData()
  297. }
  298. func accessGranted() {
  299. deviceContactAccessGranted = true
  300. }
  301. func accessDenied() {
  302. deviceContactAccessGranted = false
  303. }
  304. private func showSettingsAlert() {
  305. let alert = UIAlertController(
  306. title: "Import Contacts from to your device",
  307. message: "To chat with contacts from your device open the settings menu and enable the Contacts option",
  308. preferredStyle: .alert
  309. )
  310. alert.addAction(UIAlertAction(title: "Open Settings", style: .default) { _ in
  311. UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
  312. })
  313. alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
  314. })
  315. present(alert, animated: true)
  316. }
  317. }
  318. extension NewChatViewController: UISearchResultsUpdating {
  319. func updateSearchResults(for searchController: UISearchController) {
  320. if let searchText = searchController.searchBar.text {
  321. filterContentForSearchText(searchText)
  322. }
  323. }
  324. }
  325. struct ContactHighlights {
  326. let contactDetail: ContactDetail
  327. let indexes: [Int]
  328. }
  329. enum ContactDetail {
  330. case NAME
  331. case EMAIL
  332. }
  333. struct ContactWithSearchResults {
  334. let contact: MRContact
  335. let indexesToHighlight: [ContactHighlights]
  336. }