SettingsController.swift 21 KB

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