From c3036e118db5a0bf58684bb5efd5c45061ef6456 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Wed, 3 Apr 2024 16:48:48 -0400 Subject: [PATCH 01/32] Location text --- DuckDuckGo.xcodeproj/project.pbxproj | 18 ++++ ...etworkProtectionNavBarPopoverManager.swift | 1 + .../DefaultVPNLocationFormatter.swift | 83 +++++++++++++++++ DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 1 + .../UserText+NetworkProtectionUI.swift | 5 ++ .../Menu/StatusBarMenu.swift | 4 + .../NetworkProtectionPopover.swift | 2 + .../SwiftUI/MenuItemCustomButton.swift | 88 +++++++++++++++++++ .../NetworkProtectionStatusViewModel.swift | 3 + .../TunnelControllerView.swift | 50 +++++++++-- .../TunnelControllerViewModel.swift | 20 +++++ 11 files changed, 266 insertions(+), 9 deletions(-) create mode 100644 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI/MenuItemCustomButton.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 5ab306a610..b4f8102558 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -3256,6 +3256,13 @@ BD384ACD2BBC821D00EF3735 /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */; }; BD384ACE2BBC821D00EF3735 /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; BD8A305D2BC425B000D0669F /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = BD8A305C2BC425B000D0669F /* Lottie */; }; + BDA7647C2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */; }; + BDA7647D2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */; }; + BDA7647E2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */; }; + BDA7647F2BC4998900D0400C /* DefaultVPNLocationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */; }; + BDA764802BC4998A00D0400C /* DefaultVPNLocationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */; }; + BDA764842BC49E3F00D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */; }; + BDA764852BC49E4000D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */; }; BDE981D82BBCE4C700645880 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = BDE981D72BBCE4C700645880 /* Lottie */; }; BDE981D92BBD10D600645880 /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */; }; BDE981DA2BBD10D600645880 /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; @@ -4776,6 +4783,8 @@ BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionExternalWaitlistPixels.swift; sourceTree = ""; }; BD384AC72BBC821100EF3735 /* vpn-light-mode.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "vpn-light-mode.json"; sourceTree = ""; }; BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "vpn-dark-mode.json"; sourceTree = ""; }; + BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultVPNLocationFormatter.swift; sourceTree = ""; }; + BDA764812BC499D200D0400C /* BrowserServicesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = BrowserServicesKit; path = ../BrowserServicesKit; sourceTree = ""; }; C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureTextField.swift; sourceTree = ""; }; C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionExecutor.swift; sourceTree = ""; }; C13909F32B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillDeleteAllPasswordsExecutorTests.swift; sourceTree = ""; }; @@ -7203,6 +7212,7 @@ AA585D75248FD31100E9A3E2 = { isa = PBXGroup; children = ( + BDA764812BC499D200D0400C /* BrowserServicesKit */, 378B5886295CF2A4002C0CC0 /* Configuration */, 378E279C2970217400FCADA2 /* LocalPackages */, 7BB108552A43375D000AB95F /* LocalThirdParty */, @@ -8851,6 +8861,7 @@ EEA3EEAF2B24EB5100E8333A /* VPNLocation */ = { isa = PBXGroup; children = ( + BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */, EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */, EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */, EEC4A6682B2C87D300F7C0AA /* VPNLocationView.swift */, @@ -10772,6 +10783,7 @@ 3706FBCB293F65D500E42796 /* BookmarkHTMLImporter.swift in Sources */, 4BF97ADC2B43C5E200EB4240 /* VPNFeedbackSender.swift in Sources */, 987799F72999996B005D8EB6 /* BookmarkDatabase.swift in Sources */, + BDA7647D2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift in Sources */, 3706FBCC293F65D500E42796 /* CustomRoundedCornersShape.swift in Sources */, 3706FBCD293F65D500E42796 /* LocaleExtension.swift in Sources */, 3706FBCE293F65D500E42796 /* SavePaymentMethodViewController.swift in Sources */, @@ -11479,8 +11491,10 @@ 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */, + BDA764842BC49E3F00D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, EEC589DB2A4F1CE700BCD60C /* AppLauncher.swift in Sources */, B65DA5EF2A77CC3A00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, + BDA7647F2BC4998900D0400C /* DefaultVPNLocationFormatter.swift in Sources */, 4BF0E5072AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 7BA7CC442AD11E490042E5CE /* UserText.swift in Sources */, 4BF0E5142AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, @@ -11513,8 +11527,10 @@ 7BFE95592A9DF2AF0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, 7BA7CC5C2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, B65DA5F02A77CC3C00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, + BDA764852BC49E4000D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, 7BAF9E4D2A8A3CCB002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */, 7BA7CC392AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */, + BDA764802BC4998A00D0400C /* DefaultVPNLocationFormatter.swift in Sources */, 7BA7CC552AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */, 7BA7CC3D2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 4BA7C4DA2B3F639800AFE511 /* NetworkProtectionTunnelController.swift in Sources */, @@ -11932,6 +11948,7 @@ 4B957A7C2AC7AE700062CA31 /* NSAlert+ActiveDownloadsTermination.swift in Sources */, B62B48412ADE48DE000DECE5 /* ArrayBuilder.swift in Sources */, 4B957A7D2AC7AE700062CA31 /* IndexPathExtension.swift in Sources */, + BDA7647E2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift in Sources */, 4B957A7E2AC7AE700062CA31 /* PasswordManagementNoteItemView.swift in Sources */, B6B4D1D22B0E0DD000C26286 /* DataImportNoDataView.swift in Sources */, 4B957A7F2AC7AE700062CA31 /* NSApplicationExtension.swift in Sources */, @@ -13075,6 +13092,7 @@ 1456D6E124EFCBC300775049 /* TabBarCollectionView.swift in Sources */, 4B4D60BF2A0C848A00BCD287 /* NetworkProtection+ConvenienceInitializers.swift in Sources */, 4B6B64842BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */, + BDA7647C2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift in Sources */, 3158B1592B0BF76400AF130C /* DataBrokerProtectionFeatureDisabler.swift in Sources */, B655124829A79465009BFE1C /* NavigationActionExtension.swift in Sources */, 9FA173EB2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */, diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index f18edc61b7..fcf40ff007 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -114,6 +114,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { agentLoginItem: LoginItem.vpnMenu, isMenuBarStatusView: false, userDefaults: .netP, + locationFormatter: DefaultVPNLocationFormatter(), uninstallHandler: { [weak self] in _ = await self?.networkProtectionFeatureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: true) }) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift new file mode 100644 index 0000000000..fa2b503391 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift @@ -0,0 +1,83 @@ +// +// DefaultVPNLocationFormatter.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NetworkProtection + +struct DefaultVPNLocationFormatter: VPNLocationFormatting { + func string(from location: String?, preferredLocation: VPNSettings.SelectedLocation) -> String { + let preferredLocation = VPNLocationModel(selectedLocation: selectedLocation) + + if let location { + return preferredLocation.isNearest ? "\(location) (Nearest)" : location + } + + return preferredLocation.title + } + + @available(macOS 12, *) + func string(from location: String?, + preferredLocation: VPNSettings.SelectedLocation, + locationTextColor: Color, + preferredLocationTextColor: Color) -> AttributedString { + let preferredLocation = VPNLocationModel(selectedLocation: selectedLocation) + + if let location { + var attributedString = AttributedString( + preferredLocation.isNearest ? "\(location) (Nearest)" : location + ) + attributedString.foregroundColor = locationTextColor + if let range = attributedString.range(of: "(Nearest)") { + attributedString[range].foregroundColor = preferredLocationTextColor + } + return attributedString + } + + var attributedString = AttributedString(preferredLocation.title) + attributedString.foregroundColor = locationTextColor + return attributedString + } +} + +final class VPNLocationModel: ObservableObject { + enum LocationIcon { + case defaultIcon + case emoji(String) + } + + let title: String + let subtitle: String? + let icon: LocationIcon + let isNearest: Bool + + init(selectedLocation: VPNSettings.SelectedLocation) { + switch selectedLocation { + case .nearest: + title = UserText.vpnLocationNearestAvailable + subtitle = UserText.vpnLocationNearestAvailableSubtitle + icon = .defaultIcon + isNearest = true + case .location(let location): + let countryLabelsModel = NetworkProtectionVPNCountryLabelsModel(country: location.country) + title = countryLabelsModel.title + subtitle = selectedLocation.location?.city + icon = .emoji(countryLabelsModel.emoji) + isNearest = false + } + } +} diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index 67d2baeb1d..124ce85aec 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -256,6 +256,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { agentLoginItem: nil, isMenuBarStatusView: true, userDefaults: .netP, + locationFormatter: DefaultVPNLocationFormatter(), uninstallHandler: { [weak self] in guard let self else { return } await self.vpnUninstaller.uninstall(includingSystemExtension: true) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift index 174b133a09..0236302a96 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift @@ -29,6 +29,11 @@ final class UserText { static let networkProtectionStatusViewFeatureOn = NSLocalizedString("network.protection.status.view.feature.on", value: "DuckDuckGo VPN is ON", comment: "Text shown in NetworkProtection's status view when NetP is ON.") static let networkProtectionStatusViewTimerZero = "00:00:00" + static let netPVPNLocationNearest = NSLocalizedString("network.protection.vpn.location.nearest", value: "(Nearest)", comment: "Description of the location type in the VPN status view") + static let vpnLocationConnected = NSLocalizedString("network.protection.vpn.location.connected", value: "Connected Location", comment: "Description of the location type in the VPN status view") + static let vpnLocationSelected = NSLocalizedString("network.protection.vpn.location.selected", value: "Selected Location", comment: "Description of the location type in the VPN status view") + static let vpnDataVolume = NSLocalizedString("network.protection.vpn.data-volume", value: "Data Volume", comment: "Title for the data volume section in the VPN status view") + // MARK: - Onboarding static let networkProtectionOnboardingInstallExtensionTitle = NSLocalizedString("network.protection.onboarding.install.extension.title", value: "Install VPN System Extension", comment: "Title for the onboarding install-vpn-extension step") diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift index 86a10b44aa..5e02caa125 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift @@ -42,6 +42,7 @@ public final class StatusBarMenu: NSObject { private let agentLoginItem: LoginItem? private let isMenuBarStatusView: Bool private let userDefaults: UserDefaults + private let locationFormatter: VPNLocationFormatting private let uninstallHandler: () async -> Void // MARK: - NetP Icon publisher @@ -68,6 +69,7 @@ public final class StatusBarMenu: NSObject { agentLoginItem: LoginItem?, isMenuBarStatusView: Bool, userDefaults: UserDefaults, + locationFormatter: VPNLocationFormatting, uninstallHandler: @escaping () async -> Void) { self.model = model @@ -83,6 +85,7 @@ public final class StatusBarMenu: NSObject { self.agentLoginItem = agentLoginItem self.isMenuBarStatusView = isMenuBarStatusView self.userDefaults = userDefaults + self.locationFormatter = locationFormatter self.uninstallHandler = uninstallHandler super.init() @@ -136,6 +139,7 @@ public final class StatusBarMenu: NSObject { agentLoginItem: agentLoginItem, isMenuBarStatusView: isMenuBarStatusView, userDefaults: userDefaults, + locationFormatter: locationFormatter, uninstallHandler: uninstallHandler) popover?.behavior = .transient diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift index 073db45255..4236e963aa 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift @@ -58,6 +58,7 @@ public final class NetworkProtectionPopover: NSPopover { agentLoginItem: LoginItem?, isMenuBarStatusView: Bool, userDefaults: UserDefaults, + locationFormatter: VPNLocationFormatting, uninstallHandler: @escaping () async -> Void) { self.statusReporter = statusReporter @@ -70,6 +71,7 @@ public final class NetworkProtectionPopover: NSPopover { agentLoginItem: agentLoginItem, isMenuBarStatusView: isMenuBarStatusView, userDefaults: userDefaults, + locationFormatter: locationFormatter, uninstallHandler: uninstallHandler) super.init() diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI/MenuItemCustomButton.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI/MenuItemCustomButton.swift new file mode 100644 index 0000000000..cdd8bd79c8 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI/MenuItemCustomButton.swift @@ -0,0 +1,88 @@ +// +// MenuItemCustomButton.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +struct MenuItemCustomButton: View { + @Environment(\.colorScheme) private var colorScheme + private var label: (Bool) -> Label + private let action: () async -> Void + + private let highlightAnimationStepSpeed = 0.05 + + @State private var isHovered = false + @State private var animatingTap = false + + init(action: @escaping () async -> Void, @ViewBuilder label: @escaping (Bool) -> Label) { + self.action = action + self.label = label + } + + var body: some View { + Button(action: { + buttonTapped() + }) { + HStack { + label(isHovered) + }.padding([.top, .bottom], 3) + .padding([.leading, .trailing], 9) + } + .frame(maxWidth: .infinity, alignment: .leading) + .background( + buttonBackground(highlighted: isHovered) + ) + .contentShape(Rectangle()) + .cornerRadius(4) + .onTapGesture { + buttonTapped() + } + .onHover { hovering in + if !animatingTap { + isHovered = hovering + } + } + .buttonStyle(PlainButtonStyle()) + } + + private func buttonBackground(highlighted: Bool) -> some View { + if highlighted { + return AnyView( + VisualEffectView(material: .selection, blendingMode: .withinWindow, state: .active, isEmphasized: true)) + } else { + return AnyView(Color.clear) + } + } + + private func buttonTapped() { + animatingTap = true + isHovered = false + + DispatchQueue.main.asyncAfter(deadline: .now() + highlightAnimationStepSpeed) { + isHovered = true + + DispatchQueue.main.asyncAfter(deadline: .now() + highlightAnimationStepSpeed) { + animatingTap = false + + Task { + await action() + } + } + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index 7df51daf58..eb5509a68f 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -119,6 +119,7 @@ extension NetworkProtectionStatusView { isMenuBarStatusView: Bool, runLoopMode: RunLoop.Mode? = nil, userDefaults: UserDefaults, + locationFormatter: VPNLocationFormatting, uninstallHandler: @escaping () async -> Void) { self.tunnelController = controller @@ -135,6 +136,8 @@ extension NetworkProtectionStatusView { tunnelControllerViewModel = TunnelControllerViewModel(controller: tunnelController, onboardingStatusPublisher: onboardingStatusPublisher, statusReporter: statusReporter, + vpnSettings: .init(defaults: userDefaults), + locationFormatter: locationFormatter, appLauncher: appLauncher) connectionStatus = statusReporter.statusObserver.recentValue diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift index e1239929c0..86503918bf 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -152,6 +152,8 @@ public struct TunnelControllerView: View { Divider() .padding(EdgeInsets(top: 5, leading: 9, bottom: 5, trailing: 9)) + locationView() + if model.showServerDetails { connectionStatusView() .disabled(on: !isEnabled) @@ -214,6 +216,45 @@ public struct TunnelControllerView: View { .frame(width: 8, height: 8) } + /// Connected/Selected location + /// + private func locationView() -> some View { + VStack(alignment: .leading, spacing: 0) { + Text(model.isVPNEnabled ? UserText.vpnLocationConnected : UserText.vpnLocationSelected) + .applySectionHeaderAttributes(colorScheme: colorScheme) + .padding(EdgeInsets(top: 6, leading: 9, bottom: 6, trailing: 9)) + + MenuItemCustomButton { + model.showLocationSettings() + dismiss() + } label: { isHovered in + HStack(alignment: .center, spacing: 10) { + Text("🇻🇳") + .font(.system(size: 13)) + .frame(width: 26, height: 26) + .background(Color(hex: "B2B2B2").opacity(0.3)) + .clipShape(Circle()) + if #available(macOS 12, *) { + if isHovered { + Text(model.plainLocation) + .font(.system(size: 13)) + .foregroundColor(.white) + } else { + Text(model.formattedLocation) + .font(.system(size: 13)) + } + } else { + Text(model.plainLocation) + .font(.system(size: 13)) + .foregroundColor(isHovered ? .white: Color(.defaultText)) + } + } + } + + dividerRow() + } + } + /// Connection status: server IP address and location /// private func connectionStatusView() -> some View { @@ -222,15 +263,6 @@ public struct TunnelControllerView: View { .applySectionHeaderAttributes(colorScheme: colorScheme) .padding(EdgeInsets(top: 6, leading: 9, bottom: 6, trailing: 9)) - MenuItemButton( - iconName: .serverLocationIcon, - title: UserText.networkProtectionStatusViewLocation, - detailTitle: model.serverLocation, - textColor: Color(.defaultText)) { - model.showLocationSettings() - dismiss() - }.applyMenuAttributes() - connectionStatusRow(icon: .ipAddressIcon, title: UserText.networkProtectionStatusViewIPAddress, details: model.serverAddress) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift index 7fd4ffc654..433498e2bf 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift @@ -52,6 +52,10 @@ public final class TunnelControllerViewModel: ObservableObject { /// private let statusReporter: NetworkProtectionStatusReporter + private let vpnSettings: VPNSettings + + private let locationFormatter: VPNLocationFormatting + private let appLauncher: AppLaunching // MARK: - Misc @@ -73,12 +77,16 @@ public final class TunnelControllerViewModel: ObservableObject { onboardingStatusPublisher: OnboardingStatusPublisher, statusReporter: NetworkProtectionStatusReporter, runLoopMode: RunLoop.Mode? = nil, + vpnSettings: VPNSettings, + locationFormatter: VPNLocationFormatting, appLauncher: AppLaunching) { self.tunnelController = controller self.onboardingStatusPublisher = onboardingStatusPublisher self.statusReporter = statusReporter self.runLoopMode = runLoopMode + self.vpnSettings = vpnSettings + self.locationFormatter = locationFormatter self.appLauncher = appLauncher connectionStatus = statusReporter.statusObserver.recentValue @@ -431,6 +439,18 @@ public final class TunnelControllerViewModel: ObservableObject { } } + var plainLocation: String { + locationFormatter.string(from: internalServerLocation, preferredLocation: vpnSettings.selectedLocation) + } + + @available(macOS 12, *) + var formattedLocation: AttributedString { + locationFormatter.string(from: internalServerLocation, + preferredLocation: vpnSettings.selectedLocation, + locationTextColor: Color(.defaultText), + preferredLocationTextColor: Color(.gray)) + } + // MARK: - Toggling VPN /// Start the VPN. From c1fd48afa319a5bb34e20bacac04eb6c783a2899 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Mon, 8 Apr 2024 19:24:12 -0400 Subject: [PATCH 02/32] Refactor --- .../DefaultVPNLocationFormatter.swift | 13 +++++--- .../TunnelControllerView.swift | 33 +++++++++++++------ .../TunnelControllerViewModel.swift | 12 +++++-- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift index fa2b503391..085b5f7748 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift @@ -17,11 +17,16 @@ // import Foundation +import SwiftUI import NetworkProtection struct DefaultVPNLocationFormatter: VPNLocationFormatting { - func string(from location: String?, preferredLocation: VPNSettings.SelectedLocation) -> String { - let preferredLocation = VPNLocationModel(selectedLocation: selectedLocation) + func emoji(for country: String?) -> String? { + country.map { NetworkProtectionVPNCountryLabelsModel(country: $0).emoji } + } + + func string(from location: String?, preferredLocation someLocation: VPNSettings.SelectedLocation) -> String { + let preferredLocation = VPNLocationModel(selectedLocation: someLocation) if let location { return preferredLocation.isNearest ? "\(location) (Nearest)" : location @@ -32,10 +37,10 @@ struct DefaultVPNLocationFormatter: VPNLocationFormatting { @available(macOS 12, *) func string(from location: String?, - preferredLocation: VPNSettings.SelectedLocation, + preferredLocation someLocation: VPNSettings.SelectedLocation, locationTextColor: Color, preferredLocationTextColor: Color) -> AttributedString { - let preferredLocation = VPNLocationModel(selectedLocation: selectedLocation) + let preferredLocation = VPNLocationModel(selectedLocation: someLocation) if let location { var attributedString = AttributedString( diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift index 86503918bf..0c1cab3505 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -28,6 +28,10 @@ fileprivate extension Font { .system(size: 13, weight: .regular, design: .default) } + static var location: Font { + .system(size: 13, weight: .regular, design: .default) + } + static var content: Font { .system(size: 13, weight: .regular, design: .default) } @@ -60,6 +64,10 @@ private enum Opacity { colorScheme == .light ? Double(0.6) : Double(0.5) } + static func location(colorScheme: ColorScheme) -> Double { + colorScheme == .light ? Double(0.6) : Double(0.5) + } + static let content = Double(0.58) static let label = Double(0.9) static let description = Double(0.9) @@ -79,12 +87,15 @@ private enum Opacity { } fileprivate extension View { - func applyConnectionStatusDetailAttributes(colorScheme: ColorScheme) -> some View { - opacity(Opacity.connectionStatusDetail(colorScheme: colorScheme)) + func applyConnectionStatusDetailAttributes(colorScheme: ColorScheme) -> some View { opacity(Opacity.connectionStatusDetail(colorScheme: colorScheme)) .font(.NetworkProtection.connectionStatusDetail) .foregroundColor(Color(.defaultText)) } + func applyLocationAttributes() -> some View { + font(.NetworkProtection.location) + } + func applyContentAttributes(colorScheme: ColorScheme) -> some View { opacity(Opacity.content) .font(.NetworkProtection.content) @@ -229,23 +240,25 @@ public struct TunnelControllerView: View { dismiss() } label: { isHovered in HStack(alignment: .center, spacing: 10) { - Text("🇻🇳") - .font(.system(size: 13)) - .frame(width: 26, height: 26) - .background(Color(hex: "B2B2B2").opacity(0.3)) - .clipShape(Circle()) + if let emoji = model.emoji { + Text(emoji) + .font(.system(size: 13)) + .frame(width: 26, height: 26) + .background(Color(hex: "B2B2B2").opacity(0.3)) + .clipShape(Circle()) + } if #available(macOS 12, *) { if isHovered { Text(model.plainLocation) - .font(.system(size: 13)) + .applyLocationAttributes() .foregroundColor(.white) } else { Text(model.formattedLocation) - .font(.system(size: 13)) + .applyLocationAttributes() } } else { Text(model.plainLocation) - .font(.system(size: 13)) + .applyLocationAttributes() .foregroundColor(isHovered ? .white: Color(.defaultText)) } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift index 433498e2bf..3e28d2441f 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift @@ -91,7 +91,8 @@ public final class TunnelControllerViewModel: ObservableObject { connectionStatus = statusReporter.statusObserver.recentValue internalServerAddress = statusReporter.serverInfoObserver.recentValue.serverAddress - internalServerLocation = statusReporter.serverInfoObserver.recentValue.serverLocation?.serverLocation + internalServerAttributes = statusReporter.serverInfoObserver.recentValue.serverLocation + internalServerLocation = internalServerAttributes?.serverLocation // Particularly useful when unit testing with an initial status of our choosing. refreshInternalIsRunning() @@ -439,6 +440,13 @@ public final class TunnelControllerViewModel: ObservableObject { } } + @Published + private var internalServerAttributes: NetworkProtectionServerInfo.ServerAttributes? + + var emoji: String? { + locationFormatter.emoji(for: internalServerAttributes?.country) + } + var plainLocation: String { locationFormatter.string(from: internalServerLocation, preferredLocation: vpnSettings.selectedLocation) } @@ -448,7 +456,7 @@ public final class TunnelControllerViewModel: ObservableObject { locationFormatter.string(from: internalServerLocation, preferredLocation: vpnSettings.selectedLocation, locationTextColor: Color(.defaultText), - preferredLocationTextColor: Color(.gray)) + preferredLocationTextColor: Color(.defaultText).opacity(0.6)) } // MARK: - Toggling VPN From 556d36b522ebea870d15b23334c592f0f4399040 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Mon, 8 Apr 2024 20:17:49 -0400 Subject: [PATCH 03/32] Emoji & icon --- .../UserText+NetworkProtection.swift | 9 +++++ DuckDuckGo/Localizable.xcstrings | 2 +- .../DefaultVPNLocationFormatter.swift | 35 +++++++++++++------ ...tworkProtectionVPNCountryLabelsModel.swift | 9 +++-- .../NetworkProtectionAsset.swift | 1 + .../TunnelControllerView.swift | 3 ++ .../TunnelControllerViewModel.swift | 10 ++++-- .../NetworkProtectionAssetTests.swift | 1 + 8 files changed, 54 insertions(+), 16 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index 2a5ec04f2d..317a7c5178 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -330,6 +330,15 @@ extension UserText { static let uninstallVPNAlertTitle = "Are you sure you want to uninstall the VPN?" // "vpn.uninstall.alert.informative.text" - Informative text for the alert that comes up when the user decides to uninstall our VPN static let uninstallVPNInformativeText = "Uninstalling the DuckDuckGo VPN will disconnect the VPN and remove it from your device." + + // MARK: - VPN Screen + // "network.protection.vpn.location.nearest" - Description of the location type in the VPN status view + static let netPVPNLocationNearest = "(Nearest)" + + // "network.protection.vpn.location.subtitle.formatted.city.and.country" - Subtitle for the preferred location item that formats a city and country. E.g Chicago, United States + static func netPVPNSettingsLocationSubtitleFormattedCityAndCountry(city: String, country: String) -> String { + return "\(city), \(country)" + } } #if DBP diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 04f8d61b80..cf9456a461 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -52312,4 +52312,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift index 085b5f7748..ea53116293 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift @@ -21,11 +21,22 @@ import SwiftUI import NetworkProtection struct DefaultVPNLocationFormatter: VPNLocationFormatting { - func emoji(for country: String?) -> String? { - country.map { NetworkProtectionVPNCountryLabelsModel(country: $0).emoji } + func emoji(for country: String?, + preferredLocation someLocation: VPNSettings.SelectedLocation, + isConnected: Bool) -> String? { + let preferredLocation = VPNLocationModel(selectedLocation: someLocation) + + switch preferredLocation.icon { + case .defaultIcon: + guard isConnected, let country else { return nil } + return NetworkProtectionVPNCountryLabelsModel(country: country, useFullCountryName: true).emoji + case .emoji(let emoji): + return emoji + } } - func string(from location: String?, preferredLocation someLocation: VPNSettings.SelectedLocation) -> String { + func string(from location: String?, + preferredLocation someLocation: VPNSettings.SelectedLocation) -> String { let preferredLocation = VPNLocationModel(selectedLocation: someLocation) if let location { @@ -44,10 +55,10 @@ struct DefaultVPNLocationFormatter: VPNLocationFormatting { if let location { var attributedString = AttributedString( - preferredLocation.isNearest ? "\(location) (Nearest)" : location + preferredLocation.isNearest ? "\(location) \(UserText.netPVPNLocationNearest)" : location ) attributedString.foregroundColor = locationTextColor - if let range = attributedString.range(of: "(Nearest)") { + if let range = attributedString.range(of: UserText.netPVPNLocationNearest) { attributedString[range].foregroundColor = preferredLocationTextColor } return attributedString @@ -66,7 +77,6 @@ final class VPNLocationModel: ObservableObject { } let title: String - let subtitle: String? let icon: LocationIcon let isNearest: Bool @@ -74,13 +84,18 @@ final class VPNLocationModel: ObservableObject { switch selectedLocation { case .nearest: title = UserText.vpnLocationNearestAvailable - subtitle = UserText.vpnLocationNearestAvailableSubtitle icon = .defaultIcon isNearest = true case .location(let location): - let countryLabelsModel = NetworkProtectionVPNCountryLabelsModel(country: location.country) - title = countryLabelsModel.title - subtitle = selectedLocation.location?.city + let countryLabelsModel = NetworkProtectionVPNCountryLabelsModel(country: location.country, useFullCountryName: true) + if let city = location.city { + title = UserText.netPVPNSettingsLocationSubtitleFormattedCityAndCountry( + city: city, + country: countryLabelsModel.title + ) + } else { + title = countryLabelsModel.title + } icon = .emoji(countryLabelsModel.emoji) isNearest = false } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift index 28c6a16b19..00a3f034ea 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift @@ -23,8 +23,13 @@ struct NetworkProtectionVPNCountryLabelsModel { let emoji: String let title: String - init(country: String) { - self.title = Locale.current.localizedString(forRegionCode: country) ?? country.capitalized + init(country: String, useFullCountryName: Bool = true) { + if useFullCountryName { + self.title = Locale.current.localizedString(forRegionCode: country) ?? country.capitalized + } else { + self.title = country.localizedUppercase + } + self.emoji = Self.flag(country: country) } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift index a811f9bab1..6e02224576 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift @@ -24,6 +24,7 @@ public enum NetworkProtectionAsset: String, CaseIterable { case vpnDisabledImage = "VPNDisabled" case vpnEnabledImage = "VPN" case vpnIcon = "VPN-16" + case nearestAvailable = "VPNLocation" // Apple Icons case appleVaultIcon = "apple-vault-icon" diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift index 0c1cab3505..82882ddd34 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -246,6 +246,9 @@ public struct TunnelControllerView: View { .frame(width: 26, height: 26) .background(Color(hex: "B2B2B2").opacity(0.3)) .clipShape(Circle()) + } else { + Image(NetworkProtectionAsset.nearestAvailable) + .frame(width: 26, height: 26) } if #available(macOS 12, *) { if isHovered { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift index 3e28d2441f..b137f9fbee 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift @@ -149,7 +149,8 @@ public final class TunnelControllerViewModel: ObservableObject { Task { @MainActor in self.internalServerAddress = serverInfo.serverAddress - self.internalServerLocation = serverInfo.serverLocation?.serverLocation + self.internalServerAttributes = serverInfo.serverLocation + self.internalServerLocation = self.internalServerAttributes?.serverLocation } } .store(in: &cancellables) @@ -444,11 +445,14 @@ public final class TunnelControllerViewModel: ObservableObject { private var internalServerAttributes: NetworkProtectionServerInfo.ServerAttributes? var emoji: String? { - locationFormatter.emoji(for: internalServerAttributes?.country) + locationFormatter.emoji(for: internalServerAttributes?.country, + preferredLocation: vpnSettings.selectedLocation, + isConnected: isVPNEnabled) } var plainLocation: String { - locationFormatter.string(from: internalServerLocation, preferredLocation: vpnSettings.selectedLocation) + locationFormatter.string(from: internalServerLocation, + preferredLocation: vpnSettings.selectedLocation) } @available(macOS 12, *) diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift index c407babeec..83d08e3ceb 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift @@ -33,6 +33,7 @@ final class NetworkProtectionAssetTests: XCTestCase { .vpnDisabledImage: "VPNDisabled", .vpnEnabledImage: "VPN", .vpnIcon: "VPN-16", + .nearestAvailable: "VPNLocation", .appleVaultIcon: "apple-vault-icon", .appleVPNIcon: "apple-vpn-icon", .appleSystemSettingsIcon: "apple-system-settings-icon", From 87823f7e91c0424a009b06bfdc42e92268064c0c Mon Sep 17 00:00:00 2001 From: Anh Do Date: Mon, 8 Apr 2024 23:28:07 -0400 Subject: [PATCH 04/32] Simplify and add tests --- DuckDuckGo.xcodeproj/project.pbxproj | 20 +++++ .../DefaultVPNLocationFormatter.swift | 15 +--- .../TunnelControllerViewModel.swift | 4 +- .../MockVPNLocationFormatter.swift | 36 +++++++++ .../NetworkProtectionStatusBarMenuTests.swift | 2 + .../TunnelControllerViewModelTests.swift | 12 +++ .../DefaultVPNLocationFormatterTests.swift | 76 +++++++++++++++++++ .../Mocks/MockVPNLocationFormatter.swift | 37 +++++++++ 8 files changed, 187 insertions(+), 15 deletions(-) create mode 100644 LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/MockVPNLocationFormatter.swift create mode 100644 UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift create mode 100644 UnitTests/NetworkProtection/Mocks/MockVPNLocationFormatter.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index b4f8102558..1606f9340b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -3263,6 +3263,10 @@ BDA764802BC4998A00D0400C /* DefaultVPNLocationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */; }; BDA764842BC49E3F00D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */; }; BDA764852BC49E4000D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */; }; + BDA7648D2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */; }; + BDA7648E2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */; }; + BDA764912BC4E57200D0400C /* MockVPNLocationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA764902BC4E57200D0400C /* MockVPNLocationFormatter.swift */; }; + BDA764922BC4E57200D0400C /* MockVPNLocationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA764902BC4E57200D0400C /* MockVPNLocationFormatter.swift */; }; BDE981D82BBCE4C700645880 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = BDE981D72BBCE4C700645880 /* Lottie */; }; BDE981D92BBD10D600645880 /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */; }; BDE981DA2BBD10D600645880 /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; @@ -4785,6 +4789,8 @@ BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "vpn-dark-mode.json"; sourceTree = ""; }; BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultVPNLocationFormatter.swift; sourceTree = ""; }; BDA764812BC499D200D0400C /* BrowserServicesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = BrowserServicesKit; path = ../BrowserServicesKit; sourceTree = ""; }; + BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultVPNLocationFormatterTests.swift; sourceTree = ""; }; + BDA764902BC4E57200D0400C /* MockVPNLocationFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockVPNLocationFormatter.swift; sourceTree = ""; }; C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureTextField.swift; sourceTree = ""; }; C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionExecutor.swift; sourceTree = ""; }; C13909F32B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillDeleteAllPasswordsExecutorTests.swift; sourceTree = ""; }; @@ -6475,10 +6481,12 @@ 4BCF15E32ABB987F0083F6DF /* NetworkProtection */ = { isa = PBXGroup; children = ( + BDA7648F2BC4E56200D0400C /* Mocks */, 4BCF15E62ABB98A20083F6DF /* Resources */, 4BCF15E42ABB98990083F6DF /* NetworkProtectionRemoteMessageTests.swift */, 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */, 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */, + BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */, ); path = NetworkProtection; sourceTree = ""; @@ -8781,6 +8789,14 @@ path = View; sourceTree = ""; }; + BDA7648F2BC4E56200D0400C /* Mocks */ = { + isa = PBXGroup; + children = ( + BDA764902BC4E57200D0400C /* MockVPNLocationFormatter.swift */, + ); + path = Mocks; + sourceTree = ""; + }; BDE981DB2BBD110800645880 /* Assets */ = { isa = PBXGroup; children = ( @@ -11269,6 +11285,7 @@ 1DE03425298BC7F000CAB3D7 /* InternalUserDeciderStoreMock.swift in Sources */, 3706FE4A293F661700E42796 /* BookmarkManagedObjectTests.swift in Sources */, EEC8EB402982CD550065AA39 /* JSAlertViewModelTests.swift in Sources */, + BDA764922BC4E57200D0400C /* MockVPNLocationFormatter.swift in Sources */, 3706FE4B293F661700E42796 /* BookmarksHTMLImporterTests.swift in Sources */, 9FA75A3F2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */, 56D145E929E6BB6300E3488A /* CapturingDataImportProvider.swift in Sources */, @@ -11364,6 +11381,7 @@ B603975229C1FFAD00902A34 /* ExpectedNavigationExtension.swift in Sources */, 3706FE85293F661700E42796 /* BWRequestTests.swift in Sources */, 3706FE86293F661700E42796 /* FileDownloadManagerTests.swift in Sources */, + BDA7648E2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -13295,6 +13313,7 @@ B68412202B6A30680092F66A /* StringExtensionTests.swift in Sources */, B662D3D92755D7AD0035D4D6 /* PixelStoreTests.swift in Sources */, B6106BB526A809E60013B453 /* GeolocationProviderTests.swift in Sources */, + BDA764912BC4E57200D0400C /* MockVPNLocationFormatter.swift in Sources */, B6E6BA232BA2EDDE008AA7E1 /* FileReadResult.swift in Sources */, B6A5A2A025B96E8300AA7ADA /* AppStateChangePublisherTests.swift in Sources */, B63ED0E326B3E7FA00A9DAD1 /* CLLocationManagerMock.swift in Sources */, @@ -13373,6 +13392,7 @@ 373A1AB228451ED400586521 /* BookmarksHTMLImporterTests.swift in Sources */, 4B723E0626B0003E00E14D75 /* CSVParserTests.swift in Sources */, B60C6F8429B1BAD3007BFAA8 /* FileManagerTempDirReplacement.swift in Sources */, + BDA7648D2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift in Sources */, 85F487B5276A8F2E003CE668 /* OnboardingTests.swift in Sources */, B626A7642992506A00053070 /* SerpHeadersNavigationResponderTests.swift in Sources */, AA652CCE25DD9071009059CC /* BookmarkListTests.swift in Sources */, diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift index ea53116293..b612250034 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift @@ -21,18 +21,9 @@ import SwiftUI import NetworkProtection struct DefaultVPNLocationFormatter: VPNLocationFormatting { - func emoji(for country: String?, - preferredLocation someLocation: VPNSettings.SelectedLocation, - isConnected: Bool) -> String? { - let preferredLocation = VPNLocationModel(selectedLocation: someLocation) - - switch preferredLocation.icon { - case .defaultIcon: - guard isConnected, let country else { return nil } - return NetworkProtectionVPNCountryLabelsModel(country: country, useFullCountryName: true).emoji - case .emoji(let emoji): - return emoji - } + func emoji(for country: String?) -> String? { + guard let country else { return nil } + return NetworkProtectionVPNCountryLabelsModel(country: country, useFullCountryName: true).emoji } func string(from location: String?, diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift index b137f9fbee..0d786c6388 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift @@ -445,9 +445,7 @@ public final class TunnelControllerViewModel: ObservableObject { private var internalServerAttributes: NetworkProtectionServerInfo.ServerAttributes? var emoji: String? { - locationFormatter.emoji(for: internalServerAttributes?.country, - preferredLocation: vpnSettings.selectedLocation, - isConnected: isVPNEnabled) + locationFormatter.emoji(for: internalServerAttributes?.country) } var plainLocation: String { diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/MockVPNLocationFormatter.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/MockVPNLocationFormatter.swift new file mode 100644 index 0000000000..90fdcd7f23 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/MockVPNLocationFormatter.swift @@ -0,0 +1,36 @@ +// +// MockVPNLocationFormatter.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI +import NetworkProtection + +struct MockVPNLocationFormatter: VPNLocationFormatting { + func emoji(for country: String?) -> String? { + nil + } + + func string(from location: String?, preferredLocation: NetworkProtection.VPNSettings.SelectedLocation) -> String { + "" + } + + @available(macOS 12, *) + func string(from location: String?, preferredLocation: NetworkProtection.VPNSettings.SelectedLocation, locationTextColor: Color, preferredLocationTextColor: Color) -> AttributedString { + "" + } +} diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift index 5b9e7e0dea..967f92c995 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift @@ -58,6 +58,7 @@ final class StatusBarMenuTests: XCTestCase { agentLoginItem: nil, isMenuBarStatusView: false, userDefaults: .standard, + locationFormatter: MockVPNLocationFormatter(), uninstallHandler: { }) menu.show() @@ -83,6 +84,7 @@ final class StatusBarMenuTests: XCTestCase { agentLoginItem: nil, isMenuBarStatusView: false, userDefaults: .standard, + locationFormatter: MockVPNLocationFormatter(), uninstallHandler: { }) menu.hide() diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift index 83caf2cff6..f82c131b42 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift @@ -104,6 +104,8 @@ final class TunnelControllerViewModelTests: XCTestCase { controller: controller, onboardingStatusPublisher: Just(OnboardingStatus.completed).eraseToAnyPublisher(), statusReporter: statusReporter, + vpnSettings: .init(defaults: .standard), + locationFormatter: MockVPNLocationFormatter(), appLauncher: MockAppLauncher()) let isToggleOn = model.isToggleOn.wrappedValue @@ -124,6 +126,8 @@ final class TunnelControllerViewModelTests: XCTestCase { controller: controller, onboardingStatusPublisher: Just(OnboardingStatus.completed).eraseToAnyPublisher(), statusReporter: statusReporter, + vpnSettings: .init(defaults: .standard), + locationFormatter: MockVPNLocationFormatter(), appLauncher: MockAppLauncher()) XCTAssertEqual(model.connectionStatusDescription, UserText.networkProtectionStatusDisconnecting) @@ -151,6 +155,8 @@ final class TunnelControllerViewModelTests: XCTestCase { controller: controller, onboardingStatusPublisher: Just(OnboardingStatus.completed).eraseToAnyPublisher(), statusReporter: statusReporter, + vpnSettings: .init(defaults: .standard), + locationFormatter: MockVPNLocationFormatter(), appLauncher: MockAppLauncher()) let isToggleOn = model.isToggleOn.wrappedValue @@ -173,6 +179,8 @@ final class TunnelControllerViewModelTests: XCTestCase { controller: controller, onboardingStatusPublisher: Just(OnboardingStatus.completed).eraseToAnyPublisher(), statusReporter: statusReporter, + vpnSettings: .init(defaults: .standard), + locationFormatter: MockVPNLocationFormatter(), appLauncher: MockAppLauncher()) XCTAssertEqual(model.connectionStatusDescription, UserText.networkProtectionStatusConnecting) @@ -191,6 +199,8 @@ final class TunnelControllerViewModelTests: XCTestCase { controller: controller, onboardingStatusPublisher: Just(OnboardingStatus.completed).eraseToAnyPublisher(), statusReporter: statusReporter, + vpnSettings: .init(defaults: .standard), + locationFormatter: MockVPNLocationFormatter(), appLauncher: MockAppLauncher()) let networkProtectionWasStarted = expectation(description: "The model started the VPN when appropriate") @@ -221,6 +231,8 @@ final class TunnelControllerViewModelTests: XCTestCase { controller: controller, onboardingStatusPublisher: Just(OnboardingStatus.completed).eraseToAnyPublisher(), statusReporter: statusReporter, + vpnSettings: .init(defaults: .standard), + locationFormatter: MockVPNLocationFormatter(), appLauncher: MockAppLauncher()) let networkProtectionWasStopped = expectation(description: "The model stopped the VPN when appropriate") diff --git a/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift b/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift new file mode 100644 index 0000000000..891bd73a5b --- /dev/null +++ b/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift @@ -0,0 +1,76 @@ +// +// DefaultVPNLocationFormatterTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest +@testable import DuckDuckGo_Privacy_Browser +@testable import NetworkProtection + +final class DefaultVPNLocationFormatterTests: XCTestCase { + private var formatter: DefaultVPNLocationFormatter! + + override func setUp() { + formatter = DefaultVPNLocationFormatter() + } + + func testUSLocation() { + let server = NetworkProtectionServerInfo.ServerAttributes(city: "Lafayette", country: "us", state: "la", timezoneOffset: 0) + let preferredLocation = VPNSettings.SelectedLocation.location(.init(country: "us")) + + XCTAssertNil(formatter.emoji(for: nil)) + XCTAssertEqual(formatter.emoji(for: server.country), "🇺🇸") + + XCTAssertEqual(formatter.string(from: nil, preferredLocation: .nearest), "Nearest available") + XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: .nearest), "Lafayette, LA (Nearest)") + XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: preferredLocation), "Lafayette, LA") + + if #available(macOS 12, *) { + XCTAssertEqual(NSAttributedString(formatter.string(from: server.serverLocation, + preferredLocation: .nearest, + locationTextColor: .black, + preferredLocationTextColor: .black)).string, "Lafayette, LA (Nearest)") + XCTAssertEqual(NSAttributedString(formatter.string(from: server.serverLocation, + preferredLocation: preferredLocation, + locationTextColor: .black, + preferredLocationTextColor: .black)).string, "Lafayette, LA") + } + } + + func testCALocation() { + let server = NetworkProtectionServerInfo.ServerAttributes(city: "Toronto", country: "ca", state: "on", timezoneOffset: 0) + let preferredLocation = VPNSettings.SelectedLocation.location(.init(country: "ca")) + + XCTAssertNil(formatter.emoji(for: nil)) + XCTAssertEqual(formatter.emoji(for: server.country), "🇨🇦") + + XCTAssertEqual(formatter.string(from: nil, preferredLocation: .nearest), "Nearest available") + XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: .nearest), "Toronto, Canada (Nearest)") + XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: preferredLocation), "Toronto, Canada") + + if #available(macOS 12, *) { + XCTAssertEqual(NSAttributedString(formatter.string(from: server.serverLocation, + preferredLocation: .nearest, + locationTextColor: .black, + preferredLocationTextColor: .black)).string, "Toronto, Canada (Nearest)") + XCTAssertEqual(NSAttributedString(formatter.string(from: server.serverLocation, + preferredLocation: preferredLocation, + locationTextColor: .black, + preferredLocationTextColor: .black)).string, "Toronto, Canada") + } + } +} diff --git a/UnitTests/NetworkProtection/Mocks/MockVPNLocationFormatter.swift b/UnitTests/NetworkProtection/Mocks/MockVPNLocationFormatter.swift new file mode 100644 index 0000000000..1a75c8845c --- /dev/null +++ b/UnitTests/NetworkProtection/Mocks/MockVPNLocationFormatter.swift @@ -0,0 +1,37 @@ +// +// MockVPNLocationFormatter.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI +import NetworkProtection +@testable import DuckDuckGo_Privacy_Browser + +struct MockVPNLocationFormatter: VPNLocationFormatting { + func emoji(for country: String?) -> String? { + nil + } + + func string(from location: String?, preferredLocation: NetworkProtection.VPNSettings.SelectedLocation) -> String { + "" + } + + @available(macOS 12, *) + func string(from location: String?, preferredLocation: NetworkProtection.VPNSettings.SelectedLocation, locationTextColor: Color, preferredLocationTextColor: Color) -> AttributedString { + "" + } +} From c042a30d6ef1d8a8149061f47d928e6e0b63632e Mon Sep 17 00:00:00 2001 From: Anh Do Date: Tue, 9 Apr 2024 00:18:11 -0400 Subject: [PATCH 05/32] Fix emoji --- .../DefaultVPNLocationFormatter.swift | 16 +++++++++++--- .../TunnelControllerView.swift | 2 +- .../TunnelControllerViewModel.swift | 8 ++++++- .../MockVPNLocationFormatter.swift | 2 +- .../DefaultVPNLocationFormatterTests.swift | 22 +++++++++++++++---- .../Mocks/MockVPNLocationFormatter.swift | 2 +- 6 files changed, 41 insertions(+), 11 deletions(-) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift index b612250034..5f2bb50530 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/DefaultVPNLocationFormatter.swift @@ -21,9 +21,19 @@ import SwiftUI import NetworkProtection struct DefaultVPNLocationFormatter: VPNLocationFormatting { - func emoji(for country: String?) -> String? { - guard let country else { return nil } - return NetworkProtectionVPNCountryLabelsModel(country: country, useFullCountryName: true).emoji + func emoji(for country: String?, + preferredLocation someLocation: VPNSettings.SelectedLocation) -> String? { + if let country { + return NetworkProtectionVPNCountryLabelsModel(country: country, useFullCountryName: true).emoji + } + + let preferredLocation = VPNLocationModel(selectedLocation: someLocation) + switch preferredLocation.icon { + case .defaultIcon: + return nil + case .emoji(let emoji): + return emoji + } } func string(from location: String?, diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift index 82882ddd34..dbb124ee80 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -246,7 +246,7 @@ public struct TunnelControllerView: View { .frame(width: 26, height: 26) .background(Color(hex: "B2B2B2").opacity(0.3)) .clipShape(Circle()) - } else { + } else if model.wantsNearestLocation { Image(NetworkProtectionAsset.nearestAvailable) .frame(width: 26, height: 26) } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift index 0d786c6388..46d13e6f6e 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift @@ -444,8 +444,14 @@ public final class TunnelControllerViewModel: ObservableObject { @Published private var internalServerAttributes: NetworkProtectionServerInfo.ServerAttributes? + var wantsNearestLocation: Bool { + guard case .nearest = vpnSettings.selectedLocation else { return false } + return true + } + var emoji: String? { - locationFormatter.emoji(for: internalServerAttributes?.country) + locationFormatter.emoji(for: internalServerAttributes?.country, + preferredLocation: vpnSettings.selectedLocation) } var plainLocation: String { diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/MockVPNLocationFormatter.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/MockVPNLocationFormatter.swift index 90fdcd7f23..05abc170a5 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/MockVPNLocationFormatter.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/MockVPNLocationFormatter.swift @@ -21,7 +21,7 @@ import SwiftUI import NetworkProtection struct MockVPNLocationFormatter: VPNLocationFormatting { - func emoji(for country: String?) -> String? { + func emoji(for country: String?, preferredLocation someLocation: VPNSettings.SelectedLocation) -> String? { nil } diff --git a/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift b/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift index 891bd73a5b..d86dbbd158 100644 --- a/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift +++ b/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift @@ -31,13 +31,20 @@ final class DefaultVPNLocationFormatterTests: XCTestCase { func testUSLocation() { let server = NetworkProtectionServerInfo.ServerAttributes(city: "Lafayette", country: "us", state: "la", timezoneOffset: 0) let preferredLocation = VPNSettings.SelectedLocation.location(.init(country: "us")) + let otherPreferredLocation = VPNSettings.SelectedLocation.location(.init(country: "gb")) - XCTAssertNil(formatter.emoji(for: nil)) - XCTAssertEqual(formatter.emoji(for: server.country), "🇺🇸") + XCTAssertNil(formatter.emoji(for: nil, preferredLocation: .nearest)) + XCTAssertEqual(formatter.emoji(for: nil, preferredLocation: preferredLocation), "🇺🇸") + XCTAssertEqual(formatter.emoji(for: nil, preferredLocation: otherPreferredLocation), "🇬🇧") + XCTAssertEqual(formatter.emoji(for: server.country, preferredLocation: preferredLocation), "🇺🇸") + XCTAssertEqual(formatter.emoji(for: server.country, preferredLocation: otherPreferredLocation), "🇺🇸") XCTAssertEqual(formatter.string(from: nil, preferredLocation: .nearest), "Nearest available") + XCTAssertEqual(formatter.string(from: nil, preferredLocation: preferredLocation), "United States") + XCTAssertEqual(formatter.string(from: nil, preferredLocation: otherPreferredLocation), "United Kingdom") XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: .nearest), "Lafayette, LA (Nearest)") XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: preferredLocation), "Lafayette, LA") + XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: otherPreferredLocation), "Lafayette, LA") if #available(macOS 12, *) { XCTAssertEqual(NSAttributedString(formatter.string(from: server.serverLocation, @@ -54,13 +61,20 @@ final class DefaultVPNLocationFormatterTests: XCTestCase { func testCALocation() { let server = NetworkProtectionServerInfo.ServerAttributes(city: "Toronto", country: "ca", state: "on", timezoneOffset: 0) let preferredLocation = VPNSettings.SelectedLocation.location(.init(country: "ca")) + let otherPreferredLocation = VPNSettings.SelectedLocation.location(.init(country: "gb")) - XCTAssertNil(formatter.emoji(for: nil)) - XCTAssertEqual(formatter.emoji(for: server.country), "🇨🇦") + XCTAssertNil(formatter.emoji(for: nil, preferredLocation: .nearest)) + XCTAssertEqual(formatter.emoji(for: nil, preferredLocation: preferredLocation), "🇨🇦") + XCTAssertEqual(formatter.emoji(for: nil, preferredLocation: otherPreferredLocation), "🇬🇧") + XCTAssertEqual(formatter.emoji(for: server.country, preferredLocation: preferredLocation), "🇨🇦") + XCTAssertEqual(formatter.emoji(for: server.country, preferredLocation: otherPreferredLocation), "🇨🇦") XCTAssertEqual(formatter.string(from: nil, preferredLocation: .nearest), "Nearest available") + XCTAssertEqual(formatter.string(from: nil, preferredLocation: preferredLocation), "Canada") + XCTAssertEqual(formatter.string(from: nil, preferredLocation: otherPreferredLocation), "United Kingdom") XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: .nearest), "Toronto, Canada (Nearest)") XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: preferredLocation), "Toronto, Canada") + XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: otherPreferredLocation), "Toronto, Canada") if #available(macOS 12, *) { XCTAssertEqual(NSAttributedString(formatter.string(from: server.serverLocation, diff --git a/UnitTests/NetworkProtection/Mocks/MockVPNLocationFormatter.swift b/UnitTests/NetworkProtection/Mocks/MockVPNLocationFormatter.swift index 1a75c8845c..579ba6e4ff 100644 --- a/UnitTests/NetworkProtection/Mocks/MockVPNLocationFormatter.swift +++ b/UnitTests/NetworkProtection/Mocks/MockVPNLocationFormatter.swift @@ -22,7 +22,7 @@ import NetworkProtection @testable import DuckDuckGo_Privacy_Browser struct MockVPNLocationFormatter: VPNLocationFormatting { - func emoji(for country: String?) -> String? { + func emoji(for country: String?, preferredLocation someLocation: VPNSettings.SelectedLocation) -> String? { nil } From 8a6e697b50469baf6e3d76031775fa1cceb2d93e Mon Sep 17 00:00:00 2001 From: Anh Do Date: Tue, 9 Apr 2024 00:24:23 -0400 Subject: [PATCH 06/32] Fix newlines --- .../Views/TunnelControllerView/TunnelControllerView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift index dbb124ee80..96244b1539 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -87,7 +87,8 @@ private enum Opacity { } fileprivate extension View { - func applyConnectionStatusDetailAttributes(colorScheme: ColorScheme) -> some View { opacity(Opacity.connectionStatusDetail(colorScheme: colorScheme)) + func applyConnectionStatusDetailAttributes(colorScheme: ColorScheme) -> some View { + opacity(Opacity.connectionStatusDetail(colorScheme: colorScheme)) .font(.NetworkProtection.connectionStatusDetail) .foregroundColor(Color(.defaultText)) } From 764bee617b533c1727f72f0532fcdffcbf5a7291 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Tue, 9 Apr 2024 11:48:24 -0400 Subject: [PATCH 07/32] Remove local package --- DuckDuckGo.xcodeproj/project.pbxproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 1606f9340b..fedba867eb 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -4788,7 +4788,6 @@ BD384AC72BBC821100EF3735 /* vpn-light-mode.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "vpn-light-mode.json"; sourceTree = ""; }; BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "vpn-dark-mode.json"; sourceTree = ""; }; BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultVPNLocationFormatter.swift; sourceTree = ""; }; - BDA764812BC499D200D0400C /* BrowserServicesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = BrowserServicesKit; path = ../BrowserServicesKit; sourceTree = ""; }; BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultVPNLocationFormatterTests.swift; sourceTree = ""; }; BDA764902BC4E57200D0400C /* MockVPNLocationFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockVPNLocationFormatter.swift; sourceTree = ""; }; C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureTextField.swift; sourceTree = ""; }; @@ -7220,7 +7219,6 @@ AA585D75248FD31100E9A3E2 = { isa = PBXGroup; children = ( - BDA764812BC499D200D0400C /* BrowserServicesKit */, 378B5886295CF2A4002C0CC0 /* Configuration */, 378E279C2970217400FCADA2 /* LocalPackages */, 7BB108552A43375D000AB95F /* LocalThirdParty */, From bde56e4b19fb91f00cead79bae8833481a563105 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Tue, 9 Apr 2024 11:54:25 -0400 Subject: [PATCH 08/32] Fix swiftlint --- .../Views/TunnelControllerView/TunnelControllerViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift index 46d13e6f6e..dfd96cd277 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift @@ -450,7 +450,7 @@ public final class TunnelControllerViewModel: ObservableObject { } var emoji: String? { - locationFormatter.emoji(for: internalServerAttributes?.country, + locationFormatter.emoji(for: internalServerAttributes?.country, preferredLocation: vpnSettings.selectedLocation) } From dbf27d9c7916f92128f2c0b05d318df6a382b993 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Tue, 9 Apr 2024 22:56:40 -0400 Subject: [PATCH 09/32] Update BSK --- .../DefaultVPNLocationFormatterTests.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift b/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift index d86dbbd158..0c52440181 100644 --- a/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift +++ b/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift @@ -42,19 +42,19 @@ final class DefaultVPNLocationFormatterTests: XCTestCase { XCTAssertEqual(formatter.string(from: nil, preferredLocation: .nearest), "Nearest available") XCTAssertEqual(formatter.string(from: nil, preferredLocation: preferredLocation), "United States") XCTAssertEqual(formatter.string(from: nil, preferredLocation: otherPreferredLocation), "United Kingdom") - XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: .nearest), "Lafayette, LA (Nearest)") - XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: preferredLocation), "Lafayette, LA") - XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: otherPreferredLocation), "Lafayette, LA") + XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: .nearest), "Lafayette, United States (Nearest)") + XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: preferredLocation), "Lafayette, United States") + XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: otherPreferredLocation), "Lafayette, United States") if #available(macOS 12, *) { XCTAssertEqual(NSAttributedString(formatter.string(from: server.serverLocation, preferredLocation: .nearest, locationTextColor: .black, - preferredLocationTextColor: .black)).string, "Lafayette, LA (Nearest)") + preferredLocationTextColor: .black)).string, "Lafayette, United States (Nearest)") XCTAssertEqual(NSAttributedString(formatter.string(from: server.serverLocation, preferredLocation: preferredLocation, locationTextColor: .black, - preferredLocationTextColor: .black)).string, "Lafayette, LA") + preferredLocationTextColor: .black)).string, "Lafayette, United States") } } From ee8b174bf71a993e6d23a723adaca467f2d6ef94 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Fri, 12 Apr 2024 00:10:15 -0400 Subject: [PATCH 10/32] Update BSK --- DuckDuckGo.xcodeproj/project.pbxproj | 4 ++-- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index fedba867eb..de7412bbba 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14568,8 +14568,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 133.1.0; + branch = "anh/netp/location-selection"; + kind = branch; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 517700a5d4..c07ef413be 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "4699a5ff3d0669736e87f6da808884f245d80ede", - "version" : "133.1.0" + "branch" : "anh/netp/location-selection", + "revision" : "dc6725de9e7019ef7e2b0bb7438b9d98010808f8" } }, { From 90a199bd65b09f49e646bf21d5dd36817e73967c Mon Sep 17 00:00:00 2001 From: Anh Do Date: Tue, 9 Apr 2024 22:56:40 -0400 Subject: [PATCH 11/32] Update BSK --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c07ef413be..8c8bedd1f1 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "branch" : "anh/netp/location-selection", - "revision" : "dc6725de9e7019ef7e2b0bb7438b9d98010808f8" + "branch" : "anh/netp/screen-improvements", + "revision" : "a73f48e3a349f298c481135a2e03ddda34f0d0aa" } }, { From bd72d61730c65b52ee92c6ca3ef99f883a157296 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Thu, 11 Apr 2024 23:24:31 -0400 Subject: [PATCH 12/32] Update BSK --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8c8bedd1f1..4119925f4a 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,7 +33,7 @@ "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { "branch" : "anh/netp/screen-improvements", - "revision" : "a73f48e3a349f298c481135a2e03ddda34f0d0aa" + "revision" : "de38f0b78b2fd2dbbdd33cec48d2b1643fcf9d4a" } }, { From b014b12b07e0212eaf6affe8e325ab67281d6c68 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Tue, 9 Apr 2024 11:50:11 -0400 Subject: [PATCH 13/32] Update BSK --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4119925f4a..9a48391db4 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,7 +33,7 @@ "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { "branch" : "anh/netp/screen-improvements", - "revision" : "de38f0b78b2fd2dbbdd33cec48d2b1643fcf9d4a" + "revision" : "4a82b066114d7e6beed403e3345b4f58c26f4994" } }, { From 2d2dbb139149e44eb3d6bb0cac189d3f48bade0e Mon Sep 17 00:00:00 2001 From: Anh Do Date: Wed, 10 Apr 2024 14:13:47 -0400 Subject: [PATCH 14/32] Monitor data volume --- .../MainWindow/MainViewController.swift | 3 +- .../View/NetPPopoverManagerMock.swift | 6 +++ ...etworkProtectionNavBarPopoverManager.swift | 5 ++- .../VPNMetadataCollector.swift | 3 +- DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 8 +++- .../TunnelControllerIPCService.swift | 34 ++++++++++++++++ .../DataVolumeObserverThroughIPC.swift | 40 +++++++++++++++++++ .../TunnelControllerIPCClient.swift | 20 +++++++++- .../TunnelControllerIPCServer.swift | 14 +++++++ 9 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/Observers/DataVolumeObserverThroughIPC.swift diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 04b5810087..f2b57e1c54 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -93,7 +93,8 @@ final class MainViewController: NSViewController { serverInfoObserver: ipcClient.ipcServerInfoObserver, connectionErrorObserver: ipcClient.ipcConnectionErrorObserver, connectivityIssuesObserver: connectivityIssuesObserver, - controllerErrorMessageObserver: controllerErrorMessageObserver + controllerErrorMessageObserver: controllerErrorMessageObserver, + dataVolumeObserver: ipcClient.ipcDataVolumeObserver ) }() diff --git a/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift b/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift index 0af23bdcd5..15d7a146f4 100644 --- a/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift +++ b/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift @@ -62,6 +62,12 @@ final class IPCClientMock: NetworkProtectionIPCClient { var recentValue: String? } var ipcControllerErrorMessageObserver: any NetworkProtection.ControllerErrorMesssageObserver = ControllerErrorMesssageObserverMock() + + final class DataVolumeObserverMock: NetworkProtection.DataVolumeObserver { + var publisher: AnyPublisher = PassthroughSubject().eraseToAnyPublisher() + var recentValue: DataVolume = .init() + } + var ipcDataVolumeObserver: any NetworkProtection.DataVolumeObserver = DataVolumeObserverMock() func start() {} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index fcf40ff007..27bacb11ad 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -32,6 +32,7 @@ protocol NetworkProtectionIPCClient { var ipcStatusObserver: ConnectionStatusObserver { get } var ipcServerInfoObserver: ConnectionServerInfoObserver { get } var ipcConnectionErrorObserver: ConnectionErrorObserver { get } + var ipcDataVolumeObserver: DataVolumeObserver { get } func start() func stop() @@ -41,6 +42,7 @@ extension TunnelControllerIPCClient: NetworkProtectionIPCClient { public var ipcStatusObserver: any NetworkProtection.ConnectionStatusObserver { connectionStatusObserver } public var ipcServerInfoObserver: any NetworkProtection.ConnectionServerInfoObserver { serverInfoObserver } public var ipcConnectionErrorObserver: any NetworkProtection.ConnectionErrorObserver { connectionErrorObserver } + public var ipcDataVolumeObserver: any NetworkProtection.DataVolumeObserver { dataVolumeObserver } } final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { @@ -69,7 +71,8 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { serverInfoObserver: ipcClient.ipcServerInfoObserver, connectionErrorObserver: ipcClient.ipcConnectionErrorObserver, connectivityIssuesObserver: ConnectivityIssueObserverThroughDistributedNotifications(), - controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications() + controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications(), + dataVolumeObserver: ipcClient.ipcDataVolumeObserver ) let onboardingStatusPublisher = UserDefaults.netP.networkProtectionOnboardingStatusPublisher diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index c24d64bf6e..b4e463df17 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -121,7 +121,8 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { serverInfoObserver: ipcClient.serverInfoObserver, connectionErrorObserver: ipcClient.connectionErrorObserver, connectivityIssuesObserver: ConnectivityIssueObserverThroughDistributedNotifications(), - controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications() + controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications(), + dataVolumeObserver: ipcClient.dataVolumeObserver ) // Force refresh just in case. A refresh is requested when the IPC client is created, but distributed notifications don't guarantee delivery diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index 124ce85aec..6dea291314 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -187,12 +187,18 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { platformNotificationCenter: NSWorkspace.shared.notificationCenter, platformDidWakeNotification: NSWorkspace.didWakeNotification) + let dataVolumeObserver = DataVolumeObserverThroughSession( + tunnelSessionProvider: tunnelController, + platformNotificationCenter: NSWorkspace.shared.notificationCenter, + platformDidWakeNotification: NSWorkspace.didWakeNotification) + return DefaultNetworkProtectionStatusReporter( statusObserver: statusObserver, serverInfoObserver: serverInfoObserver, connectionErrorObserver: errorObserver, connectivityIssuesObserver: DisabledConnectivityIssueObserver(), - controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications() + controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications(), + dataVolumeObserver: dataVolumeObserver ) }() diff --git a/DuckDuckGoVPN/TunnelControllerIPCService.swift b/DuckDuckGoVPN/TunnelControllerIPCService.swift index c8a9ce456d..2888f14218 100644 --- a/DuckDuckGoVPN/TunnelControllerIPCService.swift +++ b/DuckDuckGoVPN/TunnelControllerIPCService.swift @@ -33,6 +33,7 @@ final class TunnelControllerIPCService { private let networkExtensionController: NetworkExtensionController private let server: NetworkProtectionIPC.TunnelControllerIPCServer private let statusReporter: NetworkProtectionStatusReporter + private lazy var dataVolumeMonitor = NetworkProtectionDataVolumeMonitor() private var cancellables = Set() private let defaults: UserDefaults @@ -49,6 +50,7 @@ final class TunnelControllerIPCService { subscribeToErrorChanges() subscribeToStatusUpdates() + subscribeToDataVolumeUpdates() subscribeToServerChanges() server.serverDelegate = self @@ -84,6 +86,38 @@ final class TunnelControllerIPCService { } .store(in: &cancellables) } + + private func subscribeToDataVolumeUpdates() { + statusReporter.statusObserver.publisher + .subscribe(on: DispatchQueue.main) + .map { connectionStatus in + guard case .connected = connectionStatus else { return false } + return true + } + .sink { isConnected in + Task { [weak self] in + guard let self else { return } + if !isConnected { + await self.dataVolumeMonitor.stop() + } else { + await self.startDataVolumeMonitor() + } + } + } + .store(in: &cancellables) + } + + private func startDataVolumeMonitor() async { + guard await !dataVolumeMonitor.isStarted else { return } + await dataVolumeMonitor.start(with: tunnelController) { [weak self] result in + switch result { + case .failure: + break + case .success(let dataVolume): + self?.server.dataVolumeUpdated(dataVolume) + } + } + } } // MARK: - Requests from the client diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/Observers/DataVolumeObserverThroughIPC.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/Observers/DataVolumeObserverThroughIPC.swift new file mode 100644 index 0000000000..ee605da45a --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/Observers/DataVolumeObserverThroughIPC.swift @@ -0,0 +1,40 @@ +// +// DataVolumeObserverThroughIPC.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation +import NetworkProtection + +public final class DataVolumeObserverThroughIPC: DataVolumeObserver { + + private let subject = CurrentValueSubject(.init()) + + // MARK: - DataVolumeObserver + + public lazy var publisher = subject.eraseToAnyPublisher() + + public var recentValue: DataVolume { + subject.value + } + + // MARK: - Publishing Updates + + func publish(_ dataVolume: DataVolume) { + subject.send(dataVolume) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift index 769939f65e..695218ab96 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift @@ -26,6 +26,7 @@ public protocol IPCClientInterface: AnyObject { func errorChanged(_ error: String?) func serverInfoChanged(_ serverInfo: NetworkProtectionStatusServerInfo) func statusChanged(_ status: ConnectionStatus) + func dataVolumeUpdated(_ dataVolume: DataVolume) } /// This is the XPC interface with parameters that can be packed properly @@ -34,6 +35,7 @@ protocol XPCClientInterface { func errorChanged(error: String?) func serverInfoChanged(payload: Data) func statusChanged(payload: Data) + func dataVolumeUpdated(payload: Data) } public final class TunnelControllerIPCClient { @@ -47,6 +49,7 @@ public final class TunnelControllerIPCClient { public var serverInfoObserver = ConnectionServerInfoObserverThroughIPC() public var connectionErrorObserver = ConnectionErrorObserverThroughIPC() public var connectionStatusObserver = ConnectionStatusObserverThroughIPC() + public var dataVolumeObserver = DataVolumeObserverThroughIPC() /// The delegate. /// @@ -65,7 +68,8 @@ public final class TunnelControllerIPCClient { clientDelegate: self.clientDelegate, serverInfoObserver: self.serverInfoObserver, connectionErrorObserver: self.connectionErrorObserver, - connectionStatusObserver: self.connectionStatusObserver + connectionStatusObserver: self.connectionStatusObserver, + dataVolumeObserver: self.dataVolumeObserver ) xpc = XPCClient( @@ -97,15 +101,18 @@ private final class TunnelControllerXPCClientDelegate: XPCClientInterface { let serverInfoObserver: ConnectionServerInfoObserverThroughIPC let connectionErrorObserver: ConnectionErrorObserverThroughIPC let connectionStatusObserver: ConnectionStatusObserverThroughIPC + let dataVolumeObserver: DataVolumeObserverThroughIPC init(clientDelegate: IPCClientInterface?, serverInfoObserver: ConnectionServerInfoObserverThroughIPC, connectionErrorObserver: ConnectionErrorObserverThroughIPC, - connectionStatusObserver: ConnectionStatusObserverThroughIPC) { + connectionStatusObserver: ConnectionStatusObserverThroughIPC, + dataVolumeObserver: DataVolumeObserverThroughIPC) { self.clientDelegate = clientDelegate self.serverInfoObserver = serverInfoObserver self.connectionErrorObserver = connectionErrorObserver self.connectionStatusObserver = connectionStatusObserver + self.dataVolumeObserver = dataVolumeObserver } func errorChanged(error: String?) { @@ -130,6 +137,15 @@ private final class TunnelControllerXPCClientDelegate: XPCClientInterface { connectionStatusObserver.publish(status) clientDelegate?.statusChanged(status) } + + func dataVolumeUpdated(payload: Data) { + guard let dataVolume = try? JSONDecoder().decode(DataVolume.self, from: payload) else { + return + } + + dataVolumeObserver.publish(dataVolume) + clientDelegate?.dataVolumeUpdated(dataVolume) + } } // MARK: - Outgoing communication to the server diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift index 3fe62c74f3..13ee59a0d3 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift @@ -135,6 +135,20 @@ extension TunnelControllerIPCServer: IPCClientInterface { client.statusChanged(payload: payload) } } + + public func dataVolumeUpdated(_ dataVolume: DataVolume) { + let payload: Data + + do { + payload = try JSONEncoder().encode(dataVolume) + } catch { + return + } + + xpc.forEachClient { client in + client.dataVolumeUpdated(payload: payload) + } + } } // MARK: - Incoming communication from a client From f491fdc684ea4ede081f262e7130ee59320112d2 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Wed, 10 Apr 2024 20:56:29 -0400 Subject: [PATCH 15/32] Show volume on UI --- .../NetworkProtectionAsset.swift | 2 ++ .../TunnelControllerView.swift | 30 +++++++++++++++---- .../TunnelControllerViewModel.swift | 30 +++++++++++++++++++ .../NetworkProtectionAssetTests.swift | 2 ++ 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift index 6e02224576..51dcfc394a 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift @@ -25,6 +25,8 @@ public enum NetworkProtectionAsset: String, CaseIterable { case vpnEnabledImage = "VPN" case vpnIcon = "VPN-16" case nearestAvailable = "VPNLocation" + case dataReceived = "VPNDownload" + case dataSent = "VPNUpload" // Apple Icons case appleVaultIcon = "apple-vault-icon" diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift index 96244b1539..007dc7f08e 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -280,10 +280,11 @@ public struct TunnelControllerView: View { .applySectionHeaderAttributes(colorScheme: colorScheme) .padding(EdgeInsets(top: 6, leading: 9, bottom: 6, trailing: 9)) - connectionStatusRow(icon: .ipAddressIcon, - title: UserText.networkProtectionStatusViewIPAddress, + connectionStatusRow(title: UserText.networkProtectionStatusViewIPAddress, details: model.serverAddress) + dataVolumeRow(title: UserText.vpnDataVolume, details: model.dataVolume) + dividerRow() } } @@ -322,11 +323,8 @@ public struct TunnelControllerView: View { .padding(EdgeInsets(top: 3, leading: 9, bottom: 3, trailing: 9)) } - private func connectionStatusRow(icon: NetworkProtectionAsset, title: String, details: String) -> some View { + private func connectionStatusRow(title: String, details: String) -> some View { HStack(spacing: 0) { - Image(icon) - .padding([.trailing], 8) - Text(title) .applyLabelAttributes(colorScheme: colorScheme) .fixedSize() @@ -340,4 +338,24 @@ public struct TunnelControllerView: View { } .padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 9)) } + + private func dataVolumeRow(title: String, details: TunnelControllerViewModel.FormattedDataVolume) -> some View { + HStack(spacing: 0) { + Text(title) + .applyLabelAttributes(colorScheme: colorScheme) + .fixedSize() + + Spacer(minLength: 2) + + Group { + Image(NetworkProtectionAsset.dataReceived) + Text(details.received) + Image(NetworkProtectionAsset.dataSent) + .padding(.leading, 4) + Text(details.sent) + } + .foregroundColor(Color(.defaultText).opacity(0.6)) + } + .padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 9)) + } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift index dfd96cd277..b94b0af9c5 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift @@ -23,6 +23,7 @@ import SwiftUI @MainActor public final class TunnelControllerViewModel: ObservableObject { + typealias FormattedDataVolume = (received: String, sent: String) /// The NetP service. /// @@ -56,6 +57,13 @@ public final class TunnelControllerViewModel: ObservableObject { private let locationFormatter: VPNLocationFormatting + private static let byteCountFormatter: ByteCountFormatter = { + let formatter = ByteCountFormatter() + formatter.allowsNonnumericFormatting = false + formatter.allowedUnits = [.useKB, .useMB, .useGB] + return formatter + }() + private let appLauncher: AppLaunching // MARK: - Misc @@ -70,6 +78,7 @@ public final class TunnelControllerViewModel: ObservableObject { private static let statusDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.statusDispatchQueue", qos: .userInteractive) private static let connectivityIssuesDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.connectivityIssuesDispatchQueue", qos: .userInteractive) private static let serverInfoDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.serverInfoDispatchQueue", qos: .userInteractive) + private static let dataVolumeDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.dataVolumeDispatchQueue", qos: .userInteractive) // MARK: - Initialization & Deinitialization @@ -90,6 +99,7 @@ public final class TunnelControllerViewModel: ObservableObject { self.appLauncher = appLauncher connectionStatus = statusReporter.statusObserver.recentValue + dataVolume = statusReporter.dataVolumeObserver.recentValue.formatted(using: Self.byteCountFormatter) internalServerAddress = statusReporter.serverInfoObserver.recentValue.serverAddress internalServerAttributes = statusReporter.serverInfoObserver.recentValue.serverLocation internalServerLocation = internalServerAttributes?.serverLocation @@ -100,6 +110,7 @@ public final class TunnelControllerViewModel: ObservableObject { subscribeToOnboardingStatusChanges() subscribeToStatusChanges() subscribeToServerInfoChanges() + subscribeToDataVolumeUpdates() } deinit { @@ -156,6 +167,15 @@ public final class TunnelControllerViewModel: ObservableObject { .store(in: &cancellables) } + private func subscribeToDataVolumeUpdates() { + statusReporter.dataVolumeObserver.publisher + .subscribe(on: Self.dataVolumeDispatchQueue) + .map { $0.formatted(using: Self.byteCountFormatter) } + .receive(on: DispatchQueue.main) + .assign(to: \.dataVolume, onWeaklyHeld: self) + .store(in: &cancellables) + } + // MARK: - ON/OFF Toggle private func startTimer() { @@ -444,6 +464,9 @@ public final class TunnelControllerViewModel: ObservableObject { @Published private var internalServerAttributes: NetworkProtectionServerInfo.ServerAttributes? + @Published + var dataVolume: FormattedDataVolume + var wantsNearestLocation: Bool { guard case .nearest = vpnSettings.selectedLocation else { return false } return true @@ -508,3 +531,10 @@ public final class TunnelControllerViewModel: ObservableObject { } } } + +extension DataVolume { + func formatted(using formatter: ByteCountFormatter) -> TunnelControllerViewModel.FormattedDataVolume { + (received: formatter.string(fromByteCount: downloadTotal), + sent: formatter.string(fromByteCount: uploadTotal)) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift index 83d08e3ceb..9b2e88dae7 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift @@ -34,6 +34,8 @@ final class NetworkProtectionAssetTests: XCTestCase { .vpnEnabledImage: "VPN", .vpnIcon: "VPN-16", .nearestAvailable: "VPNLocation", + .dataReceived: "VPNDownload", + .dataSent: "VPNUpload", .appleVaultIcon: "apple-vault-icon", .appleVPNIcon: "apple-vpn-icon", .appleSystemSettingsIcon: "apple-system-settings-icon", From dc09631727b40459495e8bbe2359dc08d768f74a Mon Sep 17 00:00:00 2001 From: Anh Do Date: Wed, 10 Apr 2024 21:29:41 -0400 Subject: [PATCH 16/32] Remove animationView config --- .../Views/TunnelControllerView/TunnelControllerView.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift index 007dc7f08e..c277832e8d 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -208,10 +208,6 @@ public struct TunnelControllerView: View { @ViewBuilder private func headerAnimationView(_ animationName: String) -> some View { LottieView(animation: .named(animationName)) - .configure { animationView in - animationView.contentMode = .scaleAspectFit - animationView.clipsToBounds = true - } .playing(withIntro: .init( skipIntro: model.isVPNEnabled && !model.isToggleDisabled, introStartFrame: 0, From 628a36dbac900b963dd1b1c9aeceece269ab94c7 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Wed, 10 Apr 2024 21:36:52 -0400 Subject: [PATCH 17/32] Fix styling --- .../View/NetPPopoverManagerMock.swift | 2 +- .../TunnelControllerIPCService.swift | 34 ------------------- .../TunnelControllerView.swift | 25 ++++++++++---- .../TunnelControllerViewModel.swift | 26 ++++++++------ 4 files changed, 34 insertions(+), 53 deletions(-) diff --git a/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift b/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift index 15d7a146f4..0007a3e54a 100644 --- a/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift +++ b/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift @@ -62,7 +62,7 @@ final class IPCClientMock: NetworkProtectionIPCClient { var recentValue: String? } var ipcControllerErrorMessageObserver: any NetworkProtection.ControllerErrorMesssageObserver = ControllerErrorMesssageObserverMock() - + final class DataVolumeObserverMock: NetworkProtection.DataVolumeObserver { var publisher: AnyPublisher = PassthroughSubject().eraseToAnyPublisher() var recentValue: DataVolume = .init() diff --git a/DuckDuckGoVPN/TunnelControllerIPCService.swift b/DuckDuckGoVPN/TunnelControllerIPCService.swift index 2888f14218..c8a9ce456d 100644 --- a/DuckDuckGoVPN/TunnelControllerIPCService.swift +++ b/DuckDuckGoVPN/TunnelControllerIPCService.swift @@ -33,7 +33,6 @@ final class TunnelControllerIPCService { private let networkExtensionController: NetworkExtensionController private let server: NetworkProtectionIPC.TunnelControllerIPCServer private let statusReporter: NetworkProtectionStatusReporter - private lazy var dataVolumeMonitor = NetworkProtectionDataVolumeMonitor() private var cancellables = Set() private let defaults: UserDefaults @@ -50,7 +49,6 @@ final class TunnelControllerIPCService { subscribeToErrorChanges() subscribeToStatusUpdates() - subscribeToDataVolumeUpdates() subscribeToServerChanges() server.serverDelegate = self @@ -86,38 +84,6 @@ final class TunnelControllerIPCService { } .store(in: &cancellables) } - - private func subscribeToDataVolumeUpdates() { - statusReporter.statusObserver.publisher - .subscribe(on: DispatchQueue.main) - .map { connectionStatus in - guard case .connected = connectionStatus else { return false } - return true - } - .sink { isConnected in - Task { [weak self] in - guard let self else { return } - if !isConnected { - await self.dataVolumeMonitor.stop() - } else { - await self.startDataVolumeMonitor() - } - } - } - .store(in: &cancellables) - } - - private func startDataVolumeMonitor() async { - guard await !dataVolumeMonitor.isStarted else { return } - await dataVolumeMonitor.start(with: tunnelController) { [weak self] result in - switch result { - case .failure: - break - case .success(let dataVolume): - self?.server.dataVolumeUpdated(dataVolume) - } - } - } } // MARK: - Requests from the client diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift index c277832e8d..b3f4b18d59 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -28,6 +28,10 @@ fileprivate extension Font { .system(size: 13, weight: .regular, design: .default) } + static var dataVolume: Font { + .system(size: 13, weight: .regular, design: .default) + } + static var location: Font { .system(size: 13, weight: .regular, design: .default) } @@ -64,7 +68,7 @@ private enum Opacity { colorScheme == .light ? Double(0.6) : Double(0.5) } - static func location(colorScheme: ColorScheme) -> Double { + static func dataVolume(colorScheme: ColorScheme) -> Double { colorScheme == .light ? Double(0.6) : Double(0.5) } @@ -93,6 +97,12 @@ fileprivate extension View { .foregroundColor(Color(.defaultText)) } + func applyDataVolumeAttributes(colorScheme: ColorScheme) -> some View { + opacity(Opacity.dataVolume(colorScheme: colorScheme)) + .font(.NetworkProtection.dataVolume) + .foregroundColor(Color(.defaultText)) + } + func applyLocationAttributes() -> some View { font(.NetworkProtection.location) } @@ -253,7 +263,7 @@ public struct TunnelControllerView: View { .applyLocationAttributes() .foregroundColor(.white) } else { - Text(model.formattedLocation) + Text(model.formattedLocation(colorScheme: colorScheme)) .applyLocationAttributes() } } else { @@ -279,7 +289,7 @@ public struct TunnelControllerView: View { connectionStatusRow(title: UserText.networkProtectionStatusViewIPAddress, details: model.serverAddress) - dataVolumeRow(title: UserText.vpnDataVolume, details: model.dataVolume) + dataVolumeRow(title: UserText.vpnDataVolume, dataVolume: model.formattedDataVolume) dividerRow() } @@ -335,7 +345,7 @@ public struct TunnelControllerView: View { .padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 9)) } - private func dataVolumeRow(title: String, details: TunnelControllerViewModel.FormattedDataVolume) -> some View { + private func dataVolumeRow(title: String, dataVolume: TunnelControllerViewModel.FormattedDataVolume) -> some View { HStack(spacing: 0) { Text(title) .applyLabelAttributes(colorScheme: colorScheme) @@ -345,12 +355,13 @@ public struct TunnelControllerView: View { Group { Image(NetworkProtectionAsset.dataReceived) - Text(details.received) + Text(dataVolume.dataReceived) Image(NetworkProtectionAsset.dataSent) .padding(.leading, 4) - Text(details.sent) + Text(dataVolume.dataSent) } - .foregroundColor(Color(.defaultText).opacity(0.6)) + .applyDataVolumeAttributes(colorScheme: colorScheme) + .fixedSize() } .padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 9)) } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift index b94b0af9c5..bbc985ba0f 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift @@ -23,7 +23,10 @@ import SwiftUI @MainActor public final class TunnelControllerViewModel: ObservableObject { - typealias FormattedDataVolume = (received: String, sent: String) + public struct FormattedDataVolume { + public let dataSent: String + public let dataReceived: String + } /// The NetP service. /// @@ -99,7 +102,7 @@ public final class TunnelControllerViewModel: ObservableObject { self.appLauncher = appLauncher connectionStatus = statusReporter.statusObserver.recentValue - dataVolume = statusReporter.dataVolumeObserver.recentValue.formatted(using: Self.byteCountFormatter) + formattedDataVolume = statusReporter.dataVolumeObserver.recentValue.formatted(using: Self.byteCountFormatter) internalServerAddress = statusReporter.serverInfoObserver.recentValue.serverAddress internalServerAttributes = statusReporter.serverInfoObserver.recentValue.serverLocation internalServerLocation = internalServerAttributes?.serverLocation @@ -172,7 +175,7 @@ public final class TunnelControllerViewModel: ObservableObject { .subscribe(on: Self.dataVolumeDispatchQueue) .map { $0.formatted(using: Self.byteCountFormatter) } .receive(on: DispatchQueue.main) - .assign(to: \.dataVolume, onWeaklyHeld: self) + .assign(to: \.formattedDataVolume, onWeaklyHeld: self) .store(in: &cancellables) } @@ -465,7 +468,7 @@ public final class TunnelControllerViewModel: ObservableObject { private var internalServerAttributes: NetworkProtectionServerInfo.ServerAttributes? @Published - var dataVolume: FormattedDataVolume + var formattedDataVolume: FormattedDataVolume var wantsNearestLocation: Bool { guard case .nearest = vpnSettings.selectedLocation else { return false } @@ -483,11 +486,12 @@ public final class TunnelControllerViewModel: ObservableObject { } @available(macOS 12, *) - var formattedLocation: AttributedString { - locationFormatter.string(from: internalServerLocation, - preferredLocation: vpnSettings.selectedLocation, - locationTextColor: Color(.defaultText), - preferredLocationTextColor: Color(.defaultText).opacity(0.6)) + func formattedLocation(colorScheme: ColorScheme) -> AttributedString { + let opacity = colorScheme == .light ? Double(0.6) : Double(0.5) + return locationFormatter.string(from: internalServerLocation, + preferredLocation: vpnSettings.selectedLocation, + locationTextColor: Color(.defaultText), + preferredLocationTextColor: Color(.defaultText).opacity(opacity)) } // MARK: - Toggling VPN @@ -534,7 +538,7 @@ public final class TunnelControllerViewModel: ObservableObject { extension DataVolume { func formatted(using formatter: ByteCountFormatter) -> TunnelControllerViewModel.FormattedDataVolume { - (received: formatter.string(fromByteCount: downloadTotal), - sent: formatter.string(fromByteCount: uploadTotal)) + .init(dataSent: formatter.string(fromByteCount: bytesSent), + dataReceived: formatter.string(fromByteCount: bytesReceived)) } } From a8330a8c3d473cab8ea04b3e92acb66249c1b7bf Mon Sep 17 00:00:00 2001 From: Anh Do Date: Thu, 11 Apr 2024 00:50:06 -0400 Subject: [PATCH 18/32] Revert accidental delete --- DuckDuckGoVPN/TunnelControllerIPCService.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/DuckDuckGoVPN/TunnelControllerIPCService.swift b/DuckDuckGoVPN/TunnelControllerIPCService.swift index c8a9ce456d..ad0faba0cb 100644 --- a/DuckDuckGoVPN/TunnelControllerIPCService.swift +++ b/DuckDuckGoVPN/TunnelControllerIPCService.swift @@ -50,6 +50,7 @@ final class TunnelControllerIPCService { subscribeToErrorChanges() subscribeToStatusUpdates() subscribeToServerChanges() + subscribeToDataVolumeUpdates() server.serverDelegate = self } @@ -84,6 +85,15 @@ final class TunnelControllerIPCService { } .store(in: &cancellables) } + + private func subscribeToDataVolumeUpdates() { + statusReporter.dataVolumeObserver.publisher + .subscribe(on: DispatchQueue.main) + .sink { [weak self] dataVolume in + self?.server.dataVolumeUpdated(dataVolume) + } + .store(in: &cancellables) + } } // MARK: - Requests from the client From 5dd91c329dea5f13eabf6e7ef8866d50a2fb72bb Mon Sep 17 00:00:00 2001 From: Anh Do Date: Thu, 11 Apr 2024 22:46:25 -0400 Subject: [PATCH 19/32] Update menu item copy --- .../Common/Localizables/UserText+NetworkProtection.swift | 4 ++-- DuckDuckGoDBPBackgroundAgent/UserText.swift | 4 ++-- DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 6 +++--- DuckDuckGoVPN/UserText.swift | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index 317a7c5178..2e48bd7238 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -42,8 +42,8 @@ extension UserText { static let networkProtectionInviteSuccessMessage = "DuckDuckGo's VPN secures all of your device's Internet traffic anytime, anywhere." // MARK: - Navigation Bar Status View - // "network.protection.navbar.status.view.share.feedback" - Menu item for 'Send VPN Feedback' in the VPN status view that's shown in the navigation bar - static let networkProtectionNavBarStatusViewShareFeedback = "Send VPN Feedback…" + // "network.protection.navbar.status.view.share.feedback" - Menu item for 'Share Feedback' in the VPN status view that's shown in the navigation bar + static let networkProtectionNavBarStatusViewShareFeedback = "Share Feedback…" // "network.protection.status.menu.vpn.settings" - The status menu 'VPN Settings' menu item static let networkProtectionNavBarStatusMenuVPNSettings = "VPN Settings…" // "network.protection.status.menu.faq" - The status menu 'FAQ' menu item diff --git a/DuckDuckGoDBPBackgroundAgent/UserText.swift b/DuckDuckGoDBPBackgroundAgent/UserText.swift index 5b460321e5..b868dfe356 100644 --- a/DuckDuckGoDBPBackgroundAgent/UserText.swift +++ b/DuckDuckGoDBPBackgroundAgent/UserText.swift @@ -21,6 +21,6 @@ import Foundation final class UserText { // MARK: - Status Menu - static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Send VPN Feedback...", comment: "The status menu 'Send VPN Feedback' menu item") - static let networkProtectionStatusMenuOpenDuckDuckGo = NSLocalizedString("network.protection.status.menu.open.duckduckgo", value: "Open DuckDuckGo...", comment: "The status menu 'Open DuckDuckGo' menu item") + static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Share Feedback…", comment: "The status menu 'Share Feedback' menu item") + static let networkProtectionStatusMenuOpenDuckDuckGo = NSLocalizedString("network.protection.status.menu.open.duckduckgo", value: "Open DuckDuckGo…", comment: "The status menu 'Open DuckDuckGo' menu item") } diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index 6dea291314..034d768892 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -251,12 +251,12 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuFAQ, action: { [weak self] in await self?.appLauncher.launchApp(withCommand: .showFAQ) }), + StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuShareFeedback, action: { [weak self] in + await self?.appLauncher.launchApp(withCommand: .shareFeedback) + }), StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuOpenDuckDuckGo, action: { [weak self] in await self?.appLauncher.launchApp(withCommand: .justOpen) }), - StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuShareFeedback, action: { [weak self] in - await self?.appLauncher.launchApp(withCommand: .shareFeedback) - }) ] }, agentLoginItem: nil, diff --git a/DuckDuckGoVPN/UserText.swift b/DuckDuckGoVPN/UserText.swift index 57ea5e3c3b..4b779e92de 100644 --- a/DuckDuckGoVPN/UserText.swift +++ b/DuckDuckGoVPN/UserText.swift @@ -24,5 +24,5 @@ final class UserText { static let networkProtectionStatusMenuVPNSettings = NSLocalizedString("network.protection.status.menu.vpn.settings", value: "VPN Settings…", comment: "The status menu 'VPN Settings' menu item") static let networkProtectionStatusMenuFAQ = NSLocalizedString("network.protection.status.menu.faq", value: "Frequently Asked Questions…", comment: "The status menu 'FAQ' menu item") static let networkProtectionStatusMenuOpenDuckDuckGo = NSLocalizedString("network.protection.status.menu.vpn.open-duckduckgo", value: "Open DuckDuckGo…", comment: "The status menu 'Open DuckDuckGo' menu item") - static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Send VPN Feedback…", comment: "The status menu 'Send VPN Feedback' menu item") + static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Share Feedback…", comment: "The status menu 'Share Feedback' menu item") } From ec611c4e12ec5258e81eb99019cdc0fca5d7f714 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Thu, 11 Apr 2024 22:55:32 -0400 Subject: [PATCH 20/32] Open new browser window if no windows exist --- DuckDuckGo/Application/URLEventHandler.swift | 2 ++ DuckDuckGo/Windows/View/WindowControllersManager.swift | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/DuckDuckGo/Application/URLEventHandler.swift b/DuckDuckGo/Application/URLEventHandler.swift index e261efdf12..696d2b59d7 100644 --- a/DuckDuckGo/Application/URLEventHandler.swift +++ b/DuckDuckGo/Application/URLEventHandler.swift @@ -145,6 +145,8 @@ final class URLEventHandler { WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) case AppLaunchCommand.shareFeedback.launchURL: WindowControllersManager.shared.showShareFeedbackModal() + case AppLaunchCommand.justOpen.launchURL: + WindowControllersManager.shared.showNewWindow() case AppLaunchCommand.showVPNLocations.launchURL: WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) WindowControllersManager.shared.showLocationPickerSheet() diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index ae37e64c41..f28e90c26b 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -237,6 +237,13 @@ extension WindowControllersManager { } } + func showNewWindow() { + guard WindowControllersManager.shared.lastKeyMainWindowController == nil else { return } + let tabCollection = TabCollection(tabs: []) + let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) + _ = WindowsManager.openNewWindow(with: tabCollectionViewModel) + } + func showLocationPickerSheet() { let locationsViewController = VPNLocationsHostingViewController() let locationsWindowController = locationsViewController.wrappedInWindowController() From b06646f9575bf9bfc05ee5c369c45b10f3c108f9 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Fri, 12 Apr 2024 00:19:27 -0400 Subject: [PATCH 21/32] Update BSK --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index de7412bbba..c7f43fd49d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14568,7 +14568,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { - branch = "anh/netp/location-selection"; + branch = "anh/netp/screen-improvements"; kind = branch; }; }; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9a48391db4..e59793995f 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,7 +33,7 @@ "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { "branch" : "anh/netp/screen-improvements", - "revision" : "4a82b066114d7e6beed403e3345b4f58c26f4994" + "revision" : "ae13b6ab3880db92776938ae1be4aceb4a3b4ea1" } }, { From fe192bf3d9bbc8e4420609581dacdb27e4252551 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Fri, 12 Apr 2024 00:35:38 -0400 Subject: [PATCH 22/32] Update tests --- .../TunnelControllerViewModelTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift index f82c131b42..884dc3d002 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift @@ -166,7 +166,7 @@ final class TunnelControllerViewModelTests: XCTestCase { XCTAssertEqual(model.featureStatusDescription, UserText.networkProtectionStatusViewFeatureOn) XCTAssertTrue(model.showServerDetails) XCTAssertEqual(model.serverAddress, mockServerIP) - XCTAssertEqual(model.serverLocation, "El Segundo, CA...") + XCTAssertEqual(model.serverLocation, "El Segundo, United States...") } /// We expect the model to properly reflect the connecting status. From 14b0d31884132ab94e6a04a28417dfe6639564bd Mon Sep 17 00:00:00 2001 From: Anh Do Date: Fri, 12 Apr 2024 00:48:52 -0400 Subject: [PATCH 23/32] Add tests --- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../TunnelControllerViewModel.swift | 2 +- .../TunnelControllerViewModelTests.swift | 26 ++++++++++++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e59793995f..dcc3605227 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,7 +33,7 @@ "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { "branch" : "anh/netp/screen-improvements", - "revision" : "ae13b6ab3880db92776938ae1be4aceb4a3b4ea1" + "revision" : "3b354a70cdfff5d46d2c8f525748dee63a36ad88" } }, { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift index bbc985ba0f..e28b2aed75 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift @@ -23,7 +23,7 @@ import SwiftUI @MainActor public final class TunnelControllerViewModel: ObservableObject { - public struct FormattedDataVolume { + public struct FormattedDataVolume: Equatable { public let dataSent: String public let dataReceived: String } diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift index 884dc3d002..e7785da3f4 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift @@ -36,12 +36,14 @@ final class TunnelControllerViewModelTests: XCTestCase { let connectionErrorObserver: ConnectionErrorObserver let connectivityIssuesObserver: ConnectivityIssueObserver let controllerErrorMessageObserver: ControllerErrorMesssageObserver + let dataVolumeObserver: DataVolumeObserver init(status: ConnectionStatus, isHavingConnectivityIssues: Bool = false, serverInfo: NetworkProtectionStatusServerInfo = MockStatusReporter.defaultServerInfo, tunnelErrorMessage: String? = nil, - controllerErrorMessage: String? = nil) { + controllerErrorMessage: String? = nil, + dataVolume: DataVolume = .init()) { let mockStatusObserver = MockConnectionStatusObserver() mockStatusObserver.subject.send(status) @@ -62,6 +64,10 @@ final class TunnelControllerViewModelTests: XCTestCase { let mockControllerErrorMessageObserver = MockControllerErrorMesssageObserver() mockControllerErrorMessageObserver.subject.send(controllerErrorMessage) controllerErrorMessageObserver = mockControllerErrorMessageObserver + + let mockDataVolumeObserver = MockDataVolumeObserver() + mockDataVolumeObserver.subject.send(dataVolume) + dataVolumeObserver = mockDataVolumeObserver } func forceRefresh() { @@ -189,6 +195,24 @@ final class TunnelControllerViewModelTests: XCTestCase { XCTAssertFalse(model.showServerDetails) } + /// We expect the model to properly reflect the data volume. + /// + @MainActor + func testProperlyReflectsDataVolume() async throws { + let controller = MockTunnelController() + let statusReporter = MockStatusReporter(status: .connected(connectedDate: Date()), + dataVolume: .init(bytesSent: 512000, bytesReceived: 1024000)) + let model = TunnelControllerViewModel( + controller: controller, + onboardingStatusPublisher: Just(OnboardingStatus.completed).eraseToAnyPublisher(), + statusReporter: statusReporter, + vpnSettings: .init(defaults: .standard), + locationFormatter: MockVPNLocationFormatter(), + appLauncher: MockAppLauncher()) + + XCTAssertEqual(model.formattedDataVolume, .init(dataSent: "512 KB", dataReceived: "1 MB")) + } + /// We expect that setting the model's `isRunning` to `true`, will start the VPN. /// @MainActor From 5d4ea263a442c3e046eb90f4fc15f0a4ca84c1b8 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Tue, 16 Apr 2024 10:03:09 -0400 Subject: [PATCH 24/32] Update Nearest Location copy --- .../Common/Localizables/UserText+NetworkProtection.swift | 2 +- .../NetworkProtection/DefaultVPNLocationFormatterTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index 2e48bd7238..ff606365ee 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -299,7 +299,7 @@ extension UserText { // "vpn.location.description.nearest" - Nearest city setting description static let vpnLocationNearest = "Nearest" // "vpn.location.description.nearest.available" - Nearest available location setting description - static let vpnLocationNearestAvailable = "Nearest available" + static let vpnLocationNearestAvailable = "Nearest Location" // "vpn.location.nearest.available.title" - Subtitle underneath the nearest available vpn location preference text. static let vpnLocationNearestAvailableSubtitle = "Automatically connect to the nearest server we can find." diff --git a/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift b/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift index 0c52440181..56190708be 100644 --- a/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift +++ b/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift @@ -39,7 +39,7 @@ final class DefaultVPNLocationFormatterTests: XCTestCase { XCTAssertEqual(formatter.emoji(for: server.country, preferredLocation: preferredLocation), "🇺🇸") XCTAssertEqual(formatter.emoji(for: server.country, preferredLocation: otherPreferredLocation), "🇺🇸") - XCTAssertEqual(formatter.string(from: nil, preferredLocation: .nearest), "Nearest available") + XCTAssertEqual(formatter.string(from: nil, preferredLocation: .nearest), "Nearest Location") XCTAssertEqual(formatter.string(from: nil, preferredLocation: preferredLocation), "United States") XCTAssertEqual(formatter.string(from: nil, preferredLocation: otherPreferredLocation), "United Kingdom") XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: .nearest), "Lafayette, United States (Nearest)") @@ -69,7 +69,7 @@ final class DefaultVPNLocationFormatterTests: XCTestCase { XCTAssertEqual(formatter.emoji(for: server.country, preferredLocation: preferredLocation), "🇨🇦") XCTAssertEqual(formatter.emoji(for: server.country, preferredLocation: otherPreferredLocation), "🇨🇦") - XCTAssertEqual(formatter.string(from: nil, preferredLocation: .nearest), "Nearest available") + XCTAssertEqual(formatter.string(from: nil, preferredLocation: .nearest), "Nearest Location") XCTAssertEqual(formatter.string(from: nil, preferredLocation: preferredLocation), "Canada") XCTAssertEqual(formatter.string(from: nil, preferredLocation: otherPreferredLocation), "United Kingdom") XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: .nearest), "Toronto, Canada (Nearest)") From 896789f9815fd6535afa7a3ec6146e9aefe1160e Mon Sep 17 00:00:00 2001 From: Anh Do Date: Tue, 16 Apr 2024 10:16:54 -0400 Subject: [PATCH 25/32] Use SecondaryColor for description --- .../NetworkProtectionColor.swift | 1 + .../SecondaryColor.colorset/Contents.json | 38 +++++++++++++++++++ .../TunnelControllerView.swift | 10 ++--- 3 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Colors/SecondaryColor.colorset/Contents.json diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionColor.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionColor.swift index 010085527a..2145e2da36 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionColor.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionColor.swift @@ -31,6 +31,7 @@ extension Color { /// enum NetworkProtectionColor: String { case defaultText = "TextColor" + case secondaryText = "SecondaryColor" case linkColor = "LinkBlueColor" case onboardingButtonBackgroundColor = "OnboardingButtonBackgroundColor" #if swift(<5.9) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Colors/SecondaryColor.colorset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Colors/SecondaryColor.colorset/Contents.json new file mode 100644 index 0000000000..6649fc47e0 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Colors/SecondaryColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.600", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.500", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift index b3f4b18d59..66f10ed3a7 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -74,7 +74,6 @@ private enum Opacity { static let content = Double(0.58) static let label = Double(0.9) - static let description = Double(0.9) static let link = Double(1) static func sectionHeader(colorScheme: ColorScheme) -> Double { @@ -113,10 +112,9 @@ fileprivate extension View { .foregroundColor(Color(.defaultText)) } - func applyDescriptionAttributes(colorScheme: ColorScheme) -> some View { - opacity(Opacity.description) - .font(.NetworkProtection.description) - .foregroundColor(Color(.defaultText)) + func applyDescriptionAttributes() -> some View { + font(.NetworkProtection.description) + .foregroundColor(Color(.secondaryText)) } func applyLabelAttributes(colorScheme: ColorScheme) -> some View { @@ -200,7 +198,7 @@ public struct TunnelControllerView: View { Text(model.isToggleOn.wrappedValue ? UserText.networkProtectionStatusHeaderMessageOn : UserText.networkProtectionStatusHeaderMessageOff) .multilineText() .multilineTextAlignment(.center) - .applyDescriptionAttributes(colorScheme: colorScheme) + .applyDescriptionAttributes() .fixedSize(horizontal: false, vertical: true) .padding(EdgeInsets(top: 8, leading: 16, bottom: 16, trailing: 16)) } From 536c52b13e2e88412028cbf30090eaa4e70306e2 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Tue, 16 Apr 2024 11:13:55 -0400 Subject: [PATCH 26/32] Update arrow icon --- .../Icons/VPNDownload.imageset/Contents.json | 3 --- .../Icons/VPNLocation.imageset/Contents.json | 3 --- .../VPNLocation.imageset/VPNLocation.pdf | Bin 1497 -> 2495 bytes .../Icons/VPNUpload.imageset/Contents.json | 3 --- .../TunnelControllerView.swift | 9 +++++++-- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/Contents.json index c9df096cfc..ef66f63922 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/Contents.json +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/Contents.json @@ -8,8 +8,5 @@ "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/Contents.json index bd59670506..09ed1f69f3 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/Contents.json +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/Contents.json @@ -8,8 +8,5 @@ "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/VPNLocation.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/VPNLocation.pdf index 2cfa6eb89db640e3a2009cfb2f34c25538f92b91..ddcabe468b93c60f767774dd5febb091afc88f18 100644 GIT binary patch literal 2495 zcmc&#OK;Rj5We@X=u0FxVE4Oi2}L4Lt#%P%W)Db+gX1*Gurqenc2wVzqhj-UsLfHYsll~Q|+wP7l zC@u!*vZ){Lw+!NYe->?e!zKJRMb+LHO{mJRJLkv8gRAAkbr*wP)XiqV7NM;lnliL8 z#Q9@)x$KHA^lvi$lm!=w6ZG)oRbX%ee* zrv={-YMoWi5}K(@d&LP8ne))JiS%BA21qIsMre|Zu-YRgSqrPAj+#zuXP{{kFSHRo zmJ6jNVMZF~qO~vEF-|1ChppfaoiiPGugLa8^Rd z5|l|JG}&cj&_`$+>)6wEskde%RA%uRo9OgHn3Q>606Fz(JK6Ofmf5 ztncrN=UtEK&c<8Pu#u*l{GYqH18@juV}H0EriLwsX6+gJnTel*+Ozs_Z<52lO2$p0 zF#xuf>YJ*nyOw{XU4v)1(p7OA2H5 z11v&YJQ392fWD|dS2swN(+w7U%=ol^fUVrizujeAVt#MAq8qkT#QEl@MOUor+g;no zCVXaCO_KGUL;tz6R$C5ycqjlpyc(h0U^qU396DzQlE&;q4(#k|`F+D|Un`@5zhkI` zdzx2VxT99r6svV;7}5L38OVLn)|>E>zgXS8>?uwcbq#)rzF4nxx%w7}3zVttil&R~ ND482}baeji;ya&5Aff;O literal 1497 zcmZvcZEw>s5Xay9Q{0sQ&g{5S^XTE-Q+`e)5m=&aCvW{vnMCf7`hMzG8~U=KA+PRH5! zO%LB#MiX3gG+T>t+3QI&s{}_GRM09AX3-~`46OwxxN;_%OkH5Zo-&Kp2F{E*vgvG+ zvWXLMkimEt6U_uG+0h8KJzQlNlyf!!8rDfW0@5bNl-I$v(VmhFk(2cx9K8)1?vgax zrewi%j2=?5I;Bzqfns)HM_96lQ{UP3tGu+_u@!vvEXZV-eIn@gF-8p+y``7hb>2sU zkOsSvt&mw~ATsSNAzFK56o`#MN1Pw2v}IHx$z-iI2D)vJ2NKZ;(fv zCLf~7(Yj!b(!|)@qeM2RHf$*hgHSScgxn$;hkCS+&A~$`*sEgC z#FKE+qD}@i9@DoLV;XjFC2&T9v3a9$WtO+C1{4_=au6dLQxEAQbkZA_w9d+%OspL% zVvnrYfTFJcr)LG3k8eaJK8xI5eeVp7mV0X3sFB0ie#qfo`@XHJx^3v;E7}=j^5yrB zk|uY>qU^!Pa=t7s>KA&*W$VUnqV0fov%%A}Z0hIDtZXP>7}MU}WnHypg&p%PFu=QF z14gb#xCMp~GTPlW@0W8(r5wSOhJ7yUXV~&9{SC;TC!D4|PdUJ5+;A88WziO^dU0s_ zvMC>hq9C^6=YR6f8ArhVK&`qNRU-<}na2*L+~@xw9Kb`wY>}ITRjkq818O(La#e0b zzI@+ZAsxp}y)IAacsW1WCS@|M>y~oBU4kd~%ReR5cb99iX}fS(2Pck>u0P%U1+f@H A?EnA( diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/Contents.json index 4efc61b832..410f073223 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/Contents.json +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/Contents.json @@ -8,8 +8,5 @@ "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift index 66f10ed3a7..3ff7b66dca 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -252,8 +252,13 @@ public struct TunnelControllerView: View { .background(Color(hex: "B2B2B2").opacity(0.3)) .clipShape(Circle()) } else if model.wantsNearestLocation { - Image(NetworkProtectionAsset.nearestAvailable) - .frame(width: 26, height: 26) + ZStack { + Circle() + .fill(Color(hex: "B2B2B2").opacity(0.3)) + .frame(width: 26, height: 26) + Image(NetworkProtectionAsset.nearestAvailable) + .frame(width: 16, height: 16) + } } if #available(macOS 12, *) { if isHovered { From 4a48593a5a896952903949058b58509ccb142113 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Tue, 16 Apr 2024 11:23:10 -0400 Subject: [PATCH 27/32] Update hover color --- .../TunnelControllerView/TunnelControllerView.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift index 3ff7b66dca..3beb886cff 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -256,8 +256,15 @@ public struct TunnelControllerView: View { Circle() .fill(Color(hex: "B2B2B2").opacity(0.3)) .frame(width: 26, height: 26) - Image(NetworkProtectionAsset.nearestAvailable) - .frame(width: 16, height: 16) + if isHovered { + Image(NetworkProtectionAsset.nearestAvailable) + .renderingMode(.template) + .foregroundColor(.white) + .frame(width: 16, height: 16) + } else { + Image(NetworkProtectionAsset.nearestAvailable) + .frame(width: 16, height: 16) + } } } if #available(macOS 12, *) { From 9c62b8caf3553610b90c154c69f85826cd12dd8f Mon Sep 17 00:00:00 2001 From: Anh Do Date: Tue, 16 Apr 2024 11:30:29 -0400 Subject: [PATCH 28/32] Update list item height --- .../Views/TunnelControllerView/TunnelControllerView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift index 3beb886cff..aa8ec59ac1 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -352,7 +352,7 @@ public struct TunnelControllerView: View { .applyConnectionStatusDetailAttributes(colorScheme: colorScheme) .fixedSize() } - .padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 9)) + .padding(EdgeInsets(top: 6, leading: 10, bottom: 0, trailing: 9)) } private func dataVolumeRow(title: String, dataVolume: TunnelControllerViewModel.FormattedDataVolume) -> some View { From ea788eea8be33ac94f5567234ab00da15671cdd2 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Tue, 16 Apr 2024 11:42:08 -0400 Subject: [PATCH 29/32] Update data volume icons --- .../VPNDownload.imageset/vpn-download.pdf | Bin 1357 -> 1351 bytes .../Icons/VPNUpload.imageset/vpn-upload.pdf | Bin 1361 -> 1355 bytes .../TunnelControllerView.swift | 5 ++++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/vpn-download.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/vpn-download.pdf index 55844a6647538a3ec000cd7c8e36e8f014467691..d5288602de6bba00209b51321e5dfb6fcd53f99d 100644 GIT binary patch literal 1351 zcmZXUOK;mS49D;K6ucB@50&+%1QZ3<^nqa;mZiHDJ9v(oW=QPKc86}CzLeuvS|Lc} zpGAuNlg!ET`u1FMWK4p9>gO*8aB%_I%}aZC-{x&RJjw2-$U$%?(t^v&!-vIkoy?dz zvi}^*P5T6z;EE2irmbVXXW4c9yDj4C?g|$7^QWR=e^_=SVbd_lyDJ2oi(xlz#&>FZ z6*O8cq1dzW*!xXWlnl97289H}F(}jy_Qb4rs03Ip3`&iFTnTSN=!mkxfa3uTI-KkZ zX1EPO2@Q%Xs}%~^O)PMuWKi~)Av)y(Ini1gG$1KTr%Z?Xj3-nu&7JdBx!F0*jSv_d z1l~FGFeCky$*?~)3ypMBR%tb*up_lFNK100)Vo9lSq4E`&WJ>-NQ_=Ggo35ASZ}l@ zXpQn1x=Ia5Y9nNzPU$T*_ufcH1)|6-T0w?_D-*oaBoMt89-SrDiD1dA3{vSMV(2^- zV6-cyVvhcI%uj*!QOrmes;Bb4ZGwV5u}4<2Z!9%)->E}erw%@hg+nsA`p6enukj8V>yIWLc z8%w$|&D;Q&c})yRwGPqD4JjlvP4=hlnnHz?!3u_Z7S%J^%6IWML}s4ck*AbkGhR3) zzR25rS8a|>U+VbC1Ry(fdin-uEmfItza*)W7L+5RF?oYLHSAlYs7h%~hJl9U7)e1h zI-s`Bx4T%gRK1VaB*$q}?c*7oZr5iAQL$@vhJuJT!4g1Bi8-#7ckasQwoQv@`U8dh8S9DMT zk)S*x6bCjv_V&^gB?}y-_ZYx%6VRZ51M}P)M5l;(?-l8Iu0;rFJEBBKP&p|Vb_FwB zS>u!g$*uG*1lZ3E1rI?cizP%7HdY}C3!{{GfK+)EjFOrAl8 zbWoeoMNH*8#JRGm71m2SdT6}rl?(1 z%Nc3zB|$%1YHk!7;mA^Rbi$J%$p+LxQjIj%S{W0@j*}W`_~4vgAj8p0ZD~+P%VKWn zm|(?bXlA?2;&!8qGAD6zqIQQ_A!>-Y*;Mak~B zJNHb^mbdqpf;j+z9INj?0P*Syv%9zU;jzuzcsPmfr^t~r%hST^>%#|PxlUprkK{kb za?`$`BDkW5(6n{T_mJJj-`gUt9&T{)I6oB){DSOG;HClcr?bKFk`KFaGhKQnxV7G+ zpi3*Ym00Xye0aKPiV_?PGH}IFGZ&+P><1Gj6xvm*Eiq;GjpqLX*t1yF#Y4S{S zGSDgGI3vBuB-p>2xkgzft6WYY>?m8FOK$WOC}&z|?MLFM}eYm@B+_J!WT##-M8z|+o_EYvtgYbJ6?}8P0YKptl9>j zzR?8H%D(>j88KVto7jU-alOrNsyBS12>J#eH+?f7D&1Y;plB7F>a{LngK0!py|zVF zwy`9GY3>HN%xhw3dPH+KM3B%l-5<7V@)T15tI^J)dL>zL;(kM9rit=D)0Ec1a(GQSdPe%pU22^^5Q>4qkN169Z%3qS6k=X zU92G$@BJ;Mx)BDvQ<^Jm;&w+oIP&;M)JP` CDJHQ1 literal 1361 zcmZXUOHbQC5XbNPDdvI%2i80LUI`(U1W?shrJ*;}gX=6IRbsccsc1hvvv&L-tfFN7 z^UmYHGn13M<;^*A#~6Zu>gO*8aB%_I^-Ftu-{x)XpRoHWauD2#wBYix|FBrDlNpn5 z>_5kH(>{SBxT1rsY3rEpS++x7l{HQ|klae|LV(@GQ1B3BvY108$Hpoo$HFM(9gqyCf>9E5r^n7w!8DiTSkIy2 z!8#L2_vmdxW7L9;aF!l&3>Jl+avaBvx?koD^~(R9h~EHfRPrgo35&SdT6p zQ&cg!%L!@jB|$$~YHk!7;mA^Rbi$J%xecg;q&w1FYh_Frica3hhY!x_88RHL)Rvlc zuq>vMjuCdmw4h)n`RMZ)NuR6;@V*ra_QW1p$-c4F)`y-Q8a(xKKOiJ}Y64%@Os(OJ3QgG<>E!>U^51s zgfH?o-&LEprZ07TWC9%P<)?43#wY`X4~vT5?VubHwZRecG?vFmQI*mf4N(Zn^$7XN zeOu?-U94HE-p3_f$7xgT;~AW8*Ju5uWUH!bAqnm-IJ@8ekCc7)r Date: Tue, 16 Apr 2024 11:42:53 -0400 Subject: [PATCH 30/32] Remove unused assets --- .../NetworkProtectionAsset.swift | 2 -- .../Icons/IP-16.imageset/Contents.json | 15 --------------- .../Icons/IP-16.imageset/IP-16.pdf | Bin 2306 -> 0 bytes .../Server-Location-16.imageset/Contents.json | 15 --------------- .../Server-Location-16.pdf | Bin 5417 -> 0 bytes .../NetworkProtectionAssetTests.swift | 2 -- 6 files changed, 34 deletions(-) delete mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/Contents.json delete mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/IP-16.pdf delete mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/Server-Location-16.imageset/Contents.json delete mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/Server-Location-16.imageset/Server-Location-16.pdf diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift index 51dcfc394a..a8288b7dbd 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift @@ -19,8 +19,6 @@ import Foundation public enum NetworkProtectionAsset: String, CaseIterable { - case ipAddressIcon = "IP-16" - case serverLocationIcon = "Server-Location-16" case vpnDisabledImage = "VPNDisabled" case vpnEnabledImage = "VPN" case vpnIcon = "VPN-16" diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/Contents.json deleted file mode 100644 index 657dbb1f2c..0000000000 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "IP-16.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/IP-16.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/IP-16.pdf deleted file mode 100644 index f68365bd0a32b0df9ab73a1f2fe798fffde1ffe0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2306 zcmb7`OOG5i5P#1_vWev{g5Stp)B6@IC@`**EtTIu9>t6d5Bab2n2!o-dGr=^X z>Zbwjl~Ypqpd6eEBI;;$XvquJ5lYIyJVe-f)17kQhLJ|XJ>#O6jyvIvml|pZKG_7a z2S{-7r!WP!ONdE|8VYYa2bEbhDxsVW2}RWS3!733D$61kty4C^sUit{)}Rq36@5eo zhITm+f|0>f7Hn`LMCV}(>2;7F$Gwm?f=fVxvPLE#W~(KNdyYe`uad=yQK@w~qlzR( z_1{XO`bd=n79lAfYm~gs)dOVtD z@)qTi0$-bnTqV*q6)|W4DH2wpg&@{G1dFy7Ci!UOhR6(v0Kq;49VO42k`@5!sfLCx zMGu4e{wp@Kpa1{+WvbQx{eY#KnA_@r!WYcc%Ia`_HG-BZMBZU#YegWmL8=JZy@ENw z*dRTIWMh*VeAGy)+`3;OEVX?LQOr3&0iC%-C!nRvy0fWSMVdMWeT-GN$t+#l+22Vl z*;c&MO>Dp{my^SoSE{_Yo;d$4wI|N4F8yNAQm zSiq0tW*5@F?WP7452|{kSe4&%?NKoUHpI?rxbc;|v;ctXBANI0NIZ zpl5{z=-^5Uo8xzf%VAocuCIPNkMEh} zfo;CxU+&@Ua2#-1FkADE&xD}4uaOE4{J}-1RDf#!7fKESc6CZi-7a3vhvPJ!8D{Uh zdkDv~bvcdC`LpBW^DQX5{jyv*I-CQ%dvpAI1ncvED~IzXyQ5{a?B?d1pI-k1mv#N>tM%)zm*e9n{d4hq{Q7st$ImZ6 zt^x2ytDaw;j<*lX^@rm>?hePBUw^Z{|8D=U!}Id@#TnawtGzs2ji>Rq@i6{$cv!~s z+lS}F3CVqG{EpSXXvC zlf6wf3oSV}Z&!zR=dgiAn|<_qESOpnf7kB0(wS7UqrhB?Y)^R=-DKo8xzNcTw6JDg6Jb$%jVyVQK`tFH(;^GLaEV3p!*S+*6 zl$LVTejiJ7M8*>t>lT5o? zwZYrEx6a{EjSkbN6GWq5543qp1P2Itb1J?m1cpT9+WodMW(y}xur(xzBa%lv6Vzg~ zUXh3-u2aM$5?2aXbvcU6rn%rcl1nVXuNaiUv@gZfnoF7lpMN&W6P7OPFFKh=+8xai1`9W`)@Vr?tvv+Z>2%QRx=~ zrK*q`Z3}&0bfi_7wJw5X;~^$a6j41mon!+cvK@)7iTueFTOx7PDk54}>aSAoQAl-# zRfz=1Qz5O0Vv0+%F@*%ylCP-|y_!Pp7C2{F4`MwLlP@7sBZ+-5!WY5~Mu4!GJj6(V z69^Sil}Q}6sIul%QV&L8!<8K9kVKzGQOtjkqXQ zt`Xx57HXCg(cn8O2SU!+%0sdR*%K1tS(rGKz>_#ID3N=8B5#lv@q@Ai8}nKT&VpnB z=p_zOa|n#ESQ}Km2Mal#=}S&vfp%KxZdXa6^~f7sAn@2Jq~I%OB?ILs0AbZ5`^JfmtwYv=%!XW!>xF^Wn9pp&S7T{5qIr3Rg}=?uuZa(6Q0O-2o{ z>Z~%@S1~!$AK{Gn;Ul(tl!2;J=3@*M86)AoC$95@Bw{HFmQ zVy;oFkd}}lG{gzAM2smaX-ikr!q2PpYSwa!v9wIVY9F+x#2G}Y1JgbNo9S7W`&cIV z2_E(_;gF<^5V5Zq=tA{rfT=NXO{QGX+AO61K(>d0^u`md!NGP|f?8T#qqf%hh2rg$IrMs_k@X zOv{MIxQ{|-Tycu=z(~MZ`*t>vCX0G10n{Y(0%>ZD0BAclU;%Xl(ikMpXL$_826~={ zj=KcV857jm#X)VCy4j51Om$Ka^>nIfST^S{0$p040misS`k4OcR*EM3qHQTj|BnF|BlWUGb<)R0>#YD7Jfc3MNckPuX5cdIf})!&3}=7B{dO|?J0pcbf0^8LzPP_EXDI^zOH zm8nI~@PUy_1#%>vV7*!?ijjx?iINR?C~Vw~7FDMPwzD#zLjfoHc(-{IB*F~&@blt! zz%7!gDxTq??^0&WA2WyuNdHZ}!Rif?;9Qb2z?r&n+D!58)=7n7-6YI(Vn?j$tqFsG zm$2+z;Rv!w{Mc8h5gFfV}?ulpF(&s!Y-p)+;vClUlE+6et+KUO_2(JF3wy z)`&-+24&1A9jYF$k|@F$1if+v=MWK|z?iEzRJ$2M9QxDNtH>;-R~)@jB2-m(GZfKf zq%06H%e3{1VKnNsLc8gXdKpeanWmh^8+<_bXr5Oi7hs#)6JxjDOqt8-ekDCbW5=r# z(HWX49rxAUM38}&8sDtMibkKKz};|0F9J1ZF>mv_HzjgPPbZ{XcyR0dBE=8@)T5|3 zDGlwqb{Fm8 z-Gi5P7Rc)uGb%LKOZ;X_s36n zx8Iz8UcaBi&4}3i#5A8SC*2&MPd_~!j?b$;z};-ey+1v^93Q!n$z^~aZ=b+c#-|SL zzR?L9-|nt|d-n-bS9x%=Uha8+`iZ^njs4FcyOS79cPGig=W*e5{Qd3A?fvQV>#aXO z9e-G8Rm(YN`u|GxGVTFyKVH`Q)A~4|fSvjcC{jCbAl^`4ui~AA*u2`99Fyn;dU?9N zyFWfHe9XN5%?HBq#q;Um_~rV=-KQ_lLAky;onBTs+@pT|-QB;ASl>Tt-afr_cX+8t M%e!~K`op*X2D&^hD*ylh diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift index 9b2e88dae7..b49d24d5b4 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift @@ -28,8 +28,6 @@ final class NetworkProtectionAssetTests: XCTestCase { /// func testAssetEnumValuesAreUnchanged() { let assetsAndExpectedRawValues: [NetworkProtectionAsset: String] = [ - .ipAddressIcon: "IP-16", - .serverLocationIcon: "Server-Location-16", .vpnDisabledImage: "VPNDisabled", .vpnEnabledImage: "VPN", .vpnIcon: "VPN-16", From de164d1a62c3ad84dd5a7ece96efda6e53f72614 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Thu, 18 Apr 2024 09:29:23 -0400 Subject: [PATCH 31/32] Address review comments --- DuckDuckGo/Application/URLEventHandler.swift | 2 +- .../Common/Localizables/UserText+NetworkProtection.swift | 4 ++-- DuckDuckGo/Windows/View/WindowControllersManager.swift | 2 +- DuckDuckGoDBPBackgroundAgent/UserText.swift | 2 +- DuckDuckGoVPN/UserText.swift | 2 +- .../Colors/SecondaryColor.colorset/Contents.json | 8 ++++---- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/DuckDuckGo/Application/URLEventHandler.swift b/DuckDuckGo/Application/URLEventHandler.swift index 696d2b59d7..a0f4956d8c 100644 --- a/DuckDuckGo/Application/URLEventHandler.swift +++ b/DuckDuckGo/Application/URLEventHandler.swift @@ -146,7 +146,7 @@ final class URLEventHandler { case AppLaunchCommand.shareFeedback.launchURL: WindowControllersManager.shared.showShareFeedbackModal() case AppLaunchCommand.justOpen.launchURL: - WindowControllersManager.shared.showNewWindow() + WindowControllersManager.shared.showMainWindow() case AppLaunchCommand.showVPNLocations.launchURL: WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) WindowControllersManager.shared.showLocationPickerSheet() diff --git a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index ff606365ee..4dd17342ea 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -42,8 +42,8 @@ extension UserText { static let networkProtectionInviteSuccessMessage = "DuckDuckGo's VPN secures all of your device's Internet traffic anytime, anywhere." // MARK: - Navigation Bar Status View - // "network.protection.navbar.status.view.share.feedback" - Menu item for 'Share Feedback' in the VPN status view that's shown in the navigation bar - static let networkProtectionNavBarStatusViewShareFeedback = "Share Feedback…" + // "network.protection.navbar.status.view.share.feedback" - Menu item for 'Share VPN Feedback' in the VPN status view that's shown in the navigation bar + static let networkProtectionNavBarStatusViewShareFeedback = "Share VPN Feedback…" // "network.protection.status.menu.vpn.settings" - The status menu 'VPN Settings' menu item static let networkProtectionNavBarStatusMenuVPNSettings = "VPN Settings…" // "network.protection.status.menu.faq" - The status menu 'FAQ' menu item diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index f28e90c26b..10758ce56d 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -237,7 +237,7 @@ extension WindowControllersManager { } } - func showNewWindow() { + func showMainWindow() { guard WindowControllersManager.shared.lastKeyMainWindowController == nil else { return } let tabCollection = TabCollection(tabs: []) let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) diff --git a/DuckDuckGoDBPBackgroundAgent/UserText.swift b/DuckDuckGoDBPBackgroundAgent/UserText.swift index b868dfe356..608cab9e14 100644 --- a/DuckDuckGoDBPBackgroundAgent/UserText.swift +++ b/DuckDuckGoDBPBackgroundAgent/UserText.swift @@ -21,6 +21,6 @@ import Foundation final class UserText { // MARK: - Status Menu - static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Share Feedback…", comment: "The status menu 'Share Feedback' menu item") + static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Share VPN Feedback…", comment: "The status menu 'Share VPN Feedback' menu item") static let networkProtectionStatusMenuOpenDuckDuckGo = NSLocalizedString("network.protection.status.menu.open.duckduckgo", value: "Open DuckDuckGo…", comment: "The status menu 'Open DuckDuckGo' menu item") } diff --git a/DuckDuckGoVPN/UserText.swift b/DuckDuckGoVPN/UserText.swift index 4b779e92de..4c64c664eb 100644 --- a/DuckDuckGoVPN/UserText.swift +++ b/DuckDuckGoVPN/UserText.swift @@ -24,5 +24,5 @@ final class UserText { static let networkProtectionStatusMenuVPNSettings = NSLocalizedString("network.protection.status.menu.vpn.settings", value: "VPN Settings…", comment: "The status menu 'VPN Settings' menu item") static let networkProtectionStatusMenuFAQ = NSLocalizedString("network.protection.status.menu.faq", value: "Frequently Asked Questions…", comment: "The status menu 'FAQ' menu item") static let networkProtectionStatusMenuOpenDuckDuckGo = NSLocalizedString("network.protection.status.menu.vpn.open-duckduckgo", value: "Open DuckDuckGo…", comment: "The status menu 'Open DuckDuckGo' menu item") - static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Share Feedback…", comment: "The status menu 'Share Feedback' menu item") + static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Share VPN Feedback…", comment: "The status menu 'Share VPN Feedback' menu item") } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Colors/SecondaryColor.colorset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Colors/SecondaryColor.colorset/Contents.json index 6649fc47e0..8f7e96555c 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Colors/SecondaryColor.colorset/Contents.json +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Colors/SecondaryColor.colorset/Contents.json @@ -22,10 +22,10 @@ "color" : { "color-space" : "srgb", "components" : { - "alpha" : "0.500", - "blue" : "0x00", - "green" : "0x00", - "red" : "0x00" + "alpha" : "0.850", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" } }, "idiom" : "universal" From c1ad30963838559b38eacf178644ea8ef9994661 Mon Sep 17 00:00:00 2001 From: Anh Do Date: Thu, 18 Apr 2024 09:45:26 -0400 Subject: [PATCH 32/32] Fix dark mode colors --- .../Assets.xcassets/Icons/VPNDownload.imageset/Contents.json | 3 +++ .../Assets.xcassets/Icons/VPNUpload.imageset/Contents.json | 3 +++ .../Views/TunnelControllerView/TunnelControllerView.swift | 3 +++ 3 files changed, 9 insertions(+) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/Contents.json index ef66f63922..e030aab433 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/Contents.json +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/Contents.json @@ -8,5 +8,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/Contents.json index 410f073223..c7fec6a1a8 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/Contents.json +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/Contents.json @@ -8,5 +8,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift index e3bc424f6a..c4ede6adeb 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -263,6 +263,7 @@ public struct TunnelControllerView: View { .frame(width: 16, height: 16) } else { Image(NetworkProtectionAsset.nearestAvailable) + .renderingMode(colorScheme == .light ? .original : .template) .frame(width: 16, height: 16) } } @@ -365,10 +366,12 @@ public struct TunnelControllerView: View { Group { Image(NetworkProtectionAsset.dataReceived) + .renderingMode(colorScheme == .light ? .original : .template) .frame(width: 12, height: 12) Text(dataVolume.dataReceived) .applyDataVolumeAttributes(colorScheme: colorScheme) Image(NetworkProtectionAsset.dataSent) + .renderingMode(colorScheme == .light ? .original : .template) .frame(width: 12, height: 12) .padding(.leading, 4) Text(dataVolume.dataSent)