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. 102
      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
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.loggedin.LoggedInNode import io.element.android.appnav.loggedin.LoggedInNode
import io.element.android.appnav.room.RoomFlowNode 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.createroom.api.CreateRoomEntryPoint
import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueService
@ -213,7 +214,7 @@ class LoggedInFlowNode @AssistedInject constructor(
@Parcelize @Parcelize
data class Room( data class Room(
val roomId: RoomId, val roomId: RoomId,
val initialElement: RoomLoadedFlowNode.NavTarget = RoomLoadedFlowNode.NavTarget.Messages val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages
) : NavTarget ) : NavTarget
@Parcelize @Parcelize
@ -273,7 +274,7 @@ class LoggedInFlowNode @AssistedInject constructor(
} }
override fun onRoomSettingsClicked(roomId: RoomId) { 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() { override fun onReportBugClicked() {
@ -290,7 +291,7 @@ class LoggedInFlowNode @AssistedInject constructor(
.build() .build()
} }
is NavTarget.Room -> { is NavTarget.Room -> {
val callback = object : RoomLoadedFlowNode.Callback { val callback = object : JoinedRoomLoadedFlowNode.Callback {
override fun onOpenRoom(roomId: RoomId) { override fun onOpenRoom(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId)) backstack.push(NavTarget.Room(roomId))
} }
@ -317,7 +318,7 @@ class LoggedInFlowNode @AssistedInject constructor(
} }
override fun onOpenRoomNotificationSettings(roomId: RoomId) { 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) val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
@ -349,6 +350,10 @@ class LoggedInFlowNode @AssistedInject constructor(
backstack.pop() backstack.pop()
} }
override fun onInviteClicked(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId))
}
override fun onInviteAccepted(roomId: RoomId) { override fun onInviteAccepted(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId)) backstack.push(NavTarget.Room(roomId))
} }

