From e77fba76ce9ac9fb082fa3cf531869a8032329de Mon Sep 17 00:00:00 2001 From: Jon Petersson Date: Tue, 17 Sep 2024 11:26:44 +0200 Subject: [PATCH] Add counter and 'all' switch to DNS content blockers --- .../Classes/AccessbilityIdentifier.swift | 1 + .../Settings/SettingsCell.swift | 5 +- .../VPNSettings/CustomDNSCellFactory.swift | 17 ++- .../VPNSettings/CustomDNSDataSource.swift | 112 ++++++++++-------- .../VPNSettingsViewController.swift | 1 + .../VPNSettings/VPNSettingsViewModel.swift | 43 ++++++- 6 files changed, 128 insertions(+), 51 deletions(-) diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index c673fdfe5797..b828221a8863 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -179,6 +179,7 @@ public enum AccessibilityIdentifier: String { case wireGuardPort // Custom DNS + case blockAll case blockAdvertising case blockTracking case blockMalware diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift index 8b4db565d072..c55b5f0aa2f8 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift @@ -59,6 +59,7 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { } } + private var subCellLeadingIndentation: CGFloat = 0 private let buttonWidth: CGFloat = 24 private let infoButton: UIButton = { let button = UIButton(type: .custom) @@ -85,6 +86,8 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { infoButton.isHidden = true infoButton.addTarget(self, action: #selector(handleInfoButton(_:)), for: .touchUpInside) + subCellLeadingIndentation = contentView.layoutMargins.left + UIMetrics.TableView.cellIndentationWidth + titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.font = UIFont.systemFont(ofSize: 17) titleLabel.textColor = UIColor.Cell.titleTextColor @@ -149,7 +152,7 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { } func applySubCellStyling() { - contentView.layoutMargins.left += UIMetrics.TableView.cellIndentationWidth + contentView.layoutMargins.left = subCellLeadingIndentation backgroundView?.backgroundColor = UIColor.Cell.Background.indentationLevelOne } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSCellFactory.swift b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSCellFactory.swift index e1e6b7680f1e..11536c55f31f 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSCellFactory.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSCellFactory.swift @@ -46,7 +46,7 @@ final class CustomDNSCellFactory: CellFactoryProtocol { cell.titleLabel.text = title cell.accessibilityIdentifier = preference.accessibilityIdentifier cell.applySubCellStyling() - cell.setOn(toggleSetting, animated: false) + cell.setOn(toggleSetting, animated: true) cell.action = { [weak self] isOn in self?.delegate?.didChangeState( for: preference, @@ -58,6 +58,21 @@ final class CustomDNSCellFactory: CellFactoryProtocol { // swiftlint:disable:next function_body_length func configureCell(_ cell: UITableViewCell, item: CustomDNSDataSource.Item, indexPath: IndexPath) { switch item { + case .blockAll: + let localizedString = NSLocalizedString( + "BLOCK_ALL_CELL_LABEL", + tableName: "VPNSettings", + value: "All", + comment: "" + ) + + configure( + cell, + toggleSetting: viewModel.blockAll, + title: localizedString, + for: .blockAll + ) + case .blockAdvertising: let localizedString = NSLocalizedString( "BLOCK_ADS_CELL_LABEL", diff --git a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSDataSource.swift b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSDataSource.swift index a2bce16dac83..d6ea9cbc6033 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSDataSource.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSDataSource.swift @@ -49,6 +49,7 @@ final class CustomDNSDataSource: UITableViewDiffableDataSource< } enum Item: Hashable { + case blockAll case blockAdvertising case blockTracking case blockMalware @@ -61,11 +62,21 @@ final class CustomDNSDataSource: UITableViewDiffableDataSource< case dnsServerInfo static var contentBlockers: [Item] { - [.blockAdvertising, .blockTracking, .blockMalware, .blockGambling, .blockAdultContent, .blockSocialMedia] + [ + .blockAll, + .blockAdvertising, + .blockTracking, + .blockMalware, + .blockGambling, + .blockAdultContent, + .blockSocialMedia, + ] } var accessibilityIdentifier: AccessibilityIdentifier { switch self { + case .blockAll: + return .blockAll case .blockAdvertising: return .blockAdvertising case .blockTracking: @@ -403,85 +414,74 @@ final class CustomDNSDataSource: UITableViewDiffableDataSource< } } - private func setBlockAdvertising(_ isEnabled: Bool) { + private func setBlockAll(_ isEnabled: Bool) { let oldViewModel = viewModel + viewModel.setBlockAll(isEnabled) + reloadBlockerData(isEnabled, oldViewModel: oldViewModel) - viewModel.setBlockAdvertising(isEnabled) - - if oldViewModel.customDNSPrecondition != viewModel.customDNSPrecondition { - reloadDnsServerInfo() + [ + .blockAdvertising, + .blockTracking, + .blockMalware, + .blockAdultContent, + .blockGambling, + .blockSocialMedia, + ].forEach { item in + reload(item: item) } + } - if !isEditing { - delegate?.didChangeViewModel(viewModel) - } + private func setBlockAdvertising(_ isEnabled: Bool) { + let oldViewModel = viewModel + viewModel.setBlockAdvertising(isEnabled) + reloadBlockerData(isEnabled, oldViewModel: oldViewModel) } private func setBlockTracking(_ isEnabled: Bool) { let oldViewModel = viewModel - viewModel.setBlockTracking(isEnabled) - - if oldViewModel.customDNSPrecondition != viewModel.customDNSPrecondition { - reloadDnsServerInfo() - } - - if !isEditing { - delegate?.didChangeViewModel(viewModel) - } + reloadBlockerData(isEnabled, oldViewModel: oldViewModel) } private func setBlockMalware(_ isEnabled: Bool) { let oldViewModel = viewModel - viewModel.setBlockMalware(isEnabled) - - if oldViewModel.customDNSPrecondition != viewModel.customDNSPrecondition { - reloadDnsServerInfo() - } - - if !isEditing { - delegate?.didChangeViewModel(viewModel) - } + reloadBlockerData(isEnabled, oldViewModel: oldViewModel) } private func setBlockAdultContent(_ isEnabled: Bool) { let oldViewModel = viewModel - viewModel.setBlockAdultContent(isEnabled) - - if oldViewModel.customDNSPrecondition != viewModel.customDNSPrecondition { - reloadDnsServerInfo() - } - - if !isEditing { - delegate?.didChangeViewModel(viewModel) - } + reloadBlockerData(isEnabled, oldViewModel: oldViewModel) } private func setBlockGambling(_ isEnabled: Bool) { let oldViewModel = viewModel - viewModel.setBlockGambling(isEnabled) - - if oldViewModel.customDNSPrecondition != viewModel.customDNSPrecondition { - reloadDnsServerInfo() - } - - if !isEditing { - delegate?.didChangeViewModel(viewModel) - } + reloadBlockerData(isEnabled, oldViewModel: oldViewModel) } private func setBlockSocialMedia(_ isEnabled: Bool) { let oldViewModel = viewModel - viewModel.setBlockSocialMedia(isEnabled) + reloadBlockerData(isEnabled, oldViewModel: oldViewModel) + } + private func reloadBlockerData(_ isEnabled: Bool, oldViewModel: VPNSettingsViewModel) { if oldViewModel.customDNSPrecondition != viewModel.customDNSPrecondition { reloadDnsServerInfo() } + if !isEnabled || viewModel.allBlockersEnabled { + reload(item: .blockAll) + } + + if + let index = snapshot().sectionIdentifiers.firstIndex(of: .contentBlockers), + let headerView = tableView?.headerView(forSection: index) as? SettingsHeaderView { + configureContentBlockersHeader(headerView) + } + if !isEditing { delegate?.didChangeViewModel(viewModel) } @@ -588,8 +588,23 @@ final class CustomDNSDataSource: UITableViewDiffableDataSource< comment: "" ) - header.titleLabel.text = title + let enabledBlockersCount = viewModel.enabledBlockersCount + let attributedTitle = NSMutableAttributedString(string: title) + let blockerCountText = NSAttributedString(string: " (\(enabledBlockersCount))", attributes: [ + .foregroundColor: UIColor.primaryTextColor.withAlphaComponent(0.6), + ]) + + if enabledBlockersCount > 0 { + attributedTitle.append(blockerCountText) + } + + UIView.transition(with: header.titleLabel, duration: 0.2, options: .transitionCrossDissolve) { + header.titleLabel.attributedText = attributedTitle + } + header.titleLabel.sizeToFit() + header.accessibilityCustomActionName = title + header.accessibilityValue = "\(enabledBlockersCount)" header.accessibilityIdentifier = .dnsContentBlockersHeaderView header.infoButtonHandler = { [weak self] in @@ -618,6 +633,9 @@ final class CustomDNSDataSource: UITableViewDiffableDataSource< extension CustomDNSDataSource: CustomDNSCellEventHandler { func didChangeState(for preference: Item, isOn: Bool) { switch preference { + case .blockAll: + setBlockAll(isOn) + case .blockAdvertising: setBlockAdvertising(isOn) diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift index 32facd0ecab8..5ce99f689f5a 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift @@ -128,6 +128,7 @@ extension VPNSettingsViewController: VPNSettingsDataSourceDelegate { interactor.evaluateDaitaSettingsCompatibility(settings) } + // swiftlint:disable:next function_body_length func showPrompt( for item: VPNSettingsPromptAlertItem, onSave: @escaping () -> Void, diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift index 94fde408b6a9..1c125e19fd48 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift @@ -84,6 +84,7 @@ struct DNSServerEntry: Equatable, Hashable { } struct VPNSettingsViewModel: Equatable { + private(set) var blockAll: Bool private(set) var blockAdvertising: Bool private(set) var blockTracking: Bool private(set) var blockMalware: Bool @@ -104,39 +105,69 @@ struct VPNSettingsViewModel: Equatable { static let defaultWireGuardPorts: [UInt16] = [51820, 53] + var enabledBlockersCount: Int { + [ + blockAdvertising, + blockTracking, + blockMalware, + blockAdultContent, + blockGambling, + blockSocialMedia, + ].filter { $0 }.count + } + + var allBlockersEnabled: Bool { + enabledBlockersCount == CustomDNSDataSource.Item.contentBlockers.filter { $0 != .blockAll }.count + } + + mutating func setBlockAll(_ newValue: Bool) { + blockAll = newValue + blockAdvertising = newValue + blockTracking = newValue + blockMalware = newValue + blockAdultContent = newValue + blockGambling = newValue + blockSocialMedia = newValue + enableCustomDNS = false + } + mutating func setBlockAdvertising(_ newValue: Bool) { blockAdvertising = newValue + blockAll = allBlockersEnabled enableCustomDNS = false } mutating func setBlockTracking(_ newValue: Bool) { blockTracking = newValue + blockAll = allBlockersEnabled enableCustomDNS = false } mutating func setBlockMalware(_ newValue: Bool) { blockMalware = newValue + blockAll = allBlockersEnabled enableCustomDNS = false } mutating func setBlockAdultContent(_ newValue: Bool) { blockAdultContent = newValue + blockAll = allBlockersEnabled enableCustomDNS = false } mutating func setBlockGambling(_ newValue: Bool) { blockGambling = newValue + blockAll = allBlockersEnabled enableCustomDNS = false } mutating func setBlockSocialMedia(_ newValue: Bool) { blockSocialMedia = newValue + blockAll = allBlockersEnabled enableCustomDNS = false } mutating func setEnableCustomDNS(_ newValue: Bool) { - blockTracking = false - blockAdvertising = false enableCustomDNS = newValue } @@ -195,12 +226,20 @@ struct VPNSettingsViewModel: Equatable { init(from tunnelSettings: LatestTunnelSettings = LatestTunnelSettings()) { let dnsSettings = tunnelSettings.dnsSettings + blockAdvertising = dnsSettings.blockingOptions.contains(.blockAdvertising) blockTracking = dnsSettings.blockingOptions.contains(.blockTracking) blockMalware = dnsSettings.blockingOptions.contains(.blockMalware) blockAdultContent = dnsSettings.blockingOptions.contains(.blockAdultContent) blockGambling = dnsSettings.blockingOptions.contains(.blockGambling) blockSocialMedia = dnsSettings.blockingOptions.contains(.blockSocialMedia) + blockAll = blockAdvertising + && blockTracking + && blockMalware + && blockAdultContent + && blockGambling + && blockSocialMedia + enableCustomDNS = dnsSettings.enableCustomDNS customDNSDomains = dnsSettings.customDNSDomains.map { ipAddress in DNSServerEntry(identifier: UUID(), address: "\(ipAddress)")