From 828ee38c09ed68337ea88a931a5de0265e5908a5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 17 Oct 2025 10:44:03 +0200 Subject: [PATCH] Let the order of FeatureFlag follow the order they are declared in the code. Using map.keys does not guarantee that the order is kept, so using List instead. --- .../developer/DeveloperSettingsPresenter.kt | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index 7106b53503..73df8cbbfb 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -13,13 +13,13 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.key -import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.compose.runtime.snapshots.SnapshotStateList import dev.zacsweers.metro.Inject import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.preferences.impl.developer.tracing.toLogLevel @@ -31,7 +31,6 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState -import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildType @@ -61,12 +60,8 @@ class DeveloperSettingsPresenter( @Composable override fun present(): DeveloperSettingsState { val rageshakeState = rageshakePresenter.present() - - val features = remember { - mutableStateMapOf() - } val enabledFeatures = remember { - mutableStateMapOf() + mutableStateListOf() } val cacheSize = remember { mutableStateOf>(AsyncData.Uninitialized) @@ -105,11 +100,10 @@ class DeveloperSettingsPresenter( } } .forEach { feature -> - features[feature.key] = feature - enabledFeatures[feature.key] = featureFlagService.isFeatureEnabled(feature) + enabledFeatures.add(EnabledFeature(feature, featureFlagService.isFeatureEnabled(feature))) } } - val featureUiModels = createUiModels(features, enabledFeatures) + val featureUiModels = createUiModels(enabledFeatures) val coroutineScope = rememberCoroutineScope() // Compute cache size each time the clear cache action value is changed LaunchedEffect(clearCacheAction.value.isSuccess()) { @@ -119,7 +113,6 @@ class DeveloperSettingsPresenter( fun handleEvents(event: DeveloperSettingsEvents) { when (event) { is DeveloperSettingsEvents.UpdateEnabledFeature -> coroutineScope.updateEnabledFeature( - features, enabledFeatures, event.feature, event.isEnabled, @@ -172,19 +165,17 @@ class DeveloperSettingsPresenter( @Composable private fun createUiModels( - features: SnapshotStateMap, - enabledFeatures: SnapshotStateMap + enabledFeatures: SnapshotStateList, ): List { - return features.values.map { feature -> - key(feature.key) { - val isEnabled = enabledFeatures[feature.key].orFalse() - remember(feature, isEnabled) { + return enabledFeatures.map { enabledFeature -> + key(enabledFeature.feature.key) { + remember(enabledFeature) { FeatureUiModel( - key = feature.key, - title = feature.title, - description = feature.description, + key = enabledFeature.feature.key, + title = enabledFeature.feature.title, + description = enabledFeature.feature.description, icon = null, - isEnabled = isEnabled + isEnabled = enabledFeature.isEnabled ) } } @@ -192,15 +183,15 @@ class DeveloperSettingsPresenter( } private fun CoroutineScope.updateEnabledFeature( - features: SnapshotStateMap, - enabledFeatures: SnapshotStateMap, + enabledFeatures: SnapshotStateList, featureUiModel: FeatureUiModel, enabled: Boolean, @Suppress("UNUSED_PARAMETER") triggerClearCache: () -> Unit, ) = launch { - val feature = features[featureUiModel.key] ?: return@launch + val featureIndex = enabledFeatures.indexOfFirst { it.feature.key == featureUiModel.key }.takeIf { it != -1 } ?: return@launch + val feature = enabledFeatures[featureIndex].feature if (featureFlagService.setFeatureEnabled(feature, enabled)) { - enabledFeatures[featureUiModel.key] = enabled + enabledFeatures[featureIndex] = enabledFeatures[featureIndex].copy(isEnabled = enabled) } } @@ -225,3 +216,8 @@ private fun customElementCallUrlValidator(url: String?): Boolean { if (parsedUrl.host.isNullOrBlank()) error("Missing host") }.isSuccess } + +private data class EnabledFeature( + val feature: Feature, + val isEnabled: Boolean, +)