NewChatViewController.swift 13 KB

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