mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-11-01 07:53:26 +00:00
Review fixes: banner in the sheet, real fix targets, tor-clear tests
- The people/DM sheet covers the root header, so the connectivity banner vanished exactly where people sit longest. It now mirrors into the sheet via a top safe-area inset over both the people list and the DM view. - "tap to fix" for powered-off Bluetooth opened the macOS privacy permission pane, where an already-authorized person can't fix anything. New SystemSettings.bluetoothPower routes to the Bluetooth pane; denied still goes to the privacy anchor. The one-shot alert's settings button routes by state the same way. - Tests pin that torBlocked clears on tor-ready and on preference change (no stale "tor can't connect" after toggling tor off). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
09b3f294ac
commit
12b55afdad
@ -14,17 +14,22 @@ import AppKit
|
||||
|
||||
enum SystemSettings {
|
||||
case bluetooth
|
||||
/// The radio power toggle, distinct from the `.bluetooth` privacy
|
||||
/// permission anchor: an already-authorized person whose radio is
|
||||
/// switched off can't fix anything from the privacy pane.
|
||||
case bluetoothPower
|
||||
case location
|
||||
case microphone
|
||||
|
||||
#if os(macOS)
|
||||
private static let baseURL = "x-apple.systempreferences:com.apple.preference.security"
|
||||
|
||||
private var macPrivacyAnchor: String {
|
||||
private var macURLString: String {
|
||||
switch self {
|
||||
case .bluetooth: "Privacy_Bluetooth"
|
||||
case .location: "Privacy_LocationServices"
|
||||
case .microphone: "Privacy_Microphone"
|
||||
case .bluetooth: "\(Self.baseURL)?Privacy_Bluetooth"
|
||||
case .bluetoothPower: "x-apple.systempreferences:com.apple.BluetoothSettings"
|
||||
case .location: "\(Self.baseURL)?Privacy_LocationServices"
|
||||
case .microphone: "\(Self.baseURL)?Privacy_Microphone"
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -35,8 +40,7 @@ enum SystemSettings {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
#elseif os(macOS)
|
||||
let urlString = "\(Self.baseURL)?\(macPrivacyAnchor)"
|
||||
if let url = URL(string: urlString) {
|
||||
if let url = URL(string: macURLString) {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -73,19 +73,22 @@ struct ConnectivityStatusBanner: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bluetooth problems deep-link to settings; the tor stall has no
|
||||
/// in-app remedy (the network is blocking it), so that banner is inert.
|
||||
private var opensSettings: Bool {
|
||||
/// Bluetooth problems deep-link to where the fix actually lives —
|
||||
/// powered-off goes to the radio controls, denied to the privacy
|
||||
/// permission pane. The tor stall has no in-app remedy (the network is
|
||||
/// blocking it), so that banner is inert.
|
||||
private var settingsDestination: SystemSettings? {
|
||||
switch issue {
|
||||
case .bluetoothOff, .bluetoothDenied: return true
|
||||
case .bluetoothUnsupported, .torBlocked: return false
|
||||
case .bluetoothOff: return .bluetoothPower
|
||||
case .bluetoothDenied: return .bluetooth
|
||||
case .bluetoothUnsupported, .torBlocked: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if opensSettings {
|
||||
Button(action: { SystemSettings.bluetooth.open() }) {
|
||||
if let destination = settingsDestination {
|
||||
Button(action: { destination.open() }) {
|
||||
label
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
@ -183,6 +183,18 @@ struct ContentPeopleSheetView: View {
|
||||
.environmentObject(verificationModel)
|
||||
}
|
||||
}
|
||||
// This sheet covers the root header where the connectivity
|
||||
// banner lives; a person sitting in the people list or a DM
|
||||
// would otherwise get no persistent signal that the radio is
|
||||
// off or tor is stalled. Mirror it here.
|
||||
.safeAreaInset(edge: .top, spacing: 0) {
|
||||
if let issue = ConnectivityIssue.resolve(
|
||||
bluetoothState: appChromeModel.bluetoothState,
|
||||
torBlocked: appChromeModel.torBlocked
|
||||
) {
|
||||
ConnectivityStatusBanner(issue: issue)
|
||||
}
|
||||
}
|
||||
}
|
||||
.themedSheetBackground()
|
||||
.foregroundColor(palette.primary)
|
||||
@ -285,7 +297,10 @@ struct ContentPeopleSheetView: View {
|
||||
isPresented: bluetoothAlertBinding
|
||||
) {
|
||||
Button("content.alert.bluetooth_required.settings") {
|
||||
SystemSettings.bluetooth.open()
|
||||
// Powered-off needs the radio controls, not the privacy pane.
|
||||
(appChromeModel.bluetoothState == .poweredOff
|
||||
? SystemSettings.bluetoothPower
|
||||
: SystemSettings.bluetooth).open()
|
||||
}
|
||||
Button("common.ok", role: .cancel) {}
|
||||
} message: {
|
||||
|
||||
@ -409,7 +409,10 @@ struct ContentView: View {
|
||||
})
|
||||
.alert("content.alert.bluetooth_required.title", isPresented: rootBluetoothAlertBinding) {
|
||||
Button("content.alert.bluetooth_required.settings") {
|
||||
SystemSettings.bluetooth.open()
|
||||
// Powered-off needs the radio controls, not the privacy pane.
|
||||
(appChromeModel.bluetoothState == .poweredOff
|
||||
? SystemSettings.bluetoothPower
|
||||
: SystemSettings.bluetooth).open()
|
||||
}
|
||||
Button("common.ok", role: .cancel) {}
|
||||
} message: {
|
||||
|
||||
@ -165,14 +165,33 @@ struct ChatViewModelTorTests {
|
||||
viewModel.torStatusAnnounced = true
|
||||
viewModel.torInitialReadyAnnounced = true
|
||||
viewModel.torRestartPending = true
|
||||
viewModel.torBlocked = true
|
||||
|
||||
// Action
|
||||
viewModel.handleTorPreferenceChanged(Notification(name: .init("test")))
|
||||
try? await Task.sleep(nanoseconds: 100_000_000)
|
||||
|
||||
// Assert: all flags reset
|
||||
// Assert: all flags reset — torBlocked drives the connectivity
|
||||
// banner; toggling tor off must not leave a stale "tor can't
|
||||
// connect" line for a network nobody is waiting on.
|
||||
#expect(!viewModel.torStatusAnnounced)
|
||||
#expect(!viewModel.torInitialReadyAnnounced)
|
||||
#expect(!viewModel.torRestartPending)
|
||||
#expect(!viewModel.torBlocked)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func handleTorDidBecomeReady_clearsBlockedBanner() async {
|
||||
let (viewModel, _) = makeTestableViewModel()
|
||||
|
||||
// Setup: a stalled bootstrap raised the banner, then tor got through.
|
||||
viewModel.torBlocked = true
|
||||
|
||||
// Action
|
||||
viewModel.handleTorDidBecomeReady()
|
||||
try? await Task.sleep(nanoseconds: 100_000_000)
|
||||
|
||||
// Assert
|
||||
#expect(!viewModel.torBlocked)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user