feat(join by alias) : improve state management
This commit is contained in:
parent
3142cd7275
commit
cfbd67d4fd
4 changed files with 92 additions and 34 deletions
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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() }
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue