Rework HomeserverResolver
This commit is contained in:
parent
763159651a
commit
e1e984cfb0
4 changed files with 67 additions and 83 deletions
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue