Room : continue improving members loading

This commit is contained in:
ganfra 2023-04-21 14:39:47 +02:00
parent f02ee307cc
commit b0152059ff
26 changed files with 329 additions and 166 deletions

View file

@ -101,18 +101,12 @@ class RoomFlowNode @AssistedInject constructor(
private fun fetchRoomMembers() = lifecycleScope.launch { private fun fetchRoomMembers() = lifecycleScope.launch {
val room = inputs.room val room = inputs.room
/* room.updateMembers()
room.fetchMembers()
.map {
room.updateMembers()
}
.onFailure { .onFailure {
Timber.e(it, "Fail to fetch members for room ${room.roomId}") Timber.e(it, "Fail to fetch members for room ${room.roomId}")
}.onSuccess { }.onSuccess {
Timber.v("Success fetching members for room ${room.roomId}") Timber.v("Success fetching members for room ${room.roomId}")
} }
*/
} }
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {

View file

@ -55,6 +55,7 @@ dependencies {
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.libraries.dateformatter.test)
testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.tests.testutils)
androidTestImplementation(libs.test.junitext) androidTestImplementation(libs.test.junitext)
ksp(libs.showkase.processor) ksp(libs.showkase.processor)

View file

@ -75,6 +75,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
@Composable @Composable
fun TimelineView( fun TimelineView(
@ -100,11 +101,11 @@ fun TimelineView(
itemsIndexed( itemsIndexed(
items = state.timelineItems, items = state.timelineItems,
contentType = { _, timelineItem -> timelineItem.contentType() }, contentType = { _, timelineItem -> timelineItem.contentType() },
key = { _, timelineItem -> timelineItem.key() }, key = { _, timelineItem -> timelineItem.identifier() },
) { index, timelineItem -> ) { index, timelineItem ->
TimelineItemRow( TimelineItemRow(
timelineItem = timelineItem, timelineItem = timelineItem,
isHighlighted = timelineItem.key() == state.highlightedEventId?.value, isHighlighted = timelineItem.identifier() == state.highlightedEventId?.value,
onClick = onMessageClicked, onClick = onMessageClicked,
onLongClick = onMessageLongClicked onLongClick = onMessageLongClicked
) )
@ -114,27 +115,22 @@ fun TimelineView(
} }
} }
/*
TimelineScrollHelper( TimelineScrollHelper(
lazyListState = lazyListState, lazyListState = lazyListState,
timelineItems = state.timelineItems, timelineItems = state.timelineItems,
onLoadMore = ::onReachedLoadMore onLoadMore = ::onReachedLoadMore
) )
*/
} }
} }
private fun TimelineItem.key(): String { private fun TimelineItem.contentType() = when (this) {
return when (this) { is TimelineItem.Event -> content.javaClass.simpleName
is TimelineItem.Event -> id is TimelineItem.Virtual -> model.javaClass.simpleName
is TimelineItem.Virtual -> id }.also {
} Timber.v("ContentType = $it")
}
private fun TimelineItem.contentType(): Int {
// Todo optimize for each subtype
return when (this) {
is TimelineItem.Event -> 0
is TimelineItem.Virtual -> 1
}
} }
@Composable @Composable

View file

