Merge pull request #4302 from element-hq/feature/fga/join_room_by_alias
Feature : join room by address
This commit is contained in:
commit
3c30bec1c2
58 changed files with 880 additions and 122 deletions
|
|
@ -357,8 +357,8 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
NavTarget.CreateRoom -> {
|
NavTarget.CreateRoom -> {
|
||||||
val callback = object : CreateRoomEntryPoint.Callback {
|
val callback = object : CreateRoomEntryPoint.Callback {
|
||||||
override fun onSuccess(roomId: RoomId) {
|
override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>) {
|
||||||
backstack.replace(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
backstack.replace(NavTarget.Room(roomIdOrAlias = roomIdOrAlias, serverNames = serverNames))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
|
||||||
interface CreateRoomEntryPoint : FeatureEntryPoint {
|
interface CreateRoomEntryPoint : FeatureEntryPoint {
|
||||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||||
|
|
@ -21,6 +21,6 @@ interface CreateRoomEntryPoint : FeatureEntryPoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onSuccess(roomId: RoomId)
|
fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.createroom
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
|
import io.element.android.features.createroom.impl.CreateRoomFlowNode.NavTarget
|
||||||
|
import io.element.android.libraries.architecture.overlay.Overlay
|
||||||
|
import io.element.android.libraries.architecture.overlay.operation.hide
|
||||||
|
import io.element.android.libraries.architecture.overlay.operation.show
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
|
||||||
|
interface CreateRoomNavigator : Plugin {
|
||||||
|
fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>)
|
||||||
|
fun onCreateNewRoom()
|
||||||
|
fun onShowJoinRoomByAddress()
|
||||||
|
fun onDismissJoinRoomByAddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultCreateRoomNavigator(
|
||||||
|
private val backstack: BackStack<NavTarget>,
|
||||||
|
private val overlay: Overlay<NavTarget>,
|
||||||
|
private val openRoom: (RoomIdOrAlias, List<String>) -> Unit,
|
||||||
|
) : CreateRoomNavigator {
|
||||||
|
override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>) = openRoom(roomIdOrAlias, serverNames)
|
||||||
|
|
||||||
|
override fun onCreateNewRoom() {
|
||||||
|
backstack.push(NavTarget.NewRoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShowJoinRoomByAddress() {
|
||||||
|
overlay.show(NavTarget.JoinByAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDismissJoinRoomByAddress() {
|
||||||
|
overlay.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.createroom.CreateRoomNavigator
|
||||||
import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
|
import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
|
||||||
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode
|
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode
|
||||||
import io.element.android.features.createroom.impl.di.CreateRoomComponent
|
import io.element.android.features.createroom.impl.di.CreateRoomComponent
|
||||||
|
|
@ -46,6 +47,7 @@ class ConfigureRoomFlowNode @AssistedInject constructor(
|
||||||
private val component by lazy {
|
private val component by lazy {
|
||||||
parent!!.bindings<CreateRoomComponent.ParentBindings>().createRoomComponentBuilder().build()
|
parent!!.bindings<CreateRoomComponent.ParentBindings>().createRoomComponentBuilder().build()
|
||||||
}
|
}
|
||||||
|
private val navigator = plugins<CreateRoomNavigator>().first()
|
||||||
|
|
||||||
override val daggerComponent: Any
|
override val daggerComponent: Any
|
||||||
get() = component
|
get() = component
|
||||||
|
|
@ -69,8 +71,7 @@ class ConfigureRoomFlowNode @AssistedInject constructor(
|
||||||
createNode<AddPeopleNode>(buildContext = buildContext, plugins = listOf(callback))
|
createNode<AddPeopleNode>(buildContext = buildContext, plugins = listOf(callback))
|
||||||
}
|
}
|
||||||
NavTarget.ConfigureRoom -> {
|
NavTarget.ConfigureRoom -> {
|
||||||
val callbacks = plugins<ConfigureRoomNode.Callback>()
|
createNode<ConfigureRoomNode>(buildContext = buildContext, plugins = listOf(navigator))
|
||||||
createNode<ConfigureRoomNode>(buildContext = buildContext, plugins = callbacks)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,25 +8,28 @@
|
||||||
package io.element.android.features.createroom.impl
|
package io.element.android.features.createroom.impl
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.navigation.transition.JumpToEndTransitionHandler
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.plugin.plugins
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.createroom.DefaultCreateRoomNavigator
|
||||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||||
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode
|
import io.element.android.features.createroom.impl.joinbyaddress.JoinRoomByAddressNode
|
||||||
import io.element.android.features.createroom.impl.root.CreateRoomRootNode
|
import io.element.android.features.createroom.impl.root.CreateRoomRootNode
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
|
import io.element.android.libraries.architecture.OverlayView
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
|
|
@ -47,35 +50,38 @@ class CreateRoomFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object NewRoom : NavTarget
|
data object NewRoom : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object JoinByAddress : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val navigator = DefaultCreateRoomNavigator(
|
||||||
|
backstack = backstack,
|
||||||
|
overlay = overlay,
|
||||||
|
openRoom = { roomIdOrAlias, viaServers ->
|
||||||
|
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onOpenRoom(roomIdOrAlias, viaServers) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
NavTarget.Root -> {
|
NavTarget.Root -> {
|
||||||
val callback = object : CreateRoomRootNode.Callback {
|
createNode<CreateRoomRootNode>(buildContext = buildContext, plugins = listOf(navigator))
|
||||||
override fun onCreateNewRoom() {
|
|
||||||
backstack.push(NavTarget.NewRoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartChatSuccess(roomId: RoomId) {
|
|
||||||
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onSuccess(roomId) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
createNode<CreateRoomRootNode>(buildContext = buildContext, plugins = listOf(callback))
|
|
||||||
}
|
}
|
||||||
NavTarget.NewRoom -> {
|
NavTarget.NewRoom -> {
|
||||||
val callback = object : ConfigureRoomNode.Callback {
|
createNode<ConfigureRoomFlowNode>(buildContext = buildContext, plugins = listOf(navigator))
|
||||||
override fun onCreateRoomSuccess(roomId: RoomId) {
|
}
|
||||||
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onSuccess(roomId) }
|
NavTarget.JoinByAddress -> {
|
||||||
}
|
createNode<JoinRoomByAddressNode>(buildContext = buildContext, plugins = listOf(navigator))
|
||||||
}
|
|
||||||
createNode<ConfigureRoomFlowNode>(buildContext = buildContext, plugins = listOf(callback))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
BackstackView()
|
Box(modifier = modifier) {
|
||||||
|
BackstackView()
|
||||||
|
OverlayView(transitionHandler = remember { JumpToEndTransitionHandler() })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,9 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.MobileScreen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.createroom.CreateRoomNavigator
|
||||||
import io.element.android.features.createroom.impl.di.CreateRoomScope
|
import io.element.android.features.createroom.impl.di.CreateRoomScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
|
||||||
@ContributesNode(CreateRoomScope::class)
|
@ContributesNode(CreateRoomScope::class)
|
||||||
|
|
@ -29,6 +30,8 @@ class ConfigureRoomNode @AssistedInject constructor(
|
||||||
private val presenter: ConfigureRoomPresenter,
|
private val presenter: ConfigureRoomPresenter,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
private val navigator = plugins<CreateRoomNavigator>().first()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
onResume = {
|
onResume = {
|
||||||
|
|
@ -37,14 +40,6 @@ class ConfigureRoomNode @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback : Plugin {
|
|
||||||
fun onCreateRoomSuccess(roomId: RoomId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onCreateRoomSuccess(roomId: RoomId) {
|
|
||||||
plugins<Callback>().forEach { it.onCreateRoomSuccess(roomId) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
@ -52,7 +47,9 @@ class ConfigureRoomNode @AssistedInject constructor(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onBackClick = this::navigateUp,
|
onBackClick = this::navigateUp,
|
||||||
onCreateRoomSuccess = this::onCreateRoomSuccess,
|
onCreateRoomSuccess = {
|
||||||
|
navigator.onOpenRoom(roomIdOrAlias = it.toRoomIdOrAlias(), serverNames = emptyList())
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.joinbyaddress
|
||||||
|
|
||||||
|
sealed interface JoinRoomByAddressEvents {
|
||||||
|
data object Dismiss : JoinRoomByAddressEvents
|
||||||
|
data object Continue : JoinRoomByAddressEvents
|
||||||
|
data class UpdateAddress(val address: String) : JoinRoomByAddressEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.joinbyaddress
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.createroom.CreateRoomNavigator
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
||||||
|
@ContributesNode(SessionScope::class)
|
||||||
|
class JoinRoomByAddressNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
presenterFactory: JoinRoomByAddressPresenter.Factory,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
private val navigator = plugins<CreateRoomNavigator>().first()
|
||||||
|
private val presenter = presenterFactory.create(navigator)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
val state = presenter.present()
|
||||||
|
JoinRoomByAddressView(
|
||||||
|
state = state,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.features.createroom.CreateRoomNavigator
|
||||||
|
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.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,
|
||||||
|
private val client: MatrixClient,
|
||||||
|
private val roomAliasHelper: RoomAliasHelper,
|
||||||
|
) : Presenter<JoinRoomByAddressState> {
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(navigator: CreateRoomNavigator): JoinRoomByAddressPresenter
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): JoinRoomByAddressState {
|
||||||
|
var address by remember { mutableStateOf("") }
|
||||||
|
var internalAddressState by remember { mutableStateOf<RoomAddressState>(RoomAddressState.Unknown) }
|
||||||
|
var validateAddress: Boolean by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
fun handleEvents(event: JoinRoomByAddressEvents) {
|
||||||
|
when (event) {
|
||||||
|
JoinRoomByAddressEvents.Continue -> {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomAddressStateEffect(
|
||||||
|
fullAddress = address,
|
||||||
|
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,
|
||||||
|
eventSink = ::handleEvents
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onRoomFound(state: RoomAddressState.RoomFound) {
|
||||||
|
navigator.onDismissJoinRoomByAddress()
|
||||||
|
navigator.onOpenRoom(
|
||||||
|
roomIdOrAlias = state.resolved.roomId.toRoomIdOrAlias(),
|
||||||
|
serverNames = state.resolved.servers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RoomAddressStateEffect(
|
||||||
|
fullAddress: String,
|
||||||
|
onRoomAddressStateChange: (RoomAddressState) -> Unit,
|
||||||
|
) {
|
||||||
|
val onChange by rememberUpdatedState(onRoomAddressStateChange)
|
||||||
|
LaunchedEffect(fullAddress) {
|
||||||
|
// 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.Resolving)
|
||||||
|
onChange(client.resolveRoomAddress(roomAlias))
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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,
|
||||||
|
val addressState: RoomAddressState,
|
||||||
|
val eventSink: (JoinRoomByAddressEvents) -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed interface RoomAddressState {
|
||||||
|
data object Unknown : RoomAddressState
|
||||||
|
data object Invalid : RoomAddressState
|
||||||
|
data object Resolving : RoomAddressState
|
||||||
|
data object RoomNotFound : RoomAddressState
|
||||||
|
data class RoomFound(val resolved: ResolvedRoomAlias) : RoomAddressState
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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(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())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aJoinRoomByAddressState(
|
||||||
|
address: String = "",
|
||||||
|
addressState: RoomAddressState = RoomAddressState.Unknown,
|
||||||
|
eventSink: (JoinRoomByAddressEvents) -> Unit = {},
|
||||||
|
) = JoinRoomByAddressState(
|
||||||
|
address = address,
|
||||||
|
addressState = addressState,
|
||||||
|
eventSink = eventSink
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.joinbyaddress
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
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
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
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.features.createroom.impl.R
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextFieldValidity
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun JoinRoomByAddressView(
|
||||||
|
state: JoinRoomByAddressState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
ModalBottomSheet(
|
||||||
|
modifier = modifier,
|
||||||
|
sheetState = sheetState,
|
||||||
|
onDismissRequest = {
|
||||||
|
state.eventSink(JoinRoomByAddressEvents.Dismiss)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(all = 16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
RoomAddressField(
|
||||||
|
address = state.address,
|
||||||
|
addressState = state.addressState,
|
||||||
|
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(),
|
||||||
|
showProgress = state.addressState is RoomAddressState.Resolving,
|
||||||
|
onClick = {
|
||||||
|
state.eventSink(JoinRoomByAddressEvents.Continue)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RoomAddressField(
|
||||||
|
address: String,
|
||||||
|
addressState: RoomAddressState,
|
||||||
|
requestFocus: Boolean,
|
||||||
|
onAddressChange: (String) -> Unit,
|
||||||
|
onContinue: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
if (requestFocus) {
|
||||||
|
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
||||||
|
}
|
||||||
|
TextField(
|
||||||
|
modifier = modifier.focusRequester(focusRequester),
|
||||||
|
value = address,
|
||||||
|
label = stringResource(R.string.screen_start_chat_join_room_by_address_action),
|
||||||
|
placeholder = stringResource(R.string.screen_start_chat_join_room_by_address_placeholder),
|
||||||
|
supportingText = when (addressState) {
|
||||||
|
RoomAddressState.Invalid -> stringResource(R.string.screen_start_chat_join_room_by_address_invalid_address)
|
||||||
|
is RoomAddressState.RoomFound -> stringResource(R.string.screen_start_chat_join_room_by_address_room_found)
|
||||||
|
RoomAddressState.RoomNotFound -> stringResource(R.string.screen_start_chat_join_room_by_address_room_not_found)
|
||||||
|
RoomAddressState.Unknown, RoomAddressState.Resolving -> stringResource(R.string.screen_start_chat_join_room_by_address_supporting_text)
|
||||||
|
},
|
||||||
|
validity = when (addressState) {
|
||||||
|
RoomAddressState.Unknown, RoomAddressState.Resolving -> TextFieldValidity.None
|
||||||
|
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() }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun JoinRoomByAddressViewPreview(
|
||||||
|
@PreviewParameter(JoinRoomByAddressStateProvider::class) state: JoinRoomByAddressState
|
||||||
|
) = ElementPreview {
|
||||||
|
JoinRoomByAddressView(state = state)
|
||||||
|
}
|
||||||
|
|
@ -20,9 +20,10 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.MobileScreen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.createroom.CreateRoomNavigator
|
||||||
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
|
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
|
|
@ -33,18 +34,7 @@ class CreateRoomRootNode @AssistedInject constructor(
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val inviteFriendsUseCase: InviteFriendsUseCase,
|
private val inviteFriendsUseCase: InviteFriendsUseCase,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
interface Callback : Plugin {
|
private val navigator = plugins<CreateRoomNavigator>().first()
|
||||||
fun onCreateNewRoom()
|
|
||||||
fun onStartChatSuccess(roomId: RoomId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onCreateNewRoom() {
|
|
||||||
plugins<Callback>().forEach { it.onCreateNewRoom() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onStartChatSuccess(roomId: RoomId) {
|
|
||||||
plugins<Callback>().forEach { it.onStartChatSuccess(roomId) }
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
|
|
@ -60,8 +50,11 @@ class CreateRoomRootNode @AssistedInject constructor(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onCloseClick = this::navigateUp,
|
onCloseClick = this::navigateUp,
|
||||||
onNewRoomClick = ::onCreateNewRoom,
|
onNewRoomClick = navigator::onCreateNewRoom,
|
||||||
onOpenDM = ::onStartChatSuccess,
|
onOpenDM = {
|
||||||
|
navigator.onOpenRoom(roomIdOrAlias = it.toRoomIdOrAlias(), serverNames = emptyList())
|
||||||
|
},
|
||||||
|
onJoinByAddressClick = navigator::onShowJoinRoomByAddress,
|
||||||
onInviteFriendsClick = { invitePeople(activity) }
|
onInviteFriendsClick = { invitePeople(activity) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ fun CreateRoomRootView(
|
||||||
onNewRoomClick: () -> Unit,
|
onNewRoomClick: () -> Unit,
|
||||||
onOpenDM: (RoomId) -> Unit,
|
onOpenDM: (RoomId) -> Unit,
|
||||||
onInviteFriendsClick: () -> Unit,
|
onInviteFriendsClick: () -> Unit,
|
||||||
|
onJoinByAddressClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
@ -89,6 +90,7 @@ fun CreateRoomRootView(
|
||||||
state = state,
|
state = state,
|
||||||
onNewRoomClick = onNewRoomClick,
|
onNewRoomClick = onNewRoomClick,
|
||||||
onInvitePeopleClick = onInviteFriendsClick,
|
onInvitePeopleClick = onInviteFriendsClick,
|
||||||
|
onJoinByAddressClick = onJoinByAddressClick,
|
||||||
onDmClick = onOpenDM,
|
onDmClick = onOpenDM,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -153,6 +155,7 @@ private fun CreateRoomActionButtonsList(
|
||||||
state: CreateRoomRootState,
|
state: CreateRoomRootState,
|
||||||
onNewRoomClick: () -> Unit,
|
onNewRoomClick: () -> Unit,
|
||||||
onInvitePeopleClick: () -> Unit,
|
onInvitePeopleClick: () -> Unit,
|
||||||
|
onJoinByAddressClick: () -> Unit,
|
||||||
onDmClick: (RoomId) -> Unit,
|
onDmClick: (RoomId) -> Unit,
|
||||||
) {
|
) {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
|
|
@ -170,6 +173,13 @@ private fun CreateRoomActionButtonsList(
|
||||||
onClick = onInvitePeopleClick,
|
onClick = onInvitePeopleClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
|
CreateRoomActionButton(
|
||||||
|
iconRes = CompoundDrawables.ic_compound_room,
|
||||||
|
text = stringResource(R.string.screen_start_chat_join_room_by_address_action),
|
||||||
|
onClick = onJoinByAddressClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
if (state.userListState.recentDirectRooms.isNotEmpty()) {
|
if (state.userListState.recentDirectRooms.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
ListSectionHeader(
|
ListSectionHeader(
|
||||||
|
|
@ -230,6 +240,7 @@ internal fun CreateRoomRootViewPreview(@PreviewParameter(CreateRoomRootStateProv
|
||||||
onCloseClick = {},
|
onCloseClick = {},
|
||||||
onNewRoomClick = {},
|
onNewRoomClick = {},
|
||||||
onOpenDM = {},
|
onOpenDM = {},
|
||||||
|
onJoinByAddressClick = {},
|
||||||
onInviteFriendsClick = {},
|
onInviteFriendsClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,4 +20,10 @@ You can change this anytime in room settings."</string>
|
||||||
<string name="screen_create_room_title">"Create a room"</string>
|
<string name="screen_create_room_title">"Create a room"</string>
|
||||||
<string name="screen_create_room_topic_label">"Topic (optional)"</string>
|
<string name="screen_create_room_topic_label">"Topic (optional)"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"An error occurred when trying to start a chat"</string>
|
<string name="screen_start_chat_error_starting_chat">"An error occurred when trying to start a chat"</string>
|
||||||
|
<string name="screen_start_chat_join_room_by_address_action">"Join room by address"</string>
|
||||||
|
<string name="screen_start_chat_join_room_by_address_invalid_address">"Not a valid address"</string>
|
||||||
|
<string name="screen_start_chat_join_room_by_address_placeholder">"Enter…"</string>
|
||||||
|
<string name="screen_start_chat_join_room_by_address_room_found">"Matching room found"</string>
|
||||||
|
<string name="screen_start_chat_join_room_by_address_room_not_found">"Room not found"</string>
|
||||||
|
<string name="screen_start_chat_join_room_by_address_supporting_text">"e.g. #room-name:matrix.org"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl
|
||||||
|
|
||||||
|
import io.element.android.features.createroom.CreateRoomNavigator
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
|
||||||
|
class FakeCreateRoomNavigator(
|
||||||
|
private val openRoomLambda: (roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>) -> Unit = { _, _ -> },
|
||||||
|
private val createNewRoomLambda: () -> Unit = {},
|
||||||
|
private val showJoinRoomByAddressLambda: () -> Unit = {},
|
||||||
|
private val dismissJoinRoomByAddressLambda: () -> Unit = {},
|
||||||
|
) : CreateRoomNavigator {
|
||||||
|
override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>) {
|
||||||
|
openRoomLambda(roomIdOrAlias, serverNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateNewRoom() {
|
||||||
|
createNewRoomLambda()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShowJoinRoomByAddress() {
|
||||||
|
showJoinRoomByAddressLambda()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDismissJoinRoomByAddress() {
|
||||||
|
dismissJoinRoomByAddressLambda()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.joinbyaddress
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.createroom.CreateRoomNavigator
|
||||||
|
import io.element.android.features.createroom.impl.FakeCreateRoomNavigator
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
|
||||||
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class JoinRoomByAddressPresenterTest {
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
val presenter = createJoinRoomByAddressPresenter()
|
||||||
|
presenter.test {
|
||||||
|
with(awaitItem()) {
|
||||||
|
assertThat(address).isEmpty()
|
||||||
|
assertThat(addressState).isEqualTo(RoomAddressState.Unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - invalid address`() = runTest {
|
||||||
|
val presenter = createJoinRoomByAddressPresenter(
|
||||||
|
roomAliasHelper = FakeRoomAliasHelper(
|
||||||
|
isRoomAliasValidLambda = { false }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
with(awaitItem()) {
|
||||||
|
eventSink(JoinRoomByAddressEvents.UpdateAddress("invalid_address"))
|
||||||
|
}
|
||||||
|
with(awaitItem()) {
|
||||||
|
assertThat(address).isEqualTo("invalid_address")
|
||||||
|
assertThat(addressState).isEqualTo(RoomAddressState.Unknown)
|
||||||
|
eventSink(JoinRoomByAddressEvents.Continue)
|
||||||
|
}
|
||||||
|
// The address should be marked as invalid only after the user tries to continue
|
||||||
|
with(awaitItem()) {
|
||||||
|
assertThat(address).isEqualTo("invalid_address")
|
||||||
|
assertThat(addressState).isEqualTo(RoomAddressState.Invalid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - room found`() = runTest {
|
||||||
|
val openRoomLambda = lambdaRecorder<RoomIdOrAlias, List<String>, Unit> { _, _ -> }
|
||||||
|
val dismissJoinRoomByAddressLambda = lambdaRecorder<Unit> { }
|
||||||
|
val navigator = FakeCreateRoomNavigator(
|
||||||
|
openRoomLambda = openRoomLambda,
|
||||||
|
dismissJoinRoomByAddressLambda = dismissJoinRoomByAddressLambda
|
||||||
|
)
|
||||||
|
val presenter = createJoinRoomByAddressPresenter(navigator = navigator)
|
||||||
|
presenter.test {
|
||||||
|
with(awaitItem()) {
|
||||||
|
eventSink(JoinRoomByAddressEvents.UpdateAddress("#room_found:matrix.org"))
|
||||||
|
}
|
||||||
|
with(awaitItem()) {
|
||||||
|
assertThat(address).isEqualTo("#room_found:matrix.org")
|
||||||
|
assertThat(addressState).isEqualTo(RoomAddressState.Unknown)
|
||||||
|
}
|
||||||
|
with(awaitItem()) {
|
||||||
|
assertThat(address).isEqualTo("#room_found:matrix.org")
|
||||||
|
assertThat(addressState).isInstanceOf(RoomAddressState.RoomFound::class.java)
|
||||||
|
eventSink(JoinRoomByAddressEvents.Continue)
|
||||||
|
}
|
||||||
|
assert(openRoomLambda).isCalledOnce()
|
||||||
|
assert(dismissJoinRoomByAddressLambda).isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - room not found`() = runTest {
|
||||||
|
val presenter = createJoinRoomByAddressPresenter(
|
||||||
|
matrixClient = FakeMatrixClient(
|
||||||
|
resolveRoomAliasResult = { Result.failure(RuntimeException()) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
with(awaitItem()) {
|
||||||
|
eventSink(JoinRoomByAddressEvents.UpdateAddress("#room_not_found:matrix.org"))
|
||||||
|
}
|
||||||
|
with(awaitItem()) {
|
||||||
|
assertThat(address).isEqualTo("#room_not_found:matrix.org")
|
||||||
|
assertThat(addressState).isEqualTo(RoomAddressState.Unknown)
|
||||||
|
eventSink(JoinRoomByAddressEvents.Continue)
|
||||||
|
}
|
||||||
|
with(awaitItem()) {
|
||||||
|
assertThat(address).isEqualTo("#room_not_found:matrix.org")
|
||||||
|
assertThat(addressState).isEqualTo(RoomAddressState.Resolving)
|
||||||
|
}
|
||||||
|
with(awaitItem()) {
|
||||||
|
assertThat(address).isEqualTo("#room_not_found:matrix.org")
|
||||||
|
assertThat(addressState).isEqualTo(RoomAddressState.RoomNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - dismiss`() = runTest {
|
||||||
|
val dismissJoinRoomByAddressLambda = lambdaRecorder<Unit> { }
|
||||||
|
val navigator = FakeCreateRoomNavigator(
|
||||||
|
dismissJoinRoomByAddressLambda = dismissJoinRoomByAddressLambda
|
||||||
|
)
|
||||||
|
val presenter = createJoinRoomByAddressPresenter(navigator = navigator)
|
||||||
|
presenter.test {
|
||||||
|
with(awaitItem()) {
|
||||||
|
eventSink(JoinRoomByAddressEvents.Dismiss)
|
||||||
|
}
|
||||||
|
assert(dismissJoinRoomByAddressLambda).isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createJoinRoomByAddressPresenter(
|
||||||
|
navigator: CreateRoomNavigator = FakeCreateRoomNavigator(),
|
||||||
|
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||||
|
roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(),
|
||||||
|
): JoinRoomByAddressPresenter {
|
||||||
|
return JoinRoomByAddressPresenter(
|
||||||
|
navigator = navigator,
|
||||||
|
client = matrixClient,
|
||||||
|
roomAliasHelper = roomAliasHelper,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.joinbyaddress
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performTextInput
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.features.createroom.impl.R
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class JoinRoomByAddressViewTest {
|
||||||
|
@get:Rule
|
||||||
|
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `entering text emits the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomByAddressEvents>()
|
||||||
|
rule.setJoinRoomByAddressView(
|
||||||
|
aJoinRoomByAddressState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val text = rule.activity.getString(R.string.screen_start_chat_join_room_by_address_action)
|
||||||
|
rule.onNodeWithText(text).performTextInput("#address:matrix.org")
|
||||||
|
eventsRecorder.assertSingle(JoinRoomByAddressEvents.UpdateAddress("#address:matrix.org"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on continue emits the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomByAddressEvents>()
|
||||||
|
rule.setJoinRoomByAddressView(
|
||||||
|
aJoinRoomByAddressState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_continue)
|
||||||
|
eventsRecorder.assertSingle(JoinRoomByAddressEvents.Continue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinRoomByAddressView(
|
||||||
|
state: JoinRoomByAddressState,
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
JoinRoomByAddressView(state = state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -101,6 +101,21 @@ class CreateRoomRootViewTest {
|
||||||
rule.onNodeWithText(firstRoom.matrixUser.getBestName()).performClick()
|
rule.onNodeWithText(firstRoom.matrixUser.getBestName()).performClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
|
@Test
|
||||||
|
fun `clicking on Join room by address invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<CreateRoomRootEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setCreateRoomRootView(
|
||||||
|
aCreateRoomRootState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onJoinRoomByAddressClick = it
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_start_chat_join_room_by_address_action)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCreateRoomRootView(
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCreateRoomRootView(
|
||||||
|
|
@ -109,6 +124,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCreat
|
||||||
onNewRoomClick: () -> Unit = EnsureNeverCalled(),
|
onNewRoomClick: () -> Unit = EnsureNeverCalled(),
|
||||||
onOpenDM: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
onOpenDM: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onInviteFriendsClick: () -> Unit = EnsureNeverCalled(),
|
onInviteFriendsClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onJoinRoomByAddressClick: () -> Unit = EnsureNeverCalled(),
|
||||||
) {
|
) {
|
||||||
setContent {
|
setContent {
|
||||||
CreateRoomRootView(
|
CreateRoomRootView(
|
||||||
|
|
@ -117,6 +133,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCreat
|
||||||
onNewRoomClick = onNewRoomClick,
|
onNewRoomClick = onNewRoomClick,
|
||||||
onOpenDM = onOpenDM,
|
onOpenDM = onOpenDM,
|
||||||
onInviteFriendsClick = onInviteFriendsClick,
|
onInviteFriendsClick = onInviteFriendsClick,
|
||||||
|
onJoinByAddressClick = onJoinRoomByAddressClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground
|
import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextField
|
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextFieldValidity
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -90,7 +91,7 @@ fun BugReportView(
|
||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
}),
|
}),
|
||||||
minLines = 3,
|
minLines = 3,
|
||||||
isError = state.isDescriptionInError,
|
validity = if (state.isDescriptionInError) TextFieldValidity.Invalid else TextFieldValidity.None,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
|
||||||
|
|
@ -152,8 +152,7 @@ private fun RoomListScaffold(
|
||||||
onClick = onCreateRoomClick
|
onClick = onCreateRoomClick
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
// Note cannot use Icons.Outlined.EditSquare, it does not exist :/
|
imageVector = CompoundIcons.Plus(),
|
||||||
imageVector = CompoundIcons.Compose(),
|
|
||||||
contentDescription = stringResource(id = R.string.screen_roomlist_a11y_create_message),
|
contentDescription = stringResource(id = R.string.screen_roomlist_a11y_create_message),
|
||||||
tint = ElementTheme.colors.iconOnSolidPrimary,
|
tint = ElementTheme.colors.iconOnSolidPrimary,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextField
|
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextFieldValidity
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -99,7 +100,7 @@ private fun Content(text: String, onTextChange: (String) -> Unit, hasError: Bool
|
||||||
Icon(imageVector = image, description)
|
Icon(imageVector = image, description)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isError = hasError,
|
validity = if (hasError) TextFieldValidity.Invalid else TextFieldValidity.None,
|
||||||
supportingText = if (hasError) {
|
supportingText = if (hasError) {
|
||||||
stringResource(R.string.screen_reset_encryption_password_error)
|
stringResource(R.string.screen_reset_encryption_password_error)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ fun TextField(
|
||||||
placeholder: String? = null,
|
placeholder: String? = null,
|
||||||
leadingIcon: @Composable (() -> Unit)? = null,
|
leadingIcon: @Composable (() -> Unit)? = null,
|
||||||
trailingIcon: @Composable (() -> Unit)? = null,
|
trailingIcon: @Composable (() -> Unit)? = null,
|
||||||
isError: Boolean = false,
|
validity: TextFieldValidity = TextFieldValidity.None,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
singleLine: Boolean = false,
|
singleLine: Boolean = false,
|
||||||
|
|
@ -93,7 +93,7 @@ fun TextField(
|
||||||
readOnly = readOnly,
|
readOnly = readOnly,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
isFocused = isFocused,
|
isFocused = isFocused,
|
||||||
isError = isError,
|
validity = validity,
|
||||||
leadingIcon = leadingIcon,
|
leadingIcon = leadingIcon,
|
||||||
placeholder = placeholder,
|
placeholder = placeholder,
|
||||||
isTextEmpty = value.isEmpty(),
|
isTextEmpty = value.isEmpty(),
|
||||||
|
|
@ -114,7 +114,7 @@ fun TextField(
|
||||||
placeholder: String? = null,
|
placeholder: String? = null,
|
||||||
leadingIcon: @Composable (() -> Unit)? = null,
|
leadingIcon: @Composable (() -> Unit)? = null,
|
||||||
trailingIcon: @Composable (() -> Unit)? = null,
|
trailingIcon: @Composable (() -> Unit)? = null,
|
||||||
isError: Boolean = false,
|
validity: TextFieldValidity? = null,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
singleLine: Boolean = false,
|
singleLine: Boolean = false,
|
||||||
|
|
@ -149,7 +149,7 @@ fun TextField(
|
||||||
readOnly = readOnly,
|
readOnly = readOnly,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
isFocused = isFocused,
|
isFocused = isFocused,
|
||||||
isError = isError,
|
validity = validity,
|
||||||
leadingIcon = leadingIcon,
|
leadingIcon = leadingIcon,
|
||||||
placeholder = placeholder,
|
placeholder = placeholder,
|
||||||
isTextEmpty = value.text.isEmpty(),
|
isTextEmpty = value.text.isEmpty(),
|
||||||
|
|
@ -166,7 +166,7 @@ private fun DecorationBox(
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
isFocused: Boolean,
|
isFocused: Boolean,
|
||||||
isError: Boolean,
|
validity: TextFieldValidity?,
|
||||||
placeholder: String?,
|
placeholder: String?,
|
||||||
isTextEmpty: Boolean,
|
isTextEmpty: Boolean,
|
||||||
supportingText: String?,
|
supportingText: String?,
|
||||||
|
|
@ -187,7 +187,7 @@ private fun DecorationBox(
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
readOnly = readOnly,
|
readOnly = readOnly,
|
||||||
isFocused = isFocused,
|
isFocused = isFocused,
|
||||||
isError = isError
|
isError = validity == TextFieldValidity.Invalid
|
||||||
) {
|
) {
|
||||||
Row(modifier = Modifier.padding(16.dp)) {
|
Row(modifier = Modifier.padding(16.dp)) {
|
||||||
if (leadingIcon != null) {
|
if (leadingIcon != null) {
|
||||||
|
|
@ -216,7 +216,7 @@ private fun DecorationBox(
|
||||||
}
|
}
|
||||||
if (supportingText != null) {
|
if (supportingText != null) {
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
SupportingTextLayout(isError, supportingText)
|
SupportingTextLayout(validity, supportingText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -254,24 +254,45 @@ private fun TextFieldContainer(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SupportingTextLayout(isError: Boolean, supportingText: String) {
|
private fun SupportingTextLayout(validity: TextFieldValidity?, supportingText: String) {
|
||||||
Row(horizontalArrangement = spacedBy(4.dp)) {
|
Row(horizontalArrangement = spacedBy(4.dp)) {
|
||||||
if (isError) {
|
when (validity) {
|
||||||
Icon(
|
TextFieldValidity.Invalid -> {
|
||||||
imageVector = CompoundIcons.Error(),
|
Icon(
|
||||||
contentDescription = null,
|
imageVector = CompoundIcons.Error(),
|
||||||
modifier = Modifier.size(16.dp),
|
contentDescription = null,
|
||||||
tint = ElementTheme.colors.iconCriticalPrimary
|
modifier = Modifier.size(16.dp),
|
||||||
)
|
tint = ElementTheme.colors.iconCriticalPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TextFieldValidity.Valid -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.CheckCircleSolid(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = ElementTheme.colors.iconSuccessPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = supportingText,
|
text = supportingText,
|
||||||
color = if (isError) ElementTheme.colors.textCriticalPrimary else ElementTheme.colors.textSecondary,
|
color = when (validity) {
|
||||||
|
TextFieldValidity.Invalid -> ElementTheme.colors.textCriticalPrimary
|
||||||
|
TextFieldValidity.Valid -> ElementTheme.colors.textSuccessPrimary
|
||||||
|
else -> ElementTheme.colors.textSecondary
|
||||||
|
},
|
||||||
style = ElementTheme.typography.fontBodySmRegular,
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class TextFieldValidity {
|
||||||
|
None,
|
||||||
|
Invalid,
|
||||||
|
Valid
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun textFieldStyle(enabled: Boolean): TextStyle {
|
private fun textFieldStyle(enabled: Boolean): TextStyle {
|
||||||
return ElementTheme.typography.fontBodyLgRegular.copy(
|
return ElementTheme.typography.fontBodyLgRegular.copy(
|
||||||
|
|
@ -283,11 +304,11 @@ private fun textFieldStyle(enabled: Boolean): TextStyle {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(group = PreviewGroup.TextFields)
|
@Preview(group = PreviewGroup.TextFields, heightDp = 1000)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TextFieldsLightPreview() = ElementPreviewLight { ContentToPreview() }
|
internal fun TextFieldsLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||||
|
|
||||||
@Preview(group = PreviewGroup.TextFields)
|
@Preview(group = PreviewGroup.TextFields, heightDp = 1000)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TextFieldsDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
internal fun TextFieldsDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||||
|
|
||||||
|
|
@ -295,15 +316,15 @@ internal fun TextFieldsDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||||
@ExcludeFromCoverage
|
@ExcludeFromCoverage
|
||||||
private fun ContentToPreview() {
|
private fun ContentToPreview() {
|
||||||
Column(modifier = Modifier.padding(4.dp)) {
|
Column(modifier = Modifier.padding(4.dp)) {
|
||||||
allBooleans.forEach { isError ->
|
TextFieldValidity.entries.forEach { validity ->
|
||||||
allBooleans.forEach { enabled ->
|
allBooleans.forEach { enabled ->
|
||||||
allBooleans.forEach { readonly ->
|
allBooleans.forEach { readonly ->
|
||||||
TextField(
|
TextField(
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
label = "Label",
|
label = "Label",
|
||||||
value = "Hello er=${isError.asInt()}, en=${enabled.asInt()}, ro=${readonly.asInt()}",
|
value = "Hello val=$validity, en=${enabled.asInt()}, ro=${readonly.asInt()}",
|
||||||
supportingText = "Supporting text",
|
supportingText = "Supporting text",
|
||||||
isError = isError,
|
validity = validity,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
readOnly = readonly,
|
readOnly = readonly,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextField
|
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextFieldValidity
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
@ -56,7 +57,10 @@ fun RoomAddressField(
|
||||||
}
|
}
|
||||||
else -> supportingText
|
else -> supportingText
|
||||||
},
|
},
|
||||||
isError = addressValidity.isError(),
|
validity = when (addressValidity) {
|
||||||
|
RoomAddressValidity.InvalidSymbols, RoomAddressValidity.NotAvailable -> TextFieldValidity.Invalid
|
||||||
|
else -> TextFieldValidity.None
|
||||||
|
},
|
||||||
onValueChange = onAddressChange,
|
onValueChange = onAddressChange,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,4 @@ sealed interface RoomAddressValidity {
|
||||||
data object InvalidSymbols : RoomAddressValidity
|
data object InvalidSymbols : RoomAddressValidity
|
||||||
data object NotAvailable : RoomAddressValidity
|
data object NotAvailable : RoomAddressValidity
|
||||||
data object Valid : RoomAddressValidity
|
data object Valid : RoomAddressValidity
|
||||||
|
|
||||||
fun isError(): Boolean {
|
|
||||||
return this is InvalidSymbols || this is NotAvailable
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ddac666d01929c8df834cff1e932eba12dbf377c7954cd4fdf2fa13bbb1e77be
|
||||||
|
size 16758
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ae779c152a099185e14a4301ff47825cfd809a81e651e36cb3261fb3371f61e7
|
||||||
|
size 17424
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c3c89ad3af16b43b6798c30077651b7c9eefd0059d847f5ff643877a2d3d3403
|
||||||
|
size 17025
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f39dc9170e9e85d1f56b8b7f1b80fbbb7d5acaa762a585a6c52b494c8b25b285
|
||||||
|
size 20884
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:30c89188415418f6015349fe9371792052f8c171272ce7ccb45352e79b15774f
|
||||||
|
size 20192
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a67b688327c3507e0072fad948b62c19acfb454aa05ac42832ee1a980c725492
|
||||||
|
size 20174
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f4285614e42cec849331dfac0a8d67786cc95f768815d5818a1c06fb97f14530
|
||||||
|
size 15599
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9610111f2ad914faf1f890faa1b593ccf04ec42248f04586f403a0f8bfbd5991
|
||||||
|
size 16196
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9be8c0f212d2ba8d1e4746038280114f50367f078e5ef63ae4e5faa730552630
|
||||||
|
size 15629
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ca43cfd340541ee672c5b79727f4033fa21a4e5351dbef55cf48999946d3a2c6
|
||||||
|
size 19474
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e04c7204d560370bcd2ed9bf775a892b65f22bd395f7079375e963cd46cce8ec
|
||||||
|
size 18710
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d9824bb57bbe04b48d5a2750a56a9cd36cd32bf4a4a2ebf9dd19c213e1cfb9d6
|
||||||
|
size 18565
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c6c1aed2fb05e5220d335bc0ffc9a6851e9207efd2dcabb2ce2100e61516bc57
|
oid sha256:f19636ec23d81de10c454b3a0f4c957b999b0fd706363854eb81b62d3072ebcd
|
||||||
size 21409
|
size 25781
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:35e14513f620f9060ed5b7d8db55175f96d57b45f4187b4f9e2be4bb56016dcd
|
oid sha256:e86339de00f7e6d44c1d36004c79a9c077d7d99d55ba01da8ea9e4d3b4567367
|
||||||
size 48218
|
size 53085
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d2d669d6ce18e6cf641b85f7edf1870b96f3289d55dd89779e1c6388147b4055
|
oid sha256:c9f8d6270b9db5463649ce357463cec93945d924bebaeb7da14bfa2936088620
|
||||||
size 20409
|
size 24735
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7976aeaf533239d0bda8204e305d1ef6b977b0f7f8755025c36fe76b1a36e8c4
|
oid sha256:baa7c2ce997853e150eeb0193798af8d2ec1084b1791da579b2de6544e07d06f
|
||||||
size 48097
|
size 52657
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:09b8dcff40d9231cbd5b81db4836081046dba2bab37d9b4e7bae49abf7debd22
|
oid sha256:5b120ebcbb9dae050b83d6d769918e2cb2a0088ea9670df9d15adc967ca69bbd
|
||||||
size 82933
|
size 82399
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:edb52d5fc252ee719f6aec12faf98b8976eb9a61992dba510c06386999975ed2
|
oid sha256:aba3ee6d75f3b8671e4249e98386a689f59a8635bc43693c8333ea794b368726
|
||||||
size 105988
|
size 105477
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:09b8dcff40d9231cbd5b81db4836081046dba2bab37d9b4e7bae49abf7debd22
|
oid sha256:5b120ebcbb9dae050b83d6d769918e2cb2a0088ea9670df9d15adc967ca69bbd
|
||||||
size 82933
|
size 82399
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e398afce8d17e333d7f33ed134dbc1de6651e525f72dff0980827df2609b5273
|
oid sha256:fff21c904f2fdfb25a39a931fc53909c35555247128959bd4cdd0c839b70c105
|
||||||
size 83578
|
size 83039
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:85925c39a59d3ceca4da5234084a3218a33670a9e3fbd3121ef601e06fb93297
|
oid sha256:58178368a0a5de71b236c9f0f82457f0cc1a94ae0f828ba72c0851bbce0a29e9
|
||||||
size 100367
|
size 99858
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0762f1f181107fad31c233bb1013cf82c6de2b94500e592609fdcc43e53a5807
|
oid sha256:9988051950123717fc5e2e6b6db2be0b905c6d47e81a485a20cd23daab9fbba1
|
||||||
size 46550
|
size 46019
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3fef8280fdce4608a30e24063699ab99bcb55865b2df51b070e2dc0eafce23a6
|
oid sha256:f65131302fab3b0ba710528947376b57230a74731aa6549473998c30f4e07f6e
|
||||||
size 41271
|
size 40681
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:dc64620fffb86bb5bcbb175795ebde2b84c18e6989f99528046b1ab14f3c9005
|
oid sha256:56ec28811fc81c49f1f3fa5c9125d668b0401ee4645feb33508ba32a33a6bf55
|
||||||
size 89396
|
size 88850
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1ee1b7d094db4cd00af34d1ef79c6e71ed61cec585612c538a0bc870c64b9966
|
oid sha256:3125f559a0531c2b3b63957a7ca7cff2a718c5a2e00f5213e38e6e65b005f942
|
||||||
size 111814
|
size 111313
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:dc64620fffb86bb5bcbb175795ebde2b84c18e6989f99528046b1ab14f3c9005
|
oid sha256:56ec28811fc81c49f1f3fa5c9125d668b0401ee4645feb33508ba32a33a6bf55
|
||||||
size 89396
|
size 88850
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:28ba0acc4ef26efdb4bc2f18c1643a82a180cfa429302da9df3ebce73f3f6552
|
oid sha256:961ea9cb682807f6a423cb3fa69f3d7a8838339fdb33de8de8baf55ac22ec239
|
||||||
size 89287
|
size 88758
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:49097acd61396b5190aa1820c61d826b04738dd56acb4cdfb5df5b04c486e961
|
oid sha256:3cdebb7664e5a7aefbdc35f8d442cc39a07f5debc3fc9ceb7be384eefdf3da33
|
||||||
size 106227
|
size 105760
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2e339f50db6e8b06bdf5a26559827f92f5a9fd2f20bce4a45d4edf5d45e071b9
|
oid sha256:2264d2aa03b36178a6ffa315a1147dba5cd6a698ae2fb4fe0942857964de2b47
|
||||||
size 53790
|
size 53250
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9496a4dd417cb64c20a1d53fb27f9f567f0eefc8c0ad428a9c6c4041f66eddb0
|
oid sha256:1c6dc3c0c72d1ea8a980f393d7cfe2c9e0392016c8fc25b901817dd077d8526c
|
||||||
size 47845
|
size 47265
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:165bbcda11115c490250a86ac45d84277362a8c0fc0931473f1dc729b5eaa70d
|
oid sha256:0eb5d9fc0da36b69863c9ab9ebac6ef2cfe5d57c0f161be2ebc6247229819d17
|
||||||
size 42804
|
size 43209
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:86b121d1b8c7ea4b2401ca6ea36897b630f41b5fa9961c5bcb8746a142b8c26f
|
oid sha256:514cc5ef29ef732a2b03010cf05a79607498919fc0c9d8afedc8713b77b24ae0
|
||||||
size 44373
|
size 44877
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,8 @@
|
||||||
"includeRegex" : [
|
"includeRegex" : [
|
||||||
"screen_create_room_.*",
|
"screen_create_room_.*",
|
||||||
"screen\\.create_room\\..*",
|
"screen\\.create_room\\..*",
|
||||||
"screen_start_chat_.*"
|
"screen_start_chat_.*",
|
||||||
|
"screen\\.start_chat\\..*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue