Skip to content

Commit 498f63e

Browse files
committed
feat(join by alias) : introduce the JoinRoomByAddress
1 parent 2bfa629 commit 498f63e

File tree

10 files changed

+355
-1
lines changed

10 files changed

+355
-1
lines changed

features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/CreateRoomNavigator.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
1919
interface CreateRoomNavigator : Plugin {
2020
fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias)
2121
fun onCreateNewRoom()
22+
fun onShowJoinRoomByAddress()
23+
fun onDismissJoinRoomByAddress()
2224
}
2325

2426
class DefaultCreateRoomNavigator(
2527
private val backstack: BackStack<NavTarget>,
28+
private val overlay: Overlay<NavTarget>,
2629
private val openRoom: (RoomIdOrAlias) -> Unit,
2730
) : CreateRoomNavigator {
2831

@@ -32,4 +35,11 @@ class DefaultCreateRoomNavigator(
3235
backstack.push(NavTarget.NewRoom)
3336
}
3437

38+
override fun onShowJoinRoomByAddress() {
39+
overlay.show(NavTarget.JoinByAddress)
40+
}
41+
42+
override fun onDismissJoinRoomByAddress() {
43+
overlay.hide()
44+
}
3545
}

features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package io.element.android.features.createroom.impl
99

1010
import android.os.Parcelable
11+
import androidx.compose.foundation.layout.Box
1112
import androidx.compose.runtime.Composable
1213
import androidx.compose.runtime.remember
1314
import androidx.compose.ui.Modifier
@@ -22,9 +23,11 @@ import dagger.assisted.AssistedInject
2223
import io.element.android.anvilannotations.ContributesNode
2324
import io.element.android.features.createroom.DefaultCreateRoomNavigator
2425
import io.element.android.features.createroom.api.CreateRoomEntryPoint
26+
import io.element.android.features.createroom.impl.joinbyaddress.JoinRoomByAddressNode
2527
import io.element.android.features.createroom.impl.root.CreateRoomRootNode
2628
import io.element.android.libraries.architecture.BackstackView
2729
import io.element.android.libraries.architecture.BaseFlowNode
30+
import io.element.android.libraries.architecture.OverlayView
2831
import io.element.android.libraries.architecture.createNode
2932
import io.element.android.libraries.di.SessionScope
3033
import io.element.android.libraries.matrix.api.core.RoomId
@@ -50,10 +53,13 @@ class CreateRoomFlowNode @AssistedInject constructor(
5053
@Parcelize
5154
data object NewRoom : NavTarget
5255

56+
@Parcelize
57+
data object JoinByAddress : NavTarget
5358
}
5459

5560
private val navigator = DefaultCreateRoomNavigator(
5661
backstack = backstack,
62+
overlay = overlay,
5763
openRoom = { roomIdOrAlias ->
5864
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onOpenRoom(roomIdOrAlias) }
5965
}
@@ -67,11 +73,17 @@ class CreateRoomFlowNode @AssistedInject constructor(
6773
NavTarget.NewRoom -> {
6874
createNode<ConfigureRoomFlowNode>(buildContext = buildContext, plugins = listOf(navigator))
6975
}
76+
NavTarget.JoinByAddress -> {
77+
createNode<JoinRoomByAddressNode>(buildContext = buildContext, plugins = listOf(navigator))
78+
}
7079
}
7180
}
7281

