mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-27 03:05:21 +00:00
Keep open DMs alive across Bluetooth settings (#1460)
Presenting the root Bluetooth alert while the people/DM sheet was up forced SwiftUI to dismiss the sheet, whose binding setter called endConversation() — destroying the open DM. The root alert is now gated behind a modal-presentation guard (scene active, no competing modal), a second copy presents from inside the sheet, and the sheet-dismissal setter only ends the conversation when the dismissal is genuinely explicit. Includes review fix: the voice-recording error alert (mic permission denied inside an open DM) destroyed the DM through the exact same path — it now participates in the same guard and gating pattern at both root and sheet level.
This commit is contained in:
@@ -6,11 +6,28 @@ import UIKit
|
|||||||
import AppKit
|
import AppKit
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ContentPeopleSheetView: View {
|
struct ContentPeopleSheetView: View {
|
||||||
@EnvironmentObject private var appChromeModel: AppChromeModel
|
@EnvironmentObject private var appChromeModel: AppChromeModel
|
||||||
@EnvironmentObject private var privateConversationModel: PrivateConversationModel
|
@EnvironmentObject private var privateConversationModel: PrivateConversationModel
|
||||||
@EnvironmentObject private var verificationModel: VerificationModel
|
@EnvironmentObject private var verificationModel: VerificationModel
|
||||||
@EnvironmentObject private var conversationUIModel: ConversationUIModel
|
@EnvironmentObject private var conversationUIModel: ConversationUIModel
|
||||||
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
|
||||||
@Binding var showSidebar: Bool
|
@Binding var showSidebar: Bool
|
||||||
@Binding var messageText: String
|
@Binding var messageText: String
|
||||||
@@ -23,6 +40,7 @@ struct ContentPeopleSheetView: View {
|
|||||||
var isTextFieldFocused: FocusState<Bool>.Binding
|
var isTextFieldFocused: FocusState<Bool>.Binding
|
||||||
@ObservedObject var voiceRecordingVM: VoiceRecordingViewModel
|
@ObservedObject var voiceRecordingVM: VoiceRecordingViewModel
|
||||||
@Binding var autocompleteDebounceTimer: Timer?
|
@Binding var autocompleteDebounceTimer: Timer?
|
||||||
|
@State private var showVerifySheet = false
|
||||||
@ThemedPalette private var palette
|
@ThemedPalette private var palette
|
||||||
|
|
||||||
let headerHeight: CGFloat
|
let headerHeight: CGFloat
|
||||||
@@ -35,6 +53,75 @@ struct ContentPeopleSheetView: View {
|
|||||||
@Binding var showMacImagePicker: Bool
|
@Binding var showMacImagePicker: Bool
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
private func modalPresentationState(
|
||||||
|
includingVoiceAlert: Bool
|
||||||
|
) -> ContentPeopleSheetModalPresentationState {
|
||||||
|
#if os(iOS)
|
||||||
|
let isMediaPickerPresented = showImagePicker
|
||||||
|
#else
|
||||||
|
let isMediaPickerPresented = showMacImagePicker
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return ContentPeopleSheetModalPresentationState(
|
||||||
|
isImagePreviewPresented: imagePreviewURL != nil,
|
||||||
|
isVerificationSheetPresented: showVerifySheet,
|
||||||
|
legacyPrivateMediaConsentRequest:
|
||||||
|
conversationUIModel.legacyPrivateMediaConsentRequest,
|
||||||
|
isVoiceAlertPresented: includingVoiceAlert && voiceRecordingVM.showAlert,
|
||||||
|
isMediaPickerPresented: isMediaPickerPresented
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
Binding(
|
||||||
|
get: {
|
||||||
|
scenePhase == .active
|
||||||
|
&& appChromeModel.showBluetoothAlert
|
||||||
|
&& !hasModalPresentation
|
||||||
|
},
|
||||||
|
set: { isPresented in
|
||||||
|
guard !isPresented,
|
||||||
|
scenePhase == .active,
|
||||||
|
!hasModalPresentation else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
appChromeModel.showBluetoothAlert = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
var body: some View {
|
||||||
let legacyConsentRequest = conversationUIModel.legacyPrivateMediaConsentRequest
|
let legacyConsentRequest = conversationUIModel.legacyPrivateMediaConsentRequest
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
@@ -78,7 +165,8 @@ struct ContentPeopleSheetView: View {
|
|||||||
#endif
|
#endif
|
||||||
} else {
|
} else {
|
||||||
ContentPeopleListView(
|
ContentPeopleListView(
|
||||||
showSidebar: $showSidebar
|
showSidebar: $showSidebar,
|
||||||
|
showVerifySheet: $showVerifySheet
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,6 +270,27 @@ struct ContentPeopleSheetView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#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
|
||||||
|
) {
|
||||||
|
Button("content.alert.bluetooth_required.settings") {
|
||||||
|
SystemSettings.bluetooth.open()
|
||||||
|
}
|
||||||
|
Button("common.ok", role: .cancel) {}
|
||||||
|
} message: {
|
||||||
|
Text(appChromeModel.bluetoothAlertMessage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,8 +305,7 @@ private struct ContentPeopleListView: View {
|
|||||||
@ThemedPalette private var palette
|
@ThemedPalette private var palette
|
||||||
|
|
||||||
@Binding var showSidebar: Bool
|
@Binding var showSidebar: Bool
|
||||||
|
@Binding var showVerifySheet: Bool
|
||||||
@State private var showVerifySheet = false
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
|||||||
@@ -14,6 +14,61 @@ import AppKit
|
|||||||
#endif
|
#endif
|
||||||
import BitFoundation
|
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 isScreenshotPrivacyAlertPresented = false
|
||||||
|
var isMediaPickerPresented = false
|
||||||
|
|
||||||
|
var hasPresentation: Bool {
|
||||||
|
isPeopleSheetPresented
|
||||||
|
|| isAppInfoPresented
|
||||||
|
|| isFingerprintPresented
|
||||||
|
|| isLocationChannelsSheetPresented
|
||||||
|
|| isNoticesSheetPresented
|
||||||
|
|| 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 macOS 14+, disables the default system focus ring on TextFields.
|
||||||
/// On earlier macOS versions and on iOS this is a no-op.
|
/// On earlier macOS versions and on iOS this is a no-op.
|
||||||
struct FocusEffectDisabledModifier: ViewModifier {
|
struct FocusEffectDisabledModifier: ViewModifier {
|
||||||
@@ -43,6 +98,7 @@ struct ContentView: View {
|
|||||||
@FocusState private var isTextFieldFocused: Bool
|
@FocusState private var isTextFieldFocused: Bool
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
@Environment(\.appTheme) private var appTheme
|
@Environment(\.appTheme) private var appTheme
|
||||||
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
@State private var showSidebar = false
|
@State private var showSidebar = false
|
||||||
@State private var selectedMessageSender: String?
|
@State private var selectedMessageSender: String?
|
||||||
@State private var selectedMessageSenderID: PeerID?
|
@State private var selectedMessageSenderID: PeerID?
|
||||||
@@ -80,6 +136,80 @@ struct ContentView: View {
|
|||||||
|
|
||||||
private var usesGlassLayout: Bool { appTheme.usesGlassChrome }
|
private var usesGlassLayout: Bool { appTheme.usesGlassChrome }
|
||||||
|
|
||||||
|
private var isPeopleSheetPresented: Bool {
|
||||||
|
showSidebar || selectedPrivatePeerID != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func rootModalPresentationState(
|
||||||
|
includingVoiceAlert: Bool
|
||||||
|
) -> ContentRootModalPresentationState {
|
||||||
|
#if os(iOS)
|
||||||
|
let isMediaPickerPresented = showImagePicker
|
||||||
|
#else
|
||||||
|
let isMediaPickerPresented = showMacImagePicker
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return ContentRootModalPresentationState(
|
||||||
|
appChromeModel: appChromeModel,
|
||||||
|
isPeopleSheetPresented: isPeopleSheetPresented,
|
||||||
|
isImagePreviewPresented: imagePreviewURL != nil,
|
||||||
|
isVerificationSheetPresented: showVerifySheet,
|
||||||
|
isVoiceAlertPresented: includingVoiceAlert && voiceRecordingVM.showAlert,
|
||||||
|
isMediaPickerPresented: isMediaPickerPresented
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
Binding(
|
||||||
|
get: {
|
||||||
|
scenePhase == .active
|
||||||
|
&& appChromeModel.showBluetoothAlert
|
||||||
|
&& !hasRootModalPresentation
|
||||||
|
},
|
||||||
|
set: { isPresented in
|
||||||
|
guard !isPresented,
|
||||||
|
scenePhase == .active,
|
||||||
|
!hasRootModalPresentation else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
appChromeModel.showBluetoothAlert = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
var body: some View {
|
||||||
mainContent
|
mainContent
|
||||||
.onAppear {
|
.onAppear {
|
||||||
@@ -121,11 +251,20 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.sheet(
|
.sheet(
|
||||||
isPresented: Binding(
|
isPresented: Binding(
|
||||||
get: { showSidebar || selectedPrivatePeerID != nil },
|
get: { isPeopleSheetPresented },
|
||||||
set: { isPresented in
|
set: { isPresented in
|
||||||
if !isPresented {
|
if !isPresented {
|
||||||
showSidebar = false
|
showSidebar = false
|
||||||
privateConversationModel.endConversation()
|
// 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,
|
||||||
|
!voiceRecordingVM.showAlert {
|
||||||
|
privateConversationModel.endConversation()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -226,7 +365,7 @@ struct ContentView: View {
|
|||||||
ImagePreviewView(url: url)
|
ImagePreviewView(url: url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert("Recording Error", isPresented: $voiceRecordingVM.showAlert, actions: {
|
.alert("Recording Error", isPresented: rootVoiceAlertBinding, actions: {
|
||||||
Button("common.ok", role: .cancel) {}
|
Button("common.ok", role: .cancel) {}
|
||||||
if voiceRecordingVM.state == .permissionDenied {
|
if voiceRecordingVM.state == .permissionDenied {
|
||||||
Button("location_channels.action.open_settings") {
|
Button("location_channels.action.open_settings") {
|
||||||
@@ -236,7 +375,7 @@ struct ContentView: View {
|
|||||||
}, message: {
|
}, message: {
|
||||||
Text(voiceRecordingVM.state.alertMessage)
|
Text(voiceRecordingVM.state.alertMessage)
|
||||||
})
|
})
|
||||||
.alert("content.alert.bluetooth_required.title", isPresented: $appChromeModel.showBluetoothAlert) {
|
.alert("content.alert.bluetooth_required.title", isPresented: rootBluetoothAlertBinding) {
|
||||||
Button("content.alert.bluetooth_required.settings") {
|
Button("content.alert.bluetooth_required.settings") {
|
||||||
SystemSettings.bluetooth.open()
|
SystemSettings.bluetooth.open()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -554,6 +554,103 @@ struct ViewSmokeTests {
|
|||||||
#expect(featureModels.privateConversationModel.selectedHeaderState?.headerPeerID == peerID)
|
#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("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() {
|
||||||
|
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
|
@Test
|
||||||
func geohashAndTextMessageViews_renderCoreBranches() {
|
func geohashAndTextMessageViews_renderCoreBranches() {
|
||||||
let (viewModel, _, _) = makeSmokeViewModel()
|
let (viewModel, _, _) = makeSmokeViewModel()
|
||||||
|
|||||||
Reference in New Issue
Block a user