QrPageController.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import UIKit
  2. import DcCore
  3. class QrPageController: UIPageViewController {
  4. private let dcContext: DcContext
  5. private let dcAccounts: DcAccounts
  6. var progressObserver: NSObjectProtocol?
  7. var qrCodeReaderController: QrCodeReaderController?
  8. private var selectedIndex: Int = 0
  9. private var qrCodeHint: String {
  10. var qrCodeHint = ""
  11. if dcContext.isConfigured() {
  12. // we cannot use dc_contact_get_displayname() as this would result in "Me" instead of the real name
  13. let name = dcContext.getConfig("displayname") ?? ""
  14. let addr = dcContext.getConfig("addr") ?? ""
  15. var nameAndAddress = ""
  16. if name.isEmpty {
  17. nameAndAddress = addr
  18. } else {
  19. nameAndAddress = "\(name) (\(addr))"
  20. }
  21. qrCodeHint = String.localizedStringWithFormat(
  22. String.localized("qrshow_join_contact_hint"),
  23. nameAndAddress
  24. )
  25. }
  26. return qrCodeHint
  27. }
  28. private lazy var qrSegmentControl: UISegmentedControl = {
  29. let control = UISegmentedControl(
  30. items: [String.localized("qrshow_title"), String.localized("qrscan_title")]
  31. )
  32. control.tintColor = DcColors.primary
  33. control.addTarget(self, action: #selector(qrSegmentControlChanged), for: .valueChanged)
  34. control.selectedSegmentIndex = 0
  35. return control
  36. }()
  37. private lazy var moreButton: UIBarButtonItem = {
  38. let image: UIImage?
  39. if #available(iOS 13.0, *) {
  40. image = UIImage(systemName: "ellipsis.circle")
  41. } else {
  42. image = UIImage(named: "ic_more")
  43. }
  44. return UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(showMoreOptions))
  45. }()
  46. init(dcAccounts: DcAccounts) {
  47. self.dcAccounts = dcAccounts
  48. self.dcContext = dcAccounts.getSelected()
  49. super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
  50. }
  51. required init?(coder: NSCoder) {
  52. fatalError("init(coder:) has not been implemented")
  53. }
  54. // MARK: - lifecycle
  55. override func viewDidLoad() {
  56. super.viewDidLoad()
  57. dataSource = self
  58. delegate = self
  59. navigationItem.titleView = qrSegmentControl
  60. navigationItem.rightBarButtonItem = moreButton
  61. let qrController = QrViewController(dcContext: dcContext, qrCodeHint: qrCodeHint)
  62. setViewControllers(
  63. [qrController],
  64. direction: .forward,
  65. animated: true,
  66. completion: nil
  67. )
  68. }
  69. override func viewWillAppear(_ animated: Bool) {
  70. // QrCodeReaderController::viewWillAppear() is on called on section change, not on main-tab change
  71. if let qrCodeReaderController = self.qrCodeReaderController {
  72. qrCodeReaderController.startSession()
  73. }
  74. updateHintTextIfNeeded() // needed in case user changes profile name
  75. }
  76. override func viewWillDisappear(_ animated: Bool) {
  77. // QrCodeReaderController::viewWillDisappear() is on called on section change, not on main-tab change
  78. if let qrCodeReaderController = self.qrCodeReaderController {
  79. qrCodeReaderController.stopSession()
  80. }
  81. self.progressObserver = nil
  82. }
  83. // MARK: - actions
  84. @objc private func qrSegmentControlChanged(_ sender: UISegmentedControl) {
  85. if sender.selectedSegmentIndex == 0 {
  86. let qrController = QrViewController(dcContext: dcContext, qrCodeHint: qrCodeHint)
  87. setViewControllers([qrController], direction: .reverse, animated: true, completion: nil)
  88. } else {
  89. let qrCodeReaderController = makeQRReader()
  90. self.qrCodeReaderController = qrCodeReaderController
  91. setViewControllers([qrCodeReaderController], direction: .forward, animated: false, completion: nil)
  92. }
  93. }
  94. @objc private func showMoreOptions() {
  95. let alert = UIAlertController(title: String.localized("qr_code"), message: nil, preferredStyle: .safeActionSheet)
  96. if qrSegmentControl.selectedSegmentIndex == 0 {
  97. alert.addAction(UIAlertAction(title: String.localized("menu_copy_to_clipboard"), style: .default, handler: copyToClipboard(_:)))
  98. } else {
  99. alert.addAction(UIAlertAction(title: String.localized("paste_from_clipboard"), style: .default, handler: pasteFromClipboard(_:)))
  100. }
  101. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  102. self.present(alert, animated: true, completion: nil)
  103. }
  104. @objc func copyToClipboard(_ action: UIAlertAction) {
  105. UIPasteboard.general.string = dcContext.getSecurejoinQr(chatId: 0)
  106. }
  107. @objc func pasteFromClipboard(_ action: UIAlertAction) {
  108. handleQrCode(UIPasteboard.general.string ?? "")
  109. }
  110. // MARK: - factory
  111. private func makeQRReader() -> QrCodeReaderController {
  112. let qrReader = QrCodeReaderController(title: String.localized("qrscan_title"))
  113. qrReader.delegate = self
  114. return qrReader
  115. }
  116. // MARK: - update
  117. private func updateHintTextIfNeeded() {
  118. for case let qrViewController as QrViewController in self.viewControllers ?? [] {
  119. let newHint = qrCodeHint
  120. if qrCodeHint != qrViewController.qrCodeHint {
  121. qrViewController.qrCodeHint = newHint
  122. }
  123. }
  124. }
  125. // MARK: - coordinator
  126. private func showChats() {
  127. if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
  128. appDelegate.appCoordinator.showTab(index: appDelegate.appCoordinator.chatsTab)
  129. }
  130. }
  131. private func showChat(chatId: Int) {
  132. if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
  133. appDelegate.appCoordinator.showChat(chatId: chatId, animated: false, clearViewControllerStack: true)
  134. }
  135. }
  136. }
  137. // MARK: - UIPageViewControllerDataSource, UIPageViewControllerDelegate
  138. extension QrPageController: UIPageViewControllerDataSource, UIPageViewControllerDelegate {
  139. func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
  140. if viewController is QrViewController {
  141. return nil
  142. }
  143. return QrViewController(dcContext: dcContext, qrCodeHint: qrCodeHint)
  144. }
  145. func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
  146. if viewController is QrViewController {
  147. return makeQRReader()
  148. }
  149. return nil
  150. }
  151. func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
  152. if completed {
  153. if previousViewControllers.first is QrViewController {
  154. qrSegmentControl.selectedSegmentIndex = 1
  155. } else {
  156. qrSegmentControl.selectedSegmentIndex = 0
  157. }
  158. }
  159. }
  160. }
  161. // MARK: - QRCodeDelegate
  162. extension QrPageController: QrCodeReaderDelegate {
  163. func handleQrCode(_ code: String) {
  164. self.showChats()
  165. let qrParsed: DcLot = self.dcContext.checkQR(qrCode: code)
  166. let state = Int32(qrParsed.state)
  167. switch state {
  168. case DC_QR_ASK_VERIFYCONTACT:
  169. let nameAndAddress = dcContext.getContact(id: qrParsed.id).nameNAddr
  170. joinSecureJoin(alertMessage: String.localizedStringWithFormat(String.localized("ask_start_chat_with"), nameAndAddress), code: code)
  171. case DC_QR_ASK_VERIFYGROUP:
  172. let groupName = qrParsed.text1 ?? "ErrGroupName"
  173. joinSecureJoin(alertMessage: String.localizedStringWithFormat(String.localized("qrscan_ask_join_group"), groupName), code: code)
  174. case DC_QR_FPR_WITHOUT_ADDR:
  175. let msg = String.localized("qrscan_no_addr_found") + "\n\n" +
  176. String.localized("qrscan_fingerprint_label") + ":\n" + (qrParsed.text1 ?? "")
  177. let alert = UIAlertController(title: msg, message: nil, preferredStyle: .alert)
  178. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: nil))
  179. present(alert, animated: true, completion: nil)
  180. case DC_QR_FPR_MISMATCH:
  181. let nameAndAddress = dcContext.getContact(id: qrParsed.id).nameNAddr
  182. let msg = String.localizedStringWithFormat(String.localized("qrscan_fingerprint_mismatch"), nameAndAddress)
  183. let alert = UIAlertController(title: msg, message: nil, preferredStyle: .alert)
  184. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: nil))
  185. present(alert, animated: true, completion: nil)
  186. case DC_QR_ADDR, DC_QR_FPR_OK:
  187. let nameAndAddress = dcContext.getContact(id: qrParsed.id).nameNAddr
  188. let msg = String.localizedStringWithFormat(String.localized(state==DC_QR_ADDR ? "ask_start_chat_with" : "qrshow_x_verified"), nameAndAddress)
  189. let alert = UIAlertController(title: msg, message: nil, preferredStyle: .alert)
  190. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .default, handler: nil))
  191. alert.addAction(UIAlertAction(title: String.localized("start_chat"), style: .default, handler: { _ in
  192. let chatId = self.dcContext.createChatByContactId(contactId: qrParsed.id)
  193. self.showChat(chatId: chatId)
  194. }))
  195. present(alert, animated: true, completion: nil)
  196. case DC_QR_TEXT:
  197. let msg = String.localizedStringWithFormat(String.localized("qrscan_contains_text"), qrParsed.text1 ?? "")
  198. let alert = UIAlertController(title: msg, message: nil, preferredStyle: .alert)
  199. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: nil))
  200. present(alert, animated: true, completion: nil)
  201. case DC_QR_URL:
  202. let url = qrParsed.text1 ?? ""
  203. let msg = String.localizedStringWithFormat(String.localized("qrscan_contains_url"), url)
  204. let alert = UIAlertController(title: msg, message: nil, preferredStyle: .alert)
  205. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .default, handler: nil))
  206. alert.addAction(UIAlertAction(title: String.localized("open"), style: .default, handler: { _ in
  207. if let url = URL(string: url) {
  208. UIApplication.shared.open(url)
  209. }
  210. }))
  211. present(alert, animated: true, completion: nil)
  212. case DC_QR_ACCOUNT, DC_QR_LOGIN, DC_QR_BACKUP:
  213. if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
  214. appDelegate.appCoordinator.presentWelcomeController(accountCode: code)
  215. }
  216. case DC_QR_WEBRTC_INSTANCE:
  217. guard let domain = qrParsed.text1 else { return }
  218. let alert = UIAlertController(title: String.localizedStringWithFormat(String.localized("videochat_instance_from_qr"), domain),
  219. message: nil,
  220. preferredStyle: .alert)
  221. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .default))
  222. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: { [weak self] _ in
  223. guard let self = self else { return }
  224. let success = self.dcContext.setConfigFromQR(qrCode: code)
  225. if !success {
  226. logger.warning("Could not set webrtc instance from QR code.")
  227. // TODO: alert?!
  228. }
  229. }))
  230. present(alert, animated: true)
  231. case DC_QR_WITHDRAW_VERIFYCONTACT:
  232. let alert = UIAlertController(title: String.localized("withdraw_verifycontact_explain"),
  233. message: nil, preferredStyle: .alert)
  234. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .default))
  235. alert.addAction(UIAlertAction(title: String.localized("withdraw_qr_code"), style: .destructive, handler: { [weak self] _ in
  236. _ = self?.dcContext.setConfigFromQR(qrCode: code)
  237. }))
  238. present(alert, animated: true)
  239. case DC_QR_REVIVE_VERIFYCONTACT:
  240. let alert = UIAlertController(title: String.localized("revive_verifycontact_explain"),
  241. message: nil, preferredStyle: .alert)
  242. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .default))
  243. alert.addAction(UIAlertAction(title: String.localized("revive_qr_code"), style: .default, handler: { [weak self] _ in
  244. _ = self?.dcContext.setConfigFromQR(qrCode: code)
  245. }))
  246. present(alert, animated: true)
  247. case DC_QR_WITHDRAW_VERIFYGROUP:
  248. guard let groupName = qrParsed.text1 else { return }
  249. let alert = UIAlertController(title: String.localizedStringWithFormat(String.localized("withdraw_verifygroup_explain"), groupName),
  250. message: nil, preferredStyle: .alert)
  251. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .default))
  252. alert.addAction(UIAlertAction(title: String.localized("withdraw_qr_code"), style: .destructive, handler: { [weak self] _ in
  253. _ = self?.dcContext.setConfigFromQR(qrCode: code)
  254. }))
  255. present(alert, animated: true)
  256. case DC_QR_REVIVE_VERIFYGROUP:
  257. guard let groupName = qrParsed.text1 else { return }
  258. let alert = UIAlertController(title: String.localizedStringWithFormat(String.localized("revive_verifygroup_explain"), groupName),
  259. message: nil, preferredStyle: .alert)
  260. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .default))
  261. alert.addAction(UIAlertAction(title: String.localized("revive_qr_code"), style: .default, handler: { [weak self] _ in
  262. _ = self?.dcContext.setConfigFromQR(qrCode: code)
  263. }))
  264. present(alert, animated: true)
  265. default:
  266. var msg = String.localizedStringWithFormat(String.localized("qrscan_contains_text"), code)
  267. if state == DC_QR_ERROR {
  268. if let errorMsg = qrParsed.text1 {
  269. msg = errorMsg + "\n\n" + msg
  270. }
  271. }
  272. let alert = UIAlertController(title: msg, message: nil, preferredStyle: .alert)
  273. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: nil))
  274. present(alert, animated: true, completion: nil)
  275. }
  276. }
  277. private func joinSecureJoin(alertMessage: String, code: String) {
  278. let alert = UIAlertController(title: alertMessage,
  279. message: nil,
  280. preferredStyle: .alert)
  281. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .default, handler: nil))
  282. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: { _ in
  283. let chatId = self.dcContext.joinSecurejoin(qrCode: code)
  284. if chatId != 0 {
  285. self.showChat(chatId: chatId)
  286. } else {
  287. self.showErrorAlert(error: self.dcContext.lastErrorString)
  288. }
  289. }))
  290. present(alert, animated: true, completion: nil)
  291. }
  292. private func showErrorAlert(error: String) {
  293. let alert = UIAlertController(title: String.localized("error"), message: error, preferredStyle: .alert)
  294. alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: { _ in
  295. alert.dismiss(animated: true, completion: nil)
  296. }))
  297. }
  298. }