feat(join by alias) : improve state management

This commit is contained in:
ganfra 2025-02-24 20:39:16 +01:00
parent 3142cd7275
commit cfbd67d4fd
4 changed files with 92 additions and 34 deletions

View file

@ -9,6 +9,7 @@ package io.element.android.features.createroom.impl.joinbyaddress
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -22,9 +23,13 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeoutOrNull
import kotlin.time.Duration.Companion.seconds
private const val ADDRESS_RESOLVE_TIMEOUT_IN_SECONDS = 10
class JoinRoomByAddressPresenter @AssistedInject constructor( class JoinRoomByAddressPresenter @AssistedInject constructor(
@Assisted private val navigator: CreateRoomNavigator, @Assisted private val navigator: CreateRoomNavigator,
@ -40,16 +45,20 @@ class JoinRoomByAddressPresenter @AssistedInject constructor(
@Composable @Composable
override fun present(): JoinRoomByAddressState { override fun present(): JoinRoomByAddressState {
var address by remember { mutableStateOf("") } var address by remember { mutableStateOf("") }
var addressState by remember { mutableStateOf<RoomAddressState>(RoomAddressState.Unknown) } var internalAddressState by remember { mutableStateOf<RoomAddressState>(RoomAddressState.Unknown) }
var validateAddress: Boolean by remember { mutableStateOf(false) }
fun handleEvents(event: JoinRoomByAddressEvents) { fun handleEvents(event: JoinRoomByAddressEvents) {
when (event) { when (event) {
JoinRoomByAddressEvents.Continue -> { JoinRoomByAddressEvents.Continue -> {
navigator.onDismissJoinRoomByAddress() when (val currentState = internalAddressState) {
navigator.onOpenRoom(RoomIdOrAlias.Alias(RoomAlias(address))) is RoomAddressState.RoomFound -> onRoomFound(currentState)
else -> validateAddress = true
}
} }
JoinRoomByAddressEvents.Dismiss -> navigator.onDismissJoinRoomByAddress() JoinRoomByAddressEvents.Dismiss -> navigator.onDismissJoinRoomByAddress()
is JoinRoomByAddressEvents.UpdateAddress -> { is JoinRoomByAddressEvents.UpdateAddress -> {
validateAddress = false
address = event.address.trim() address = event.address.trim()
} }
} }
@ -57,9 +66,25 @@ class JoinRoomByAddressPresenter @AssistedInject constructor(
RoomAddressStateEffect( RoomAddressStateEffect(
fullAddress = address, fullAddress = address,
onRoomAddressStateChange = { addressState = it } onRoomAddressStateChange = { addressState ->
internalAddressState = addressState
if (addressState is RoomAddressState.RoomFound && validateAddress) {
onRoomFound(addressState)
}
}
) )
val addressState by remember {
derivedStateOf {
// We only want to show the "RoomFound" state as long as the user didn't validate the address.
if (validateAddress || internalAddressState is RoomAddressState.RoomFound) {
internalAddressState
} else {
RoomAddressState.Unknown
}
}
}
return JoinRoomByAddressState( return JoinRoomByAddressState(
address = address, address = address,
addressState = addressState, addressState = addressState,
@ -67,6 +92,11 @@ class JoinRoomByAddressPresenter @AssistedInject constructor(
) )
} }
private fun onRoomFound(state: RoomAddressState.RoomFound) {
navigator.onDismissJoinRoomByAddress()
navigator.onOpenRoom(state.resolved.roomId.toRoomIdOrAlias())
}
@Composable @Composable
private fun RoomAddressStateEffect( private fun RoomAddressStateEffect(
fullAddress: String, fullAddress: String,
@ -74,24 +104,35 @@ class JoinRoomByAddressPresenter @AssistedInject constructor(
) { ) {
val onChange by rememberUpdatedState(onRoomAddressStateChange) val onChange by rememberUpdatedState(onRoomAddressStateChange)
LaunchedEffect(fullAddress) { LaunchedEffect(fullAddress) {
if (fullAddress.isEmpty()) { // Whenever the address changes, reset the state to unknown
onChange(RoomAddressState.Unknown) onChange(RoomAddressState.Unknown)
return@LaunchedEffect // debounce the room address resolution
}
// debounce the room address validation
delay(300) delay(300)
val roomAlias = tryOrNull { RoomAlias(fullAddress) } val roomAlias = tryOrNull { RoomAlias(fullAddress) }
if (roomAlias == null || !roomAliasHelper.isRoomAliasValid(roomAlias)) { if (roomAlias != null && roomAliasHelper.isRoomAliasValid(roomAlias)) {
onChange(RoomAddressState.Invalid) onChange(RoomAddressState.Resolving)
onChange(client.resolveRoomAddress(roomAlias))
} else { } else {
onChange(RoomAddressState.Valid(matchingRoomFound = false)) onChange(RoomAddressState.Invalid)
client.resolveRoomAlias(roomAlias)
.onSuccess { resolved ->
onChange(RoomAddressState.Valid(matchingRoomFound = resolved.isPresent))
}
} }
} }
} }
private suspend fun MatrixClient.resolveRoomAddress(roomAlias: RoomAlias): RoomAddressState {
return withTimeoutOrNull(ADDRESS_RESOLVE_TIMEOUT_IN_SECONDS.seconds) {
resolveRoomAlias(roomAlias)
.fold(
onSuccess = { resolved ->
if (resolved.isPresent) {
RoomAddressState.RoomFound(resolved.get())
} else {
RoomAddressState.RoomNotFound
}
},
onFailure = { _ -> RoomAddressState.RoomNotFound }
)
} ?: RoomAddressState.RoomNotFound
}
} }

View file

@ -8,6 +8,7 @@
package io.element.android.features.createroom.impl.joinbyaddress package io.element.android.features.createroom.impl.joinbyaddress
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
data class JoinRoomByAddressState( data class JoinRoomByAddressState(
val address: String, val address: String,
@ -19,5 +20,7 @@ data class JoinRoomByAddressState(
sealed interface RoomAddressState { sealed interface RoomAddressState {
data object Unknown : RoomAddressState data object Unknown : RoomAddressState
data object Invalid : RoomAddressState data object Invalid : RoomAddressState
data class Valid(val matchingRoomFound: Boolean) : RoomAddressState data object Resolving : RoomAddressState
data object RoomNotFound : RoomAddressState
data class RoomFound(val resolved: ResolvedRoomAlias) : RoomAddressState
} }

View file

@ -8,16 +8,21 @@
package io.element.android.features.createroom.impl.joinbyaddress package io.element.android.features.createroom.impl.joinbyaddress
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
open class JoinRoomByAddressStateProvider : PreviewParameterProvider<JoinRoomByAddressState> { open class JoinRoomByAddressStateProvider : PreviewParameterProvider<JoinRoomByAddressState> {
override val values: Sequence<JoinRoomByAddressState> override val values: Sequence<JoinRoomByAddressState>
get() = sequenceOf( get() = sequenceOf(
aJoinRoomByAddressState(), aJoinRoomByAddressState(),
aJoinRoomByAddressState("#room-"), aJoinRoomByAddressState(address = "#room-"),
aJoinRoomByAddressState("#room-", addressState = RoomAddressState.Invalid), aJoinRoomByAddressState(address = "#room-", addressState = RoomAddressState.Invalid),
aJoinRoomByAddressState("#room-name:matrix.org", addressState = RoomAddressState.Valid(true)), aJoinRoomByAddressState(address = "#room-name:matrix.org", addressState = RoomAddressState.Resolving),
aJoinRoomByAddressState("#room-name-here:matrix.org", addressState = RoomAddressState.Valid(false)), aJoinRoomByAddressState(address = "#room-name-none:matrix.org", addressState = RoomAddressState.RoomNotFound),
// Add other states here aJoinRoomByAddressState(
address = "#room-name:matrix.org",
addressState = RoomAddressState.RoomFound(ResolvedRoomAlias(RoomId("!aRoom:id"), emptyList())),
),
) )
} }

View file

@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
@ -23,7 +24,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
@ -60,13 +63,16 @@ fun JoinRoomByAddressView(
requestFocus = sheetState.isVisible, requestFocus = sheetState.isVisible,
onAddressChange = { onAddressChange = {
state.eventSink(JoinRoomByAddressEvents.UpdateAddress(it)) state.eventSink(JoinRoomByAddressEvents.UpdateAddress(it))
} },
onContinue = {
state.eventSink(JoinRoomByAddressEvents.Continue)
},
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Button( Button(
text = stringResource(CommonStrings.action_continue), text = stringResource(CommonStrings.action_continue),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = state.addressState is RoomAddressState.Valid, showProgress = state.addressState is RoomAddressState.Resolving,
onClick = { onClick = {
state.eventSink(JoinRoomByAddressEvents.Continue) state.eventSink(JoinRoomByAddressEvents.Continue)
} }
@ -81,6 +87,7 @@ private fun RoomAddressField(
addressState: RoomAddressState, addressState: RoomAddressState,
requestFocus: Boolean, requestFocus: Boolean,
onAddressChange: (String) -> Unit, onAddressChange: (String) -> Unit,
onContinue: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@ -94,24 +101,26 @@ private fun RoomAddressField(
placeholder = "Enter...", placeholder = "Enter...",
supportingText = when (addressState) { supportingText = when (addressState) {
RoomAddressState.Invalid -> "Not a valid address" RoomAddressState.Invalid -> "Not a valid address"
RoomAddressState.Unknown -> "e.g. #room-name:matrix.org" is RoomAddressState.RoomFound -> "Matching room found"
is RoomAddressState.Valid -> if (addressState.matchingRoomFound) { RoomAddressState.RoomNotFound -> "Room not found"
"Matching room found" RoomAddressState.Unknown, RoomAddressState.Resolving -> "e.g. #room-name:matrix.org"
} else {
"e.g. #room-name:matrix.org"
}
}, },
validity = when (addressState) { validity = when (addressState) {
RoomAddressState.Unknown -> null RoomAddressState.Unknown, RoomAddressState.Resolving -> null
RoomAddressState.Invalid -> TextFieldValidity.Invalid RoomAddressState.Invalid, RoomAddressState.RoomNotFound -> TextFieldValidity.Invalid
is RoomAddressState.Valid -> if (addressState.matchingRoomFound) TextFieldValidity.Valid else null is RoomAddressState.RoomFound -> TextFieldValidity.Valid
}, },
onValueChange = onAddressChange, onValueChange = onAddressChange,
singleLine = true, singleLine = true,
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None, capitalization = KeyboardCapitalization.None,
autoCorrectEnabled = false, autoCorrectEnabled = false,
keyboardType = KeyboardType.Uri,
imeAction = ImeAction.Go
), ),
keyboardActions = KeyboardActions(
onGo = { onContinue() }
)
) )
} }