Add 'send private read receipts' option in advanced settings (#2290)
* Add 'send private read receipts' option in advanced settings * Create `SessionPreferencesStore` that stores the settings for the current use separate from those of the app. * Rename `PreferencesStore` to `AppPreferencesStore` to split the preferences. --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
b7945675c9
commit
7e58f719fe
38 changed files with 314 additions and 85 deletions
|
|
@ -21,6 +21,7 @@ import io.element.android.compound.theme.Theme
|
|||
sealed interface AdvancedSettingsEvents {
|
||||
data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||
data class SetSendPublicReadReceiptsEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||
data object ChangeTheme : AdvancedSettingsEvents
|
||||
data object CancelChangeTheme : AdvancedSettingsEvents
|
||||
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
|
||||
|
|
|
|||
|
|
@ -25,40 +25,48 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.compound.theme.mapToTheme
|
||||
import io.element.android.features.preferences.api.store.PreferencesStore
|
||||
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class AdvancedSettingsPresenter @Inject constructor(
|
||||
private val preferencesStore: PreferencesStore,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||
) : Presenter<AdvancedSettingsState> {
|
||||
@Composable
|
||||
override fun present(): AdvancedSettingsState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val isRichTextEditorEnabled by preferencesStore
|
||||
val isRichTextEditorEnabled by appPreferencesStore
|
||||
.isRichTextEditorEnabledFlow()
|
||||
.collectAsState(initial = false)
|
||||
val isDeveloperModeEnabled by preferencesStore
|
||||
val isDeveloperModeEnabled by appPreferencesStore
|
||||
.isDeveloperModeEnabledFlow()
|
||||
.collectAsState(initial = false)
|
||||
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore
|
||||
.isSendPublicReadReceiptsEnabled()
|
||||
.collectAsState(initial = true)
|
||||
val theme by remember {
|
||||
preferencesStore.getThemeFlow().mapToTheme()
|
||||
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||
}
|
||||
.collectAsState(initial = Theme.System)
|
||||
var showChangeThemeDialog by remember { mutableStateOf(false) }
|
||||
fun handleEvents(event: AdvancedSettingsEvents) {
|
||||
when (event) {
|
||||
is AdvancedSettingsEvents.SetRichTextEditorEnabled -> localCoroutineScope.launch {
|
||||
preferencesStore.setRichTextEditorEnabled(event.enabled)
|
||||
appPreferencesStore.setRichTextEditorEnabled(event.enabled)
|
||||
}
|
||||
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch {
|
||||
preferencesStore.setDeveloperModeEnabled(event.enabled)
|
||||
appPreferencesStore.setDeveloperModeEnabled(event.enabled)
|
||||
}
|
||||
is AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled -> localCoroutineScope.launch {
|
||||
sessionPreferencesStore.setSendPublicReadReceipts(event.enabled)
|
||||
}
|
||||
AdvancedSettingsEvents.CancelChangeTheme -> showChangeThemeDialog = false
|
||||
AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true
|
||||
is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch {
|
||||
preferencesStore.setTheme(event.theme.name)
|
||||
appPreferencesStore.setTheme(event.theme.name)
|
||||
showChangeThemeDialog = false
|
||||
}
|
||||
}
|
||||
|
|
@ -67,6 +75,7 @@ class AdvancedSettingsPresenter @Inject constructor(
|
|||
return AdvancedSettingsState(
|
||||
isRichTextEditorEnabled = isRichTextEditorEnabled,
|
||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||
isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled,
|
||||
theme = theme,
|
||||
showChangeThemeDialog = showChangeThemeDialog,
|
||||
eventSink = { handleEvents(it) }
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import io.element.android.compound.theme.Theme
|
|||
data class AdvancedSettingsState(
|
||||
val isRichTextEditorEnabled: Boolean,
|
||||
val isDeveloperModeEnabled: Boolean,
|
||||
val isSendPublicReadReceiptsEnabled: Boolean,
|
||||
val theme: Theme,
|
||||
val showChangeThemeDialog: Boolean,
|
||||
val eventSink: (AdvancedSettingsEvents) -> Unit
|
||||
|
|
|
|||
|
|
@ -26,16 +26,19 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSett
|
|||
aAdvancedSettingsState(isRichTextEditorEnabled = true),
|
||||
aAdvancedSettingsState(isDeveloperModeEnabled = true),
|
||||
aAdvancedSettingsState(showChangeThemeDialog = true),
|
||||
aAdvancedSettingsState(isSendPublicReadReceiptsEnabled = true),
|
||||
)
|
||||
}
|
||||
|
||||
fun aAdvancedSettingsState(
|
||||
isRichTextEditorEnabled: Boolean = false,
|
||||
isDeveloperModeEnabled: Boolean = false,
|
||||
isSendPublicReadReceiptsEnabled: Boolean = false,
|
||||
showChangeThemeDialog: Boolean = false,
|
||||
) = AdvancedSettingsState(
|
||||
isRichTextEditorEnabled = isRichTextEditorEnabled,
|
||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||
isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled,
|
||||
theme = Theme.System,
|
||||
showChangeThemeDialog = showChangeThemeDialog,
|
||||
eventSink = {}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,18 @@ fun AdvancedSettingsView(
|
|||
onChange = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(it)) },
|
||||
),
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = stringResource(id = R.string.screen_advanced_settings_send_read_receipts))
|
||||
},
|
||||
supportingContent = {
|
||||
Text(text = stringResource(id = R.string.screen_advanced_settings_send_read_receipts_description))
|
||||
},
|
||||
trailingContent = ListItemContent.Switch(
|
||||
checked = state.isSendPublicReadReceiptsEnabled,
|
||||
onChange = { state.eventSink(AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled(it)) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (state.showChangeThemeDialog) {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||
import io.element.android.appconfig.ElementCallConfig
|
||||
import io.element.android.features.preferences.api.store.PreferencesStore
|
||||
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
|
||||
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
|
||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter
|
||||
|
|
@ -51,7 +51,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
|||
private val computeCacheSizeUseCase: ComputeCacheSizeUseCase,
|
||||
private val clearCacheUseCase: ClearCacheUseCase,
|
||||
private val rageshakePresenter: RageshakePreferencesPresenter,
|
||||
private val preferencesStore: PreferencesStore,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : Presenter<DeveloperSettingsState> {
|
||||
@Composable
|
||||
override fun present(): DeveloperSettingsState {
|
||||
|
|
@ -69,7 +69,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
|||
val clearCacheAction = remember {
|
||||
mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
|
||||
}
|
||||
val customElementCallBaseUrl by preferencesStore
|
||||
val customElementCallBaseUrl by appPreferencesStore
|
||||
.getCustomElementCallBaseUrlFlow()
|
||||
.collectAsState(initial = null)
|
||||
|
||||
|
|
@ -100,7 +100,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
|||
is DeveloperSettingsEvents.SetCustomElementCallBaseUrl -> coroutineScope.launch {
|
||||
// If the URL is either empty or the default one, we want to save 'null' to remove the custom URL
|
||||
val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() && it != ElementCallConfig.DEFAULT_BASE_URL }
|
||||
preferencesStore.setCustomElementCallBaseUrl(urlToSave)
|
||||
appPreferencesStore.setCustomElementCallBaseUrl(urlToSave)
|
||||
}
|
||||
DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
<string name="screen_advanced_settings_developer_mode">"Developer mode"</string>
|
||||
<string name="screen_advanced_settings_developer_mode_description">"Enable to have access to features and functionality for developers."</string>
|
||||
<string name="screen_advanced_settings_rich_text_editor_description">"Disable the rich text editor to type Markdown manually."</string>
|
||||
<string name="screen_advanced_settings_send_read_receipts">"Read receipts"</string>
|
||||
<string name="screen_advanced_settings_send_read_receipts_description">"If turned off, your read receipts won\'t be sent to anyone. You will still receive read receipts from other users."</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Enable option to view message source in the timeline."</string>
|
||||
<string name="screen_edit_profile_display_name">"Display name"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Your display name"</string>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ 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.featureflag.test.InMemoryPreferencesStore
|
||||
import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore
|
||||
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -34,8 +35,7 @@ class AdvancedSettingsPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val store = InMemoryPreferencesStore()
|
||||
val presenter = AdvancedSettingsPresenter(store)
|
||||
val presenter = createAdvancedSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -43,14 +43,14 @@ class AdvancedSettingsPresenterTest {
|
|||
assertThat(initialState.isDeveloperModeEnabled).isFalse()
|
||||
assertThat(initialState.isRichTextEditorEnabled).isFalse()
|
||||
assertThat(initialState.showChangeThemeDialog).isFalse()
|
||||
assertThat(initialState.isSendPublicReadReceiptsEnabled).isTrue()
|
||||
assertThat(initialState.theme).isEqualTo(Theme.System)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - developer mode on off`() = runTest {
|
||||
val store = InMemoryPreferencesStore()
|
||||
val presenter = AdvancedSettingsPresenter(store)
|
||||
val presenter = createAdvancedSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -65,8 +65,7 @@ class AdvancedSettingsPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - rich text editor on off`() = runTest {
|
||||
val store = InMemoryPreferencesStore()
|
||||
val presenter = AdvancedSettingsPresenter(store)
|
||||
val presenter = createAdvancedSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -79,10 +78,24 @@ class AdvancedSettingsPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - send public read receipts off on`() = runTest {
|
||||
val presenter = createAdvancedSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitLastSequentialItem()
|
||||
assertThat(initialState.isSendPublicReadReceiptsEnabled).isTrue()
|
||||
initialState.eventSink.invoke(AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled(false))
|
||||
assertThat(awaitItem().isSendPublicReadReceiptsEnabled).isFalse()
|
||||
initialState.eventSink.invoke(AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled(true))
|
||||
assertThat(awaitItem().isSendPublicReadReceiptsEnabled).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - change theme`() = runTest {
|
||||
val store = InMemoryPreferencesStore()
|
||||
val presenter = AdvancedSettingsPresenter(store)
|
||||
val presenter = createAdvancedSettingsPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -102,4 +115,12 @@ class AdvancedSettingsPresenterTest {
|
|||
assertThat(withNewTheme.theme).isEqualTo(Theme.Light)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAdvancedSettingsPresenter(
|
||||
appPreferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||
) = AdvancedSettingsPresenter(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataSto
|
|||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
|
||||
import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -114,7 +114,7 @@ class DeveloperSettingsPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - custom element call base url`() = runTest {
|
||||
val preferencesStore = InMemoryPreferencesStore()
|
||||
val preferencesStore = InMemoryAppPreferencesStore()
|
||||
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferencesStore)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -149,14 +149,14 @@ class DeveloperSettingsPresenterTest {
|
|||
cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(),
|
||||
clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(),
|
||||
rageshakePresenter: DefaultRageshakePreferencesPresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()),
|
||||
preferencesStore: InMemoryPreferencesStore = InMemoryPreferencesStore(),
|
||||
preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||
): DeveloperSettingsPresenter {
|
||||
return DeveloperSettingsPresenter(
|
||||
featureFlagService = featureFlagService,
|
||||
computeCacheSizeUseCase = cacheSizeUseCase,
|
||||
clearCacheUseCase = clearCacheUseCase,
|
||||
rageshakePresenter = rageshakePresenter,
|
||||
preferencesStore = preferencesStore,
|
||||
appPreferencesStore = preferencesStore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue