summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorNikolay Davydov <nik.davidov@gmail.com>2022-08-20 18:40:26 +0400
committerAndrej Mihajlov <and@mullvad.net>2022-08-23 13:34:27 +0200
commit98a05d57e134aebf19a2b1db72233139a39dd912 (patch)
treee0260eb6786691ac6c516228049c6ac8b6353d9b
parentea00c0c6c1fe38d01bee3ef0fe60d362749b64b0 (diff)
downloadmullvadvpn-98a05d57e134aebf19a2b1db72233139a39dd912.tar.xz
mullvadvpn-98a05d57e134aebf19a2b1db72233139a39dd912.zip
Add shortcuts
-rw-r--r--ios/CHANGELOG.md1
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj12
-rw-r--r--ios/MullvadVPN/SettingsDataSource.swift20
-rw-r--r--ios/MullvadVPN/SettingsNavigationController.swift4
-rw-r--r--ios/MullvadVPN/SettingsViewController.swift2
-rw-r--r--ios/MullvadVPN/ShortcutsDataSource.swift153
-rw-r--r--ios/MullvadVPN/ShortcutsDataSourceDelegate.swift14
-rw-r--r--ios/MullvadVPN/ShortcutsViewController.swift123
8 files changed, 328 insertions, 1 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md
index 8d297676f1..ab0a074415 100644
--- a/ios/CHANGELOG.md
+++ b/ios/CHANGELOG.md
@@ -32,6 +32,7 @@ Line wrap the file at 100 chars. Th
- Add ability to manage registered devices if too many devices detected during log-in.
- Add intents: start VPN, stop VPN, reconnect VPN (acts as start VPN when the tunnel is down,
otherwise picks new relay).
+- Add menu item to control shortcuts.
### Changed
- When logged into an account with no time left, a new view is shown instead of account settings,
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 8d67e122ac..0c7c5a77d5 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -307,6 +307,9 @@
58FEEB46260A028D00A621A8 /* GeoJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB45260A028D00A621A8 /* GeoJSON.swift */; };
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */; };
58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF2C02281BDE02009EF542 /* SettingsManager.swift */; };
+ 75FD0C2128B108570021E33E /* ShortcutsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FD0C2028B108570021E33E /* ShortcutsDataSource.swift */; };
+ 75FD0C2328B109860021E33E /* ShortcutsDataSourceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FD0C2228B109860021E33E /* ShortcutsDataSourceDelegate.swift */; };
+ 75FD0C2528B117D30021E33E /* ShortcutsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FD0C2428B117D30021E33E /* ShortcutsViewController.swift */; };
E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */; };
E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */; };
E1187ABF289BE76F0024E748 /* RESTCreateApplePaymentResponse+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABE289BE76F0024E748 /* RESTCreateApplePaymentResponse+Localization.swift */; };
@@ -598,6 +601,9 @@
58FEEB45260A028D00A621A8 /* GeoJSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoJSON.swift; sourceTree = "<group>"; };
58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticKeyboardResponder.swift; sourceTree = "<group>"; };
58FF2C02281BDE02009EF542 /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; };
+ 75FD0C2028B108570021E33E /* ShortcutsDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsDataSource.swift; sourceTree = "<group>"; };
+ 75FD0C2228B109860021E33E /* ShortcutsDataSourceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsDataSourceDelegate.swift; sourceTree = "<group>"; };
+ 75FD0C2428B117D30021E33E /* ShortcutsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsViewController.swift; sourceTree = "<group>"; };
E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeViewController.swift; sourceTree = "<group>"; };
E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeContentView.swift; sourceTree = "<group>"; };
E1187ABE289BE76F0024E748 /* RESTCreateApplePaymentResponse+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RESTCreateApplePaymentResponse+Localization.swift"; sourceTree = "<group>"; };
@@ -959,6 +965,9 @@
584D26C5270C8741004EA533 /* SettingsDNSTextCell.swift */,
580F8B88281A79A7002E0998 /* SettingsManager */,
58E6771E24ADFE7800AA26E7 /* SettingsNavigationController.swift */,
+ 75FD0C2428B117D30021E33E /* ShortcutsViewController.swift */,
+ 75FD0C2228B109860021E33E /* ShortcutsDataSourceDelegate.swift */,
+ 75FD0C2028B108570021E33E /* ShortcutsDataSource.swift */,
584D26C1270C8542004EA533 /* SettingsStaticTextFooterView.swift */,
58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */,
58CCA01122424D11004F3011 /* SettingsViewController.swift */,
@@ -1391,6 +1400,7 @@
582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */,
58554F7D280D6FE000013055 /* RESTURLSession.swift in Sources */,
5820EDAB288FF0D2006BF4E4 /* DeviceRowView.swift in Sources */,
+ 75FD0C2128B108570021E33E /* ShortcutsDataSource.swift in Sources */,
589D28822846306C00F9A7B3 /* GroupOperation.swift in Sources */,
5846227726E22A7C0035F7C2 /* AppStorePaymentManagerDelegate.swift in Sources */,
5871FB8325498CA20051A0A4 /* Swizzle.swift in Sources */,
@@ -1414,6 +1424,7 @@
58CC40EF24A601900019D96E /* ObserverList.swift in Sources */,
58CCA01822426713004F3011 /* AccountViewController.swift in Sources */,
5820674E26E6510200655B05 /* REST.swift in Sources */,
+ 75FD0C2528B117D30021E33E /* ShortcutsViewController.swift in Sources */,
5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */,
58F7CA882692E34000FC59FD /* WireguardKeysContentView.swift in Sources */,
58554F7B280B125F00013055 /* RESTAccountsProxy.swift in Sources */,
@@ -1479,6 +1490,7 @@
58B9EB152489139B00095626 /* DisplayChainedError.swift in Sources */,
587B753F2668E5A700DEF7E9 /* NotificationContainerView.swift in Sources */,
58554F79280B037400013055 /* RESTAccessTokenManager.swift in Sources */,
+ 75FD0C2328B109860021E33E /* ShortcutsDataSourceDelegate.swift in Sources */,
58421034282E4B1500F24E46 /* TunnelSettingsV2+REST.swift in Sources */,
58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */,
5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */,
diff --git a/ios/MullvadVPN/SettingsDataSource.swift b/ios/MullvadVPN/SettingsDataSource.swift
index cb457b042a..44e4891570 100644
--- a/ios/MullvadVPN/SettingsDataSource.swift
+++ b/ios/MullvadVPN/SettingsDataSource.swift
@@ -43,6 +43,7 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab
enum Item: String {
case account
case preferences
+ case shortcuts
case version
case problemReport
case faq
@@ -92,7 +93,7 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab
if TunnelManager.shared.deviceState.isLoggedIn {
newSnapshot.appendSections([.main])
- newSnapshot.appendItems([.account, .preferences], in: .main)
+ newSnapshot.appendItems([.account, .preferences, .shortcuts], in: .main)
}
newSnapshot.appendSections([.version, .problemReport])
@@ -152,6 +153,23 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab
return cell
+ case .shortcuts:
+ let cell = tableView.dequeueReusableCell(
+ withIdentifier: CellReuseIdentifiers.basicCell.rawValue,
+ for: indexPath
+ ) as! SettingsCell
+ cell.titleLabel.text = NSLocalizedString(
+ "SHORTCUTS_CELL_LABEL",
+ tableName: "Settings",
+ value: "Shortcuts",
+ comment: ""
+ )
+ cell.detailTitleLabel.text = nil
+ cell.accessibilityIdentifier = nil
+ cell.disclosureType = .chevron
+
+ return cell
+
case .version:
let cell = tableView.dequeueReusableCell(
withIdentifier: CellReuseIdentifiers.basicCell.rawValue,
diff --git a/ios/MullvadVPN/SettingsNavigationController.swift b/ios/MullvadVPN/SettingsNavigationController.swift
index f462f603c3..0476ac23fa 100644
--- a/ios/MullvadVPN/SettingsNavigationController.swift
+++ b/ios/MullvadVPN/SettingsNavigationController.swift
@@ -13,6 +13,7 @@ enum SettingsNavigationRoute {
case root
case account
case preferences
+ case shortcuts
case problemReport
}
@@ -119,6 +120,9 @@ class SettingsNavigationController: CustomNavigationController, SettingsViewCont
case .preferences:
return PreferencesViewController()
+ case .shortcuts:
+ return ShortcutsViewController()
+
case .problemReport:
return ProblemReportViewController()
}
diff --git a/ios/MullvadVPN/SettingsViewController.swift b/ios/MullvadVPN/SettingsViewController.swift
index 910e933319..0048c2eed3 100644
--- a/ios/MullvadVPN/SettingsViewController.swift
+++ b/ios/MullvadVPN/SettingsViewController.swift
@@ -102,6 +102,8 @@ extension SettingsDataSource.Item {
return .account
case .preferences:
return .preferences
+ case .shortcuts:
+ return .shortcuts
case .version:
return nil
case .problemReport:
diff --git a/ios/MullvadVPN/ShortcutsDataSource.swift b/ios/MullvadVPN/ShortcutsDataSource.swift
new file mode 100644
index 0000000000..d021e6f458
--- /dev/null
+++ b/ios/MullvadVPN/ShortcutsDataSource.swift
@@ -0,0 +1,153 @@
+//
+// ShortcutsDataSource.swift
+// MullvadVPN
+//
+// Created by Nikolay Davydov on 20.08.2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import IntentsUI
+import UIKit
+
+final class ShortcutsDataSource: NSObject, UITableViewDataSource, UITableViewDelegate {
+ private enum CellReuseIdentifiers: String, CaseIterable {
+ case basicCell
+
+ var reusableViewClass: AnyClass {
+ switch self {
+ case .basicCell:
+ return SettingsCell.self
+ }
+ }
+ }
+
+ enum Section: String {
+ case shortcuts
+ }
+
+ enum Item: String, CaseIterable {
+ case start
+ case reconnect
+ case stop
+ }
+
+ private var snapshot = DataSourceSnapshot<Section, Item>()
+
+ weak var delegate: ShortcutsDataSourceDelegate?
+
+ func configure(_ tableView: UITableView) {
+ CellReuseIdentifiers.allCases.forEach { cellIdentifier in
+ tableView.register(
+ cellIdentifier.reusableViewClass,
+ forCellReuseIdentifier: cellIdentifier.rawValue
+ )
+ }
+ tableView.dataSource = self
+ tableView.delegate = self
+ }
+
+ override init() {
+ super.init()
+ updateDataSnapshot()
+ }
+
+ private func updateDataSnapshot() {
+ var newSnapshot = DataSourceSnapshot<Section, Item>()
+ newSnapshot.appendSections([.shortcuts])
+ newSnapshot.appendItems(Item.allCases, in: .shortcuts)
+ snapshot = newSnapshot
+ }
+
+ // MARK: - UITableViewDataSource
+
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ let sectionIdentifier = snapshot.section(at: section)!
+ return snapshot.numberOfItems(in: sectionIdentifier) ?? 0
+ }
+
+ func numberOfSections(in tableView: UITableView) -> Int {
+ return snapshot.numberOfSections()
+ }
+
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let item = snapshot.itemForIndexPath(indexPath)!
+ switch item {
+ case .start, .reconnect, .stop:
+ let cell = tableView.dequeueReusableCell(
+ withIdentifier: CellReuseIdentifiers.basicCell.rawValue,
+ for: indexPath
+ )
+ if let cell = cell as? SettingsCell {
+ cell.titleLabel.text = item.title
+ }
+ return cell
+ }
+ }
+
+ // MARK: - UITableViewDelegate
+
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ guard let item = snapshot.itemForIndexPath(indexPath) else { return }
+ delegate?.shortcutsDataSource(self, didSelectItem: item)
+ tableView.deselectRow(at: indexPath, animated: true)
+ }
+}
+
+extension ShortcutsDataSource.Item {
+ var title: String {
+ switch self {
+ case .start:
+ return NSLocalizedString(
+ "SHORTCUTS_NAME_START_VPN",
+ tableName: "Shortcuts",
+ value: "Start VPN",
+ comment: ""
+ )
+ case .reconnect:
+ return NSLocalizedString(
+ "SHORTCUTS_NAME_RECONNECT_VPN",
+ tableName: "Shortcuts",
+ value: "Reconnect VPN",
+ comment: ""
+ )
+ case .stop:
+ return NSLocalizedString(
+ "SHORTCUTS_NAME_STOP_VPN",
+ tableName: "Shortcuts",
+ value: "Stop VPN",
+ comment: ""
+ )
+ }
+ }
+
+ var shortcut: INShortcut? {
+ let intent: INIntent
+ switch self {
+ case .start:
+ intent = StartVPNIntent()
+ case .reconnect:
+ intent = ReconnectVPNIntent()
+ case .stop:
+ intent = StopVPNIntent()
+ }
+ intent.suggestedInvocationPhrase = title
+ guard let shortcut = INShortcut(intent: intent) else {
+ assertionFailure("The shortcut has an invalid intent.")
+ return nil
+ }
+ return shortcut
+ }
+
+ init?(voiceShortcut: INVoiceShortcut) {
+ switch voiceShortcut.shortcut.intent {
+ case is StartVPNIntent:
+ self = .start
+ case is ReconnectVPNIntent:
+ self = .reconnect
+ case is StopVPNIntent:
+ self = .stop
+ default:
+ return nil
+ }
+ }
+}
diff --git a/ios/MullvadVPN/ShortcutsDataSourceDelegate.swift b/ios/MullvadVPN/ShortcutsDataSourceDelegate.swift
new file mode 100644
index 0000000000..2fde666721
--- /dev/null
+++ b/ios/MullvadVPN/ShortcutsDataSourceDelegate.swift
@@ -0,0 +1,14 @@
+//
+// ShortcutsDataSourceDelegate.swift
+// MullvadVPN
+//
+// Created by Nikolay Davydov on 20.08.2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+protocol ShortcutsDataSourceDelegate: AnyObject {
+ func shortcutsDataSource(
+ _ dataSource: ShortcutsDataSource,
+ didSelectItem item: ShortcutsDataSource.Item
+ )
+}
diff --git a/ios/MullvadVPN/ShortcutsViewController.swift b/ios/MullvadVPN/ShortcutsViewController.swift
new file mode 100644
index 0000000000..94b6abba0b
--- /dev/null
+++ b/ios/MullvadVPN/ShortcutsViewController.swift
@@ -0,0 +1,123 @@
+//
+// ShortcutsViewController.swift
+// MullvadVPN
+//
+// Created by Nikolay Davydov on 20.08.2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import IntentsUI
+import UIKit
+
+final class ShortcutsViewController: UITableViewController,
+ ShortcutsDataSourceDelegate,
+ INUIAddVoiceShortcutViewControllerDelegate,
+ INUIEditVoiceShortcutViewControllerDelegate
+{
+ private let dataSource = ShortcutsDataSource()
+
+ override var preferredStatusBarStyle: UIStatusBarStyle {
+ return .lightContent
+ }
+
+ init() {
+ super.init(style: .grouped)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ tableView.backgroundColor = .secondaryColor
+ tableView.separatorColor = .secondaryColor
+ tableView.rowHeight = UITableView.automaticDimension
+ tableView.estimatedRowHeight = 60
+
+ dataSource.configure(tableView)
+ dataSource.delegate = self
+
+ navigationItem.title = NSLocalizedString(
+ "NAVIGATION_TITLE",
+ tableName: "Shortcuts",
+ value: "Shortcuts",
+ comment: ""
+ )
+ }
+
+ private func handleSelectShortcut(_ shortcut: INShortcut, item: ShortcutsDataSource.Item) {
+ INVoiceShortcutCenter.shared.getAllVoiceShortcuts { [weak self] shortcuts, error in
+ DispatchQueue.main.async {
+ guard let self = self else { return }
+ let controller: UIViewController
+ if let voiceShortcut = shortcuts?.first(where: { voiceShortcut in
+ ShortcutsDataSource.Item(voiceShortcut: voiceShortcut) == item
+ }) {
+ let editShortcutController = INUIEditVoiceShortcutViewController(
+ voiceShortcut: voiceShortcut
+ )
+ editShortcutController.delegate = self
+ controller = editShortcutController
+ } else {
+ let addShortcutController = INUIAddVoiceShortcutViewController(
+ shortcut: shortcut
+ )
+ addShortcutController.delegate = self
+ controller = addShortcutController
+ }
+ controller.modalPresentationStyle = .formSheet
+ self.present(controller, animated: true)
+ }
+ }
+ }
+
+ // MARK: - ShortcutsDataSourceDelegate
+
+ func shortcutsDataSource(
+ _ dataSource: ShortcutsDataSource,
+ didSelectItem item: ShortcutsDataSource.Item
+ ) {
+ if let shortcut = item.shortcut {
+ handleSelectShortcut(shortcut, item: item)
+ }
+ }
+
+ // MARK: - INUIAddVoiceShortcutViewControllerDelegate
+
+ func addVoiceShortcutViewController(
+ _ controller: INUIAddVoiceShortcutViewController,
+ didFinishWith voiceShortcut: INVoiceShortcut?,
+ error: Error?
+ ) {
+ controller.dismiss(animated: true)
+ }
+
+ func addVoiceShortcutViewControllerDidCancel(_ controller: INUIAddVoiceShortcutViewController) {
+ controller.dismiss(animated: true)
+ }
+
+ // MARK: - INUIEditVoiceShortcutViewControllerDelegate
+
+ func editVoiceShortcutViewController(
+ _ controller: INUIEditVoiceShortcutViewController,
+ didUpdate voiceShortcut: INVoiceShortcut?,
+ error: Error?
+ ) {
+ controller.dismiss(animated: true)
+ }
+
+ func editVoiceShortcutViewController(
+ _ controller: INUIEditVoiceShortcutViewController,
+ didDeleteVoiceShortcutWithIdentifier deletedVoiceShortcutIdentifier: UUID
+ ) {
+ controller.dismiss(animated: true)
+ }
+
+ func editVoiceShortcutViewControllerDidCancel(
+ _ controller: INUIEditVoiceShortcutViewController
+ ) {
+ controller.dismiss(animated: true)
+ }
+}