From cfbd67d4fd8fdde110d2c2379f877a3b3c5b9935 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 24 Feb 2025 20:39:16 +0100 Subject: [PATCH] feat(join by alias) : improve state management --- .../JoinRoomByAddressPresenter.kt | 75 ++++++++++++++----- .../joinbyaddress/JoinRoomByAddressState.kt | 5 +- .../JoinRoomByAddressStateProvider.kt | 15 ++-- .../joinbyaddress/JoinRoomByAddressView.kt | 31 +++++--- 4 files changed, 92 insertions(+), 34 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressPresenter.kt index d721deb467..be1334e253 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressPresenter.kt @@ -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.Unknown) } + var internalAddressState by remember { mutableStateOf(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 + } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressState.kt index 6d5be3dfef..11791181e1 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressState.kt @@ -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 } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressStateProvider.kt index de20b1bc1c..12e605e04c 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressStateProvider.kt @@ -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 { override val values: Sequence 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())), + ), ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressView.kt index 5e7d515090..8c27cbfe81 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/joinbyaddress/JoinRoomByAddressView.kt @@ -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() } + ) ) }