diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt index 4182c32a42..bbd9f62689 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt @@ -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" } diff --git a/changelog.d/+remove-element-call-flag.misc b/changelog.d/+remove-element-call-flag.misc new file mode 100644 index 0000000000..e019d57ac6 --- /dev/null +++ b/changelog.d/+remove-element-call-flag.misc @@ -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. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 175cd34c97..a9a9e7fa50 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -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) { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt index fea42baf5f..37641d684c 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt +++ b/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 sealed interface AdvancedSettingsEvents { data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents - data class SetCustomElementCallBaseUrl(val baseUrl: String?) : AdvancedSettingsEvents } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt index 4bb5abfa19..4b6b0354b6 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt @@ -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 { @Composable @@ -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( 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 - } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt index cd56078b27..899eac2744 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt +++ b/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 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, -) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt index d3a2dee3f4..5ab50c8a16 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt @@ -24,17 +24,14 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider 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( 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)) } - ) - } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt index ce67916178..376f14f7bf 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt +++ b/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 sealed interface DeveloperSettingsEvents { data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents + data class SetCustomElementCallBaseUrl(val baseUrl: String?) : DeveloperSettingsEvents data object ClearCache: DeveloperSettingsEvents } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index ebcd44b4db..ae6c70188b 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/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 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 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( private val computeCacheSizeUseCase: ComputeCacheSizeUseCase, private val clearCacheUseCase: ClearCacheUseCase, private val rageshakePresenter: RageshakePreferencesPresenter, + private val preferencesStore: PreferencesStore, ) : Presenter { @Composable @@ -64,8 +70,12 @@ class DeveloperSettingsPresenter @Inject constructor( val clearCacheAction = remember { mutableStateOf>(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( 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( 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( } } +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 +} + diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt index 8d79c9241d..d49e94b309 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt +++ b/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 import io.element.android.libraries.featureflag.ui.model.FeatureUiModel import kotlinx.collections.immutable.ImmutableList -data class DeveloperSettingsState constructor( +data class DeveloperSettingsState( val features: ImmutableList, val cacheSize: Async, val rageshakeState: RageshakePreferencesState, val clearCacheAction: Async, + val customElementCallBaseUrlState: CustomElementCallBaseUrlState, val eventSink: (DeveloperSettingsEvents) -> Unit ) + +data class CustomElementCallBaseUrlState( + val baseUrl: String?, + val defaultUrl: String, + val validator: (String?) -> Boolean, +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt index ee5c897987..719b736c09 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt @@ -26,6 +26,13 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider !isUsingDefaultUrl(value) }, + keyboardOptions = KeyboardOptions.Default.copy(autoCorrect = false, keyboardType = KeyboardType.Uri), + onChange = { state.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl(it)) } + ) + } +} + @Composable private fun FeatureListContent( state: DeveloperSettingsState, diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt index c2a7fb3e20..1ea04c11dd 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt @@ -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 { @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 { @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 { 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() - } - } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index 88778d4227..fb10ee272a 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -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 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 { @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 { 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 { @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 { 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 { 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, + ) + } } diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index b37dd538fd..0c08d25385 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -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", diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt index 6c9f24c979..89553cef3e 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt @@ -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 } diff --git a/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 b/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 deleted file mode 100644 index 2c48d3e55d..0000000000 --- a/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 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a4e1af15c571d1f087005849b627d79387f8f5557bbc4233768bb3c2d940d628 -size 48510 diff --git a/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 b/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 deleted file mode 100644 index 519d9d4d10..0000000000 --- a/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 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:aa25ebf20fe62af56a548c3e962ae2e76e6e8e1b7e685d021306b733613e49eb -size 45462 diff --git a/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 b/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 index 00797a4f11..89d469e66d 100644 --- a/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 +++ b/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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c221b092112d787bdc0f9db9e9da3afe75d6b5754b98db119df3c63514da03b4 -size 53808 +oid sha256:b040737c81fb04596312a207e58c75580e48fdd70c1f5ee6abd3c73a846c0337 +size 58489 diff --git a/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 b/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 index 00797a4f11..89d469e66d 100644 --- a/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 +++ b/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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c221b092112d787bdc0f9db9e9da3afe75d6b5754b98db119df3c63514da03b4 -size 53808 +oid sha256:b040737c81fb04596312a207e58c75580e48fdd70c1f5ee6abd3c73a846c0337 +size 58489 diff --git a/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 b/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 new file mode 100644 index 0000000000..312f3bb41c --- /dev/null +++ b/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 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b01d0d26eddb4b6edd900f0f17568d76108e4be97c979d63c5dcca1ca0ed83e +size 56942 diff --git a/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 b/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 index 46ec60efb6..0682509a00 100644 --- a/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 +++ b/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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:419ba4098c8b77a6e6ea2af72eeb5a614a9982d04e003fedfcdee92772484eb1 -size 48839 +oid sha256:491beebfede0316e804b818c65794bd0c8f8125fc94f4a89f8e1b3ded5afbf3a +size 53597 diff --git a/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 b/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 index 46ec60efb6..0682509a00 100644 --- a/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 +++ b/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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:419ba4098c8b77a6e6ea2af72eeb5a614a9982d04e003fedfcdee92772484eb1 -size 48839 +oid sha256:491beebfede0316e804b818c65794bd0c8f8125fc94f4a89f8e1b3ded5afbf3a +size 53597 diff --git a/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 b/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 new file mode 100644 index 0000000000..39d9744130 --- /dev/null +++ b/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 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16b24b31812e18fbf54568f2bae63c0827dc30e73d7924cdab6387df0c2ad41b +size 52062