mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 19:25:23 +00:00
Keep recording-error alerts from ending the open DM
Voice recording happens inside the people/DM sheet, but its error alert was still presented unconditionally from the root view. A mic-permission failure therefore force-dismissed the sheet and ended the conversation through exactly the reconciliation path this branch fixes for the Bluetooth alert. Treat it the same way: the sheet-dismiss guard now also ignores reconciliation while the voice alert is up, the root copy of the alert defers to any other root modal (people sheet included), and the sheet presents its own copy gated on the sheet's modal state. The voice alert keeps priority over the Bluetooth alert on both levels via the existing isVoiceAlertPresented flag, now mirrored into the people-sheet state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,12 +10,14 @@ struct ContentPeopleSheetModalPresentationState {
|
||||
var isImagePreviewPresented = false
|
||||
var isVerificationSheetPresented = false
|
||||
var legacyPrivateMediaConsentRequest: LegacyPrivateMediaConsentRequest? = nil
|
||||
var isVoiceAlertPresented = false
|
||||
var isMediaPickerPresented = false
|
||||
|
||||
var hasPresentation: Bool {
|
||||
isImagePreviewPresented
|
||||
|| isVerificationSheetPresented
|
||||
|| legacyPrivateMediaConsentRequest != nil
|
||||
|| isVoiceAlertPresented
|
||||
|| isMediaPickerPresented
|
||||
}
|
||||
}
|
||||
@@ -51,7 +53,9 @@ struct ContentPeopleSheetView: View {
|
||||
@Binding var showMacImagePicker: Bool
|
||||
#endif
|
||||
|
||||
private var hasModalPresentation: Bool {
|
||||
private func modalPresentationState(
|
||||
includingVoiceAlert: Bool
|
||||
) -> ContentPeopleSheetModalPresentationState {
|
||||
#if os(iOS)
|
||||
let isMediaPickerPresented = showImagePicker
|
||||
#else
|
||||
@@ -63,8 +67,19 @@ struct ContentPeopleSheetView: View {
|
||||
isVerificationSheetPresented: showVerifySheet,
|
||||
legacyPrivateMediaConsentRequest:
|
||||
conversationUIModel.legacyPrivateMediaConsentRequest,
|
||||
isVoiceAlertPresented: includingVoiceAlert && voiceRecordingVM.showAlert,
|
||||
isMediaPickerPresented: isMediaPickerPresented
|
||||
).hasPresentation
|
||||
)
|
||||
}
|
||||
|
||||
private var hasModalPresentation: Bool {
|
||||
modalPresentationState(includingVoiceAlert: true).hasPresentation
|
||||
}
|
||||
|
||||
/// The voice alert cannot defer to itself: its own binding must keep
|
||||
/// reporting `true` while it is the presented modal.
|
||||
private var hasModalPresentationBesidesVoiceAlert: Bool {
|
||||
modalPresentationState(includingVoiceAlert: false).hasPresentation
|
||||
}
|
||||
|
||||
private var bluetoothAlertBinding: Binding<Bool> {
|
||||
@@ -85,6 +100,28 @@ struct ContentPeopleSheetView: View {
|
||||
)
|
||||
}
|
||||
|
||||
/// Voice recording happens inside this sheet, so its error alert must
|
||||
/// present from here as well: the root copy defers whenever this sheet
|
||||
/// is up, exactly like the Bluetooth alert above. Presenting from the
|
||||
/// root instead would force-dismiss the sheet and end the conversation.
|
||||
private var voiceAlertBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: {
|
||||
scenePhase == .active
|
||||
&& voiceRecordingVM.showAlert
|
||||
&& !hasModalPresentationBesidesVoiceAlert
|
||||
},
|
||||
set: { isPresented in
|
||||
guard !isPresented,
|
||||
scenePhase == .active,
|
||||
!hasModalPresentationBesidesVoiceAlert else {
|
||||
return
|
||||
}
|
||||
voiceRecordingVM.showAlert = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let legacyConsentRequest = conversationUIModel.legacyPrivateMediaConsentRequest
|
||||
NavigationStack {
|
||||
@@ -233,6 +270,16 @@ struct ContentPeopleSheetView: View {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
.alert("Recording Error", isPresented: voiceAlertBinding, actions: {
|
||||
Button("common.ok", role: .cancel) {}
|
||||
if voiceRecordingVM.state == .permissionDenied {
|
||||
Button("location_channels.action.open_settings") {
|
||||
SystemSettings.microphone.open()
|
||||
}
|
||||
}
|
||||
}, message: {
|
||||
Text(voiceRecordingVM.state.alertMessage)
|
||||
})
|
||||
.alert(
|
||||
"content.alert.bluetooth_required.title",
|
||||
isPresented: bluetoothAlertBinding
|
||||
|
||||
@@ -140,7 +140,9 @@ struct ContentView: View {
|
||||
showSidebar || selectedPrivatePeerID != nil
|
||||
}
|
||||
|
||||
private var hasRootModalPresentation: Bool {
|
||||
private func rootModalPresentationState(
|
||||
includingVoiceAlert: Bool
|
||||
) -> ContentRootModalPresentationState {
|
||||
#if os(iOS)
|
||||
let isMediaPickerPresented = showImagePicker
|
||||
#else
|
||||
@@ -152,9 +154,19 @@ struct ContentView: View {
|
||||
isPeopleSheetPresented: isPeopleSheetPresented,
|
||||
isImagePreviewPresented: imagePreviewURL != nil,
|
||||
isVerificationSheetPresented: showVerifySheet,
|
||||
isVoiceAlertPresented: voiceRecordingVM.showAlert,
|
||||
isVoiceAlertPresented: includingVoiceAlert && voiceRecordingVM.showAlert,
|
||||
isMediaPickerPresented: isMediaPickerPresented
|
||||
).hasPresentation
|
||||
)
|
||||
}
|
||||
|
||||
private var hasRootModalPresentation: Bool {
|
||||
rootModalPresentationState(includingVoiceAlert: true).hasPresentation
|
||||
}
|
||||
|
||||
/// The voice alert cannot defer to itself: its own binding must keep
|
||||
/// reporting `true` while it is the presented modal.
|
||||
private var hasRootModalPresentationBesidesVoiceAlert: Bool {
|
||||
rootModalPresentationState(includingVoiceAlert: false).hasPresentation
|
||||
}
|
||||
|
||||
private var rootBluetoothAlertBinding: Binding<Bool> {
|
||||
@@ -175,6 +187,29 @@ struct ContentView: View {
|
||||
)
|
||||
}
|
||||
|
||||
/// Voice recording errors can surface while the people/DM sheet is up
|
||||
/// (recording happens inside the sheet). Presenting the root alert then
|
||||
/// would force-dismiss the sheet, so the root copy defers to any other
|
||||
/// root modal; the sheet presents its own copy. Mirrors the Bluetooth
|
||||
/// alert treatment above.
|
||||
private var rootVoiceAlertBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: {
|
||||
scenePhase == .active
|
||||
&& voiceRecordingVM.showAlert
|
||||
&& !hasRootModalPresentationBesidesVoiceAlert
|
||||
},
|
||||
set: { isPresented in
|
||||
guard !isPresented,
|
||||
scenePhase == .active,
|
||||
!hasRootModalPresentationBesidesVoiceAlert else {
|
||||
return
|
||||
}
|
||||
voiceRecordingVM.showAlert = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
mainContent
|
||||
.onAppear {
|
||||
@@ -220,12 +255,14 @@ struct ContentView: View {
|
||||
set: { isPresented in
|
||||
if !isPresented {
|
||||
showSidebar = false
|
||||
// Scene/background and Bluetooth-alert presentation
|
||||
// reconciliation are not user requests to leave the
|
||||
// conversation. Keep the selected DM so the sheet
|
||||
// remains live when the app returns from Settings.
|
||||
// Scene/background and alert-presentation
|
||||
// reconciliation (Bluetooth-off, recording errors)
|
||||
// are not user requests to leave the conversation.
|
||||
// Keep the selected DM so the sheet remains live
|
||||
// when the app returns from Settings.
|
||||
if scenePhase == .active,
|
||||
!appChromeModel.showBluetoothAlert {
|
||||
!appChromeModel.showBluetoothAlert,
|
||||
!voiceRecordingVM.showAlert {
|
||||
privateConversationModel.endConversation()
|
||||
}
|
||||
}
|
||||
@@ -328,7 +365,7 @@ struct ContentView: View {
|
||||
ImagePreviewView(url: url)
|
||||
}
|
||||
}
|
||||
.alert("Recording Error", isPresented: $voiceRecordingVM.showAlert, actions: {
|
||||
.alert("Recording Error", isPresented: rootVoiceAlertBinding, actions: {
|
||||
Button("common.ok", role: .cancel) {}
|
||||
if voiceRecordingVM.state == .permissionDenied {
|
||||
Button("location_channels.action.open_settings") {
|
||||
|
||||
@@ -579,6 +579,20 @@ struct ViewSmokeTests {
|
||||
)
|
||||
}
|
||||
|
||||
@Test("Bluetooth alerts wait for the voice recording error alert")
|
||||
func bluetoothAlertGuards_includeVoiceAlert() {
|
||||
#expect(
|
||||
ContentRootModalPresentationState(
|
||||
isVoiceAlertPresented: true
|
||||
).hasPresentation
|
||||
)
|
||||
#expect(
|
||||
ContentPeopleSheetModalPresentationState(
|
||||
isVoiceAlertPresented: true
|
||||
).hasPresentation
|
||||
)
|
||||
}
|
||||
|
||||
@Test("Root Bluetooth alert waits for screenshot privacy alert")
|
||||
@MainActor
|
||||
func rootBluetoothAlertGuard_tracksScreenshotPrivacyState() {
|
||||
|
||||
Reference in New Issue
Block a user