diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt index 8df4cb5671..120321bddf 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt @@ -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. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt index cbc5220678..ca62c4b134 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt @@ -50,12 +50,6 @@ interface JoinedRoom : BaseRoom { */ val knockRequestsFlow: Flow> - /** - * 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. */ diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt index 5ef34d9bed..e006dcdba6 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt @@ -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 = 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> = mxCallbackFlow { val initial = emptyList() channel.trySend(initial) @@ -645,7 +630,6 @@ class JoinedRustRoom( override fun destroy() { baseRoom.destroy() liveInnerTimeline.destroy() - roomCoroutineScope.cancel() } private fun InnerTimeline.map( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt index c9a1629e7b..10c07a1ac9 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt @@ -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 = roomMemberListFetcher.membersFlow - override val roomInfoFlow: StateFlow = MutableStateFlow(initialRoomInfo) - override val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId") + override val roomInfoFlow: StateFlow = 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 = withContext(roomDispatcher) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt index ee9592f155..511e94be2f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt @@ -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, ) ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt new file mode 100644 index 0000000000..8a9050a884 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt @@ -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(), + ) + } +}