Browse Source

Rework HomeserverResolver

feature/fga/small_timeline_improvements
Benoit Marty 1 year ago
parent
commit
319d74b12b
  1. 33
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt
  2. 94
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt
  3. 5
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt
  4. 18
      features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt

33
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt

@ -17,15 +17,15 @@ @@ -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( @@ -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<List<HomeserverData>> 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)
}
}

94
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 @@ -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( @@ -44,35 +43,25 @@ class DefaultHomeserverResolver @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val wellknownRequest: WellknownRequest,
) : HomeserverResolver {
private val mutableFlow: MutableStateFlow<Async<List<HomeserverData>>> = MutableStateFlow(Async.Uninitialized)
override fun flow(): StateFlow<Async<List<HomeserverData>>> = 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<String>): Job {
val currentList = mutableListOf<HomeserverData>()
return launch {
override suspend fun resolve(userInput: String): Flow<Async<List<HomeserverData>>> = 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<HomeserverData>())
// 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( @@ -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<String> {
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( @@ -120,6 +106,8 @@ class DefaultHomeserverResolver @Inject constructor(
add("${data}.com")
add("${data}.io")
}
// Always try what the user has entered
add(data)
}
}
}

5
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt

@ -17,12 +17,11 @@ @@ -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<Async<List<HomeserverData>>>
suspend fun accept(userInput: String)
suspend fun resolve(userInput: String): Flow<Async<List<HomeserverData>>>
}

18
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 @@ -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<List<HomeserverData>> = emptyList()
@ -28,21 +28,17 @@ class FakeHomeServerResolver : HomeserverResolver { @@ -28,21 +28,17 @@ class FakeHomeServerResolver : HomeserverResolver {
pendingResult = result
}
private val mutableFlow: MutableStateFlow<Async<List<HomeserverData>>> = MutableStateFlow(Async.Uninitialized)
override fun flow(): StateFlow<Async<List<HomeserverData>>> = mutableFlow
override suspend fun accept(userInput: String) {
mutableFlow.tryEmit(Async.Uninitialized)
override suspend fun resolve(userInput: String): Flow<Async<List<HomeserverData>>> = 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))
}
}
}

Loading…
Cancel
Save