From cd4271e510a46b200b4f78ac0cff0c0e847e1a5f Mon Sep 17 00:00:00 2001 From: jack Date: Sat, 25 Jul 2026 22:02:13 +0200 Subject: [PATCH] Prevent Bluetooth alerts from interrupting modal warnings --- bitchat/Views/ContentSheetViews.swift | 4 ++ bitchat/Views/ContentView.swift | 36 +++++++++++++++-- bitchatTests/ViewSmokeTests.swift | 58 +++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/bitchat/Views/ContentSheetViews.swift b/bitchat/Views/ContentSheetViews.swift index 898fc625..f5e1058c 100644 --- a/bitchat/Views/ContentSheetViews.swift +++ b/bitchat/Views/ContentSheetViews.swift @@ -9,11 +9,13 @@ import AppKit struct ContentPeopleSheetModalPresentationState { var isImagePreviewPresented = false var isVerificationSheetPresented = false + var legacyPrivateMediaConsentRequest: LegacyPrivateMediaConsentRequest? = nil var isMediaPickerPresented = false var hasPresentation: Bool { isImagePreviewPresented || isVerificationSheetPresented + || legacyPrivateMediaConsentRequest != nil || isMediaPickerPresented } } @@ -59,6 +61,8 @@ struct ContentPeopleSheetView: View { return ContentPeopleSheetModalPresentationState( isImagePreviewPresented: imagePreviewURL != nil, isVerificationSheetPresented: showVerifySheet, + legacyPrivateMediaConsentRequest: + conversationUIModel.legacyPrivateMediaConsentRequest, isMediaPickerPresented: isMediaPickerPresented ).hasPresentation } diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index f4674f64..4ae4753c 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -23,6 +23,7 @@ struct ContentRootModalPresentationState { var isImagePreviewPresented = false var isVerificationSheetPresented = false var isVoiceAlertPresented = false + var isScreenshotPrivacyAlertPresented = false var isMediaPickerPresented = false var hasPresentation: Bool { @@ -34,10 +35,40 @@ struct ContentRootModalPresentationState { || isImagePreviewPresented || isVerificationSheetPresented || isVoiceAlertPresented + || isScreenshotPrivacyAlertPresented || isMediaPickerPresented } } +extension ContentRootModalPresentationState { + @MainActor + init( + appChromeModel: AppChromeModel, + isPeopleSheetPresented: Bool = false, + isImagePreviewPresented: Bool = false, + isVerificationSheetPresented: Bool = false, + isVoiceAlertPresented: Bool = false, + isMediaPickerPresented: Bool = false + ) { + self.init( + isPeopleSheetPresented: isPeopleSheetPresented, + isAppInfoPresented: appChromeModel.isAppInfoPresented, + isFingerprintPresented: + appChromeModel.showingFingerprintFor != nil, + isLocationChannelsSheetPresented: + appChromeModel.isLocationChannelsSheetPresented, + isNoticesSheetPresented: + appChromeModel.isNoticesSheetPresented, + isImagePreviewPresented: isImagePreviewPresented, + isVerificationSheetPresented: isVerificationSheetPresented, + isVoiceAlertPresented: isVoiceAlertPresented, + isScreenshotPrivacyAlertPresented: + appChromeModel.showScreenshotPrivacyWarning, + isMediaPickerPresented: 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 { @@ -108,11 +139,8 @@ struct ContentView: View { #endif return ContentRootModalPresentationState( + appChromeModel: appChromeModel, isPeopleSheetPresented: isPeopleSheetPresented, - isAppInfoPresented: appChromeModel.isAppInfoPresented, - isFingerprintPresented: appChromeModel.showingFingerprintFor != nil, - isLocationChannelsSheetPresented: appChromeModel.isLocationChannelsSheetPresented, - isNoticesSheetPresented: appChromeModel.isNoticesSheetPresented, isImagePreviewPresented: imagePreviewURL != nil, isVerificationSheetPresented: showVerifySheet, isVoiceAlertPresented: voiceRecordingVM.showAlert, diff --git a/bitchatTests/ViewSmokeTests.swift b/bitchatTests/ViewSmokeTests.swift index 591d5b04..b2317057 100644 --- a/bitchatTests/ViewSmokeTests.swift +++ b/bitchatTests/ViewSmokeTests.swift @@ -576,6 +576,64 @@ struct ViewSmokeTests { ) } + @Test("Root Bluetooth alert waits for screenshot privacy alert") + @MainActor + func rootBluetoothAlertGuard_tracksScreenshotPrivacyState() { + let (viewModel, _, _) = makeSmokeViewModel() + let featureModels = makeSmokeFeatureModels(for: viewModel) + + #expect( + !ContentRootModalPresentationState( + appChromeModel: featureModels.appChromeModel + ).hasPresentation + ) + + featureModels.appChromeModel.showScreenshotPrivacyWarning = true + + #expect( + ContentRootModalPresentationState( + appChromeModel: featureModels.appChromeModel + ).hasPresentation + ) + } + + @Test("People-sheet Bluetooth alert waits for legacy media consent") + @MainActor + func peopleSheetBluetoothAlertGuard_tracksLegacyConsentState() async { + let (viewModel, _, _) = makeSmokeViewModel() + let featureModels = makeSmokeFeatureModels(for: viewModel) + + #expect( + !ContentPeopleSheetModalPresentationState( + legacyPrivateMediaConsentRequest: + featureModels.conversationUIModel + .legacyPrivateMediaConsentRequest + ).hasPresentation + ) + + viewModel.enqueueLegacyPrivateMediaConsent( + for: PeerID(str: "5152535455565758"), + transferId: "legacy-consent-transfer", + messageID: "legacy-consent-message" + ) { _ in } + defer { viewModel.cancelAllLegacyPrivateMediaConsents() } + + let consentPropagated = await TestHelpers.waitUntil { + featureModels.conversationUIModel + .legacyPrivateMediaConsentRequest != nil + } + #expect( + consentPropagated + ) + #expect( + ContentPeopleSheetModalPresentationState( + legacyPrivateMediaConsentRequest: + featureModels.conversationUIModel + .legacyPrivateMediaConsentRequest + ).hasPresentation + ) + } + @Test func geohashAndTextMessageViews_renderCoreBranches() { let (viewModel, _, _) = makeSmokeViewModel()