Add Advanced Settings screen in the preferences.

This commit is contained in:
Benoit Marty 2023-09-14 10:25:39 +02:00
parent 239730702d
commit 2d9cdd1acf
17 changed files with 524 additions and 1 deletions

View file

@ -42,6 +42,7 @@ dependencies {
implementation(projects.libraries.network)
implementation(projects.libraries.pushstore.api)
implementation(projects.libraries.pushstore.test)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
implementation(projects.features.rageshake.api)
@ -55,6 +56,7 @@ dependencies {
implementation(libs.accompanist.placeholder)
implementation(libs.coil.compose)
implementation(libs.androidx.browser)
implementation(libs.androidx.datastore.preferences)
api(projects.features.preferences.api)
ksp(libs.showkase.processor)

View file

@ -31,11 +31,12 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.preferences.impl.about.AboutNode
import io.element.android.features.preferences.impl.advanced.AdvancedSettingsNode
import io.element.android.features.preferences.impl.analytics.AnalyticsSettingsNode
import io.element.android.features.preferences.impl.developer.DeveloperSettingsNode
import io.element.android.features.preferences.impl.developer.tracing.ConfigureTracingNode
import io.element.android.features.preferences.impl.notifications.NotificationSettingsNode
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingNode
import io.element.android.features.preferences.impl.developer.tracing.ConfigureTracingNode
import io.element.android.features.preferences.impl.root.PreferencesRootNode
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
@ -63,6 +64,9 @@ class PreferencesFlowNode @AssistedInject constructor(
@Parcelize
data object DeveloperSettings : NavTarget
@Parcelize
data object AdvancedSettings : NavTarget
@Parcelize
data object ConfigureTracing : NavTarget
@ -106,6 +110,10 @@ class PreferencesFlowNode @AssistedInject constructor(
override fun onOpenNotificationSettings() {
backstack.push(NavTarget.NotificationSettings)
}
override fun onOpenAdvancedSettings() {
backstack.push(NavTarget.AdvancedSettings)
}
}
createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback))
}
@ -138,6 +146,9 @@ class PreferencesFlowNode @AssistedInject constructor(
val input = EditDefaultNotificationSettingNode.Inputs(navTarget.isOneToOne)
createNode<EditDefaultNotificationSettingNode>(buildContext, plugins = listOf(input))
}
NavTarget.AdvancedSettings -> {
createNode<AdvancedSettingsNode>(buildContext)
}
}
}

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.advanced
sealed interface AdvancedSettingsEvents {
data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
}

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.advanced
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
class AdvancedSettingsNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: AdvancedSettingsPresenter,
) : Node(buildContext, plugins = plugins) {
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
AdvancedSettingsView(
state = state,
modifier = modifier,
onBackPressed = ::navigateUp
)
}
}

View file