@ -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.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class TimelineItemEventFactory @Inject constructor( class TimelineItemEventFactory @Inject constructor(
@ -43,7 +42,6 @@ class TimelineItemEventFactory @Inject constructor(
val senderDisplayName: String? val senderDisplayName: String?
val senderAvatarUrl: String? val senderAvatarUrl: String?
Timber.v("SenderProfile($currentSender) = ${currentTimelineItem.event.senderProfile}")
when (val senderProfile = currentTimelineItem.event.senderProfile) { when (val senderProfile = currentTimelineItem.event.senderProfile) {
ProfileTimelineDetails.Unavailable, ProfileTimelineDetails.Unavailable,
ProfileTimelineDetails.Pending, ProfileTimelineDetails.Pending,

View file

@ -131,7 +131,6 @@ class MessagesPresenterTest {
appCoroutineScope = this, appCoroutineScope = this,
room = matrixRoom room = matrixRoom
) )
val timelinePresenter = TimelinePresenter( val timelinePresenter = TimelinePresenter(
timelineItemsFactory = aTimelineItemsFactory(), timelineItemsFactory = aTimelineItemsFactory(),
room = matrixRoom, room = matrixRoom,

View file

@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.messages.fixtures package io.element.android.features.messages.fixtures
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory 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.TimelineItemDaySeparatorFactory
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
internal fun aTimelineItemsFactory() = TimelineItemsFactory( internal fun aTimelineItemsFactory() = TimelineItemsFactory(
dispatchers = testCoroutineDispatchers(), dispatchers = testCoroutineDispatchers(),

View file

@ -53,6 +53,7 @@ dependencies {
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.features.userlist.impl) testImplementation(projects.features.userlist.impl)
testImplementation(projects.features.userlist.test) testImplementation(projects.features.userlist.test)
testImplementation(projects.tests.testutils)
ksp(libs.showkase.processor) ksp(libs.showkase.processor)
} }

View file

@ -17,75 +17,60 @@
package io.element.android.features.roomdetails.impl package io.element.android.features.roomdetails.impl
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState 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.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter 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.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.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.getDmMember import io.element.android.libraries.matrix.api.room.getDmMemberFlow
import io.element.android.libraries.matrix.api.room.memberCount import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class RoomDetailsPresenter @Inject constructor( class RoomDetailsPresenter @Inject constructor(
private val room: MatrixRoom, private val room: MatrixRoom,
private val roomMembershipObserver: RoomMembershipObserver, private val roomMembershipObserver: RoomMembershipObserver,
private val coroutineDispatchers: CoroutineDispatchers,
) : Presenter<RoomDetailsState> { ) : Presenter<RoomDetailsState> {
@Composable @Composable
override fun present(): RoomDetailsState { override fun present(): RoomDetailsState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var leaveRoomWarning by remember { val leaveRoomWarning = remember {
mutableStateOf<LeaveRoomWarning?>(null) mutableStateOf<LeaveRoomWarning?>(null)
} }
var error by remember { val error = remember {
mutableStateOf<RoomDetailsError?>(null) mutableStateOf<RoomDetailsError?>(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<Async<Int>> = remember { val roomType = getRoomType(dmMemberState)
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
}
fun handleEvents(event: RoomDetailsEvent) { fun handleEvents(event: RoomDetailsEvent) {
when (event) { when (event) {
is RoomDetailsEvent.LeaveRoom -> { is RoomDetailsEvent.LeaveRoom -> {
if (event.needsConfirmation) { coroutineScope.leaveRoom(
leaveRoomWarning = LeaveRoomWarning.computeLeaveRoomWarning(room.isPublic, memberCount.value) needsConfirmation = event.needsConfirmation,
} else { memberCount = memberCount,
coroutineScope.launch(Dispatchers.IO) { leaveRoomWarning = leaveRoomWarning,
room.leave() error = error,
.onSuccess { )
roomMembershipObserver.notifyUserLeftRoom(room.roomId)
}.onFailure {
error = RoomDetailsError.AlertGeneric
}
leaveRoomWarning = null
}
}
} }
is RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning = null is RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning.value = null
RoomDetailsEvent.ClearError -> error = null RoomDetailsEvent.ClearError -> error.value = null
} }
} }
@ -95,12 +80,56 @@ class RoomDetailsPresenter @Inject constructor(
roomAlias = room.alias, roomAlias = room.alias,
roomAvatarUrl = room.avatarUrl, roomAvatarUrl = room.avatarUrl,
roomTopic = room.topic, roomTopic = room.topic,
memberCount = memberCount.value, memberCount = memberCount,
isEncrypted = room.isEncrypted, isEncrypted = room.isEncrypted,
displayLeaveRoomWarning = leaveRoomWarning, displayLeaveRoomWarning = leaveRoomWarning.value,
error = error, error = error.value,
roomType = roomType, roomType = roomType.value,
eventSink = ::handleEvents, eventSink = ::handleEvents,
) )
} }
@Composable
private fun getRoomType(dmMember: RoomMember?): State<RoomDetailsType> = remember(dmMember) {
derivedStateOf {
if (dmMember != null) {
RoomDetailsType.Dm(dmMember)
} else {
RoomDetailsType.Room
}
}
}
@Composable
private fun getMemberCount(membersState: MatrixRoomMembersState): State<Async<Int>> = 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<Int>,
leaveRoomWarning: MutableState<LeaveRoomWarning?>,
error: MutableState<RoomDetailsError?>,
) = 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
}
}
} }

View file

@ -14,15 +14,10 @@
* limitations under the License. * 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 io.element.android.libraries.matrix.ui.model.MatrixUser
import kotlinx.coroutines.test.UnconfinedTestDispatcher
// TODO Move to common module to reuse sealed interface RoomMemberListEvents {
internal fun testCoroutineDispatchers() = CoroutineDispatchers( data class SelectUser(val user: MatrixUser) : RoomMemberListEvents
io = UnconfinedTestDispatcher(), }
computation = UnconfinedTestDispatcher(),
main = UnconfinedTestDispatcher(),
diffUpdateDispatcher = UnconfinedTestDispatcher(),
)

View file

@ -28,9 +28,6 @@ import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
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.RoomMember 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) @ContributesNode(RoomScope::class)
class RoomMemberListNode @AssistedInject constructor( class RoomMemberListNode @AssistedInject constructor(
@ -46,12 +43,9 @@ class RoomMemberListNode @AssistedInject constructor(
private val callbacks = plugins<Callback>() private val callbacks = plugins<Callback>()
private fun onUserSelected(matrixUser: MatrixUser) { private fun openRoomMemberDetails(roomMember: RoomMember) {
val member = room.getMember(matrixUser.id) callbacks.forEach {
if (member != null) { it.openRoomMemberDetails(roomMember)
callbacks.forEach { it.openRoomMemberDetails(member) }
} else {
Timber.e("Could find room member ${matrixUser.id} in room ${room.roomId}")
} }
} }
@ -62,7 +56,7 @@ class RoomMemberListNode @AssistedInject constructor(
state = state, state = state,
modifier = modifier, modifier = modifier,
onBackPressed = { navigateUp() }, onBackPressed = { navigateUp() },
onUserSelected = ::onUserSelected, onMemberSelected = this::openRoomMemberDetails,
) )
} }
} }

View file

@ -18,8 +18,10 @@ package io.element.android.features.roomdetails.impl.members
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.SelectionMode
import io.element.android.features.userlist.api.UserListDataSource import io.element.android.features.userlist.api.UserListDataSource
import io.element.android.features.userlist.api.UserListDataStore 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.features.userlist.api.UserListPresenterArgs
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
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.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 io.element.android.libraries.matrix.ui.model.MatrixUser
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList 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 kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Named import javax.inject.Named
@ -39,6 +47,8 @@ class RoomMemberListPresenter @Inject constructor(
private val userListPresenterFactory: UserListPresenter.Factory, private val userListPresenterFactory: UserListPresenter.Factory,
@Named("RoomMembers") private val userListDataSource: UserListDataSource, @Named("RoomMembers") private val userListDataSource: UserListDataSource,
private val userListDataStore: UserListDataStore, private val userListDataStore: UserListDataStore,
private val room: MatrixRoom,
private val coroutineDispatchers: CoroutineDispatchers,
) : Presenter<RoomMemberListState> { ) : Presenter<RoomMemberListState> {
private val userListPresenter by lazy { private val userListPresenter by lazy {
@ -51,17 +61,33 @@ class RoomMemberListPresenter @Inject constructor(
@Composable @Composable
override fun present(): RoomMemberListState { override fun present(): RoomMemberListState {
val coroutineScope = rememberCoroutineScope()
val userListState = userListPresenter.present() val userListState = userListPresenter.present()
val allUsers = remember { mutableStateOf<Async<ImmutableList<MatrixUser>>>(Async.Loading()) } val allUsers = remember { mutableStateOf<Async<ImmutableList<MatrixUser>>>(Async.Loading()) }
val selectedMember: MutableState<RoomMember?> = remember {
mutableStateOf(null)
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
withContext(Dispatchers.IO) { withContext(coroutineDispatchers.io) {
allUsers.value = Async.Success(userListDataSource.search("").toImmutableList()) allUsers.value = Async.Success(userListDataSource.search("").toImmutableList())
} }
} }
fun handleEvents(roomMemberListEvents: RoomMemberListEvents) {
when (roomMemberListEvents) {
is RoomMemberListEvents.SelectUser -> coroutineScope.loadRoomMember(roomMemberListEvents.user, selectedMember)
}
}
return RoomMemberListState( return RoomMemberListState(
allUsers = allUsers.value, allUsers = allUsers.value,
userListState = userListState userListState = userListState,
selectedRoomMember = selectedMember.value,
eventSink = ::handleEvents
) )
} }
private fun CoroutineScope.loadRoomMember(user: MatrixUser, selectedMember: MutableState<RoomMember?>) = launch(coroutineDispatchers.io) {
selectedMember.value = room.getMemberFlow(user.id).firstOrNull()
}
} }

View file

@ -18,11 +18,13 @@ package io.element.android.features.roomdetails.impl.members
import io.element.android.features.userlist.api.UserListState import io.element.android.features.userlist.api.UserListState
import io.element.android.libraries.architecture.Async 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 io.element.android.libraries.matrix.ui.model.MatrixUser
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class RoomMemberListState( data class RoomMemberListState(
val allUsers: Async<ImmutableList<MatrixUser>>, val allUsers: Async<ImmutableList<MatrixUser>>,
val userListState: UserListState, val userListState: UserListState,
// val eventSink: (AddPeopleEvents) -> Unit, val selectedRoomMember: RoomMember? = null,
val eventSink: (RoomMemberListEvents) -> Unit,
) )

View file

@ -39,4 +39,5 @@ internal fun aRoomMemberListState(
RoomMemberListState( RoomMemberListState(
userListState = aUserListState().copy(searchResults = searchResults), userListState = aUserListState().copy(searchResults = searchResults),
allUsers = allUsers, allUsers = allUsers,
eventSink = {}
) )

View file

@ -28,6 +28,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource 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.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text 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 import io.element.android.libraries.matrix.ui.model.MatrixUser
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -59,8 +61,19 @@ fun RoomMemberListView(
state: RoomMemberListState, state: RoomMemberListState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {}, 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( Scaffold(
topBar = { topBar = {
if (!state.userListState.isSearchActive) { if (!state.userListState.isSearchActive) {
@ -76,7 +89,7 @@ fun RoomMemberListView(
) { ) {
UserListView( UserListView(
state = state.userListState, state = state.userListState,
onUserSelected = onUserSelected, onUserSelected = ::onUserSelected,
) )
if (!state.userListState.isSearchActive) { if (!state.userListState.isSearchActive) {

View file

@ -18,26 +18,40 @@ package io.element.android.features.roomdetails.impl.members
import io.element.android.features.userlist.api.UserListDataSource import io.element.android.features.userlist.api.UserListDataSource
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.designsystem.components.avatar.AvatarData 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.core.UserId
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.ui.model.MatrixUser 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 import javax.inject.Inject
class RoomUserListDataSource @Inject constructor( class RoomUserListDataSource @Inject constructor(
private val room: MatrixRoom private val room: MatrixRoom,
private val coroutineDispatchers: CoroutineDispatchers,
) : UserListDataSource { ) : UserListDataSource {
override suspend fun search(query: String): List<MatrixUser> { override suspend fun search(query: String): List<MatrixUser> = withContext(coroutineDispatchers.io) {
return room.membersFlow.value.filter { member -> val roomMembers = room.membersStateFlow
if (query.isBlank()) { .dropWhile { it !is MatrixRoomMembersState.Ready}
true .first()
} else { .roomMembers()
val filteredMembers = if (query.isBlank()) {
roomMembers
} else {
roomMembers.filter { member ->
member.userId.value.contains(query, ignoreCase = true) member.userId.value.contains(query, ignoreCase = true)
|| member.displayName?.contains(query, ignoreCase = true).orFalse() || member.displayName?.contains(query, ignoreCase = true).orFalse()
} }
}.map(::mapMemberToMatrixUser) }
filteredMembers.map(::mapMemberToMatrixUser)
} }
override suspend fun getProfile(userId: UserId): MatrixUser? { override suspend fun getProfile(userId: UserId): MatrixUser? {
@ -55,5 +69,4 @@ class RoomUserListDataSource @Inject constructor(
) )
) )
} }
} }

View file

@ -26,6 +26,7 @@ import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
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.core.UserId 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.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.RoomMembershipState 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_ROOM_NAME
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -45,11 +47,12 @@ import org.junit.Test
class RoomDetailsPresenterTests { class RoomDetailsPresenterTests {
private val roomMembershipObserver = RoomMembershipObserver() private val roomMembershipObserver = RoomMembershipObserver()
private val testCoroutineDispatchers = testCoroutineDispatchers()
@Test @Test
fun `present - initial state is created from room info`() = runTest { fun `present - initial state is created from room info`() = runTest {
val room = aMatrixRoom() val room = aMatrixRoom()
val presenter = RoomDetailsPresenter(room, roomMembershipObserver) val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -68,13 +71,13 @@ class RoomDetailsPresenterTests {
@Test @Test
fun `present - room member count is calculated asynchronously`() = runTest { fun `present - room member count is calculated asynchronously`() = runTest {
val room = aMatrixRoom() val room = aMatrixRoom()
val presenter = RoomDetailsPresenter(room, roomMembershipObserver) val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
Truth.assertThat(initialState.memberCount).isEqualTo(Async.Uninitialized) Truth.assertThat(initialState.memberCount).isEqualTo(Async.Uninitialized)
room.givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList()))
val finalState = awaitItem() val finalState = awaitItem()
Truth.assertThat(finalState.memberCount).isEqualTo(Async.Success(0)) Truth.assertThat(finalState.memberCount).isEqualTo(Async.Success(0))
} }
@ -83,7 +86,7 @@ class RoomDetailsPresenterTests {
@Test @Test
fun `present - initial state with no room name`() = runTest { fun `present - initial state with no room name`() = runTest {
val room = aMatrixRoom(name = null) val room = aMatrixRoom(name = null)
val presenter = RoomDetailsPresenter(room, roomMembershipObserver) val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -97,30 +100,27 @@ class RoomDetailsPresenterTests {
@Test @Test
fun `present - can handle error while fetching member count`() = runTest { fun `present - can handle error while fetching member count`() = runTest {
val room = aMatrixRoom(name = null).apply { 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) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
Truth.assertThat(awaitItem().memberCount).isInstanceOf(Async.Failure::class.java) Truth.assertThat(awaitItem().memberCount).isInstanceOf(Async.Failure::class.java)
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@Test @Test
fun `present - Leave with confirmation on private room shows a specific warning`() = runTest { fun `present - Leave with confirmation on private room shows a specific warning`() = runTest {
val room = aMatrixRoom(isPublic = false) val room = aMatrixRoom(isPublic = false).apply {
val presenter = RoomDetailsPresenter(room, roomMembershipObserver) givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList()))
}
val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
// Allow room member count to load
skipItems(1)
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true))
val confirmationState = awaitItem() val confirmationState = awaitItem()
Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.PrivateRoom) Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.PrivateRoom)
@ -129,15 +129,14 @@ class RoomDetailsPresenterTests {
@Test @Test
fun `present - Leave with confirmation on empty room shows a specific warning`() = runTest { fun `present - Leave with confirmation on empty room shows a specific warning`() = runTest {
val room = aMatrixRoom(members = listOf(aRoomMember())) val room = aMatrixRoom().apply {
val presenter = RoomDetailsPresenter(room, roomMembershipObserver) givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(aRoomMember())))
}
val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
// Allow room member count to load
skipItems(1)
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true))
val confirmationState = awaitItem() val confirmationState = awaitItem()
Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.LastUserInRoom) Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.LastUserInRoom)
@ -146,15 +145,14 @@ class RoomDetailsPresenterTests {
@Test @Test
fun `present - Leave with confirmation shows a generic warning`() = runTest { fun `present - Leave with confirmation shows a generic warning`() = runTest {
val room = aMatrixRoom() val room = aMatrixRoom().apply {
val presenter = RoomDetailsPresenter(room, roomMembershipObserver) givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList()))
}
val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
// Allow room member count to load
skipItems(1)
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true))
val confirmationState = awaitItem() val confirmationState = awaitItem()
Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.Generic) Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.Generic)
@ -163,15 +161,14 @@ class RoomDetailsPresenterTests {
@Test @Test
fun `present - Leave without confirmation leaves the room`() = runTest { fun `present - Leave without confirmation leaves the room`() = runTest {
val room = aMatrixRoom() val room = aMatrixRoom().apply {
val presenter = RoomDetailsPresenter(room, roomMembershipObserver) givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList()))
}
val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
// Allow room member count to load
skipItems(1)
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false))
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
@ -188,14 +185,11 @@ class RoomDetailsPresenterTests {
val room = aMatrixRoom().apply { val room = aMatrixRoom().apply {
givenLeaveRoomError(Throwable()) givenLeaveRoomError(Throwable())
} }
val presenter = RoomDetailsPresenter(room, roomMembershipObserver) val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
// Allow room member count to load
skipItems(1)
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false))
val errorState = awaitItem() val errorState = awaitItem()
Truth.assertThat(errorState.error).isNotNull() Truth.assertThat(errorState.error).isNotNull()
@ -211,13 +205,11 @@ fun aMatrixRoom(
displayName: String = "A fallback display name", displayName: String = "A fallback display name",
topic: String? = "A topic", topic: String? = "A topic",
avatarUrl: String? = "https://matrix.org/avatar.jpg", avatarUrl: String? = "https://matrix.org/avatar.jpg",
members: List<RoomMember> = emptyList(),
isEncrypted: Boolean = true, isEncrypted: Boolean = true,
isPublic: Boolean = true, isPublic: Boolean = true,
) = FakeMatrixRoom( ) = FakeMatrixRoom(
roomId = roomId, roomId = roomId,
name = name, name = name,
initialMembers = members,
displayName = displayName, displayName = displayName,
topic = topic, topic = topic,
avatarUrl = avatarUrl, avatarUrl = avatarUrl,

View file

@ -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.impl.DefaultUserListPresenter
import io.element.android.features.userlist.test.FakeUserListDataSource import io.element.android.features.userlist.test.FakeUserListDataSource
import io.element.android.libraries.architecture.Async 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.libraries.matrix.ui.components.aMatrixUser
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import okhttp3.internal.toImmutableList import okhttp3.internal.toImmutableList
import org.junit.Test import org.junit.Test
@ -38,6 +42,8 @@ import org.junit.Test
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
class RoomMemberListPresenterTests { class RoomMemberListPresenterTests {
private val testCoroutineDispatchers = testCoroutineDispatchers()
@Test @Test
fun `present - search is done automatically on start, but is async`() = runTest { fun `present - search is done automatically on start, but is async`() = runTest {
val searchResult = listOf(aMatrixUser()) val searchResult = listOf(aMatrixUser())
@ -52,7 +58,14 @@ class RoomMemberListPresenterTests {
userListDataStore: UserListDataStore, userListDataStore: UserListDataStore,
) = DefaultUserListPresenter(args, userListDataSource, 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) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {

View file

@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.matrix.ui.model.MatrixUser
interface UserListDataSource { interface UserListDataSource {
//TODO should probably have a flow
suspend fun search(query: String): List<MatrixUser> suspend fun search(query: String): List<MatrixUser>
suspend fun getProfile(userId: UserId): MatrixUser? suspend fun getProfile(userId: UserId): MatrixUser?
} }

View file

@ -47,7 +47,9 @@ suspend fun <T> (suspend () -> T).execute(state: MutableState<Async<T>>, errorMa
} }
suspend fun <T> (suspend () -> Result<T>).executeResult(state: MutableState<Async<T>>) { suspend fun <T> (suspend () -> Result<T>).executeResult(state: MutableState<Async<T>>) {
state.value = Async.Loading() if (state.value !is Async.Success) {
state.value = Async.Loading()
}
this().fold( this().fold(
onSuccess = { onSuccess = {
state.value = Async.Success(it) state.value = Async.Success(it)

View file

@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import java.io.Closeable import java.io.Closeable
interface MatrixRoom : Closeable { interface MatrixRoom : Closeable {
@ -41,10 +42,10 @@ interface MatrixRoom : Closeable {
/** /**
* The current loaded members as a StateFlow. * The current loaded members as a StateFlow.
* Initial value is an emptyList. * Initial value is [MatrixRoomMembersState.Unknown].
* To update them you should call [updateMembers]. * To update them you should call [updateMembers].
*/ */
val membersFlow: StateFlow<List<RoomMember>> val membersStateFlow: StateFlow<MatrixRoomMembersState>
/** /**
* Try to load the room members and update the membersFlow. * Try to load the room members and update the membersFlow.
@ -70,18 +71,21 @@ interface MatrixRoom : Closeable {
suspend fun leave(): Result<Unit> suspend fun leave(): Result<Unit>
} }
fun MatrixRoom.getMember(userId: UserId): RoomMember? { fun MatrixRoom.getMemberFlow(userId: UserId): Flow<RoomMember?> {
return membersFlow.value.find { it.userId == userId } return membersStateFlow.map { state ->
} state.roomMembers().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.memberCount(): Int { fun MatrixRoom.getDmMemberFlow(): Flow<RoomMember?> {
return membersFlow.value.size return membersStateFlow.map { state ->
val members = state.roomMembers()
if (members.size == 2 && isDirect && isEncrypted) {
members.find { it.userId != this.sessionId }
} else {
null
}
}
} }

View file

@ -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<RoomMember>) : MatrixRoomMembersState
}
fun MatrixRoomMembersState.roomMembers(): List<RoomMember> {
return when (this) {
is MatrixRoomMembersState.Ready -> roomMembers
else -> emptyList()
}
}

View file

@ -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.SessionId
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.MatrixRoom 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.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -48,10 +48,10 @@ class RustMatrixRoom(
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
) : MatrixRoom { ) : MatrixRoom {
override val membersFlow: StateFlow<List<RoomMember>> override val membersStateFlow: StateFlow<MatrixRoomMembersState>
get() = cachedMembers get() = _membersStateFlow
private var cachedMembers = MutableStateFlow<List<RoomMember>>(emptyList()) private var _membersStateFlow = MutableStateFlow<MatrixRoomMembersState>(MatrixRoomMembersState.Unknown)
override fun syncUpdateFlow(): Flow<Long> { override fun syncUpdateFlow(): Flow<Long> {
return slidingSyncUpdateFlow return slidingSyncUpdateFlow
@ -122,9 +122,14 @@ class RustMatrixRoom(
get() = innerRoom.isDirect() get() = innerRoom.isDirect()
override suspend fun updateMembers(): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun updateMembers(): Result<Unit> = withContext(coroutineDispatchers.io) {
_membersStateFlow.value = MatrixRoomMembersState.Pending
runCatching { 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<String?> = override suspend fun userDisplayName(userId: UserId): Result<String?> =

View file

@ -143,12 +143,15 @@ class RustMatrixTimeline(
requiredState = listOf( requiredState = listOf(
RequiredState(key = "m.room.canonical_alias", value = ""), RequiredState(key = "m.room.canonical_alias", value = ""),
RequiredState(key = "m.room.topic", value = ""), RequiredState(key = "m.room.topic", value = ""),
RequiredState(key = "m.room.name", value = ""),
RequiredState(key = "m.room.join_rule", value = ""), RequiredState(key = "m.room.join_rule", value = ""),
), ),
timelineLimit = 20.toUInt() timelineLimit = 20.toUInt()
) )
val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings) val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings)
fetchMembers() launch {
fetchMembers()
}
listenerTokens += result.taskHandle listenerTokens += result.taskHandle
result.items result.items
} }

View file

@ -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.SessionId
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.MatrixRoom 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.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
@ -44,18 +44,16 @@ class FakeMatrixRoom(
override val alternativeAliases: List<String> = emptyList(), override val alternativeAliases: List<String> = emptyList(),
override val isPublic: Boolean = true, override val isPublic: Boolean = true,
override val isDirect: Boolean = false, override val isDirect: Boolean = false,
initialMembers: List<RoomMember> = emptyList(),
private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(), private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(),
) : MatrixRoom { ) : MatrixRoom {
private var userDisplayNameResult = Result.success<String?>(null) private var userDisplayNameResult = Result.success<String?>(null)
private var userAvatarUrlResult = Result.success<String?>(null) private var userAvatarUrlResult = Result.success<String?>(null)
private var dmMember: RoomMember? = null
private var updateMembersResult: Result<Unit> = Result.success(Unit) private var updateMembersResult: Result<Unit> = Result.success(Unit)
private var leaveRoomError: Throwable? = null private var leaveRoomError: Throwable? = null
override val membersFlow: MutableStateFlow<List<RoomMember>> = MutableStateFlow(initialMembers) override val membersStateFlow: MutableStateFlow<MatrixRoomMembersState> = MutableStateFlow(MatrixRoomMembersState.Unknown)
override suspend fun updateMembers(): Result<Unit> { override suspend fun updateMembers(): Result<Unit> {
return updateMembersResult return updateMembersResult
@ -117,12 +115,12 @@ class FakeMatrixRoom(
this.leaveRoomError = throwable this.leaveRoomError = throwable
} }
fun givenFetchMemberResult(result: Result<Unit>) { fun givenRoomMembersState(state: MatrixRoomMembersState) {
updateMembersResult = result membersStateFlow.value = state
} }
fun givenDmMember(roomMember: RoomMember) { fun givenUpdateMembersResult(result: Result<Unit>) {
this.dmMember = roomMember updateMembersResult = result
} }
fun givenUserDisplayNameResult(displayName: Result<String?>) { fun givenUserDisplayNameResult(displayName: Result<String?>) {

View file

@ -34,4 +34,6 @@ dependencies {
implementation(libs.coroutines.test) implementation(libs.coroutines.test)
implementation(projects.libraries.matrix.test) implementation(projects.libraries.matrix.test)
implementation(projects.services.appnavstate.test) implementation(projects.services.appnavstate.test)
implementation(projects.services.appnavstate.test)
implementation(projects.libraries.core)
} }

View file

@ -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,
)