Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/bma/theme switch #1844

Merged
merged 2 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion app/src/main/kotlin/io/element/android/x/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
Expand All @@ -38,6 +41,9 @@ import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.theme.theme.Theme
import io.element.android.libraries.theme.theme.isDark
import io.element.android.libraries.theme.theme.mapToTheme
import io.element.android.x.di.AppBindings
import io.element.android.x.intent.SafeUriHandler
import timber.log.Timber
Expand Down Expand Up @@ -77,7 +83,13 @@ class MainActivity : NodeActivity() {

@Composable
private fun MainContent(appBindings: AppBindings) {
ElementTheme {
val theme by remember {
appBindings.preferencesStore().getThemeFlow().mapToTheme()
}
.collectAsState(initial = Theme.System)
ElementTheme(
darkTheme = theme.isDark()
) {
CompositionLocalProvider(
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
LocalUriHandler provides SafeUriHandler(this),
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/kotlin/io/element/android/x/di/AppBindings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package io.element.android.x.di

import com.squareup.anvil.annotations.ContributesTo
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.di.AppScope
Expand All @@ -29,4 +30,5 @@ interface AppBindings {
fun tracingService(): TracingService
fun bugReporter(): BugReporter
fun lockScreenService(): LockScreenService
fun preferencesStore(): PreferencesStore
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,22 @@ import android.webkit.PermissionRequest
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.core.content.IntentCompat
import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
import io.element.android.features.call.CallForegroundService
import io.element.android.features.call.CallType
import io.element.android.features.call.di.CallBindings
import io.element.android.features.call.utils.CallIntentDataParser
import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.theme.theme.Theme
import io.element.android.libraries.theme.theme.isDark
import io.element.android.libraries.theme.theme.mapToTheme
import javax.inject.Inject

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

@Inject lateinit var callIntentDataParser: CallIntentDataParser
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
@Inject lateinit var preferencesStore: PreferencesStore

private lateinit var presenter: CallScreenPresenter

Expand Down Expand Up @@ -92,8 +100,14 @@ class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
requestAudioFocus()

setContent {
val theme by remember {
preferencesStore.getThemeFlow().mapToTheme()
}
.collectAsState(initial = Theme.System)
val state = presenter.present()
ElementTheme {
ElementTheme(
darkTheme = theme.isDark()
) {
CallScreenView(
state = state,
requestPermissions = { permissions, callback ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@

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

import io.element.android.libraries.theme.theme.Theme

sealed interface AdvancedSettingsEvents {
data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
data object ChangeTheme : AdvancedSettingsEvents
data object CancelChangeTheme : AdvancedSettingsEvents
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ package io.element.android.features.preferences.impl.advanced
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.theme.theme.Theme
import io.element.android.libraries.theme.theme.mapToTheme
import kotlinx.coroutines.launch
import javax.inject.Inject

Expand All @@ -38,7 +43,11 @@ class AdvancedSettingsPresenter @Inject constructor(
val isDeveloperModeEnabled by preferencesStore
.isDeveloperModeEnabledFlow()
.collectAsState(initial = false)

val theme by remember {
preferencesStore.getThemeFlow().mapToTheme()
}
.collectAsState(initial = Theme.System)
var showChangeThemeDialog by remember { mutableStateOf(false) }
fun handleEvents(event: AdvancedSettingsEvents) {
when (event) {
is AdvancedSettingsEvents.SetRichTextEditorEnabled -> localCoroutineScope.launch {
Expand All @@ -47,12 +56,20 @@ class AdvancedSettingsPresenter @Inject constructor(
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch {
preferencesStore.setDeveloperModeEnabled(event.enabled)
}
AdvancedSettingsEvents.CancelChangeTheme -> showChangeThemeDialog = false
AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true
is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch {
preferencesStore.setTheme(event.theme.name)
showChangeThemeDialog = false
}
}
}

return AdvancedSettingsState(
isRichTextEditorEnabled = isRichTextEditorEnabled,
isDeveloperModeEnabled = isDeveloperModeEnabled,
theme = theme,
showChangeThemeDialog = showChangeThemeDialog,
eventSink = { handleEvents(it) }
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@

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

import io.element.android.libraries.theme.theme.Theme

data class AdvancedSettingsState(
val isRichTextEditorEnabled: Boolean,
val isDeveloperModeEnabled: Boolean,
val theme: Theme,
val showChangeThemeDialog: Boolean,
val eventSink: (AdvancedSettingsEvents) -> Unit
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,26 @@
package io.element.android.features.preferences.impl.advanced

import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.theme.theme.Theme

open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSettingsState> {
override val values: Sequence<AdvancedSettingsState>
get() = sequenceOf(
aAdvancedSettingsState(),
aAdvancedSettingsState(isRichTextEditorEnabled = true),
aAdvancedSettingsState(isDeveloperModeEnabled = true),
aAdvancedSettingsState(showChangeThemeDialog = true),
)
}

fun aAdvancedSettingsState(
isRichTextEditorEnabled: Boolean = false,
isDeveloperModeEnabled: Boolean = false,
showChangeThemeDialog: Boolean = false,
) = AdvancedSettingsState(
isRichTextEditorEnabled = isRichTextEditorEnabled,
isDeveloperModeEnabled = isDeveloperModeEnabled,
theme = Theme.System,
showChangeThemeDialog = showChangeThemeDialog,
eventSink = {}
)
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.designsystem.components.dialogs.ListOption
import io.element.android.libraries.designsystem.components.dialogs.SingleSelectionDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.theme.theme.Theme
import io.element.android.libraries.theme.theme.themes
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList

@Composable
fun AdvancedSettingsView(
Expand All @@ -38,6 +47,19 @@ fun AdvancedSettingsView(
onBackPressed = onBackPressed,
title = stringResource(id = CommonStrings.common_advanced_settings)
) {
ListItem(
headlineContent = {
Text(
text = stringResource(id = CommonStrings.common_appearance)
)
},
trailingContent = ListItemContent.Text(
state.theme.toHumanReadable()
),
onClick = {
state.eventSink(AdvancedSettingsEvents.ChangeTheme)
}
)
PreferenceSwitch(
title = stringResource(id = CommonStrings.common_rich_text_editor),
subtitle = stringResource(id = R.string.screen_advanced_settings_rich_text_editor_description),
Expand All @@ -51,6 +73,39 @@ fun AdvancedSettingsView(
onCheckedChange = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(it)) },
)
}

if (state.showChangeThemeDialog) {
SingleSelectionDialog(
options = getOptions(),
initialSelection = themes.indexOf(state.theme),
onOptionSelected = {
state.eventSink(
AdvancedSettingsEvents.SetTheme(
themes[it]
)
)
},
onDismissRequest = { state.eventSink(AdvancedSettingsEvents.CancelChangeTheme) },
)
}
}

@Composable
private fun getOptions(): ImmutableList<ListOption> {
return themes.map {
ListOption(title = it.toHumanReadable())
}.toImmutableList()
}

@Composable
private fun Theme.toHumanReadable(): String {
return stringResource(
id = when (this) {
Theme.System -> CommonStrings.common_system
Theme.Dark -> CommonStrings.common_dark
Theme.Light -> CommonStrings.common_light
}
)
}

@PreviewsDayNight
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
import io.element.android.libraries.theme.theme.Theme
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem
import kotlinx.coroutines.test.runTest
Expand All @@ -42,6 +43,8 @@ class AdvancedSettingsPresenterTest {
val initialState = awaitLastSequentialItem()
assertThat(initialState.isDeveloperModeEnabled).isFalse()
assertThat(initialState.isRichTextEditorEnabled).isFalse()
assertThat(initialState.showChangeThemeDialog).isFalse()
assertThat(initialState.theme).isEqualTo(Theme.System)
}
}

Expand Down Expand Up @@ -76,4 +79,28 @@ class AdvancedSettingsPresenterTest {
assertThat(awaitItem().isRichTextEditorEnabled).isFalse()
}
}

@Test
fun `present - change theme`() = runTest {
val store = InMemoryPreferencesStore()
val presenter = AdvancedSettingsPresenter(store)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
initialState.eventSink.invoke(AdvancedSettingsEvents.ChangeTheme)
val withDialog = awaitItem()
assertThat(withDialog.showChangeThemeDialog).isTrue()
// Cancel
withDialog.eventSink(AdvancedSettingsEvents.CancelChangeTheme)
val withoutDialog = awaitItem()
assertThat(withoutDialog.showChangeThemeDialog).isFalse()
withDialog.eventSink.invoke(AdvancedSettingsEvents.ChangeTheme)
assertThat(awaitItem().showChangeThemeDialog).isTrue()
withDialog.eventSink(AdvancedSettingsEvents.SetTheme(Theme.Light))
val withNewTheme = awaitItem()
assertThat(withNewTheme.showChangeThemeDialog).isFalse()
assertThat(withNewTheme.theme).isEqualTo(Theme.Light)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,8 @@ interface PreferencesStore {
suspend fun setCustomElementCallBaseUrl(string: String?)
fun getCustomElementCallBaseUrlFlow(): Flow<String?>

suspend fun setTheme(theme: String)
fun getThemeFlow(): Flow<String?>

suspend fun reset()
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(na
private val richTextEditorKey = booleanPreferencesKey("richTextEditor")
private val developerModeKey = booleanPreferencesKey("developerMode")
private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl")
private val themeKey = stringPreferencesKey("theme")

@ContributesBinding(AppScope::class)
class DefaultPreferencesStore @Inject constructor(
Expand Down Expand Up @@ -89,6 +90,18 @@ class DefaultPreferencesStore @Inject constructor(
}
}

override suspend fun setTheme(theme: String) {
store.edit { prefs ->
prefs[themeKey] = theme
}
}

override fun getThemeFlow(): Flow<String?> {
return store.data.map { prefs ->
prefs[themeKey]
}
}

override suspend fun reset() {
store.edit { it.clear() }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ class InMemoryPreferencesStore(
isRichTextEditorEnabled: Boolean = false,
isDeveloperModeEnabled: Boolean = false,
customElementCallBaseUrl: String? = null,
theme: String? = null,
) : PreferencesStore {
private var _isRichTextEditorEnabled = MutableStateFlow(isRichTextEditorEnabled)
private var _isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled)
private var _customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl)
private var _theme = MutableStateFlow(theme)

override suspend fun setRichTextEditorEnabled(enabled: Boolean) {
_isRichTextEditorEnabled.value = enabled
Expand All @@ -53,6 +55,14 @@ class InMemoryPreferencesStore(
return _customElementCallBaseUrl
}

override suspend fun setTheme(theme: String) {
_theme.value = theme
}

override fun getThemeFlow(): Flow<String?> {
return _theme
}

override suspend fun reset() {
// No op
}
Expand Down
Loading