Skip to content

Commit

Permalink
Adds option + click support for our VPN menu to show some useful debu…
Browse files Browse the repository at this point in the history
…g information
  • Loading branch information
diegoreymendez committed Dec 22, 2023
1 parent 21845c7 commit 30dafb0
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// limitations under the License.
//

import Combine
import Foundation
import NetworkProtection
import NetworkProtectionIPC
Expand Down Expand Up @@ -61,7 +62,9 @@ final class NetworkProtectionNavBarPopoverManager {
let onboardingStatusPublisher = UserDefaults.netP.networkProtectionOnboardingStatusPublisher
_ = VPNSettings(defaults: .netP)

let popover = NetworkProtectionPopover(controller: controller, onboardingStatusPublisher: onboardingStatusPublisher, statusReporter: statusReporter) {
let popover = NetworkProtectionPopover(controller: controller,
onboardingStatusPublisher: onboardingStatusPublisher,
statusReporter: statusReporter) {
let menuItems = [
NetworkProtectionStatusView.Model.MenuItem(
name: UserText.networkProtectionNavBarStatusMenuVPNSettings, action: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ public final class StatusBarMenu: NSObject {
self.statusItem = statusItem
self.iconPublisher = NetworkProtectionIconPublisher(statusReporter: statusReporter, iconProvider: iconProvider)

popover = NetworkProtectionPopover(controller: controller, onboardingStatusPublisher: onboardingStatusPublisher, statusReporter: statusReporter, menuItems: menuItems)
popover = NetworkProtectionPopover(controller: controller,
onboardingStatusPublisher: onboardingStatusPublisher,
statusReporter: statusReporter,
menuItems: menuItems)
popover.behavior = .transient

super.init()
Expand All @@ -74,14 +77,15 @@ public final class StatusBarMenu: NSObject {

@objc
private func statusBarButtonTapped() {
let isOptionKeyPressed = NSApp.currentEvent?.modifierFlags.contains(.option) ?? false
let isRightClick = NSApp.currentEvent?.type == .rightMouseUp

guard !isRightClick else {
showContextMenu()
return
}

togglePopover()
togglePopover(isOptionKeyPressed: isOptionKeyPressed)
}

private func subscribeToIconUpdates() {
Expand All @@ -95,14 +99,15 @@ public final class StatusBarMenu: NSObject {

// MARK: - Popover

private func togglePopover() {
private func togglePopover(isOptionKeyPressed: Bool) {
if popover.isShown {
popover.close()
} else {
guard let button = statusItem.button else {
return
}

popover.setShowsDebugInformation(isOptionKeyPressed)
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//

import AppKit
import Combine
import Foundation
import SwiftUI
import NetworkProtection
Expand All @@ -43,6 +44,7 @@ public final class NetworkProtectionPopover: NSPopover {

public typealias MenuItem = NetworkProtectionStatusView.Model.MenuItem

private let debugInformationPublisher = CurrentValueSubject<Bool, Never>(false)
private let statusReporter: NetworkProtectionStatusReporter

public required init(controller: TunnelController,
Expand All @@ -57,7 +59,11 @@ public final class NetworkProtectionPopover: NSPopover {
self.animates = false
self.behavior = .semitransient

setupContentController(controller: controller, onboardingStatusPublisher: onboardingStatusPublisher, statusReporter: statusReporter, menuItems: menuItems)
setupContentController(controller: controller,
onboardingStatusPublisher: onboardingStatusPublisher,
statusReporter: statusReporter,
debugInformationPublisher: debugInformationPublisher.eraseToAnyPublisher(),
menuItems: menuItems)
}

required init?(coder: NSCoder) {
Expand All @@ -67,11 +73,13 @@ public final class NetworkProtectionPopover: NSPopover {
private func setupContentController(controller: TunnelController,
onboardingStatusPublisher: OnboardingStatusPublisher,
statusReporter: NetworkProtectionStatusReporter,
debugInformationPublisher: AnyPublisher<Bool, Never>,
menuItems: @escaping () -> [MenuItem]) {

let model = NetworkProtectionStatusView.Model(controller: controller,
onboardingStatusPublisher: onboardingStatusPublisher,
statusReporter: statusReporter,
debugInformationPublisher: debugInformationPublisher,
menuItems: menuItems)

let view = NetworkProtectionStatusView(model: model).environment(\.dismiss, { [weak self] in
Expand All @@ -93,4 +101,10 @@ public final class NetworkProtectionPopover: NSPopover {
statusReporter.forceRefresh()
super.show(relativeTo: positioningRect, of: positioningView, preferredEdge: preferredEdge)
}

// MARK: - Debug Information

func setShowsDebugInformation(_ showsDebugInformation: Bool) {
debugInformationPublisher.send(showsDebugInformation)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// DebugInformationView.swift
//
// Copyright © 2023 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 SwiftUI
import SwiftUIExtensions
import Combine
import NetworkProtection

public struct DebugInformationView: View {

@Environment(\.colorScheme) private var colorScheme
@Environment(\.isEnabled) private var isEnabled
@Environment(\.dismiss) private var dismiss

// MARK: - Model

/// The view model that this instance will use.
///
@ObservedObject var model: DebugInformationViewModel

// MARK: - Initializers

public init(model: DebugInformationViewModel) {
self.model = model
}

// MARK: - View Contents

public var body: some View {
Group {
VStack(alignment: .leading, spacing: 0) {
informationRow(title: "Bundle Path", details: model.bundlePath)
informationRow(title: "Version", details: model.version)
}

Divider()
.padding(EdgeInsets(top: 5, leading: 9, bottom: 5, trailing: 9))
}
}

// MARK: - Composite Views

private func informationRow(title: String, details: String) -> some View {
VStack(spacing: 2) {
HStack {
Text(title)
.padding(.leading, 24)
.opacity(0.6)
.font(.system(size: 12, weight: .bold, design: .default))
.fixedSize()

Spacer()
}

HStack {
Text(details)
.makeSelectable()
.multilineText()
.padding(.leading, 24)
.opacity(0.6)
.font(.system(size: 12, weight: .regular, design: .default))

Spacer()
}
}
.padding(EdgeInsets(top: 4, leading: 10, bottom: 4, trailing: 9))
}

// MARK: - Rows

private func dividerRow() -> some View {
Divider()
.padding(EdgeInsets(top: 5, leading: 9, bottom: 5, trailing: 9))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// DebugInformationViewModel.swift
//
// Copyright © 2023 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
import SwiftUI

@MainActor
public final class DebugInformationViewModel: ObservableObject {

var bundlePath: String
var version: String

// MARK: - Initialization & Deinitialization

public init(bundle: Bundle = .main) {
bundlePath = bundle.bundlePath

// swiftlint:disable:next force_cast
let shortVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String

// swiftlint:disable:next force_cast
let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String

version = shortVersion + " (build: " + buildNumber + ")"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ public struct NetworkProtectionStatusView: View {
TunnelControllerView(model: model.tunnelControllerViewModel)
.disabled(model.tunnelControllerViewDisabled)

if model.showDebugInformation {
DebugInformationView(model: DebugInformationViewModel())
}

bottomMenuView()
}
.padding(5)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ extension NetworkProtectionStatusView {
///
private let statusReporter: NetworkProtectionStatusReporter

/// The debug information publisher
///
private let debugInformationPublisher: AnyPublisher<Bool, Never>

/// Whether we're showing debug information
///
@Published
var showDebugInformation: Bool

// MARK: - Extra Menu Items

public let menuItems: () -> [MenuItem]
Expand All @@ -90,12 +99,14 @@ extension NetworkProtectionStatusView {
public init(controller: TunnelController,
onboardingStatusPublisher: OnboardingStatusPublisher,
statusReporter: NetworkProtectionStatusReporter,
debugInformationPublisher: AnyPublisher<Bool, Never>,
menuItems: @escaping () -> [MenuItem],
runLoopMode: RunLoop.Mode? = nil) {

self.tunnelController = controller
self.onboardingStatusPublisher = onboardingStatusPublisher
self.statusReporter = statusReporter
self.debugInformationPublisher = debugInformationPublisher
self.menuItems = menuItems
self.runLoopMode = runLoopMode

Expand All @@ -107,12 +118,14 @@ extension NetworkProtectionStatusView {
isHavingConnectivityIssues = statusReporter.connectivityIssuesObserver.recentValue
lastTunnelErrorMessage = statusReporter.connectionErrorObserver.recentValue
lastControllerErrorMessage = statusReporter.controllerErrorMessageObserver.recentValue
showDebugInformation = false

// Particularly useful when unit testing with an initial status of our choosing.
subscribeToStatusChanges()
subscribeToConnectivityIssues()
subscribeToTunnelErrorMessages()
subscribeToControllerErrorMessages()
subscribeToDebugInformationChanges()

onboardingStatusPublisher
.receive(on: DispatchQueue.main)
Expand Down Expand Up @@ -166,6 +179,14 @@ extension NetworkProtectionStatusView {
}.store(in: &cancellables)
}

private func subscribeToDebugInformationChanges() {
debugInformationPublisher
.removeDuplicates()
.receive(on: DispatchQueue.main)
.assign(to: \.showDebugInformation, onWeaklyHeld: self)
.store(in: &cancellables)
}

// MARK: - Connection Status: Errors

@Published
Expand Down

0 comments on commit 30dafb0

Please sign in to comment.