From d2adea54817e93af16958d723e4d191e02e71912 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:23:07 +0000 Subject: [PATCH] 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. --- apps/ios/Shared/Views/NewChat/NewChatView.swift | 14 ++++++++++---- .../chat/simplex/common/views/WelcomeView.kt | 4 ++++ plans/2026-07-30-new-profile-for-invitation.md | 11 +++++++---- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 2bc831b65d..613daf40f8 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -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 } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index 096523474f..edfd52a86e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -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 diff --git a/plans/2026-07-30-new-profile-for-invitation.md b/plans/2026-07-30-new-profile-for-invitation.md index 167d7bf0e6..95c4809147 100644 --- a/plans/2026-07-30-new-profile-for-invitation.md +++ b/plans/2026-07-30-new-profile-for-invitation.md @@ -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