@ -0,0 +1,59 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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.rememberCoroutineScope
import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.launch
import javax.inject.Inject
class AdvancedSettingsPresenter @Inject constructor(
private val preferencesStore: PreferencesStore,
) : Presenter<AdvancedSettingsState> {
@Composable
override fun present(): AdvancedSettingsState {
val localCoroutineScope = rememberCoroutineScope()
val isRichTextEditorEnabled by preferencesStore
.isRichTextEditorEnabledFlow()
.collectAsState(initial = false)
val isDeveloperModeEnabled by preferencesStore
.isDevelopModeEnabledFlow()
.collectAsState(initial = false)
fun handleEvents(event: AdvancedSettingsEvents) {
when (event) {
is AdvancedSettingsEvents.SetRichTextEditorEnabled -> localCoroutineScope.launch {
preferencesStore.setRichTextEditorEnabled(event.enabled)
}
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch {
preferencesStore.setDevelopModeEnabled(event.enabled)
}
}
}
return AdvancedSettingsState(
isRichTextEditorEnabled = isRichTextEditorEnabled,
isDeveloperModeEnabled = isDeveloperModeEnabled,
eventSink = ::handleEvents
)
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.advanced
data class AdvancedSettingsState constructor(
val isRichTextEditorEnabled: Boolean,
val isDeveloperModeEnabled: Boolean,
val eventSink: (AdvancedSettingsEvents) -> Unit
)

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.advanced
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSettingsState> {
override val values: Sequence<AdvancedSettingsState>
get() = sequenceOf(
aAdvancedSettingsState(),
aAdvancedSettingsState(isRichTextEditorEnabled = true),
aAdvancedSettingsState(isDeveloperModeEnabled = true),
)
}
fun aAdvancedSettingsState(
isRichTextEditorEnabled: Boolean = false,
isDeveloperModeEnabled: Boolean = false,
) = AdvancedSettingsState(
isRichTextEditorEnabled = isRichTextEditorEnabled,
isDeveloperModeEnabled = isDeveloperModeEnabled,
eventSink = {}
)

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.advanced
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.components.preferences.PreferenceView
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun AdvancedSettingsView(
state: AdvancedSettingsState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
PreferenceView(
modifier = modifier,
onBackPressed = onBackPressed,
title = stringResource(id = CommonStrings.common_advanced_settings)
) {
PreferenceSwitch(
title = stringResource(id = CommonStrings.common_rich_text_editor),
// TODO i18n
subtitle = "Disable the rich text editor to type Markdown manually",
isChecked = state.isRichTextEditorEnabled,
onCheckedChange = { state.eventSink(AdvancedSettingsEvents.SetRichTextEditorEnabled(it)) },
)
PreferenceSwitch(
// TODO i18n
title = "Developer mode",
// TODO i18n
subtitle = "The developer mode activates hidden features. For developers only!",
isChecked = state.isDeveloperModeEnabled,
onCheckedChange = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(it)) },
)
}
}
@DayNightPreviews
@Composable
internal fun AdvancedSettingsViewPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) =
ElementPreview {
AdvancedSettingsView(state = state, onBackPressed = { })
}

View file

