From 185c6e1d2bffa7480245332fc0de1e4290d56896 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:05:02 +0000 Subject: [PATCH] fix four more the high-effort review found, two of them my own reverts Two of these exist because I reverted half of a coupled pair while keeping the other half. iOS: apiChangeConnectionUser returns nil rather than throwing when the network retry is cancelled, so neither branch of the selectedProfile handler ran, profileSwitchStatus stayed .switchingUser, switchingProfileByTimeout latched and the picker was left permanently behind its spinner with hit testing off. I removed this else branch two passes ago believing it was only reachable with no connection - it is reachable offline, which is a row in this branch's own test matrix, and on the create path it also strands the profile just created. Kotlin has had the equivalent guard all along. Kotlin: appPreferences.incognito.set(false) went back to master's position, before the connection change - but the early return that now keeps the picker open on failure stayed. Together those clear the app-wide default while the picker still shows Incognito ticked and nothing was moved. Moved below the reassignment, where the early return puts it out of reach. changeActiveUser_ throws, and withApi does not catch, so a failure inside onCreated reached GlobalExceptionsHandler, which silently closes a modal or clears chatId - profile created, invitation not moved, nothing said. Caught and reported, matching the runCatching and the safe changeActiveUser wrapper this same function already uses. iOS: when the profile list refresh fails, the new profile is absent from users while changeProfile sets selectedUser to it, so otherUsers filters nobody out and the row count exceeds what frame(maxHeight:) allows - clipping "Add profile" at the top, for exactly the single-profile user this feature is for. Also records that Terminal/Input.hs caches any CRActiveUser as the host's current user, which keepActiveUser makes conditional. Display-only, and the response carries nothing to distinguish the two cases. --- .../Chat/ComposeMessage/ContextProfilePickerView.swift | 4 ++++ apps/ios/Shared/Views/NewChat/NewChatView.swift | 9 +++++++++ .../kotlin/chat/simplex/common/views/WelcomeView.kt | 9 ++++++++- .../chat/simplex/common/views/newchat/NewChatView.kt | 5 +++-- plans/2026-07-30-new-profile-for-invitation.md | 4 ++++ 5 files changed, 28 insertions(+), 3 deletions(-) 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