Review fixes: dismiss sheets on wipe; host the dialog durably

- Codex P1: the App Info danger-zone wipe left its sheet presented, so
  the failed-wipe banner (and the success message) rendered underneath
  an opaque modal. panicClearAllData now dismisses all chrome-owned
  presentation first; pinned by a new architecture test.
- Review nit: the confirmation dialog moved from the logo Text to the
  ContentView header stack, so a covered/removed header can't take a
  pending dialog down with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jack 2026-08-09 11:44:49 +02:00
parent 2995b6abae
commit d32daaab82
4 changed files with 56 additions and 16 deletions

View File

@ -125,6 +125,16 @@ final class AppChromeModel: ObservableObject {
}
func panicClearAllData() {
// A wipe invalidates everything on screen, and its outcome must be
// visible: the success message and the failed-wipe banner both live
// on the root timeline, so a sheet left up (the App Info danger-zone
// path keeps its sheet presented) would hide the one signal that says
// whether the wipe worked.
isAppInfoPresented = false
isLocationChannelsSheetPresented = false
isNoticesSheetPresented = false
showingFingerprintFor = nil
prepareForPanic?()
onPanicWipe()
chatViewModel.panicClearAllData()

View File

@ -61,22 +61,10 @@ struct ContentHeaderView: View {
.onTapGesture(count: 1) {
appChromeModel.presentAppInfo()
}
.confirmationDialog(
Text(
String(localized: "app_info.settings.danger.panic_confirm_title", defaultValue: "wipe all data?", comment: "Title of the confirmation dialog before a panic wipe")
),
isPresented: $appChromeModel.showPanicConfirmation,
titleVisibility: .visible
) {
Button(role: .destructive) {
appChromeModel.panicClearAllData()
} label: {
Text(
String(localized: "app_info.settings.danger.panic_confirm_action", defaultValue: "wipe everything", comment: "Destructive confirmation button that performs the panic wipe")
)
}
Button("common.cancel", role: .cancel) {}
}
// The confirmation dialog itself is hosted on ContentView
// (next to the failed-wipe banner), not on this Text: a host
// that can be covered or removed could take the pending
// dialog down with it.
// This is the only entry point to App Info, but it reads as
// static text; surface the tap. (The triple-tap panic wipe
// stays undiscoverable on purpose — it's destructive.)

View File

@ -503,6 +503,24 @@ struct ContentView: View {
PanicWipeBlockedBanner()
}
}
// Hosted here rather than on the logo Text so the pending dialog
// survives whatever happens to the header chrome.
.confirmationDialog(
Text(
String(localized: "app_info.settings.danger.panic_confirm_title", defaultValue: "wipe all data?", comment: "Title of the confirmation dialog before a panic wipe")
),
isPresented: $appChromeModel.showPanicConfirmation,
titleVisibility: .visible
) {
Button(role: .destructive) {
appChromeModel.panicClearAllData()
} label: {
Text(
String(localized: "app_info.settings.danger.panic_confirm_action", defaultValue: "wipe everything", comment: "Destructive confirmation button that performs the panic wipe")
)
}
Button("common.cancel", role: .cancel) {}
}
}
private var publicMessageList: some View {

View File

@ -482,6 +482,30 @@ struct AppArchitectureTests {
#expect(viewModel.messages.map(\.id) == ["keep-1"])
}
@Test("Panic wipe dismisses chrome sheets so its outcome is visible")
@MainActor
func panicWipeDismissesChromePresentation() {
let viewModel = makeArchitectureViewModel()
let privateInboxModel = PrivateInboxModel(conversations: ConversationStore())
let chromeModel = AppChromeModel(chatViewModel: viewModel, privateInboxModel: privateInboxModel)
// The App Info danger-zone path wipes while its own sheet is up; the
// success message and the failed-wipe banner both render on the root
// timeline, so any sheet left presented would hide the one signal
// that says whether the wipe worked.
chromeModel.presentAppInfo()
chromeModel.isLocationChannelsSheetPresented = true
chromeModel.presentNotices()
chromeModel.showFingerprint(for: PeerID(str: "peer-3"))
chromeModel.panicClearAllData()
#expect(!chromeModel.isAppInfoPresented)
#expect(!chromeModel.isLocationChannelsSheetPresented)
#expect(!chromeModel.isNoticesSheetPresented)
#expect(chromeModel.showingFingerprintFor == nil)
}
@Test("PrivateConversationModel resolves canonical header state for the selected DM")
@MainActor
func privateConversationModelResolvesSelectedHeaderState() async {