From b0152059ffbe38c38982db2e2bc02a1864cbc050 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 21 Apr 2023 14:39:47 +0200 Subject: [PATCH] Room : continue improving members loading --- .../io/element/android/appnav/RoomFlowNode.kt | 8 +- features/messages/impl/build.gradle.kts | 1 + .../messages/impl/timeline/TimelineView.kt | 26 ++-- .../event/TimelineItemEventFactory.kt | 2 - .../messages/MessagesPresenterTest.kt | 1 - .../messages/fixtures/timelineItemsFactory.kt | 4 + features/roomdetails/impl/build.gradle.kts | 1 + .../roomdetails/impl/RoomDetailsPresenter.kt | 115 +++++++++++------- .../impl/members/RoomMemberListEvents.kt} | 15 +-- .../impl/members/RoomMemberListNode.kt | 14 +-- .../impl/members/RoomMemberListPresenter.kt | 32 ++++- .../impl/members/RoomMemberListState.kt | 4 +- .../members/RoomMemberListStateProvider.kt | 1 + .../impl/members/RoomMemberListView.kt | 17 ++- .../impl/members/RoomUserListDataSource.kt | 29 +++-- .../roomdetails/RoomDetailsPresenterTests.kt | 60 ++++----- .../members/RoomMemberListPresenterTests.kt | 15 ++- .../userlist/api/UserListDataSource.kt | 1 + .../android/libraries/architecture/Async.kt | 4 +- .../libraries/matrix/api/room/MatrixRoom.kt | 30 +++-- .../matrix/api/room/MatrixRoomMembersState.kt | 31 +++++ .../matrix/impl/room/RustMatrixRoom.kt | 17 ++- .../impl/timeline/RustMatrixTimeline.kt | 5 +- .../matrix/test/room/FakeMatrixRoom.kt | 14 +-- tests/testutils/build.gradle.kts | 2 + .../testutils/TestCoroutineDispatchers.kt | 46 +++++++ 26 files changed, 329 insertions(+), 166 deletions(-) rename features/{messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/testCoroutineDispatchers.kt => roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt} (56%) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt create mode 100644 tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt index 4eaa5b83df..7cd33c2958 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -101,18 +101,12 @@ class RoomFlowNode @AssistedInject constructor( private fun fetchRoomMembers() = lifecycleScope.launch { val room = inputs.room - /* - room.fetchMembers() - .map { - 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}") } - - */ } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 9b43d41d4d..b695cc5512 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.features.networkmonitor.test) + testImplementation(projects.tests.testutils) androidTestImplementation(libs.test.junitext) ksp(libs.showkase.processor) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 7ea427a8cf..c7b071e069 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -75,6 +75,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch +import timber.log.Timber @Composable fun TimelineView( @@ -100,11 +101,11 @@ fun TimelineView( itemsIndexed( items = state.timelineItems, contentType = { _, timelineItem -> timelineItem.contentType() }, - key = { _, timelineItem -> timelineItem.key() }, + key = { _, timelineItem -> timelineItem.identifier() }, ) { index, timelineItem -> TimelineItemRow( timelineItem = timelineItem, - isHighlighted = timelineItem.key() == state.highlightedEventId?.value, + isHighlighted = timelineItem.identifier() == state.highlightedEventId?.value, onClick = onMessageClicked, onLongClick = onMessageLongClicked ) @@ -114,27 +115,22 @@ fun TimelineView( } } + /* TimelineScrollHelper( lazyListState = lazyListState, timelineItems = state.timelineItems, onLoadMore = ::onReachedLoadMore ) + + */ } } -private fun TimelineItem.key(): String { - return when (this) { - is TimelineItem.Event -> id - is TimelineItem.Virtual -> id - } -} - -private fun TimelineItem.contentType(): Int { - // Todo optimize for each subtype - return when (this) { - is TimelineItem.Event -> 0 - is TimelineItem.Virtual -> 1 - } +private fun TimelineItem.contentType() = when (this) { + is TimelineItem.Event -> content.javaClass.simpleName + is TimelineItem.Virtual -> model.javaClass.simpleName +}.also { + Timber.v("ContentType = $it") } @Composable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index c62719ae51..c21622b50f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -25,7 +25,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails import kotlinx.collections.immutable.toImmutableList -import timber.log.Timber import javax.inject.Inject class TimelineItemEventFactory @Inject constructor( @@ -43,7 +42,6 @@ class TimelineItemEventFactory @Inject constructor( val senderDisplayName: String? val senderAvatarUrl: String? - Timber.v("SenderProfile($currentSender) = ${currentTimelineItem.event.senderProfile}") when (val senderProfile = currentTimelineItem.event.senderProfile) { ProfileTimelineDetails.Unavailable, ProfileTimelineDetails.Pending, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index a7bc112174..bc898d5159 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -131,7 +131,6 @@ class MessagesPresenterTest { appCoroutineScope = this, room = matrixRoom ) - val timelinePresenter = TimelinePresenter( timelineItemsFactory = aTimelineItemsFactory(), room = matrixRoom, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt index 404e50a3bd..2d4ee3842c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.features.messages.fixtures import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory @@ -31,6 +33,8 @@ import io.element.android.features.messages.impl.timeline.factories.event.Timeli import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemDaySeparatorFactory import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi internal fun aTimelineItemsFactory() = TimelineItemsFactory( dispatchers = testCoroutineDispatchers(), diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index dc840b038f..41f117d6a4 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.features.userlist.impl) testImplementation(projects.features.userlist.test) + testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 969e2aa86b..9b6a1916c7 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -17,75 +17,60 @@ package io.element.android.features.roomdetails.impl import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.executeResult +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.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipObserver -import io.element.android.libraries.matrix.api.room.getDmMember -import io.element.android.libraries.matrix.api.room.memberCount -import kotlinx.coroutines.Dispatchers +import io.element.android.libraries.matrix.api.room.getDmMemberFlow +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject class RoomDetailsPresenter @Inject constructor( private val room: MatrixRoom, private val roomMembershipObserver: RoomMembershipObserver, + private val coroutineDispatchers: CoroutineDispatchers, ) : Presenter { @Composable override fun present(): RoomDetailsState { val coroutineScope = rememberCoroutineScope() - var leaveRoomWarning by remember { + val leaveRoomWarning = remember { mutableStateOf(null) } - var error by remember { + val error = remember { mutableStateOf(null) } + val membersState by room.membersStateFlow.collectAsState() + val memberCount by getMemberCount(membersState) + val dmMemberState by room.getDmMemberFlow() + .collectAsState(initial = null, context = coroutineDispatchers.computation) - val memberCount: MutableState> = remember { - mutableStateOf(Async.Uninitialized) - } - LaunchedEffect(Unit) { - suspend { - room.updateMembers() - .map { room.memberCount() } - }.executeResult(memberCount) - } - - val dmMember = room.getDmMember() - val roomType = if (dmMember != null) { - RoomDetailsType.Dm(dmMember) - } else { - RoomDetailsType.Room - } + val roomType = getRoomType(dmMemberState) fun handleEvents(event: RoomDetailsEvent) { when (event) { is RoomDetailsEvent.LeaveRoom -> { - if (event.needsConfirmation) { - leaveRoomWarning = LeaveRoomWarning.computeLeaveRoomWarning(room.isPublic, memberCount.value) - } else { - coroutineScope.launch(Dispatchers.IO) { - room.leave() - .onSuccess { - roomMembershipObserver.notifyUserLeftRoom(room.roomId) - }.onFailure { - error = RoomDetailsError.AlertGeneric - } - leaveRoomWarning = null - } - } + coroutineScope.leaveRoom( + needsConfirmation = event.needsConfirmation, + memberCount = memberCount, + leaveRoomWarning = leaveRoomWarning, + error = error, + ) } - is RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning = null - RoomDetailsEvent.ClearError -> error = null + is RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning.value = null + RoomDetailsEvent.ClearError -> error.value = null } } @@ -95,12 +80,56 @@ class RoomDetailsPresenter @Inject constructor( roomAlias = room.alias, roomAvatarUrl = room.avatarUrl, roomTopic = room.topic, - memberCount = memberCount.value, + memberCount = memberCount, isEncrypted = room.isEncrypted, - displayLeaveRoomWarning = leaveRoomWarning, - error = error, - roomType = roomType, + displayLeaveRoomWarning = leaveRoomWarning.value, + error = error.value, + roomType = roomType.value, eventSink = ::handleEvents, ) } + + @Composable + private fun getRoomType(dmMember: RoomMember?): State = remember(dmMember) { + derivedStateOf { + if (dmMember != null) { + RoomDetailsType.Dm(dmMember) + } else { + RoomDetailsType.Room + } + } + } + + @Composable + private fun getMemberCount(membersState: MatrixRoomMembersState): State> = remember(membersState) { + derivedStateOf { + when (membersState) { + MatrixRoomMembersState.Unknown -> Async.Uninitialized + MatrixRoomMembersState.Pending -> Async.Loading() + is MatrixRoomMembersState.Ready -> Async.Success(membersState.roomMembers.size) + is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure) + } + } + } + + private fun CoroutineScope.leaveRoom( + needsConfirmation: Boolean, + memberCount: Async, + leaveRoomWarning: MutableState, + error: MutableState, + ) = launch(coroutineDispatchers.io) { + if (needsConfirmation) { + leaveRoomWarning.value = LeaveRoomWarning.computeLeaveRoomWarning(room.isPublic, memberCount) + } else { + room.leave() + .onSuccess { + roomMembershipObserver.notifyUserLeftRoom(room.roomId) + }.onFailure { + error.value = RoomDetailsError.AlertGeneric + } + leaveRoomWarning.value = null + } + } } + + diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/testCoroutineDispatchers.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt similarity index 56% rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/testCoroutineDispatchers.kt rename to features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt index 60b0b7cf4b..4534842cac 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/testCoroutineDispatchers.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt @@ -14,15 +14,10 @@ * limitations under the License. */ -package io.element.android.features.messages.fixtures +package io.element.android.features.roomdetails.impl.members -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import kotlinx.coroutines.test.UnconfinedTestDispatcher +import io.element.android.libraries.matrix.ui.model.MatrixUser -// TODO Move to common module to reuse -internal fun testCoroutineDispatchers() = CoroutineDispatchers( - io = UnconfinedTestDispatcher(), - computation = UnconfinedTestDispatcher(), - main = UnconfinedTestDispatcher(), - diffUpdateDispatcher = UnconfinedTestDispatcher(), -) +sealed interface RoomMemberListEvents { + data class SelectUser(val user: MatrixUser) : RoomMemberListEvents +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt index 60ba07d1ee..ff2b1a0245 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -28,9 +28,6 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.getMember -import io.element.android.libraries.matrix.ui.model.MatrixUser -import timber.log.Timber @ContributesNode(RoomScope::class) class RoomMemberListNode @AssistedInject constructor( @@ -46,12 +43,9 @@ class RoomMemberListNode @AssistedInject constructor( private val callbacks = plugins() - private fun onUserSelected(matrixUser: MatrixUser) { - val member = room.getMember(matrixUser.id) - if (member != null) { - callbacks.forEach { it.openRoomMemberDetails(member) } - } else { - Timber.e("Could find room member ${matrixUser.id} in room ${room.roomId}") + private fun openRoomMemberDetails(roomMember: RoomMember) { + callbacks.forEach { + it.openRoomMemberDetails(roomMember) } } @@ -62,7 +56,7 @@ class RoomMemberListNode @AssistedInject constructor( state = state, modifier = modifier, onBackPressed = { navigateUp() }, - onUserSelected = ::onUserSelected, + onMemberSelected = this::openRoomMemberDetails, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 8841bd9d5e..897e56728b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -18,8 +18,10 @@ package io.element.android.features.roomdetails.impl.members import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import io.element.android.features.userlist.api.SelectionMode import io.element.android.features.userlist.api.UserListDataSource import io.element.android.features.userlist.api.UserListDataStore @@ -27,10 +29,16 @@ import io.element.android.features.userlist.api.UserListPresenter import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter +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.RoomMember +import io.element.android.libraries.matrix.api.room.getMemberFlow import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Named @@ -39,6 +47,8 @@ class RoomMemberListPresenter @Inject constructor( private val userListPresenterFactory: UserListPresenter.Factory, @Named("RoomMembers") private val userListDataSource: UserListDataSource, private val userListDataStore: UserListDataStore, + private val room: MatrixRoom, + private val coroutineDispatchers: CoroutineDispatchers, ) : Presenter { private val userListPresenter by lazy { @@ -51,17 +61,33 @@ class RoomMemberListPresenter @Inject constructor( @Composable override fun present(): RoomMemberListState { + val coroutineScope = rememberCoroutineScope() val userListState = userListPresenter.present() val allUsers = remember { mutableStateOf>>(Async.Loading()) } + val selectedMember: MutableState = remember { + mutableStateOf(null) + } LaunchedEffect(Unit) { - withContext(Dispatchers.IO) { + withContext(coroutineDispatchers.io) { allUsers.value = Async.Success(userListDataSource.search("").toImmutableList()) } } + + fun handleEvents(roomMemberListEvents: RoomMemberListEvents) { + when (roomMemberListEvents) { + is RoomMemberListEvents.SelectUser -> coroutineScope.loadRoomMember(roomMemberListEvents.user, selectedMember) + } + } return RoomMemberListState( allUsers = allUsers.value, - userListState = userListState + userListState = userListState, + selectedRoomMember = selectedMember.value, + eventSink = ::handleEvents ) } + + private fun CoroutineScope.loadRoomMember(user: MatrixUser, selectedMember: MutableState) = launch(coroutineDispatchers.io) { + selectedMember.value = room.getMemberFlow(user.id).firstOrNull() + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt index f5e5bd3efb..42289b9ebe 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt @@ -18,11 +18,13 @@ package io.element.android.features.roomdetails.impl.members import io.element.android.features.userlist.api.UserListState import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.ImmutableList data class RoomMemberListState( val allUsers: Async>, val userListState: UserListState, -// val eventSink: (AddPeopleEvents) -> Unit, + val selectedRoomMember: RoomMember? = null, + val eventSink: (RoomMemberListEvents) -> Unit, ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt index fc98ae7544..57012c6d86 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -39,4 +39,5 @@ internal fun aRoomMemberListState( RoomMemberListState( userListState = aUserListState().copy(searchResults = searchResults), allUsers = allUsers, + eventSink = {} ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt index f356e203f2..47aeb635df 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.pluralStringResource @@ -51,6 +52,7 @@ import io.element.android.libraries.designsystem.theme.components.CenterAlignedT import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.ui.model.MatrixUser @OptIn(ExperimentalMaterial3Api::class) @@ -59,8 +61,19 @@ fun RoomMemberListView( state: RoomMemberListState, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, - onUserSelected: (MatrixUser) -> Unit = {}, + onMemberSelected: (RoomMember) -> Unit = {}, ) { + + LaunchedEffect(state.selectedRoomMember) { + if (state.selectedRoomMember != null) { + onMemberSelected(state.selectedRoomMember) + } + } + + fun onUserSelected(user: MatrixUser) { + state.eventSink(RoomMemberListEvents.SelectUser(user)) + } + Scaffold( topBar = { if (!state.userListState.isSearchActive) { @@ -76,7 +89,7 @@ fun RoomMemberListView( ) { UserListView( state = state.userListState, - onUserSelected = onUserSelected, + onUserSelected = ::onUserSelected, ) if (!state.userListState.isSearchActive) { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt index 6bf1203e83..10d9d55c02 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt @@ -18,26 +18,40 @@ package io.element.android.features.roomdetails.impl.members import io.element.android.features.userlist.api.UserListDataSource import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.UserId 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.roomMembers import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.coroutines.flow.dropWhile +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.skip +import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.withContext import javax.inject.Inject class RoomUserListDataSource @Inject constructor( - private val room: MatrixRoom + private val room: MatrixRoom, + private val coroutineDispatchers: CoroutineDispatchers, ) : UserListDataSource { - override suspend fun search(query: String): List { - return room.membersFlow.value.filter { member -> - if (query.isBlank()) { - true - } else { + override suspend fun search(query: String): List = withContext(coroutineDispatchers.io) { + val roomMembers = room.membersStateFlow + .dropWhile { it !is MatrixRoomMembersState.Ready} + .first() + .roomMembers() + val filteredMembers = if (query.isBlank()) { + roomMembers + } else { + roomMembers.filter { member -> member.userId.value.contains(query, ignoreCase = true) || member.displayName?.contains(query, ignoreCase = true).orFalse() } - }.map(::mapMemberToMatrixUser) + } + filteredMembers.map(::mapMemberToMatrixUser) } override suspend fun getProfile(userId: UserId): MatrixUser? { @@ -55,5 +69,4 @@ class RoomUserListDataSource @Inject constructor( ) ) } - } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index 1d25a27e44..05617ebf1a 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -26,6 +26,7 @@ import io.element.android.features.roomdetails.impl.RoomDetailsPresenter import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId 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.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipState @@ -34,6 +35,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach @@ -45,11 +47,12 @@ import org.junit.Test class RoomDetailsPresenterTests { private val roomMembershipObserver = RoomMembershipObserver() + private val testCoroutineDispatchers = testCoroutineDispatchers() @Test fun `present - initial state is created from room info`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -68,13 +71,13 @@ class RoomDetailsPresenterTests { @Test fun `present - room member count is calculated asynchronously`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() Truth.assertThat(initialState.memberCount).isEqualTo(Async.Uninitialized) - + room.givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList())) val finalState = awaitItem() Truth.assertThat(finalState.memberCount).isEqualTo(Async.Success(0)) } @@ -83,7 +86,7 @@ class RoomDetailsPresenterTests { @Test fun `present - initial state with no room name`() = runTest { val room = aMatrixRoom(name = null) - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -97,30 +100,27 @@ class RoomDetailsPresenterTests { @Test fun `present - can handle error while fetching member count`() = runTest { val room = aMatrixRoom(name = null).apply { - givenFetchMemberResult(Result.failure(Throwable())) + givenRoomMembersState(MatrixRoomMembersState.Error(Throwable())) } - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { - skipItems(1) Truth.assertThat(awaitItem().memberCount).isInstanceOf(Async.Failure::class.java) - cancelAndIgnoreRemainingEvents() } } @Test fun `present - Leave with confirmation on private room shows a specific warning`() = runTest { - val room = aMatrixRoom(isPublic = false) - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val room = aMatrixRoom(isPublic = false).apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList())) + } + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - // Allow room member count to load - skipItems(1) - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) val confirmationState = awaitItem() Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.PrivateRoom) @@ -129,15 +129,14 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave with confirmation on empty room shows a specific warning`() = runTest { - val room = aMatrixRoom(members = listOf(aRoomMember())) - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val room = aMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(aRoomMember()))) + } + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - // Allow room member count to load - skipItems(1) - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) val confirmationState = awaitItem() Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.LastUserInRoom) @@ -146,15 +145,14 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave with confirmation shows a generic warning`() = runTest { - val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val room = aMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList())) + } + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - // Allow room member count to load - skipItems(1) - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) val confirmationState = awaitItem() Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.Generic) @@ -163,15 +161,14 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave without confirmation leaves the room`() = runTest { - val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val room = aMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList())) + } + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - // Allow room member count to load - skipItems(1) - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) cancelAndIgnoreRemainingEvents() @@ -188,14 +185,11 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom().apply { givenLeaveRoomError(Throwable()) } - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - // Allow room member count to load - skipItems(1) - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) val errorState = awaitItem() Truth.assertThat(errorState.error).isNotNull() @@ -211,13 +205,11 @@ fun aMatrixRoom( displayName: String = "A fallback display name", topic: String? = "A topic", avatarUrl: String? = "https://matrix.org/avatar.jpg", - members: List = emptyList(), isEncrypted: Boolean = true, isPublic: Boolean = true, ) = FakeMatrixRoom( roomId = roomId, name = name, - initialMembers = members, displayName = displayName, topic = topic, avatarUrl = avatarUrl, diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt index 373ebbb347..a5798381c0 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt @@ -29,8 +29,12 @@ import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.features.userlist.impl.DefaultUserListPresenter import io.element.android.features.userlist.test.FakeUserListDataSource import io.element.android.libraries.architecture.Async +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import okhttp3.internal.toImmutableList import org.junit.Test @@ -38,6 +42,8 @@ import org.junit.Test @ExperimentalCoroutinesApi class RoomMemberListPresenterTests { + private val testCoroutineDispatchers = testCoroutineDispatchers() + @Test fun `present - search is done automatically on start, but is async`() = runTest { val searchResult = listOf(aMatrixUser()) @@ -52,7 +58,14 @@ class RoomMemberListPresenterTests { userListDataStore: UserListDataStore, ) = DefaultUserListPresenter(args, userListDataSource, userListDataStore) } - val presenter = RoomMemberListPresenter(userListFactory, userListDataSource, userListDataStore) + val fakeRoom = FakeMatrixRoom() + val presenter = RoomMemberListPresenter( + userListPresenterFactory = userListFactory, + userListDataSource = userListDataSource, + userListDataStore = userListDataStore, + room = fakeRoom, + coroutineDispatchers = testCoroutineDispatchers + ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataSource.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataSource.kt index afe2d1ab3d..2cfd23eb61 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataSource.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataSource.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser interface UserListDataSource { + //TODO should probably have a flow suspend fun search(query: String): List suspend fun getProfile(userId: UserId): MatrixUser? } diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt index bb74dff2a9..3be961598d 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt @@ -47,7 +47,9 @@ suspend fun (suspend () -> T).execute(state: MutableState>, errorMa } suspend fun (suspend () -> Result).executeResult(state: MutableState>) { - state.value = Async.Loading() + if (state.value !is Async.Success) { + state.value = Async.Loading() + } this().fold( onSuccess = { state.value = Async.Success(it) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index fae6550a4f..2bff8fe76c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map import java.io.Closeable interface MatrixRoom : Closeable { @@ -41,10 +42,10 @@ interface MatrixRoom : Closeable { /** * The current loaded members as a StateFlow. - * Initial value is an emptyList. + * Initial value is [MatrixRoomMembersState.Unknown]. * To update them you should call [updateMembers]. */ - val membersFlow: StateFlow> + val membersStateFlow: StateFlow /** * Try to load the room members and update the membersFlow. @@ -70,18 +71,21 @@ interface MatrixRoom : Closeable { suspend fun leave(): Result } -fun MatrixRoom.getMember(userId: UserId): RoomMember? { - return membersFlow.value.find { it.userId == userId } -} - -fun MatrixRoom.getDmMember(): RoomMember? { - return if (membersFlow.value.size == 2 && isDirect && isEncrypted) { - membersFlow.value.find { it.userId != this.sessionId } - } else { - null +fun MatrixRoom.getMemberFlow(userId: UserId): Flow { + return membersStateFlow.map { state -> + state.roomMembers().find { + it.userId == userId + } } } -fun MatrixRoom.memberCount(): Int { - return membersFlow.value.size +fun MatrixRoom.getDmMemberFlow(): Flow { + return membersStateFlow.map { state -> + val members = state.roomMembers() + if (members.size == 2 && isDirect && isEncrypted) { + members.find { it.userId != this.sessionId } + } else { + null + } + } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt new file mode 100644 index 0000000000..319a5f7605 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 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.api.room + +sealed interface MatrixRoomMembersState { + object Unknown : MatrixRoomMembersState + object Pending : MatrixRoomMembersState + data class Error(val failure: Throwable) : MatrixRoomMembersState + data class Ready(val roomMembers: List) : MatrixRoomMembersState +} + +fun MatrixRoomMembersState.roomMembers(): List { + return when (this) { + is MatrixRoomMembersState.Ready -> roomMembers + else -> emptyList() + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 1b87d271dc..9e780e57de 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -22,7 +22,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import kotlinx.coroutines.CoroutineScope @@ -48,10 +48,10 @@ class RustMatrixRoom( private val coroutineDispatchers: CoroutineDispatchers, ) : MatrixRoom { - override val membersFlow: StateFlow> - get() = cachedMembers + override val membersStateFlow: StateFlow + get() = _membersStateFlow - private var cachedMembers = MutableStateFlow>(emptyList()) + private var _membersStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) override fun syncUpdateFlow(): Flow { return slidingSyncUpdateFlow @@ -122,9 +122,14 @@ class RustMatrixRoom( get() = innerRoom.isDirect() override suspend fun updateMembers(): Result = withContext(coroutineDispatchers.io) { + _membersStateFlow.value = MatrixRoomMembersState.Pending runCatching { - cachedMembers.value = innerRoom.members().map(RoomMemberMapper::map) - } + innerRoom.members().map(RoomMemberMapper::map) + }.onSuccess { + _membersStateFlow.value = MatrixRoomMembersState.Ready(it) + }.onFailure { + _membersStateFlow.value = MatrixRoomMembersState.Error(it) + }.map { } } override suspend fun userDisplayName(userId: UserId): Result = diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index 17e865f9fb..6d19e817a4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -143,12 +143,15 @@ class RustMatrixTimeline( requiredState = listOf( RequiredState(key = "m.room.canonical_alias", value = ""), RequiredState(key = "m.room.topic", value = ""), + RequiredState(key = "m.room.name", value = ""), RequiredState(key = "m.room.join_rule", value = ""), ), timelineLimit = 20.toUInt() ) val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings) - fetchMembers() + launch { + fetchMembers() + } listenerTokens += result.taskHandle result.items } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index b80b84a666..a685e369d1 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID @@ -44,18 +44,16 @@ class FakeMatrixRoom( override val alternativeAliases: List = emptyList(), override val isPublic: Boolean = true, override val isDirect: Boolean = false, - initialMembers: List = emptyList(), private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(), ) : MatrixRoom { private var userDisplayNameResult = Result.success(null) private var userAvatarUrlResult = Result.success(null) - private var dmMember: RoomMember? = null private var updateMembersResult: Result = Result.success(Unit) private var leaveRoomError: Throwable? = null - override val membersFlow: MutableStateFlow> = MutableStateFlow(initialMembers) + override val membersStateFlow: MutableStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) override suspend fun updateMembers(): Result { return updateMembersResult @@ -117,12 +115,12 @@ class FakeMatrixRoom( this.leaveRoomError = throwable } - fun givenFetchMemberResult(result: Result) { - updateMembersResult = result + fun givenRoomMembersState(state: MatrixRoomMembersState) { + membersStateFlow.value = state } - fun givenDmMember(roomMember: RoomMember) { - this.dmMember = roomMember + fun givenUpdateMembersResult(result: Result) { + updateMembersResult = result } fun givenUserDisplayNameResult(displayName: Result) { diff --git a/tests/testutils/build.gradle.kts b/tests/testutils/build.gradle.kts index 44167f906f..fda44f46d1 100644 --- a/tests/testutils/build.gradle.kts +++ b/tests/testutils/build.gradle.kts @@ -34,4 +34,6 @@ dependencies { implementation(libs.coroutines.test) implementation(projects.libraries.matrix.test) implementation(projects.services.appnavstate.test) + implementation(projects.services.appnavstate.test) + implementation(projects.libraries.core) } diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt new file mode 100644 index 0000000000..1309a14cb1 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.tests.testutils + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher + +fun testCoroutineDispatchers( + testScheduler: TestCoroutineScheduler? = null, +) = CoroutineDispatchers( + io = UnconfinedTestDispatcher(testScheduler), + computation = UnconfinedTestDispatcher(testScheduler), + main = UnconfinedTestDispatcher(testScheduler), + diffUpdateDispatcher = UnconfinedTestDispatcher(testScheduler), +) + +fun testCoroutineDispatchers( + io: TestDispatcher = UnconfinedTestDispatcher(), + computation: TestDispatcher = UnconfinedTestDispatcher(), + main: TestDispatcher = UnconfinedTestDispatcher(), + diffUpdateDispatcher: TestDispatcher = UnconfinedTestDispatcher(), +) = CoroutineDispatchers( + io = io, + computation = computation, + main = main, + diffUpdateDispatcher = diffUpdateDispatcher, +)