diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt index 5ba3146895..6c4b07b3ed 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt @@ -17,15 +17,15 @@ package io.element.android.features.login.impl.changeaccountprovider.form import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter +import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import javax.inject.Inject class ChangeAccountProviderFormPresenter @Inject constructor( @@ -35,33 +35,34 @@ class ChangeAccountProviderFormPresenter @Inject constructor( @Composable override fun present(): ChangeAccountProviderFormState { - val localCoroutineScope = rememberCoroutineScope() - - val userInput = rememberSaveable { + var userInput by rememberSaveable { mutableStateOf("") } val changeServerState = changeServerPresenter.present() - val data by homeserverResolver.flow().collectAsState() + + var data: Async> by remember { + mutableStateOf(Async.Uninitialized) + } + + LaunchedEffect(userInput) { + homeserverResolver.resolve(userInput).collect { + data = it + } + } fun handleEvents(event: ChangeAccountProviderFormEvents) { when (event) { is ChangeAccountProviderFormEvents.UserInput -> { - userInput.value = event.input - localCoroutineScope.userInput(event.input) + userInput = event.input } } } return ChangeAccountProviderFormState( - userInput = userInput.value, + userInput = userInput, userInputResult = data, changeServerState = changeServerState, eventSink = ::handleEvents ) } - - // Could be reworked using LaunchedEffect - private fun CoroutineScope.userInput(userInput: String) = launch { - homeserverResolver.accept(userInput) - } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt index 6f7274f35f..706b954b86 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt @@ -25,15 +25,14 @@ import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.isValidUrl import io.element.android.libraries.di.AppScope -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.joinAll -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext +import java.util.Collections import javax.inject.Inject /** @@ -44,35 +43,25 @@ class DefaultHomeserverResolver @Inject constructor( private val dispatchers: CoroutineDispatchers, private val wellknownRequest: WellknownRequest, ) : HomeserverResolver { - private val mutableFlow: MutableStateFlow>> = MutableStateFlow(Async.Uninitialized) - override fun flow(): StateFlow>> = mutableFlow - - private var currentJob: Job? = null - - override suspend fun accept(userInput: String) { - currentJob?.cancel() - val cleanedUpUserInput = userInput.trim().ensureProtocol().removeSuffix("/") - mutableFlow.tryEmit(Async.Uninitialized) - if (cleanedUpUserInput.length > 3) { - delay(300) - mutableFlow.tryEmit(Async.Loading()) - withContext(dispatchers.io) { - val list = getUrlCandidate(cleanedUpUserInput) - currentJob = resolveList(cleanedUpUserInput, list) - } - } - } - - private fun CoroutineScope.resolveList(userInput: String, list: List): Job { - val currentList = mutableListOf() - return launch { + override suspend fun resolve(userInput: String): Flow>> = flow { + val flowContext = currentCoroutineContext() + emit(Async.Uninitialized) + // Debounce + delay(300) + val clean = userInput.trim() + if (clean.length < 4) return@flow + emit(Async.Loading()) + val list = getUrlCandidate(clean.ensureProtocol().removeSuffix("/")) + val currentList = Collections.synchronizedList(mutableListOf()) + // Run all the requests in parallel + withContext(dispatchers.io) { list.map { async { val wellKnown = tryOrNull { wellknownRequest.execute(it) } val isValid = wellKnown?.isValid().orFalse() - val supportSlidingSync = wellKnown?.supportSlidingSync().orFalse() if (isValid) { + val supportSlidingSync = wellKnown?.supportSlidingSync().orFalse() // Emit the list as soon as possible currentList.add( HomeserverData( @@ -81,38 +70,35 @@ class DefaultHomeserverResolver @Inject constructor( supportSlidingSync = supportSlidingSync ) ) - mutableFlow.tryEmit(Async.Success(currentList)) - } - } - }.joinAll() - .also { - // If list is empty, and the user as entered an URL, do not block the user. - if (currentList.isEmpty()) { - if (userInput.isValidUrl()) { - mutableFlow.tryEmit( - Async.Success( - listOf( - HomeserverData( - homeserverUrl = userInput, - isWellknownValid = false, - supportSlidingSync = false, - ) - ) - ) - ) - } else { - mutableFlow.tryEmit(Async.Uninitialized) + withContext(flowContext) { + emit(Async.Success(currentList)) } } } + }.awaitAll() + } + // If list is empty, and the user as entered an URL, do not block the user. + if (currentList.isEmpty()) { + if (userInput.isValidUrl()) { + emit( + Async.Success( + listOf( + HomeserverData( + homeserverUrl = userInput, + isWellknownValid = false, + supportSlidingSync = false, + ) + ) + ) + ) + } else { + emit(Async.Uninitialized) + } } } private fun getUrlCandidate(data: String): List { return buildList { - // Always try what the user has entered - add(data) - if (data.contains(".")) { // TLD detected? } else { @@ -120,6 +106,8 @@ class DefaultHomeserverResolver @Inject constructor( add("${data}.com") add("${data}.io") } + // Always try what the user has entered + add(data) } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt index a7abb444f6..0c8bcfc712 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt @@ -17,12 +17,11 @@ package io.element.android.features.login.impl.changeaccountprovider.form import io.element.android.libraries.architecture.Async -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.Flow /** * Resolve homeserver base on search terms. */ interface HomeserverResolver { - fun flow(): StateFlow>> - suspend fun accept(userInput: String) + suspend fun resolve(userInput: String): Flow>> } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt index db86e09903..8a712aa4a0 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt @@ -19,8 +19,8 @@ package io.element.android.features.login.impl.changeaccountprovider.form import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow class FakeHomeServerResolver : HomeserverResolver { private var pendingResult: List> = emptyList() @@ -28,21 +28,17 @@ class FakeHomeServerResolver : HomeserverResolver { pendingResult = result } - private val mutableFlow: MutableStateFlow>> = MutableStateFlow(Async.Uninitialized) - - override fun flow(): StateFlow>> = mutableFlow - - override suspend fun accept(userInput: String) { - mutableFlow.tryEmit(Async.Uninitialized) + override suspend fun resolve(userInput: String): Flow>> = flow { + emit(Async.Uninitialized) delay(FAKE_DELAY_IN_MS) - mutableFlow.tryEmit(Async.Loading()) + emit(Async.Loading()) // Sending the pending result if (pendingResult.isEmpty()) { - mutableFlow.tryEmit(Async.Uninitialized) + emit(Async.Uninitialized) } else { pendingResult.forEach { delay(FAKE_DELAY_IN_MS) - mutableFlow.tryEmit(Async.Success(it)) + emit(Async.Success(it)) } } }