From 28c09c166865dcc7bb4d8abc337bb3aa0c8d3c7b Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 12 Aug 2025 12:37:31 +0200 Subject: [PATCH] Fix leaving the room not always dismissing the room screen (#5089) * Fix leaving the room not always dismissing the room screen Use the existing `RoomInfo` membership check to dismiss the room instead of using `RoomMembershipObserver`. * Restore `membershipObserver`, check Maestro still works * Improve the logic for the local membership change check * Remove redundant room id check --- .../android/appnav/room/RoomFlowNode.kt | 49 ++++++++++++------- .../android/libraries/core/coroutine/Flow.kt | 13 +++++ 2 files changed, 44 insertions(+), 18 deletions(-) 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 103e97b258..de537f0944 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 @@ -37,6 +37,7 @@ 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.core.bool.orFalse +import io.element.android.libraries.core.coroutine.withPreviousValue import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomAlias @@ -47,11 +48,14 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.ui.room.LoadingRoomState +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import timber.log.Timber @@ -124,8 +128,19 @@ class RoomFlowNode @AssistedInject constructor( private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List) { val roomInfoFlow = client.getRoomInfoFlow(roomId) val isSpaceFlow = roomInfoFlow.map { it.getOrNull()?.isSpace.orFalse() }.distinctUntilChanged() - val currentMembershipFlow = roomInfoFlow.map { it.getOrNull()?.currentUserMembership }.distinctUntilChanged() - combine(currentMembershipFlow, isSpaceFlow) { membership, isSpace -> + + // This observes the local membership changes for the room + val membershipUpdateFlow = membershipObserver.updates + .filter { it.roomId == roomId } + .distinctUntilChanged() + // We add a replay so we can check the last local membership update + .shareIn(lifecycleScope, started = SharingStarted.Eagerly, replay = 1) + + val currentMembershipFlow = roomInfoFlow + .map { it.getOrNull()?.currentUserMembership } + .distinctUntilChanged() + .withPreviousValue() + combine(currentMembershipFlow, isSpaceFlow) { (previousMembership, membership), isSpace -> Timber.d("Room membership: $membership") when (membership) { CurrentUserMembership.JOINED -> { @@ -146,26 +161,24 @@ class RoomFlowNode @AssistedInject constructor( } } else -> { - // Was invited or the room is not known, display the join room screen - backstack.newRoot( - NavTarget.JoinRoom( - roomId = roomId, - serverNames = serverNames, - trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite, + if (membership == CurrentUserMembership.LEFT && previousMembership == CurrentUserMembership.JOINED) { + // The user left the room in this device, remove the room from the backstack + if (!membershipUpdateFlow.first().isUserInRoom) { + navigateUp() + } + } else { + // Was invited or the room is not known, display the join room screen + backstack.newRoot( + NavTarget.JoinRoom( + roomId = roomId, + serverNames = serverNames, + trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite, + ) ) - ) + } } } }.launchIn(lifecycleScope) - - // If the user leaves the room from this client, close the room flow. - lifecycleScope.launch { - membershipObserver.updates - .first { it.roomId == roomId && !it.isUserInRoom } - .run { - navigateUp() - } - } } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/Flow.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/Flow.kt index 66fbb7f988..bb168d8eb7 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/Flow.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/Flow.kt @@ -8,7 +8,9 @@ package io.element.android.libraries.core.coroutine import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.runningFold /** * Returns the first element of the flow that is an instance of [T], waiting for it if necessary. @@ -16,3 +18,14 @@ import kotlinx.coroutines.flow.first suspend inline fun Flow<*>.firstInstanceOf(): T { return first { it is T } as T } + +/** + * Returns a flow that emits pairs of the previous and current values. + * The first emission will be a pair of `null` and the first value emitted by the source flow. + */ +fun Flow.withPreviousValue(): Flow> { + return runningFold(null) { prev: Pair?, current -> + prev?.second to current + } + .filterNotNull() +}