NewChatViewController.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. import ALCameraViewController
  2. import Contacts
  3. import UIKit
  4. class NewChatViewController: UITableViewController {
  5. weak var coordinator: NewChatCoordinator?
  6. private let sectionNew = 0
  7. private let sectionNewRowNewGroup = 0
  8. private let sectionNewRowNewContact = 1
  9. private let sectionNewRowCount = 2
  10. private let sectionImportedContacts = 1
  11. private var sectionContacts: Int { return deviceContactAccessGranted ? 1 : 2 }
  12. private var sectionsCount: Int { return deviceContactAccessGranted ? 2 : 3 }
  13. private lazy var searchController: UISearchController = {
  14. let searchController = UISearchController(searchResultsController: nil)
  15. searchController.searchResultsUpdater = self
  16. searchController.obscuresBackgroundDuringPresentation = false
  17. searchController.searchBar.placeholder = String.localized("search")
  18. return searchController
  19. }()
  20. var contactIds: [Int] = Utils.getContactIds() {
  21. didSet {
  22. tableView.reloadData()
  23. }
  24. }
  25. // contactWithSearchResults.indexesToHightLight empty by default
  26. var contacts: [ContactWithSearchResults] {
  27. return contactIds.map { ContactWithSearchResults(contact: DcContact(id: $0), indexesToHighlight: []) }
  28. }
  29. // used when seachbar is active
  30. var filteredContacts: [ContactWithSearchResults] = []
  31. // searchBar active?
  32. func isFiltering() -> Bool {
  33. return searchController.isActive && !searchBarIsEmpty()
  34. }
  35. // weak var chatDisplayer: ChatDisplayer?
  36. var syncObserver: Any?
  37. var hud: ProgressHud?
  38. lazy var deviceContactHandler: DeviceContactsHandler = {
  39. let handler = DeviceContactsHandler()
  40. handler.contactListDelegate = self
  41. return handler
  42. }()
  43. var deviceContactAccessGranted: Bool = false {
  44. didSet {
  45. tableView.reloadData()
  46. }
  47. }
  48. init() {
  49. super.init(style: .grouped)
  50. hidesBottomBarWhenPushed = true
  51. }
  52. required init?(coder _: NSCoder) {
  53. fatalError("init(coder:) has not been implemented")
  54. }
  55. override func viewDidLoad() {
  56. super.viewDidLoad()
  57. title = String.localized("menu_new_chat")
  58. deviceContactHandler.importDeviceContacts()
  59. navigationItem.searchController = searchController
  60. definesPresentationContext = true // to make sure searchbar will only be shown in this viewController
  61. if #available(iOS 11.0, *) {
  62. navigationItem.hidesSearchBarWhenScrolling = false
  63. }
  64. }
  65. override func viewWillAppear(_ animated: Bool) {
  66. super.viewWillAppear(animated)
  67. deviceContactAccessGranted = CNContactStore.authorizationStatus(for: .contacts) == .authorized
  68. contactIds = Utils.getContactIds()
  69. }
  70. override func viewDidAppear(_ animated: Bool) {
  71. super.viewDidAppear(animated)
  72. let nc = NotificationCenter.default
  73. syncObserver = nc.addObserver(
  74. forName: dcNotificationSecureJoinerProgress,
  75. object: nil,
  76. queue: nil
  77. ) { notification in
  78. if let ui = notification.userInfo {
  79. if ui["error"] as? Bool ?? false {
  80. self.hud?.error(ui["errorMessage"] as? String)
  81. } else if ui["done"] as? Bool ?? false {
  82. self.hud?.done()
  83. } else {
  84. self.hud?.progress(ui["progress"] as? Int ?? 0)
  85. }
  86. }
  87. }
  88. }
  89. override func viewDidDisappear(_ animated: Bool) {
  90. super.viewDidDisappear(animated)
  91. let nc = NotificationCenter.default
  92. if let syncObserver = self.syncObserver {
  93. nc.removeObserver(syncObserver)
  94. }
  95. }
  96. @objc func cancelButtonPressed() {
  97. dismiss(animated: true, completion: nil)
  98. }
  99. override func didReceiveMemoryWarning() {
  100. super.didReceiveMemoryWarning()
  101. // Dispose of any resources that can be recreated.
  102. }
  103. // MARK: - Table view data source
  104. override func numberOfSections(in _: UITableView) -> Int {
  105. return sectionsCount
  106. }
  107. override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
  108. if section == sectionNew {
  109. return sectionNewRowCount
  110. } else if section == sectionImportedContacts {
  111. if deviceContactAccessGranted {
  112. return isFiltering() ? filteredContacts.count : contacts.count
  113. } else {
  114. return 1
  115. }
  116. } else {
  117. return isFiltering() ? filteredContacts.count : contacts.count
  118. }
  119. }
  120. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  121. let section = indexPath.section
  122. let row = indexPath.row
  123. if section == sectionNew {
  124. if row == sectionNewRowNewGroup {
  125. // new group row
  126. let cell: UITableViewCell
  127. if let c = tableView.dequeueReusableCell(withIdentifier: "newContactCell") {
  128. cell = c
  129. } else {
  130. cell = UITableViewCell(style: .default, reuseIdentifier: "newContactCell")
  131. }
  132. cell.textLabel?.text = String.localized("menu_new_group")
  133. cell.textLabel?.textColor = view.tintColor
  134. return cell
  135. }
  136. if row == sectionNewRowNewContact {
  137. // new contact row
  138. let cell: UITableViewCell
  139. if let c = tableView.dequeueReusableCell(withIdentifier: "newContactCell") {
  140. cell = c
  141. } else {
  142. cell = UITableViewCell(style: .default, reuseIdentifier: "newContactCell")
  143. }
  144. cell.textLabel?.text = String.localized("menu_new_contact")
  145. cell.textLabel?.textColor = view.tintColor
  146. return cell
  147. }
  148. } else if section == sectionImportedContacts {
  149. // import device contacts section
  150. if deviceContactAccessGranted {
  151. let cell: ContactCell
  152. if let c = tableView.dequeueReusableCell(withIdentifier: "contactCell") as? ContactCell {
  153. cell = c
  154. } else {
  155. cell = ContactCell(style: .default, reuseIdentifier: "contactCell")
  156. }
  157. let contact: ContactWithSearchResults = contactSearchResultByRow(row)
  158. updateContactCell(cell: cell, contactWithHighlight: contact)
  159. return cell
  160. } else {
  161. let cell: ActionCell
  162. if let c = tableView.dequeueReusableCell(withIdentifier: "actionCell") as? ActionCell {
  163. cell = c
  164. } else {
  165. cell = ActionCell(style: .default, reuseIdentifier: "actionCell")
  166. }
  167. cell.actionTitle = String.localized("import_contacts")
  168. return cell
  169. }
  170. } else {
  171. // section contact list if device contacts are not imported
  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 = contactSearchResultByRow(row)
  179. updateContactCell(cell: cell, contactWithHighlight: contact)
  180. return cell
  181. }
  182. // will actually never get here but compiler not happy
  183. return UITableViewCell(style: .default, reuseIdentifier: "cell")
  184. }
  185. override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
  186. let row = indexPath.row
  187. let section = indexPath.section
  188. if section == sectionNew {
  189. if row == sectionNewRowNewGroup {
  190. coordinator?.showNewGroupController()
  191. }
  192. if row == sectionNewRowNewContact {
  193. coordinator?.showNewContactController()
  194. }
  195. } else if section == sectionImportedContacts {
  196. if deviceContactAccessGranted {
  197. showChatAt(row: row)
  198. } else {
  199. showSettingsAlert()
  200. }
  201. } else {
  202. showChatAt(row: row)
  203. }
  204. }
  205. override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
  206. if indexPath.section == sectionContacts {
  207. let contactId = contactIdByRow(indexPath.row)
  208. let edit = UITableViewRowAction(style: .normal, title: String.localized("info")) { [unowned self] _, _ in
  209. if self.searchController.isActive {
  210. self.searchController.dismiss(animated: false) {
  211. self.coordinator?.showContactDetail(contactId: contactId)
  212. }
  213. } else {
  214. self.coordinator?.showContactDetail(contactId: contactId)
  215. }
  216. }
  217. let delete = UITableViewRowAction(style: .destructive, title: String.localized("delete")) { [unowned self] _, _ in
  218. //handle delete
  219. if let dcContext = self.coordinator?.dcContext {
  220. let contactId = self.contactIdByRow(indexPath.row)
  221. self.askToDeleteContact(contactId: contactId, context: dcContext)
  222. }
  223. }
  224. edit.backgroundColor = DcColors.primary
  225. return [edit, delete]
  226. } else {
  227. return []
  228. }
  229. }
  230. override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
  231. return true
  232. }
  233. private func contactIdByRow(_ row: Int) -> Int {
  234. return isFiltering() ? filteredContacts[row].contact.id : contactIds[row]
  235. }
  236. private func contactSearchResultByRow(_ row: Int) -> ContactWithSearchResults {
  237. return isFiltering() ? filteredContacts[row] : contacts[row]
  238. }
  239. private func askToDeleteContact(contactId: Int, context: DcContext) {
  240. let contact = DcContact(id: contactId)
  241. let alert = UIAlertController(title: String.localizedStringWithFormat(String.localized("ask_delete_contact"), contact.nameNAddr),
  242. message: nil,
  243. preferredStyle: .actionSheet)
  244. alert.addAction(UIAlertAction(title: String.localized("delete"), style: .destructive, handler: { _ in
  245. self.dismiss(animated: true, completion: nil)
  246. if context.deleteContact(contactId: contactId) {
  247. self.contactIds = Utils.getContactIds()
  248. self.tableView.reloadData()
  249. }
  250. }))
  251. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: { _ in
  252. self.dismiss(animated: true, completion: nil)
  253. }))
  254. present(alert, animated: true, completion: nil)
  255. }
  256. private func askToChatWith(contactId: Int) {
  257. let dcContact = DcContact(id: contactId)
  258. let alert = UIAlertController(title: String.localizedStringWithFormat(String.localized("ask_start_chat_with"), dcContact.nameNAddr),
  259. message: nil,
  260. preferredStyle: .actionSheet)
  261. alert.addAction(UIAlertAction(title: String.localized("start_chat"), style: .default, handler: { _ in
  262. self.dismiss(animated: true, completion: nil)
  263. self.coordinator?.showNewChat(contactId: contactId)
  264. }))
  265. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: { _ in
  266. self.dismiss(animated: true, completion: nil)
  267. }))
  268. present(alert, animated: true, completion: nil)
  269. }
  270. private func showChatAt(row: Int) {
  271. if searchController.isActive {
  272. // edge case: when searchController is active but searchBar is empty -> filteredContacts is empty, so we fallback to contactIds
  273. let contactId = contactIdByRow(row)
  274. searchController.dismiss(animated: false, completion: {
  275. self.askToChatWith(contactId: contactId)
  276. })
  277. } else {
  278. let contactId = contactIds[row]
  279. self.askToChatWith(contactId: contactId)
  280. }
  281. }
  282. private func updateContactCell(cell: ContactCell, contactWithHighlight: ContactWithSearchResults) {
  283. let contact = contactWithHighlight.contact
  284. let displayName = contact.displayName
  285. let emailLabelFontSize = cell.emailLabel.font.pointSize
  286. let nameLabelFontSize = cell.nameLabel.font.pointSize
  287. cell.initialsLabel.text = Utils.getInitials(inputName: displayName)
  288. cell.setColor(contact.color)
  289. cell.setVerified(isVerified: contact.isVerified)
  290. if let emailHighlightedIndexes = contactWithHighlight.indexesToHighlight.filter({ $0.contactDetail == .EMAIL }).first {
  291. // gets here when contact is a result of current search -> highlights relevant indexes
  292. cell.emailLabel.attributedText = contact.email.boldAt(indexes: emailHighlightedIndexes.indexes, fontSize: emailLabelFontSize)
  293. } else {
  294. cell.emailLabel.attributedText = contact.email.boldAt(indexes: [], fontSize: emailLabelFontSize)
  295. }
  296. if let nameHighlightedIndexes = contactWithHighlight.indexesToHighlight.filter({ $0.contactDetail == .NAME }).first {
  297. cell.nameLabel.attributedText = displayName.boldAt(indexes: nameHighlightedIndexes.indexes, fontSize: nameLabelFontSize)
  298. } else {
  299. cell.nameLabel.attributedText = displayName.boldAt(indexes: [], fontSize: nameLabelFontSize)
  300. }
  301. }
  302. private func searchBarIsEmpty() -> Bool {
  303. return searchController.searchBar.text?.isEmpty ?? true
  304. }
  305. private func filterContentForSearchText(_ searchText: String, scope _: String = String.localized("pref_show_emails_all")) {
  306. let contactsWithHighlights: [ContactWithSearchResults] = contacts.map { contact in
  307. let indexes = contact.contact.containsExact(searchText: searchText)
  308. return ContactWithSearchResults(contact: contact.contact, indexesToHighlight: indexes)
  309. }
  310. filteredContacts = contactsWithHighlights.filter { !$0.indexesToHighlight.isEmpty }
  311. tableView.reloadData()
  312. }
  313. }
  314. extension NewChatViewController: ContactListDelegate {
  315. func deviceContactsImported() {
  316. contactIds = Utils.getContactIds()
  317. // tableView.reloadData()
  318. }
  319. func accessGranted() {
  320. deviceContactAccessGranted = true
  321. }
  322. func accessDenied() {
  323. deviceContactAccessGranted = false
  324. }
  325. private func showSettingsAlert() {
  326. let alert = UIAlertController(
  327. title: String.localized("import_contacts"),
  328. message: String.localized("import_contacts_message"),
  329. preferredStyle: .alert
  330. )
  331. alert.addAction(UIAlertAction(title: String.localized("menu_settings"), style: .default) { _ in
  332. UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
  333. })
  334. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel) { _ in
  335. })
  336. present(alert, animated: true)
  337. }
  338. }
  339. extension NewChatViewController: UISearchResultsUpdating {
  340. func updateSearchResults(for searchController: UISearchController) {
  341. if let searchText = searchController.searchBar.text {
  342. filterContentForSearchText(searchText)
  343. }
  344. }
  345. }
  346. struct ContactHighlights {
  347. let contactDetail: ContactDetail
  348. let indexes: [Int]
  349. }
  350. enum ContactDetail {
  351. case NAME
  352. case EMAIL
  353. }
  354. struct ContactWithSearchResults {
  355. let contact: DcContact
  356. let indexesToHighlight: [ContactHighlights]
  357. }