SettingsController.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  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. case mediaQuality = 12
  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. // MARK: - cells
  31. private lazy var profileCell: ContactCell = {
  32. let cell = ContactCell(style: .default, reuseIdentifier: nil)
  33. let cellViewModel = ProfileViewModel(context: dcContext)
  34. cell.updateCell(cellViewModel: cellViewModel)
  35. cell.tag = CellTags.profile.rawValue
  36. cell.accessoryType = .disclosureIndicator
  37. return cell
  38. }()
  39. private lazy 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 lazy 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("never")
  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("delete_old_messages")
  74. cell.accessoryType = .disclosureIndicator
  75. cell.detailTextLabel?.text = autodelSummary()
  76. return cell
  77. }()
  78. private lazy var mediaQualityCell: UITableViewCell = {
  79. let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
  80. cell.tag = CellTags.mediaQuality.rawValue
  81. cell.textLabel?.text = String.localized("pref_outgoing_media_quality")
  82. cell.accessoryType = .disclosureIndicator
  83. cell.detailTextLabel?.text = MediaQualityController.getValString(val: dcContext.getConfigInt("media_quality"))
  84. return cell
  85. }()
  86. private lazy var notificationSwitch: UISwitch = {
  87. let switchControl = UISwitch()
  88. switchControl.isOn = !UserDefaults.standard.bool(forKey: "notifications_disabled")
  89. switchControl.addTarget(self, action: #selector(handleNotificationToggle(_:)), for: .valueChanged)
  90. return switchControl
  91. }()
  92. private lazy var notificationCell: UITableViewCell = {
  93. let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
  94. cell.tag = CellTags.notifications.rawValue
  95. cell.textLabel?.text = String.localized("pref_notifications")
  96. cell.accessoryView = notificationSwitch
  97. cell.selectionStyle = .none
  98. return cell
  99. }()
  100. private lazy var receiptConfirmationSwitch: UISwitch = {
  101. let switchControl = UISwitch()
  102. switchControl.isOn = dcContext.mdnsEnabled
  103. switchControl.addTarget(self, action: #selector(handleReceiptConfirmationToggle(_:)), for: .valueChanged)
  104. return switchControl
  105. }()
  106. private lazy var receiptConfirmationCell: UITableViewCell = {
  107. let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
  108. cell.tag = CellTags.receiptConfirmation.rawValue
  109. cell.textLabel?.text = String.localized("pref_read_receipts")
  110. cell.accessoryView = receiptConfirmationSwitch
  111. cell.selectionStyle = .none
  112. return cell
  113. }()
  114. private lazy var autocryptSwitch: UISwitch = {
  115. let switchControl = UISwitch()
  116. switchControl.isOn = dcContext.e2eeEnabled
  117. switchControl.addTarget(self, action: #selector(handleAutocryptPreferencesToggle(_:)), for: .valueChanged)
  118. return switchControl
  119. }()
  120. private lazy var autocryptPreferencesCell: UITableViewCell = {
  121. let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
  122. cell.tag = CellTags.autocryptPreferences.rawValue
  123. cell.textLabel?.text = String.localized("autocrypt_prefer_e2ee")
  124. cell.accessoryView = autocryptSwitch
  125. cell.selectionStyle = .none
  126. return cell
  127. }()
  128. private lazy var sendAutocryptMessageCell: ActionCell = {
  129. let cell = ActionCell()
  130. cell.tag = CellTags.sendAutocryptMessage.rawValue
  131. cell.actionTitle = String.localized("autocrypt_send_asm_title")
  132. cell.selectionStyle = .default
  133. return cell
  134. }()
  135. private lazy var exportBackupCell: ActionCell = {
  136. let cell = ActionCell()
  137. cell.tag = CellTags.exportBackup.rawValue
  138. cell.actionTitle = String.localized("export_backup_desktop")
  139. cell.selectionStyle = .default
  140. return cell
  141. }()
  142. private lazy var advancedCell: ActionCell = {
  143. let cell = ActionCell()
  144. cell.tag = CellTags.advanced.rawValue
  145. cell.actionTitle = String.localized("menu_advanced")
  146. cell.selectionStyle = .default
  147. return cell
  148. }()
  149. private lazy var helpCell: ActionCell = {
  150. let cell = ActionCell()
  151. cell.tag = CellTags.help.rawValue
  152. cell.actionTitle = String.localized("menu_help")
  153. cell.selectionStyle = .default
  154. return cell
  155. }()
  156. private lazy var sections: [SectionConfigs] = {
  157. var appNameAndVersion = "Delta Chat"
  158. if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
  159. appNameAndVersion += " v" + appVersion
  160. }
  161. let profileSection = SectionConfigs(
  162. headerTitle: String.localized("pref_profile_info_headline"),
  163. footerTitle: nil,
  164. cells: [profileCell]
  165. )
  166. let preferencesSection = SectionConfigs(
  167. headerTitle: String.localized("pref_chats_and_media"),
  168. footerTitle: String.localized("pref_read_receipts_explain"),
  169. cells: [contactRequestCell, showEmailsCell, blockedContactsCell, autodelCell, mediaQualityCell, notificationCell, receiptConfirmationCell]
  170. )
  171. let autocryptSection = SectionConfigs(
  172. headerTitle: String.localized("autocrypt"),
  173. footerTitle: String.localized("autocrypt_explain"),
  174. cells: [autocryptPreferencesCell, sendAutocryptMessageCell]
  175. )
  176. let backupSection = SectionConfigs(
  177. headerTitle: nil,
  178. footerTitle: String.localized("pref_backup_explain"),
  179. cells: [advancedCell, exportBackupCell])
  180. let helpSection = SectionConfigs(
  181. headerTitle: nil,
  182. footerTitle: appNameAndVersion,
  183. cells: [helpCell]
  184. )
  185. return [profileSection, preferencesSection, autocryptSection, backupSection, helpSection]
  186. }()
  187. init(dcContext: DcContext) {
  188. self.dcContext = dcContext
  189. super.init(style: .grouped)
  190. }
  191. required init?(coder _: NSCoder) {
  192. fatalError("init(coder:) has not been implemented")
  193. }
  194. // MARK: - lifecycle
  195. override func viewDidLoad() {
  196. super.viewDidLoad()
  197. title = String.localized("menu_settings")
  198. let backButton = UIBarButtonItem(title: String.localized("menu_settings"), style: .plain, target: nil, action: nil)
  199. navigationItem.backBarButtonItem = backButton
  200. documentInteractionController.delegate = self as? UIDocumentInteractionControllerDelegate
  201. tableView.rowHeight = UITableView.automaticDimension
  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. addProgressAlertListener(progressName: dcNotificationImexProgress) { [weak self] in
  210. guard let self = self else { return }
  211. self.progressAlert?.dismiss(animated: true, completion: nil)
  212. }
  213. }
  214. override func viewDidDisappear(_ animated: Bool) {
  215. super.viewDidDisappear(animated)
  216. let nc = NotificationCenter.default
  217. if let backupProgressObserver = self.progressObserver {
  218. nc.removeObserver(backupProgressObserver)
  219. }
  220. }
  221. // MARK: - UITableViewDelegate + UITableViewDatasource
  222. override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  223. if indexPath.section == 0 && indexPath.row == 0 {
  224. return ContactCell.cellHeight
  225. } else {
  226. return UITableView.automaticDimension
  227. }
  228. }
  229. override func numberOfSections(in tableView: UITableView) -> Int {
  230. return sections.count
  231. }
  232. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  233. return sections[section].cells.count
  234. }
  235. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  236. return sections[indexPath.section].cells[indexPath.row]
  237. }
  238. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  239. guard let cell = tableView.cellForRow(at: indexPath), let cellTag = CellTags(rawValue: cell.tag) else {
  240. safe_fatalError()
  241. return
  242. }
  243. tableView.deselectRow(at: indexPath, animated: false) // to achieve highlight effect
  244. switch cellTag {
  245. case .profile: showEditSettingsController()
  246. case .contactRequest: showContactRequests()
  247. case .showEmails: showClassicMail()
  248. case .blockedContacts: showBlockedContacts()
  249. case .autodel: showAutodelOptions()
  250. case .mediaQuality: showMediaQuality()
  251. case .notifications: break
  252. case .receiptConfirmation: break
  253. case .autocryptPreferences: break
  254. case .sendAutocryptMessage: sendAutocryptSetupMessage()
  255. case .exportBackup: createBackup()
  256. case .advanced: showAdvancedDialog()
  257. case .help: showHelp()
  258. }
  259. }
  260. override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  261. return sections[section].headerTitle
  262. }
  263. override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
  264. return sections[section].footerTitle
  265. }
  266. // MARK: - actions
  267. private func createBackup() {
  268. let alert = UIAlertController(title: String.localized("pref_backup_export_explain"), message: nil, preferredStyle: .safeActionSheet)
  269. alert.addAction(UIAlertAction(title: String.localized("pref_backup_export_start_button"), style: .default, handler: { _ in
  270. self.dismiss(animated: true, completion: nil)
  271. self.startImex(what: DC_IMEX_EXPORT_BACKUP)
  272. }))
  273. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  274. present(alert, animated: true, completion: nil)
  275. }
  276. @objc private func handleNotificationToggle(_ sender: UISwitch) {
  277. UserDefaults.standard.set(!sender.isOn, forKey: "notifications_disabled")
  278. if sender.isOn {
  279. if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
  280. appDelegate.registerForNotifications()
  281. }
  282. } else {
  283. NotificationManager.removeAllNotifications()
  284. }
  285. UserDefaults.standard.synchronize()
  286. NotificationManager.updateApplicationIconBadge(reset: !sender.isOn)
  287. }
  288. @objc private func handleReceiptConfirmationToggle(_ sender: UISwitch) {
  289. dcContext.mdnsEnabled = sender.isOn
  290. }
  291. @objc private func handleAutocryptPreferencesToggle(_ sender: UISwitch) {
  292. dcContext.e2eeEnabled = sender.isOn
  293. }
  294. private func sendAutocryptSetupMessage() {
  295. let askAlert = UIAlertController(title: String.localized("autocrypt_send_asm_explain_before"), message: nil, preferredStyle: .safeActionSheet)
  296. askAlert.addAction(UIAlertAction(title: String.localized("autocrypt_send_asm_title"), style: .default, handler: { _ in
  297. let waitAlert = UIAlertController(title: String.localized("one_moment"), message: nil, preferredStyle: .alert)
  298. waitAlert.addAction(UIAlertAction(title: String.localized("cancel"), style: .default, handler: { _ in self.dcContext.stopOngoingProcess() }))
  299. self.present(waitAlert, animated: true, completion: nil)
  300. DispatchQueue.global(qos: .background).async {
  301. let sc = self.dcContext.initiateKeyTransfer()
  302. DispatchQueue.main.async {
  303. waitAlert.dismiss(animated: true, completion: nil)
  304. guard var sc = sc else {
  305. return
  306. }
  307. if sc.count == 44 {
  308. // format setup code to the typical 3 x 3 numbers
  309. sc = sc.substring(0, 4) + " - " + sc.substring(5, 9) + " - " + sc.substring(10, 14) + " -\n\n" +
  310. sc.substring(15, 19) + " - " + sc.substring(20, 24) + " - " + sc.substring(25, 29) + " -\n\n" +
  311. sc.substring(30, 34) + " - " + sc.substring(35, 39) + " - " + sc.substring(40, 44)
  312. }
  313. let text = String.localizedStringWithFormat(String.localized("autocrypt_send_asm_explain_after"), sc)
  314. let showAlert = UIAlertController(title: text, message: nil, preferredStyle: .alert)
  315. showAlert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: nil))
  316. self.present(showAlert, animated: true, completion: nil)
  317. }
  318. }
  319. }))
  320. askAlert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  321. present(askAlert, animated: true, completion: nil)
  322. }
  323. private func showAdvancedDialog() {
  324. let alert = UIAlertController(title: String.localized("menu_advanced"), message: nil, preferredStyle: .safeActionSheet)
  325. alert.addAction(UIAlertAction(title: String.localized("pref_managekeys_export_secret_keys"), style: .default, handler: { _ in
  326. let msg = String.localizedStringWithFormat(String.localized("pref_managekeys_export_explain"), self.externalPathDescr)
  327. let alert = UIAlertController(title: nil, message: msg, preferredStyle: .alert)
  328. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: { _ in
  329. self.startImex(what: DC_IMEX_EXPORT_SELF_KEYS)
  330. }))
  331. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  332. self.present(alert, animated: true, completion: nil)
  333. }))
  334. alert.addAction(UIAlertAction(title: String.localized("pref_managekeys_import_secret_keys"), style: .default, handler: { _ in
  335. let msg = String.localizedStringWithFormat(String.localized("pref_managekeys_import_explain"), self.externalPathDescr)
  336. let alert = UIAlertController(title: nil, message: msg, preferredStyle: .alert)
  337. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: { _ in
  338. self.startImex(what: DC_IMEX_IMPORT_SELF_KEYS)
  339. }))
  340. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  341. self.present(alert, animated: true, completion: nil)
  342. }))
  343. let locationStreaming = UserDefaults.standard.bool(forKey: "location_streaming")
  344. let title = locationStreaming ?
  345. "Disable on-demand location streaming" : String.localized("pref_on_demand_location_streaming")
  346. alert.addAction(UIAlertAction(title: title, style: .default, handler: { [weak self] _ in
  347. UserDefaults.standard.set(!locationStreaming, forKey: "location_streaming")
  348. if !locationStreaming {
  349. let alert = UIAlertController(title: "Thanks for trying out the experimental feature 🧪 \"Location streaming\"",
  350. message: "You will find a corresponding option in the attach menu (the paper clip) of each chat now.\n\n"
  351. + "If you want to quit the experimental feature, you can disable it at \"Settings / Advanced\".",
  352. preferredStyle: .alert)
  353. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: nil))
  354. self?.navigationController?.present(alert, animated: true, completion: nil)
  355. }
  356. }))
  357. let logAction = UIAlertAction(title: String.localized("pref_view_log"), style: .default, handler: { [weak self] _ in
  358. self?.showDebugToolkit()
  359. })
  360. alert.addAction(logAction)
  361. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  362. present(alert, animated: true, completion: nil)
  363. }
  364. private func startImex(what: Int32) {
  365. let documents = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
  366. if !documents.isEmpty {
  367. showProgressAlert(title: String.localized("imex_progress_title_desktop"), dcContext: dcContext)
  368. DispatchQueue.main.async {
  369. self.dcContext.stopIo()
  370. self.dcContext.imex(what: what, directory: documents[0])
  371. }
  372. } else {
  373. logger.error("document directory not found")
  374. }
  375. }
  376. // MARK: - updates
  377. private func updateCells() {
  378. profileCell.updateCell(cellViewModel: ProfileViewModel(context: dcContext))
  379. showEmailsCell.detailTextLabel?.text = SettingsClassicViewController.getValString(val: dcContext.showEmails)
  380. mediaQualityCell.detailTextLabel?.text = MediaQualityController.getValString(val: dcContext.getConfigInt("media_quality"))
  381. autodelCell.detailTextLabel?.text = autodelSummary()
  382. }
  383. // MARK: - coordinator
  384. private func showEditSettingsController() {
  385. let editController = EditSettingsController(dcContext: dcContext)
  386. navigationController?.pushViewController(editController, animated: true)
  387. }
  388. private func showClassicMail() {
  389. let settingsClassicViewController = SettingsClassicViewController(dcContext: dcContext)
  390. navigationController?.pushViewController(settingsClassicViewController, animated: true)
  391. }
  392. private func showMediaQuality() {
  393. let mediaQualityController = MediaQualityController(dcContext: dcContext)
  394. navigationController?.pushViewController(mediaQualityController, animated: true)
  395. }
  396. private func showBlockedContacts() {
  397. let blockedContactsController = BlockedContactsViewController()
  398. navigationController?.pushViewController(blockedContactsController, animated: true)
  399. }
  400. private func showAutodelOptions() {
  401. let settingsAutodelOverviewController = SettingsAutodelOverviewController(dcContext: dcContext)
  402. navigationController?.pushViewController(settingsAutodelOverviewController, animated: true)
  403. }
  404. private func showContactRequests() {
  405. let deaddropViewController = MailboxViewController(dcContext: dcContext, chatId: Int(DC_CHAT_ID_DEADDROP))
  406. navigationController?.pushViewController(deaddropViewController, animated: true)
  407. }
  408. private func showHelp() {
  409. navigationController?.pushViewController(HelpViewController(), animated: true)
  410. }
  411. private func showDebugToolkit() {
  412. var info = ""
  413. for name in ["notify-remote-launch", "notify-remote-receive", "notify-local-wakeup"] {
  414. let cnt = UserDefaults.standard.integer(forKey: name + "-count")
  415. let startDbl = UserDefaults.standard.double(forKey: name + "-start")
  416. let startStr = startDbl==0.0 ? "" : " since " + DateUtils.getExtendedRelativeTimeSpanString(timeStamp: startDbl)
  417. let timestampDbl = UserDefaults.standard.double(forKey: name + "-last")
  418. let timestampStr = timestampDbl==0.0 ? "" : ", last " + DateUtils.getExtendedRelativeTimeSpanString(timeStamp: timestampDbl)
  419. info += "\(name)=\(cnt)x\(startStr)\(timestampStr)\n"
  420. }
  421. if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
  422. info += "notify-token=\(appDelegate.notifyToken ?? "<unset>")\n"
  423. let timestampDbl = appDelegate.bgIoTimestamp
  424. let timestampStr = timestampDbl==0.0 ? "<unset>" : DateUtils.getExtendedRelativeTimeSpanString(timeStamp: timestampDbl)
  425. info += "last-bg-io=\(timestampStr)\n"
  426. }
  427. #if DEBUG
  428. info += "DEBUG=1\n"
  429. #else
  430. info += "DEBUG=0\n"
  431. #endif
  432. info += "\n" + dcContext.getInfo()
  433. DBDebugToolkit.add(DBCustomVariable(name: "", value: info))
  434. DBDebugToolkit.showMenu()
  435. }
  436. }