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.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.matrix.api.MatrixClient
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 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(
@Assisted private val navigator: CreateRoomNavigator,
@@ -40,16 +45,20 @@ class JoinRoomByAddressPresenter @AssistedInject constructor(
@Composable
override fun present(): JoinRoomByAddressState {
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) {
when (event) {
JoinRoomByAddressEvents.Continue -> {
navigator.onDismissJoinRoomByAddress()
navigator.onOpenRoom(RoomIdOrAlias.Alias(RoomAlias(address)))
when (val currentState = internalAddressState) {
is RoomAddressState.RoomFound -> onRoomFound(currentState)
else -> validateAddress = true
}
}
JoinRoomByAddressEvents.Dismiss -> navigator.onDismissJoinRoomByAddress()
is JoinRoomByAddressEvents.UpdateAddress -> {
validateAddress = false
address = event.address.trim()
}
}
@@ -57,9 +66,25 @@ class JoinRoomByAddressPresenter @AssistedInject constructor(
RoomAddressStateEffect(
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(
address = address,
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
private fun RoomAddressStateEffect(
fullAddress: String,
@@ -74,24 +104,35 @@ class JoinRoomByAddressPresenter @AssistedInject constructor(
) {
val onChange by rememberUpdatedState(onRoomAddressStateChange)
LaunchedEffect(fullAddress) {
if (fullAddress.isEmpty()) {
onChange(RoomAddressState.Unknown)
return@LaunchedEffect
}
// debounce the room address validation
// Whenever the address changes, reset the state to unknown
onChange(RoomAddressState.Unknown)
// debounce the room address resolution
delay(300)
val roomAlias = tryOrNull { RoomAlias(fullAddress) }
if (roomAlias == null || !roomAliasHelper.isRoomAliasValid(roomAlias)) {
onChange(RoomAddressState.Invalid)
if (roomAlias != null && roomAliasHelper.isRoomAliasValid(roomAlias)) {
onChange(RoomAddressState.Resolving)
onChange(client.resolveRoomAddress(roomAlias))
} else {
onChange(RoomAddressState.Valid(matchingRoomFound = false))
client.resolveRoomAlias(roomAlias)
.onSuccess { resolved ->
onChange(RoomAddressState.Valid(matchingRoomFound = resolved.isPresent))
}
onChange(RoomAddressState.Invalid)
}
}
}
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
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
data class JoinRoomByAddressState(
val address: String,
@@ -19,5 +20,7 @@ data class JoinRoomByAddressState(
sealed interface RoomAddressState {
data object Unknown : 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
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> {
override val values: Sequence<JoinRoomByAddressState>
get() = sequenceOf(
aJoinRoomByAddressState(),
aJoinRoomByAddressState("#room-"),
aJoinRoomByAddressState("#room-", addressState = RoomAddressState.Invalid),
aJoinRoomByAddressState("#room-name:matrix.org", addressState = RoomAddressState.Valid(true)),
aJoinRoomByAddressState("#room-name-here:matrix.org", addressState = RoomAddressState.Valid(false)),
// Add other states here
aJoinRoomByAddressState(address = "#room-"),
aJoinRoomByAddressState(address = "#room-", addressState = RoomAddressState.Invalid),
aJoinRoomByAddressState(address = "#room-name:matrix.org", addressState = RoomAddressState.Resolving),
aJoinRoomByAddressState(address = "#room-name-none:matrix.org", addressState = RoomAddressState.RoomNotFound),
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
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.res.stringResource
import androidx.compose.ui.text.input.ImeAction
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.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreview
@@ -60,13 +63,16 @@ fun JoinRoomByAddressView(
requestFocus = sheetState.isVisible,
onAddressChange = {
state.eventSink(JoinRoomByAddressEvents.UpdateAddress(it))
}
},
onContinue = {
state.eventSink(JoinRoomByAddressEvents.Continue)
},
)
Spacer(modifier = Modifier.height(24.dp))
Button(
text = stringResource(CommonStrings.action_continue),
modifier = Modifier.fillMaxWidth(),
enabled = state.addressState is RoomAddressState.Valid,
showProgress = state.addressState is RoomAddressState.Resolving,
onClick = {
state.eventSink(JoinRoomByAddressEvents.Continue)
}
@@ -81,6 +87,7 @@ private fun RoomAddressField(
addressState: RoomAddressState,
requestFocus: Boolean,
onAddressChange: (String) -> Unit,
onContinue: () -> Unit,
modifier: Modifier = Modifier,
) {
val focusRequester = remember { FocusRequester() }
@@ -94,24 +101,26 @@ private fun RoomAddressField(
placeholder = "Enter...",
supportingText = when (addressState) {
RoomAddressState.Invalid -> "Not a valid address"
RoomAddressState.Unknown -> "e.g. #room-name:matrix.org"
is RoomAddressState.Valid -> if (addressState.matchingRoomFound) {
"Matching room found"
} else {
"e.g. #room-name:matrix.org"
}
is RoomAddressState.RoomFound -> "Matching room found"
RoomAddressState.RoomNotFound -> "Room not found"
RoomAddressState.Unknown, RoomAddressState.Resolving -> "e.g. #room-name:matrix.org"
},
validity = when (addressState) {
RoomAddressState.Unknown -> null
RoomAddressState.Invalid -> TextFieldValidity.Invalid
is RoomAddressState.Valid -> if (addressState.matchingRoomFound) TextFieldValidity.Valid else null
RoomAddressState.Unknown, RoomAddressState.Resolving -> null
RoomAddressState.Invalid, RoomAddressState.RoomNotFound -> TextFieldValidity.Invalid
is RoomAddressState.RoomFound -> TextFieldValidity.Valid
},
onValueChange = onAddressChange,
singleLine = true,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrectEnabled = false,
keyboardType = KeyboardType.Uri,
imeAction = ImeAction.Go
),
keyboardActions = KeyboardActions(
onGo = { onContinue() }
)
)
}