diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 2027f2868a..8ff7ecfaa8 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -42,7 +42,8 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.loggedin.LoggedInNode import io.element.android.appnav.room.RoomFlowNode -import io.element.android.appnav.room.RoomLoadedFlowNode +import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode +import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.state.FtueService @@ -213,7 +214,7 @@ class LoggedInFlowNode @AssistedInject constructor( @Parcelize data class Room( val roomId: RoomId, - val initialElement: RoomLoadedFlowNode.NavTarget = RoomLoadedFlowNode.NavTarget.Messages + val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages ) : NavTarget @Parcelize @@ -273,7 +274,7 @@ class LoggedInFlowNode @AssistedInject constructor( } override fun onRoomSettingsClicked(roomId: RoomId) { - backstack.push(NavTarget.Room(roomId, initialElement = RoomLoadedFlowNode.NavTarget.RoomDetails)) + backstack.push(NavTarget.Room(roomId, initialElement = RoomNavigationTarget.Details)) } override fun onReportBugClicked() { @@ -290,7 +291,7 @@ class LoggedInFlowNode @AssistedInject constructor( .build() } is NavTarget.Room -> { - val callback = object : RoomLoadedFlowNode.Callback { + val callback = object : JoinedRoomLoadedFlowNode.Callback { override fun onOpenRoom(roomId: RoomId) { backstack.push(NavTarget.Room(roomId)) } @@ -317,7 +318,7 @@ class LoggedInFlowNode @AssistedInject constructor( } override fun onOpenRoomNotificationSettings(roomId: RoomId) { - backstack.push(NavTarget.Room(roomId, initialElement = RoomLoadedFlowNode.NavTarget.RoomNotificationSettings)) + backstack.push(NavTarget.Room(roomId, initialElement = RoomNavigationTarget.NotificationSettings)) } } val inputs = PreferencesEntryPoint.Params(navTarget.initialElement) @@ -349,6 +350,10 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.pop() } + override fun onInviteClicked(roomId: RoomId) { + backstack.push(NavTarget.Room(roomId)) + } + override fun onInviteAccepted(roomId: RoomId) { backstack.push(NavTarget.Room(roomId)) } 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 b45207b034..dd53d6168b 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * Copyright (c) 2024 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,15 +14,13 @@ * limitations under the License. */ -@file:OptIn(ExperimentalMaterial3Api::class) - package io.element.android.appnav.room import android.os.Parcelable -import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.modality.BuildContext @@ -36,26 +34,36 @@ import com.bumble.appyx.navmodel.backstack.operation.newRoot import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.appnav.room.joined.JoinedRoomFlowNode +import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.features.networkmonitor.api.NetworkMonitor -import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.libraries.architecture.BackstackView 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.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId -import kotlinx.coroutines.flow.distinctUntilChanged +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.roomlist.RoomListService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize +import timber.log.Timber +import kotlin.jvm.optionals.getOrNull @ContributesNode(SessionScope::class) class RoomFlowNode @AssistedInject constructor( @Assisted val buildContext: BuildContext, @Assisted plugins: List, - loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory, + private val roomListService: RoomListService, + private val roomMembershipObserver: RoomMembershipObserver, private val networkMonitor: NetworkMonitor, ) : BaseFlowNode( @@ -68,64 +76,70 @@ class RoomFlowNode @AssistedInject constructor( ) { data class Inputs( val roomId: RoomId, - val initialElement: RoomLoadedFlowNode.NavTarget = RoomLoadedFlowNode.NavTarget.Messages, + val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages, ) : NodeInputs private val inputs: Inputs = inputs() - private val loadingRoomStateStateFlow = loadingRoomStateFlowFactory.create(lifecycleScope, inputs.roomId) sealed interface NavTarget : Parcelable { @Parcelize data object Loading : NavTarget @Parcelize - data object Loaded : NavTarget + data object JoinRoom : NavTarget + + @Parcelize + data object JoinedRoom : NavTarget } override fun onBuilt() { super.onBuilt() - loadingRoomStateStateFlow - .map { - it is LoadingRoomState.Loaded - } - .distinctUntilChanged() - .onEach { isLoaded -> - if (isLoaded) { - backstack.newRoot(NavTarget.Loaded) - } else { - backstack.newRoot(NavTarget.Loading) + roomListService.getUserMembershipForRoom( + inputs.roomId + ).onEach { membership -> + Timber.d("RoomMembership = $membership") + when { + membership.getOrNull() == CurrentUserMembership.JOINED -> { + backstack.newRoot(NavTarget.JoinedRoom) } + else -> { + backstack.newRoot(NavTarget.JoinRoom) + } + } + } + .flowOn(Dispatchers.Default) + .launchIn(lifecycleScope) + + roomMembershipObserver.updates + .filter { update -> update.roomId == inputs.roomId && !update.isUserInRoom } + .onEach { + navigateUp() } .launchIn(lifecycleScope) } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.Loaded -> { - val roomFlowNodeCallback = plugins() - val awaitRoomState = loadingRoomStateStateFlow.value - if (awaitRoomState is LoadingRoomState.Loaded) { - val inputs = RoomLoadedFlowNode.Inputs(awaitRoomState.room, initialElement = inputs.initialElement) - createNode(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback) - } else { - loadingNode(buildContext, this::navigateUp) - } - } - NavTarget.Loading -> { - loadingNode(buildContext, this::navigateUp) + NavTarget.Loading -> loadingNode(buildContext) + NavTarget.JoinRoom -> joinRoomNode(buildContext) + NavTarget.JoinedRoom -> { + val roomFlowNodeCallback = plugins() + val inputs = JoinedRoomFlowNode.Inputs(inputs.roomId, initialElement = inputs.initialElement) + createNode(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback) } } } - private fun loadingNode(buildContext: BuildContext, onBackClicked: () -> Unit) = node(buildContext) { modifier -> - val loadingRoomState by loadingRoomStateStateFlow.collectAsState() - val networkStatus by networkMonitor.connectivity.collectAsState() - LoadingRoomNodeView( - state = loadingRoomState, - hasNetworkConnection = networkStatus == NetworkStatus.Online, - modifier = modifier, - onBackClicked = onBackClicked - ) + private fun loadingNode(buildContext: BuildContext) = node(buildContext) { + Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + + private fun joinRoomNode(buildContext: BuildContext) = node(buildContext) { + Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { + Text("Unknown Room") + } } @Composable diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomNavigationTarget.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomNavigationTarget.kt new file mode 100644 index 0000000000..901b2667be --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomNavigationTarget.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.appnav.room + +enum class RoomNavigationTarget { + Messages, + Details, + NotificationSettings, +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt new file mode 100644 index 0000000000..36def888ac --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package io.element.android.appnav.room.joined + +import android.os.Parcelable +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope +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.plugins +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.newRoot +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.appnav.room.RoomNavigationTarget +import io.element.android.features.networkmonitor.api.NetworkMonitor +import io.element.android.features.networkmonitor.api.NetworkStatus +import io.element.android.libraries.architecture.BackstackView +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.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.parcelize.Parcelize + +@ContributesNode(SessionScope::class) +class JoinedRoomFlowNode @AssistedInject constructor( + @Assisted val buildContext: BuildContext, + @Assisted plugins: List, + loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory, + private val networkMonitor: NetworkMonitor, +) : + BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.Loading, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins + ) { + data class Inputs( + val roomId: RoomId, + val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages, + ) : NodeInputs + + private val inputs: Inputs = inputs() + private val loadingRoomStateStateFlow = loadingRoomStateFlowFactory.create(lifecycleScope, inputs.roomId) + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Loading : NavTarget + + @Parcelize + data object Loaded : NavTarget + } + + override fun onBuilt() { + super.onBuilt() + loadingRoomStateStateFlow + .map { + it is LoadingRoomState.Loaded + } + .distinctUntilChanged() + .onEach { isLoaded -> + if (isLoaded) { + backstack.newRoot(NavTarget.Loaded) + } else { + backstack.newRoot(NavTarget.Loading) + } + } + .launchIn(lifecycleScope) + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.Loaded -> { + val roomFlowNodeCallback = plugins() + val awaitRoomState = loadingRoomStateStateFlow.value + if (awaitRoomState is LoadingRoomState.Loaded) { + val inputs = JoinedRoomLoadedFlowNode.Inputs(awaitRoomState.room, initialElement = inputs.initialElement) + createNode(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback) + } else { + loadingNode(buildContext, this::navigateUp) + } + } + NavTarget.Loading -> { + loadingNode(buildContext, this::navigateUp) + } + } + } + + private fun loadingNode(buildContext: BuildContext, onBackClicked: () -> Unit) = node(buildContext) { modifier -> + val loadingRoomState by loadingRoomStateStateFlow.collectAsState() + val networkStatus by networkMonitor.connectivity.collectAsState() + LoadingRoomNodeView( + state = loadingRoomState, + hasNetworkConnection = networkStatus == NetworkStatus.Online, + modifier = modifier, + onBackClicked = onBackClicked + ) + } + + @Composable + override fun View(modifier: Modifier) { + BackstackView( + transitionHandler = JumpToEndTransitionHandler(), + ) + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt similarity index 90% rename from appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt rename to appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt index 21e412ef63..ade1f7e147 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * Copyright (c) 2024 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.room +package io.element.android.appnav.room.joined import android.os.Parcelable import androidx.compose.runtime.Composable @@ -32,6 +32,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.di.RoomComponentFactory +import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.libraries.architecture.BackstackView @@ -46,15 +47,12 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope -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 @ContributesNode(SessionScope::class) -class RoomLoadedFlowNode @AssistedInject constructor( +class JoinedRoomLoadedFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val messagesEntryPoint: MessagesEntryPoint, @@ -63,9 +61,13 @@ class RoomLoadedFlowNode @AssistedInject constructor( private val appCoroutineScope: CoroutineScope, roomComponentFactory: RoomComponentFactory, roomMembershipObserver: RoomMembershipObserver, -) : BaseFlowNode( +) : BaseFlowNode( backstack = BackStack( - initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialElement, + initialElement = when(plugins.filterIsInstance(Inputs::class.java).first().initialElement){ + RoomNavigationTarget.Messages -> NavTarget.Messages + RoomNavigationTarget.Details -> NavTarget.RoomDetails + RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings + }, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -79,7 +81,7 @@ class RoomLoadedFlowNode @AssistedInject constructor( data class Inputs( val room: MatrixRoom, - val initialElement: NavTarget = NavTarget.Messages, + val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages, ) : NodeInputs private val inputs: Inputs = inputs() @@ -108,13 +110,6 @@ class RoomLoadedFlowNode @AssistedInject constructor( appNavigationStateService.onLeavingRoom(id) } ) - roomMembershipObserver.updates - .filter { update -> update.roomId == inputs.room.roomId && !update.isUserInRoom } - .onEach { - navigateUp() - } - .launchIn(lifecycleScope) - inputs() } private fun fetchRoomMembers() = lifecycleScope.launch { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomNodeView.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt similarity index 97% rename from appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomNodeView.kt rename to appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt index 8db8d608b2..14fb955cb5 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomNodeView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * Copyright (c) 2024 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.room +package io.element.android.appnav.room.joined import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomState.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomState.kt similarity index 96% rename from appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomState.kt rename to appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomState.kt index 0798979812..dbc190354f 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomState.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * Copyright (c) 2024 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.appnav.room +package io.element.android.appnav.room.joined import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.di.SessionScope diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt index ecc6a5dad0..5d5c66b32b 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt @@ -27,7 +27,7 @@ import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper import com.google.common.truth.Truth.assertThat import io.element.android.appnav.di.RoomComponentFactory -import io.element.android.appnav.room.RoomLoadedFlowNode +import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.libraries.architecture.childNode @@ -92,7 +92,7 @@ class RoomFlowNodeTest { messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(), roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(), coroutineScope: CoroutineScope, - ) = RoomLoadedFlowNode( + ) = JoinedRoomLoadedFlowNode( buildContext = BuildContext.root(savedStateMap = null), plugins = plugins, messagesEntryPoint = messagesEntryPoint, @@ -108,7 +108,7 @@ class RoomFlowNodeTest { // GIVEN val room = FakeMatrixRoom() val fakeMessagesEntryPoint = FakeMessagesEntryPoint() - val inputs = RoomLoadedFlowNode.Inputs(room) + val inputs = JoinedRoomLoadedFlowNode.Inputs(room) val roomFlowNode = aRoomFlowNode( plugins = listOf(inputs), messagesEntryPoint = fakeMessagesEntryPoint, @@ -118,9 +118,9 @@ class RoomFlowNodeTest { val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper() // THEN - assertThat(roomFlowNode.backstack.activeElement).isEqualTo(RoomLoadedFlowNode.NavTarget.Messages) - roomFlowNodeTestHelper.assertChildHasLifecycle(RoomLoadedFlowNode.NavTarget.Messages, Lifecycle.State.CREATED) - val messagesNode = roomFlowNode.childNode(RoomLoadedFlowNode.NavTarget.Messages)!! + assertThat(roomFlowNode.backstack.activeElement).isEqualTo(JoinedRoomLoadedFlowNode.NavTarget.Messages) + roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages, Lifecycle.State.CREATED) + val messagesNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.Messages)!! assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId) } @@ -130,7 +130,7 @@ class RoomFlowNodeTest { val room = FakeMatrixRoom() val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() - val inputs = RoomLoadedFlowNode.Inputs(room) + val inputs = JoinedRoomLoadedFlowNode.Inputs(room) val roomFlowNode = aRoomFlowNode( plugins = listOf(inputs), messagesEntryPoint = fakeMessagesEntryPoint, @@ -141,8 +141,8 @@ class RoomFlowNodeTest { // WHEN fakeMessagesEntryPoint.callback?.onRoomDetailsClicked() // THEN - roomFlowNodeTestHelper.assertChildHasLifecycle(RoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED) - val roomDetailsNode = roomFlowNode.childNode(RoomLoadedFlowNode.NavTarget.RoomDetails)!! + roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED) + val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!! assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId) } } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingRoomStateFlowFactoryTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingRoomStateFlowFactoryTest.kt index 58e35b2d58..cb8ed2da32 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingRoomStateFlowFactoryTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingRoomStateFlowFactoryTest.kt @@ -18,6 +18,8 @@ package io.element.android.appnav.room import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.appnav.room.joined.LoadingRoomState +import io.element.android.appnav.room.joined.LoadingRoomStateFlowFactory import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt index 5c526870e5..4534f10a74 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt @@ -17,7 +17,15 @@ package io.element.android.libraries.matrix.api.roomlist import androidx.compose.runtime.Immutable +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import java.util.Optional /** * Entry point for the room list api. @@ -77,4 +85,11 @@ interface RoomListService { * The state of the service as a flow. */ val state: StateFlow + + /** + * Get a flow of the room summary for a given room id. + */ + fun getUserMembershipForRoom(roomId: RoomId): Flow> } + + diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt index 70310e472e..09e0a514b1 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt @@ -16,16 +16,24 @@ package io.element.android.libraries.matrix.impl.roomlist +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.roomlist.awaitLoaded import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally +import io.element.android.libraries.matrix.impl.room.map import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn @@ -35,7 +43,9 @@ import org.matrix.rustcomponents.sdk.RoomListInput import org.matrix.rustcomponents.sdk.RoomListRange import org.matrix.rustcomponents.sdk.RoomListServiceState import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator +import org.matrix.rustcomponents.sdk.use import timber.log.Timber +import java.util.Optional import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService private const val DEFAULT_PAGE_SIZE = 20 @@ -112,6 +122,24 @@ internal class RustRoomListService( } .distinctUntilChanged() .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, RoomListService.State.Idle) + + override fun getUserMembershipForRoom(roomId: RoomId): Flow> { + return combine( + allRooms.loadedStateFlow(), + invites.loadedStateFlow(), + ) { _, _ -> + val membership = innerRoomListService.roomOrNull(roomId.value)?.use { + it.roomInfo().use { roomInfo -> + roomInfo.membership.map() + } + } + Optional.ofNullable(membership) + }.distinctUntilChanged() + } + + private fun RoomList.loadedStateFlow(): Flow { + return loadingState.filterIsInstance() + } } private fun RoomListServiceState.toRoomListState(): RoomListService.State {