Remove Element Call feature flag and revert base URL to call.element.io (#1809)

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2023-11-15 14:02:24 +01:00 committed by GitHub
parent 14cf2b0440
commit 5701e98d77
25 changed files with 164 additions and 191 deletions

View file

@ -17,5 +17,5 @@
package io.element.android.appconfig package io.element.android.appconfig
object ElementCallConfig { object ElementCallConfig {
const val DEFAULT_BASE_URL = "https://call.element.dev" const val DEFAULT_BASE_URL = "https://call.element.io"
} }

View file

@ -0,0 +1,4 @@
Remove Element Call feature flag, it's not always enabled.
- Reverted the EC base URL to `https://call.element.io`.
- Moved the option to override this URL to developer settings from advanced settings.

View file

@ -157,10 +157,10 @@ class MessagesPresenter @AssistedInject constructor(
val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true) val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true)
var enableVoiceMessages by remember { mutableStateOf(false) } var enableVoiceMessages by remember { mutableStateOf(false) }
var enableInRoomCalls by remember { mutableStateOf(false) } // TODO add min power level to use this feature in the future?
val enableInRoomCalls = true
LaunchedEffect(featureFlagsService) { LaunchedEffect(featureFlagsService) {
enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages) enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages)
enableInRoomCalls = featureFlagsService.isFeatureEnabled(FeatureFlags.InRoomCalls)
} }
fun handleEvents(event: MessagesEvents) { fun handleEvents(event: MessagesEvents) {

View file

@ -19,5 +19,4 @@ package io.element.android.features.preferences.impl.advanced
sealed interface AdvancedSettingsEvents { sealed interface AdvancedSettingsEvents {
data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
data class SetCustomElementCallBaseUrl(val baseUrl: String?) : AdvancedSettingsEvents
} }

View file

@ -17,25 +17,16 @@
package io.element.android.features.preferences.impl.advanced package io.element.android.features.preferences.impl.advanced
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import io.element.android.appconfig.ElementCallConfig
import io.element.android.features.preferences.api.store.PreferencesStore import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.URL
import javax.inject.Inject import javax.inject.Inject
class AdvancedSettingsPresenter @Inject constructor( class AdvancedSettingsPresenter @Inject constructor(
private val preferencesStore: PreferencesStore, private val preferencesStore: PreferencesStore,
private val featureFlagService: FeatureFlagService,
) : Presenter<AdvancedSettingsState> { ) : Presenter<AdvancedSettingsState> {
@Composable @Composable
@ -47,14 +38,6 @@ class AdvancedSettingsPresenter @Inject constructor(
val isDeveloperModeEnabled by preferencesStore val isDeveloperModeEnabled by preferencesStore
.isDeveloperModeEnabledFlow() .isDeveloperModeEnabledFlow()
.collectAsState(initial = false) .collectAsState(initial = false)
val customElementCallBaseUrl by preferencesStore
.getCustomElementCallBaseUrlFlow()
.collectAsState(initial = null)
var canDisplayElementCallSettings by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
canDisplayElementCallSettings = featureFlagService.isFeatureEnabled(FeatureFlags.InRoomCalls)
}
fun handleEvents(event: AdvancedSettingsEvents) { fun handleEvents(event: AdvancedSettingsEvents) {
when (event) { when (event) {
@ -64,34 +47,13 @@ class AdvancedSettingsPresenter @Inject constructor(
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch { is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch {
preferencesStore.setDeveloperModeEnabled(event.enabled) preferencesStore.setDeveloperModeEnabled(event.enabled)
} }
is AdvancedSettingsEvents.SetCustomElementCallBaseUrl -> localCoroutineScope.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)
}
} }
} }
return AdvancedSettingsState( return AdvancedSettingsState(
isRichTextEditorEnabled = isRichTextEditorEnabled, isRichTextEditorEnabled = isRichTextEditorEnabled,
isDeveloperModeEnabled = isDeveloperModeEnabled, isDeveloperModeEnabled = isDeveloperModeEnabled,
customElementCallBaseUrlState = if (canDisplayElementCallSettings) {
CustomElementCallBaseUrlState(
baseUrl = customElementCallBaseUrl,
defaultUrl = ElementCallConfig.DEFAULT_BASE_URL,
validator = ::customElementCallUrlValidator,
)
} else null,
eventSink = { handleEvents(it) } eventSink = { handleEvents(it) }
) )
} }
private fun customElementCallUrlValidator(url: String?): Boolean {
return runCatching {
if (url.isNullOrEmpty()) return@runCatching
val parsedUrl = URL(url)
if (parsedUrl.protocol !in listOf("http", "https")) error("Incorrect protocol")
if (parsedUrl.host.isNullOrBlank()) error("Missing host")
}.isSuccess
}
} }

View file

@ -19,12 +19,5 @@ package io.element.android.features.preferences.impl.advanced
data class AdvancedSettingsState( data class AdvancedSettingsState(
val isRichTextEditorEnabled: Boolean, val isRichTextEditorEnabled: Boolean,
val isDeveloperModeEnabled: Boolean, val isDeveloperModeEnabled: Boolean,
val customElementCallBaseUrlState: CustomElementCallBaseUrlState?,
val eventSink: (AdvancedSettingsEvents) -> Unit val eventSink: (AdvancedSettingsEvents) -> Unit
) )
data class CustomElementCallBaseUrlState(
val baseUrl: String?,
val defaultUrl: String,
val validator: (String?) -> Boolean,
)

View file

@ -24,17 +24,14 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSett
aAdvancedSettingsState(), aAdvancedSettingsState(),
aAdvancedSettingsState(isRichTextEditorEnabled = true), aAdvancedSettingsState(isRichTextEditorEnabled = true),
aAdvancedSettingsState(isDeveloperModeEnabled = true), aAdvancedSettingsState(isDeveloperModeEnabled = true),
aAdvancedSettingsState(customElementCallBaseUrl = "https://call.element.io"),
) )
} }
fun aAdvancedSettingsState( fun aAdvancedSettingsState(
isRichTextEditorEnabled: Boolean = false, isRichTextEditorEnabled: Boolean = false,
isDeveloperModeEnabled: Boolean = false, isDeveloperModeEnabled: Boolean = false,
customElementCallBaseUrl: String? = null,
) = AdvancedSettingsState( ) = AdvancedSettingsState(
isRichTextEditorEnabled = isRichTextEditorEnabled, isRichTextEditorEnabled = isRichTextEditorEnabled,
isDeveloperModeEnabled = isDeveloperModeEnabled, isDeveloperModeEnabled = isDeveloperModeEnabled,
customElementCallBaseUrlState = customElementCallBaseUrl?.let { CustomElementCallBaseUrlState(it, "https://call.element.io") { true } },
eventSink = {} eventSink = {}
) )

View file

@ -16,16 +16,13 @@
package io.element.android.features.preferences.impl.advanced package io.element.android.features.preferences.impl.advanced
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.components.preferences.PreferenceTextField
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -36,11 +33,6 @@ fun AdvancedSettingsView(
onBackPressed: () -> Unit, onBackPressed: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
fun isUsingDefaultUrl(value: String?): Boolean {
val defaultUrl = state.customElementCallBaseUrlState?.defaultUrl ?: return false
return value.isNullOrEmpty() || value == defaultUrl
}
PreferencePage( PreferencePage(
modifier = modifier, modifier = modifier,
onBackPressed = onBackPressed, onBackPressed = onBackPressed,
@ -58,23 +50,6 @@ fun AdvancedSettingsView(
isChecked = state.isDeveloperModeEnabled, isChecked = state.isDeveloperModeEnabled,
onCheckedChange = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(it)) }, onCheckedChange = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(it)) },
) )
state.customElementCallBaseUrlState?.let { callUrlState ->
val supportingText = if (isUsingDefaultUrl(callUrlState.baseUrl)) {
stringResource(R.string.screen_advanced_settings_element_call_base_url_description)
} else {
callUrlState.baseUrl
}
PreferenceTextField(
headline = stringResource(R.string.screen_advanced_settings_element_call_base_url),
value = callUrlState.baseUrl ?: callUrlState.defaultUrl,
supportingText = supportingText,
validation = callUrlState.validator,
onValidationErrorMessage = stringResource(R.string.screen_advanced_settings_element_call_base_url_validation_error),
displayValue = { value -> !isUsingDefaultUrl(value) },
keyboardOptions = KeyboardOptions.Default.copy(autoCorrect = false, keyboardType = KeyboardType.Uri),
onChange = { state.eventSink(AdvancedSettingsEvents.SetCustomElementCallBaseUrl(it)) }
)
}
} }
} }

View file

@ -20,5 +20,6 @@ import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
sealed interface DeveloperSettingsEvents { sealed interface DeveloperSettingsEvents {
data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents
data class SetCustomElementCallBaseUrl(val baseUrl: String?) : DeveloperSettingsEvents
data object ClearCache: DeveloperSettingsEvents data object ClearCache: DeveloperSettingsEvents
} }

View file

@ -19,12 +19,16 @@ package io.element.android.features.preferences.impl.developer
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshots.SnapshotStateMap 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.impl.tasks.ClearCacheUseCase import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter
@ -39,6 +43,7 @@ import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.URL
import javax.inject.Inject import javax.inject.Inject
class DeveloperSettingsPresenter @Inject constructor( class DeveloperSettingsPresenter @Inject constructor(
@ -46,6 +51,7 @@ class DeveloperSettingsPresenter @Inject constructor(
private val computeCacheSizeUseCase: ComputeCacheSizeUseCase, private val computeCacheSizeUseCase: ComputeCacheSizeUseCase,
private val clearCacheUseCase: ClearCacheUseCase, private val clearCacheUseCase: ClearCacheUseCase,
private val rageshakePresenter: RageshakePreferencesPresenter, private val rageshakePresenter: RageshakePreferencesPresenter,
private val preferencesStore: PreferencesStore,
) : Presenter<DeveloperSettingsState> { ) : Presenter<DeveloperSettingsState> {
@Composable @Composable
@ -64,8 +70,12 @@ class DeveloperSettingsPresenter @Inject constructor(
val clearCacheAction = remember { val clearCacheAction = remember {
mutableStateOf<Async<Unit>>(Async.Uninitialized) mutableStateOf<Async<Unit>>(Async.Uninitialized)
} }
val customElementCallBaseUrl by preferencesStore
.getCustomElementCallBaseUrlFlow()
.collectAsState(initial = null)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
FeatureFlags.values().forEach { feature -> FeatureFlags.entries.forEach { feature ->
features[feature.key] = feature features[feature.key] = feature
enabledFeatures[feature.key] = featureFlagService.isFeatureEnabled(feature) enabledFeatures[feature.key] = featureFlagService.isFeatureEnabled(feature)
} }
@ -86,6 +96,11 @@ class DeveloperSettingsPresenter @Inject constructor(
event.isEnabled, event.isEnabled,
triggerClearCache = { handleEvents(DeveloperSettingsEvents.ClearCache) } triggerClearCache = { handleEvents(DeveloperSettingsEvents.ClearCache) }
) )
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)
}
DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction) DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
} }
} }
@ -95,6 +110,11 @@ class DeveloperSettingsPresenter @Inject constructor(
cacheSize = cacheSize.value, cacheSize = cacheSize.value,
clearCacheAction = clearCacheAction.value, clearCacheAction = clearCacheAction.value,
rageshakeState = rageshakeState, rageshakeState = rageshakeState,
customElementCallBaseUrlState = CustomElementCallBaseUrlState(
baseUrl = customElementCallBaseUrl,
defaultUrl = ElementCallConfig.DEFAULT_BASE_URL,
validator = ::customElementCallUrlValidator,
),
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
@ -145,5 +165,14 @@ class DeveloperSettingsPresenter @Inject constructor(
} }
} }
private fun customElementCallUrlValidator(url: String?): Boolean {
return runCatching {
if (url.isNullOrEmpty()) return@runCatching
val parsedUrl = URL(url)
if (parsedUrl.protocol !in listOf("http", "https")) error("Incorrect protocol")
if (parsedUrl.host.isNullOrBlank()) error("Missing host")
}.isSuccess
}

View file

@ -21,10 +21,17 @@ import io.element.android.libraries.architecture.Async
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class DeveloperSettingsState constructor( data class DeveloperSettingsState(
val features: ImmutableList<FeatureUiModel>, val features: ImmutableList<FeatureUiModel>,
val cacheSize: Async<String>, val cacheSize: Async<String>,
val rageshakeState: RageshakePreferencesState, val rageshakeState: RageshakePreferencesState,
val clearCacheAction: Async<Unit>, val clearCacheAction: Async<Unit>,
val customElementCallBaseUrlState: CustomElementCallBaseUrlState,
val eventSink: (DeveloperSettingsEvents) -> Unit val eventSink: (DeveloperSettingsEvents) -> Unit
) )
data class CustomElementCallBaseUrlState(
val baseUrl: String?,
val defaultUrl: String,
val validator: (String?) -> Boolean,
)

View file

@ -26,6 +26,13 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSe
get() = sequenceOf( get() = sequenceOf(
aDeveloperSettingsState(), aDeveloperSettingsState(),
aDeveloperSettingsState().copy(clearCacheAction = Async.Loading()), aDeveloperSettingsState().copy(clearCacheAction = Async.Loading()),
aDeveloperSettingsState().copy(
customElementCallBaseUrlState = CustomElementCallBaseUrlState(
baseUrl = "https://call.element.ahoy",
defaultUrl = "https://call.element.io",
validator = { true }
)
),
) )
} }
@ -34,5 +41,6 @@ fun aDeveloperSettingsState() = DeveloperSettingsState(
rageshakeState = aRageshakePreferencesState(), rageshakeState = aRageshakePreferencesState(),
cacheSize = Async.Success("1.2 MB"), cacheSize = Async.Success("1.2 MB"),
clearCacheAction = Async.Uninitialized, clearCacheAction = Async.Uninitialized,
customElementCallBaseUrlState = CustomElementCallBaseUrlState(baseUrl = null, defaultUrl = "https://call.element.io", validator = { true }),
eventSink = {} eventSink = {}
) )

View file

@ -16,16 +16,20 @@
package io.element.android.features.preferences.impl.developer package io.element.android.features.preferences.impl.developer
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.preferences.impl.R
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.components.preferences.PreferenceTextField
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.featureflag.ui.FeatureListView import io.element.android.libraries.featureflag.ui.FeatureListView
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -47,6 +51,7 @@ fun DeveloperSettingsView(
PreferenceCategory(title = "Feature flags") { PreferenceCategory(title = "Feature flags") {
FeatureListContent(state) FeatureListContent(state)
} }
ElementCallCategory(state = state)
PreferenceCategory(title = "Rust SDK") { PreferenceCategory(title = "Rust SDK") {
PreferenceText( PreferenceText(
title = "Configure tracing", title = "Configure tracing",
@ -84,6 +89,34 @@ fun DeveloperSettingsView(
} }
} }
@Composable
private fun ElementCallCategory(
state: DeveloperSettingsState,
modifier: Modifier = Modifier,
) {
PreferenceCategory(modifier = modifier, title = "Element Call", showDivider = true) {
val callUrlState = state.customElementCallBaseUrlState
fun isUsingDefaultUrl(value: String?): Boolean {
return value.isNullOrEmpty() || value == callUrlState.defaultUrl
}
val supportingText = if (isUsingDefaultUrl(callUrlState.baseUrl)) {
stringResource(R.string.screen_advanced_settings_element_call_base_url_description)
} else {
callUrlState.baseUrl
}
PreferenceTextField(
headline = stringResource(R.string.screen_advanced_settings_element_call_base_url),
value = callUrlState.baseUrl ?: callUrlState.defaultUrl,
supportingText = supportingText,
validation = callUrlState.validator,
onValidationErrorMessage = stringResource(R.string.screen_advanced_settings_element_call_base_url_validation_error),
displayValue = { value -> !isUsingDefaultUrl(value) },
keyboardOptions = KeyboardOptions.Default.copy(autoCorrect = false, keyboardType = KeyboardType.Uri),
onChange = { state.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl(it)) }
)
}
}
@Composable @Composable
private fun FeatureListContent( private fun FeatureListContent(
state: DeveloperSettingsState, state: DeveloperSettingsState,

View file

@ -20,8 +20,6 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
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.InMemoryPreferencesStore
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.awaitLastSequentialItem
@ -37,23 +35,20 @@ class AdvancedSettingsPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val store = InMemoryPreferencesStore() val store = InMemoryPreferencesStore()
val featureFlagService = FakeFeatureFlagService() val presenter = AdvancedSettingsPresenter(store)
val presenter = AdvancedSettingsPresenter(store, featureFlagService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitLastSequentialItem() val initialState = awaitLastSequentialItem()
assertThat(initialState.isDeveloperModeEnabled).isFalse() assertThat(initialState.isDeveloperModeEnabled).isFalse()
assertThat(initialState.isRichTextEditorEnabled).isFalse() assertThat(initialState.isRichTextEditorEnabled).isFalse()
assertThat(initialState.customElementCallBaseUrlState?.baseUrl).isNull()
} }
} }
@Test @Test
fun `present - developer mode on off`() = runTest { fun `present - developer mode on off`() = runTest {
val store = InMemoryPreferencesStore() val store = InMemoryPreferencesStore()
val featureFlagService = FakeFeatureFlagService() val presenter = AdvancedSettingsPresenter(store)
val presenter = AdvancedSettingsPresenter(store, featureFlagService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -69,8 +64,7 @@ class AdvancedSettingsPresenterTest {
@Test @Test
fun `present - rich text editor on off`() = runTest { fun `present - rich text editor on off`() = runTest {
val store = InMemoryPreferencesStore() val store = InMemoryPreferencesStore()
val featureFlagService = FakeFeatureFlagService() val presenter = AdvancedSettingsPresenter(store)
val presenter = AdvancedSettingsPresenter(store, featureFlagService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -82,56 +76,4 @@ class AdvancedSettingsPresenterTest {
assertThat(awaitItem().isRichTextEditorEnabled).isFalse() assertThat(awaitItem().isRichTextEditorEnabled).isFalse()
} }
} }
@Test
fun `present - custom element call url state is null if the feature flag is disabled`() = runTest {
val store = InMemoryPreferencesStore()
val featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.InRoomCalls, false)
}
val presenter = AdvancedSettingsPresenter(store, featureFlagService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
assertThat(initialState.customElementCallBaseUrlState).isNull()
}
}
@Test
fun `present - custom element call base url`() = runTest {
val store = InMemoryPreferencesStore()
val featureFlagService = FakeFeatureFlagService(initialState = hashMapOf(FeatureFlags.InRoomCalls.key to true))
val presenter = AdvancedSettingsPresenter(store, featureFlagService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
assertThat(initialState.customElementCallBaseUrlState).isNotNull()
assertThat(initialState.customElementCallBaseUrlState?.baseUrl).isNull()
initialState.eventSink(AdvancedSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.ahoy"))
val updatedItem = awaitItem()
assertThat(updatedItem.customElementCallBaseUrlState?.baseUrl).isEqualTo("https://call.element.ahoy")
}
}
@Test
fun `present - custom element call base url validator needs at least an HTTP scheme and host`() = runTest {
val store = InMemoryPreferencesStore()
val featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.InRoomCalls, true)
}
val presenter = AdvancedSettingsPresenter(store, featureFlagService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val urlValidator = awaitLastSequentialItem().customElementCallBaseUrlState!!.validator
assertThat(urlValidator("")).isTrue() // We allow empty string to clear the value and use the default one
assertThat(urlValidator("test")).isFalse()
assertThat(urlValidator("http://")).isFalse()
assertThat(urlValidator("geo://test")).isFalse()
assertThat(urlValidator("https://call.element.io")).isTrue()
}
}
} }

