RoomMembers: change the API again..

This commit is contained in:
ganfra 2023-04-28 15:04:33 +02:00
parent 16a43befcc
commit d7cd8a2915
12 changed files with 109 additions and 73 deletions

View file

@ -17,6 +17,7 @@
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.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -33,7 +34,7 @@ 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.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.getDmMemberFlow import io.element.android.libraries.matrix.ui.room.directRoomMember
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -54,14 +55,14 @@ class RoomDetailsPresenter @Inject constructor(
val error = remember { val error = remember {
mutableStateOf<RoomDetailsError?>(null) mutableStateOf<RoomDetailsError?>(null)
} }
LaunchedEffect(Unit) {
room.updateMembers()
}
val membersState by room.membersStateFlow.collectAsState() val membersState by room.membersStateFlow.collectAsState()
val memberCount by getMemberCount(membersState) val memberCount by getMemberCount(membersState)
val dmMemberState by room.getDmMemberFlow() val dmMember by room.directRoomMember()
.collectAsState(initial = null, context = coroutineDispatchers.computation) val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember)
val roomType = getRoomType(dmMember)
val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMemberState)
val roomType = getRoomType(dmMemberState)
fun handleEvents(event: RoomDetailsEvent) { fun handleEvents(event: RoomDetailsEvent) {
when (event) { when (event) {
@ -119,9 +120,9 @@ class RoomDetailsPresenter @Inject constructor(
derivedStateOf { derivedStateOf {
when (membersState) { when (membersState) {
MatrixRoomMembersState.Unknown -> Async.Uninitialized MatrixRoomMembersState.Unknown -> Async.Uninitialized
MatrixRoomMembersState.Pending -> Async.Loading() is MatrixRoomMembersState.Pending -> Async.Loading(prevState = membersState.prevRoomMembers?.size)
is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure, prevState = membersState.prevRoomMembers?.size)
is MatrixRoomMembersState.Ready -> Async.Success(membersState.roomMembers.size) is MatrixRoomMembersState.Ready -> Async.Success(membersState.roomMembers.size)
is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure)
} }
} }
} }

View file

@ -117,7 +117,7 @@ fun RoomDetailsView(
} }
if (state.roomType is RoomDetailsType.Room) { if (state.roomType is RoomDetailsType.Room) {
val memberCount = (state.memberCount as? Async.Success<Int>)?.state val memberCount = state.memberCount.dataOrNull()
MembersSection( MembersSection(
memberCount = memberCount, memberCount = memberCount,
isLoading = state.memberCount.isLoading(), isLoading = state.memberCount.isLoading(),

View file

@ -18,7 +18,6 @@ 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 io.element.android.features.userlist.api.SelectionMode import io.element.android.features.userlist.api.SelectionMode
@ -30,14 +29,9 @@ 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.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.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.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
@ -74,6 +68,5 @@ class RoomMemberListPresenter @Inject constructor(
userListState = userListState, userListState = userListState,
) )
} }
} }

View file

@ -41,6 +41,7 @@ class RoomUserListDataSource @Inject constructor(
.dropWhile { it !is MatrixRoomMembersState.Ready } .dropWhile { it !is MatrixRoomMembersState.Ready }
.first() .first()
.roomMembers() .roomMembers()
.orEmpty()
val filteredMembers = if (query.isBlank()) { val filteredMembers = if (query.isBlank()) {
roomMembers roomMembers
} else { } else {

View file

@ -17,8 +17,8 @@
package io.element.android.features.roomdetails.impl.members.details package io.element.android.features.roomdetails.impl.members.details
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.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
@ -33,9 +33,10 @@ import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
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.getMemberFlow import io.element.android.libraries.matrix.ui.room.roomMember
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
class RoomMemberDetailsPresenter @AssistedInject constructor( class RoomMemberDetailsPresenter @AssistedInject constructor(
private val client: MatrixClient, private val client: MatrixClient,
@ -51,11 +52,14 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
override fun present(): RoomMemberDetailsState { override fun present(): RoomMemberDetailsState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) } var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
val roomMember by room.getMemberFlow(roomMemberId).collectAsState(initial = null) val roomMember by room.roomMember(roomMemberId)
// the room member is not really live...
val isBlocked = remember(roomMember?.isIgnored) { val isBlocked = remember {
mutableStateOf(roomMember?.isIgnored.orFalse()) mutableStateOf(roomMember?.isIgnored.orFalse())
} }
LaunchedEffect(Unit) {
room.updateMembers()
}
fun handleEvents(event: RoomMemberDetailsEvents) { fun handleEvents(event: RoomMemberDetailsEvents) {
when (event) { when (event) {

View file

@ -96,6 +96,7 @@ class RoomDetailsPresenterTests {
room.givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList())) 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))
cancelAndIgnoreRemainingEvents()
} }
} }
@ -129,12 +130,7 @@ class RoomDetailsPresenterTests {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
// It's not configured yet in the first iteration Truth.assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Dm(otherRoomMember))
Truth.assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Room)
// Once updated, the RoomDetailsType becomes 'Dm'
val updatedState = awaitItem()
Truth.assertThat(updatedState.roomType).isEqualTo(RoomDetailsType.Dm(otherRoomMember))
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }

View file

@ -26,6 +26,7 @@ import io.element.android.features.roomdetails.aRoomMember
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -37,12 +38,13 @@ class RoomMemberDetailsPresenterTests {
@Test @Test
fun `present - returns the room member's data, then updates it if needed`() = runTest { fun `present - returns the room member's data, then updates it if needed`() = runTest {
val roomMember = aRoomMember(displayName = "Alice")
val room = aMatrixRoom().apply { val room = aMatrixRoom().apply {
givenUserDisplayNameResult(Result.success("A custom name")) givenUserDisplayNameResult(Result.success("A custom name"))
givenUserAvatarUrlResult(Result.success("A custom avatar")) givenUserAvatarUrlResult(Result.success("A custom avatar"))
givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember)))
} }
val roomMember = aRoomMember(displayName = "Alice") val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId)
val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -60,12 +62,13 @@ class RoomMemberDetailsPresenterTests {
@Test @Test
fun `present - will recover when retrieving room member details fails`() = runTest { fun `present - will recover when retrieving room member details fails`() = runTest {
val roomMember = aRoomMember(displayName = "Alice")
val room = aMatrixRoom().apply { val room = aMatrixRoom().apply {
givenUserDisplayNameResult(Result.failure(Throwable())) givenUserDisplayNameResult(Result.failure(Throwable()))
givenUserAvatarUrlResult(Result.failure(Throwable())) givenUserAvatarUrlResult(Result.failure(Throwable()))
givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember)))
} }
val roomMember = aRoomMember(displayName = "Alice") val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId)
val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -79,12 +82,13 @@ class RoomMemberDetailsPresenterTests {
@Test @Test
fun `present - will fallback to original data if the updated data is null`() = runTest { fun `present - will fallback to original data if the updated data is null`() = runTest {
val roomMember = aRoomMember(displayName = "Alice")
val room = aMatrixRoom().apply { val room = aMatrixRoom().apply {
givenUserDisplayNameResult(Result.success(null)) givenUserDisplayNameResult(Result.success(null))
givenUserAvatarUrlResult(Result.success(null)) givenUserAvatarUrlResult(Result.success(null))
givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember)))
} }
val roomMember = aRoomMember(displayName = "Alice") val presenter =RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId)
val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -100,7 +104,7 @@ class RoomMemberDetailsPresenterTests {
fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest { fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest {
val room = aMatrixRoom() val room = aMatrixRoom()
val roomMember = aRoomMember() val roomMember = aRoomMember()
val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember) val presenter =RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -121,7 +125,7 @@ class RoomMemberDetailsPresenterTests {
fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest { fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest {
val room = aMatrixRoom() val room = aMatrixRoom()
val roomMember = aRoomMember() val roomMember = aRoomMember()
val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember) val presenter =RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -138,7 +142,7 @@ class RoomMemberDetailsPresenterTests {
fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest { fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest {
val room = aMatrixRoom() val room = aMatrixRoom()
val roomMember = aRoomMember() val roomMember = aRoomMember()
val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember) val presenter =RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {

View file

@ -23,7 +23,6 @@ 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 {
@ -74,22 +73,3 @@ interface MatrixRoom : Closeable {
suspend fun rejectInvitation(): Result<Unit> suspend fun rejectInvitation(): Result<Unit>
} }
fun MatrixRoom.getMemberFlow(userId: UserId): Flow<RoomMember?> {
return membersStateFlow.map { state ->
state.roomMembers().find {
it.userId == userId
}
}
}
fun MatrixRoom.getDmMemberFlow(): Flow<RoomMember?> {
return membersStateFlow.map { state ->
val members = state.roomMembers()
if (members.size == 2 && isDirect && isEncrypted) {
members.find { it.userId != this.sessionId }
} else {
null
}
}
}

View file

@ -18,14 +18,18 @@ package io.element.android.libraries.matrix.api.room
sealed interface MatrixRoomMembersState { sealed interface MatrixRoomMembersState {
object Unknown : MatrixRoomMembersState object Unknown : MatrixRoomMembersState
object Pending : MatrixRoomMembersState data class Pending(val prevRoomMembers: List<RoomMember>? = null) : MatrixRoomMembersState
data class Error(val failure: Throwable) : MatrixRoomMembersState data class Error(val failure: Throwable, val prevRoomMembers: List<RoomMember>? = null) : MatrixRoomMembersState
data class Ready(val roomMembers: List<RoomMember>) : MatrixRoomMembersState data class Ready(val roomMembers: List<RoomMember>) : MatrixRoomMembersState
} }
fun MatrixRoomMembersState.roomMembers(): List<RoomMember> { fun MatrixRoomMembersState.roomMembers(): List<RoomMember>? {
return when (this) { return when (this) {
is MatrixRoomMembersState.Ready -> roomMembers is MatrixRoomMembersState.Ready -> roomMembers
else -> emptyList() is MatrixRoomMembersState.Pending -> prevRoomMembers
is MatrixRoomMembersState.Error -> prevRoomMembers
else -> null
} }
} }

View file

@ -23,6 +23,7 @@ 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.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.roomMembers
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
@ -32,14 +33,13 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.SlidingSyncRoom import org.matrix.rustcomponents.sdk.SlidingSyncRoom
import org.matrix.rustcomponents.sdk.UpdateSummary import org.matrix.rustcomponents.sdk.UpdateSummary
import org.matrix.rustcomponents.sdk.genTransactionId import org.matrix.rustcomponents.sdk.genTransactionId
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
import org.matrix.rustcomponents.sdk.use
import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember
class RustMatrixRoom( class RustMatrixRoom(
override val sessionId: SessionId, override val sessionId: SessionId,
@ -128,13 +128,15 @@ 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 val currentState = _membersStateFlow.value
val currentMembers = currentState.roomMembers()
_membersStateFlow.value = MatrixRoomMembersState.Pending(prevRoomMembers = currentMembers)
runCatching { runCatching {
innerRoom.members().map(RoomMemberMapper::map) innerRoom.members().map(RoomMemberMapper::map)
}.map { }.map {
_membersStateFlow.value = MatrixRoomMembersState.Ready(it) _membersStateFlow.value = MatrixRoomMembersState.Ready(it)
}.onFailure { }.onFailure {
_membersStateFlow.value = MatrixRoomMembersState.Error(it) _membersStateFlow.value = MatrixRoomMembersState.Error(prevRoomMembers = currentMembers, failure = it)
} }
} }
@ -201,10 +203,4 @@ class RustMatrixRoom(
} }
} }
private fun findRoomMember(userId: UserId, action: (RustRoomMember).() -> Unit) {
return innerRoom.members()
.find { it.userId() == userId.value }
?.use(action)
?: error("No member with userId $userId exists in room $roomId")
}
} }

View file

@ -153,8 +153,7 @@ class RustMatrixTimeline(
RequiredState(key = "m.room.topic", value = ""), RequiredState(key = "m.room.topic", value = ""),
RequiredState(key = "m.room.join_rules", value = ""), RequiredState(key = "m.room.join_rules", value = ""),
), ),
//TODO allow configuration timelineLimit = null
timelineLimit = 20.toUInt()
) )
val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings) val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings)
launch { launch {

View file

@ -0,0 +1,58 @@
/*
* 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.ui.room
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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.roomMembers
@Composable
fun MatrixRoom.roomMember(userId: UserId): State<RoomMember?> {
val roomMembersState by membersStateFlow.collectAsState()
val roomMembers = roomMembersState.roomMembers()
return remember(roomMembers) {
derivedStateOf {
roomMembers?.find {
it.userId == userId
}
}
}
}
@Composable
fun MatrixRoom.directRoomMember(): State<RoomMember?> {
val roomMembersState by membersStateFlow.collectAsState()
val roomMembers = roomMembersState.roomMembers()
return remember(roomMembers) {
derivedStateOf {
if (roomMembers == null) {
null
} else if (roomMembers.size == 2 && isDirect && isEncrypted) {
roomMembers.find { it.userId != this.sessionId }
} else {
null
}
}
}
}