swallow Back while creating instead of leaving it unhandled

enableClose = false removes ModalView's handler rather than neutering it,
so Compose passed Back down to ChatView's, closing the chat behind the form
that was still on screen. Register a no-op handler that wins while creating.

Also guard the iOS incognito row on contactConnection, as Kotlin already
does: incognitoEnabled is a binding onto the app-wide default, so starting
a change that cannot happen wrote that preference twice to undo itself.
This commit is contained in:
Narasimha-sc
2026-08-07 16:23:07 +00:00
parent 66a313f328
commit d2adea5481
3 changed files with 21 additions and 8 deletions
@@ -428,9 +428,11 @@ private struct ActiveProfilePicker: View {
dismiss()
}
} else {
// Nothing to change, so nothing happened. Without this the status
// stays .switchingIncognito and busy leaves the whole picker -
// including the new "Add profile" row - permanently dead.
// Backstop for a nil connection coming back without a throw: the
// row above cannot start this with contactConnection nil. Without
// it the status stays .switchingIncognito and busy leaves the
// whole picker - including "Add profile" - permanently dead. Both
// writes in one hop, so the re-entrant onChange sees .idle.
await MainActor.run {
profileSwitchStatus = .idle
incognitoEnabled = !incognito
@@ -665,7 +667,11 @@ private struct ActiveProfilePicker: View {
@ViewBuilder private func profilePicker() -> some View {
let incognitoOption = Button {
if !incognitoEnabled {
// contactConnection too, as Kotlin's IncognitoUserOption checks: with nothing to
// change the handler below does nothing, and incognitoEnabled is a binding onto
// the app-wide default, so toggling it here would write that preference twice
// for an action that cannot happen.
if !incognitoEnabled && contactConnection != nil {
incognitoEnabled = true
profileSwitchStatus = .switchingIncognito
}
@@ -376,6 +376,10 @@ fun createProfileForInvitation(rhId: Long?, onCreated: suspend (User) -> Unit) {
// and the next attempt at the same name fails as a duplicate. iOS closes the same
// window with interactiveDismissDisabled.
ModalView(close, enableClose = !chatModel.creatingProfileForInvitation.value) {
// Consume Back rather than leaving it unhandled: ModalView's own handler is disabled
// above, and Compose would then pass the event down to ChatView's, closing the chat
// behind the form. Registered after ModalView's, so it wins while it is enabled.
BackHandler(enabled = chatModel.creatingProfileForInvitation.value, onBack = {})
CreateProfile(chatModel, close, submitting = chatModel.creatingProfileForInvitation.value) { displayName, shortDescr, image ->
if (chatModel.creatingProfileForInvitation.value) return@CreateProfile
chatModel.creatingProfileForInvitation.value = true
@@ -371,10 +371,13 @@ re-derive them.
not.** `ModalView(close, enableClose = !creatingProfileForInvitation)` now disables Back,
Esc and the back arrow while the profile is being created — the Kotlin equivalent of
iOS's `interactiveDismissDisabled`, following `MigrateFromDevice`/`ChooseServerOperators`,
which use the same idiom. Note the Android consequence: with no enabled `BackHandler`,
Back falls through to whatever handles it beneath rather than doing nothing. That is the
same trade-off the migration screens already accept, and it beats the alternative of
orphaning a profile. The separate case below is still open.
which use the same idiom. **`enableClose = false` is not enough on its own**: it removes
the handler rather than neutering it, and Compose then passes Back to the next enabled
one — `ChatView`'s (`ChatView.kt`, unconditional), which closes the chat behind the still
visible form. So the form also registers `BackHandler(enabled = creating, onBack = {})`
to swallow the event; it composes after `ModalView`'s and therefore wins while enabled.
The migration screens have the same fall-through, but they are not layered over a screen
with its own handler. The separate case below is still open.
- **A modal pushed on top of the create form is indistinguishable from backing out of it**
(`WelcomeView.kt`). The guard is `!isLastModalOpenNotClosing(...)`, which is also false