mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-14 09:20:28 +00:00
ios: fix the fallback paths and guard the picker during the reassignment
Follow-up to the previous iOS commit. Still not compiled - no Swift toolchain here. The "core ignored keepActiveUser" path in the compose picker rested on a false premise. It claimed the form stays open and the alert draws over it, but the resync it performs replaces the chat list with the new profile's, and the prepared chat stayed with the previous one - so getChat returns nothing, the pushed chat view renders nothing, and this picker, which is the sheet's presenter, is removed by the switch itself. The alert was then raised into a hierarchy being torn down and discarded. The form is now dismissed first, and the chat id cleared only if it is still the chat this picker belongs to, since a notification tap may have navigated elsewhere meanwhile. Rows stay live during the reassignment. creatingProfile is cleared as soon as createProfileForChat returns, which is the moment it hands off to changeProfile's fire-and-forget Task, and none of the rows had a disabled state at all - so a second change could be started on top of the first, two concurrent reassignments of the same prepared contact and two racing user switches. This is what the Kotlin side made onCreated suspending for. The one-time link picker's new else branch reset the switch status but not the selection, so after a cancelled retry the profile that was not switched to kept the checkmark and tapping it did nothing - the view thought it was already selected. Its "core ignored the flag" path likewise left the picker pointing at a profile that is no longer active. The incognito handler has the identical latch the else branch was added for, and hardening the row's disabled state on profileSwitchStatus made that stuck path disable the row permanently, so it gets the same treatment - and its two state writes move onto the main actor, where the rest of that block already was. Both swallowed errors are now logged, and the comment claiming two .sheet modifiers on one view conflict is dropped - it contradicted the sibling file and the deployment target.
This commit is contained in:
@@ -23,6 +23,7 @@ struct ContextProfilePickerView: View {
|
||||
@State private var showIncognitoSheet = false
|
||||
@State private var showAddProfile = false
|
||||
@State private var creatingProfile = false
|
||||
@State private var changingProfile = false
|
||||
|
||||
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
|
||||
|
||||
@@ -166,8 +167,11 @@ struct ContextProfilePickerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var busy: Bool { creatingProfile || changingProfile }
|
||||
|
||||
private func profilerPickerUserOption(_ user: User) -> some View {
|
||||
Button {
|
||||
if busy { return }
|
||||
if !chat.chatInfo.profileChangeProhibited {
|
||||
if selectedUser == user {
|
||||
if !incognitoDefault {
|
||||
@@ -235,7 +239,7 @@ struct ContextProfilePickerView: View {
|
||||
.padding(.trailing)
|
||||
.frame(height: USER_ROW_SIZE)
|
||||
}
|
||||
.disabled(creatingProfile)
|
||||
.disabled(busy)
|
||||
}
|
||||
|
||||
// Creates a profile to use for this invitation. It is created without becoming
|
||||
@@ -271,25 +275,43 @@ struct ContextProfilePickerView: View {
|
||||
// resync to what the host actually did and report it. The failure is the
|
||||
// switch, not the creation, so it is not rethrown into the form's "error
|
||||
// creating profile" handler.
|
||||
//
|
||||
// Dismiss the form first. The switch below replaces the chat list with the new
|
||||
// user's, and the prepared chat stayed with the previous one - so this view,
|
||||
// which is the sheet's presenter, is removed from the hierarchy by the switch
|
||||
// itself. Nothing shown from inside it would survive, and an alert raised
|
||||
// while it is being torn down is discarded.
|
||||
await MainActor.run { showAddProfile = false }
|
||||
do {
|
||||
try await changeActiveUserAsync_(newUser.userId, viewPwd: nil)
|
||||
// The prepared chat stayed with the previous profile and is not in this
|
||||
// one's list, so leave it rather than showing an empty chat view.
|
||||
await MainActor.run { chatModel.chatId = nil }
|
||||
} catch {}
|
||||
// The form stays open, as it does for any other failure. showAlert presents
|
||||
// through UIKit on the top view controller, so it is shown over the sheet.
|
||||
} catch {
|
||||
logger.error("changeActiveUserAsync_ error: \(responseError(error))")
|
||||
}
|
||||
await MainActor.run {
|
||||
// The prepared chat is not in the new user's list, so a pushed chat view
|
||||
// would render blank. Only clear it if it is still the chat this picker
|
||||
// belongs to - a notification tap may have navigated elsewhere by now.
|
||||
if chatModel.chatId == chat.id { chatModel.chatId = nil }
|
||||
showAlert(NSLocalizedString("Error changing chat profile", comment: "alert title"))
|
||||
}
|
||||
return
|
||||
}
|
||||
await MainActor.run { showAddProfile = false }
|
||||
// changingProfile set here, not only inside changeProfile's Task: the defer above
|
||||
// clears creatingProfile as soon as this function returns, and that Task has not
|
||||
// necessarily started by then - the rows would be live in between.
|
||||
await MainActor.run {
|
||||
showAddProfile = false
|
||||
changingProfile = true
|
||||
}
|
||||
changeProfile(newUser)
|
||||
}
|
||||
|
||||
private func changeProfile(_ newUser: User) {
|
||||
Task {
|
||||
// Two round trips follow; without this every row, including "Add profile",
|
||||
// stays live and a second change can be started on top of this one.
|
||||
await MainActor.run { changingProfile = true }
|
||||
defer { Task { @MainActor in changingProfile = false } }
|
||||
do {
|
||||
if let contact = chat.chatInfo.contact {
|
||||
let updatedContact = try await apiChangePreparedContactUser(contactId: contact.contactId, newUserId: newUser.userId)
|
||||
@@ -339,6 +361,7 @@ struct ContextProfilePickerView: View {
|
||||
|
||||
private func incognitoOption() -> some View {
|
||||
Button {
|
||||
if busy { return }
|
||||
if !chat.chatInfo.profileChangeProhibited {
|
||||
if incognitoDefault {
|
||||
listExpanded.toggle()
|
||||
|
||||
@@ -427,12 +427,20 @@ private struct ActiveProfilePicker: View {
|
||||
profileSwitchStatus = .idle
|
||||
dismiss()
|
||||
}
|
||||
} else {
|
||||
// Same latch as in the selectedProfile handler below: without
|
||||
// this, a nil result leaves profileSwitchStatus stuck and the
|
||||
// picker dimmed behind a spinner with hit testing off.
|
||||
await MainActor.run {
|
||||
profileSwitchStatus = .idle
|
||||
incognitoEnabled = !incognito
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
profileSwitchStatus = .idle
|
||||
incognitoEnabled = !incognito
|
||||
logger.error("apiSetConnectionIncognito error: \(responseError(error))")
|
||||
await MainActor.run {
|
||||
profileSwitchStatus = .idle
|
||||
incognitoEnabled = !incognito
|
||||
showErrorAlert(error, NSLocalizedString("Error changing to incognito!", comment: ""))
|
||||
}
|
||||
}
|
||||
@@ -481,10 +489,18 @@ private struct ActiveProfilePicker: View {
|
||||
}
|
||||
} else {
|
||||
// No connection, or apiChangeConnectionUser returned nothing:
|
||||
// nothing was moved, so don't switch or dismiss - and reset the
|
||||
// nothing was moved, so don't switch or dismiss. Reset the
|
||||
// status, which otherwise latches the picker into its spinner
|
||||
// with hit testing off.
|
||||
await MainActor.run { profileSwitchStatus = .idle }
|
||||
// with hit testing off, and put selectedProfile back on the
|
||||
// active user as the catch below does - otherwise the profile
|
||||
// that was not switched to keeps the checkmark, and tapping it
|
||||
// does nothing because the view thinks it is already selected.
|
||||
await MainActor.run {
|
||||
profileSwitchStatus = .idle
|
||||
if let currentUser = chatModel.currentUser {
|
||||
selectedProfile = currentUser
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
@@ -619,6 +635,12 @@ private struct ActiveProfilePicker: View {
|
||||
// controller, so it appears over the form, which stays open as on any other
|
||||
// failure.
|
||||
await MainActor.run {
|
||||
// The app has switched to the new profile, so make the picker agree with
|
||||
// it rather than leaving the checkmark on a profile that is no longer
|
||||
// active. The connection stayed with the previous profile and cannot be
|
||||
// moved from here; the alert says so.
|
||||
profileSwitchStatus = .idle
|
||||
selectedProfile = newUser
|
||||
showAlert(NSLocalizedString("Error changing chat profile", comment: "alert title"))
|
||||
}
|
||||
return
|
||||
@@ -692,9 +714,8 @@ private struct ActiveProfilePicker: View {
|
||||
}
|
||||
.opacity(switchingProfileByTimeout ? 0.4 : 1)
|
||||
// Attached to the picker, not to the row: the row lives in a lazy container that
|
||||
// may dispose it, taking the presented sheet with it. Not the root either - two
|
||||
// .sheet modifiers on the same view conflict, and body already presents
|
||||
// IncognitoHelp.
|
||||
// may dispose it, taking the presented sheet with it. Unlike the compose picker
|
||||
// this view is never replaced, so the picker is a stable enough owner.
|
||||
.sheet(isPresented: $showAddProfile) {
|
||||
NavigationView {
|
||||
CreateProfile(onSubmit: { displayName, shortDescr, image in
|
||||
|
||||
Reference in New Issue
Block a user