mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-02 04:49:22 +00:00
core, ui: strip PR comments, fix Android profile-for-invitation issues
- remove all comments introduced by this PR across the touched files, per review decision - fix the "New Profile" row being hidden behind the bottom app bar in one-handed UI mode once the profile list grows long enough to reach it - stop force-setting connChatUsed on reassignment, so the "keep unused invitation?" prompt correctly fires for a genuinely unused invitation - fix NewChatView's contactConnection state going stale after a reassignment (it never reflected the new connection), causing both the keep/delete decision and any later picker reopen to reference an already-superseded connection
This commit is contained in:
@@ -280,10 +280,6 @@ func apiGetActiveUser(ctrl: chat_ctrl? = nil) throws -> User? {
|
||||
}
|
||||
}
|
||||
|
||||
/// keepActiveUser creates the profile *without* activating it, for the invitation
|
||||
/// pickers: the reassignment APIs resolve the prepared chat or connection under the active
|
||||
/// user, so the profile that owns it has to stay active until it has moved. The response
|
||||
/// is the created user either way, which is then not the active one.
|
||||
func apiCreateActiveUser(_ p: Profile?, pastTimestamp: Bool = false, keepActiveUser: Bool = false, ctrl: chat_ctrl? = nil) throws -> User {
|
||||
let r: ChatResponse0 = try chatSendCmdSync(.createActiveUser(profile: p, pastTimestamp: pastTimestamp, keepActiveUser: keepActiveUser), ctrl: ctrl)
|
||||
if case let .activeUser(user) = r { return user }
|
||||
|
||||
@@ -137,7 +137,7 @@ struct ContextProfilePickerView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: USER_ROW_SIZE * min(MAX_VISIBLE_USER_ROWS, CGFloat(users.count + 2))) // + 1 for incognito, + 1 for "Add profile"
|
||||
.frame(maxHeight: USER_ROW_SIZE * min(MAX_VISIBLE_USER_ROWS, CGFloat(users.count + 2)))
|
||||
.onAppear {
|
||||
DispatchQueue.main.async {
|
||||
withAnimation(nil) {
|
||||
@@ -177,13 +177,10 @@ struct ContextProfilePickerView: View {
|
||||
if !incognitoDefault {
|
||||
listExpanded.toggle()
|
||||
} else if !busy {
|
||||
// Gated like the sibling write in incognitoOption, and like both of
|
||||
// the Kotlin ones: only expand/collapse stays live while busy.
|
||||
incognitoDefault = false
|
||||
listExpanded = false
|
||||
}
|
||||
} else if selectedUser != user {
|
||||
// Only the branch that starts work; expand/collapse is local
|
||||
if busy { return }
|
||||
changingProfile = true
|
||||
changeProfile(user)
|
||||
@@ -251,11 +248,7 @@ struct ContextProfilePickerView: View {
|
||||
.disabled(busy)
|
||||
}
|
||||
|
||||
// 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: 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
|
||||
@@ -266,32 +259,20 @@ struct ContextProfilePickerView: View {
|
||||
let ownerUserId = await MainActor.run { chatModel.currentUser?.userId }
|
||||
let profile = Profile(displayName: displayName, fullName: "", shortDescr: shortDescr, image: image)
|
||||
let newUser = try apiCreateActiveUser(profile, keepActiveUser: true)
|
||||
// Checked before refreshing the lists below: on this path the core has already
|
||||
// activated the new profile, so they would disagree with chatModel.currentUser
|
||||
// until the resync lands - and changeActiveUserAsync_ refreshes them anyway.
|
||||
if newUser.activeUser {
|
||||
// An older remote host ignored keepActiveUser, so the reassignment would
|
||||
// fail. Resync and report - not rethrown, or the form blames the creation.
|
||||
do {
|
||||
try await changeActiveUserAsync_(newUser.userId, viewPwd: nil)
|
||||
} catch {
|
||||
logger.error("changeActiveUserAsync_ error: \(responseError(error))")
|
||||
}
|
||||
await MainActor.run {
|
||||
// Unconditional: the switch only removes this view when it succeeded
|
||||
activeSheet = nil
|
||||
// Registered here rather than trusting the resync: changeActiveUserAsync_
|
||||
// calls apiSetActiveUserAsync and listUsersAsync before the MainActor.run
|
||||
// that writes m.users, so a throw leaves both lists without the profile and
|
||||
// a retry with the same name is refused as a duplicate.
|
||||
if !chatModel.users.contains(where: { $0.user.userId == newUser.userId }) {
|
||||
chatModel.users.append(UserInfo(user: newUser, unreadCount: 0))
|
||||
}
|
||||
if !users.contains(where: { $0.userId == newUser.userId }) {
|
||||
users.append(newUser)
|
||||
}
|
||||
// Only if it switched, which the active user tells us: the prepared chat
|
||||
// is then gone from the reloaded list and would render blank.
|
||||
if chatModel.currentUser?.userId == newUser.userId && chatModel.chatId == chat.id {
|
||||
chatModel.chatId = nil
|
||||
}
|
||||
@@ -299,11 +280,6 @@ struct ContextProfilePickerView: View {
|
||||
alertAfterDismissal(NSLocalizedString("Error changing chat profile", comment: "alert title"))
|
||||
return
|
||||
}
|
||||
// Below the branch above, which returns and registers its own copy: appending here
|
||||
// as well would leave two entries flagged activeUser. Above the guard below, which
|
||||
// returns without refreshing either list - the profile exists by now, so it has to
|
||||
// appear in both or the next attempt at the same name is a duplicate.
|
||||
// users is otherwise only filled in onAppear.
|
||||
await MainActor.run {
|
||||
if !chatModel.users.contains(where: { $0.user.userId == newUser.userId }) {
|
||||
chatModel.users.append(UserInfo(user: newUser, unreadCount: 0))
|
||||
@@ -313,10 +289,6 @@ struct ContextProfilePickerView: View {
|
||||
}
|
||||
}
|
||||
let updatedUsers = try? await listUsersAsync()
|
||||
// changeProfile resolves the prepared chat under whatever is active when it runs,
|
||||
// and a notification action can have switched it while we were creating. After the
|
||||
// await above, not before it: checked first, that await reopens the very window
|
||||
// this closes. Kotlin orders it the same way.
|
||||
let activeUserId = await MainActor.run { chatModel.currentUser?.userId }
|
||||
guard activeUserId == ownerUserId else {
|
||||
await MainActor.run { activeSheet = nil }
|
||||
@@ -326,24 +298,16 @@ struct ContextProfilePickerView: View {
|
||||
await MainActor.run {
|
||||
if let updatedUsers = updatedUsers {
|
||||
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 }
|
||||
}
|
||||
// changingProfile here too: the defer clears creatingProfile as soon as this returns
|
||||
activeSheet = nil
|
||||
changingProfile = true
|
||||
}
|
||||
changeProfile(newUser, dismissingSheet: true)
|
||||
}
|
||||
|
||||
/// [dismissingSheet] only on the create path, which closes the form first: the delay is
|
||||
/// there to outlast a sheet dismissal, and on the plain row tap there is no sheet, so it
|
||||
/// would just detach the error from the tap that caused it - master alerted at once.
|
||||
private func changeProfile(_ newUser: User, dismissingSheet: Bool = false) {
|
||||
func report(_ title: String, _ message: String? = nil) {
|
||||
// Both hop to main: this runs inside the Task below, and showAlert presents a
|
||||
// UIAlertController. alertAfterDismissal does it via asyncAfter, so the
|
||||
// immediate path has to do it too rather than calling showAlert here.
|
||||
if dismissingSheet {
|
||||
alertAfterDismissal(title, message)
|
||||
} else {
|
||||
@@ -403,9 +367,6 @@ struct ContextProfilePickerView: View {
|
||||
if incognitoDefault {
|
||||
listExpanded.toggle()
|
||||
} else if !busy {
|
||||
// As in profilerPickerUserOption: a failed changeProfile would leave the
|
||||
// incognito default on while the picker still shows the chat profile.
|
||||
// Only this branch - expanding and collapsing is local, as on Kotlin.
|
||||
incognitoDefault = true
|
||||
listExpanded = false
|
||||
}
|
||||
|
||||
@@ -79,8 +79,6 @@ func showAlertReturningController(
|
||||
return alert
|
||||
}
|
||||
|
||||
/// 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)
|
||||
|
||||
@@ -428,11 +428,6 @@ private struct ActiveProfilePicker: View {
|
||||
dismiss()
|
||||
}
|
||||
} else {
|
||||
// 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
|
||||
@@ -492,10 +487,6 @@ 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
|
||||
@@ -528,10 +519,6 @@ private struct ActiveProfilePicker: View {
|
||||
}
|
||||
|
||||
|
||||
// The picker must be dead from the moment work starts: switchingProfileByTimeout only
|
||||
// latches half a second later, to keep the spinner from flickering, and in that window
|
||||
// a second row tap would run apiChangeConnectionUser against a connection that
|
||||
// recreateConn is already deleting.
|
||||
private var busy: Bool { creatingProfile || switchingProfileByTimeout || profileSwitchStatus != .idle }
|
||||
|
||||
@ViewBuilder private func viewBody() -> some View {
|
||||
@@ -561,8 +548,6 @@ private struct ActiveProfilePicker: View {
|
||||
|
||||
private func profilerPickerUserOption(_ user: User) -> some View {
|
||||
Button {
|
||||
// contactConnection as in incognitoOption: with nothing to change the handler
|
||||
// does nothing and the backstop writes the app-wide default straight back.
|
||||
if selectedProfile == user && incognitoEnabled && contactConnection != nil {
|
||||
incognitoEnabled = false
|
||||
profileSwitchStatus = .switchingIncognito
|
||||
@@ -604,11 +589,7 @@ private struct ActiveProfilePicker: View {
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
let alreadyCreating = await MainActor.run { () -> Bool in
|
||||
if creatingProfile { return true }
|
||||
creatingProfile = true
|
||||
@@ -619,43 +600,24 @@ private struct ActiveProfilePicker: View {
|
||||
let ownerUserId = await MainActor.run { chatModel.currentUser?.userId }
|
||||
let profile = Profile(displayName: displayName, fullName: "", shortDescr: shortDescr, image: image)
|
||||
let newUser = try apiCreateActiveUser(profile, keepActiveUser: true)
|
||||
// Checked before refreshing the lists below: on this path the core has already
|
||||
// activated the new profile, so they would disagree with chatModel.currentUser
|
||||
// until the resync lands - and changeActiveUserAsync_ refreshes them anyway.
|
||||
if newUser.activeUser {
|
||||
// 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 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
|
||||
profileSwitchStatus = .idle
|
||||
// Whatever the resync above ended up with - newUser if it switched, the
|
||||
// previous profile if it threw. Not newUser unconditionally, or a failed
|
||||
// switch leaves the checkmark on a profile that is not active.
|
||||
selectedProfile = chatModel.currentUser ?? selectedProfile
|
||||
// Registered here rather than trusting the resync: changeActiveUserAsync_
|
||||
// calls apiSetActiveUserAsync and listUsersAsync before the MainActor.run
|
||||
// that writes m.users, so a throw leaves that list untouched.
|
||||
if !chatModel.users.contains(where: { $0.user.userId == newUser.userId }) {
|
||||
chatModel.users.append(UserInfo(user: newUser, unreadCount: 0))
|
||||
}
|
||||
// And this snapshot is otherwise only filled in onAppear, so without it the
|
||||
// picker lists the old profiles and no row shows a checkmark.
|
||||
profiles = chatModel.users.map { $0.user }
|
||||
}
|
||||
alertAfterDismissal(NSLocalizedString("Error changing chat profile", comment: "alert title"))
|
||||
return
|
||||
}
|
||||
// Below the branch above, which returns: adding it there would leave two entries
|
||||
// flagged activeUser, and its own resync refreshes the list anyway. Above the guard
|
||||
// below, which returns without refreshing either list - the profile exists by now,
|
||||
// so it has to appear in both or the next attempt at the same name is a duplicate.
|
||||
// profiles is otherwise only filled in onAppear.
|
||||
await MainActor.run {
|
||||
if !chatModel.users.contains(where: { $0.user.userId == newUser.userId }) {
|
||||
chatModel.users.append(UserInfo(user: newUser, unreadCount: 0))
|
||||
@@ -663,10 +625,6 @@ private struct ActiveProfilePicker: View {
|
||||
profiles = chatModel.users.map { $0.user }
|
||||
}
|
||||
let updatedUsers = try? await listUsersAsync()
|
||||
// apiChangeConnectionUser resolves pccConnId under whatever is active when the
|
||||
// selectedProfile handler runs, and a notification action can have switched it.
|
||||
// After the await above, not before it: checked first, that await reopens the very
|
||||
// window this closes. Kotlin orders it the same way.
|
||||
let activeUserId = await MainActor.run { chatModel.currentUser?.userId }
|
||||
guard activeUserId == ownerUserId else {
|
||||
await MainActor.run { showAddProfile = false }
|
||||
@@ -675,8 +633,6 @@ private struct ActiveProfilePicker: View {
|
||||
}
|
||||
await MainActor.run {
|
||||
if let updatedUsers = updatedUsers { chatModel.users = updatedUsers }
|
||||
// Derived from chatModel.users, which already holds the new profile - without
|
||||
// it selectedProfile below points at a profile that has no row in the picker.
|
||||
profiles = chatModel.users.map { $0.user }
|
||||
showAddProfile = false
|
||||
selectedProfile = newUser
|
||||
@@ -686,10 +642,6 @@ private struct ActiveProfilePicker: View {
|
||||
|
||||
@ViewBuilder private func profilePicker() -> some View {
|
||||
let incognitoOption = Button {
|
||||
// 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
|
||||
@@ -741,24 +693,17 @@ private struct ActiveProfilePicker: View {
|
||||
}
|
||||
}
|
||||
|
||||
// Outside the branch above: inside it the row would disappear whenever the
|
||||
// active profile is filtered out by the search text. Only offered when there
|
||||
// is a connection to move to the new profile.
|
||||
if contactConnection != nil {
|
||||
addProfileOption
|
||||
}
|
||||
}
|
||||
.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. 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
|
||||
try await createProfileForConnection(displayName, shortDescr, image)
|
||||
}, submitting: creatingProfile)
|
||||
}
|
||||
// The submit Task is unstructured and SwiftUI will not cancel it on dismissal
|
||||
.interactiveDismissDisabled(creatingProfile)
|
||||
}
|
||||
}
|
||||
|
||||
-4
@@ -880,10 +880,6 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
/** [keepActiveUser] creates the profile *without* activating it, for the invitation
|
||||
* pickers: the reassignment APIs resolve the prepared chat or connection under the
|
||||
* active user, so the profile that owns it has to stay active until it has moved. The
|
||||
* response is the created user either way, which is then not the active one. */
|
||||
suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, pastTimestamp: Boolean = false, keepActiveUser: Boolean = false, ctrl: ChatCtrl? = null): User? {
|
||||
val r = sendCmd(rh, CC.CreateActiveUser(p, pastTimestamp = pastTimestamp, keepActiveUser = keepActiveUser), ctrl)
|
||||
if (r is API.Result && r.res is CR.ActiveUser) return r.res.user.updateRemoteHostId(rh)
|
||||
|
||||
-51
@@ -60,10 +60,6 @@ fun bioFitsLimit(bio: String): Boolean {
|
||||
return chatJsonLength(bio) <= MAX_BIO_LENGTH_BYTES
|
||||
}
|
||||
|
||||
/** [onSubmit] replaces the default create-and-activate action, so the same form can be
|
||||
* used to create a profile for an invitation - which must not switch the active user
|
||||
* until the invitation has been moved onto it. Optional, so the existing call sites are
|
||||
* untouched. */
|
||||
@Composable
|
||||
fun CreateProfile(chatModel: ChatModel, close: () -> Unit, submitting: Boolean = false, onSubmit: ((displayName: String, shortDescr: String, image: String?) -> Unit)? = null) {
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -362,44 +358,23 @@ private fun CreateFirstProfileDesktop(chatModel: ChatModel, close: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates a profile for an invitation and hands it to [onCreated], which moves the
|
||||
* invitation onto it. The profile is created *without* becoming active: the reassignment
|
||||
* APIs resolve the prepared chat or connection under the active user, so the profile that
|
||||
* owns the invitation has to stay active until [onCreated] has run. [onCreated] suspends
|
||||
* so the in-flight flag covers the reassignment as well as the creation. */
|
||||
fun createProfileForInvitation(rhId: Long?, onCreated: suspend (User) -> Unit) {
|
||||
if (chatModel.creatingProfileForInvitation.value) return
|
||||
// fullscreen: center nulls chatId, closing the chat the invitation is in; end leaves the
|
||||
// compose picker live in the pane beside the form; start disposes the picker that opened it.
|
||||
val modalManager = ModalManager.fullscreen
|
||||
if (modalManager.isLastModalOpenNotClosing(ModalViewId.CONTEXT_USER_PICKER_NEW_PROFILE)) return
|
||||
modalManager.showCustomModal(id = ModalViewId.CONTEXT_USER_PICKER_NEW_PROFILE) { close ->
|
||||
// Back, Esc and the back arrow must not dismiss the form while the profile is being
|
||||
// created: it would exist with the invitation never moved onto it and nothing said,
|
||||
// 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
|
||||
// On Main, like the pickers' own handlers: every call here suspends into IO, and the
|
||||
// chat model is updated on Main by the receiver loop.
|
||||
val job = withApi {
|
||||
try {
|
||||
val ownerUserId = chatModel.currentUser.value?.userId
|
||||
val profile = Profile(displayName.trim(), "", shortDescr.trim().ifEmpty { null }, image = image)
|
||||
val newUser = controller.apiCreateActiveUser(rhId, profile, keepActiveUser = true) ?: return@withApi
|
||||
if (newUser.activeUser) {
|
||||
// An older remote host ignored the flag and activated it, so the reassignment
|
||||
// would fail. Resync to what the host did and report it, with the form
|
||||
// dismissed - the app is about to be showing a different profile.
|
||||
if (modalManager.isLastModalOpenNotClosing(ModalViewId.CONTEXT_USER_PICKER_NEW_PROFILE)) close()
|
||||
// changeActiveUser_, not changeActiveUser: the latter shows its own alert on
|
||||
// failure, which would stack with the one below reporting the same thing.
|
||||
suspend fun tryResync(): Boolean {
|
||||
return try {
|
||||
controller.changeActiveUser_(newUser.remoteHostId, newUser.userId, null)
|
||||
@@ -412,9 +387,6 @@ fun createProfileForInvitation(rhId: Long?, onCreated: suspend (User) -> Unit) {
|
||||
}
|
||||
}
|
||||
if (!tryResync()) tryResync()
|
||||
// Not relying on that resync to list it: changeActiveUser_ writes currentUser
|
||||
// inside its mutex before it calls listUsers, so a host that drops between the
|
||||
// two leaves the model pointing at a profile its own list does not contain.
|
||||
chatModel.changingActiveUserMutex.withLock {
|
||||
if (chatModel.remoteHostId() == rhId && chatModel.users.none { it.user.userId == newUser.userId }) {
|
||||
val isActive = chatModel.currentUser.value?.userId == newUser.userId
|
||||
@@ -429,39 +401,20 @@ fun createProfileForInvitation(rhId: Long?, onCreated: suspend (User) -> Unit) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_changing_user))
|
||||
return@withApi
|
||||
}
|
||||
// Below the branch above, which returns and registers its own copy: appending
|
||||
// here as well would leave two entries flagged activeUser. Above the
|
||||
// check below, which returns without listing it - the profile exists by now, so
|
||||
// it has to be in the list or the next attempt at the same name is a duplicate.
|
||||
// Just this row, not a listUsers refresh: that clears and refills the whole list
|
||||
// from Main while changeActiveUser_ can be doing the same from withBGApi, and it
|
||||
// has nothing to add - newUser is the API's own record and a profile created a
|
||||
// moment ago has no unread messages.
|
||||
chatModel.changingActiveUserMutex.withLock {
|
||||
if (chatModel.remoteHostId() == rhId && chatModel.users.none { it.user.userId == newUser.userId }) {
|
||||
chatModel.users.add(UserInfo(newUser, 0))
|
||||
}
|
||||
}
|
||||
// onCreated resolves the invitation under whatever is active when it runs, and a
|
||||
// notification tap or a host switch can have changed that while we were creating.
|
||||
if (chatModel.currentUser.value?.userId != ownerUserId || chatModel.remoteHostId() != rhId) {
|
||||
// Closed: the profile exists, so leaving the form up means the next Create
|
||||
// fails on the duplicate name for as long as the name is unchanged.
|
||||
if (modalManager.isLastModalOpenNotClosing(ModalViewId.CONTEXT_USER_PICKER_NEW_PROFILE)) close()
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_changing_user))
|
||||
return@withApi
|
||||
}
|
||||
// Form no longer on top. Don't move the invitation under a screen the user left -
|
||||
// but the two ways to get here need different treatment.
|
||||
if (!modalManager.isLastModalOpenNotClosing(ModalViewId.CONTEXT_USER_PICKER_NEW_PROFILE)) {
|
||||
if (modalManager.isModalOpenNotClosing(ModalViewId.CONTEXT_USER_PICKER_NEW_PROFILE)) {
|
||||
// Still there, just covered - a deep link or notification action opened a modal
|
||||
// over it, which on Android shares the one stack. The form comes back with the
|
||||
// name still typed, and Create would then fail as a duplicate, so say what
|
||||
// happened rather than leaving the only silent branch in this function.
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_changing_user))
|
||||
} else {
|
||||
// Genuinely dismissed - don't report an error for something they chose.
|
||||
Log.i(TAG, "createProfileForInvitation: form closed before the invitation moved")
|
||||
}
|
||||
return@withApi
|
||||
@@ -470,10 +423,6 @@ fun createProfileForInvitation(rhId: Long?, onCreated: suspend (User) -> Unit) {
|
||||
try {
|
||||
onCreated(newUser)
|
||||
} catch (e: Exception) {
|
||||
// Not a cancellation: this catch exists because changeActiveUser_ throws and
|
||||
// withApi does not catch, so the global handler would close a modal or clear
|
||||
// chatId with nothing said - but reporting a cancelled coroutine as a failure
|
||||
// would be wrong, and rethrowing keeps it cancelling.
|
||||
if (e is CancellationException) throw e
|
||||
Log.e(TAG, "createProfileForInvitation: moving the invitation failed: ${e.stackTraceToString()}")
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_changing_user))
|
||||
|
||||
-9
@@ -43,8 +43,6 @@ fun ComposeContextProfilePickerView(
|
||||
val users = chatModel.users.map { it.user }.filter { u -> u.activeUser || !u.hidden }
|
||||
val listExpanded = remember { mutableStateOf(false) }
|
||||
val changingProfile = remember { mutableStateOf(false) }
|
||||
// Creating stays set until the invitation has moved, which is after the form has closed
|
||||
// and this picker is interactive again
|
||||
val busy = changingProfile.value || chatModel.creatingProfileForInvitation.value
|
||||
|
||||
val maxHeightInPx = with(LocalDensity.current) { windowHeight().toPx() }
|
||||
@@ -104,8 +102,6 @@ fun ComposeContextProfilePickerView(
|
||||
chatMoved = true
|
||||
}
|
||||
}
|
||||
// Only switch if the chat moved, or the user ends up in another profile with the
|
||||
// invitation left behind. apiChangePrepared*User reports the failure itself.
|
||||
if (chatMoved) {
|
||||
val switched = try {
|
||||
chatModel.controller.changeActiveUser_(
|
||||
@@ -132,8 +128,6 @@ fun ComposeContextProfilePickerView(
|
||||
}
|
||||
|
||||
fun changeProfile(newUser: User) {
|
||||
// Set before withApi, which dispatches - the rows would be live until it runs.
|
||||
// The create path is covered by creatingProfileForInvitation instead.
|
||||
changingProfile.value = true
|
||||
withApi {
|
||||
try { changeProfileTo(newUser) } finally { changingProfile.value = false }
|
||||
@@ -153,9 +147,6 @@ fun ComposeContextProfilePickerView(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT + 8.dp)
|
||||
// busy gates the branches that change something, not the row: this picker has no
|
||||
// spinner or dimming, so gating the row left it inert with no feedback and not even
|
||||
// collapsible during a slow change. Expanding and collapsing is local. As on iOS.
|
||||
.clickable(onClick = {
|
||||
if (!chat.chatInfo.profileChangeProhibited) {
|
||||
if (selectedUser.value.userId == user.userId) {
|
||||
|
||||
+6
-18
@@ -75,7 +75,7 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC
|
||||
* It will be dropped automatically when connection established or when user goes away from this screen.
|
||||
**/
|
||||
if (chatModel.showingInvitation.value != null && ModalManager.start.openModalCount() <= 1) {
|
||||
val conn = contactConnection.value
|
||||
val conn = chatModel.showingInvitation.value?.conn
|
||||
if (chatModel.showingInvitation.value?.connChatUsed == false && conn != null) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.keep_unused_invitation_question),
|
||||
@@ -205,8 +205,7 @@ private fun updateShownConnection(previousConnId: String, conn: PendingContactCo
|
||||
chatModel.showingInvitation.value = chatModel.showingInvitation.value?.copy(
|
||||
conn = conn,
|
||||
connId = conn.id,
|
||||
connLink = conn.connLinkInv ?: CreatedConnLink("", null),
|
||||
connChatUsed = true
|
||||
connLink = conn.connLinkInv ?: CreatedConnLink("", null)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -295,21 +294,19 @@ fun ActiveProfilePicker(
|
||||
showIncognito: Boolean = true
|
||||
) {
|
||||
val switchingProfile = remember { mutableStateOf(false) }
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val currentContactConnection = remember { mutableStateOf(contactConnection) }
|
||||
val incognito = remember {
|
||||
chatModel.showingInvitation.value?.conn?.incognito ?: controller.appPrefs.incognito.get()
|
||||
}
|
||||
val selectedProfile by remember { chatModel.currentUser }
|
||||
val searchTextOrPassword = rememberSaveable { search }
|
||||
// Intentionally don't use derivedStateOf in order to NOT change an order after user was selected.
|
||||
// Keyed on the count too, or a profile created from this picker is missing from it.
|
||||
val filteredProfiles = remember(searchTextOrPassword.value, chatModel.users.size) {
|
||||
filteredProfiles(chatModel.users.map { it.user }.sortedBy { !it.activeUser }, searchTextOrPassword.value)
|
||||
}
|
||||
|
||||
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
// Busy until the connection has moved onto the new profile, not just until it exists
|
||||
val busy = switchingProfile.value || chatModel.creatingProfileForInvitation.value
|
||||
|
||||
LaunchedEffect(busy) {
|
||||
@@ -328,8 +325,6 @@ fun ActiveProfilePicker(
|
||||
val conn = currentContactConnection.value
|
||||
if (conn != null) {
|
||||
updatedConn = controller.apiChangeConnectionUser(rhId, conn.pccConnId, user.userId)
|
||||
// Not moved - leave the picker open rather than stranding a profile just created
|
||||
// for this invitation. This call provisions a new queue, so it is what fails offline.
|
||||
if (updatedConn == null) return
|
||||
currentContactConnection.value = updatedConn
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -337,8 +332,6 @@ fun ActiveProfilePicker(
|
||||
updateShownConnection(conn.id, 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)
|
||||
|
||||
val switched = try {
|
||||
@@ -430,9 +423,6 @@ fun ActiveProfilePicker(
|
||||
val updatedConn = controller.apiSetConnectionIncognito(rhId, conn.pccConnId, true)
|
||||
if (updatedConn != null) {
|
||||
currentContactConnection.value = updatedConn
|
||||
// Only once the connection is actually incognito, as on the profile path
|
||||
// above: set before the call, a failure leaves the app-wide default on and
|
||||
// the next connection silently uses a random profile.
|
||||
appPreferences.incognito.set(true)
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateContactConnection(rhId, updatedConn)
|
||||
@@ -460,7 +450,6 @@ fun ActiveProfilePicker(
|
||||
) {
|
||||
LazyColumnWithScrollBar(Modifier.padding(top = topPaddingToContent(false)), userScrollEnabled = !busy) {
|
||||
item {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
if (oneHandUI.value) {
|
||||
Spacer(Modifier.padding(top = DEFAULT_PADDING + 5.dp))
|
||||
}
|
||||
@@ -501,15 +490,14 @@ fun ActiveProfilePicker(
|
||||
ProfilePickerUserOption(p)
|
||||
}
|
||||
}
|
||||
// Outside the branch above, or the row disappears when search filters the active
|
||||
// profile out. Only with a connection to move - the share list has none.
|
||||
if (contactConnection != null) {
|
||||
item {
|
||||
NewProfileOption()
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(Modifier.imePadding().padding(bottom = DEFAULT_BOTTOM_PADDING))
|
||||
val bottomBarClearance = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp
|
||||
Spacer(Modifier.imePadding().padding(bottom = DEFAULT_BOTTOM_PADDING + bottomBarClearance))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -597,7 +585,7 @@ private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contact
|
||||
search = search,
|
||||
close = close,
|
||||
rhId = rhId,
|
||||
contactConnection = contactConnection.value
|
||||
contactConnection = chatModel.showingInvitation.value?.conn
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user