102
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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,15 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.appnav.room package io.element.android.appnav.room
import android.os.Parcelable 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.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.modality.BuildContext 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.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode 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.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs 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.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId 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.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber
import kotlin.jvm.optionals.getOrNull
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
class RoomFlowNode @AssistedInject constructor( class RoomFlowNode @AssistedInject constructor(
@Assisted val buildContext: BuildContext, @Assisted val buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory, private val roomListService: RoomListService,
private val roomMembershipObserver: RoomMembershipObserver,
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
) : ) :
BaseFlowNode<RoomFlowNode.NavTarget>( BaseFlowNode<RoomFlowNode.NavTarget>(
@ -68,64 +76,70 @@ class RoomFlowNode @AssistedInject constructor(
) { ) {
data class Inputs( data class Inputs(
val roomId: RoomId, val roomId: RoomId,
val initialElement: RoomLoadedFlowNode.NavTarget = RoomLoadedFlowNode.NavTarget.Messages, val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages,
) : NodeInputs ) : NodeInputs
private val inputs: Inputs = inputs() private val inputs: Inputs = inputs()
private val loadingRoomStateStateFlow = loadingRoomStateFlowFactory.create(lifecycleScope, inputs.roomId)
sealed interface NavTarget : Parcelable { sealed interface NavTarget : Parcelable {
@Parcelize @Parcelize
data object Loading : NavTarget data object Loading : NavTarget
@Parcelize @Parcelize
data object Loaded : NavTarget data object JoinRoom : NavTarget
@Parcelize
data object JoinedRoom : NavTarget
} }
override fun onBuilt() { override fun onBuilt() {
super.onBuilt() super.onBuilt()
loadingRoomStateStateFlow roomListService.getUserMembershipForRoom(
.map { inputs.roomId
it is LoadingRoomState.Loaded ).onEach { membership ->
} Timber.d("RoomMembership = $membership")
.distinctUntilChanged() when {
.onEach { isLoaded -> membership.getOrNull() == CurrentUserMembership.JOINED -> {
if (isLoaded) { backstack.newRoot(NavTarget.JoinedRoom)
backstack.newRoot(NavTarget.Loaded)
} else {
backstack.newRoot(NavTarget.Loading)
} }
else -> {
backstack.newRoot(NavTarget.JoinRoom)
}
}
}
.flowOn(Dispatchers.Default)
.launchIn(lifecycleScope)
roomMembershipObserver.updates
.filter { update -> update.roomId == inputs.roomId && !update.isUserInRoom }
.onEach {
navigateUp()
} }
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
} }
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
NavTarget.Loaded -> { NavTarget.Loading -> loadingNode(buildContext)
val roomFlowNodeCallback = plugins<RoomLoadedFlowNode.Callback>() NavTarget.JoinRoom -> joinRoomNode(buildContext)
val awaitRoomState = loadingRoomStateStateFlow.value NavTarget.JoinedRoom -> {
if (awaitRoomState is LoadingRoomState.Loaded) { val roomFlowNodeCallback = plugins<JoinedRoomLoadedFlowNode.Callback>()
val inputs = RoomLoadedFlowNode.Inputs(awaitRoomState.room, initialElement = inputs.initialElement) val inputs = JoinedRoomFlowNode.Inputs(inputs.roomId, initialElement = inputs.initialElement)
createNode<RoomLoadedFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback) createNode<JoinedRoomFlowNode>(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 -> private fun loadingNode(buildContext: BuildContext) = node(buildContext) {
val loadingRoomState by loadingRoomStateStateFlow.collectAsState() Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
val networkStatus by networkMonitor.connectivity.collectAsState() CircularProgressIndicator()
LoadingRoomNodeView( }
state = loadingRoomState, }
hasNetworkConnection = networkStatus == NetworkStatus.Online,
modifier = modifier, private fun joinRoomNode(buildContext: BuildContext) = node(buildContext) {
onBackClicked = onBackClicked Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
) Text("Unknown Room")
}
} }
@Composable @Composable

23
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,
}

138
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<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 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.appnav.room package io.element.android.appnav.room.joined
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -32,6 +32,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.di.RoomComponentFactory 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.messages.api.MessagesEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
import io.element.android.libraries.architecture.BackstackView 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.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope 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.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
class RoomLoadedFlowNode @AssistedInject constructor( class JoinedRoomLoadedFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val messagesEntryPoint: MessagesEntryPoint, private val messagesEntryPoint: MessagesEntryPoint,
@ -63,9 +61,13 @@ class RoomLoadedFlowNode @AssistedInject constructor(
private val appCoroutineScope: CoroutineScope, private val appCoroutineScope: CoroutineScope,
roomComponentFactory: RoomComponentFactory, roomComponentFactory: RoomComponentFactory,
roomMembershipObserver: RoomMembershipObserver, roomMembershipObserver: RoomMembershipObserver,
) : BaseFlowNode<RoomLoadedFlowNode.NavTarget>( ) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
backstack = BackStack( 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, savedStateMap = buildContext.savedStateMap,
), ),
buildContext = buildContext, buildContext = buildContext,
@ -79,7 +81,7 @@ class RoomLoadedFlowNode @AssistedInject constructor(
data class Inputs( data class Inputs(
val room: MatrixRoom, val room: MatrixRoom,
val initialElement: NavTarget = NavTarget.Messages, val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages,
) : NodeInputs ) : NodeInputs
private val inputs: Inputs = inputs() private val inputs: Inputs = inputs()
@ -108,13 +110,6 @@ class RoomLoadedFlowNode @AssistedInject constructor(
appNavigationStateService.onLeavingRoom(id) 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 { 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 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * 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.Box
import androidx.compose.foundation.layout.Column 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 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * 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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.di.SessionScope 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
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.appnav.di.RoomComponentFactory 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.messages.api.MessagesEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
import io.element.android.libraries.architecture.childNode import io.element.android.libraries.architecture.childNode
@ -92,7 +92,7 @@ class RoomFlowNodeTest {
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(), messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(), roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
) = RoomLoadedFlowNode( ) = JoinedRoomLoadedFlowNode(
buildContext = BuildContext.root(savedStateMap = null), buildContext = BuildContext.root(savedStateMap = null),
plugins = plugins, plugins = plugins,
messagesEntryPoint = messagesEntryPoint, messagesEntryPoint = messagesEntryPoint,
@ -108,7 +108,7 @@ class RoomFlowNodeTest {
// GIVEN // GIVEN
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val inputs = RoomLoadedFlowNode.Inputs(room) val inputs = JoinedRoomLoadedFlowNode.Inputs(room)
val roomFlowNode = aRoomFlowNode( val roomFlowNode = aRoomFlowNode(
plugins = listOf(inputs), plugins = listOf(inputs),
messagesEntryPoint = fakeMessagesEntryPoint, messagesEntryPoint = fakeMessagesEntryPoint,
@ -118,9 +118,9 @@ class RoomFlowNodeTest {
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper() val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
// THEN // THEN
assertThat(roomFlowNode.backstack.activeElement).isEqualTo(RoomLoadedFlowNode.NavTarget.Messages) assertThat(roomFlowNode.backstack.activeElement).isEqualTo(JoinedRoomLoadedFlowNode.NavTarget.Messages)
roomFlowNodeTestHelper.assertChildHasLifecycle(RoomLoadedFlowNode.NavTarget.Messages, Lifecycle.State.CREATED) roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages, Lifecycle.State.CREATED)
val messagesNode = roomFlowNode.childNode(RoomLoadedFlowNode.NavTarget.Messages)!! val messagesNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.Messages)!!
assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId) assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId)
} }
@ -130,7 +130,7 @@ class RoomFlowNodeTest {
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
val inputs = RoomLoadedFlowNode.Inputs(room) val inputs = JoinedRoomLoadedFlowNode.Inputs(room)
val roomFlowNode = aRoomFlowNode( val roomFlowNode = aRoomFlowNode(
plugins = listOf(inputs), plugins = listOf(inputs),
messagesEntryPoint = fakeMessagesEntryPoint, messagesEntryPoint = fakeMessagesEntryPoint,
@ -141,8 +141,8 @@ class RoomFlowNodeTest {
// WHEN // WHEN
fakeMessagesEntryPoint.callback?.onRoomDetailsClicked() fakeMessagesEntryPoint.callback?.onRoomDetailsClicked()
// THEN // THEN
roomFlowNodeTestHelper.assertChildHasLifecycle(RoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED) roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED)
val roomDetailsNode = roomFlowNode.childNode(RoomLoadedFlowNode.NavTarget.RoomDetails)!! val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!!
assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId) 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
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat 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.api.roomlist.RoomList
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_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 @@
package io.element.android.libraries.matrix.api.roomlist package io.element.android.libraries.matrix.api.roomlist
import androidx.compose.runtime.Immutable 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.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. * Entry point for the room list api.
@ -77,4 +85,11 @@ interface RoomListService {
* The state of the service as a flow. * The state of the service as a flow.
*/ */
val state: StateFlow<State> 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 @@
package io.element.android.libraries.matrix.impl.roomlist 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.DynamicRoomList
import io.element.android.libraries.matrix.api.roomlist.RoomList 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.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService 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.api.roomlist.loadAllIncrementally
import io.element.android.libraries.matrix.impl.room.map
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn 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.RoomListRange
import org.matrix.rustcomponents.sdk.RoomListServiceState import org.matrix.rustcomponents.sdk.RoomListServiceState
import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber import timber.log.Timber
import java.util.Optional
import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService
private const val DEFAULT_PAGE_SIZE = 20 private const val DEFAULT_PAGE_SIZE = 20
@ -112,6 +122,24 @@ internal class RustRoomListService(
} }
.distinctUntilChanged() .distinctUntilChanged()
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, RoomListService.State.Idle) .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 { private fun RoomListServiceState.toRoomListState(): RoomListService.State {

Loading…
Cancel
Save