From 0de4dd6ef4b41a649e2b2def80da11a031800c84 Mon Sep 17 00:00:00 2001 From: jack Date: Sat, 25 Jul 2026 21:41:59 +0200 Subject: [PATCH] Prevent Bluetooth alerts from competing with sheets --- bitchat/Views/ContentSheetViews.swift | 32 +++++++++++++----- bitchat/Views/ContentView.swift | 48 +++++++++++++++++++++------ bitchatTests/ViewSmokeTests.swift | 25 ++++++++++++++ 3 files changed, 87 insertions(+), 18 deletions(-) diff --git a/bitchat/Views/ContentSheetViews.swift b/bitchat/Views/ContentSheetViews.swift index 49d069c1..898fc625 100644 --- a/bitchat/Views/ContentSheetViews.swift +++ b/bitchat/Views/ContentSheetViews.swift @@ -6,6 +6,18 @@ import UIKit import AppKit #endif +struct ContentPeopleSheetModalPresentationState { + var isImagePreviewPresented = false + var isVerificationSheetPresented = false + var isMediaPickerPresented = false + + var hasPresentation: Bool { + isImagePreviewPresented + || isVerificationSheetPresented + || isMediaPickerPresented + } +} + struct ContentPeopleSheetView: View { @EnvironmentObject private var appChromeModel: AppChromeModel @EnvironmentObject private var privateConversationModel: PrivateConversationModel @@ -24,6 +36,7 @@ struct ContentPeopleSheetView: View { var isTextFieldFocused: FocusState.Binding @ObservedObject var voiceRecordingVM: VoiceRecordingViewModel @Binding var autocompleteDebounceTimer: Timer? + @State private var showVerifySheet = false @ThemedPalette private var palette let headerHeight: CGFloat @@ -37,14 +50,17 @@ struct ContentPeopleSheetView: View { #endif private var hasModalPresentation: Bool { - if imagePreviewURL != nil { - return true - } #if os(iOS) - return showImagePicker + let isMediaPickerPresented = showImagePicker #else - return showMacImagePicker + let isMediaPickerPresented = showMacImagePicker #endif + + return ContentPeopleSheetModalPresentationState( + isImagePreviewPresented: imagePreviewURL != nil, + isVerificationSheetPresented: showVerifySheet, + isMediaPickerPresented: isMediaPickerPresented + ).hasPresentation } private var bluetoothAlertBinding: Binding { @@ -108,7 +124,8 @@ struct ContentPeopleSheetView: View { #endif } else { ContentPeopleListView( - showSidebar: $showSidebar + showSidebar: $showSidebar, + showVerifySheet: $showVerifySheet ) } } @@ -237,8 +254,7 @@ private struct ContentPeopleListView: View { @ThemedPalette private var palette @Binding var showSidebar: Bool - - @State private var showVerifySheet = false + @Binding var showVerifySheet: Bool var body: some View { VStack(spacing: 0) { diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 596ffd5e..f4674f64 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -14,6 +14,30 @@ import AppKit #endif import BitFoundation +struct ContentRootModalPresentationState { + var isPeopleSheetPresented = false + var isAppInfoPresented = false + var isFingerprintPresented = false + var isLocationChannelsSheetPresented = false + var isNoticesSheetPresented = false + var isImagePreviewPresented = false + var isVerificationSheetPresented = false + var isVoiceAlertPresented = false + var isMediaPickerPresented = false + + var hasPresentation: Bool { + isPeopleSheetPresented + || isAppInfoPresented + || isFingerprintPresented + || isLocationChannelsSheetPresented + || isNoticesSheetPresented + || isImagePreviewPresented + || isVerificationSheetPresented + || isVoiceAlertPresented + || isMediaPickerPresented + } +} + /// On macOS 14+, disables the default system focus ring on TextFields. /// On earlier macOS versions and on iOS this is a no-op. struct FocusEffectDisabledModifier: ViewModifier { @@ -77,19 +101,23 @@ struct ContentView: View { } private var hasRootModalPresentation: Bool { - if isPeopleSheetPresented - || appChromeModel.isAppInfoPresented - || appChromeModel.showingFingerprintFor != nil - || imagePreviewURL != nil - || showVerifySheet - || voiceRecordingVM.showAlert { - return true - } #if os(iOS) - return showImagePicker + let isMediaPickerPresented = showImagePicker #else - return showMacImagePicker + let isMediaPickerPresented = showMacImagePicker #endif + + return ContentRootModalPresentationState( + isPeopleSheetPresented: isPeopleSheetPresented, + isAppInfoPresented: appChromeModel.isAppInfoPresented, + isFingerprintPresented: appChromeModel.showingFingerprintFor != nil, + isLocationChannelsSheetPresented: appChromeModel.isLocationChannelsSheetPresented, + isNoticesSheetPresented: appChromeModel.isNoticesSheetPresented, + isImagePreviewPresented: imagePreviewURL != nil, + isVerificationSheetPresented: showVerifySheet, + isVoiceAlertPresented: voiceRecordingVM.showAlert, + isMediaPickerPresented: isMediaPickerPresented + ).hasPresentation } private var rootBluetoothAlertBinding: Binding { diff --git a/bitchatTests/ViewSmokeTests.swift b/bitchatTests/ViewSmokeTests.swift index 6c104e53..591d5b04 100644 --- a/bitchatTests/ViewSmokeTests.swift +++ b/bitchatTests/ViewSmokeTests.swift @@ -551,6 +551,31 @@ struct ViewSmokeTests { #expect(featureModels.privateConversationModel.selectedHeaderState?.headerPeerID == peerID) } + @Test("Root Bluetooth alert waits for location and notices sheets") + func rootBluetoothAlertGuard_includesHeaderSheets() { + #expect(!ContentRootModalPresentationState().hasPresentation) + #expect( + ContentRootModalPresentationState( + isLocationChannelsSheetPresented: true + ).hasPresentation + ) + #expect( + ContentRootModalPresentationState( + isNoticesSheetPresented: true + ).hasPresentation + ) + } + + @Test("People-sheet Bluetooth alert waits for local verification sheet") + func peopleSheetBluetoothAlertGuard_includesVerificationSheet() { + #expect(!ContentPeopleSheetModalPresentationState().hasPresentation) + #expect( + ContentPeopleSheetModalPresentationState( + isVerificationSheetPresented: true + ).hasPresentation + ) + } + @Test func geohashAndTextMessageViews_renderCoreBranches() { let (viewModel, _, _) = makeSmokeViewModel()