Load JoinedRoom in home screen, pass it to the room flow (#5817)

* Load `JoinedRoom` in `HomeFlowNode.navigateToRoom`, then pass it to the next navigation nodes

* Add delayed loading indicator for cases when loading the room takes too long

* Avoid an extra FFI call in `RustRoomFactory`.

Use `RoomInfo.membership` instead.

Also use `computation` dispatcher, since it should reduce the delay when switching contexts.

* Remove the dispatcher usage when loading the room in `HomeFlowNode`, we immediately call a method that changes the dispatcher used

* Make sure only a single room is opened at a time
This commit is contained in:
Jorge Martin Espinosa
2025-12-02 16:22:55 +01:00
committed by GitHub
parent c292a732d0
commit 77be19bf3b
12 changed files with 180 additions and 53 deletions

View File

@@ -39,7 +39,6 @@ import com.bumble.appyx.navmodel.backstack.operation.replace
import com.bumble.appyx.navmodel.backstack.operation.singleTop
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.annotations.ContributesNode
import io.element.android.appnav.loggedin.LoggedInNode
import io.element.android.appnav.loggedin.MediaPreviewConfigMigration
@@ -84,6 +83,7 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
import io.element.android.libraries.matrix.api.verification.VerificationRequest
@@ -109,6 +109,7 @@ import java.util.UUID
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toKotlinDuration
import im.vector.app.features.analytics.plan.JoinedRoom as JoinedRoomAnalyticsEvent
@ContributesNode(SessionScope::class)
@AssistedInject
@@ -265,7 +266,7 @@ class LoggedInFlowNode(
data class Room(
val roomIdOrAlias: RoomIdOrAlias,
val serverNames: List<String> = emptyList(),
val trigger: JoinedRoom.Trigger? = null,
val trigger: JoinedRoomAnalyticsEvent.Trigger? = null,
val roomDescription: RoomDescription? = null,
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Root(),
val targetId: UUID = UUID.randomUUID(),
@@ -315,8 +316,13 @@ class LoggedInFlowNode(
}
NavTarget.Home -> {
val callback = object : HomeEntryPoint.Callback {
override fun navigateToRoom(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) {
backstack.push(
NavTarget.Room(
roomIdOrAlias = roomId.toRoomIdOrAlias(),
initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom)
)
)
}
override fun navigateToSettings() {
@@ -365,7 +371,7 @@ class LoggedInFlowNode(
val target = NavTarget.Room(
roomIdOrAlias = data.roomIdOrAlias,
serverNames = data.viaParameters,
trigger = JoinedRoom.Trigger.Timeline,
trigger = JoinedRoomAnalyticsEvent.Trigger.Timeline,
initialElement = RoomNavigationTarget.Root(data.eventId),
)
if (pushToBackstack) {
@@ -479,7 +485,7 @@ class LoggedInFlowNode(
NavTarget.Room(
roomIdOrAlias = roomDescription.roomId.toRoomIdOrAlias(),
roomDescription = roomDescription,
trigger = JoinedRoom.Trigger.RoomDirectory,
trigger = JoinedRoomAnalyticsEvent.Trigger.RoomDirectory,
)
)
}
@@ -519,7 +525,7 @@ class LoggedInFlowNode(
suspend fun attachRoom(
roomIdOrAlias: RoomIdOrAlias,
serverNames: List<String> = emptyList(),
trigger: JoinedRoom.Trigger? = null,
trigger: JoinedRoomAnalyticsEvent.Trigger? = null,
eventId: EventId? = null,
clearBackstack: Boolean,
): RoomFlowNode {

View File

@@ -19,10 +19,10 @@ 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.active
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.annotations.ContributesNode
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
@@ -60,10 +60,13 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
import im.vector.app.features.analytics.plan.JoinedRoom as JoinedRoomAnalyticsEvent
import io.element.android.libraries.matrix.api.room.JoinedRoom as JoinedRoomInstance
@ContributesNode(SessionScope::class)
@AssistedInject
@@ -77,7 +80,14 @@ class RoomFlowNode(
private val analyticsService: AnalyticsService,
) : BaseFlowNode<RoomFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Loading,
initialElement = run {
val joinedRoom = (plugins.filterIsInstance<Inputs>().first().initialElement as? RoomNavigationTarget.Root)?.joinedRoom
if (joinedRoom != null) {
NavTarget.JoinedRoom(joinedRoom)
} else {
NavTarget.Loading
}
},
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
@@ -87,7 +97,7 @@ class RoomFlowNode(
val roomIdOrAlias: RoomIdOrAlias,
val roomDescription: Optional<RoomDescription>,
val serverNames: List<String>,
val trigger: Optional<JoinedRoom.Trigger>,
val trigger: Optional<JoinedRoomAnalyticsEvent.Trigger>,
val initialElement: RoomNavigationTarget,
) : NodeInputs
@@ -104,11 +114,16 @@ class RoomFlowNode(
data class JoinRoom(
val roomId: RoomId,
val serverNames: List<String>,
val trigger: im.vector.app.features.analytics.plan.JoinedRoom.Trigger,
val trigger: JoinedRoomAnalyticsEvent.Trigger,
) : NavTarget
@Parcelize
data class JoinedRoom(val roomId: RoomId) : NavTarget
data class JoinedRoom(
val roomId: RoomId,
@IgnoredOnParcel val joinedRoom: JoinedRoomInstance? = null,
) : NavTarget {
constructor(joinedRoom: JoinedRoomInstance) : this(joinedRoom.roomId, joinedRoom)
}
}
override fun onBuilt() {
@@ -133,7 +148,9 @@ class RoomFlowNode(
}
private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List<String>) {
val roomInfoFlow = client.getRoomInfoFlow(roomId)
val joinedRoom = (inputs.initialElement as? RoomNavigationTarget.Root)?.joinedRoom
val roomInfoFlow = joinedRoom?.roomInfoFlow?.map { Optional.of(it) }
?: client.getRoomInfoFlow(roomId)
// This observes the local membership changes for the room
val membershipUpdateFlow = membershipObserver.updates
@@ -149,6 +166,11 @@ class RoomFlowNode(
currentMembershipFlow.onEach { (previousMembership, membership) ->
Timber.d("Room membership: $membership")
if (membership == CurrentUserMembership.JOINED) {
val currentNavTarget = backstack.active?.key?.navTarget
if (currentNavTarget is NavTarget.JoinedRoom && currentNavTarget.roomId == roomId) {
Timber.d("Already in JoinedRoom $roomId, do nothing")
return@onEach
}
backstack.newRoot(NavTarget.JoinedRoom(roomId))
} else {
val leavingFromCurrentDevice =
@@ -163,7 +185,7 @@ class RoomFlowNode(
NavTarget.JoinRoom(
roomId = roomId,
serverNames = serverNames,
trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite,
trigger = inputs.trigger.getOrNull() ?: JoinedRoomAnalyticsEvent.Trigger.Invite,
)
)
}
@@ -209,7 +231,8 @@ class RoomFlowNode(
val roomFlowNodeCallback = plugins<JoinedRoomLoadedFlowNode.Callback>()
val inputs = JoinedRoomFlowNode.Inputs(
roomId = navTarget.roomId,
initialElement = inputs.initialElement
initialElement = inputs.initialElement,
joinedRoom = navTarget.joinedRoom,
)
createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
}

View File

@@ -10,12 +10,15 @@ package io.element.android.appnav.room
import android.os.Parcelable
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
sealed interface RoomNavigationTarget : Parcelable {
@Parcelize
data class Root(
val eventId: EventId? = null,
@IgnoredOnParcel val joinedRoom: JoinedRoom? = null,
) : RoomNavigationTarget
@Parcelize

View File

@@ -38,6 +38,7 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -63,11 +64,12 @@ class JoinedRoomFlowNode(
) {
data class Inputs(
val roomId: RoomId,
val joinedRoom: JoinedRoom?,
val initialElement: RoomNavigationTarget,
) : NodeInputs
private val inputs: Inputs = inputs()
private val loadingRoomStateStateFlow = loadingRoomStateFlowFactory.create(lifecycleScope, inputs.roomId)
private val loadingRoomStateStateFlow = loadingRoomStateFlowFactory.create(lifecycleScope, inputs.roomId, inputs.joinedRoom)
sealed interface NavTarget : Parcelable {
@Parcelize

View File

@@ -23,6 +23,19 @@ import kotlinx.coroutines.test.runTest
import org.junit.Test
class LoadingBaseRoomStateFlowFactoryTest {
@Test
fun `flow should emit only Loaded when we already pass a JoinedRoom`() = runTest {
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID))
val matrixClient = FakeMatrixClient(A_SESSION_ID)
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
flowFactory
.create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = room)
.test {
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room))
ensureAllEventsConsumed()
}
}
@Test
fun `flow should emit Loading and then Loaded when there is a room in cache`() = runTest {
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID))
@@ -31,7 +44,7 @@ class LoadingBaseRoomStateFlowFactoryTest {
}
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
flowFactory
.create(this, A_ROOM_ID)
.create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = null)
.test {
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room))
@@ -45,7 +58,7 @@ class LoadingBaseRoomStateFlowFactoryTest {
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
flowFactory
.create(this, A_ROOM_ID)
.create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = null)
.test {
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
matrixClient.givenGetRoomResult(A_ROOM_ID, room)
@@ -60,7 +73,7 @@ class LoadingBaseRoomStateFlowFactoryTest {
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
flowFactory
.create(this, A_ROOM_ID)
.create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = null)
.test {
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))