diff options
| author | Nikolay Davydov <nik.davidov@gmail.com> | 2022-08-20 18:40:26 +0400 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-08-23 13:34:27 +0200 |
| commit | 98a05d57e134aebf19a2b1db72233139a39dd912 (patch) | |
| tree | e0260eb6786691ac6c516228049c6ac8b6353d9b | |
| parent | ea00c0c6c1fe38d01bee3ef0fe60d362749b64b0 (diff) | |
| download | mullvadvpn-98a05d57e134aebf19a2b1db72233139a39dd912.tar.xz mullvadvpn-98a05d57e134aebf19a2b1db72233139a39dd912.zip | |
Add shortcuts
| -rw-r--r-- | ios/CHANGELOG.md | 1 | ||||
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 12 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsDataSource.swift | 20 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsNavigationController.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsViewController.swift | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN/ShortcutsDataSource.swift | 153 | ||||
| -rw-r--r-- | ios/MullvadVPN/ShortcutsDataSourceDelegate.swift | 14 | ||||
| -rw-r--r-- | ios/MullvadVPN/ShortcutsViewController.swift | 123 |
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) + } +} |
