SettingsController.swift 19 KB

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