fix(qr): Preserve incoming channels when adding from QR (#6013)

This commit is contained in:
Jeremiah K
2026-06-29 11:17:19 -05:00
committed by GitHub
parent fef124bc1f
commit 2dba958e1e
3 changed files with 90 additions and 3 deletions

View File

@@ -63,6 +63,7 @@ import org.meshtastic.core.resources.replace
import org.meshtastic.core.resources.replace_channels_and_settings_description
import org.meshtastic.core.ui.component.ChannelSelection
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.util.mergeChannelSettingsForAdd
import org.meshtastic.proto.ChannelSet
@Composable
@@ -107,9 +108,7 @@ fun ScannedQrCodeDialog(
),
)
} else {
// To guarantee consistent ordering, using a LinkedHashSet which iterates through
// its entries according to the order an item was *first* inserted.
val result = (channels.settings + incoming.settings).distinct()
val result = mergeChannelSettingsForAdd(channels.settings, incoming.settings)
channels.copy(settings = result)
}
}

View File

@@ -154,3 +154,20 @@ suspend fun applyReplacementChannelSet(
}
radioConfigRepository.replaceAllSettings(channelSet.settings)
}
/**
* Builds the ADD-mode preview list for QR import. Existing channels are preserved at their positions; every incoming
* channel is appended in order without deduplication.
*
* Structural `.distinct()` was previously used here, but it silently dropped incoming channels that matched existing
* entries, shifting later channels to wrong indices and hiding them from the user. The caller (UI) lets the user select
* which incoming channels to keep.
*
* @param existing The current [ChannelSettings] list on the radio. Preserved in order.
* @param incoming The imported [ChannelSettings] list. Appended in order.
* @return The concatenated list `[existing..., incoming...]`.
*/
fun mergeChannelSettingsForAdd(
existing: List<ChannelSettings>,
incoming: List<ChannelSettings>,
): List<ChannelSettings> = existing + incoming

View File

@@ -141,4 +141,75 @@ class ProtoExtensionsTest {
assertEquals(2, result[2].index)
assertEquals(secondaryB, result[2].settings)
}
// --- mergeChannelSettingsForAdd tests ---
@Test
fun merge_preserves_all_existing_channels_in_order() {
val existing = listOf(ChannelSettings(name = "A"), ChannelSettings(name = "B"))
val result = mergeChannelSettingsForAdd(existing, incoming = emptyList())
assertEquals(2, result.size)
assertEquals("A", result[0].name)
assertEquals("B", result[1].name)
}
@Test
fun merge_appends_all_incoming_channels_in_order() {
val incoming = listOf(ChannelSettings(name = "C"), ChannelSettings(name = "D"))
val result = mergeChannelSettingsForAdd(existing = emptyList(), incoming)
assertEquals(2, result.size)
assertEquals("C", result[0].name)
assertEquals("D", result[1].name)
}
@Test
fun merge_preserves_structurally_equal_channels() {
val channel = ChannelSettings(name = "LongFast", psk = byteArrayOf(1).toByteString())
val result = mergeChannelSettingsForAdd(existing = listOf(channel), incoming = listOf(channel))
assertEquals(2, result.size)
}
@Test
fun merge_preserves_same_name_different_psk() {
val existingChan = ChannelSettings(name = "A", psk = byteArrayOf(1).toByteString())
val incomingChan = ChannelSettings(name = "A", psk = byteArrayOf(2).toByteString())
val result = mergeChannelSettingsForAdd(listOf(existingChan), listOf(incomingChan))
assertEquals(2, result.size)
}
@Test
fun merge_preserves_same_psk_different_name() {
val psk = byteArrayOf(1, 2).toByteString()
val existingChan = ChannelSettings(name = "A", psk = psk)
val incomingChan = ChannelSettings(name = "B", psk = psk)
val result = mergeChannelSettingsForAdd(listOf(existingChan), listOf(incomingChan))
assertEquals(2, result.size)
}
@Test
fun merge_preserves_duplicate_inside_incoming() {
val a = ChannelSettings(name = "A", psk = byteArrayOf(1).toByteString())
val b = ChannelSettings(name = "B", psk = byteArrayOf(2).toByteString())
val result = mergeChannelSettingsForAdd(existing = emptyList(), incoming = listOf(a, a, b))
assertEquals(3, result.size)
}
@Test
fun merge_both_empty_produces_empty_list() {
val result = mergeChannelSettingsForAdd(existing = emptyList(), incoming = emptyList())
assertTrue(result.isEmpty())
}
}