diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-04-25 18:02:55 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-04-25 18:02:55 +0200 |
| commit | 5ddf2a83859a4a1f313fa9e1455af1339bddf28b (patch) | |
| tree | 239d4f3c7f30af75e1ad5a67982f20214d7980d8 | |
| parent | 43412a54f7812194e6358920dc95a9246541fa36 (diff) | |
| parent | b7199a6598a10ce747730eb01abe3161146df8f2 (diff) | |
| download | mullvadvpn-5ddf2a83859a4a1f313fa9e1455af1339bddf28b.tar.xz mullvadvpn-5ddf2a83859a4a1f313fa9e1455af1339bddf28b.zip | |
Merge branch 'improve-content-blocker-settings-ios-36'
11 files changed, 454 insertions, 49 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 3180a3d59e..ad4cec56cd 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -359,6 +359,7 @@ 58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */; }; 58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF2C02281BDE02009EF542 /* SettingsManager.swift */; }; 7A09C98129D99215000C2CAC /* String+FuzzyMatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */; }; + 7A7AD28F29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28E29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift */; }; 7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */; }; 7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */; }; 7AD2DA1529DC4EB900250737 /* UISearchBar+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD2DA1429DC4EB900250737 /* UISearchBar+Appearance.swift */; }; @@ -945,6 +946,7 @@ 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>"; }; 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FuzzyMatch.swift"; sourceTree = "<group>"; }; + 7A7AD28E29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContentBlockersHeaderView.swift; sourceTree = "<group>"; }; 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstTimeLaunch.swift; sourceTree = "<group>"; }; 7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootConfiguration.swift; sourceTree = "<group>"; }; 7AD2DA1429DC4EB900250737 /* UISearchBar+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISearchBar+Appearance.swift"; sourceTree = "<group>"; }; @@ -1276,17 +1278,19 @@ 583FE01829C19709006E85F9 /* Settings */ = { isa = PBXGroup; children = ( - 5864AF0029C7879B005B0CD9 /* SettingsCellFactory.swift */, - 58CCA01122424D11004F3011 /* SettingsViewController.swift */, - 5867770F290975E8006F721F /* SettingsInteractorFactory.swift */, - 58677711290976FB006F721F /* SettingsInteractor.swift */, - 582BB1AE229566420055B6EF /* SettingsCell.swift */, - 584D26C1270C8542004EA533 /* SettingsStaticTextFooterView.swift */, - 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */, + 582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */, 5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */, + 582BB1AE229566420055B6EF /* SettingsCell.swift */, + 5864AF0029C7879B005B0CD9 /* SettingsCellFactory.swift */, + 7A7AD28E29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift */, 58EE2E38272FF814003BFF93 /* SettingsDataSource.swift */, 58EE2E39272FF814003BFF93 /* SettingsDataSourceDelegate.swift */, 584D26C5270C8741004EA533 /* SettingsDNSTextCell.swift */, + 58677711290976FB006F721F /* SettingsInteractor.swift */, + 5867770F290975E8006F721F /* SettingsInteractorFactory.swift */, + 584D26C1270C8542004EA533 /* SettingsStaticTextFooterView.swift */, + 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */, + 58CCA01122424D11004F3011 /* SettingsViewController.swift */, ); path = Settings; sourceTree = "<group>"; @@ -2715,6 +2719,7 @@ 58607A4D2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift in Sources */, 06410E07292D108E00AFC18C /* SettingsStore.swift in Sources */, 586A950D290125F0007BAF2B /* PresentAlertOperation.swift in Sources */, + 7A7AD28F29DEDB1C00480EF1 /* SettingsContentBlockersHeaderView.swift in Sources */, 5878F50229CDB989003D4BE2 /* ChangeLogCoordinator.swift in Sources */, 58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */, 58B26E262943522400D5980C /* NotificationProvider.swift in Sources */, diff --git a/ios/MullvadVPN/Classes/CustomDateComponentsFormatting.swift b/ios/MullvadVPN/Classes/CustomDateComponentsFormatting.swift index 307978927b..0034494437 100644 --- a/ios/MullvadVPN/Classes/CustomDateComponentsFormatting.swift +++ b/ios/MullvadVPN/Classes/CustomDateComponentsFormatting.swift @@ -31,7 +31,7 @@ extension CustomDateComponentsFormatting { let dateComponents = calendar.dateComponents([.year, .day], from: start, to: end) let years = dateComponents.year ?? 0 - var days = dateComponents.day ?? 0 + let days = dateComponents.day ?? 0 if years >= 2 { formatter.allowedUnits = [.year] diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconInfo.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconInfo.imageset/Contents.json new file mode 100644 index 0000000000..87bd86d2ec --- /dev/null +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconInfo.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "IconInfo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconInfo.imageset/IconInfo.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconInfo.imageset/IconInfo.pdf new file mode 100644 index 0000000000..07f05b149b --- /dev/null +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconInfo.imageset/IconInfo.pdf @@ -0,0 +1,71 @@ +%PDF-1.5 +% +4 0 obj +<< /Length 5 0 R + /Filter /FlateDecode +>> +stream +xUMn0> +`=B̢2"a焁np<w"d +fnâzL?A1H]بKQ8FXBGj=2q0rMb gK(5ly!%F.-W +V8Pwf2q͖qhVkz"_~g床mDrucY8-W:IMxAXUZݝe- }~V {+/ױzM+ٰze+j
S27@>{~b +endstream +endobj +5 0 obj + 296 +endobj +3 0 obj +<< + /ExtGState << + /a0 << /CA 1 /ca 1 >> + >> +>> +endobj +2 0 obj +<< /Type /Page % 1 + /Parent 1 0 R + /MediaBox [ 0 0 18 18 ] + /Contents 4 0 R + /Group << + /Type /Group + /S /Transparency + /I true + /CS /DeviceRGB + >> + /Resources 3 0 R +>> +endobj +1 0 obj +<< /Type /Pages + /Kids [ 2 0 R ] + /Count 1 +>> +endobj +6 0 obj +<< /Producer (cairo 1.16.0 (https://cairographics.org)) + /CreationDate (20200729113941+00'00) +>> +endobj +7 0 obj +<< /Type /Catalog + /Pages 1 0 R +>> +endobj +xref +0 8 +0000000000 65535 f +0000000698 00000 n +0000000482 00000 n +0000000410 00000 n +0000000015 00000 n +0000000388 00000 n +0000000763 00000 n +0000000877 00000 n +trailer +<< /Size 8 + /Root 7 0 R + /Info 6 0 R +>> +startxref +929 +%%EOF diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift index 8f78abd844..38fc56090f 100644 --- a/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift +++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift @@ -12,6 +12,7 @@ protocol PreferencesCellEventHandler { func addDNSEntry() func didChangeDNSEntry(with identifier: UUID, inputString: String) -> Bool func didChangeState(for item: PreferencesDataSource.Item, isOn: Bool) + func didPressInfoButton(for item: PreferencesDataSource.Item) } final class PreferencesCellFactory: CellFactoryProtocol { @@ -45,6 +46,7 @@ final class PreferencesCellFactory: CellFactoryProtocol { comment: "" ) cell.accessibilityHint = nil + cell.applySubCellStyling() cell.setOn(viewModel.blockAdvertising, animated: false) cell.action = { [weak self] isOn in self?.delegate?.didChangeState( @@ -63,6 +65,7 @@ final class PreferencesCellFactory: CellFactoryProtocol { comment: "" ) cell.accessibilityHint = nil + cell.applySubCellStyling() cell.setOn(viewModel.blockTracking, animated: false) cell.action = { [weak self] isOn in self?.delegate?.didChangeState( @@ -81,7 +84,12 @@ final class PreferencesCellFactory: CellFactoryProtocol { comment: "" ) cell.accessibilityHint = nil + cell.applySubCellStyling() + cell.setInfoButtonIsVisible(true) cell.setOn(viewModel.blockMalware, animated: false) + cell.infoButtonHandler = { [weak self] in + self?.delegate?.didPressInfoButton(for: .blockMalware) + } cell.action = { [weak self] isOn in self?.delegate?.didChangeState(for: .blockMalware, isOn: isOn) } @@ -96,6 +104,7 @@ final class PreferencesCellFactory: CellFactoryProtocol { comment: "" ) cell.accessibilityHint = nil + cell.applySubCellStyling() cell.setOn(viewModel.blockAdultContent, animated: false) cell.action = { [weak self] isOn in self?.delegate?.didChangeState( @@ -114,6 +123,7 @@ final class PreferencesCellFactory: CellFactoryProtocol { comment: "" ) cell.accessibilityHint = nil + cell.applySubCellStyling() cell.setOn(viewModel.blockGambling, animated: false) cell.action = { [weak self] isOn in self?.delegate?.didChangeState( diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift index b750f5cb68..c57eb887f9 100644 --- a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift +++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift @@ -12,6 +12,8 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< PreferencesDataSource.Section, PreferencesDataSource.Item >, UITableViewDelegate { + typealias InfoButtonHandler = (PreferencesDataSource.Item) -> Void + enum CellReuseIdentifiers: String, CaseIterable { case settingSwitch case dnsServer @@ -30,11 +32,14 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< } private enum HeaderFooterReuseIdentifiers: String, CaseIterable { + case contentBlockerHeader case customDNSFooter case spacer var reusableViewClass: AnyClass { switch self { + case .contentBlockerHeader: + return SettingsContentBlockersHeaderView.self case .customDNSFooter: return SettingsStaticTextFooterView.self case .spacer: @@ -43,8 +48,8 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< } } - enum Section: String, Hashable { - case mullvadDNS + enum Section: String, Hashable, CaseIterable { + case contentBlockers case customDNS } @@ -58,6 +63,10 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< case addDNSServer case dnsServer(_ uniqueID: UUID) + static var contentBlockers: [Item] { + return [.blockAdvertising, .blockTracking, .blockMalware, .blockAdultContent, .blockGambling] + } + var accessibilityIdentifier: String { switch self { case .blockAdvertising: @@ -101,7 +110,9 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< private var isEditing = false - private(set) var viewModel = PreferencesViewModel() + private(set) var viewModel = PreferencesViewModel() { didSet { + preferencesCellFactory.viewModel = viewModel + }} private(set) var viewModelBeforeEditing = PreferencesViewModel() private let preferencesCellFactory: PreferencesCellFactory private weak var tableView: UITableView? @@ -142,7 +153,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< updateSnapshot() reloadCustomDNSFooter() - updateCellFactory(with: viewModel) viewModel.customDNSDomains.forEach { entry in self.reload(item: .dnsServer(entry.identifier)) } @@ -160,7 +170,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< viewModel = mergedViewModel } - updateCellFactory(with: viewModel) updateSnapshot() reloadCustomDNSFooter() } @@ -225,7 +234,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< let removedEntry = viewModel.customDNSDomains.remove(at: sourceIndex) viewModel.customDNSDomains.insert(removedEntry, at: destinationIndex) - updateCellFactory(with: viewModel) updateSnapshot() } @@ -236,16 +244,29 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - return tableView.dequeueReusableHeaderFooterView( - withIdentifier: HeaderFooterReuseIdentifiers.spacer.rawValue - ) + let sectionIdentifier = snapshot().sectionIdentifiers[section] + + switch sectionIdentifier { + case .contentBlockers: + let view = tableView + .dequeueReusableHeaderFooterView( + withIdentifier: HeaderFooterReuseIdentifiers.contentBlockerHeader.rawValue + ) as! SettingsContentBlockersHeaderView + configureContentBlockersHeader(view) + return view + + case .customDNS: + return tableView.dequeueReusableHeaderFooterView( + withIdentifier: HeaderFooterReuseIdentifiers.spacer.rawValue + ) + } } func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { let sectionIdentifier = snapshot().sectionIdentifiers[section] switch sectionIdentifier { - case .mullvadDNS: + case .contentBlockers: return nil case .customDNS: @@ -260,14 +281,22 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< } func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return UIMetrics.sectionSpacing + let sectionIdentifier = snapshot().sectionIdentifiers[section] + + switch sectionIdentifier { + case .contentBlockers: + return UITableView.automaticDimension + + case .customDNS: + return UIMetrics.sectionSpacing + } } func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { let sectionIdentifier = snapshot().sectionIdentifiers[section] switch sectionIdentifier { - case .mullvadDNS: + case .contentBlockers: return 0 case .customDNS: @@ -347,25 +376,30 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< } private func updateSnapshot(animated: Bool = false, completion: (() -> Void)? = nil) { - var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() + var newSnapshot = NSDiffableDataSourceSnapshot<Section, Item>() - snapshot.appendSections([.mullvadDNS, .customDNS]) - snapshot.appendItems( - [.blockAdvertising, .blockTracking, .blockMalware, .blockAdultContent, .blockGambling], - toSection: .mullvadDNS - ) - snapshot.appendItems([.useCustomDNS], toSection: .customDNS) + newSnapshot.appendSections(Section.allCases) + + let oldSnapshot = snapshot() + if oldSnapshot.indexOfSection(.contentBlockers) != nil { + newSnapshot.appendItems( + oldSnapshot.itemIdentifiers(inSection: .contentBlockers), + toSection: .contentBlockers + ) + } + + newSnapshot.appendItems([.useCustomDNS], toSection: .customDNS) let dnsServerItems = viewModel.customDNSDomains.map { entry in return Item.dnsServer(entry.identifier) } - snapshot.appendItems(dnsServerItems, toSection: .customDNS) + newSnapshot.appendItems(dnsServerItems, toSection: .customDNS) if isEditing, viewModel.customDNSDomains.count < DNSSettings.maxAllowedCustomDNSDomains { - snapshot.appendItems([.addDNSServer], toSection: .customDNS) + newSnapshot.appendItems([.addDNSServer], toSection: .customDNS) } - apply(snapshot, completion: completion) + apply(newSnapshot, completion: completion) } private func reload(item: Item) { @@ -376,10 +410,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< } } - func updateCellFactory(with viewModel: PreferencesViewModel) { - preferencesCellFactory.viewModel = viewModel - } - private func setBlockAdvertising(_ isEnabled: Bool) { let oldViewModel = viewModel @@ -478,7 +508,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< let newDNSEntry = DNSServerEntry(address: "") viewModel.customDNSDomains.append(newDNSEntry) - updateCellFactory(with: viewModel) updateSnapshot(animated: true) { [weak self] in if oldViewModel.customDNSPrecondition != self?.viewModel.customDNSPrecondition { self?.reloadCustomDNSFooter() @@ -516,7 +545,6 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< viewModel.customDNSDomains.remove(at: entryIndex) - updateCellFactory(with: viewModel) updateSnapshot(animated: true) { [weak self] in if oldViewModel.customDNSPrecondition != self?.viewModel.customDNSPrecondition { self?.reloadCustomDNSFooter() @@ -525,14 +553,46 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< } private func reloadCustomDNSFooter() { - updateCellFactory(with: viewModel) reload(item: .useCustomDNS) let sectionIndex = snapshot().indexOfSection(.customDNS)! - if let reusableView = tableView? - .footerView(forSection: sectionIndex) as? SettingsStaticTextFooterView - { - configureFooterView(reusableView) + + UIView.performWithoutAnimation { + tableView?.performBatchUpdates({ + if let reusableView = tableView?.footerView(forSection: sectionIndex) as? SettingsStaticTextFooterView { + configureFooterView(reusableView) + } + }) + } + } + + private func configureContentBlockersHeader(_ reusableView: SettingsContentBlockersHeaderView) { + reusableView.titleLabel.text = NSLocalizedString( + "BLOCK_ADS_CELL_LABEL", + tableName: "Preferences", + value: "DNS content blockers", + comment: "" + ) + + reusableView.infoButtonHandler = { [weak self] in + if let self = self { + self.delegate?.preferencesDataSource(self, didPressInfoButton: nil) + } + } + + reusableView.didCollapseHandler = { [weak self] headerView in + guard let self = self else { return } + + var snapshot = self.snapshot() + + if headerView.isExpanded { + snapshot.deleteItems(Item.contentBlockers) + } else { + snapshot.appendItems(Item.contentBlockers, toSection: .contentBlockers) + } + + headerView.isExpanded.toggle() + self.apply(snapshot, animatingDifferences: true) } } @@ -580,4 +640,8 @@ extension PreferencesDataSource: PreferencesCellEventHandler { ) -> Bool { return handleDNSEntryChange(with: identifier, inputString: inputString) } + + func didPressInfoButton(for item: Item) { + delegate?.preferencesDataSource(self, didPressInfoButton: item) + } } diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSourceDelegate.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSourceDelegate.swift index 84f18d93e8..b7242814fe 100644 --- a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSourceDelegate.swift +++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSourceDelegate.swift @@ -13,4 +13,9 @@ protocol PreferencesDataSourceDelegate: AnyObject { _ dataSource: PreferencesDataSource, didChangeViewModel viewModel: PreferencesViewModel ) + + func preferencesDataSource( + _ dataSource: PreferencesDataSource, + didPressInfoButton item: PreferencesDataSource.Item? + ) } diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift index e18ea20e7f..93ccdaa854 100644 --- a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift +++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift @@ -11,6 +11,7 @@ import UIKit class PreferencesViewController: UITableViewController, PreferencesDataSourceDelegate { private let interactor: PreferencesInteractor private var dataSource: PreferencesDataSource? + private let alertPresenter = AlertPresenter() override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent @@ -47,8 +48,10 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel interactor.dnsSettingsDidChange = { [weak self] newDNSSettings in self?.dataSource?.update(from: newDNSSettings) } - dataSource?.update(from: interactor.dnsSettings) + + tableView.tableHeaderView = + UIView(frame: .init(origin: .zero, size: .init(width: 0, height: UIMetrics.sectionSpacing))) } override func setEditing(_ editing: Bool, animated: Bool) { @@ -62,6 +65,23 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel super.setEditing(editing, animated: animated) } + private func showContentBlockerInfo(with message: String) { + let alertController = UIAlertController( + title: nil, + message: message, + preferredStyle: .alert + ) + alertController.addAction( + UIAlertAction(title: NSLocalizedString( + "PREFERENCES_CONTENT_BLOCKERS_OK_ACTION", + tableName: "ContentBlockers", + value: "Got it!", + comment: "" + ), style: .cancel) + ) + alertPresenter.enqueue(alertController, presentingController: self) + } + // MARK: - PreferencesDataSourceDelegate func preferencesDataSource( @@ -72,4 +92,31 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel interactor.setDNSSettings(dnsSettings) } + + func preferencesDataSource( + _ dataSource: PreferencesDataSource, + didPressInfoButton item: PreferencesDataSource.Item? + ) { + let message: String + + switch item { + case .blockMalware: + message = NSLocalizedString( + "PREFERENCES_CONTENT_BLOCKERS_MALWARE", + tableName: "ContentBlockers", + value: "Warning: The malware blocker is not an anti-virus and should not be treated as such, this is just an extra layer of protection.", + comment: "" + ) + + default: + message = NSLocalizedString( + "PREFERENCES_CONTENT_BLOCKERS_GENERAL", + tableName: "ContentBlockers", + value: "When this feature is enabled it stops the device from contacting certain domains or websites known for distributing ads, malware, trackers and more. This might cause issues on certain websites, services, and programs.", + comment: "" + ) + } + + showContentBlockerInfo(with: message) + } } diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift index 8157e300cc..b7320d9ef3 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift @@ -29,9 +29,12 @@ enum SettingsDisclosureType { } class SettingsCell: UITableViewCell { + typealias InfoButtonHandler = () -> Void + let titleLabel = UILabel() let detailTitleLabel = UILabel() let disclosureImageView = UIImageView(image: nil) + var infoButtonHandler: InfoButtonHandler? var disclosureType: SettingsDisclosureType = .none { didSet { @@ -52,6 +55,15 @@ class SettingsCell: UITableViewCell { } } + private let buttonWidth: CGFloat = 24 + private let infoButton: UIButton = { + let button = UIButton(type: .custom) + button.accessibilityIdentifier = "InfoButton" + button.tintColor = .white + button.setImage(UIImage(named: "IconInfo"), for: .normal) + return button + }() + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -65,6 +77,13 @@ class SettingsCell: UITableViewCell { backgroundColor = .clear contentView.backgroundColor = .clear + infoButton.isHidden = true + infoButton.addTarget( + self, + action: #selector(handleInfoButton(_:)), + for: .touchUpInside + ) + titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.font = UIFont.systemFont(ofSize: 17) titleLabel.textColor = UIColor.Cell.titleTextColor @@ -81,21 +100,30 @@ class SettingsCell: UITableViewCell { setLayoutMargins() - contentView.addConstrainedSubviews([titleLabel, detailTitleLabel]) { + let buttonAreaWidth = UIMetrics.contentLayoutMargins.left + UIMetrics.contentLayoutMargins.right + buttonWidth + + contentView.addConstrainedSubviews([titleLabel, infoButton, detailTitleLabel]) { switch style { case .subtitle: - titleLabel.pinEdgesToSuperviewMargins(.all().excluding(.bottom)) + titleLabel.pinEdgesToSuperviewMargins(.init([.top(0), .leading(0)])) detailTitleLabel.pinEdgesToSuperviewMargins(.all().excluding(.top)) detailTitleLabel.topAnchor.constraint(equalToSystemSpacingBelow: titleLabel.bottomAnchor, multiplier: 1) + infoButton.trailingAnchor.constraint(greaterThanOrEqualTo: contentView.trailingAnchor) default: titleLabel.pinEdgesToSuperviewMargins(.all().excluding(.trailing)) detailTitleLabel.pinEdgesToSuperviewMargins(.all().excluding(.leading)) - detailTitleLabel.leadingAnchor.constraint( - greaterThanOrEqualToSystemSpacingAfter: titleLabel.trailingAnchor, - multiplier: 1 - ) + detailTitleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: infoButton.trailingAnchor) } + + infoButton.pinEdgesToSuperview(.init([.top(0)])) + infoButton.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor) + infoButton.leadingAnchor.constraint( + equalTo: titleLabel.trailingAnchor, + constant: -UIMetrics.interButtonSpacing + ) + infoButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor) + infoButton.widthAnchor.constraint(equalToConstant: buttonAreaWidth) } } @@ -106,9 +134,23 @@ class SettingsCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() + setInfoButtonIsVisible(false) setLayoutMargins() } + func applySubCellStyling() { + contentView.layoutMargins.left += UIMetrics.cellIndentationWidth + backgroundView?.backgroundColor = UIColor.SubCell.backgroundColor + } + + func setInfoButtonIsVisible(_ visible: Bool) { + infoButton.isHidden = !visible + } + + @objc private func handleInfoButton(_ sender: UIControl) { + infoButtonHandler?() + } + private func setLayoutMargins() { // Set layout margins for standard acceessories added into the cell (reorder control, etc..) layoutMargins = UIMetrics.settingsCellLayoutMargins diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsContentBlockersHeaderView.swift b/ios/MullvadVPN/View controllers/Settings/SettingsContentBlockersHeaderView.swift new file mode 100644 index 0000000000..6e21173d49 --- /dev/null +++ b/ios/MullvadVPN/View controllers/Settings/SettingsContentBlockersHeaderView.swift @@ -0,0 +1,143 @@ +// +// SettingsContentBlockersHeaderView.swift +// MullvadVPN +// +// Created by Jon Petersson on 2023-04-06. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +class SettingsContentBlockersHeaderView: UITableViewHeaderFooterView { + typealias InfoButtonHandler = () -> Void + typealias CollapseHandler = (SettingsContentBlockersHeaderView) -> Void + + let titleLabel: UILabel = { + let titleLabel = UILabel() + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.font = .systemFont(ofSize: 17) + titleLabel.textColor = UIColor.Cell.titleTextColor + titleLabel.numberOfLines = 0 + return titleLabel + }() + + let infoButton: UIButton = { + let button = UIButton(type: .custom) + button.accessibilityIdentifier = "InfoButton" + button.tintColor = .white + button.setImage(UIImage(named: "IconInfo"), for: .normal) + return button + }() + + let collapseButton: UIButton = { + let button = UIButton(type: .custom) + button.accessibilityIdentifier = "CollapseButton" + button.isAccessibilityElement = false + button.tintColor = .white + return button + }() + + var isExpanded = false { + didSet { + updateCollapseImage() + updateAccessibilityCustomActions() + } + } + + var didCollapseHandler: CollapseHandler? + var infoButtonHandler: InfoButtonHandler? + + private let chevronDown = UIImage(named: "IconChevronDown") + private let chevronUp = UIImage(named: "IconChevronUp") + private let buttonWidth: CGFloat = 24 + + override init(reuseIdentifier: String?) { + super.init(reuseIdentifier: reuseIdentifier) + + infoButton.addTarget( + self, + action: #selector(handleInfoButton(_:)), + for: .touchUpInside + ) + + collapseButton.addTarget( + self, + action: #selector(handleCollapseButton(_:)), + for: .touchUpInside + ) + + contentView.layoutMargins = UIMetrics.settingsCellLayoutMargins + contentView.backgroundColor = UIColor.Cell.backgroundColor + + let buttonAreaWidth = UIMetrics.contentLayoutMargins.left + UIMetrics.contentLayoutMargins.right + buttonWidth + + contentView.addConstrainedSubviews([titleLabel, infoButton, collapseButton]) { + titleLabel.pinEdgesToSuperviewMargins(.all().excluding(.trailing).excluding(.bottom)) + titleLabel.bottomAnchor.constraint( + equalTo: contentView.bottomAnchor, + constant: -contentView.layoutMargins.bottom + ).withPriority(.defaultHigh) + + infoButton.pinEdgesToSuperview(.init([.top(0), .bottom(0)])) + infoButton.leadingAnchor.constraint( + equalTo: titleLabel.trailingAnchor, + constant: -UIMetrics.interButtonSpacing + ) + infoButton.widthAnchor.constraint(equalToConstant: buttonAreaWidth) + + collapseButton.pinEdgesToSuperview(.all().excluding(.leading)) + collapseButton.leadingAnchor.constraint(greaterThanOrEqualTo: infoButton.trailingAnchor) + collapseButton.widthAnchor.constraint(equalToConstant: buttonAreaWidth) + } + + updateCollapseImage() + updateAccessibilityCustomActions() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func handleInfoButton(_ sender: UIControl) { + infoButtonHandler?() + } + + @objc private func handleCollapseButton(_ sender: UIControl) { + didCollapseHandler?(self) + } + + @objc private func toggleCollapseAccessibilityAction() -> Bool { + didCollapseHandler?(self) + return true + } + + private func updateCollapseImage() { + let image = isExpanded ? chevronUp : chevronDown + + collapseButton.setImage(image, for: .normal) + } + + private func updateAccessibilityCustomActions() { + let actionName = isExpanded + ? NSLocalizedString( + "CONTENT_BLOCKERS_COLLAPSE_ACCESSIBILITY_ACTION", + tableName: "Settings", + value: "Collapse content blockers", + comment: "" + ) + : NSLocalizedString( + "CONTENT_BLOCKERS_EXPAND_ACCESSIBILITY_ACTION", + tableName: "Settings", + value: "Expand content blockers", + comment: "" + ) + + accessibilityCustomActions = [ + UIAccessibilityCustomAction( + name: actionName, + target: self, + selector: #selector(toggleCollapseAccessibilityAction) + ), + ] + } +} diff --git a/ios/convert-assets.rb b/ios/convert-assets.rb index 41fd7a7919..9b2954faea 100755 --- a/ios/convert-assets.rb +++ b/ios/convert-assets.rb @@ -27,6 +27,7 @@ GRAPHICAL_ASSETS = [ "icon-chevron.svg", "icon-extLink.svg", "icon-fail.svg", + "icon-info.svg", "icon-reload.svg", "icon-settings.svg", "icon-spinner.svg", @@ -45,7 +46,8 @@ GRAPHICAL_ASSETS = [ # graphical assets to resize. RESIZE_ASSETS = { - "icon-tick.svg" => ["icon-tick-sml.svg", 16, 16], + "icon-info.svg" => ["icon-info.svg", 18, 18], + "icon-tick.svg" => ["icon-tick-sml.svg", 16, 16] } # Additional assets generated from SVG -> vector PDF |