@ -46,6 +46,7 @@ class PreferencesRootNode @AssistedInject constructor(
fun onOpenAbout()
fun onOpenDeveloperSettings()
fun onOpenNotificationSettings()
fun onOpenAdvancedSettings()
}
private fun onOpenBugReport() {
@ -60,6 +61,10 @@ class PreferencesRootNode @AssistedInject constructor(
plugins<Callback>().forEach { it.onOpenDeveloperSettings() }
}
private fun onOpenAdvancedSettings() {
plugins<Callback>().forEach { it.onOpenAdvancedSettings() }
}
private fun onOpenAnalytics() {
plugins<Callback>().forEach { it.onOpenAnalytics() }
}
@ -100,6 +105,7 @@ class PreferencesRootNode @AssistedInject constructor(
onOpenAbout = this::onOpenAbout,
onVerifyClicked = this::onVerifyClicked,
onOpenDeveloperSettings = this::onOpenDeveloperSettings,
onOpenAdvancedSettings = this::onOpenAdvancedSettings,
onSuccessLogout = { onSuccessLogout(activity, it) },
onManageAccountClicked = { onManageAccountClicked(activity, it, isDark) },
onOpenNotificationSettings = this::onOpenNotificationSettings

View file

@ -25,6 +25,7 @@ import androidx.compose.material.icons.outlined.Help
import androidx.compose.material.icons.outlined.InsertChart
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.OpenInNew
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.VerifiedUser
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -58,6 +59,7 @@ fun PreferencesRootView(
onOpenRageShake: () -> Unit,
onOpenAbout: () -> Unit,
onOpenDeveloperSettings: () -> Unit,
onOpenAdvancedSettings: () -> Unit,
onSuccessLogout: (logoutUrlResult: String?) -> Unit,
onOpenNotificationSettings: () -> Unit,
modifier: Modifier = Modifier,
@ -125,6 +127,13 @@ fun PreferencesRootView(
DeveloperPreferencesView(onOpenDeveloperSettings)
HorizontalDivider()
}
HorizontalDivider()
PreferenceText(
title = stringResource(id = CommonStrings.common_advanced_settings),
icon = Icons.Outlined.Settings,
onClick = onOpenAdvancedSettings,
)
HorizontalDivider()
LogoutPreferenceView(
state = state.logoutState,
onSuccessLogout = onSuccessLogout,
@ -168,6 +177,7 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
onOpenAnalytics = {},
onOpenRageShake = {},
onOpenDeveloperSettings = {},
onOpenAdvancedSettings = {},
onOpenAbout = {},
onVerifyClicked = {},
onSuccessLogout = {},

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.preferences.api"
}
dependencies {
implementation(libs.coroutines.core)
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.api.store
import kotlinx.coroutines.flow.Flow
interface PreferencesStore {
suspend fun setRichTextEditorEnabled(enabled: Boolean)
fun isRichTextEditorEnabledFlow(): Flow<Boolean>
suspend fun setDevelopModeEnabled(enabled: Boolean)
fun isDevelopModeEnabledFlow(): Flow<Boolean>
suspend fun reset()
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("io.element.android-library")
alias(libs.plugins.anvil)
}
android {
namespace = "io.element.android.libraries.preferences.impl"
}
anvil {
generateDaggerFactories.set(true)
}
dependencies {
api(projects.libraries.preferences.api)
implementation(libs.dagger)
implementation(libs.androidx.datastore.preferences)
implementation(projects.libraries.di)
implementation(projects.libraries.core)
}

View file

@ -0,0 +1,75 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.preferences.impl.store
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.bool.orTrue
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_preferences")
private val richTextEditorKey = booleanPreferencesKey("richTextEditor")
private val developerModeKey = booleanPreferencesKey("developerMode")
@ContributesBinding(AppScope::class)
class DefaultPreferencesStore @Inject constructor(
@ApplicationContext context: Context
) : PreferencesStore {
private val store = context.dataStore
override suspend fun setRichTextEditorEnabled(enabled: Boolean) {
store.edit { prefs ->
prefs[richTextEditorKey] = enabled
}
}
override fun isRichTextEditorEnabledFlow(): Flow<Boolean> {
return store.data.map { prefs ->
// enabled by default
prefs[richTextEditorKey].orTrue()
}
}
override suspend fun setDevelopModeEnabled(enabled: Boolean) {
store.edit { prefs ->
prefs[developerModeKey] = enabled
}
}
override fun isDevelopModeEnabledFlow(): Flow<Boolean> {
return store.data.map { prefs ->
// disabled by default
prefs[developerModeKey].orFalse()
}
}
override suspend fun reset() {
store.edit { it.clear() }
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.preferences.test"
dependencies {
api(projects.libraries.preferences.api)
implementation(libs.coroutines.core)
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.featureflag.test
import io.element.android.features.preferences.api.store.PreferencesStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class InMemoryPreferencesStore(
isRichTextEditorEnabled: Boolean = false,
isDeveloperModeEnabled: Boolean = false,
) : PreferencesStore {
private var _isRichTextEditorEnabled = MutableStateFlow(isRichTextEditorEnabled)
private var _isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled)
override suspend fun setRichTextEditorEnabled(enabled: Boolean) {
_isRichTextEditorEnabled.value = enabled
}
override fun isRichTextEditorEnabledFlow(): Flow<Boolean> {
return _isRichTextEditorEnabled
}
override suspend fun setDevelopModeEnabled(enabled: Boolean) {
_isDeveloperModeEnabled.value = enabled
}
override fun isDevelopModeEnabledFlow(): Flow<Boolean> {
return _isDeveloperModeEnabled
}
override suspend fun reset() {
// No op
}
}

View file

@ -92,6 +92,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
implementation(project(":libraries:pushproviders:unifiedpush"))
implementation(project(":libraries:featureflag:impl"))
implementation(project(":libraries:pushstore:impl"))
implementation(project(":libraries:preferences:impl"))
implementation(project(":libraries:architecture"))
implementation(project(":libraries:dateformatter:impl"))
implementation(project(":libraries:di"))