AdvancedViewController.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. import UIKit
  2. import DcCore
  3. import Intents
  4. internal final class AdvancedViewController: 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 autocryptPreferences
  12. case sendAutocryptMessage
  13. case manageKeys
  14. case experimentalFeatures
  15. case videoChat
  16. case viewLog
  17. }
  18. private var dcContext: DcContext
  19. internal let dcAccounts: DcAccounts
  20. private let externalPathDescr = "File Sharing/Delta Chat"
  21. // MARK: - ProgressAlertHandler
  22. weak var progressAlert: UIAlertController?
  23. var progressObserver: NSObjectProtocol?
  24. // MARK: - cells
  25. private lazy var autocryptSwitch: UISwitch = {
  26. let switchControl = UISwitch()
  27. switchControl.isOn = dcContext.e2eeEnabled
  28. switchControl.addTarget(self, action: #selector(handleAutocryptPreferencesToggle(_:)), for: .valueChanged)
  29. return switchControl
  30. }()
  31. private lazy var autocryptPreferencesCell: UITableViewCell = {
  32. let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
  33. cell.tag = CellTags.autocryptPreferences.rawValue
  34. cell.textLabel?.text = String.localized("autocrypt_prefer_e2ee")
  35. cell.accessoryView = autocryptSwitch
  36. cell.selectionStyle = .none
  37. return cell
  38. }()
  39. private lazy var sendAutocryptMessageCell: ActionCell = {
  40. let cell = ActionCell()
  41. cell.tag = CellTags.sendAutocryptMessage.rawValue
  42. cell.actionTitle = String.localized("autocrypt_send_asm_title")
  43. return cell
  44. }()
  45. private lazy var manageKeysCell: UITableViewCell = {
  46. let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
  47. cell.tag = CellTags.manageKeys.rawValue
  48. cell.textLabel?.text = String.localized("pref_manage_keys")
  49. cell.accessoryType = .disclosureIndicator
  50. return cell
  51. }()
  52. private lazy var experimentalFeaturesCell: UITableViewCell = {
  53. let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
  54. cell.tag = CellTags.experimentalFeatures.rawValue
  55. cell.textLabel?.text = String.localized("pref_experimental_features")
  56. cell.accessoryType = .disclosureIndicator
  57. return cell
  58. }()
  59. private lazy var videoChatInstanceCell: UITableViewCell = {
  60. let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
  61. cell.tag = CellTags.videoChat.rawValue
  62. cell.textLabel?.text = String.localized("videochat_instance")
  63. cell.accessoryType = .disclosureIndicator
  64. return cell
  65. }()
  66. private lazy var viewLogCell: ActionCell = {
  67. let cell = ActionCell()
  68. cell.tag = CellTags.viewLog.rawValue
  69. cell.actionTitle = String.localized("pref_view_log")
  70. return cell
  71. }()
  72. private lazy var sections: [SectionConfigs] = {
  73. let autocryptSection = SectionConfigs(
  74. headerTitle: String.localized("autocrypt"),
  75. footerTitle: String.localized("autocrypt_explain"),
  76. cells: [autocryptPreferencesCell, sendAutocryptMessageCell]
  77. )
  78. let miscSection = SectionConfigs(
  79. headerTitle: nil,
  80. footerTitle: nil,
  81. cells: [manageKeysCell, experimentalFeaturesCell, videoChatInstanceCell])
  82. let viewLogSection = SectionConfigs(
  83. headerTitle: nil,
  84. footerTitle: nil,
  85. cells: [viewLogCell])
  86. return [autocryptSection, miscSection, viewLogSection]
  87. }()
  88. init(dcAccounts: DcAccounts) {
  89. self.dcContext = dcAccounts.getSelected()
  90. self.dcAccounts = dcAccounts
  91. super.init(style: .grouped)
  92. }
  93. required init?(coder _: NSCoder) {
  94. fatalError("init(coder:) has not been implemented")
  95. }
  96. // MARK: - lifecycle
  97. override func viewDidLoad() {
  98. super.viewDidLoad()
  99. title = String.localized("menu_advanced")
  100. tableView.rowHeight = UITableView.automaticDimension
  101. }
  102. override func viewWillAppear(_ animated: Bool) {
  103. super.viewWillAppear(animated)
  104. updateCells()
  105. }
  106. override func viewDidAppear(_ animated: Bool) {
  107. super.viewDidAppear(animated)
  108. addProgressAlertListener(dcAccounts: dcAccounts, progressName: dcNotificationImexProgress) { [weak self] in
  109. guard let self = self else { return }
  110. self.progressAlert?.dismiss(animated: true)
  111. }
  112. }
  113. override func viewDidDisappear(_ animated: Bool) {
  114. super.viewDidDisappear(animated)
  115. let nc = NotificationCenter.default
  116. if let backupProgressObserver = self.progressObserver {
  117. nc.removeObserver(backupProgressObserver)
  118. }
  119. }
  120. // MARK: - UITableViewDelegate + UITableViewDatasource
  121. override func numberOfSections(in tableView: UITableView) -> Int {
  122. return sections.count
  123. }
  124. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  125. return sections[section].cells.count
  126. }
  127. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  128. return sections[indexPath.section].cells[indexPath.row]
  129. }
  130. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  131. guard let cell = tableView.cellForRow(at: indexPath), let cellTag = CellTags(rawValue: cell.tag) else {
  132. safe_fatalError()
  133. return
  134. }
  135. tableView.deselectRow(at: indexPath, animated: false)
  136. switch cellTag {
  137. case .autocryptPreferences: break
  138. case .sendAutocryptMessage: sendAutocryptSetupMessage()
  139. case .manageKeys: showManageKeysDialog()
  140. case .experimentalFeatures: showExperimentalDialog()
  141. case .videoChat: showVideoChatInstance()
  142. case .viewLog: showLogViewController()
  143. }
  144. }
  145. override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  146. return sections[section].headerTitle
  147. }
  148. override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
  149. return sections[section].footerTitle
  150. }
  151. // MARK: - actions
  152. @objc private func handleAutocryptPreferencesToggle(_ sender: UISwitch) {
  153. dcContext.e2eeEnabled = sender.isOn
  154. }
  155. private func sendAutocryptSetupMessage() {
  156. let askAlert = UIAlertController(title: String.localized("autocrypt_send_asm_explain_before"), message: nil, preferredStyle: .safeActionSheet)
  157. askAlert.addAction(UIAlertAction(title: String.localized("autocrypt_send_asm_title"), style: .default, handler: { _ in
  158. let sc = self.dcContext.initiateKeyTransfer()
  159. guard var sc = sc else {
  160. return
  161. }
  162. if sc.count == 44 {
  163. // format setup code to the typical 3 x 3 numbers
  164. sc = sc.substring(0, 4) + " - " + sc.substring(5, 9) + " - " + sc.substring(10, 14) + " -\n\n" +
  165. sc.substring(15, 19) + " - " + sc.substring(20, 24) + " - " + sc.substring(25, 29) + " -\n\n" +
  166. sc.substring(30, 34) + " - " + sc.substring(35, 39) + " - " + sc.substring(40, 44)
  167. }
  168. let text = String.localizedStringWithFormat(String.localized("autocrypt_send_asm_explain_after"), sc)
  169. let showAlert = UIAlertController(title: text, message: nil, preferredStyle: .alert)
  170. showAlert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: nil))
  171. self.present(showAlert, animated: true, completion: nil)
  172. }))
  173. askAlert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  174. present(askAlert, animated: true, completion: nil)
  175. }
  176. private func showLogViewController() {
  177. let controller = LogViewController(dcContext: dcContext)
  178. navigationController?.pushViewController(controller, animated: true)
  179. }
  180. private func showExperimentalDialog() {
  181. let alert = UIAlertController(title: String.localized("pref_experimental_features"), message: nil, preferredStyle: .safeActionSheet)
  182. let broadcastLists = UserDefaults.standard.bool(forKey: "broadcast_lists")
  183. alert.addAction(UIAlertAction(title: (broadcastLists ? "✔︎ " : "") + String.localized("broadcast_lists"),
  184. style: .default, handler: { [weak self] _ in
  185. guard let self = self else { return }
  186. UserDefaults.standard.set(!broadcastLists, forKey: "broadcast_lists")
  187. if !broadcastLists {
  188. let alert = UIAlertController(title: "Thanks for trying out the experimental feature 🧪 \"Broadcast Lists\"!",
  189. message: "You can now create new \"Broadcast Lists\" from the \"New Chat\" dialog\n\n"
  190. + "In case you are using more than one device, broadcast lists are currently not synced between them\n\n"
  191. + "If you want to quit the experimental feature, you can disable it at \"Settings / Advanced\".",
  192. preferredStyle: .alert)
  193. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: nil))
  194. self.navigationController?.present(alert, animated: true, completion: nil)
  195. }
  196. }))
  197. let locationStreaming = UserDefaults.standard.bool(forKey: "location_streaming")
  198. let title = (locationStreaming ? "✔︎ " : "") + String.localized("pref_on_demand_location_streaming")
  199. alert.addAction(UIAlertAction(title: title, style: .default, handler: { [weak self] _ in
  200. guard let self = self else { return }
  201. UserDefaults.standard.set(!locationStreaming, forKey: "location_streaming")
  202. if !locationStreaming {
  203. let alert = UIAlertController(title: "Thanks for trying out the experimental feature 🧪 \"Location streaming\"",
  204. message: "You will find a corresponding option in the attach menu (the paper clip) of each chat now.\n\n"
  205. + "If you want to quit the experimental feature, you can disable it at \"Settings / Advanced\".",
  206. preferredStyle: .alert)
  207. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: nil))
  208. self.navigationController?.present(alert, animated: true, completion: nil)
  209. } else if self.dcContext.isSendingLocationsToChat(chatId: 0) {
  210. guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
  211. return
  212. }
  213. appDelegate.locationManager.disableLocationStreamingInAllChats()
  214. }
  215. }))
  216. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  217. present(alert, animated: true, completion: nil)
  218. }
  219. private func showManageKeysDialog() {
  220. let alert = UIAlertController(title: String.localized("pref_manage_keys"), message: nil, preferredStyle: .safeActionSheet)
  221. alert.addAction(UIAlertAction(title: String.localized("pref_managekeys_export_secret_keys"), style: .default, handler: { _ in
  222. let msg = String.localizedStringWithFormat(String.localized("pref_managekeys_export_explain"), self.externalPathDescr)
  223. let alert = UIAlertController(title: String.localized("pref_managekeys_export_secret_keys"), message: msg, preferredStyle: .alert)
  224. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: { _ in
  225. self.startImex(what: DC_IMEX_EXPORT_SELF_KEYS)
  226. }))
  227. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  228. self.present(alert, animated: true, completion: nil)
  229. }))
  230. alert.addAction(UIAlertAction(title: String.localized("pref_managekeys_import_secret_keys"), style: .default, handler: { _ in
  231. let msg = String.localizedStringWithFormat(String.localized("pref_managekeys_import_explain"), self.externalPathDescr)
  232. let alert = UIAlertController(title: String.localized("pref_managekeys_import_secret_keys"), message: msg, preferredStyle: .alert)
  233. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: { _ in
  234. self.startImex(what: DC_IMEX_IMPORT_SELF_KEYS)
  235. }))
  236. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  237. self.present(alert, animated: true, completion: nil)
  238. }))
  239. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  240. present(alert, animated: true, completion: nil)
  241. }
  242. private func presentError(message: String) {
  243. let error = UIAlertController(title: nil, message: message, preferredStyle: .alert)
  244. error.addAction(UIAlertAction(title: String.localized("ok"), style: .cancel))
  245. present(error, animated: true)
  246. }
  247. private func startImex(what: Int32, passphrase: String? = nil) {
  248. let documents = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
  249. if !documents.isEmpty {
  250. showProgressAlert(title: String.localized(what==DC_IMEX_IMPORT_SELF_KEYS ? "pref_managekeys_import_secret_keys" : "pref_managekeys_export_secret_keys"), dcContext: dcContext)
  251. DispatchQueue.main.async {
  252. self.dcAccounts.stopIo()
  253. self.dcContext.imex(what: what, directory: documents[0], passphrase: passphrase)
  254. }
  255. } else {
  256. logger.error("document directory not found")
  257. }
  258. }
  259. // MARK: - updates
  260. private func updateCells() {
  261. videoChatInstanceCell.detailTextLabel?.text = VideoChatInstanceViewController.getValString(val: dcContext.getConfig("webrtc_instance") ?? "")
  262. }
  263. // MARK: - coordinator
  264. private func showVideoChatInstance() {
  265. let videoInstanceController = VideoChatInstanceViewController(dcContext: dcContext)
  266. navigationController?.pushViewController(videoInstanceController, animated: true)
  267. }
  268. }