Skip to content

Commit e3968d8

Browse files
authored
Merge pull request #1844 from vector-im/feature/bma/themeSwitch
Feature/bma/theme switch
2 parents a8fbb88 + 5e95da9 commit e3968d8

File tree

21 files changed

+234
-15
lines changed

21 files changed

+234
-15
lines changed

app/src/main/kotlin/io/element/android/x/MainActivity.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import androidx.compose.foundation.layout.fillMaxSize
2525
import androidx.compose.material3.MaterialTheme
2626
import androidx.compose.runtime.Composable
2727
import androidx.compose.runtime.CompositionLocalProvider
28+
import androidx.compose.runtime.collectAsState
29+
import androidx.compose.runtime.getValue
30+
import androidx.compose.runtime.remember
2831
import androidx.compose.ui.Modifier
2932
import androidx.compose.ui.platform.LocalUriHandler
3033
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@@ -38,6 +41,9 @@ import io.element.android.libraries.architecture.bindings
3841
import io.element.android.libraries.core.log.logger.LoggerTag
3942
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
4043
import io.element.android.libraries.theme.ElementTheme
44+
import io.element.android.libraries.theme.theme.Theme
45+
import io.element.android.libraries.theme.theme.isDark
46+
import io.element.android.libraries.theme.theme.mapToTheme
4147
import io.element.android.x.di.AppBindings
4248
import io.element.android.x.intent.SafeUriHandler
4349
import timber.log.Timber
@@ -77,7 +83,13 @@ class MainActivity : NodeActivity() {
7783

7884
@Composable
7985
private fun MainContent(appBindings: AppBindings) {
80-
ElementTheme {
86+
val theme by remember {
87+
appBindings.preferencesStore().getThemeFlow().mapToTheme()
88+
}
89+
.collectAsState(initial = Theme.System)
90+
ElementTheme(
91+
darkTheme = theme.isDark()
92+
) {
8193
CompositionLocalProvider(
8294
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
8395
LocalUriHandler provides SafeUriHandler(this),

app/src/main/kotlin/io/element/android/x/di/AppBindings.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package io.element.android.x.di
1818

1919
import com.squareup.anvil.annotations.ContributesTo
2020
import io.element.android.features.lockscreen.api.LockScreenService
21+
import io.element.android.features.preferences.api.store.PreferencesStore
2122
import io.element.android.features.rageshake.api.reporter.BugReporter
2223
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
2324
import io.element.android.libraries.di.AppScope
@@ -29,4 +30,5 @@ interface AppBindings {
2930
fun tracingService(): TracingService
3031
fun bugReporter(): BugReporter
3132
fun lockScreenService(): LockScreenService
33+
fun preferencesStore(): PreferencesStore
3234
}

features/call/src/main/kotlin/io/element/android/features/call/ui/ElementCallActivity.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,22 @@ import android.webkit.PermissionRequest
3131
import androidx.activity.compose.setContent
3232
import androidx.activity.result.ActivityResultLauncher
3333
import androidx.activity.result.contract.ActivityResultContracts
34+
import androidx.compose.runtime.collectAsState
35+
import androidx.compose.runtime.getValue
3436
import androidx.compose.runtime.mutableStateOf
37+
import androidx.compose.runtime.remember
3538
import androidx.core.content.IntentCompat
3639
import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
3740
import io.element.android.features.call.CallForegroundService
3841
import io.element.android.features.call.CallType
3942
import io.element.android.features.call.di.CallBindings
4043
import io.element.android.features.call.utils.CallIntentDataParser
44+
import io.element.android.features.preferences.api.store.PreferencesStore
4145
import io.element.android.libraries.architecture.bindings
4246
import io.element.android.libraries.theme.ElementTheme
47+
import io.element.android.libraries.theme.theme.Theme
48+
import io.element.android.libraries.theme.theme.isDark
49+
import io.element.android.libraries.theme.theme.mapToTheme
4350
import javax.inject.Inject
4451

4552
class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
@@ -60,6 +67,7 @@ class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
6067

6168
@Inject lateinit var callIntentDataParser: CallIntentDataParser
6269
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
70+
@Inject lateinit var preferencesStore: PreferencesStore
6371

6472
private lateinit var presenter: CallScreenPresenter
6573

@@ -92,8 +100,14 @@ class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
92100
requestAudioFocus()
93101

94102
setContent {
103+
val theme by remember {
104+
preferencesStore.getThemeFlow().mapToTheme()
105+
}
106+
.collectAsState(initial = Theme.System)
95107
val state = presenter.present()
96-
ElementTheme {
108+
ElementTheme(
109+
darkTheme = theme.isDark()
110+
) {
97111
CallScreenView(
98112
state = state,
99113
requestPermissions = { permissions, callback ->

features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616

1717
package io.element.android.features.preferences.impl.advanced
1818

19+
import io.element.android.libraries.theme.theme.Theme
20+
1921
sealed interface AdvancedSettingsEvents {
2022
data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents
2123
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
24+
data object ChangeTheme : AdvancedSettingsEvents
25+
data object CancelChangeTheme : AdvancedSettingsEvents
26+
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
2227
}

features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@ package io.element.android.features.preferences.impl.advanced
1919
import androidx.compose.runtime.Composable
2020
import androidx.compose.runtime.collectAsState
2121
import androidx.compose.runtime.getValue
22+
import androidx.compose.runtime.mutableStateOf
23+
import androidx.compose.runtime.remember
2224
import androidx.compose.runtime.rememberCoroutineScope
25+
import androidx.compose.runtime.setValue
2326
import io.element.android.features.preferences.api.store.PreferencesStore
2427
import io.element.android.libraries.architecture.Presenter
28+
import io.element.android.libraries.theme.theme.Theme
29+
import io.element.android.libraries.theme.theme.mapToTheme
2530
import kotlinx.coroutines.launch
2631
import javax.inject.Inject
2732

@@ -38,7 +43,11 @@ class AdvancedSettingsPresenter @Inject constructor(
3843
val isDeveloperModeEnabled by preferencesStore
3944
.isDeveloperModeEnabledFlow()
4045
.collectAsState(initial = false)
41-
46+
val theme by remember {
47+
preferencesStore.getThemeFlow().mapToTheme()
48+
}
49+
.collectAsState(initial = Theme.System)
50+
var showChangeThemeDialog by remember { mutableStateOf(false) }
4251
fun handleEvents(event: AdvancedSettingsEvents) {
4352
when (event) {
4453
is AdvancedSettingsEvents.SetRichTextEditorEnabled -> localCoroutineScope.launch {
@@ -47,12 +56,20 @@ class AdvancedSettingsPresenter @Inject constructor(
4756
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch {
4857
preferencesStore.setDeveloperModeEnabled(event.enabled)
4958
}
59+
AdvancedSettingsEvents.CancelChangeTheme -> showChangeThemeDialog = false
60+
AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true
61+
is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch {
62+
preferencesStore.setTheme(event.theme.name)
63+
showChangeThemeDialog = false
64+
}
5065
}
5166
}
5267

5368
return AdvancedSettingsState(
5469
isRichTextEditorEnabled = isRichTextEditorEnabled,
5570
isDeveloperModeEnabled = isDeveloperModeEnabled,
71+
theme = theme,
72+
showChangeThemeDialog = showChangeThemeDialog,
5673
eventSink = { handleEvents(it) }
5774
)
5875
}

features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@
1616

1717
package io.element.android.features.preferences.impl.advanced
1818

19+
import io.element.android.libraries.theme.theme.Theme
20+
1921
data class AdvancedSettingsState(
2022
val isRichTextEditorEnabled: Boolean,
2123
val isDeveloperModeEnabled: Boolean,
24+
val theme: Theme,
25+
val showChangeThemeDialog: Boolean,
2226
val eventSink: (AdvancedSettingsEvents) -> Unit
2327
)

features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,26 @@
1717
package io.element.android.features.preferences.impl.advanced
1818

1919
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
20+
import io.element.android.libraries.theme.theme.Theme
2021

2122
open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSettingsState> {
2223
override val values: Sequence<AdvancedSettingsState>
2324
get() = sequenceOf(
2425
aAdvancedSettingsState(),
2526
aAdvancedSettingsState(isRichTextEditorEnabled = true),
2627
aAdvancedSettingsState(isDeveloperModeEnabled = true),
28+
aAdvancedSettingsState(showChangeThemeDialog = true),
2729
)
2830
}
2931

3032
fun aAdvancedSettingsState(
3133
isRichTextEditorEnabled: Boolean = false,
3234
isDeveloperModeEnabled: Boolean = false,
35+
showChangeThemeDialog: Boolean = false,
3336
) = AdvancedSettingsState(
3437
isRichTextEditorEnabled = isRichTextEditorEnabled,
3538
isDeveloperModeEnabled = isDeveloperModeEnabled,
39+
theme = Theme.System,
40+
showChangeThemeDialog = showChangeThemeDialog,
3641
eventSink = {}
3742
)

features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,20 @@ import androidx.compose.ui.Modifier
2121
import androidx.compose.ui.res.stringResource
2222
import androidx.compose.ui.tooling.preview.PreviewParameter
2323
import io.element.android.features.preferences.impl.R
24+
import io.element.android.libraries.designsystem.components.dialogs.ListOption
25+
import io.element.android.libraries.designsystem.components.dialogs.SingleSelectionDialog
26+
import io.element.android.libraries.designsystem.components.list.ListItemContent
2427
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
2528
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
2629
import io.element.android.libraries.designsystem.preview.ElementPreview
2730
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
31+
import io.element.android.libraries.designsystem.theme.components.ListItem
32+
import io.element.android.libraries.designsystem.theme.components.Text
33+
import io.element.android.libraries.theme.theme.Theme
34+
import io.element.android.libraries.theme.theme.themes
2835
import io.element.android.libraries.ui.strings.CommonStrings
36+
import kotlinx.collections.immutable.ImmutableList
37+
import kotlinx.collections.immutable.toImmutableList
2938

3039
@Composable
3140
fun AdvancedSettingsView(
@@ -38,6 +47,19 @@ fun AdvancedSettingsView(
3847
onBackPressed = onBackPressed,
3948
title = stringResource(id = CommonStrings.common_advanced_settings)
4049
) {
50+
ListItem(
51+
headlineContent = {
52+
Text(
53+
text = stringResource(id = CommonStrings.common_appearance)
54+
)
55+
},
56+
trailingContent = ListItemContent.Text(
57+
state.theme.toHumanReadable()
58+
),
59+
onClick = {
60+
state.eventSink(AdvancedSettingsEvents.ChangeTheme)
61+
}
62+
)
4163
PreferenceSwitch(
4264
title = stringResource(id = CommonStrings.common_rich_text_editor),
4365
subtitle = stringResource(id = R.string.screen_advanced_settings_rich_text_editor_description),
@@ -51,6 +73,39 @@ fun AdvancedSettingsView(
5173
onCheckedChange = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(it)) },
5274
)
5375
}
76+
77+
if (state.showChangeThemeDialog) {
78+
SingleSelectionDialog(
79+
options = getOptions(),
80+
initialSelection = themes.indexOf(state.theme),
81+
onOptionSelected = {
82+
state.eventSink(
83+
AdvancedSettingsEvents.SetTheme(
84+
themes[it]
85+
)
86+
)
87+
},
88+
onDismissRequest = { state.eventSink(AdvancedSettingsEvents.CancelChangeTheme) },
89+
)
90+
}
91+
}
92+
93+
@Composable
94+
private fun getOptions(): ImmutableList<ListOption> {
95+
return themes.map {
96+
ListOption(title = it.toHumanReadable())
97+
}.toImmutableList()
98+
}
99+
100+
@Composable
101+
private fun Theme.toHumanReadable(): String {
102+
return stringResource(
103+
id = when (this) {
104+
Theme.System -> CommonStrings.common_system
105+
Theme.Dark -> CommonStrings.common_dark
106+
Theme.Light -> CommonStrings.common_light
107+
}
108+
)
54109
}
55110

56111
@PreviewsDayNight

features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import app.cash.molecule.moleculeFlow
2121
import app.cash.turbine.test
2222
import com.google.common.truth.Truth.assertThat
2323
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
24+
import io.element.android.libraries.theme.theme.Theme
2425
import io.element.android.tests.testutils.WarmUpRule
2526
import io.element.android.tests.testutils.awaitLastSequentialItem
2627
import kotlinx.coroutines.test.runTest
@@ -42,6 +43,8 @@ class AdvancedSettingsPresenterTest {
4243
val initialState = awaitLastSequentialItem()
4344
assertThat(initialState.isDeveloperModeEnabled).isFalse()
4445
assertThat(initialState.isRichTextEditorEnabled).isFalse()
46+
assertThat(initialState.showChangeThemeDialog).isFalse()
47+
assertThat(initialState.theme).isEqualTo(Theme.System)
4548
}
4649
}
4750

@@ -76,4 +79,28 @@ class AdvancedSettingsPresenterTest {
7679
assertThat(awaitItem().isRichTextEditorEnabled).isFalse()
7780
}
7881
}
82+
83+
@Test
84+
fun `present - change theme`() = runTest {
85+
val store = InMemoryPreferencesStore()
86+
val presenter = AdvancedSettingsPresenter(store)
87+
moleculeFlow(RecompositionMode.Immediate) {
88+
presenter.present()
89+
}.test {
90+
val initialState = awaitLastSequentialItem()
91+
initialState.eventSink.invoke(AdvancedSettingsEvents.ChangeTheme)
92+
val withDialog = awaitItem()
93+
assertThat(withDialog.showChangeThemeDialog).isTrue()
94+
// Cancel
95+
withDialog.eventSink(AdvancedSettingsEvents.CancelChangeTheme)
96+
val withoutDialog = awaitItem()
97+
assertThat(withoutDialog.showChangeThemeDialog).isFalse()
98+
withDialog.eventSink.invoke(AdvancedSettingsEvents.ChangeTheme)
99+
assertThat(awaitItem().showChangeThemeDialog).isTrue()
100+
withDialog.eventSink(AdvancedSettingsEvents.SetTheme(Theme.Light))
101+
val withNewTheme = awaitItem()
102+
assertThat(withNewTheme.showChangeThemeDialog).isFalse()
103+
assertThat(withNewTheme.theme).isEqualTo(Theme.Light)
104+
}
105+
}
79106
}

libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/PreferencesStore.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,8 @@ interface PreferencesStore {
2828
suspend fun setCustomElementCallBaseUrl(string: String?)
2929
fun getCustomElementCallBaseUrlFlow(): Flow<String?>
3030

31+
suspend fun setTheme(theme: String)
32+
fun getThemeFlow(): Flow<String?>
33+
3134
suspend fun reset()
3235
}

0 commit comments

Comments
 (0)