From b50350aaa021dc6948403c8832390d0657eadf91 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 1 Jun 2023 12:03:27 +0200 Subject: [PATCH] [Room details] Open room member details when clicking on user data in timeline (#482) --- .../io/element/android/appnav/RoomFlowNode.kt | 15 ++++++++++++++- .../element/android/appnav/RoomFlowNodeTest.kt | 7 ++++++- changelog.d/480.feature | 1 + .../features/messages/api/MessagesEntryPoint.kt | 2 ++ .../features/messages/impl/MessagesFlowNode.kt | 5 +++++ .../features/messages/impl/MessagesNode.kt | 7 +++++++ .../features/messages/impl/MessagesView.kt | 8 +++++++- .../messages/impl/timeline/TimelineView.kt | 14 ++++++++++++++ features/roomdetails/api/build.gradle.kts | 1 + .../roomdetails/api/RoomDetailsEntryPoint.kt | 17 ++++++++++++++++- .../impl/DefaultRoomDetailsEntryPoint.kt | 16 ++++++++++++++-- .../roomdetails/impl/RoomDetailsFlowNode.kt | 6 ++++-- .../members/details/RoomMemberDetailsNode.kt | 6 +++--- .../details/RoomMemberDetailsPresenter.kt | 4 ++-- .../matrix/ui/room/MatrixRoomMembers.kt | 6 +++--- 15 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 changelog.d/480.feature 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 0493ae7db1..fb3a8d566e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -39,6 +39,7 @@ import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope +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.RoomMembershipObserver import io.element.android.services.appnavstate.api.AppNavigationStateService @@ -119,11 +120,20 @@ class RoomFlowNode @AssistedInject constructor( override fun onRoomDetailsClicked() { backstack.push(NavTarget.RoomDetails) } + + override fun onUserDataClicked(userId: UserId) { + backstack.push(NavTarget.RoomMemberDetails(userId)) + } } messagesEntryPoint.createNode(this, buildContext, callback) } NavTarget.RoomDetails -> { - roomDetailsEntryPoint.createNode(this, buildContext, emptyList()) + val inputs = RoomDetailsEntryPoint.Inputs(RoomDetailsEntryPoint.InitialTarget.RoomDetails) + roomDetailsEntryPoint.createNode(this, buildContext, inputs, emptyList()) + } + is NavTarget.RoomMemberDetails -> { + val inputs = RoomDetailsEntryPoint.Inputs(RoomDetailsEntryPoint.InitialTarget.RoomMemberDetails(navTarget.userId)) + roomDetailsEntryPoint.createNode(this, buildContext, inputs, emptyList()) } } } @@ -134,6 +144,9 @@ class RoomFlowNode @AssistedInject constructor( @Parcelize object RoomDetails : NavTarget + + @Parcelize + data class RoomMemberDetails(val userId: UserId) : NavTarget } private val timeline = inputs.room.timeline() diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt index a151be665c..daff06af16 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt @@ -60,7 +60,12 @@ class RoomFlowNodeTest { var nodeId: String? = null - override fun createNode(parentNode: Node, buildContext: BuildContext, plugins: List): Node { + override fun createNode( + parentNode: Node, + buildContext: BuildContext, + inputs: RoomDetailsEntryPoint.Inputs, + plugins: List + ): Node { return node(buildContext) {}.also { nodeId = it.id } diff --git a/changelog.d/480.feature b/changelog.d/480.feature new file mode 100644 index 0000000000..e64dcc1f33 --- /dev/null +++ b/changelog.d/480.feature @@ -0,0 +1 @@ +Open room member details when tapping on a user in the timeline diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt index 9f15a77f4c..f1ed5c18dd 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt @@ -20,6 +20,7 @@ import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.matrix.api.core.UserId interface MessagesEntryPoint : FeatureEntryPoint { fun createNode( @@ -30,5 +31,6 @@ interface MessagesEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onRoomDetailsClicked() + fun onUserDataClicked(userId: UserId) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 1e10a553f1..00e74b026c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -39,6 +39,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaSource import kotlinx.collections.immutable.ImmutableList import kotlinx.parcelize.Parcelize @@ -89,6 +90,10 @@ class MessagesFlowNode @AssistedInject constructor( override fun onPreviewAttachments(attachments: ImmutableList) { backstack.push(NavTarget.AttachmentPreview(attachments.first())) } + + override fun onUserDataClicked(userId: UserId) { + callback?.onUserDataClicked(userId) + } } createNode(buildContext, listOf(callback)) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 626b3bd682..96f76d3a34 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -28,6 +28,7 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.UserId import kotlinx.collections.immutable.ImmutableList @ContributesNode(RoomScope::class) @@ -43,6 +44,7 @@ class MessagesNode @AssistedInject constructor( fun onRoomDetailsClicked() fun onEventClicked(event: TimelineItem.Event) fun onPreviewAttachments(attachments: ImmutableList) + fun onUserDataClicked(userId: UserId) } private fun onRoomDetailsClicked() { @@ -57,6 +59,10 @@ class MessagesNode @AssistedInject constructor( callback?.onPreviewAttachments(attachments) } + private fun onUserDataClicked(userId: UserId) { + callback?.onUserDataClicked(userId) + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -66,6 +72,7 @@ class MessagesNode @AssistedInject constructor( onRoomDetailsClicked = this::onRoomDetailsClicked, onEventClicked = this::onEventClicked, onPreviewAttachments = this::onPreviewAttachments, + onUserDataClicked = this::onUserDataClicked, modifier = modifier, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 8d08b77a49..3aca34ee2b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -85,6 +85,7 @@ 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.TopAppBar import io.element.android.libraries.designsystem.utils.LogCompositions +import io.element.android.libraries.matrix.api.core.UserId import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch import timber.log.Timber @@ -97,6 +98,7 @@ fun MessagesView( onBackPressed: () -> Unit, onRoomDetailsClicked: () -> Unit, onEventClicked: (event: TimelineItem.Event) -> Unit, + onUserDataClicked: (UserId) -> Unit, onPreviewAttachments: (ImmutableList) -> Unit, modifier: Modifier = Modifier, ) { @@ -203,6 +205,7 @@ fun MessagesView( .consumeWindowInsets(padding), onMessageClicked = ::onMessageClicked, onMessageLongClicked = ::onMessageLongClicked, + onUserDataClicked = onUserDataClicked, ) }, snackbarHost = { @@ -240,6 +243,7 @@ fun MessagesViewContent( state: MessagesState, modifier: Modifier = Modifier, onMessageClicked: (TimelineItem.Event) -> Unit = {}, + onUserDataClicked: (UserId) -> Unit = {}, onMessageLongClicked: (TimelineItem.Event) -> Unit = {}, ) { Column( @@ -255,6 +259,7 @@ fun MessagesViewContent( modifier = Modifier.weight(1f), onMessageClicked = onMessageClicked, onMessageLongClicked = onMessageLongClicked, + onUserDataClicked = onUserDataClicked, ) } MessageComposerView( @@ -354,6 +359,7 @@ private fun ContentToPreview(state: MessagesState) { onBackPressed = {}, onRoomDetailsClicked = {}, onEventClicked = {}, - onPreviewAttachments = {} + onPreviewAttachments = {}, + onUserDataClicked = {}, ) } 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 cf7c3c9bf1..f5ebdcc796 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 @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope @@ -87,6 +88,7 @@ import io.element.android.libraries.designsystem.theme.LocalColors import io.element.android.libraries.designsystem.theme.components.FloatingActionButton import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.core.UserId import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @@ -95,6 +97,7 @@ import kotlinx.coroutines.launch fun TimelineView( state: TimelineState, modifier: Modifier = Modifier, + onUserDataClicked: (UserId) -> Unit = {}, onMessageClicked: (TimelineItem.Event) -> Unit = {}, onMessageLongClicked: (TimelineItem.Event) -> Unit = {}, ) { @@ -120,6 +123,7 @@ fun TimelineView( highlightedItem = state.highlightedEventId?.value, onClick = onMessageClicked, onLongClick = onMessageLongClicked, + onUserDataClick = onUserDataClicked, ) if (index == state.timelineItems.lastIndex) { onReachedLoadMore() @@ -139,6 +143,7 @@ fun TimelineView( fun TimelineItemRow( timelineItem: TimelineItem, highlightedItem: String?, + onUserDataClick: (UserId) -> Unit, onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, modifier: Modifier = Modifier @@ -173,6 +178,7 @@ fun TimelineItemRow( isHighlighted = highlightedItem == timelineItem.identifier(), onClick = ::onClick, onLongClick = ::onLongClick, + onUserDataClick = onUserDataClick, modifier = modifier, ) } @@ -203,6 +209,7 @@ fun TimelineItemRow( highlightedItem = highlightedItem, onClick = onClick, onLongClick = onLongClick, + onUserDataClick = onUserDataClick, ) } } @@ -230,15 +237,21 @@ fun TimelineItemEventRow( isHighlighted: Boolean, onClick: () -> Unit, onLongClick: () -> Unit, + onUserDataClick: (UserId) -> Unit, modifier: Modifier = Modifier ) { val interactionSource = remember { MutableInteractionSource() } + fun onUserDataClicked() { + onUserDataClick(event.senderId) + } + val (parentAlignment, contentAlignment) = if (event.isMine) { Pair(Alignment.CenterEnd, Alignment.End) } else { Pair(Alignment.CenterStart, Alignment.Start) } + Box( modifier = modifier .fillMaxWidth() @@ -257,6 +270,7 @@ fun TimelineItemEventRow( Modifier .zIndex(1f) .offset(y = 12.dp) + .clickable(onClick = ::onUserDataClicked) ) } val bubbleState = BubbleState( diff --git a/features/roomdetails/api/build.gradle.kts b/features/roomdetails/api/build.gradle.kts index c93ec69f89..ddc062cb3b 100644 --- a/features/roomdetails/api/build.gradle.kts +++ b/features/roomdetails/api/build.gradle.kts @@ -16,6 +16,7 @@ plugins { id("io.element.android-library") + id("kotlin-parcelize") } android { diff --git a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt index 560e9d5dd7..e73d63f38c 100644 --- a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt +++ b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt @@ -16,11 +16,26 @@ package io.element.android.features.roomdetails.api +import android.os.Parcelable import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.parcelize.Parcelize interface RoomDetailsEntryPoint : FeatureEntryPoint { - fun createNode(parentNode: Node, buildContext: BuildContext, plugins: List): Node + + sealed interface InitialTarget : Parcelable { + @Parcelize + object RoomDetails : InitialTarget + + @Parcelize + data class RoomMemberDetails(val roomMemberId: UserId) : InitialTarget + } + + data class Inputs(val initialElement: InitialTarget) : NodeInputs + + fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs, plugins: List): Node } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt index eebdbea062..be6b915212 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt @@ -21,13 +21,25 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint +import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint.InitialTarget +import io.element.android.features.roomdetails.impl.RoomDetailsFlowNode.NavTarget import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.AppScope import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultRoomDetailsEntryPoint @Inject constructor() : RoomDetailsEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext, plugins: List): Node { - return parentNode.createNode(buildContext, plugins) + override fun createNode( + parentNode: Node, + buildContext: BuildContext, + inputs: RoomDetailsEntryPoint.Inputs, + plugins: List + ): Node { + return parentNode.createNode(buildContext, plugins + inputs) } } + +internal fun InitialTarget.toNavTarget() = when (this) { + is InitialTarget.RoomDetails -> NavTarget.RoomDetails + is InitialTarget.RoomMemberDetails -> NavTarget.RoomMemberDetails(roomMemberId) +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 97fb3311cb..02783775bf 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -28,6 +28,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode import io.element.android.features.roomdetails.impl.members.RoomMemberListNode import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode @@ -44,7 +45,7 @@ class RoomDetailsFlowNode @AssistedInject constructor( @Assisted plugins: List, ) : BackstackNode( backstack = BackStack( - initialElement = NavTarget.RoomDetails, + initialElement = plugins.filterIsInstance().first().initialElement.toNavTarget(), savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -95,7 +96,8 @@ class RoomDetailsFlowNode @AssistedInject constructor( createNode(buildContext) } is NavTarget.RoomMemberDetails -> { - createNode(buildContext, listOf(RoomMemberDetailsNode.Inputs(navTarget.roomMemberId))) + val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId)) + createNode(buildContext, plugins) } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt index 7fd4dd3876..bd4258ac88 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt @@ -42,11 +42,11 @@ class RoomMemberDetailsNode @AssistedInject constructor( presenterFactory: RoomMemberDetailsPresenter.Factory, ) : Node(buildContext, plugins = plugins) { - data class Inputs( - val roomMemberId: UserId, + data class RoomMemberDetailsInput( + val roomMemberId: UserId ) : NodeInputs - private val inputs = inputs() + private val inputs = inputs() private val presenter = presenterFactory.create(inputs.roomMemberId) @Composable diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt index 594152e241..7ef6cd4aa4 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt @@ -33,7 +33,7 @@ import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.ui.room.getRoomMember +import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -51,7 +51,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( override fun present(): RoomMemberDetailsState { val coroutineScope = rememberCoroutineScope() var confirmationDialog by remember { mutableStateOf(null) } - val roomMember by room.getRoomMember(roomMemberId) + val roomMember by room.getRoomMemberAsState(roomMemberId) // the room member is not really live... val isBlocked = remember { mutableStateOf(roomMember?.isIgnored.orFalse()) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt index 061ce365eb..5980bb138f 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt @@ -29,13 +29,13 @@ import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.roomMembers @Composable -fun MatrixRoom.getRoomMember(userId: UserId): State { +fun MatrixRoom.getRoomMemberAsState(userId: UserId): State { val roomMembersState by membersStateFlow.collectAsState() - return getRoomMember(roomMembersState = roomMembersState, userId = userId) + return getRoomMemberAsState(roomMembersState = roomMembersState, userId = userId) } @Composable -fun getRoomMember(roomMembersState: MatrixRoomMembersState, userId: UserId): State { +fun getRoomMemberAsState(roomMembersState: MatrixRoomMembersState, userId: UserId): State { val roomMembers = roomMembersState.roomMembers() return remember(roomMembers) { derivedStateOf {