Return cached room members before fetching new ones, do it in batches (#2274)

* Use cached users from the Rust SDK.

Also lazy load received users by batches.

* Create `RoomMemberListFetcher` to wrap all the room member loading logic

* Ensure we clear `RoomMember` Rust references if the fetching coroutine is canceled
This commit is contained in:
Jorge Martin Espinosa 2024-01-23 18:23:20 +01:00 committed by GitHub
parent 83267173c7
commit f9c5cd07a9
17 changed files with 579 additions and 84 deletions

View file

@ -118,14 +118,7 @@ class RoomLoadedFlowNode @AssistedInject constructor(
} }
private fun fetchRoomMembers() = lifecycleScope.launch { private fun fetchRoomMembers() = lifecycleScope.launch {
val room = inputs.room inputs.room.updateMembers()
room.updateMembers()
.onFailure {
Timber.e(it, "Fail to fetch members for room ${room.roomId}")
}
.onSuccess {
Timber.v("Success fetching members for room ${room.roomId}")
}
} }
private fun createRoomDetailsNode(buildContext: BuildContext, initialTarget: RoomDetailsEntryPoint.InitialTarget): Node { private fun createRoomDetailsNode(buildContext: BuildContext, initialTarget: RoomDetailsEntryPoint.InitialTarget): Node {

View file

@ -26,11 +26,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.Lifecycle
import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.leaveroom.api.LeaveRoomPresenter
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -76,7 +78,13 @@ class RoomDetailsPresenter @Inject constructor(
room.updateRoomNotificationSettings() room.updateRoomNotificationSettings()
observeNotificationSettings() observeNotificationSettings()
} }
room.updateMembers() }
// Update room members only when first presenting the node
OnLifecycleEvent { _, event ->
if (event == Lifecycle.Event.ON_CREATE) {
scope.launch { room.updateMembers() }
}
} }
val membersState by room.membersStateFlow.collectAsState() val membersState by room.membersStateFlow.collectAsState()

View file

