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 eedaeb6b35
commit 2e2d68ba83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 180 additions and 53 deletions

View file

@ -13,6 +13,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
interface HomeEntryPoint : FeatureEntryPoint {
fun createNode(
@ -22,7 +23,7 @@ interface HomeEntryPoint : FeatureEntryPoint {
): Node
interface Callback : Plugin {
fun navigateToRoom(roomId: RoomId)
fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?)
fun navigateToCreateRoom()
fun navigateToSettings()
fun navigateToSetUpRecovery()

View file

@ -14,6 +14,8 @@ import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
@ -41,20 +43,33 @@ import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.features.reportroom.api.ReportRoomEntryPoint
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesEntryPoint
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesListType
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.appyx.launchMolecule
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.deeplink.api.usecase.InviteFriendsUseCase
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.utils.DelayedVisibility
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.milliseconds
@ContributesNode(SessionScope::class)
@AssistedInject
@ -71,6 +86,7 @@ class HomeFlowNode(
private val declineInviteAndBlockUserEntryPoint: DeclineInviteAndBlockEntryPoint,
private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint,
private val leaveRoomRenderer: LeaveRoomRenderer,
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
) : BaseFlowNode<HomeFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
@ -150,9 +166,58 @@ class HomeFlowNode(
return node(buildContext) { modifier ->
val state by stateFlow.collectAsState()
val activity = requireNotNull(LocalActivity.current)
val loadingJoinedRoomJob = remember { mutableStateOf<AsyncData<Job>>(AsyncData.Uninitialized) }
if (loadingJoinedRoomJob.value.isLoading()) {
DelayedVisibility(duration = 400.milliseconds) {
ProgressDialog(
onDismissRequest = {
loadingJoinedRoomJob.value.dataOrNull()?.cancel()
loadingJoinedRoomJob.value = AsyncData.Uninitialized
}
)
}
}
fun navigateToRoom(
roomId: RoomId,
) {
if (!loadingJoinedRoomJob.value.isUninitialized()) {
Timber.w("Already loading a room, ignoring navigateToRoom for $roomId")
return
}
val job = sessionCoroutineScope.launch {
runCatchingExceptions {
matrixClient.getJoinedRoom(roomId)
}.fold(
onSuccess = { joinedRoom ->
if (isActive) {
callback.navigateToRoom(roomId, joinedRoom)
loadingJoinedRoomJob.value = AsyncData.Success(coroutineContext.job)
// Wait a bit before resetting the state to avoid allowing to open several rooms
delay(200.milliseconds)
loadingJoinedRoomJob.value = AsyncData.Uninitialized
}
},
onFailure = {
// If the operation wasn't cancelled, navigate without the room, using the room id
if (it !is CancellationException) {
callback.navigateToRoom(roomId, null)
}
loadingJoinedRoomJob.value = AsyncData.Failure(error = it, prevData = coroutineContext.job)
// Wait a bit before resetting the state to avoid allowing to open several rooms
delay(200.milliseconds)
loadingJoinedRoomJob.value = AsyncData.Uninitialized
}
)
}
loadingJoinedRoomJob.value = AsyncData.Loading(job)
}
HomeView(
homeState = state,
onRoomClick = callback::navigateToRoom,
onRoomClick = ::navigateToRoom,
onSettingsClick = callback::navigateToSettings,
onStartChatClick = callback::navigateToCreateRoom,
onSetUpRecoveryClick = callback::navigateToSetUpRecovery,
@ -165,7 +230,7 @@ class HomeFlowNode(
acceptDeclineInviteView = {
acceptDeclineInviteView.Render(
state = state.roomListState.acceptDeclineInviteState,
onAcceptInviteSuccess = callback::navigateToRoom,
onAcceptInviteSuccess = ::navigateToRoom,
onDeclineInviteSuccess = { },
modifier = Modifier
)

View file

@ -13,17 +13,19 @@ import com.bumble.appyx.core.modality.BuildContext
import com.google.common.truth.Truth.assertThat
import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DefaultHomeEntryPointTest {
@Test
fun `test node builder`() {
fun `test node builder`() = runTest {
val entryPoint = DefaultHomeEntryPoint()
val parentNode = TestParentNode.create { buildContext, plugins ->
HomeFlowNode(
@ -39,10 +41,11 @@ class DefaultHomeEntryPointTest {
declineInviteAndBlockUserEntryPoint = { _, _, _ -> lambdaError() },
changeRoomMemberRolesEntryPoint = { _, _, _, _ -> lambdaError() },
leaveRoomRenderer = { _, _, _ -> lambdaError() },
sessionCoroutineScope = backgroundScope,
)
}
val callback = object : HomeEntryPoint.Callback {
override fun navigateToRoom(roomId: RoomId) = lambdaError()
override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) = lambdaError()
override fun navigateToCreateRoom() = lambdaError()
override fun navigateToSettings() = lambdaError()
override fun navigateToSetUpRecovery() = lambdaError()

View file

@ -65,6 +65,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -261,6 +262,7 @@ class TimelinePresenter(
items
}
.onEach(redactedVoiceMessageManager::onEachMatrixTimelineItem)
.flowOn(dispatchers.computation)
.launchIn(this)
}