diff --git a/app/src/main/kotlin/io/element/android/x/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt index b522edd137..e97a040e91 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -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() diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index a676df1d32..6c7e554c37 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -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, diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt index bf4f836294..5fa3beb36a 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt @@ -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, diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt index 73233fe453..2c4deab65e 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt @@ -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, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt index 6209c19be2..34adfeca99 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt @@ -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, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt index 5f95806469..ae32bb01a5 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt @@ -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(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, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt index 7cb9e9ce86..0525130048 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt @@ -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, val mediaPreviewConfigState: MediaPreviewConfigState, val eventSink: (AdvancedSettingsEvents) -> Unit ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt index 6cbe6e5c51..87df614074 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt @@ -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 { override val values: Sequence @@ -36,6 +38,7 @@ fun aAdvancedSettingsState( isSharePresenceEnabled: Boolean = false, mediaOptimizationState: MediaOptimizationState = MediaOptimizationState.AllMedia(isEnabled = false), theme: ThemeOption = ThemeOption.System, + availableThemeOptions: ImmutableList = ThemeOption.entries.toImmutableList(), hideInviteAvatars: Boolean = false, timelineMediaPreviewValue: MediaPreviewValue = MediaPreviewValue.On, setTimelineMediaPreviewAction: AsyncAction = AsyncAction.Uninitialized, @@ -46,6 +49,7 @@ fun aAdvancedSettingsState( isSharePresenceEnabled = isSharePresenceEnabled, mediaOptimizationState = mediaOptimizationState, theme = theme, + availableThemeOptions = availableThemeOptions, mediaPreviewConfigState = MediaPreviewConfigState( hideInviteAvatars = hideInviteAvatars, timelineMediaPreviewValue = timelineMediaPreviewValue, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt index 2e4d6f9ce5..230f5fa739 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt @@ -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)) } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt index 942d549dab..fe121e4fe9 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt @@ -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() diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt index 36fd30983e..e46e350415 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt @@ -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() diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt index fd00abb4f4..04cb90b746 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt @@ -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.mapToTheme(): Flow = map { +fun Flow.mapToTheme(allowBlackTheme: Boolean = true): Flow = map { when (it) { null -> Theme.System else -> Theme.valueOf(it) - } + }.coerceBlackTheme(allowBlackTheme) } diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt index 8fd7c5f041..3f43cfea67 100644 --- a/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt +++ b/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt @@ -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() + } + } } diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index bdb9a32e89..c159659fb0 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -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) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementThemeApp.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementThemeApp.kt index a7bd12bc13..c0700620bc 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementThemeApp.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementThemeApp.kt @@ -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) { diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 6cd9dec60c..40e99fa07f 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -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",