@ -19,11 +19,8 @@ package io.element.android.features.roomdetails.impl.members
import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomMembers
import kotlinx.coroutines.flow.dropWhile
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@ -32,11 +29,8 @@ class RoomMemberListDataSource @Inject constructor(
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
) { ) {
suspend fun search(query: String): List<RoomMember> = withContext(coroutineDispatchers.io) { suspend fun search(query: String): List<RoomMember> = withContext(coroutineDispatchers.io) {
val roomMembers = room.membersStateFlow val roomMembersState = room.membersStateFlow.value
.dropWhile { it !is MatrixRoomMembersState.Ready } val roomMembers = roomMembersState.roomMembers().orEmpty()
.first()
.roomMembers()
.orEmpty()
val filteredMembers = if (query.isBlank()) { val filteredMembers = if (query.isBlank()) {
roomMembers roomMembers
} else { } else {

View file

@ -30,8 +30,10 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
import io.element.android.libraries.matrix.api.room.roomMembers
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@ -55,9 +57,12 @@ class RoomMemberListPresenter @Inject constructor(
value = room.canInvite().getOrElse { false } value = room.canInvite().getOrElse { false }
} }
LaunchedEffect(Unit) { LaunchedEffect(membersState) {
if (membersState is MatrixRoomMembersState.Unknown) {
return@LaunchedEffect
}
withContext(coroutineDispatchers.io) { withContext(coroutineDispatchers.io) {
val members = roomMemberListDataSource.search("").groupBy { it.membership } val members = membersState.roomMembers().orEmpty().groupBy { it.membership }
roomMembers = AsyncData.Success( roomMembers = AsyncData.Success(
RoomMembers( RoomMembers(
invited = members.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(), invited = members.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(),

View file

@ -16,6 +16,7 @@
package io.element.android.features.roomdetails package io.element.android.features.roomdetails
import androidx.lifecycle.Lifecycle
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
@ -47,9 +48,11 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.tests.testutils.FakeLifecycleOwner
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import io.element.android.tests.testutils.withFakeLifecycleOwner
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
@ -63,6 +66,10 @@ class RoomDetailsPresenterTests {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
private val fakeLifecycleOwner = FakeLifecycleOwner().apply {
givenState(Lifecycle.State.RESUMED)
}
private fun TestScope.createRoomDetailsPresenter( private fun TestScope.createRoomDetailsPresenter(
room: MatrixRoom, room: MatrixRoom,
leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(), leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(),
@ -94,7 +101,9 @@ class RoomDetailsPresenterTests {
val room = aMatrixRoom() val room = aMatrixRoom()
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.roomId).isEqualTo(room.roomId.value) assertThat(initialState.roomId).isEqualTo(room.roomId.value)
@ -116,7 +125,9 @@ class RoomDetailsPresenterTests {
} }
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
skipItems(1) skipItems(1)
val updatedState = awaitItem() val updatedState = awaitItem()
@ -133,7 +144,9 @@ class RoomDetailsPresenterTests {
val room = aMatrixRoom(name = null) val room = aMatrixRoom(name = null)
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.roomName).isEqualTo(room.displayName) assertThat(initialState.roomName).isEqualTo(room.displayName)
@ -155,7 +168,9 @@ class RoomDetailsPresenterTests {
} }
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Dm(otherRoomMember)) assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Dm(otherRoomMember))
@ -171,7 +186,9 @@ class RoomDetailsPresenterTests {
} }
val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
// Initially false // Initially false
assertThat(awaitItem().canInvite).isFalse() assertThat(awaitItem().canInvite).isFalse()
@ -189,7 +206,9 @@ class RoomDetailsPresenterTests {
} }
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
assertThat(awaitItem().canInvite).isFalse() assertThat(awaitItem().canInvite).isFalse()
@ -204,7 +223,9 @@ class RoomDetailsPresenterTests {
} }
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
assertThat(awaitItem().canInvite).isFalse() assertThat(awaitItem().canInvite).isFalse()
@ -222,7 +243,9 @@ class RoomDetailsPresenterTests {
} }
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
// Initially false // Initially false
assertThat(awaitItem().canEdit).isFalse() assertThat(awaitItem().canEdit).isFalse()
@ -251,7 +274,9 @@ class RoomDetailsPresenterTests {
} }
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
// Initially false // Initially false
assertThat(awaitItem().canEdit).isFalse() assertThat(awaitItem().canEdit).isFalse()
@ -281,7 +306,9 @@ class RoomDetailsPresenterTests {
} }
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
skipItems(1) skipItems(1)
@ -302,7 +329,9 @@ class RoomDetailsPresenterTests {
} }
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
// Initially false // Initially false
assertThat(awaitItem().canEdit).isFalse() assertThat(awaitItem().canEdit).isFalse()
@ -323,7 +352,9 @@ class RoomDetailsPresenterTests {
} }
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
// Initially false, and no further events // Initially false, and no further events
assertThat(awaitItem().canEdit).isFalse() assertThat(awaitItem().canEdit).isFalse()
@ -341,7 +372,9 @@ class RoomDetailsPresenterTests {
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
// The initial state is "hidden" and no further state changes happen // The initial state is "hidden" and no further state changes happen
assertThat(awaitItem().roomTopic).isEqualTo(RoomTopicState.Hidden) assertThat(awaitItem().roomTopic).isEqualTo(RoomTopicState.Hidden)
@ -360,7 +393,9 @@ class RoomDetailsPresenterTests {
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
// Ignore the initial state // Ignore the initial state
skipItems(1) skipItems(1)
@ -382,7 +417,9 @@ class RoomDetailsPresenterTests {
dispatchers = testCoroutineDispatchers() dispatchers = testCoroutineDispatchers()
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
awaitItem().eventSink(RoomDetailsEvent.LeaveRoom) awaitItem().eventSink(RoomDetailsEvent.LeaveRoom)
@ -403,7 +440,9 @@ class RoomDetailsPresenterTests {
notificationSettingsService = notificationSettingsService, notificationSettingsService = notificationSettingsService,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
notificationSettingsService.setRoomNotificationMode(room.roomId, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) notificationSettingsService.setRoomNotificationMode(room.roomId, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
val updatedState = consumeItemsUntilPredicate { val updatedState = consumeItemsUntilPredicate {
@ -420,7 +459,9 @@ class RoomDetailsPresenterTests {
val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) val room = aMatrixRoom(notificationSettingsService = notificationSettingsService)
val presenter = createRoomDetailsPresenter(room = room, notificationSettingsService = notificationSettingsService) val presenter = createRoomDetailsPresenter(room = room, notificationSettingsService = notificationSettingsService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
awaitItem().eventSink(RoomDetailsEvent.MuteNotification) awaitItem().eventSink(RoomDetailsEvent.MuteNotification)
val updatedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { val updatedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) {
@ -440,7 +481,9 @@ class RoomDetailsPresenterTests {
val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) val room = aMatrixRoom(notificationSettingsService = notificationSettingsService)
val presenter = createRoomDetailsPresenter(room = room, notificationSettingsService = notificationSettingsService) val presenter = createRoomDetailsPresenter(room = room, notificationSettingsService = notificationSettingsService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() withFakeLifecycleOwner(fakeLifecycleOwner) {
presenter.present()
}
}.test { }.test {
awaitItem().eventSink(RoomDetailsEvent.UnmuteNotification) awaitItem().eventSink(RoomDetailsEvent.UnmuteNotification)
val updatedState = consumeItemsUntilPredicate { val updatedState = consumeItemsUntilPredicate {

View file

@ -47,15 +47,20 @@ class RoomMemberListPresenterTests {
@Test @Test
fun `search is done automatically on start, but is async`() = runTest { fun `search is done automatically on start, but is async`() = runTest {
val presenter = createPresenter() val room = FakeMatrixRoom()
val presenter = createPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.roomMembers).isInstanceOf(AsyncData.Loading::class.java) assertThat(initialState.roomMembers).isInstanceOf(AsyncData.Loading::class.java)
assertThat(initialState.searchQuery).isEmpty() assertThat(initialState.searchQuery).isEmpty()
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
assertThat(initialState.isSearchActive).isFalse() assertThat(initialState.isSearchActive).isFalse()
room.givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
// Skip item while the new members state is processed
skipItems(1)
val loadedState = awaitItem() val loadedState = awaitItem()
assertThat(loadedState.roomMembers).isInstanceOf(AsyncData.Success::class.java) assertThat(loadedState.roomMembers).isInstanceOf(AsyncData.Success::class.java)
assertThat((loadedState.roomMembers as AsyncData.Success).data.invited).isEqualTo(listOf(aVictor(), aWalter())) assertThat((loadedState.roomMembers as AsyncData.Success).data.invited).isEqualTo(listOf(aVictor(), aWalter()))

View file

@ -75,7 +75,7 @@ interface MatrixRoom : Closeable {
/** /**
* Try to load the room members and update the membersFlow. * Try to load the room members and update the membersFlow.
*/ */
suspend fun updateMembers(): Result<Unit> suspend fun updateMembers()
suspend fun updateRoomNotificationSettings(): Result<Unit> suspend fun updateRoomNotificationSettings(): Result<Unit>

View file

@ -53,4 +53,5 @@ dependencies {
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(libs.coroutines.test) testImplementation(libs.coroutines.test)
testImplementation(libs.test.turbine)
} }

View file

@ -18,7 +18,7 @@ package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.impl.room.RoomMemberMapper import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
import org.matrix.rustcomponents.sdk.MessageLikeEventContent import org.matrix.rustcomponents.sdk.MessageLikeEventContent
import org.matrix.rustcomponents.sdk.StateEventContent import org.matrix.rustcomponents.sdk.StateEventContent

View file

@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use

View file

@ -18,7 +18,6 @@ package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.core.coroutine.parallelMap
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -39,7 +38,6 @@ import io.element.android.libraries.matrix.api.room.Mention
import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.room.roomNotificationSettings import io.element.android.libraries.matrix.api.room.roomNotificationSettings
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
@ -51,20 +49,17 @@ import io.element.android.libraries.matrix.impl.media.toMSC3246range
import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService
import io.element.android.libraries.matrix.impl.poll.toInner import io.element.android.libraries.matrix.impl.poll.toInner
import io.element.android.libraries.matrix.impl.room.location.toInner import io.element.android.libraries.matrix.impl.room.location.toInner
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher
import io.element.android.libraries.matrix.impl.timeline.AsyncMatrixTimeline import io.element.android.libraries.matrix.impl.timeline.AsyncMatrixTimeline
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import io.element.android.libraries.matrix.impl.util.destroyAll
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow 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.RustWidgetDriver
import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl
import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -75,7 +70,6 @@ import org.matrix.rustcomponents.sdk.EventTimelineItem
import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomInfo
import org.matrix.rustcomponents.sdk.RoomInfoListener import org.matrix.rustcomponents.sdk.RoomInfoListener
import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomMember
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
import org.matrix.rustcomponents.sdk.WidgetCapabilities import org.matrix.rustcomponents.sdk.WidgetCapabilities
@ -125,8 +119,8 @@ class RustMatrixRoom(
private val roomMembersDispatcher = coroutineDispatchers.io.limitedParallelism(8) private val roomMembersDispatcher = coroutineDispatchers.io.limitedParallelism(8)
private val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId") private val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
private val _membersStateFlow = MutableStateFlow<MatrixRoomMembersState>(MatrixRoomMembersState.Unknown)
private val _syncUpdateFlow = MutableStateFlow(0L) private val _syncUpdateFlow = MutableStateFlow(0L)
private val roomMemberListFetcher = RoomMemberListFetcher(innerRoom, roomMembersDispatcher)
private val _roomNotificationSettingsStateFlow = MutableStateFlow<MatrixRoomNotificationSettingsState>(MatrixRoomNotificationSettingsState.Unknown) private val _roomNotificationSettingsStateFlow = MutableStateFlow<MatrixRoomNotificationSettingsState>(MatrixRoomNotificationSettingsState.Unknown)
override val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState> = _roomNotificationSettingsStateFlow override val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState> = _roomNotificationSettingsStateFlow
@ -135,7 +129,7 @@ class RustMatrixRoom(
_syncUpdateFlow.value = systemClock.epochMillis() _syncUpdateFlow.value = systemClock.epochMillis()
} }
override val membersStateFlow: StateFlow<MatrixRoomMembersState> = _membersStateFlow.asStateFlow() override val membersStateFlow: StateFlow<MatrixRoomMembersState> = roomMemberListFetcher.membersFlow
override val syncUpdateFlow: StateFlow<Long> = _syncUpdateFlow.asStateFlow() override val syncUpdateFlow: StateFlow<Long> = _syncUpdateFlow.asStateFlow()
@ -192,35 +186,7 @@ class RustMatrixRoom(
override val activeMemberCount: Long override val activeMemberCount: Long
get() = innerRoom.activeMembersCount().toLong() get() = innerRoom.activeMembersCount().toLong()
override suspend fun updateMembers(): Result<Unit> = withContext(roomMembersDispatcher) { override suspend fun updateMembers() = roomMemberListFetcher.fetchRoomMembers()
val currentState = _membersStateFlow.value
val currentMembers = currentState.roomMembers()?.toImmutableList()
_membersStateFlow.value = MatrixRoomMembersState.Pending(prevRoomMembers = currentMembers)
var rustMembers: List<RoomMember>? = null
try {
rustMembers = innerRoom.members().use { membersIterator ->
buildList {
while (true) {
// Loading the whole membersIterator as a stop-gap measure.
// We should probably implement some sort of paging in the future.
ensureActive()
addAll(membersIterator.nextChunk(1000u) ?: break)
}
}
}
val mappedMembers = rustMembers.parallelMap(RoomMemberMapper::map)
_membersStateFlow.value = MatrixRoomMembersState.Ready(mappedMembers.toImmutableList())
Result.success(Unit)
} catch (exception: CancellationException) {
_membersStateFlow.value = MatrixRoomMembersState.Error(prevRoomMembers = currentMembers, failure = exception)
throw exception
} catch (exception: Exception) {
_membersStateFlow.value = MatrixRoomMembersState.Error(prevRoomMembers = currentMembers, failure = exception)
Result.failure(exception)
} finally {
rustMembers?.destroyAll()
}
}
override suspend fun userDisplayName(userId: UserId): Result<String?> = withContext(roomDispatcher) { override suspend fun userDisplayName(userId: UserId): Result<String?> = withContext(roomDispatcher) {
runCatching { runCatching {

View file

@ -0,0 +1,129 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.room.member
import io.element.android.libraries.core.coroutine.parallelMap
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.impl.util.destroyAll
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.RoomInterface
import org.matrix.rustcomponents.sdk.RoomMembersIterator
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.coroutineContext
/**
* This class fetches the room members for a given room in a 'paginated' way, and taking into account previous cached values.
*/
internal class RoomMemberListFetcher(
private val room: RoomInterface,
private val dispatcher: CoroutineDispatcher,
private val pageSize: Int = 1000,
) {
private val updatedRoomMemberMutex = Mutex()
private val roomId = room.id()
private val _membersFlow = MutableStateFlow<MatrixRoomMembersState>(MatrixRoomMembersState.Unknown)
val membersFlow: StateFlow<MatrixRoomMembersState> = _membersFlow
/**
* Fetches the room members for the given room.
* It will emit the cached members first, and then the updated members in batches of [pageSize] items, through [membersFlow].
* @param withCache Whether to load the cached members first. Defaults to true.
*/
suspend fun fetchRoomMembers(withCache: Boolean = true) {
if (updatedRoomMemberMutex.isLocked) {
Timber.i("Room members are already being updated for room $roomId")
return
}
updatedRoomMemberMutex.withLock {
withContext(dispatcher) {
// Load cached members as fallback and to get faster results
if (withCache) {
if (_membersFlow.value !is MatrixRoomMembersState.Ready) {
fetchCachedRoomMembers()
} else {
Timber.i("No need to load cached members found for room $roomId")
}
}
val prevRoomMembers = (_membersFlow.value as? MatrixRoomMembersState.Ready)?.roomMembers?.toImmutableList()
_membersFlow.value = MatrixRoomMembersState.Pending(prevRoomMembers = prevRoomMembers)
try {
// Start loading new members
parseAndEmitMembers(room.members())
} catch (exception: CancellationException) {
Timber.d("Cancelled loading updated members for room $roomId")
throw exception
} catch (exception: Exception) {
Timber.e(exception, "Failed to load updated members for room $roomId")
_membersFlow.value = MatrixRoomMembersState.Error(exception, prevRoomMembers)
}
}
}
}
internal suspend fun fetchCachedRoomMembers() = withContext(dispatcher) {
Timber.i("Loading cached members for room $roomId")
try {
val iterator = room.membersNoSync()
parseAndEmitMembers(iterator)
} catch (exception: CancellationException) {
Timber.d("Cancelled loading cached members for room $roomId")
throw exception
} catch (exception: Exception) {
Timber.e(exception, "Failed to load cached members for room $roomId")
_membersFlow.value = MatrixRoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList())
}
}
private suspend fun parseAndEmitMembers(roomMembersIterator: RoomMembersIterator) {
roomMembersIterator.use { iterator ->
val results = buildList {
while (true) {
// Loading the whole membersIterator as a stop-gap measure.
// We should probably implement some sort of paging in the future.
coroutineContext.ensureActive()
val chunk = iterator.nextChunk(pageSize.toUInt())
val members = try {
// Load next chunk. If null (no more items), exit the loop
chunk?.parallelMap(RoomMemberMapper::map) ?: break
} finally {
// Make sure we clear all member references
chunk?.destroyAll()
}
addAll(members)
Timber.i("Emitting first $size members for room $roomId")
_membersFlow.value = MatrixRoomMembersState.Ready(toImmutableList())
}
}
if (results.isEmpty()) {
_membersFlow.value = MatrixRoomMembersState.Ready(results.toImmutableList())
}
}
}
}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.libraries.matrix.impl.room package io.element.android.libraries.matrix.impl.room.member
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember

View file

@ -19,7 +19,7 @@ package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.matrix.impl.notificationsettings.RoomNotificationSettingsMapper import io.element.android.libraries.matrix.impl.notificationsettings.RoomNotificationSettingsMapper
import io.element.android.libraries.matrix.impl.room.RoomMemberMapper import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomInfo
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use

View file

@ -0,0 +1,303 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.room.member
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.A_USER_ID_3
import io.element.android.libraries.matrix.test.A_USER_ID_4
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.rustcomponents.sdk.MembershipState
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomMember
import org.matrix.rustcomponents.sdk.RoomMembersIterator
class RoomMemberListFetcherTest {
@Test
fun `fetchCachedRoomMembers - emits cached members, if any`() = runTest {
val room = FakeRustRoom(getMembersNoSync = {
FakeRoomMembersIterator(
listOf(
FakeRustRoomMember(A_USER_ID),
FakeRustRoomMember(A_USER_ID_2),
FakeRustRoomMember(A_USER_ID_3),
)
)
})
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
fetcher.membersFlow.test {
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
fetcher.fetchCachedRoomMembers()
val readyItem = awaitItem()
assertThat(readyItem).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
assertThat((readyItem as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(3)
}
}
@Test
fun `fetchCachedRoomMembers - emits empty list, if no members exist`() = runTest {
val room = FakeRustRoom(getMembersNoSync = {
FakeRoomMembersIterator(emptyList())
})
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
fetcher.membersFlow.test {
fetcher.fetchCachedRoomMembers()
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
assertThat(awaitItem().roomMembers()).isEmpty()
}
}
@Test
fun `fetchCachedRoomMembers - emits Error on error found`() = runTest {
val room = FakeRustRoom(getMembersNoSync = {
error("Some unexpected issue")
})
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
fetcher.membersFlow.test {
fetcher.fetchCachedRoomMembers()
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Error::class.java)
}
}
@Test
fun `fetchCachedRoomMembers - emits items using page size`() = runTest {
val room = FakeRustRoom(getMembersNoSync = {
FakeRoomMembersIterator(
listOf(
FakeRustRoomMember(A_USER_ID),
FakeRustRoomMember(A_USER_ID_2),
FakeRustRoomMember(A_USER_ID_3),
)
)
})
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default, pageSize = 2)
fetcher.membersFlow.test {
fetcher.fetchCachedRoomMembers()
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(2)
assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(3)
ensureAllEventsConsumed()
}
}
@Test
fun `fetchRoomMembers - with 'withCache' set to false emits only new members, if any`() = runTest {
val room = FakeRustRoom(getMembers = {
FakeRoomMembersIterator(
listOf(
FakeRustRoomMember(A_USER_ID),
FakeRustRoomMember(A_USER_ID_2),
FakeRustRoomMember(A_USER_ID_3),
)
)
})
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
fetcher.membersFlow.test {
fetcher.fetchRoomMembers(withCache = false)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(3)
}
}
@Test
fun `fetchRoomMembers - on error it emits an Error item`() = runTest {
val room = FakeRustRoom(getMembers = { error("An unexpected error") })
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
fetcher.membersFlow.test {
fetcher.fetchRoomMembers(withCache = false)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Error::class.java)
}
}
@Test
fun `fetchRoomMembers - with 'withCache' returns cached items first, then new ones`() = runTest {
val room = FakeRustRoom(
getMembersNoSync = {
FakeRoomMembersIterator(listOf(FakeRustRoomMember(A_USER_ID_4)))
},
getMembers = {
FakeRoomMembersIterator(
listOf(
FakeRustRoomMember(A_USER_ID),
FakeRustRoomMember(A_USER_ID_2),
FakeRustRoomMember(A_USER_ID_3),
)
)
}
)
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
fetcher.membersFlow.test {
fetcher.fetchRoomMembers(withCache = true)
// Initial
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
// Loaded cached
awaitItem().let { cached ->
assertThat(cached).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
assertThat(cached.roomMembers()).hasSize(1)
}
// Start loading new
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
awaitItem().let { ready ->
assertThat(ready).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
assertThat(ready.roomMembers()).hasSize(3)
}
}
}
@Test
fun `fetchRoomMembers - with 'withCache' skips cache if there is already a ready state`() = runTest {
val room = FakeRustRoom(
getMembersNoSync = {
FakeRoomMembersIterator(listOf(FakeRustRoomMember(A_USER_ID_4)))
},
getMembers = {
FakeRoomMembersIterator(
listOf(
FakeRustRoomMember(A_USER_ID),
FakeRustRoomMember(A_USER_ID_2),
FakeRustRoomMember(A_USER_ID_3),
)
)
}
)
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
// Set a ready state
fetcher.fetchRoomMembers(withCache = false)
fetcher.membersFlow.test {
// Start loading new members
fetcher.fetchRoomMembers(withCache = true)
// Previous ready state
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
// New pending state
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
// New ready state
awaitItem().let { ready ->
assertThat(ready).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
assertThat(ready.roomMembers()).hasSize(3)
}
}
}
}
class FakeRustRoom(
private val getMembers: () -> RoomMembersIterator = { FakeRoomMembersIterator() },
private val getMembersNoSync: () -> RoomMembersIterator = { FakeRoomMembersIterator() },
) : Room(NoPointer) {
override fun id(): String {
return A_ROOM_ID.value
}
override suspend fun members(): RoomMembersIterator {
return getMembers()
}
override suspend fun membersNoSync(): RoomMembersIterator {
return getMembersNoSync()
}
override fun close() {
// No-op
}
}
class FakeRoomMembersIterator(
private var members: List<RoomMember>? = null
) : RoomMembersIterator(NoPointer) {
override fun len(): UInt {
return members?.size?.toUInt() ?: 0u
}
override fun nextChunk(chunkSize: UInt): List<RoomMember>? {
if (members?.isEmpty() == true) {
return null
}
return members?.let {
val result = it.take(chunkSize.toInt())
members = it.subList(result.size, it.size)
result
}
}
}
class FakeRustRoomMember(
private val userId: UserId,
private val displayName: String? = null,
private val avatarUrl: String? = null,
private val membership: MembershipState = MembershipState.JOIN,
private val isNameAmbiguous: Boolean = false,
private val powerLevel: Long = 0L,
) : RoomMember(NoPointer) {
override fun userId(): String {
return userId.value
}
override fun displayName(): String? {
return displayName
}
override fun avatarUrl(): String? {
return avatarUrl
}
override fun membership(): MembershipState {
return membership
}
override fun isNameAmbiguous(): Boolean {
return isNameAmbiguous
}
override fun powerLevel(): Long {
return powerLevel
}
override fun normalizedPowerLevel(): Long {
return powerLevel
}
override fun isIgnored(): Boolean {
return false
}
}

View file

@ -171,9 +171,7 @@ class FakeMatrixRoom(
override val roomNotificationSettingsStateFlow: MutableStateFlow<MatrixRoomNotificationSettingsState> = override val roomNotificationSettingsStateFlow: MutableStateFlow<MatrixRoomNotificationSettingsState> =
MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown) MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown)
override suspend fun updateMembers(): Result<Unit> = simulateLongTask { override suspend fun updateMembers() = Unit
updateMembersResult
}
override suspend fun updateRoomNotificationSettings(): Result<Unit> = simulateLongTask { override suspend fun updateRoomNotificationSettings(): Result<Unit> = simulateLongTask {
val notificationSettings = notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow() val notificationSettings = notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow()

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.tests.testutils
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
@Stable
@Composable
fun <T> withFakeLifecycleOwner(lifecycleOwner: FakeLifecycleOwner = FakeLifecycleOwner(), block: @Composable () -> T): T {
var state: T? by remember { mutableStateOf(null) }
CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) {
state = block()
}
return state!!
}
@SuppressLint("VisibleForTests")
class FakeLifecycleOwner : LifecycleOwner {
override val lifecycle: Lifecycle = LifecycleRegistry.createUnsafe(this)
fun givenState(state: Lifecycle.State) {
(lifecycle as LifecycleRegistry).currentState = state
}
}