diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextProfilePickerView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextProfilePickerView.swift index 0c5362df7f..52fa8a0b07 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextProfilePickerView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextProfilePickerView.swift @@ -47,9 +47,8 @@ struct ContextProfilePickerView: View { profilePicker() } } - // On the Group, not the row or profilePicker(): both are disposed while this is - // presented - the row by its lazy container, the picker when listExpanded flips. - // Stacking with body's IncognitoHelp sheet is fine from iOS 14.5; target is 15. + // On the Group: the row and profilePicker() are both disposed while this is + // presented. Stacking sheets is supported from iOS 14.5; the target is 15. .sheet(isPresented: $showAddProfile) { NavigationView { CreateProfile(onSubmit: { displayName, shortDescr, image in @@ -177,7 +176,7 @@ struct ContextProfilePickerView: View { listExpanded = false } } else if selectedUser != user { - // Only the branch that starts work: expand/collapse is local + // Only the branch that starts work; expand/collapse is local if busy { return } changingProfile = true changeProfile(user) @@ -245,13 +244,11 @@ struct ContextProfilePickerView: View { .disabled(busy) } - // Creates a profile to use for this invitation. It is created without becoming - // active, because changeProfile below reassigns the prepared chat and the API - // resolves that chat under the currently active user - so the profile that owns the - // invitation has to stay active until the chat has been moved. + // Created without becoming active: changeProfile below resolves the prepared chat + // under the active user, so the profile that owns it must stay active until it moves. private func createProfileForChat(_ displayName: String, _ shortDescr: String?, _ image: String?) async throws { - // Atomic check-and-set on the main actor: a plain check-then-set leaves a window - // where two submits both pass, and @State must not be read off the main actor. + // Atomic check-and-set: check-then-set lets two submits through, and @State + // must not be read off the main actor. let alreadyCreating = await MainActor.run { () -> Bool in if creatingProfile { return true } creatingProfile = true @@ -265,15 +262,13 @@ struct ContextProfilePickerView: View { await MainActor.run { if let updatedUsers = updatedUsers { chatModel.users = updatedUsers - // Otherwise only filled in onAppear, so the profile just created is - // missing from the picker, and the row count the frame uses is one short. + // Only filled in onAppear otherwise, so the new profile is missing here users = updatedUsers.map { $0.user }.filter { u in u.activeUser || !u.hidden } } } if newUser.activeUser { - // An older remote host ignored keepActiveUser and activated it, so the - // reassignment would fail. Resync to what the host did and report it - not - // rethrown, or the form reports it as a failure to create the profile. + // An older remote host ignored keepActiveUser, so the reassignment would + // fail. Resync and report - not rethrown, or the form blames the creation. let switched: Bool do { try await changeActiveUserAsync_(newUser.userId, viewPwd: nil) @@ -283,19 +278,15 @@ struct ContextProfilePickerView: View { switched = false } await MainActor.run { - // Dismissed unconditionally: the switch removes this view - and the sheet - // it presents - only when it succeeded. If it threw, the form would be - // left over a profile that already exists. + // Unconditional: the switch only removes this view when it succeeded showAddProfile = false - // Only if the switch happened: the chat is then absent from the reloaded - // list and would render blank. If it failed, the chat is still fine. + // Only if it switched: the chat is then gone from the list and renders blank if switched && chatModel.chatId == chat.id { chatModel.chatId = nil } } alertAfterDismissal(NSLocalizedString("Error changing chat profile", comment: "alert title")) return } - // changingProfile here too: the defer above clears creatingProfile as soon as this - // returns, and changeProfile's Task has not necessarily started by then. + // Here too: the defer clears creatingProfile as soon as this returns await MainActor.run { showAddProfile = false changingProfile = true diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift index 4fdacc0ed0..e4898824ac 100644 --- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift +++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift @@ -70,9 +70,8 @@ func showAlert( } } -/// An alert raised while a sheet is dismissing is presented on a controller that is going -/// away, and is dropped - getTopViewController() keeps returning it until the transition -/// ends. Use this when the caller has just dismissed something. +/// getTopViewController() keeps returning a sheet until its dismissal transition ends, so +/// an alert raised right after dismissing one is presented on it and dropped. func alertAfterDismissal(_ title: String, _ message: String? = nil) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { showAlert(title, message: message) diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 7ee916dfca..7fcd042714 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -580,9 +580,8 @@ private struct ActiveProfilePicker: View { .disabled(busy) } - // Creates a profile for this invitation without activating it, then routes through - // the same selectedProfile path as picking an existing profile, so the connection - // change and the switch cannot drift apart. + // Created without activating, then routed through the same selectedProfile path as + // picking an existing profile, so the connection change and the switch cannot drift. private func createProfileForConnection(_ displayName: String, _ shortDescr: String?, _ image: String?) async throws { // Atomic check-and-set on the main actor: a plain check-then-set leaves a window // where two submits both pass, and @State must not be read off the main actor. @@ -601,21 +600,17 @@ private struct ActiveProfilePicker: View { profiles = chatModel.users.map { $0.user } } if newUser.activeUser { - // An older core ignored keepActiveUser and switched instead, so the connection - // change would fail. Not rethrown, or the form reports it as a creation failure. + // An older core ignored keepActiveUser, so the connection change would fail do { try await changeActiveUserAsync_(newUser.userId, viewPwd: nil) } catch { logger.error("changeActiveUserAsync_ error: \(responseError(error))") } - // Dismiss the form, as the compose picker does on the same failure: the app is - // now showing a different profile and the connection stayed with the previous - // one, so there is nothing left to do here and a second attempt would fail the - // same way. + // Dismiss: the app now shows a different profile and the connection stayed + // with the previous one, so a second attempt would fail the same way. await MainActor.run { showAddProfile = false - // Make the picker agree with the profile that is now active rather than - // leaving the checkmark on one that is not. + // Make the picker agree with the profile that is now active profileSwitchStatus = .idle selectedProfile = newUser } @@ -699,9 +694,7 @@ private struct ActiveProfilePicker: View { try await createProfileForConnection(displayName, shortDescr, image) }, submitting: creatingProfile) } - // The submit runs in an unstructured Task that SwiftUI does not cancel, so a - // swipe-to-dismiss mid-create would still create the profile and switch to it - // while every state write landed on a dismissed view. + // The submit Task is unstructured and SwiftUI will not cancel it on dismissal .interactiveDismissDisabled(creatingProfile) } }