diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextProfilePickerView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextProfilePickerView.swift index 7c39b8e59b..a290603add 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextProfilePickerView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextProfilePickerView.swift @@ -295,6 +295,10 @@ struct ContextProfilePickerView: View { chatModel.users = updatedUsers // Only filled in onAppear otherwise, so the new profile is missing here users = updatedUsers.map { $0.user }.filter { u in u.activeUser || !u.hidden } + } else if !users.contains(where: { $0.userId == newUser.userId }) { + // changeProfile sets selectedUser to it, and otherUsers filters on that - + // absent from users, nothing is filtered out and a row is clipped. + users.append(newUser) } // changingProfile here too: the defer clears creatingProfile as soon as this returns showAddProfile = false diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 9e0d9f9ebc..e40fb8a2da 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -479,6 +479,15 @@ private struct ActiveProfilePicker: View { ) } } + } else { + // apiChangeConnectionUser returns nil rather than throwing when + // the retry is cancelled - offline. Without this the status stays + // .switchingUser, switchingProfileByTimeout latches, and the + // picker is left permanently behind its spinner. + await MainActor.run { + profileSwitchStatus = .idle + selectedProfile = chatModel.currentUser ?? selectedProfile + } } } catch { await MainActor.run { 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 10caf342e4..bc32d7a195 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 @@ -414,7 +414,14 @@ fun createProfileForInvitation(rhId: Long?, onCreated: suspend (User) -> Unit) { return@withApi } close() - onCreated(newUser) + try { + onCreated(newUser) + } catch (e: Exception) { + // changeActiveUser_ throws, and withApi does not catch - the global handler + // would close a modal or clear chatId with nothing said about the failure. + Log.e(TAG, "createProfileForInvitation: moving the invitation failed: ${e.stackTraceToString()}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_changing_user)) + } } finally { chatModel.creatingProfileForInvitation.value = false } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index d46bd169c3..1c98bee8af 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -323,8 +323,6 @@ fun ActiveProfilePicker( try { var updatedConn: PendingContactConnection? = null - appPreferences.incognito.set(false) - if (contactConnection != null) { updatedConn = controller.apiChangeConnectionUser(rhId, contactConnection.pccConnId, user.userId) // Not moved - leave the picker open rather than stranding a profile just created @@ -335,6 +333,9 @@ fun ActiveProfilePicker( updateShownConnection(updatedConn) } } + // After the move, not before: the picker now stays open on failure, and clearing the + // app-wide default there would leave it off with Incognito still ticked in the picker + appPreferences.incognito.set(false) controller.changeActiveUser_( rhId = user.remoteHostId, diff --git a/plans/2026-07-30-new-profile-for-invitation.md b/plans/2026-07-30-new-profile-for-invitation.md index 3c98479662..670b3f5a9f 100644 --- a/plans/2026-07-30-new-profile-for-invitation.md +++ b/plans/2026-07-30-new-profile-for-invitation.md @@ -188,6 +188,10 @@ Two hazards left as they are on master, so review does not keep re-raising them: profile first is local and fast and barely widens it. Fixing it needs a `ModalViewId` on the picker at both call sites, including `ShareListView`, which does not offer this feature at all. +- `Terminal/Input.hs` caches any `CRActiveUser` as the remote host's current user. With + `keepActiveUser` that response can carry a profile that is deliberately not active, so a + CLI acting as a controller caches the wrong one until the next `/user`. Display-only, + and there is no flag in the response to distinguish the two cases. - `alertAfterDismissal` waits a fixed 0.5s for a sheet transition rather than observing it. A slow device or a late-released interactive dismissal can still outlast it. The deterministic version needs the presenting controller's completion handler, which is