Rework HomeserverResolver

This commit is contained in:
Benoit Marty 2023-06-08 22:04:12 +02:00
parent 763159651a
commit e1e984cfb0
4 changed files with 67 additions and 83 deletions

View file

@ -17,15 +17,15 @@
package io.element.android.features.login.impl.changeaccountprovider.form package io.element.android.features.login.impl.changeaccountprovider.form
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf 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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter 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 io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class ChangeAccountProviderFormPresenter @Inject constructor( class ChangeAccountProviderFormPresenter @Inject constructor(
@ -35,33 +35,34 @@ class ChangeAccountProviderFormPresenter @Inject constructor(
@Composable @Composable
override fun present(): ChangeAccountProviderFormState { override fun present(): ChangeAccountProviderFormState {
val localCoroutineScope = rememberCoroutineScope() var userInput by rememberSaveable {
val userInput = rememberSaveable {
mutableStateOf("") mutableStateOf("")
} }
val changeServerState = changeServerPresenter.present() val changeServerState = changeServerPresenter.present()
val data by homeserverResolver.flow().collectAsState()
var data: Async<List<HomeserverData>> by remember {
mutableStateOf(Async.Uninitialized)
}
LaunchedEffect(userInput) {
homeserverResolver.resolve(userInput).collect {
data = it
}
}
fun handleEvents(event: ChangeAccountProviderFormEvents) { fun handleEvents(event: ChangeAccountProviderFormEvents) {
when (event) { when (event) {
is ChangeAccountProviderFormEvents.UserInput -> { is ChangeAccountProviderFormEvents.UserInput -> {
userInput.value = event.input userInput = event.input
localCoroutineScope.userInput(event.input)
} }
} }
} }
return ChangeAccountProviderFormState( return ChangeAccountProviderFormState(
userInput = userInput.value, userInput = userInput,
userInputResult = data, userInputResult = data,
changeServerState = changeServerState, changeServerState = changeServerState,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
// Could be reworked using LaunchedEffect
private fun CoroutineScope.userInput(userInput: String) = launch {
homeserverResolver.accept(userInput)
}
} }

View file

@ -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.ensureProtocol
import io.element.android.libraries.core.uri.isValidUrl import io.element.android.libraries.core.uri.isValidUrl
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.Collections
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -44,35 +43,25 @@ class DefaultHomeserverResolver @Inject constructor(
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val wellknownRequest: WellknownRequest, private val wellknownRequest: WellknownRequest,
) : HomeserverResolver { ) : HomeserverResolver {
private val mutableFlow: MutableStateFlow<Async<List<HomeserverData>>> = MutableStateFlow(Async.Uninitialized)
override fun flow(): StateFlow<Async<List<HomeserverData>>> = mutableFlow override suspend fun resolve(userInput: String): Flow<Async<List<HomeserverData>>> = flow {
val flowContext = currentCoroutineContext()
private var currentJob: Job? = null emit(Async.Uninitialized)
// Debounce
override suspend fun accept(userInput: String) { delay(300)
currentJob?.cancel() val clean = userInput.trim()
val cleanedUpUserInput = userInput.trim().ensureProtocol().removeSuffix("/") if (clean.length < 4) return@flow
mutableFlow.tryEmit(Async.Uninitialized) emit(Async.Loading())
if (cleanedUpUserInput.length > 3) { val list = getUrlCandidate(clean.ensureProtocol().removeSuffix("/"))
delay(300) val currentList = Collections.synchronizedList(mutableListOf<HomeserverData>())
mutableFlow.tryEmit(Async.Loading()) // Run all the requests in parallel
withContext(dispatchers.io) { withContext(dispatchers.io) {
val list = getUrlCandidate(cleanedUpUserInput)
currentJob = resolveList(cleanedUpUserInput, list)
}
}
}
private fun CoroutineScope.resolveList(userInput: String, list: List<String>): Job {
val currentList = mutableListOf<HomeserverData>()
return launch {
list.map { list.map {
async { async {
val wellKnown = tryOrNull { wellknownRequest.execute(it) } val wellKnown = tryOrNull { wellknownRequest.execute(it) }
val isValid = wellKnown?.isValid().orFalse() val isValid = wellKnown?.isValid().orFalse()
val supportSlidingSync = wellKnown?.supportSlidingSync().orFalse()
if (isValid) { if (isValid) {
val supportSlidingSync = wellKnown?.supportSlidingSync().orFalse()
// Emit the list as soon as possible // Emit the list as soon as possible
currentList.add( currentList.add(
HomeserverData( HomeserverData(
@ -81,38 +70,35 @@ class DefaultHomeserverResolver @Inject constructor(
supportSlidingSync = supportSlidingSync supportSlidingSync = supportSlidingSync
) )
) )
mutableFlow.tryEmit(Async.Success(currentList)) withContext(flowContext) {
} emit(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)
} }
} }
} }
}.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<String> { private fun getUrlCandidate(data: String): List<String> {
return buildList { return buildList {
// Always try what the user has entered
add(data)
if (data.contains(".")) { if (data.contains(".")) {
// TLD detected? // TLD detected?
} else { } else {
@ -120,6 +106,8 @@ class DefaultHomeserverResolver @Inject constructor(
add("${data}.com") add("${data}.com")
add("${data}.io") add("${data}.io")
} }
// Always try what the user has entered
add(data)
} }
} }
} }

View file

@ -17,12 +17,11 @@
package io.element.android.features.login.impl.changeaccountprovider.form package io.element.android.features.login.impl.changeaccountprovider.form
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.Flow
/** /**
* Resolve homeserver base on search terms. * Resolve homeserver base on search terms.
*/ */
interface HomeserverResolver { interface HomeserverResolver {
fun flow(): StateFlow<Async<List<HomeserverData>>> suspend fun resolve(userInput: String): Flow<Async<List<HomeserverData>>>
suspend fun accept(userInput: String)
} }

View file

@ -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.architecture.Async
import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flow
class FakeHomeServerResolver : HomeserverResolver { class FakeHomeServerResolver : HomeserverResolver {
private var pendingResult: List<List<HomeserverData>> = emptyList() private var pendingResult: List<List<HomeserverData>> = emptyList()
@ -28,21 +28,17 @@ class FakeHomeServerResolver : HomeserverResolver {
pendingResult = result pendingResult = result
} }
private val mutableFlow: MutableStateFlow<Async<List<HomeserverData>>> = MutableStateFlow(Async.Uninitialized) override suspend fun resolve(userInput: String): Flow<Async<List<HomeserverData>>> = flow {
emit(Async.Uninitialized)
override fun flow(): StateFlow<Async<List<HomeserverData>>> = mutableFlow
override suspend fun accept(userInput: String) {
mutableFlow.tryEmit(Async.Uninitialized)
delay(FAKE_DELAY_IN_MS) delay(FAKE_DELAY_IN_MS)
mutableFlow.tryEmit(Async.Loading()) emit(Async.Loading())
// Sending the pending result // Sending the pending result
if (pendingResult.isEmpty()) { if (pendingResult.isEmpty()) {
mutableFlow.tryEmit(Async.Uninitialized) emit(Async.Uninitialized)
} else { } else {
pendingResult.forEach { pendingResult.forEach {
delay(FAKE_DELAY_IN_MS) delay(FAKE_DELAY_IN_MS)
mutableFlow.tryEmit(Async.Success(it)) emit(Async.Success(it))
} }
} }
} }