7382
@Composable
7483
override fun View(modifier: Modifier) {
75-
BackstackView()
84+
Box(modifier = modifier) {
85+
BackstackView()
86+
OverlayView(transitionHandler = remember { JumpToEndTransitionHandler() })
87+
}
7688
}
7789
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.createroom.impl.joinbyaddress
9+
10+
sealed interface JoinRoomByAddressEvents {
11+
data object Dismiss : JoinRoomByAddressEvents
12+
data object Continue: JoinRoomByAddressEvents
13+
data class UpdateAddress(val address: String) : JoinRoomByAddressEvents
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.createroom.impl.joinbyaddress
9+
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.ui.Modifier
12+
import com.bumble.appyx.core.modality.BuildContext
13+
import com.bumble.appyx.core.node.Node
14+
import com.bumble.appyx.core.plugin.Plugin
15+
import com.bumble.appyx.core.plugin.plugins
16+
import dagger.assisted.Assisted
17+
import dagger.assisted.AssistedInject
18+
import io.element.android.anvilannotations.ContributesNode
19+
import io.element.android.features.createroom.CreateRoomNavigator
20+
import io.element.android.libraries.di.SessionScope
21+
22+
@ContributesNode(SessionScope::class)
23+
class JoinRoomByAddressNode @AssistedInject constructor(
24+
@Assisted buildContext: BuildContext,
25+
@Assisted plugins: List<Plugin>,
26+
presenterFactory: JoinRoomByAddressPresenter.Factory,
27+
) : Node(buildContext, plugins = plugins) {
28+
29+
private val navigator = plugins<CreateRoomNavigator>().first()
30+
private val presenter = presenterFactory.create(navigator)
31+
32+
@Composable
33+
override fun View(modifier: Modifier) {
34+
val state = presenter.present()
35+
JoinRoomByAddressView(
36+
state = state,
37+
modifier = modifier
38+
)
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.createroom.impl.joinbyaddress
9+
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.runtime.LaunchedEffect
12+
import androidx.compose.runtime.getValue
13+
import androidx.compose.runtime.mutableStateOf
14+
import androidx.compose.runtime.remember
15+
import androidx.compose.runtime.rememberUpdatedState
16+
import androidx.compose.runtime.setValue
17+
import dagger.assisted.Assisted
18+
import dagger.assisted.AssistedFactory
19+
import dagger.assisted.AssistedInject
20+
import io.element.android.features.createroom.CreateRoomNavigator
21+
import io.element.android.libraries.architecture.Presenter
22+
import io.element.android.libraries.core.data.tryOrNull
23+
import io.element.android.libraries.matrix.api.MatrixClient
24+
import io.element.android.libraries.matrix.api.core.RoomAlias
25+
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
26+
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
27+
import kotlinx.coroutines.delay
28+
29+
class JoinRoomByAddressPresenter @AssistedInject constructor(
30+
@Assisted private val navigator: CreateRoomNavigator,
31+
private val client: MatrixClient,
32+
private val roomAliasHelper: RoomAliasHelper,
33+
) : Presenter<JoinRoomByAddressState> {
34+
35+
@AssistedFactory
36+
interface Factory {
37+
fun create(navigator: CreateRoomNavigator): JoinRoomByAddressPresenter
38+
}
39+
40+
@Composable
41+
override fun present(): JoinRoomByAddressState {
42+
var address by remember { mutableStateOf("") }
43+
var addressState by remember { mutableStateOf<RoomAddressState>(RoomAddressState.Unknown) }
44+
45+
fun handleEvents(event: JoinRoomByAddressEvents) {
46+
when (event) {
47+
JoinRoomByAddressEvents.Continue -> {
48+
navigator.onDismissJoinRoomByAddress()
49+
navigator.onOpenRoom(RoomIdOrAlias.Alias(RoomAlias(address)))
50+
}
51+
JoinRoomByAddressEvents.Dismiss -> navigator.onDismissJoinRoomByAddress()
52+
is JoinRoomByAddressEvents.UpdateAddress -> {
53+
address = event.address.trim()
54+
}
55+
}
56+
}
57+
58+
RoomAddressStateEffect(
59+
fullAddress = address,
60+
onRoomAddressStateChange = { addressState = it }
61+
)
62+
63+
return JoinRoomByAddressState(
64+
address = address,
65+
addressState = addressState,
66+
eventSink = ::handleEvents
67+
)
68+
}
69+
70+
@Composable
71+
private fun RoomAddressStateEffect(
72+
fullAddress: String,
73+
onRoomAddressStateChange: (RoomAddressState) -> Unit,
74+
) {
75+
val onChange by rememberUpdatedState(onRoomAddressStateChange)
76+
LaunchedEffect(fullAddress) {
77+
if (fullAddress.isEmpty()) {
78+
onChange(RoomAddressState.Unknown)
79+
return@LaunchedEffect
80+
}
81+
// debounce the room address validation
82+
delay(300)
83+
val roomAlias = tryOrNull { RoomAlias(fullAddress) }
84+
if (roomAlias == null || !roomAliasHelper.isRoomAliasValid(roomAlias)) {
85+
onChange(RoomAddressState.Invalid)
86+
} else {
87+
onChange(RoomAddressState.Valid(matchingRoomFound = false))
88+
client.resolveRoomAlias(roomAlias)
89+
.onSuccess { resolved ->
90+
onChange(RoomAddressState.Valid(matchingRoomFound = resolved.isPresent))
91+
}
92+
}
93+
}
94+
}
95+
}
96+
97+
98+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.createroom.impl.joinbyaddress
9+
10+
import androidx.compose.runtime.Immutable
11+
12+
data class JoinRoomByAddressState(
13+
val address: String,
14+
val addressState: RoomAddressState,
15+
val eventSink: (JoinRoomByAddressEvents) -> Unit
16+
)
17+
18+
@Immutable
19+
sealed interface RoomAddressState {
20+
data object Unknown : RoomAddressState
21+
data object Invalid : RoomAddressState
22+
data class Valid(val matchingRoomFound: Boolean) : RoomAddressState
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.createroom.impl.joinbyaddress
9+
10+
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
11+
12+
open class JoinRoomByAddressStateProvider : PreviewParameterProvider<JoinRoomByAddressState> {
13+
override val values: Sequence<JoinRoomByAddressState>
14+
get() = sequenceOf(
15+
aJoinRoomByAddressState(),
16+
aJoinRoomByAddressState("#room-"),
17+
aJoinRoomByAddressState("#room-", addressState = RoomAddressState.Invalid),
18+
aJoinRoomByAddressState("#room-name:matrix.org", addressState = RoomAddressState.Valid(true)),
19+
aJoinRoomByAddressState("#room-name-here:matrix.org", addressState = RoomAddressState.Valid(false)),
20+
// Add other states here
21+
)
22+
}
23+
24+
fun aJoinRoomByAddressState(
25+
address: String = "",
26+
addressState: RoomAddressState = RoomAddressState.Unknown,
27+
) = JoinRoomByAddressState(
28+
address = address,
29+
addressState = addressState,
30+
eventSink = {}
31+
)

0 commit comments

Comments
 (0)