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
This commit is contained in:
Jorge Martin Espinosa 2025-08-12 12:37:31 +02:00 committed by GitHub
parent 308cbb4380
commit 28c09c1668
2 changed files with 44 additions and 18 deletions

View file

@ -37,6 +37,7 @@ 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.core.bool.orFalse 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.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias 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.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.ui.room.LoadingRoomState import io.element.android.libraries.matrix.ui.room.LoadingRoomState
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
@ -124,8 +128,19 @@ class RoomFlowNode @AssistedInject constructor(
private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List<String>) { private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List<String>) {
val roomInfoFlow = client.getRoomInfoFlow(roomId) val roomInfoFlow = client.getRoomInfoFlow(roomId)
val isSpaceFlow = roomInfoFlow.map { it.getOrNull()?.isSpace.orFalse() }.distinctUntilChanged() 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") Timber.d("Room membership: $membership")
when (membership) { when (membership) {
CurrentUserMembership.JOINED -> { CurrentUserMembership.JOINED -> {
@ -146,26 +161,24 @@ class RoomFlowNode @AssistedInject constructor(
} }
} }
else -> { else -> {
// Was invited or the room is not known, display the join room screen if (membership == CurrentUserMembership.LEFT && previousMembership == CurrentUserMembership.JOINED) {
backstack.newRoot( // The user left the room in this device, remove the room from the backstack
NavTarget.JoinRoom( if (!membershipUpdateFlow.first().isUserInRoom) {
roomId = roomId, navigateUp()
serverNames = serverNames, }
trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite, } 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) }.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 { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {

View file

@ -8,7 +8,9 @@
package io.element.android.libraries.core.coroutine package io.element.android.libraries.core.coroutine
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first 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. * 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 <reified T> Flow<*>.firstInstanceOf(): T { suspend inline fun <reified T> Flow<*>.firstInstanceOf(): T {
return first { it is T } as 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 <T> Flow<T>.withPreviousValue(): Flow<Pair<T?, T>> {
return runningFold(null) { prev: Pair<T?, T>?, current ->
prev?.second to current
}
.filterNotNull()
}