SettingsController.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. import JGProgressHUD
  2. import UIKit
  3. import DcCore
  4. internal final class SettingsViewController: UITableViewController {
  5. private struct SectionConfigs {
  6. let headerTitle: String?
  7. let footerTitle: String?
  8. let cells: [UITableViewCell]
  9. }
  10. private enum CellTags: Int {
  11. case profile = 0
  12. case contactRequest = 1
  13. case preferences = 2
  14. case blockedContacts = 3
  15. case notifications = 4
  16. case receiptConfirmation = 5
  17. case autocryptPreferences = 6
  18. case sendAutocryptMessage = 7
  19. case exportBackup = 8
  20. case advanced = 9
  21. case help = 10
  22. case autodel = 11
  23. }
  24. weak var coordinator: SettingsCoordinator?
  25. private var dcContext: DcContext
  26. private let externalPathDescr = "File Sharing/Delta Chat"
  27. let documentInteractionController = UIDocumentInteractionController()
  28. var backupProgressObserver: Any?
  29. var configureProgressObserver: Any?
  30. private lazy var hudHandler: HudHandler = {
  31. let hudHandler = HudHandler(parentView: self.view)
  32. return hudHandler
  33. }()
  34. // MARK: - cells
  35. private let profileHeader = ContactDetailHeader()
  36. private lazy var profileCell: ProfileCell = {
  37. let displayName = dcContext.displayname ?? String.localized("pref_your_name")
  38. let email = dcContext.addr ?? ""
  39. let selfContact = DcContact(id: Int(DC_CONTACT_ID_SELF))
  40. let cell = ProfileCell(contact: selfContact, displayName: displayName, address: email)
  41. cell.tag = CellTags.profile.rawValue
  42. return cell
  43. }()
  44. private var contactRequestCell: UITableViewCell = {
  45. let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
  46. cell.tag = CellTags.contactRequest.rawValue
  47. cell.textLabel?.text = String.localized("menu_deaddrop")
  48. cell.accessoryType = .disclosureIndicator
  49. return cell
  50. }()
  51. private lazy var chatPreferenceCell: UITableViewCell = {
  52. let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
  53. cell.tag = CellTags.preferences.rawValue
  54. cell.textLabel?.text = String.localized("pref_show_emails")
  55. cell.accessoryType = .disclosureIndicator
  56. cell.detailTextLabel?.text = SettingsClassicViewController.getValString(val: dcContext.showEmails)
  57. return cell
  58. }()
  59. private var blockedContactsCell: UITableViewCell = {
  60. let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
  61. cell.tag = CellTags.blockedContacts.rawValue
  62. cell.textLabel?.text = String.localized("pref_blocked_contacts")
  63. cell.accessoryType = .disclosureIndicator
  64. return cell
  65. }()
  66. func autodelSummary() -> String {
  67. let delDeviceAfter = dcContext.getConfigInt("delete_device_after")
  68. let delServerAfter = dcContext.getConfigInt("delete_server_after")
  69. if delDeviceAfter==0 && delServerAfter==0 {
  70. return String.localized("off")
  71. } else {
  72. return String.localized("on")
  73. }
  74. }
  75. private lazy var autodelCell: UITableViewCell = {
  76. let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
  77. cell.tag = CellTags.autodel.rawValue
  78. cell.textLabel?.text = String.localized("autodel_title")
  79. cell.accessoryType = .disclosureIndicator
  80. cell.detailTextLabel?.text = autodelSummary()
  81. return cell
  82. }()
  83. private var notificationSwitch: UISwitch = {
  84. let switchControl = UISwitch()
  85. switchControl.isUserInteractionEnabled = false // toggled by cell tap
  86. switchControl.isOn = !UserDefaults.standard.bool(forKey: "notifications_disabled")
  87. return switchControl
  88. }()
  89. private lazy var notificationCell: UITableViewCell = {
  90. let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
  91. cell.tag = CellTags.notifications.rawValue
  92. cell.textLabel?.text = String.localized("pref_notifications")
  93. cell.accessoryView = notificationSwitch
  94. return cell
  95. }()
  96. private lazy var receiptConfirmationSwitch: UISwitch = {
  97. let switchControl = UISwitch()
  98. switchControl.isUserInteractionEnabled = false // toggled by cell tap
  99. switchControl.isOn = dcContext.mdnsEnabled
  100. return switchControl
  101. }()
  102. private lazy var receiptConfirmationCell: UITableViewCell = {
  103. let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
  104. cell.tag = CellTags.receiptConfirmation.rawValue
  105. cell.textLabel?.text = String.localized("pref_read_receipts")
  106. cell.accessoryView = receiptConfirmationSwitch
  107. return cell
  108. }()
  109. private lazy var autocryptSwitch: UISwitch = {
  110. let switchControl = UISwitch()
  111. switchControl.isUserInteractionEnabled = false // toggled by cell tap
  112. switchControl.isOn = dcContext.e2eeEnabled
  113. return switchControl
  114. }()
  115. private lazy var autocryptPreferencesCell: UITableViewCell = {
  116. let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
  117. cell.tag = CellTags.autocryptPreferences.rawValue
  118. cell.textLabel?.text = String.localized("autocrypt_prefer_e2ee")
  119. cell.accessoryView = autocryptSwitch
  120. return cell
  121. }()
  122. private var sendAutocryptMessageCell: ActionCell = {
  123. let cell = ActionCell()
  124. cell.tag = CellTags.sendAutocryptMessage.rawValue
  125. cell.actionTitle = String.localized("autocrypt_send_asm_title")
  126. cell.selectionStyle = .default
  127. return cell
  128. }()
  129. private var exportBackupCell: ActionCell = {
  130. let cell = ActionCell()
  131. cell.tag = CellTags.exportBackup.rawValue
  132. cell.actionTitle = String.localized("export_backup_desktop")
  133. cell.selectionStyle = .default
  134. return cell
  135. }()
  136. private var advancedCell: ActionCell = {
  137. let cell = ActionCell()
  138. cell.tag = CellTags.advanced.rawValue
  139. cell.actionTitle = String.localized("menu_advanced")
  140. cell.selectionStyle = .default
  141. return cell
  142. }()
  143. private var helpCell: ActionCell = {
  144. let cell = ActionCell()
  145. cell.tag = CellTags.help.rawValue
  146. cell.actionTitle = String.localized("menu_help")
  147. cell.selectionStyle = .default
  148. return cell
  149. }()
  150. private lazy var sections: [SectionConfigs] = {
  151. var appNameAndVersion = "Delta Chat"
  152. if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
  153. appNameAndVersion += " v" + appVersion
  154. }
  155. let profileSection = SectionConfigs(
  156. headerTitle: String.localized("pref_profile_info_headline"),
  157. footerTitle: nil,
  158. cells: [profileCell]
  159. )
  160. let preferencesSection = SectionConfigs(
  161. headerTitle: nil,
  162. footerTitle: String.localized("pref_read_receipts_explain"),
  163. cells: [contactRequestCell, chatPreferenceCell, blockedContactsCell, autodelCell, notificationCell, receiptConfirmationCell]
  164. )
  165. let autocryptSection = SectionConfigs(
  166. headerTitle: String.localized("autocrypt"),
  167. footerTitle: String.localized("autocrypt_explain"),
  168. cells: [autocryptPreferencesCell, sendAutocryptMessageCell]
  169. )
  170. let backupSection = SectionConfigs(
  171. headerTitle: nil,
  172. footerTitle: String.localized("pref_backup_explain"),
  173. cells: [advancedCell, exportBackupCell])
  174. let helpSection = SectionConfigs(
  175. headerTitle: nil,
  176. footerTitle: appNameAndVersion,
  177. cells: [helpCell]
  178. )
  179. return [profileSection, preferencesSection, autocryptSection, backupSection, helpSection]
  180. }()
  181. init(dcContext: DcContext) {
  182. self.dcContext = dcContext
  183. super.init(style: .grouped)
  184. }
  185. required init?(coder _: NSCoder) {
  186. fatalError("init(coder:) has not been implemented")
  187. }
  188. // MARK: - lifecycle
  189. override func viewDidLoad() {
  190. super.viewDidLoad()
  191. title = String.localized("menu_settings")
  192. let backButton = UIBarButtonItem(title: String.localized("menu_settings"), style: .plain, target: nil, action: nil)
  193. navigationItem.backBarButtonItem = backButton
  194. documentInteractionController.delegate = self as? UIDocumentInteractionControllerDelegate
  195. }
  196. override func viewWillAppear(_ animated: Bool) {
  197. super.viewWillAppear(animated)
  198. updateCells()
  199. }
  200. override func viewDidAppear(_ animated: Bool) {
  201. super.viewDidAppear(animated)
  202. let nc = NotificationCenter.default
  203. backupProgressObserver = nc.addObserver(
  204. forName: dcNotificationImexProgress,
  205. object: nil,
  206. queue: nil
  207. ) { notification in
  208. if let ui = notification.userInfo {
  209. if ui["error"] as? Bool ?? false {
  210. self.hudHandler.setHudError(ui["errorMessage"] as? String)
  211. } else if ui["done"] as? Bool ?? false {
  212. self.hudHandler.setHudDone(callback: nil)
  213. } else {
  214. self.hudHandler.setHudProgress(ui["progress"] as? Int ?? 0)
  215. }
  216. }
  217. }
  218. configureProgressObserver = nc.addObserver(
  219. forName: dcNotificationConfigureProgress,
  220. object: nil,
  221. queue: nil
  222. ) { notification in
  223. if let ui = notification.userInfo {
  224. if ui["error"] as? Bool ?? false {
  225. self.hudHandler.setHudError(ui["errorMessage"] as? String)
  226. } else if ui["done"] as? Bool ?? false {
  227. self.hudHandler.setHudDone(callback: nil)
  228. } else {
  229. self.hudHandler.setHudProgress(ui["progress"] as? Int ?? 0)
  230. }
  231. }
  232. }
  233. }
  234. override func viewDidDisappear(_ animated: Bool) {
  235. super.viewDidDisappear(animated)
  236. let nc = NotificationCenter.default
  237. if let backupProgressObserver = self.backupProgressObserver {
  238. nc.removeObserver(backupProgressObserver)
  239. }
  240. if let configureProgressObserver = self.configureProgressObserver {
  241. nc.removeObserver(configureProgressObserver)
  242. }
  243. }
  244. // MARK: - UITableViewDelegate + UITableViewDatasource
  245. override func numberOfSections(in tableView: UITableView) -> Int {
  246. return sections.count
  247. }
  248. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  249. return sections[section].cells.count
  250. }
  251. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  252. return sections[indexPath.section].cells[indexPath.row]
  253. }
  254. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  255. guard let cell = tableView.cellForRow(at: indexPath), let cellTag = CellTags(rawValue: cell.tag) else {
  256. safe_fatalError()
  257. return
  258. }
  259. tableView.deselectRow(at: indexPath, animated: false) // to achieve highlight effect
  260. switch cellTag {
  261. case .profile: self.coordinator?.showEditSettingsController()
  262. case .contactRequest: self.coordinator?.showContactRequests()
  263. case .preferences: coordinator?.showClassicMail()
  264. case .blockedContacts: coordinator?.showBlockedContacts()
  265. case .autodel: coordinator?.showAutodelOptions()
  266. case .notifications: handleNotificationToggle()
  267. case .receiptConfirmation: handleReceiptConfirmationToggle()
  268. case .autocryptPreferences: handleAutocryptPreferencesToggle()
  269. case .sendAutocryptMessage: sendAutocryptSetupMessage()
  270. case .exportBackup: createBackup()
  271. case .advanced: showAdvancedDialog()
  272. case .help: coordinator?.showHelp()
  273. }
  274. }
  275. override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  276. return sections[section].headerTitle
  277. }
  278. override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
  279. return sections[section].footerTitle
  280. }
  281. // MARK: - actions
  282. private func createBackup() {
  283. let alert = UIAlertController(title: String.localized("pref_backup_export_explain"), message: nil, preferredStyle: .safeActionSheet)
  284. alert.addAction(UIAlertAction(title: String.localized("pref_backup_export_start_button"), style: .default, handler: { _ in
  285. self.dismiss(animated: true, completion: nil)
  286. self.startImex(what: DC_IMEX_EXPORT_BACKUP)
  287. }))
  288. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  289. present(alert, animated: true, completion: nil)
  290. }
  291. private func handleNotificationToggle() {
  292. notificationSwitch.isOn = !notificationSwitch.isOn
  293. UserDefaults.standard.set(!notificationSwitch.isOn, forKey: "notifications_disabled")
  294. UserDefaults.standard.synchronize()
  295. }
  296. private func handleReceiptConfirmationToggle() {
  297. receiptConfirmationSwitch.isOn = !receiptConfirmationSwitch.isOn
  298. dcContext.mdnsEnabled = receiptConfirmationSwitch.isOn
  299. dcContext.configure()
  300. }
  301. private func handleAutocryptPreferencesToggle() {
  302. autocryptSwitch.isOn = !autocryptSwitch.isOn
  303. dcContext.e2eeEnabled = autocryptSwitch.isOn
  304. dcContext.configure()
  305. }
  306. private func sendAutocryptSetupMessage() {
  307. let askAlert = UIAlertController(title: String.localized("autocrypt_send_asm_explain_before"), message: nil, preferredStyle: .safeActionSheet)
  308. askAlert.addAction(UIAlertAction(title: String.localized("autocrypt_send_asm_title"), style: .default, handler: { _ in
  309. let waitAlert = UIAlertController(title: String.localized("one_moment"), message: nil, preferredStyle: .alert)
  310. waitAlert.addAction(UIAlertAction(title: String.localized("cancel"), style: .default, handler: { _ in self.dcContext.stopOngoingProcess() }))
  311. self.present(waitAlert, animated: true, completion: nil)
  312. DispatchQueue.global(qos: .background).async {
  313. let sc = self.dcContext.initiateKeyTransfer()
  314. DispatchQueue.main.async {
  315. waitAlert.dismiss(animated: true, completion: nil)
  316. guard var sc = sc else {
  317. return
  318. }
  319. if sc.count == 44 {
  320. // format setup code to the typical 3 x 3 numbers
  321. sc = sc.substring(0, 4) + " - " + sc.substring(5, 9) + " - " + sc.substring(10, 14) + " -\n\n" +
  322. sc.substring(15, 19) + " - " + sc.substring(20, 24) + " - " + sc.substring(25, 29) + " -\n\n" +
  323. sc.substring(30, 34) + " - " + sc.substring(35, 39) + " - " + sc.substring(40, 44)
  324. }
  325. let text = String.localizedStringWithFormat(String.localized("autocrypt_send_asm_explain_after"), sc)
  326. let showAlert = UIAlertController(title: text, message: nil, preferredStyle: .alert)
  327. showAlert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: nil))
  328. self.present(showAlert, animated: true, completion: nil)
  329. }
  330. }
  331. }))
  332. askAlert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  333. present(askAlert, animated: true, completion: nil)
  334. }
  335. private func showAdvancedDialog() {
  336. let alert = UIAlertController(title: String.localized("menu_advanced"), message: nil, preferredStyle: .safeActionSheet)
  337. alert.addAction(UIAlertAction(title: String.localized("pref_managekeys_export_secret_keys"), style: .default, handler: { _ in
  338. let msg = String.localizedStringWithFormat(String.localized("pref_managekeys_export_explain"), self.externalPathDescr)
  339. let alert = UIAlertController(title: nil, message: msg, preferredStyle: .alert)
  340. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: { _ in
  341. self.startImex(what: DC_IMEX_EXPORT_SELF_KEYS)
  342. }))
  343. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  344. self.present(alert, animated: true, completion: nil)
  345. }))
  346. alert.addAction(UIAlertAction(title: String.localized("pref_managekeys_import_secret_keys"), style: .default, handler: { _ in
  347. let msg = String.localizedStringWithFormat(String.localized("pref_managekeys_import_explain"), self.externalPathDescr)
  348. let alert = UIAlertController(title: nil, message: msg, preferredStyle: .alert)
  349. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: { _ in
  350. self.startImex(what: DC_IMEX_IMPORT_SELF_KEYS)
  351. }))
  352. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  353. self.present(alert, animated: true, completion: nil)
  354. }))
  355. let locationStreaming = UserDefaults.standard.bool(forKey: "location_streaming")
  356. let title = locationStreaming ?
  357. "Disable on-demand location streaming" : String.localized("pref_on_demand_location_streaming")
  358. alert.addAction(UIAlertAction(title: title, style: .default, handler: { _ in
  359. UserDefaults.standard.set(!locationStreaming, forKey: "location_streaming")
  360. }))
  361. let logAction = UIAlertAction(title: String.localized("pref_view_log"), style: .default, handler: { [unowned self] _ in self.coordinator?.showDebugToolkit()})
  362. alert.addAction(logAction)
  363. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  364. present(alert, animated: true, completion: nil)
  365. }
  366. private func startImex(what: Int32) {
  367. let documents = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
  368. if !documents.isEmpty {
  369. self.hudHandler.showHud(String.localized("one_moment"))
  370. DispatchQueue.main.async {
  371. self.dcContext.imex(what: what, directory: documents[0])
  372. }
  373. } else {
  374. logger.error("document directory not found")
  375. }
  376. }
  377. // MARK: - updates
  378. private func updateCells() {
  379. let displayName = dcContext.displayname ?? String.localized("pref_your_name")
  380. let email = dcContext.addr ?? ""
  381. let selfContact = DcContact(id: Int(DC_CONTACT_ID_SELF))
  382. profileCell.update(contact: selfContact, displayName: displayName, address: email)
  383. chatPreferenceCell.detailTextLabel?.text = SettingsClassicViewController.getValString(val: dcContext.showEmails)
  384. autodelCell.detailTextLabel?.text = autodelSummary()
  385. }
  386. }