Add "Allow black theme" feature flag
This commit is contained in:
parent
104ae4752a
commit
5e6a6af409
16 changed files with 139 additions and 9 deletions
|
|
@ -71,6 +71,7 @@ class MainActivity : NodeActivity() {
|
|||
}.collectAsState(SemanticColorsLightDark.default)
|
||||
ElementThemeApp(
|
||||
appPreferencesStore = appBindings.preferencesStore(),
|
||||
featureFlagService = appBindings.featureFlagService(),
|
||||
compoundLight = colors.light,
|
||||
compoundDark = colors.dark,
|
||||
buildMeta = appBindings.buildMeta()
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
|||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -144,6 +145,7 @@ class LoggedInFlowNode(
|
|||
private val syncService: SyncService,
|
||||
private val enterpriseService: EnterpriseService,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val buildMeta: BuildMeta,
|
||||
snackbarDispatcher: SnackbarDispatcher,
|
||||
private val analyticsService: AnalyticsService,
|
||||
|
|
@ -667,6 +669,7 @@ class LoggedInFlowNode(
|
|||
}.collectAsState(SemanticColorsLightDark.default)
|
||||
ElementThemeApp(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
featureFlagService = featureFlagService,
|
||||
compoundLight = colors.light,
|
||||
compoundDark = colors.dark,
|
||||
buildMeta = buildMeta,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import io.element.android.libraries.audio.api.AudioFocusRequester
|
|||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import timber.log.Timber
|
||||
|
||||
|
|
@ -66,6 +67,7 @@ class ElementCallActivity :
|
|||
@Inject lateinit var callIntentDataParser: CallIntentDataParser
|
||||
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
|
||||
@Inject lateinit var appPreferencesStore: AppPreferencesStore
|
||||
@Inject lateinit var featureFlagService: FeatureFlagService
|
||||
@Inject lateinit var enterpriseService: EnterpriseService
|
||||
@Inject lateinit var pictureInPicturePresenter: PictureInPicturePresenter
|
||||
@Inject lateinit var buildMeta: BuildMeta
|
||||
|
|
@ -114,6 +116,7 @@ class ElementCallActivity :
|
|||
}.collectAsState(SemanticColorsLightDark.default)
|
||||
ElementThemeApp(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
featureFlagService = featureFlagService,
|
||||
compoundLight = colors.light,
|
||||
compoundDark = colors.dark,
|
||||
buildMeta = buildMeta,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import io.element.android.libraries.architecture.bindings
|
|||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.filter
|
||||
|
|
@ -57,6 +58,9 @@ class IncomingCallActivity : AppCompatActivity() {
|
|||
@Inject
|
||||
lateinit var appPreferencesStore: AppPreferencesStore
|
||||
|
||||
@Inject
|
||||
lateinit var featureFlagService: FeatureFlagService
|
||||
|
||||
@Inject
|
||||
lateinit var enterpriseService: EnterpriseService
|
||||
|
||||
|
|
@ -88,6 +92,7 @@ class IncomingCallActivity : AppCompatActivity() {
|
|||
}.collectAsState(SemanticColorsLightDark.default)
|
||||
ElementThemeApp(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
featureFlagService = featureFlagService,
|
||||
compoundLight = colors.light,
|
||||
compoundDark = colors.dark,
|
||||
buildMeta = buildMeta,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import io.element.android.features.lockscreen.impl.unlock.di.PinUnlockBindings
|
|||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -43,6 +44,7 @@ class PinUnlockActivity : AppCompatActivity() {
|
|||
@Inject lateinit var presenter: PinUnlockPresenter
|
||||
@Inject lateinit var lockScreenService: LockScreenService
|
||||
@Inject lateinit var appPreferencesStore: AppPreferencesStore
|
||||
@Inject lateinit var featureFlagService: FeatureFlagService
|
||||
@Inject lateinit var enterpriseService: EnterpriseService
|
||||
@Inject lateinit var buildMeta: BuildMeta
|
||||
|
||||
|
|
@ -56,6 +58,7 @@ class PinUnlockActivity : AppCompatActivity() {
|
|||
}.collectAsState(SemanticColorsLightDark.default)
|
||||
ElementThemeApp(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
featureFlagService = featureFlagService,
|
||||
compoundLight = colors.light,
|
||||
compoundDark = colors.dark,
|
||||
buildMeta = buildMeta,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService
|
|||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
|
@ -45,8 +46,11 @@ class AdvancedSettingsPresenter(
|
|||
val isSharePresenceEnabled by remember {
|
||||
sessionPreferencesStore.isSharePresenceEnabled()
|
||||
}.collectAsState(initial = true)
|
||||
val theme = remember {
|
||||
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||
val isBlackThemeAllowed by remember {
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.AllowBlackTheme)
|
||||
}.collectAsState(initial = false)
|
||||
val theme = remember(isBlackThemeAllowed) {
|
||||
appPreferencesStore.getThemeFlow().mapToTheme(isBlackThemeAllowed)
|
||||
}.collectAsState(initial = Theme.System)
|
||||
|
||||
val mediaPreviewConfigState = mediaPreviewConfigStateStore.state()
|
||||
|
|
@ -66,6 +70,14 @@ class AdvancedSettingsPresenter(
|
|||
value = featureFlagService.isFeatureEnabled(FeatureFlags.SelectableMediaQuality)
|
||||
}
|
||||
|
||||
val availableThemeOptions = remember(isBlackThemeAllowed) {
|
||||
if (isBlackThemeAllowed) {
|
||||
ThemeOption.entries
|
||||
} else {
|
||||
ThemeOption.entries.filterNot { it == ThemeOption.Black }
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
val mediaOptimizationState by produceState<MediaOptimizationState?>(null) {
|
||||
val hasSplitMediaQualityOptionsFlow = featureFlagService.isFeatureEnabledFlow(FeatureFlags.SelectableMediaQuality)
|
||||
combine(
|
||||
|
|
@ -119,6 +131,7 @@ class AdvancedSettingsPresenter(
|
|||
isSharePresenceEnabled = isSharePresenceEnabled,
|
||||
mediaOptimizationState = mediaOptimizationState,
|
||||
theme = themeOption,
|
||||
availableThemeOptions = availableThemeOptions,
|
||||
mediaPreviewConfigState = mediaPreviewConfigState,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,12 +14,14 @@ import androidx.compose.ui.res.stringResource
|
|||
import io.element.android.libraries.designsystem.components.preferences.DropdownOption
|
||||
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class AdvancedSettingsState(
|
||||
val isDeveloperModeEnabled: Boolean,
|
||||
val isSharePresenceEnabled: Boolean,
|
||||
val mediaOptimizationState: MediaOptimizationState?,
|
||||
val theme: ThemeOption,
|
||||
val availableThemeOptions: ImmutableList<ThemeOption>,
|
||||
val mediaPreviewConfigState: MediaPreviewConfigState,
|
||||
val eventSink: (AdvancedSettingsEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
|
||||
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSettingsState> {
|
||||
override val values: Sequence<AdvancedSettingsState>
|
||||
|
|
@ -36,6 +38,7 @@ fun aAdvancedSettingsState(
|
|||
isSharePresenceEnabled: Boolean = false,
|
||||
mediaOptimizationState: MediaOptimizationState = MediaOptimizationState.AllMedia(isEnabled = false),
|
||||
theme: ThemeOption = ThemeOption.System,
|
||||
availableThemeOptions: ImmutableList<ThemeOption> = ThemeOption.entries.toImmutableList(),
|
||||
hideInviteAvatars: Boolean = false,
|
||||
timelineMediaPreviewValue: MediaPreviewValue = MediaPreviewValue.On,
|
||||
setTimelineMediaPreviewAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
|
|
@ -46,6 +49,7 @@ fun aAdvancedSettingsState(
|
|||
isSharePresenceEnabled = isSharePresenceEnabled,
|
||||
mediaOptimizationState = mediaOptimizationState,
|
||||
theme = theme,
|
||||
availableThemeOptions = availableThemeOptions,
|
||||
mediaPreviewConfigState = MediaPreviewConfigState(
|
||||
hideInviteAvatars = hideInviteAvatars,
|
||||
timelineMediaPreviewValue = timelineMediaPreviewValue,
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
|
|||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun AdvancedSettingsView(
|
||||
|
|
@ -75,7 +74,7 @@ fun AdvancedSettingsView(
|
|||
PreferenceDropdown(
|
||||
title = stringResource(id = CommonStrings.common_appearance),
|
||||
selectedOption = state.theme,
|
||||
options = ThemeOption.entries.toImmutableList(),
|
||||
options = state.availableThemeOptions,
|
||||
onSelectOption = { themeOption ->
|
||||
state.eventSink(AdvancedSettingsEvents.SetTheme(themeOption))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import app.cash.molecule.RecompositionMode
|
|||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
|
|
@ -20,6 +21,7 @@ import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
|
|||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
|
|
@ -40,6 +42,9 @@ class AdvancedSettingsPresenterTest {
|
|||
assertThat(isSharePresenceEnabled).isTrue()
|
||||
assertThat(mediaOptimizationState).isNull()
|
||||
assertThat(theme).isEqualTo(ThemeOption.System)
|
||||
assertThat(availableThemeOptions).isEqualTo(
|
||||
listOf(ThemeOption.System, ThemeOption.Light, ThemeOption.Dark).toImmutableList()
|
||||
)
|
||||
assertThat(mediaPreviewConfigState.hideInviteAvatars).isFalse()
|
||||
assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On)
|
||||
assertThat(mediaPreviewConfigState.setHideInviteAvatarsAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
|
|
@ -204,6 +209,42 @@ class AdvancedSettingsPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - black theme option shown when feature flag enabled`() = runTest {
|
||||
val presenter = createAdvancedSettingsPresenter(
|
||||
featureFlagService = FakeFeatureFlagService().apply {
|
||||
setFeatureEnabled(FeatureFlags.AllowBlackTheme, true)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
|
||||
with(awaitItem()) {
|
||||
assertThat(availableThemeOptions).contains(ThemeOption.Black)
|
||||
assertThat(availableThemeOptions).isEqualTo(ThemeOption.entries.toImmutableList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - stored black theme falls back to dark when feature flag disabled`() = runTest {
|
||||
val appPreferencesStore = InMemoryAppPreferencesStore().apply {
|
||||
setTheme(Theme.Black.name)
|
||||
}
|
||||
val presenter = createAdvancedSettingsPresenter(appPreferencesStore = appPreferencesStore)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
|
||||
with(awaitItem()) {
|
||||
assertThat(theme).isEqualTo(ThemeOption.Dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - hide invite avatars`() = runTest {
|
||||
val mediaPreviewStore = FakeMediaPreviewConfigStateStore()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import androidx.activity.ComponentActivity
|
|||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
|
|
@ -24,9 +25,11 @@ import io.element.android.services.analytics.compose.LocalAnalyticsService
|
|||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.assertNoNodeWithText
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
|
|
@ -65,6 +68,31 @@ class AdvancedSettingsViewTest {
|
|||
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTheme(ThemeOption.Dark))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `black theme is shown when available`() {
|
||||
rule.setAdvancedSettingsView(
|
||||
state = aAdvancedSettingsState(
|
||||
availableThemeOptions = ThemeOption.entries.toImmutableList(),
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_appearance)
|
||||
rule.run {
|
||||
val text = activity.getString(CommonStrings.common_black)
|
||||
onNodeWithText(text).assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `black theme is hidden when unavailable`() {
|
||||
rule.setAdvancedSettingsView(
|
||||
state = aAdvancedSettingsState(
|
||||
availableThemeOptions = ThemeOption.entries.filterNot { it == ThemeOption.Black }.toImmutableList(),
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_appearance)
|
||||
rule.assertNoNodeWithText(CommonStrings.common_black)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on View source emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ enum class Theme {
|
|||
Light,
|
||||
}
|
||||
|
||||
private fun Theme.coerceBlackTheme(allowBlackTheme: Boolean): Theme {
|
||||
return if (this == Theme.Black && !allowBlackTheme) Theme.Dark else this
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Theme.isDark(): Boolean {
|
||||
return when (this) {
|
||||
|
|
@ -29,9 +33,9 @@ fun Theme.isDark(): Boolean {
|
|||
}
|
||||
}
|
||||
|
||||
fun Flow<String?>.mapToTheme(): Flow<Theme> = map {
|
||||
fun Flow<String?>.mapToTheme(allowBlackTheme: Boolean = true): Flow<Theme> = map {
|
||||
when (it) {
|
||||
null -> Theme.System
|
||||
else -> Theme.valueOf(it)
|
||||
}
|
||||
}.coerceBlackTheme(allowBlackTheme)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import app.cash.molecule.RecompositionMode
|
|||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -72,4 +73,14 @@ class ThemeTest {
|
|||
assertThat(awaitItem()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapToTheme falls back to dark when black theme is disabled`() = runTest {
|
||||
flowOf(Theme.Black.name)
|
||||
.mapToTheme(allowBlackTheme = false)
|
||||
.test {
|
||||
assertThat(awaitItem()).isEqualTo(Theme.Dark)
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ dependencies {
|
|||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import io.element.android.compound.theme.mapToTheme
|
|||
import io.element.android.compound.tokens.generated.SemanticColors
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
|
||||
val LocalBuildMeta = staticCompositionLocalOf {
|
||||
|
|
@ -53,15 +55,18 @@ val LocalBuildMeta = staticCompositionLocalOf {
|
|||
@Composable
|
||||
fun ElementThemeApp(
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
featureFlagService: FeatureFlagService,
|
||||
compoundLight: SemanticColors,
|
||||
compoundDark: SemanticColors,
|
||||
buildMeta: BuildMeta,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val isBlackThemeAllowed by remember {
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.AllowBlackTheme)
|
||||
}.collectAsState(initial = false)
|
||||
val theme by remember {
|
||||
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||
}
|
||||
.collectAsState(initial = Theme.System)
|
||||
appPreferencesStore.getThemeFlow().mapToTheme(allowBlackTheme = isBlackThemeAllowed)
|
||||
}.collectAsState(initial = Theme.System)
|
||||
LaunchedEffect(theme) {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
when (theme) {
|
||||
|
|
|
|||
|
|
@ -147,6 +147,13 @@ enum class FeatureFlags(
|
|||
defaultValue = { false },
|
||||
isFinished = false,
|
||||
),
|
||||
AllowBlackTheme(
|
||||
key = "feature.allow_black_theme",
|
||||
title = "Allow black theme",
|
||||
description = "Allow selecting the black appearance theme for battery saving on OLED.",
|
||||
defaultValue = { false },
|
||||
isFinished = false,
|
||||
),
|
||||
LiveLocationSharing(
|
||||
key = "feature.liveLocationSharing",
|
||||
title = "Live location sharing",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue