Browse Source

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

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
pull/1816/head
Jorge Martin Espinosa 10 months ago committed by GitHub
parent
commit
f752147837
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt
  2. 4
      changelog.d/+remove-element-call-flag.misc
  3. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
  4. 1
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt
  5. 38
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt
  6. 7
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt
  7. 3
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt
  8. 25
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt
  9. 1
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt
  10. 31
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt
  11. 9
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt
  12. 8
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt
  13. 37
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt
  14. 64
      features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt
  15. 86
      features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt
  16. 6
      libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
  17. 1
      libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt
  18. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_1_null_3,NEXUS_5,1.0,en].png
  19. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_2_null_3,NEXUS_5,1.0,en].png
  20. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-3_3_null_0,NEXUS_5,1.0,en].png
  21. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-3_3_null_1,NEXUS_5,1.0,en].png
  22. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-3_3_null_2,NEXUS_5,1.0,en].png
  23. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-3_4_null_0,NEXUS_5,1.0,en].png
  24. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-3_4_null_1,NEXUS_5,1.0,en].png
  25. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-3_4_null_2,NEXUS_5,1.0,en].png

2
appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt

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

4
changelog.d/+remove-element-call-flag.misc

@ -0,0 +1,4 @@ @@ -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.

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt

@ -157,10 +157,10 @@ class MessagesPresenter @AssistedInject constructor( @@ -157,10 +157,10 @@ class MessagesPresenter @AssistedInject constructor(
val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true)
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) {
enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages)
enableInRoomCalls = featureFlagsService.isFeatureEnabled(FeatureFlags.InRoomCalls)
}
fun handleEvents(event: MessagesEvents) {

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

@ -19,5 +19,4 @@ package io.element.android.features.preferences.impl.advanced @@ -19,5 +19,4 @@ 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
data class SetCustomElementCallBaseUrl(val baseUrl: String?) : AdvancedSettingsEvents
}

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

@ -17,25 +17,16 @@ @@ -17,25 +17,16 @@
package io.element.android.features.preferences.impl.advanced
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.appconfig.ElementCallConfig
import io.element.android.features.preferences.api.store.PreferencesStore
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 java.net.URL
import javax.inject.Inject
class AdvancedSettingsPresenter @Inject constructor(
private val preferencesStore: PreferencesStore,
private val featureFlagService: FeatureFlagService,
) : Presenter<AdvancedSettingsState> {
@Composable
@ -47,14 +38,6 @@ class AdvancedSettingsPresenter @Inject constructor( @@ -47,14 +38,6 @@ class AdvancedSettingsPresenter @Inject constructor(
val isDeveloperModeEnabled by preferencesStore
.isDeveloperModeEnabledFlow()
.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) {
when (event) {
@ -64,34 +47,13 @@ class AdvancedSettingsPresenter @Inject constructor( @@ -64,34 +47,13 @@ class AdvancedSettingsPresenter @Inject constructor(
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch {
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(
isRichTextEditorEnabled = isRichTextEditorEnabled,
isDeveloperModeEnabled = isDeveloperModeEnabled,
customElementCallBaseUrlState = if (canDisplayElementCallSettings) {
CustomElementCallBaseUrlState(
baseUrl = customElementCallBaseUrl,
defaultUrl = ElementCallConfig.DEFAULT_BASE_URL,
validator = ::customElementCallUrlValidator,
)
} else null,
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
}
}

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

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

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

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

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

@ -16,16 +16,13 @@ @@ -16,16 +16,13 @@
package io.element.android.features.preferences.impl.advanced
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.PreviewParameter
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.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.PreviewsDayNight
import io.element.android.libraries.ui.strings.CommonStrings
@ -36,11 +33,6 @@ fun AdvancedSettingsView( @@ -36,11 +33,6 @@ fun AdvancedSettingsView(
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
fun isUsingDefaultUrl(value: String?): Boolean {
val defaultUrl = state.customElementCallBaseUrlState?.defaultUrl ?: return false
return value.isNullOrEmpty() || value == defaultUrl
}
PreferencePage(
modifier = modifier,
onBackPressed = onBackPressed,
@ -58,23 +50,6 @@ fun AdvancedSettingsView( @@ -58,23 +50,6 @@ fun AdvancedSettingsView(
isChecked = state.isDeveloperModeEnabled,
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)) }
)
}
}
}

1
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt

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

31
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt

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

9
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt

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

8
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt

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

37
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt

@ -16,16 +16,20 @@ @@ -16,16 +16,20 @@
package io.element.android.features.preferences.impl.developer
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
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.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.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.PreviewsDayNight
import io.element.android.libraries.featureflag.ui.FeatureListView
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import io.element.android.libraries.ui.strings.CommonStrings
@ -47,6 +51,7 @@ fun DeveloperSettingsView( @@ -47,6 +51,7 @@ fun DeveloperSettingsView(
PreferenceCategory(title = "Feature flags") {
FeatureListContent(state)
}
ElementCallCategory(state = state)
PreferenceCategory(title = "Rust SDK") {
PreferenceText(
title = "Configure tracing",
@ -84,6 +89,34 @@ fun DeveloperSettingsView( @@ -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
private fun FeatureListContent(
state: DeveloperSettingsState,

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

@ -20,8 +20,6 @@ import app.cash.molecule.RecompositionMode @@ -20,8 +20,6 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
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.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem
@ -37,23 +35,20 @@ class AdvancedSettingsPresenterTest { @@ -37,23 +35,20 @@ class AdvancedSettingsPresenterTest {
@Test
fun `present - initial state`() = runTest {
val store = InMemoryPreferencesStore()
val featureFlagService = FakeFeatureFlagService()
val presenter = AdvancedSettingsPresenter(store, featureFlagService)
val presenter = AdvancedSettingsPresenter(store)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
assertThat(initialState.isDeveloperModeEnabled).isFalse()
assertThat(initialState.isRichTextEditorEnabled).isFalse()
assertThat(initialState.customElementCallBaseUrlState?.baseUrl).isNull()
}
}
@Test
fun `present - developer mode on off`() = runTest {
val store = InMemoryPreferencesStore()
val featureFlagService = FakeFeatureFlagService()
val presenter = AdvancedSettingsPresenter(store, featureFlagService)
val presenter = AdvancedSettingsPresenter(store)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -69,8 +64,7 @@ class AdvancedSettingsPresenterTest { @@ -69,8 +64,7 @@ class AdvancedSettingsPresenterTest {
@Test
fun `present - rich text editor on off`() = runTest {
val store = InMemoryPreferencesStore()
val featureFlagService = FakeFeatureFlagService()
val presenter = AdvancedSettingsPresenter(store, featureFlagService)
val presenter = AdvancedSettingsPresenter(store)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -82,56 +76,4 @@ class AdvancedSettingsPresenterTest { @@ -82,56 +76,4 @@ class AdvancedSettingsPresenterTest {
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()
}
}
}

86
features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt

@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionMode @@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
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.FakeComputeCacheSizeUseCase
import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter
@ -28,7 +29,9 @@ import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataSto @@ -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.featureflag.api.FeatureFlags
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.awaitLastSequentialItem
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@ -40,13 +43,7 @@ class DeveloperSettingsPresenterTest { @@ -40,13 +43,7 @@ class DeveloperSettingsPresenterTest {
@Test
fun `present - ensures initial state is correct`() = runTest {
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(),
FakeClearCacheUseCase(),
rageshakePresenter
)
val presenter = createDeveloperSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -54,6 +51,8 @@ class DeveloperSettingsPresenterTest { @@ -54,6 +51,8 @@ class DeveloperSettingsPresenterTest {
assertThat(initialState.features).isEmpty()
assertThat(initialState.clearCacheAction).isEqualTo(Async.Uninitialized)
assertThat(initialState.cacheSize).isEqualTo(Async.Uninitialized)
assertThat(initialState.customElementCallBaseUrlState).isNotNull()
assertThat(initialState.customElementCallBaseUrlState.baseUrl).isNull()
val loadedState = awaitItem()
assertThat(loadedState.rageshakeState.isEnabled).isFalse()
assertThat(loadedState.rageshakeState.isSupported).isTrue()
@ -64,32 +63,20 @@ class DeveloperSettingsPresenterTest { @@ -64,32 +63,20 @@ class DeveloperSettingsPresenterTest {
@Test
fun `present - ensures feature list is loaded`() = runTest {
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(),
FakeClearCacheUseCase(),
rageshakePresenter,
)
val presenter = createDeveloperSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val state = awaitItem()
assertThat(state.features).hasSize(FeatureFlags.values().size)
assertThat(state.features).hasSize(FeatureFlags.entries.size)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - ensures state is updated when enabled feature event is triggered`() = runTest {
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(),
FakeClearCacheUseCase(),
rageshakePresenter,
)
val presenter = createDeveloperSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -109,12 +96,7 @@ class DeveloperSettingsPresenterTest { @@ -109,12 +96,7 @@ class DeveloperSettingsPresenterTest {
fun `present - clear cache`() = runTest {
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
val clearCacheUseCase = FakeClearCacheUseCase()
val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService(),
FakeComputeCacheSizeUseCase(),
clearCacheUseCase,
rageshakePresenter,
)
val presenter = createDeveloperSettingsPresenter(clearCacheUseCase = clearCacheUseCase, rageshakePresenter = rageshakePresenter)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -130,4 +112,52 @@ class DeveloperSettingsPresenterTest { @@ -130,4 +112,52 @@ class DeveloperSettingsPresenterTest {
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,
)
}
}

6
libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt

@ -55,12 +55,6 @@ enum class FeatureFlags( @@ -55,12 +55,6 @@ enum class FeatureFlags(
description = "Allow user to lock/unlock the app with a pin code or biometrics",
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(
key = "feature.mentions",
title = "Mentions",

1
libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt

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

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_1_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_2_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-3_3_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-3_3_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-3_3_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-3_4_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-3_4_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-3_4_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.
Loading…
Cancel
Save