Skip to content
This repository has been archived by the owner on Feb 24, 2025. It is now read-only.

Commit

Permalink
VPN screen improvements (#2550)
Browse files Browse the repository at this point in the history
  • Loading branch information
quanganhdo authored Apr 19, 2024
1 parent 58bfb4c commit b59493f
Show file tree
Hide file tree
Showing 56 changed files with 1,026 additions and 107 deletions.
80 changes: 79 additions & 1 deletion DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/BrowserServicesKit",
"state" : {
"revision" : "4ce049682cb47a9fb510237070666e5e8bf1e07b",
"version" : "137.0.0"
"revision" : "b8f0e5db431c63943b509d522c157f870ef03ae0",
"version" : "138.0.0"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGo/Application/URLEventHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,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()
Expand Down
15 changes: 12 additions & 3 deletions DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
Expand Down Expand Up @@ -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."

Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion DuckDuckGo/MainWindow/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ final class MainViewController: NSViewController {
serverInfoObserver: ipcClient.ipcServerInfoObserver,
connectionErrorObserver: ipcClient.ipcConnectionErrorObserver,
connectivityIssuesObserver: connectivityIssuesObserver,
controllerErrorMessageObserver: controllerErrorMessageObserver
controllerErrorMessageObserver: controllerErrorMessageObserver,
dataVolumeObserver: ipcClient.ipcDataVolumeObserver
)
}()

Expand Down
6 changes: 6 additions & 0 deletions DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ final class IPCClientMock: NetworkProtectionIPCClient {
}
var ipcControllerErrorMessageObserver: any NetworkProtection.ControllerErrorMesssageObserver = ControllerErrorMesssageObserverMock()

final class DataVolumeObserverMock: NetworkProtection.DataVolumeObserver {
var publisher: AnyPublisher<DataVolume, Never> = PassthroughSubject().eraseToAnyPublisher()
var recentValue: DataVolume = .init()
}
var ipcDataVolumeObserver: any NetworkProtection.DataVolumeObserver = DataVolumeObserverMock()

func start(completion: @escaping (Error?) -> Void) {
completion(nil)
}
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ protocol NetworkProtectionIPCClient {
var ipcStatusObserver: ConnectionStatusObserver { get }
var ipcServerInfoObserver: ConnectionServerInfoObserver { get }
var ipcConnectionErrorObserver: ConnectionErrorObserver { get }
var ipcDataVolumeObserver: DataVolumeObserver { get }

func start(completion: @escaping (Error?) -> Void)
func stop(completion: @escaping (Error?) -> Void)
Expand All @@ -38,6 +39,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 {
Expand All @@ -57,7 +59,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager {

// swiftlint:disable:next function_body_length
func show(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) {
let popover = networkProtectionPopover ?? {
let popover = {

let controller = NetworkProtectionIPCTunnelController(ipcClient: ipcClient)

Expand All @@ -66,7 +68,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
Expand Down Expand Up @@ -111,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)
})
Expand All @@ -132,6 +136,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager {
func toggle(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) {
if let networkProtectionPopover, networkProtectionPopover.isShown {
networkProtectionPopover.close()
self.networkProtectionPopover = nil
} else {
let featureVisibility = DefaultNetworkProtectionVisibility()

Expand All @@ -145,5 +150,6 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager {

func close() {
networkProtectionPopover?.close()
networkProtectionPopover = nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//
// 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 SwiftUI
import NetworkProtection

struct DefaultVPNLocationFormatter: VPNLocationFormatting {
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?,
preferredLocation someLocation: VPNSettings.SelectedLocation) -> String {
let preferredLocation = VPNLocationModel(selectedLocation: someLocation)

if let location {
return preferredLocation.isNearest ? "\(location) (Nearest)" : location
}

return preferredLocation.title
}

@available(macOS 12, *)
func string(from location: String?,
preferredLocation someLocation: VPNSettings.SelectedLocation,
locationTextColor: Color,
preferredLocationTextColor: Color) -> AttributedString {
let preferredLocation = VPNLocationModel(selectedLocation: someLocation)

if let location {
var attributedString = AttributedString(
preferredLocation.isNearest ? "\(location) \(UserText.netPVPNLocationNearest)" : location
)
attributedString.foregroundColor = locationTextColor
if let range = attributedString.range(of: UserText.netPVPNLocationNearest) {
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 icon: LocationIcon
let isNearest: Bool

init(selectedLocation: VPNSettings.SelectedLocation) {
switch selectedLocation {
case .nearest:
title = UserText.vpnLocationNearestAvailable
icon = .defaultIcon
isNearest = true
case .location(let location):
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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
3 changes: 2 additions & 1 deletion DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGo/Windows/View/WindowControllersManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions DuckDuckGoDBPBackgroundAgent/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 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")
}
15 changes: 11 additions & 4 deletions DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,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
)
}()

Expand Down Expand Up @@ -242,17 +248,18 @@ 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,
isMenuBarStatusView: true,
userDefaults: .netP,
locationFormatter: DefaultVPNLocationFormatter(),
uninstallHandler: { [weak self] in
guard let self else { return }
await self.vpnUninstaller.uninstall(includingSystemExtension: true)
Expand Down
10 changes: 10 additions & 0 deletions DuckDuckGoVPN/TunnelControllerIPCService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ final class TunnelControllerIPCService {
subscribeToErrorChanges()
subscribeToStatusUpdates()
subscribeToServerChanges()
subscribeToDataVolumeUpdates()

server.serverDelegate = self
}
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGoVPN/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 VPN Feedback…", comment: "The status menu 'Share VPN Feedback' menu item")
}
2 changes: 1 addition & 1 deletion LocalPackages/DataBrokerProtection/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ let package = Package(
targets: ["DataBrokerProtection"])
],
dependencies: [
.package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "137.0.0"),
.package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.0.0"),
.package(path: "../PixelKit"),
.package(path: "../SwiftUIExtensions"),
.package(path: "../XPCHelper"),
Expand Down
2 changes: 1 addition & 1 deletion LocalPackages/NetworkProtectionMac/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ let package = Package(
.library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]),
],
dependencies: [
.package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "137.0.0"),
.package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.0.0"),
.package(path: "../XPCHelper"),
.package(path: "../SwiftUIExtensions"),
.package(path: "../LoginItems"),
Expand Down
Loading

0 comments on commit b59493f

Please sign in to comment.