diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt index 048c1826f2..a752920800 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt @@ -43,17 +43,19 @@ import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.designsystem.components.async.AsyncFailure import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.di.SessionScope 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.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.RoomMembershipObserver -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import timber.log.Timber import java.util.Optional @@ -87,43 +89,72 @@ class RoomFlowNode @AssistedInject constructor( data object Loading : NavTarget @Parcelize - data object JoinRoom : NavTarget + data class JoinRoom(val roomId: RoomId) : NavTarget + + @Parcelize + data class RoomAliasError(val roomAlias: RoomAlias) : NavTarget @Parcelize data class JoinedRoom(val roomId: RoomId) : NavTarget } - private var roomMembershipJob: Job? = null - override fun onBuilt() { super.onBuilt() + resolveRoomId() + } + + private fun resolveRoomId() { + lifecycleScope.launch { + when (val i = inputs.roomIdOrAlias) { + is RoomIdOrAlias.Alias -> { + client.resolveRoomAlias(i.roomAlias) + .onFailure { + Timber.e(it, "Failed to resolve room alias") + backstack.newRoot(NavTarget.RoomAliasError(i.roomAlias)) + } + .onSuccess { + subscribeToRoomInfoFlow(it) + } + } + is RoomIdOrAlias.Id -> { + subscribeToRoomInfoFlow(i.roomId) + } + } + } + } + + private fun subscribeToRoomInfoFlow(roomId: RoomId) { client.getRoomInfoFlow( - inputs.roomIdOrAlias + roomId ).onEach { roomInfo -> Timber.d("Room membership: ${roomInfo.map { it.currentUserMembership }}") val info = roomInfo.getOrNull() if (info?.currentUserMembership == CurrentUserMembership.JOINED) { - backstack.newRoot(NavTarget.JoinedRoom(info.id)) - // When leaving the room from this session only, navigate up. - roomMembershipJob?.cancel() - roomMembershipJob = roomMembershipObserver.updates - .filter { update -> update.roomId == info.id && !update.isUserInRoom } - .onEach { - navigateUp() - } - .launchIn(lifecycleScope) + backstack.newRoot(NavTarget.JoinedRoom(roomId)) } else { - backstack.newRoot(NavTarget.JoinRoom) + backstack.newRoot(NavTarget.JoinRoom(roomId)) } } .launchIn(lifecycleScope) + + // When leaving the room from this session only, navigate up. + roomMembershipObserver.updates + .filter { update -> update.roomId == roomId && !update.isUserInRoom } + .onEach { + navigateUp() + } + .launchIn(lifecycleScope) } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Loading -> loadingNode(buildContext) - NavTarget.JoinRoom -> { - val inputs = JoinRoomEntryPoint.Inputs(inputs.roomIdOrAlias, roomDescription = inputs.roomDescription) + is NavTarget.JoinRoom -> { + val inputs = JoinRoomEntryPoint.Inputs( + roomId = navTarget.roomId, + roomIdOrAlias = inputs.roomIdOrAlias, + roomDescription = inputs.roomDescription, + ) joinRoomEntryPoint.createNode(this, buildContext, inputs) } is NavTarget.JoinedRoom -> { @@ -131,6 +162,7 @@ class RoomFlowNode @AssistedInject constructor( val inputs = JoinedRoomFlowNode.Inputs(navTarget.roomId, initialElement = inputs.initialElement) createNode(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback) } + is NavTarget.RoomAliasError -> roomAliasErrorNode(buildContext, navTarget.roomAlias) } } @@ -140,6 +172,15 @@ class RoomFlowNode @AssistedInject constructor( } } + private fun roomAliasErrorNode(buildContext: BuildContext, roomAlias: RoomAlias) = node(buildContext) { + Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { + AsyncFailure( + throwable = Exception("Unable to resolve alias ${roomAlias.value}"), + onRetry = { resolveRoomId() }, + ) + } + } + @Composable override fun View(modifier: Modifier) { BackstackView(transitionHandler = JumpToEndTransitionHandler()) diff --git a/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt b/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt index 0986af81d3..d62a9819c9 100644 --- a/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt +++ b/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt @@ -21,6 +21,7 @@ import com.bumble.appyx.core.node.Node import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import java.util.Optional @@ -28,6 +29,7 @@ interface JoinRoomEntryPoint : FeatureEntryPoint { fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs): Node data class Inputs( + val roomId: RoomId, val roomIdOrAlias: RoomIdOrAlias, val roomDescription: Optional, ) : NodeInputs diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt index 516c789079..d83a5f4b20 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt @@ -37,7 +37,11 @@ class JoinRoomNode @AssistedInject constructor( private val acceptDeclineInviteView: AcceptDeclineInviteView, ) : Node(buildContext, plugins = plugins) { private val inputs: JoinRoomEntryPoint.Inputs = inputs() - private val presenter = presenterFactory.create(inputs.roomIdOrAlias, inputs.roomDescription) + private val presenter = presenterFactory.create( + inputs.roomId, + inputs.roomIdOrAlias, + inputs.roomDescription, + ) @Composable override fun View(modifier: Modifier) { diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt index 818857c69a..7bc0f0facf 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt @@ -30,6 +30,7 @@ import io.element.android.features.invite.api.response.InviteData import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.MatrixRoomInfo @@ -38,19 +39,24 @@ import kotlinx.coroutines.launch import java.util.Optional class JoinRoomPresenter @AssistedInject constructor( + @Assisted private val roomId: RoomId, @Assisted private val roomIdOrAlias: RoomIdOrAlias, @Assisted private val roomDescription: Optional, private val matrixClient: MatrixClient, private val acceptDeclineInvitePresenter: Presenter, ) : Presenter { interface Factory { - fun create(roomIdOrAlias: RoomIdOrAlias, roomDescription: Optional): JoinRoomPresenter + fun create( + roomId: RoomId, + roomIdOrAlias: RoomIdOrAlias, + roomDescription: Optional, + ): JoinRoomPresenter } @Composable override fun present(): JoinRoomState { val coroutineScope = rememberCoroutineScope() - val roomInfo by matrixClient.getRoomInfoFlow(roomIdOrAlias).collectAsState(initial = Optional.empty()) + val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty()) val contentState by produceState(initialValue = ContentState.Loading(roomIdOrAlias), key1 = roomInfo) { value = when { roomInfo.isPresent -> { diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt index 0bce71b885..3a9e0aec48 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt @@ -25,6 +25,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import java.util.Optional @@ -37,8 +38,13 @@ object JoinRoomModule { acceptDeclineInvitePresenter: Presenter, ): JoinRoomPresenter.Factory { return object : JoinRoomPresenter.Factory { - override fun create(roomIdOrAlias: RoomIdOrAlias, roomDescription: Optional): JoinRoomPresenter { + override fun create( + roomId: RoomId, + roomIdOrAlias: RoomIdOrAlias, + roomDescription: Optional, + ): JoinRoomPresenter { return JoinRoomPresenter( + roomId = roomId, roomIdOrAlias = roomIdOrAlias, roomDescription = roomDescription, matrixClient = client, diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt index e61dd18aad..543a8ad9d7 100644 --- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt @@ -23,7 +23,9 @@ import io.element.android.features.invite.api.response.anAcceptDeclineInviteStat import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.Presenter 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.RoomId +import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME @@ -49,7 +51,7 @@ class JoinRoomPresenterTest { val presenter = createJoinRoomPresenter() presenter.test { awaitItem().also { state -> - assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID)) + assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias())) assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown) assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState()) } @@ -245,6 +247,7 @@ class JoinRoomPresenterTest { ): JoinRoomPresenter { return JoinRoomPresenter( roomId = roomId, + roomIdOrAlias = roomId.toRoomIdOrAlias(), roomDescription = roomDescription, matrixClient = matrixClient, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter @@ -255,7 +258,7 @@ class JoinRoomPresenterTest { roomId: RoomId = A_ROOM_ID, name: String? = A_ROOM_NAME, topic: String? = "A room about something", - alias: String? = "#alias:matrix.org", + alias: RoomAlias? = RoomAlias("#alias:matrix.org"), avatarUrl: String? = null, joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN, numberOfMembers: Long = 2L diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index e6bbee4f9a..ac2a00dbdc 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -95,7 +95,7 @@ interface MatrixClient : Closeable { suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result fun roomMembershipObserver(): RoomMembershipObserver - fun getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias): Flow> + fun getRoomInfoFlow(roomId: RoomId): Flow> fun isMe(userId: UserId?) = userId == sessionId diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index adc96d0b84..48683d77ba 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -562,33 +562,18 @@ class RustMatrixClient( override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver - override fun getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias): Flow> { + override fun getRoomInfoFlow(roomId: RoomId): Flow> { return flow { - val roomId = when (roomIdOrAlias) { - is RoomIdOrAlias.Alias -> { - resolveRoomAlias(roomIdOrAlias.roomAlias) - .onFailure { - // TODO Get a way to emit an error - Timber.e("Unable to resolve room alias ${roomIdOrAlias.roomAlias}") - emit(Optional.empty()) - return@flow - } - .getOrNull() - } - is RoomIdOrAlias.Id -> roomIdOrAlias.roomId + var room = getRoom(roomId) + if (room == null) { + emit(Optional.empty()) + awaitRoom(roomId, INFINITE) + room = getRoom(roomId) } - if (roomId != null) { - var room = getRoom(roomId) - if (room == null) { - emit(Optional.empty()) - awaitRoom(roomId, INFINITE) - room = getRoom(roomId) - } - room?.use { - room.roomInfoFlow - .map { roomInfo -> Optional.of(roomInfo) } - .collect(this) - } + room?.use { + room.roomInfoFlow + .map { roomInfo -> Optional.of(roomInfo) } + .collect(this) } }.distinctUntilChanged() } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 1fc51cda94..42557db1dd 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -107,7 +107,7 @@ class FakeMatrixClient( Result.success(it) } - var getRoomInfoFlowLambda = { _: RoomIdOrAlias -> + var getRoomInfoFlowLambda = { _: RoomId -> flowOf>(Optional.empty()) } @@ -293,5 +293,5 @@ class FakeMatrixClient( return Result.success(visitedRoomsId) } - override fun getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias) = getRoomInfoFlowLambda(roomIdOrAlias) + override fun getRoomInfoFlow(roomId: RoomId) = getRoomInfoFlowLambda(roomId) }