Browse Source

Room navigation : add a JoinedRoomFlowNode so we use RoomFlowNode for managing different routes

pull/2695/head
ganfra 6 months ago
parent
commit
5a192b49d7
  1. 15
      appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
  2. 96
      appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt
  3. 23
      appnav/src/main/kotlin/io/element/android/appnav/room/RoomNavigationTarget.kt
  4. 138
      appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt
  5. 27
      appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt
  6. 4
      appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt
  7. 4
      appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomState.kt
  8. 18
      appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt
  9. 2
      appnav/src/test/kotlin/io/element/android/appnav/room/LoadingRoomStateFlowFactoryTest.kt
  10. 15
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt
  11. 28
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt

15
appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt

@ -42,7 +42,8 @@ import dagger.assisted.AssistedInject @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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))
}

96
appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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 @@ -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<Plugin>,
loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory,
private val roomListService: RoomListService,
private val roomMembershipObserver: RoomMembershipObserver,
private val networkMonitor: NetworkMonitor,
) :
BaseFlowNode<RoomFlowNode.NavTarget>(
@ -68,64 +76,70 @@ class RoomFlowNode @AssistedInject constructor( @@ -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
roomListService.getUserMembershipForRoom(
inputs.roomId
).onEach { membership ->
Timber.d("RoomMembership = $membership")
when {
membership.getOrNull() == CurrentUserMembership.JOINED -> {
backstack.newRoot(NavTarget.JoinedRoom)
}
else -> {
backstack.newRoot(NavTarget.JoinRoom)
}
}
.distinctUntilChanged()
.onEach { isLoaded ->
if (isLoaded) {
backstack.newRoot(NavTarget.Loaded)
} else {
backstack.newRoot(NavTarget.Loading)
}
.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<RoomLoadedFlowNode.Callback>()
val awaitRoomState = loadingRoomStateStateFlow.value
if (awaitRoomState is LoadingRoomState.Loaded) {
val inputs = RoomLoadedFlowNode.Inputs(awaitRoomState.room, initialElement = inputs.initialElement)
createNode<RoomLoadedFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
} else {
loadingNode(buildContext, this::navigateUp)
NavTarget.Loading -> loadingNode(buildContext)
NavTarget.JoinRoom -> joinRoomNode(buildContext)
NavTarget.JoinedRoom -> {
val roomFlowNodeCallback = plugins<JoinedRoomLoadedFlowNode.Callback>()
val inputs = JoinedRoomFlowNode.Inputs(inputs.roomId, initialElement = inputs.initialElement)
createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
}
}
NavTarget.Loading -> {
loadingNode(buildContext, this::navigateUp)
}
private fun loadingNode(buildContext: BuildContext) = node(buildContext) {
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
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 joinRoomNode(buildContext: BuildContext) = node(buildContext) {
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("Unknown Room")
}
}
@Composable

23
appnav/src/main/kotlin/io/element/android/appnav/room/RoomNavigationTarget.kt

@ -0,0 +1,23 @@ @@ -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,
}

138
appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt

@ -0,0 +1,138 @@ @@ -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<Plugin>,
loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory,
private val networkMonitor: NetworkMonitor,
) :
BaseFlowNode<JoinedRoomFlowNode.NavTarget>(
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<JoinedRoomLoadedFlowNode.Callback>()
val awaitRoomState = loadingRoomStateStateFlow.value
if (awaitRoomState is LoadingRoomState.Loaded) {
val inputs = JoinedRoomLoadedFlowNode.Inputs(awaitRoomState.room, initialElement = inputs.initialElement)
createNode<JoinedRoomLoadedFlowNode>(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(),
)
}
}

27
appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt → appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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 @@ -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 @@ -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<Plugin>,
private val messagesEntryPoint: MessagesEntryPoint,
@ -63,9 +61,13 @@ class RoomLoadedFlowNode @AssistedInject constructor( @@ -63,9 +61,13 @@ class RoomLoadedFlowNode @AssistedInject constructor(
private val appCoroutineScope: CoroutineScope,
roomComponentFactory: RoomComponentFactory,
roomMembershipObserver: RoomMembershipObserver,
) : BaseFlowNode<RoomLoadedFlowNode.NavTarget>(
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
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( @@ -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( @@ -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<Inputs>()
}
private fun fetchRoomMembers() = lifecycleScope.launch {

4
appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomNodeView.kt → appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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

4
appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomState.kt → appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomState.kt

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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

18
appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt

@ -27,7 +27,7 @@ import com.bumble.appyx.testing.junit4.util.MainDispatcherRule @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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)
}
}

2
appnav/src/test/kotlin/io/element/android/appnav/room/LoadingRoomStateFlowFactoryTest.kt

@ -18,6 +18,8 @@ package io.element.android.appnav.room @@ -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

15
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt

@ -17,7 +17,15 @@ @@ -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 { @@ -77,4 +85,11 @@ interface RoomListService {
* The state of the service as a flow.
*/
val state: StateFlow<State>
/**
* Get a flow of the room summary for a given room id.
*/
fun getUserMembershipForRoom(roomId: RoomId): Flow<Optional<CurrentUserMembership>>
}

28
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt

@ -16,16 +16,24 @@ @@ -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 @@ -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( @@ -112,6 +122,24 @@ internal class RustRoomListService(
}
.distinctUntilChanged()
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, RoomListService.State.Idle)
override fun getUserMembershipForRoom(roomId: RoomId): Flow<Optional<CurrentUserMembership>> {
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<RoomList.LoadingState.Loaded> {
return loadingState.filterIsInstance()
}
}
private fun RoomListServiceState.toRoomListState(): RoomListService.State {

Loading…
Cancel
Save