Keep proxy-chain deletion targets tied to stable row keys (#6200)

Store the pending member key instead of its list position and resolve that key when deletion is confirmed. Remove the member and its corresponding row key together; if the target no longer exists, leave the draft unchanged.

Extract the deletion fix from the accessibility assembly without importing accessibility actions, dialog wording changes, or drag refactors. Reuse the same key-based removal for blank members.

Validation: 75 JVM tests passed, including five deletion regressions; Play Store debug Kotlin compilation and x86_64 assembly passed. Emulator smoke check covered confirmation across rotation and immediate blank-member removal. Keyboard/D-pad and runtime reorder-during-confirmation scenarios were not run.
This commit is contained in:
eliotcougar authored and nerpa committed 2026-09-14 16:53:36 +03:00
1 parent 1fd05e16b5
commit 6a6a44a380
3 files changed
+87 -10

No files matched your search

@@ -0,0 +1,14 @@
package com.v2ray.ang.ui.server
/** Removes one draft member and its row key together, resolving its current position at confirmation. */
internal fun withoutProxyChainMember(
members: List<String>,
memberKeys: List<String>,
memberKey: String,
): Pair<List<String>, List<String>> {
val index = memberKeys.indexOf(memberKey)
if (index < 0) return members to memberKeys
return members.toMutableList().also { it.removeAt(index) } to
memberKeys.toMutableList().also { it.removeAt(index) }
}
@@ -203,7 +203,7 @@ fun ProxyChainScreen(
var members by rememberSaveable { mutableStateOf(initialMembers) }
var memberKeys by rememberSaveable { mutableStateOf(List(initialMembers.size) { UUID.randomUUID().toString() }) }
var showProfileDeleteConfirm by remember { mutableStateOf(false) }
var memberToDeleteIndex by rememberSaveable { mutableStateOf<Int?>(null) }
var memberToDeleteKey by rememberSaveable { mutableStateOf<String?>(null) }
val showDelete = editGuid.isNotEmpty() && !isRunning
val lazyListState = rememberLazyListState()
@@ -285,7 +285,8 @@ fun ProxyChainScreen(
}
itemsIndexed(items = members, key = { index, _ -> memberKeys[index] }) { index, member ->
ReorderableItem(reorderableState, key = memberKeys[index]) { isDragging ->
val memberKey = memberKeys[index]
ReorderableItem(reorderableState, key = memberKey) { isDragging ->
val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
Surface(shadowElevation = elevation) {
Row(
@@ -314,10 +315,11 @@ fun ProxyChainScreen(
)
IconButton(onClick = {
if (member.isBlank()) {
members = members.toMutableList().also { it.removeAt(index) }
memberKeys = memberKeys.toMutableList().also { it.removeAt(index) }
val (remainingMembers, remainingKeys) = withoutProxyChainMember(members, memberKeys, memberKey)
members = remainingMembers
memberKeys = remainingKeys
} else {
memberToDeleteIndex = index
memberToDeleteKey = memberKey
}
}) {
Icon(
@@ -339,15 +341,16 @@ fun ProxyChainScreen(
onDismiss = { showProfileDeleteConfirm = false }
)
}
memberToDeleteIndex?.let { index ->
memberToDeleteKey?.let { memberKey ->
DeleteConfirmDialog(
message = stringResource(R.string.confirm_delete_proxy_chain_member),
onConfirm = {
members = members.toMutableList().also { it.removeAt(index) }
memberKeys = memberKeys.toMutableList().also { it.removeAt(index) }
memberToDeleteIndex = null
val (remainingMembers, remainingKeys) = withoutProxyChainMember(members, memberKeys, memberKey)
members = remainingMembers
memberKeys = remainingKeys
memberToDeleteKey = null
},
onDismiss = { memberToDeleteIndex = null }
onDismiss = { memberToDeleteKey = null }
)
}
}
@@ -0,0 +1,60 @@
package com.v2ray.ang.ui.server
import org.junit.Assert.assertEquals
import org.junit.Assert.assertSame
import org.junit.Test
class ProxyChainMembersTest {
@Test
fun removalFollowsThePendingKeyAfterReordering() {
val pendingKey = "two"
val members = listOf("Two", "One", "Three")
val keys = listOf("two", "one", "three")
val (remainingMembers, remainingKeys) = withoutProxyChainMember(members, keys, pendingKey)
assertEquals(listOf("One", "Three"), remainingMembers)
assertEquals(listOf("one", "three"), remainingKeys)
assertEquals(listOf("Two", "One", "Three"), members)
assertEquals(listOf("two", "one", "three"), keys)
}
@Test
fun duplicateNamesDoNotChangeWhichMemberIsRemoved() {
val (members, keys) = withoutProxyChainMember(
listOf("Same", "Same", "Other"),
listOf("first", "second", "third"),
"second",
)
assertEquals(listOf("Same", "Other"), members)
assertEquals(listOf("first", "third"), keys)
}
@Test
fun missingMemberLeavesBothListsUnchanged() {
val members = listOf("One", "Three")
val keys = listOf("one", "three")
val (remainingMembers, remainingKeys) = withoutProxyChainMember(members, keys, "two")
assertSame(members, remainingMembers)
assertSame(keys, remainingKeys)
}
@Test
fun emptyChainRemainsEmpty() {
assertEquals(
emptyList<String>() to emptyList<String>(),
withoutProxyChainMember(emptyList(), emptyList(), "missing"),
)
}
@Test
fun blankMemberIsRemovedWithItsOwnKey() {
assertEquals(
listOf("One") to listOf("one"),
withoutProxyChainMember(listOf("One", ""), listOf("one", "blank"), "blank"),
)
}
}