Fix issues on JoinedRoom / BaseRoom (#4724)
* Import type * Add test to cover an existing issue. roomCoroutineScope is not cancelled when the class is destroyed * Cancel roomCoroutineScope when the class is destroyed * Move `isOneToOne` to BaseRoom, we do not need a JoinedRoom for it. * Let `roomInfoFlow` be implemented by RustBaseRoom. It should fix a few issues where we rely on the room info to be live, and it was not the case on RustBaseRoom. * Add more assertions. The test would fail anyway if roomCoroutineScope was still active, but it's more explicit with these assertions.
This commit is contained in:
@@ -55,6 +55,12 @@ interface BaseRoom : Closeable {
|
||||
*/
|
||||
fun info(): RoomInfo = roomInfoFlow.value
|
||||
|
||||
/**
|
||||
* A one-to-one is a room with exactly 2 members.
|
||||
* See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules).
|
||||
*/
|
||||
val isOneToOne: Boolean get() = info().activeMembersCount == 2L
|
||||
|
||||
/**
|
||||
* Try to load the room members and update the membersFlow.
|
||||
*/
|
||||
|
||||
@@ -50,12 +50,6 @@ interface JoinedRoom : BaseRoom {
|
||||
*/
|
||||
val knockRequestsFlow: Flow<List<KnockRequest>>
|
||||
|
||||
/**
|
||||
* A one-to-one is a room with exactly 2 members.
|
||||
* See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules).
|
||||
*/
|
||||
val isOneToOne: Boolean get() = info().activeMembersCount == 2L
|
||||
|
||||
/**
|
||||
* The live timeline of the room. Must be used to send Event to a room.
|
||||
*/
|
||||
|
||||
@@ -59,11 +59,8 @@ import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
||||
import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver
|
||||
import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
@@ -71,12 +68,10 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.DateDividerMode
|
||||
import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener
|
||||
import org.matrix.rustcomponents.sdk.KnockRequestsListener
|
||||
import org.matrix.rustcomponents.sdk.RoomInfoListener
|
||||
import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType
|
||||
import org.matrix.rustcomponents.sdk.TimelineConfiguration
|
||||
import org.matrix.rustcomponents.sdk.TimelineFilter
|
||||
@@ -93,7 +88,6 @@ import java.io.File
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import org.matrix.rustcomponents.sdk.IdentityStatusChange as RustIdentityStateChange
|
||||
import org.matrix.rustcomponents.sdk.KnockRequest as InnerKnockRequest
|
||||
import org.matrix.rustcomponents.sdk.RoomInfo as InnerRoomInfo
|
||||
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
|
||||
|
||||
class JoinedRustRoom(
|
||||
@@ -101,7 +95,6 @@ class JoinedRustRoom(
|
||||
private val liveInnerTimeline: InnerTimeline,
|
||||
private val notificationSettingsService: NotificationSettingsService,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val roomInfoMapper: RoomInfoMapper,
|
||||
private val systemClock: SystemClock,
|
||||
private val roomContentForwarder: RoomContentForwarder,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
@@ -112,14 +105,6 @@ class JoinedRustRoom(
|
||||
|
||||
override val syncUpdateFlow = MutableStateFlow(0L)
|
||||
|
||||
override val roomInfoFlow: StateFlow<io.element.android.libraries.matrix.api.room.RoomInfo> = mxCallbackFlow {
|
||||
innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener {
|
||||
override fun call(roomInfo: InnerRoomInfo) {
|
||||
channel.trySend(roomInfoMapper.map(roomInfo))
|
||||
}
|
||||
})
|
||||
}.stateIn(roomCoroutineScope, started = SharingStarted.Lazily, initialValue = baseRoom.info())
|
||||
|
||||
override val roomTypingMembersFlow: Flow<List<UserId>> = mxCallbackFlow {
|
||||
val initial = emptyList<UserId>()
|
||||
channel.trySend(initial)
|
||||
@@ -645,7 +630,6 @@ class JoinedRustRoom(
|
||||
override fun destroy() {
|
||||
baseRoom.destroy()
|
||||
liveInnerTimeline.destroy()
|
||||
roomCoroutineScope.cancel()
|
||||
}
|
||||
|
||||
private fun InnerTimeline.map(
|
||||
|
||||
@@ -31,10 +31,14 @@ import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
|
||||
import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsMapper
|
||||
import io.element.android.libraries.matrix.impl.roomdirectory.map
|
||||
import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType
|
||||
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.RoomInfoListener
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import timber.log.Timber
|
||||
import uniffi.matrix_sdk_base.EncryptionState
|
||||
@@ -48,6 +52,7 @@ class RustBaseRoom(
|
||||
private val roomSyncSubscriber: RoomSyncSubscriber,
|
||||
private val roomMembershipObserver: RoomMembershipObserver,
|
||||
sessionCoroutineScope: CoroutineScope,
|
||||
roomInfoMapper: RoomInfoMapper,
|
||||
initialRoomInfo: RoomInfo,
|
||||
) : BaseRoom {
|
||||
override val roomId = RoomId(innerRoom.id())
|
||||
@@ -62,10 +67,16 @@ class RustBaseRoom(
|
||||
|
||||
override val membersStateFlow: StateFlow<RoomMembersState> = roomMemberListFetcher.membersFlow
|
||||
|
||||
override val roomInfoFlow: StateFlow<RoomInfo> = MutableStateFlow(initialRoomInfo)
|
||||
|
||||
override val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
|
||||
|
||||
override val roomInfoFlow: StateFlow<RoomInfo> = mxCallbackFlow {
|
||||
innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener {
|
||||
override fun call(roomInfo: org.matrix.rustcomponents.sdk.RoomInfo) {
|
||||
channel.trySend(roomInfoMapper.map(roomInfo))
|
||||
}
|
||||
})
|
||||
}.stateIn(roomCoroutineScope, started = SharingStarted.Lazily, initialValue = initialRoomInfo)
|
||||
|
||||
override suspend fun subscribeToSync() = roomSyncSubscriber.subscribe(roomId)
|
||||
|
||||
override suspend fun updateMembers() {
|
||||
@@ -98,6 +109,7 @@ class RustBaseRoom(
|
||||
|
||||
override fun destroy() {
|
||||
innerRoom.destroy()
|
||||
roomCoroutineScope.cancel()
|
||||
}
|
||||
|
||||
override suspend fun userDisplayName(userId: UserId): Result<String?> = withContext(roomDispatcher) {
|
||||
|
||||
@@ -98,6 +98,7 @@ class RustRoomFactory(
|
||||
coroutineDispatchers = dispatchers,
|
||||
roomSyncSubscriber = roomSyncSubscriber,
|
||||
roomMembershipObserver = roomMembershipObserver,
|
||||
roomInfoMapper = roomInfoMapper,
|
||||
initialRoomInfo = roomInfoMapper.map(initialRoomInfo),
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
)
|
||||
@@ -127,7 +128,6 @@ class RustRoomFactory(
|
||||
liveInnerTimeline = roomReferences.room.timeline(),
|
||||
coroutineDispatchers = dispatchers,
|
||||
systemClock = systemClock,
|
||||
roomInfoMapper = roomInfoMapper,
|
||||
featureFlagService = featureFlagService,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.room
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoom
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService
|
||||
import io.element.android.libraries.matrix.test.A_DEVICE_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class RustBaseRoomTest {
|
||||
@Test
|
||||
fun `RustBaseRoom should cancel the room coroutine scope when it is destroyed`() = runTest {
|
||||
val rustBaseRoom = createRustBaseRoom(
|
||||
// Not using backgroundScope here, but the test scope
|
||||
sessionCoroutineScope = this
|
||||
)
|
||||
assertThat(rustBaseRoom.roomCoroutineScope.isActive).isTrue()
|
||||
rustBaseRoom.destroy()
|
||||
assertThat(rustBaseRoom.roomCoroutineScope.isActive).isFalse()
|
||||
}
|
||||
|
||||
private fun TestScope.createRustBaseRoom(
|
||||
sessionCoroutineScope: CoroutineScope,
|
||||
): RustBaseRoom {
|
||||
val dispatchers = testCoroutineDispatchers()
|
||||
return RustBaseRoom(
|
||||
sessionId = A_SESSION_ID,
|
||||
deviceId = A_DEVICE_ID,
|
||||
innerRoom = FakeRustRoom(),
|
||||
coroutineDispatchers = dispatchers,
|
||||
roomSyncSubscriber = RoomSyncSubscriber(
|
||||
roomListService = FakeRustRoomListService(),
|
||||
dispatchers = dispatchers,
|
||||
),
|
||||
roomMembershipObserver = RoomMembershipObserver(),
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
roomInfoMapper = RoomInfoMapper(),
|
||||
initialRoomInfo = aRoomInfo(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user