View file

@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.ElementCallConfig
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter
@ -28,7 +29,9 @@ import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataSto
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -40,13 +43,7 @@ class DeveloperSettingsPresenterTest {
@Test @Test
fun `present - ensures initial state is correct`() = runTest { fun `present - ensures initial state is correct`() = runTest {
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()) val presenter = createDeveloperSettingsPresenter()
val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(),
FakeClearCacheUseCase(),
rageshakePresenter
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -54,6 +51,8 @@ class DeveloperSettingsPresenterTest {
assertThat(initialState.features).isEmpty() assertThat(initialState.features).isEmpty()
assertThat(initialState.clearCacheAction).isEqualTo(Async.Uninitialized) assertThat(initialState.clearCacheAction).isEqualTo(Async.Uninitialized)
assertThat(initialState.cacheSize).isEqualTo(Async.Uninitialized) assertThat(initialState.cacheSize).isEqualTo(Async.Uninitialized)
assertThat(initialState.customElementCallBaseUrlState).isNotNull()
assertThat(initialState.customElementCallBaseUrlState.baseUrl).isNull()
val loadedState = awaitItem() val loadedState = awaitItem()
assertThat(loadedState.rageshakeState.isEnabled).isFalse() assertThat(loadedState.rageshakeState.isEnabled).isFalse()
assertThat(loadedState.rageshakeState.isSupported).isTrue() assertThat(loadedState.rageshakeState.isSupported).isTrue()
@ -64,32 +63,20 @@ class DeveloperSettingsPresenterTest {
@Test @Test
fun `present - ensures feature list is loaded`() = runTest { fun `present - ensures feature list is loaded`() = runTest {
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()) val presenter = createDeveloperSettingsPresenter()
val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(),
FakeClearCacheUseCase(),
rageshakePresenter,
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) skipItems(1)
val state = awaitItem() val state = awaitItem()
assertThat(state.features).hasSize(FeatureFlags.values().size) assertThat(state.features).hasSize(FeatureFlags.entries.size)
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@Test @Test
fun `present - ensures state is updated when enabled feature event is triggered`() = runTest { fun `present - ensures state is updated when enabled feature event is triggered`() = runTest {
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()) val presenter = createDeveloperSettingsPresenter()
val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(),
FakeClearCacheUseCase(),
rageshakePresenter,
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -109,12 +96,7 @@ class DeveloperSettingsPresenterTest {
fun `present - clear cache`() = runTest { fun `present - clear cache`() = runTest {
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()) val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
val clearCacheUseCase = FakeClearCacheUseCase() val clearCacheUseCase = FakeClearCacheUseCase()
val presenter = DeveloperSettingsPresenter( val presenter = createDeveloperSettingsPresenter(clearCacheUseCase = clearCacheUseCase, rageshakePresenter = rageshakePresenter)
FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(),
clearCacheUseCase,
rageshakePresenter,
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -130,4 +112,52 @@ class DeveloperSettingsPresenterTest {
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@Test
fun `present - custom element call base url`() = runTest {
val preferencesStore = InMemoryPreferencesStore()
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferencesStore)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.customElementCallBaseUrlState.baseUrl).isNull()
initialState.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.ahoy"))
val updatedItem = awaitItem()
assertThat(updatedItem.customElementCallBaseUrlState.baseUrl).isEqualTo("https://call.element.ahoy")
assertThat(updatedItem.customElementCallBaseUrlState.defaultUrl).isEqualTo(ElementCallConfig.DEFAULT_BASE_URL)
}
}
@Test
fun `present - custom element call base url validator needs at least an HTTP scheme and host`() = runTest {
val presenter = createDeveloperSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val urlValidator = awaitLastSequentialItem().customElementCallBaseUrlState.validator
assertThat(urlValidator("")).isTrue() // We allow empty string to clear the value and use the default one
assertThat(urlValidator("test")).isFalse()
assertThat(urlValidator("http://")).isFalse()
assertThat(urlValidator("geo://test")).isFalse()
assertThat(urlValidator("https://call.element.io")).isTrue()
}
}
private fun createDeveloperSettingsPresenter(
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(),
clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(),
rageshakePresenter: DefaultRageshakePreferencesPresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()),
preferencesStore: InMemoryPreferencesStore = InMemoryPreferencesStore(),
): DeveloperSettingsPresenter {
return DeveloperSettingsPresenter(
featureFlagService = featureFlagService,
computeCacheSizeUseCase = cacheSizeUseCase,
clearCacheUseCase = clearCacheUseCase,
rageshakePresenter = rageshakePresenter,
preferencesStore = preferencesStore,
)
}
} }

View file

@ -55,12 +55,6 @@ enum class FeatureFlags(
description = "Allow user to lock/unlock the app with a pin code or biometrics", description = "Allow user to lock/unlock the app with a pin code or biometrics",
defaultValue = true, defaultValue = true,
), ),
InRoomCalls(
key = "feature.elementcall",
title = "Element call in rooms",
description = "Allow user to start or join a call in a room",
defaultValue = true,
),
Mentions( Mentions(
key = "feature.mentions", key = "feature.mentions",
title = "Mentions", title = "Mentions",

View file

@ -39,7 +39,6 @@ class StaticFeatureFlagProvider @Inject constructor() :
FeatureFlags.NotificationSettings -> true FeatureFlags.NotificationSettings -> true
FeatureFlags.VoiceMessages -> true FeatureFlags.VoiceMessages -> true
FeatureFlags.PinUnlock -> true FeatureFlags.PinUnlock -> true
FeatureFlags.InRoomCalls -> true
FeatureFlags.Mentions -> false FeatureFlags.Mentions -> false
FeatureFlags.SecureStorage -> false FeatureFlags.SecureStorage -> false
} }

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a4e1af15c571d1f087005849b627d79387f8f5557bbc4233768bb3c2d940d628
size 48510

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:aa25ebf20fe62af56a548c3e962ae2e76e6e8e1b7e685d021306b733613e49eb
size 45462

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c221b092112d787bdc0f9db9e9da3afe75d6b5754b98db119df3c63514da03b4 oid sha256:b040737c81fb04596312a207e58c75580e48fdd70c1f5ee6abd3c73a846c0337
size 53808 size 58489

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c221b092112d787bdc0f9db9e9da3afe75d6b5754b98db119df3c63514da03b4 oid sha256:b040737c81fb04596312a207e58c75580e48fdd70c1f5ee6abd3c73a846c0337
size 53808 size 58489

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5b01d0d26eddb4b6edd900f0f17568d76108e4be97c979d63c5dcca1ca0ed83e
size 56942

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:419ba4098c8b77a6e6ea2af72eeb5a614a9982d04e003fedfcdee92772484eb1 oid sha256:491beebfede0316e804b818c65794bd0c8f8125fc94f4a89f8e1b3ded5afbf3a
size 48839 size 53597

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:419ba4098c8b77a6e6ea2af72eeb5a614a9982d04e003fedfcdee92772484eb1 oid sha256:491beebfede0316e804b818c65794bd0c8f8125fc94f4a89f8e1b3ded5afbf3a
size 48839 size 53597

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:16b24b31812e18fbf54568f2bae63c0827dc30e73d7924cdab6387df0c2ad41b
size 52062