Add Labs screen for beta testing of public features (#5465)

* Add Labs screen:

- Make `Feature` have an `isInLabs` boolean to distinguish private feature flags from public ones.
- Have `FeatureFlagsService` provide the list of available flags.
- Display the labs item in the settings screen only if there are available public features.
- Remove public feature toggles from developer options.
- Implement the labs screen with the public features.
- Add a clear cache step to the threads feature toggle
- Update screenshots

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa 2025-10-07 12:02:54 +02:00 committed by GitHub
parent a497703a90
commit 9714abe032
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 684 additions and 17 deletions

View file

@ -36,4 +36,10 @@ interface Feature {
* If true: the feature is finished, it will not appear in the developer options screen.
*/
val isFinished: Boolean
/**
* Whether the feature is only available in Labs (and not in developer options).
* Feature flags that set this to `true` can be enabled by any users, not only those that have enabled developer mode.
*/
val isInLabs: Boolean
}

View file

@ -33,4 +33,9 @@ interface FeatureFlagService {
* is registered
*/
suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean
/**
* @return the list of available (not finished) features that can be toggled.
*/
fun getAvailableFeatures(): List<Feature>
}

View file

@ -19,6 +19,7 @@ enum class FeatureFlags(
override val description: String? = null,
override val defaultValue: (BuildMeta) -> Boolean,
override val isFinished: Boolean,
override val isInLabs: Boolean = false,
) : Feature {
RoomDirectorySearch(
key = "feature.roomdirectorysearch",
@ -98,6 +99,7 @@ enum class FeatureFlags(
description = "Renders thread messages as a dedicated timeline. Restarting the app is required for this setting to fully take effect.",
defaultValue = { false },
isFinished = false,
isInLabs = true,
),
MultiAccount(
key = "feature.multi_account",

View file

@ -14,6 +14,7 @@ import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.featureflag.api.Feature
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
@ -40,4 +41,8 @@ class DefaultFeatureFlagService(
?.let { true }
?: false
}
override fun getAvailableFeatures(): List<Feature> {
return FeatureFlags.entries.filter { !it.isFinished }
}
}

View file

@ -0,0 +1,20 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.featureflag.test
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.featureflag.api.Feature
data class FakeFeature(
override val key: String,
override val title: String,
override val description: String? = null,
override val defaultValue: (BuildMeta) -> Boolean = { false },
override val isFinished: Boolean = false,
override val isInLabs: Boolean = false,
) : Feature

View file

@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
class FakeFeatureFlagService(
initialState: Map<String, Boolean> = emptyMap(),
private val buildMeta: BuildMeta = aBuildMeta(),
var providedAvailableFeatures: List<Feature> = emptyList(),
) : FeatureFlagService {
private val enabledFeatures = initialState
.mapValues { MutableStateFlow(it.value) }
@ -31,4 +32,8 @@ class FakeFeatureFlagService(
override fun isFeatureEnabledFlow(feature: Feature): Flow<Boolean> {
return enabledFeatures.getOrPut(feature.key) { MutableStateFlow(feature.defaultValue(buildMeta)) }
}
override fun getAvailableFeatures(): List<Feature> {
return providedAvailableFeatures
}
}

View file

@ -7,9 +7,12 @@
package io.element.android.libraries.featureflag.ui.model
import io.element.android.libraries.designsystem.theme.components.IconSource
data class FeatureUiModel(
val key: String,
val title: String,
val description: String?,
val icon: IconSource?,
val isEnabled: Boolean
)

View file

@ -12,7 +12,7 @@ import kotlinx.collections.immutable.persistentListOf
fun aFeatureUiModelList(): ImmutableList<FeatureUiModel> {
return persistentListOf(
FeatureUiModel("key1", "Display State Events", "Show state events in the timeline", true),
FeatureUiModel("key2", "Display Room Events", null, false),
FeatureUiModel(key = "key1", title = "Display State Events", description = "Show state events in the timeline", icon = null, isEnabled = true),
FeatureUiModel(key = "key2", title = "Display Room Events", description = null, icon = null, isEnabled = false),
)
}