Merge pull request #4302 from element-hq/feature/fga/join_room_by_alias
Feature : join room by address
This commit is contained in:
commit
efb5accd57
58 changed files with 880 additions and 122 deletions
|
|
@ -357,8 +357,8 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
}
|
||||
NavTarget.CreateRoom -> {
|
||||
val callback = object : CreateRoomEntryPoint.Callback {
|
||||
override fun onSuccess(roomId: RoomId) {
|
||||
backstack.replace(NavTarget.Room(roomId.toRoomIdOrAlias()))
|
||||
override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>) {
|
||||
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.plugin.Plugin
|
||||
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 {
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
|
@ -21,6 +21,6 @@ interface CreateRoomEntryPoint : FeatureEntryPoint {
|
|||
}
|
||||
|
||||
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.AssistedInject
|
||||
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.configureroom.ConfigureRoomNode
|
||||
import io.element.android.features.createroom.impl.di.CreateRoomComponent
|
||||
|
|
@ -46,6 +47,7 @@ class ConfigureRoomFlowNode @AssistedInject constructor(
|
|||
private val component by lazy {
|
||||
parent!!.bindings<CreateRoomComponent.ParentBindings>().createRoomComponentBuilder().build()
|
||||
}
|
||||
private val navigator = plugins<CreateRoomNavigator>().first()
|
||||
|
||||
override val daggerComponent: Any
|
||||
get() = component
|
||||
|
|
@ -69,8 +71,7 @@ class ConfigureRoomFlowNode @AssistedInject constructor(
|
|||
createNode<AddPeopleNode>(buildContext = buildContext, plugins = listOf(callback))
|
||||
}
|
||||
NavTarget.ConfigureRoom -> {
|
||||
val callbacks = plugins<ConfigureRoomNode.Callback>()
|
||||
createNode<ConfigureRoomNode>(buildContext = buildContext, plugins = callbacks)
|
||||
createNode<ConfigureRoomNode>(buildContext = buildContext, plugins = listOf(navigator))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,25 +8,28 @@
|
|||
package io.element.android.features.createroom.impl
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
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.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
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.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.libraries.architecture.BackstackView
|
||||
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.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
|
|
@ -47,35 +50,38 @@ class CreateRoomFlowNode @AssistedInject constructor(
|
|||
|
||||
@Parcelize
|
||||
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 {
|
||||
return when (navTarget) {
|
||||
NavTarget.Root -> {
|
||||
val callback = object : CreateRoomRootNode.Callback {
|
||||
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))
|
||||
createNode<CreateRoomRootNode>(buildContext = buildContext, plugins = listOf(navigator))
|
||||
}
|
||||
NavTarget.NewRoom -> {
|
||||
val callback = object : ConfigureRoomNode.Callback {
|
||||
override fun onCreateRoomSuccess(roomId: RoomId) {
|
||||
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onSuccess(roomId) }
|
||||
}
|
||||
}
|
||||
createNode<ConfigureRoomFlowNode>(buildContext = buildContext, plugins = listOf(callback))
|
||||
createNode<ConfigureRoomFlowNode>(buildContext = buildContext, plugins = listOf(navigator))
|
||||
}
|
||||
NavTarget.JoinByAddress -> {
|
||||
createNode<JoinRoomByAddressNode>(buildContext = buildContext, plugins = listOf(navigator))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
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 im.vector.app.features.analytics.plan.MobileScreen
|
||||
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.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
||||
@ContributesNode(CreateRoomScope::class)
|
||||
|
|
@ -29,6 +30,8 @@ class ConfigureRoomNode @AssistedInject constructor(
|
|||
private val presenter: ConfigureRoomPresenter,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private val navigator = plugins<CreateRoomNavigator>().first()
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
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
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
|
|
@ -52,7 +47,9 @@ class ConfigureRoomNode @AssistedInject constructor(
|
|||
state = state,
|
||||
modifier = modifier,
|
||||
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 im.vector.app.features.analytics.plan.MobileScreen
|
||||
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.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
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
|
|
@ -33,18 +34,7 @@ class CreateRoomRootNode @AssistedInject constructor(
|
|||
private val analyticsService: AnalyticsService,
|
||||
private val inviteFriendsUseCase: InviteFriendsUseCase,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
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) }
|
||||
}
|
||||
private val navigator = plugins<CreateRoomNavigator>().first()
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
|
|
@ -60,8 +50,11 @@ class CreateRoomRootNode @AssistedInject constructor(
|
|||
state = state,
|
||||
modifier = modifier,
|
||||
onCloseClick = this::navigateUp,
|
||||
onNewRoomClick = ::onCreateNewRoom,
|
||||
onOpenDM = ::onStartChatSuccess,
|
||||
onNewRoomClick = navigator::onCreateNewRoom,
|
||||
onOpenDM = {
|
||||
navigator.onOpenRoom(roomIdOrAlias = it.toRoomIdOrAlias(), serverNames = emptyList())
|
||||
},
|
||||
onJoinByAddressClick = navigator::onShowJoinRoomByAddress,
|
||||
onInviteFriendsClick = { invitePeople(activity) }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ fun CreateRoomRootView(
|
|||
onNewRoomClick: () -> Unit,
|
||||
onOpenDM: (RoomId) -> Unit,
|
||||
onInviteFriendsClick: () -> Unit,
|
||||
onJoinByAddressClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
|
|
@ -89,6 +90,7 @@ fun CreateRoomRootView(
|
|||
state = state,
|
||||
onNewRoomClick = onNewRoomClick,
|
||||
onInvitePeopleClick = onInviteFriendsClick,
|
||||
onJoinByAddressClick = onJoinByAddressClick,
|
||||
onDmClick = onOpenDM,
|
||||
)
|
||||
}
|
||||
|
|
@ -153,6 +155,7 @@ private fun CreateRoomActionButtonsList(
|
|||
state: CreateRoomRootState,
|
||||
onNewRoomClick: () -> Unit,
|
||||
onInvitePeopleClick: () -> Unit,
|
||||
onJoinByAddressClick: () -> Unit,
|
||||
onDmClick: (RoomId) -> Unit,
|
||||
) {
|
||||
LazyColumn {
|
||||
|
|
@ -170,6 +173,13 @@ private fun CreateRoomActionButtonsList(
|
|||
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()) {
|
||||
item {
|
||||
ListSectionHeader(
|
||||
|
|
@ -230,6 +240,7 @@ internal fun CreateRoomRootViewPreview(@PreviewParameter(CreateRoomRootStateProv
|
|||
onCloseClick = {},
|
||||
onNewRoomClick = {},
|
||||
onOpenDM = {},
|
||||
onJoinByAddressClick = {},
|
||||
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_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_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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@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(
|
||||
|
|
@ -109,6 +124,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCreat
|
|||
onNewRoomClick: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenDM: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onInviteFriendsClick: () -> Unit = EnsureNeverCalled(),
|
||||
onJoinRoomByAddressClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
CreateRoomRootView(
|
||||
|
|
@ -117,6 +133,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCreat
|
|||
onNewRoomClick = onNewRoomClick,
|
||||
onOpenDM = onOpenDM,
|
||||
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.theme.components.Button
|
||||
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
|
||||
|
||||
@Composable
|
||||
|
|
@ -90,7 +91,7 @@ fun BugReportView(
|
|||
keyboardController?.hide()
|
||||
}),
|
||||
minLines = 3,
|
||||
isError = state.isDescriptionInError,
|
||||
validity = if (state.isDescriptionInError) TextFieldValidity.Invalid else TextFieldValidity.None,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
|
|
|||
|
|
@ -152,8 +152,7 @@ private fun RoomListScaffold(
|
|||
onClick = onCreateRoomClick
|
||||
) {
|
||||
Icon(
|
||||
// Note cannot use Icons.Outlined.EditSquare, it does not exist :/
|
||||
imageVector = CompoundIcons.Compose(),
|
||||
imageVector = CompoundIcons.Plus(),
|
||||
contentDescription = stringResource(id = R.string.screen_roomlist_a11y_create_message),
|
||||
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.Icon
|
||||
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
|
||||
|
||||
@Composable
|
||||
|
|
@ -99,7 +100,7 @@ private fun Content(text: String, onTextChange: (String) -> Unit, hasError: Bool
|
|||
Icon(imageVector = image, description)
|
||||
}
|
||||
},
|
||||
isError = hasError,
|
||||
validity = if (hasError) TextFieldValidity.Invalid else TextFieldValidity.None,
|
||||
supportingText = if (hasError) {
|
||||
stringResource(R.string.screen_reset_encryption_password_error)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ fun TextField(
|
|||
placeholder: String? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
isError: Boolean = false,
|
||||
validity: TextFieldValidity = TextFieldValidity.None,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
singleLine: Boolean = false,
|
||||
|
|
@ -93,7 +93,7 @@ fun TextField(
|
|||
readOnly = readOnly,
|
||||
enabled = enabled,
|
||||
isFocused = isFocused,
|
||||
isError = isError,
|
||||
validity = validity,
|
||||
leadingIcon = leadingIcon,
|
||||
placeholder = placeholder,
|
||||
isTextEmpty = value.isEmpty(),
|
||||
|
|
@ -114,7 +114,7 @@ fun TextField(
|
|||
placeholder: String? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
isError: Boolean = false,
|
||||
validity: TextFieldValidity? = null,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
singleLine: Boolean = false,
|
||||
|
|
@ -149,7 +149,7 @@ fun TextField(
|
|||
readOnly = readOnly,
|
||||
enabled = enabled,
|
||||
isFocused = isFocused,
|
||||
isError = isError,
|
||||
validity = validity,
|
||||
leadingIcon = leadingIcon,
|
||||
placeholder = placeholder,
|
||||
isTextEmpty = value.text.isEmpty(),
|
||||
|
|
@ -166,7 +166,7 @@ private fun DecorationBox(
|
|||
enabled: Boolean,
|
||||
readOnly: Boolean,
|
||||
isFocused: Boolean,
|
||||
isError: Boolean,
|
||||
validity: TextFieldValidity?,
|
||||
placeholder: String?,
|
||||
isTextEmpty: Boolean,
|
||||
supportingText: String?,
|
||||
|
|
@ -187,7 +187,7 @@ private fun DecorationBox(
|
|||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
isFocused = isFocused,
|
||||
isError = isError
|
||||
isError = validity == TextFieldValidity.Invalid
|
||||
) {
|
||||
Row(modifier = Modifier.padding(16.dp)) {
|
||||
if (leadingIcon != null) {
|
||||
|
|
@ -216,7 +216,7 @@ private fun DecorationBox(
|
|||
}
|
||||
if (supportingText != null) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
SupportingTextLayout(isError, supportingText)
|
||||
SupportingTextLayout(validity, supportingText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -254,24 +254,45 @@ private fun TextFieldContainer(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun SupportingTextLayout(isError: Boolean, supportingText: String) {
|
||||
private fun SupportingTextLayout(validity: TextFieldValidity?, supportingText: String) {
|
||||
Row(horizontalArrangement = spacedBy(4.dp)) {
|
||||
if (isError) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Error(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = ElementTheme.colors.iconCriticalPrimary
|
||||
)
|
||||
when (validity) {
|
||||
TextFieldValidity.Invalid -> {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Error(),
|
||||
contentDescription = null,
|
||||
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 = 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class TextFieldValidity {
|
||||
None,
|
||||
Invalid,
|
||||
Valid
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun textFieldStyle(enabled: Boolean): TextStyle {
|
||||
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
|
||||
internal fun TextFieldsLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview(group = PreviewGroup.TextFields)
|
||||
@Preview(group = PreviewGroup.TextFields, heightDp = 1000)
|
||||
@Composable
|
||||
internal fun TextFieldsDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
|
|
@ -295,15 +316,15 @@ internal fun TextFieldsDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
|||
@ExcludeFromCoverage
|
||||
private fun ContentToPreview() {
|
||||
Column(modifier = Modifier.padding(4.dp)) {
|
||||
allBooleans.forEach { isError ->
|
||||
TextFieldValidity.entries.forEach { validity ->
|
||||
allBooleans.forEach { enabled ->
|
||||
allBooleans.forEach { readonly ->
|
||||
TextField(
|
||||
onValueChange = {},
|
||||
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",
|
||||
isError = isError,
|
||||
validity = validity,
|
||||
enabled = enabled,
|
||||
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.theme.components.Text
|
||||
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.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
|
@ -56,7 +57,10 @@ fun RoomAddressField(
|
|||
}
|
||||
else -> supportingText
|
||||
},
|
||||
isError = addressValidity.isError(),
|
||||
validity = when (addressValidity) {
|
||||
RoomAddressValidity.InvalidSymbols, RoomAddressValidity.NotAvailable -> TextFieldValidity.Invalid
|
||||
else -> TextFieldValidity.None
|
||||
},
|
||||
onValueChange = onAddressChange,
|
||||
singleLine = true,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,8 +19,4 @@ sealed interface RoomAddressValidity {
|
|||
data object InvalidSymbols : RoomAddressValidity
|
||||
data object NotAvailable : 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
|
||||
oid sha256:c6c1aed2fb05e5220d335bc0ffc9a6851e9207efd2dcabb2ce2100e61516bc57
|
||||
size 21409
|
||||
oid sha256:f19636ec23d81de10c454b3a0f4c957b999b0fd706363854eb81b62d3072ebcd
|
||||
size 25781
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:35e14513f620f9060ed5b7d8db55175f96d57b45f4187b4f9e2be4bb56016dcd
|
||||
size 48218
|
||||
oid sha256:e86339de00f7e6d44c1d36004c79a9c077d7d99d55ba01da8ea9e4d3b4567367
|
||||
size 53085
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d2d669d6ce18e6cf641b85f7edf1870b96f3289d55dd89779e1c6388147b4055
|
||||
size 20409
|
||||
oid sha256:c9f8d6270b9db5463649ce357463cec93945d924bebaeb7da14bfa2936088620
|
||||
size 24735
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7976aeaf533239d0bda8204e305d1ef6b977b0f7f8755025c36fe76b1a36e8c4
|
||||
size 48097
|
||||
oid sha256:baa7c2ce997853e150eeb0193798af8d2ec1084b1791da579b2de6544e07d06f
|
||||
size 52657
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:09b8dcff40d9231cbd5b81db4836081046dba2bab37d9b4e7bae49abf7debd22
|
||||
size 82933
|
||||
oid sha256:5b120ebcbb9dae050b83d6d769918e2cb2a0088ea9670df9d15adc967ca69bbd
|
||||
size 82399
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:edb52d5fc252ee719f6aec12faf98b8976eb9a61992dba510c06386999975ed2
|
||||
size 105988
|
||||
oid sha256:aba3ee6d75f3b8671e4249e98386a689f59a8635bc43693c8333ea794b368726
|
||||
size 105477
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:09b8dcff40d9231cbd5b81db4836081046dba2bab37d9b4e7bae49abf7debd22
|
||||
size 82933
|
||||
oid sha256:5b120ebcbb9dae050b83d6d769918e2cb2a0088ea9670df9d15adc967ca69bbd
|
||||
size 82399
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e398afce8d17e333d7f33ed134dbc1de6651e525f72dff0980827df2609b5273
|
||||
size 83578
|
||||
oid sha256:fff21c904f2fdfb25a39a931fc53909c35555247128959bd4cdd0c839b70c105
|
||||
size 83039
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:85925c39a59d3ceca4da5234084a3218a33670a9e3fbd3121ef601e06fb93297
|
||||
size 100367
|
||||
oid sha256:58178368a0a5de71b236c9f0f82457f0cc1a94ae0f828ba72c0851bbce0a29e9
|
||||
size 99858
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0762f1f181107fad31c233bb1013cf82c6de2b94500e592609fdcc43e53a5807
|
||||
size 46550
|
||||
oid sha256:9988051950123717fc5e2e6b6db2be0b905c6d47e81a485a20cd23daab9fbba1
|
||||
size 46019
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3fef8280fdce4608a30e24063699ab99bcb55865b2df51b070e2dc0eafce23a6
|
||||
size 41271
|
||||
oid sha256:f65131302fab3b0ba710528947376b57230a74731aa6549473998c30f4e07f6e
|
||||
size 40681
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dc64620fffb86bb5bcbb175795ebde2b84c18e6989f99528046b1ab14f3c9005
|
||||
size 89396
|
||||
oid sha256:56ec28811fc81c49f1f3fa5c9125d668b0401ee4645feb33508ba32a33a6bf55
|
||||
size 88850
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1ee1b7d094db4cd00af34d1ef79c6e71ed61cec585612c538a0bc870c64b9966
|
||||
size 111814
|
||||
oid sha256:3125f559a0531c2b3b63957a7ca7cff2a718c5a2e00f5213e38e6e65b005f942
|
||||
size 111313
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dc64620fffb86bb5bcbb175795ebde2b84c18e6989f99528046b1ab14f3c9005
|
||||
size 89396
|
||||
oid sha256:56ec28811fc81c49f1f3fa5c9125d668b0401ee4645feb33508ba32a33a6bf55
|
||||
size 88850
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:28ba0acc4ef26efdb4bc2f18c1643a82a180cfa429302da9df3ebce73f3f6552
|
||||
size 89287
|
||||
oid sha256:961ea9cb682807f6a423cb3fa69f3d7a8838339fdb33de8de8baf55ac22ec239
|
||||
size 88758
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:49097acd61396b5190aa1820c61d826b04738dd56acb4cdfb5df5b04c486e961
|
||||
size 106227
|
||||
oid sha256:3cdebb7664e5a7aefbdc35f8d442cc39a07f5debc3fc9ceb7be384eefdf3da33
|
||||
size 105760
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2e339f50db6e8b06bdf5a26559827f92f5a9fd2f20bce4a45d4edf5d45e071b9
|
||||
size 53790
|
||||
oid sha256:2264d2aa03b36178a6ffa315a1147dba5cd6a698ae2fb4fe0942857964de2b47
|
||||
size 53250
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9496a4dd417cb64c20a1d53fb27f9f567f0eefc8c0ad428a9c6c4041f66eddb0
|
||||
size 47845
|
||||
oid sha256:1c6dc3c0c72d1ea8a980f393d7cfe2c9e0392016c8fc25b901817dd077d8526c
|
||||
size 47265
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:165bbcda11115c490250a86ac45d84277362a8c0fc0931473f1dc729b5eaa70d
|
||||
size 42804
|
||||
oid sha256:0eb5d9fc0da36b69863c9ab9ebac6ef2cfe5d57c0f161be2ebc6247229819d17
|
||||
size 43209
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:86b121d1b8c7ea4b2401ca6ea36897b630f41b5fa9961c5bcb8746a142b8c26f
|
||||
size 44373
|
||||
oid sha256:514cc5ef29ef732a2b03010cf05a79607498919fc0c9d8afedc8713b77b24ae0
|
||||
size 44877
|
||||
|
|
|
|||
|
|
@ -64,7 +64,8 @@
|
|||
"includeRegex" : [
|
||||
"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