From dcaac703fc99d982ef328853323d92e46c800206 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 20 Nov 2025 16:41:43 +0100 Subject: [PATCH] change(members): make sure state is not lost when navigating --- .../impl/members/RoomMemberListEvents.kt | 1 + .../impl/members/RoomMemberListNode.kt | 6 +- .../impl/members/RoomMemberListPresenter.kt | 18 +- .../impl/members/RoomMemberListState.kt | 6 + .../members/RoomMemberListStateProvider.kt | 207 ++++++++++-------- .../impl/members/RoomMemberListView.kt | 40 +--- 6 files changed, 153 insertions(+), 125 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt index 00332e391a..9a8798124e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt @@ -11,6 +11,7 @@ package io.element.android.features.roomdetails.impl.members import io.element.android.libraries.matrix.api.room.RoomMember sealed interface RoomMemberListEvents { + data class ChangeSelectedSection(val section: SelectedSection): RoomMemberListEvents data class UpdateSearchQuery(val query: String) : RoomMemberListEvents data class OnSearchActiveChanged(val active: Boolean) : RoomMemberListEvents data class RoomMemberSelected(val roomMember: RoomMember) : RoomMemberListEvents diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt index 4d915aa294..750b111fc3 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -9,6 +9,8 @@ package io.element.android.features.roomdetails.impl.members import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext @@ -21,6 +23,7 @@ import io.element.android.annotations.ContributesNode import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents import io.element.android.features.roommembermoderation.api.RoomMemberModerationRenderer +import io.element.android.libraries.architecture.appyx.launchMolecule import io.element.android.libraries.architecture.callback import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.UserId @@ -41,6 +44,7 @@ class RoomMemberListNode( } private val callback: Callback = callback() + private val stateFlow = launchMolecule { presenter.present() } init { lifecycle.subscribe( @@ -64,7 +68,7 @@ class RoomMemberListNode( @Composable override fun View(modifier: Modifier) { - val state = presenter.present() + val state by stateFlow.collectAsState() RoomMemberListView( state = state, modifier = modifier, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index e69ddb744a..65385e5f08 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -10,6 +10,7 @@ package io.element.android.features.roomdetails.impl.members import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -18,7 +19,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject -import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents.ShowActionsForUser import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -53,7 +54,7 @@ class RoomMemberListPresenter( private val roomMembersModerationPresenter: Presenter, private val encryptionService: EncryptionService, ) : Presenter { - private var roomMembers: AsyncData by mutableStateOf(AsyncData.Loading()) + private val powerLevelRoomMemberComparator = PowerLevelRoomMemberComparator() @Composable @@ -77,6 +78,9 @@ class RoomMemberListPresenter( .launchIn(this) } + var roomMembers: AsyncData by remember { mutableStateOf(AsyncData.Loading())} + var selectedSection by remember { mutableStateOf(SelectedSection.MEMBERS)} + // Update the room members when the screen is loaded LaunchedEffect(Unit) { room.updateMembers() @@ -160,7 +164,14 @@ class RoomMemberListPresenter( is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query is RoomMemberListEvents.RoomMemberSelected -> - roomModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.roomMember.toMatrixUser())) + roomModerationState.eventSink(ShowActionsForUser(event.roomMember.toMatrixUser())) + is RoomMemberListEvents.ChangeSelectedSection -> selectedSection = event.section + } + } + + if (!roomModerationState.canBan && selectedSection == SelectedSection.BANNED) { + SideEffect { + selectedSection = SelectedSection.MEMBERS } } @@ -171,6 +182,7 @@ class RoomMemberListPresenter( isSearchActive = isSearchActive, canInvite = canInvite, moderationState = roomModerationState, + selectedSection = selectedSection, eventSink = ::handleEvent, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt index 404ecb523e..498dfd0180 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt @@ -21,10 +21,16 @@ data class RoomMemberListState( val searchResults: SearchBarResultState>, val isSearchActive: Boolean, val canInvite: Boolean, + val selectedSection: SelectedSection, val moderationState: RoomMemberModerationState, val eventSink: (RoomMemberListEvents) -> Unit, ) +enum class SelectedSection { + MEMBERS, + BANNED +} + data class RoomMembers( val invited: ImmutableList, val joined: ImmutableList, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt index 2194019e70..312efef35c 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -21,107 +21,131 @@ import kotlinx.collections.immutable.persistentListOf internal class RoomMemberListStateProvider : PreviewParameterProvider { override val values: Sequence - get() = sequenceOf( - aRoomMemberListState( - roomMembers = AsyncData.Success( - RoomMembers( - invited = persistentListOf(aVictor().withIdentity(), aWalter().withIdentity()), - joined = persistentListOf(anAlice().withIdentity(), aBob().withIdentity(), aWalter().withIdentity()), - banned = persistentListOf(), - ) - ) - ), - aRoomMemberListState( - roomMembers = AsyncData.Success( - RoomMembers( - invited = persistentListOf(aVictor().withIdentity(), aWalter().withIdentity()), - joined = persistentListOf( - anAlice().withIdentity(identityState = IdentityState.Verified), - aBob().withIdentity(identityState = IdentityState.PinViolation), - aWalter().withIdentity(identityState = IdentityState.VerificationViolation) - ), - banned = persistentListOf(), - ) - ), - moderationState = aRoomMemberModerationState(canBan = true) - ), - aRoomMemberListState(roomMembers = AsyncData.Loading()), - aRoomMemberListState().copy(canInvite = true), - aRoomMemberListState().copy(isSearchActive = false), - aRoomMemberListState().copy(isSearchActive = true), - aRoomMemberListState().copy(isSearchActive = true, searchQuery = "someone"), - aRoomMemberListState().copy( - isSearchActive = true, - searchQuery = "@someone:matrix.org", - searchResults = SearchBarResultState.Results( - AsyncData.Success( - RoomMembers( - invited = persistentListOf(aVictor().withIdentity()), - joined = persistentListOf(anAlice().withIdentity()), - banned = persistentListOf(), - ) - ) - ), - ), - aRoomMemberListState().copy( - isSearchActive = true, - searchQuery = "something-with-no-results", - searchResults = SearchBarResultState.NoResultsFound() - ), - aRoomMemberListState( - roomMembers = AsyncData.Failure(Exception("Error details")), - ), - ) + get() = roomMemberListStates() + bannedRoomMemberListStates() } -internal class RoomMemberListStateBannedProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aRoomMemberListState( - roomMembers = AsyncData.Success( - RoomMembers( - invited = persistentListOf(), - joined = persistentListOf(), - banned = persistentListOf( - aRoomMember(userId = UserId("@alice:example.com"), displayName = "Alice").withIdentity(), - aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob").withIdentity(), - aRoomMember(userId = UserId("@charlie:example.com"), displayName = "Charlie").withIdentity(), - ), - ) - ), - moderationState = aRoomMemberModerationState(), +private fun roomMemberListStates(): Sequence = sequenceOf( + aRoomMemberListState( + roomMembers = AsyncData.Success( + RoomMembers( + invited = persistentListOf(aVictor().withIdentity(), aWalter().withIdentity()), + joined = persistentListOf(anAlice().withIdentity(), aBob().withIdentity(), aWalter().withIdentity()), + banned = persistentListOf(), ), - aRoomMemberListState( - roomMembers = AsyncData.Loading( - RoomMembers( - invited = persistentListOf(), - joined = persistentListOf(), - banned = persistentListOf( - aRoomMember(userId = UserId("@alice:example.com"), displayName = "Alice").withIdentity(), - aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob").withIdentity(), - aRoomMember(userId = UserId("@charlie:example.com"), displayName = "Charlie").withIdentity(), - ), - ) + ), + selectedSection = SelectedSection.MEMBERS, + ), + aRoomMemberListState( + roomMembers = AsyncData.Success( + RoomMembers( + invited = persistentListOf(aVictor().withIdentity(), aWalter().withIdentity()), + joined = persistentListOf( + anAlice().withIdentity(identityState = IdentityState.Verified), + aBob().withIdentity(identityState = IdentityState.PinViolation), + aWalter().withIdentity(identityState = IdentityState.VerificationViolation) ), - moderationState = aRoomMemberModerationState(), - ), - aRoomMemberListState( - roomMembers = AsyncData.Success( - RoomMembers( - invited = persistentListOf(), - joined = persistentListOf(), - banned = persistentListOf(), - ) - ), - moderationState = aRoomMemberModerationState(), + banned = persistentListOf(), ) - ) -} + ), + selectedSection = SelectedSection.MEMBERS, + moderationState = aRoomMemberModerationState(canBan = true) + ), + aRoomMemberListState( + roomMembers = AsyncData.Loading(), + selectedSection = SelectedSection.MEMBERS, + ), + aRoomMemberListState().copy( + canInvite = true, + selectedSection = SelectedSection.MEMBERS, + ), + aRoomMemberListState().copy( + isSearchActive = false, + selectedSection = SelectedSection.MEMBERS, + ), + aRoomMemberListState().copy( + isSearchActive = true, + selectedSection = SelectedSection.MEMBERS, + ), + aRoomMemberListState().copy( + isSearchActive = true, + searchQuery = "someone", + selectedSection = SelectedSection.MEMBERS, + ), + aRoomMemberListState().copy( + isSearchActive = true, + searchQuery = "@someone:matrix.org", + searchResults = SearchBarResultState.Results( + AsyncData.Success( + RoomMembers( + invited = persistentListOf(aVictor().withIdentity()), + joined = persistentListOf(anAlice().withIdentity()), + banned = persistentListOf(), + ) + ) + ), + selectedSection = SelectedSection.MEMBERS, + ), + aRoomMemberListState().copy( + isSearchActive = true, + searchQuery = "something-with-no-results", + searchResults = SearchBarResultState.NoResultsFound(), + selectedSection = SelectedSection.MEMBERS, + ), + aRoomMemberListState( + roomMembers = AsyncData.Failure(Exception("Error details")), + selectedSection = SelectedSection.MEMBERS, + ), +) + +private fun bannedRoomMemberListStates(): Sequence = sequenceOf( + aRoomMemberListState( + roomMembers = AsyncData.Success( + RoomMembers( + invited = persistentListOf(), + joined = persistentListOf(), + banned = persistentListOf( + aRoomMember(userId = UserId("@alice:example.com"), displayName = "Alice").withIdentity(), + aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob").withIdentity(), + aRoomMember(userId = UserId("@charlie:example.com"), displayName = "Charlie").withIdentity(), + ), + ) + ), + moderationState = aRoomMemberModerationState(), + selectedSection = SelectedSection.BANNED, + ), + aRoomMemberListState( + roomMembers = AsyncData.Loading( + RoomMembers( + invited = persistentListOf(), + joined = persistentListOf(), + banned = persistentListOf( + aRoomMember(userId = UserId("@alice:example.com"), displayName = "Alice").withIdentity(), + aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob").withIdentity(), + aRoomMember(userId = UserId("@charlie:example.com"), displayName = "Charlie").withIdentity(), + ), + ) + ), + moderationState = aRoomMemberModerationState(), + selectedSection = SelectedSection.BANNED, + ), + aRoomMemberListState( + roomMembers = AsyncData.Success( + RoomMembers( + invited = persistentListOf(), + joined = persistentListOf(), + banned = persistentListOf(), + ) + ), + moderationState = aRoomMemberModerationState(), + selectedSection = SelectedSection.BANNED, + ) +) internal fun aRoomMemberListState( roomMembers: AsyncData = AsyncData.Loading(), searchResults: SearchBarResultState> = SearchBarResultState.Initial(), moderationState: RoomMemberModerationState = aRoomMemberModerationState(), + selectedSection: SelectedSection = SelectedSection.MEMBERS, ) = RoomMemberListState( roomMembers = roomMembers, searchQuery = "", @@ -129,6 +153,7 @@ internal fun aRoomMemberListState( isSearchActive = false, canInvite = false, moderationState = moderationState, + selectedSection = SelectedSection.MEMBERS, eventSink = {} ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt index 2b1ebb175c..b2a51aa446 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt @@ -30,10 +30,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -48,6 +45,7 @@ import io.element.android.features.roomdetails.impl.R import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon @@ -68,17 +66,11 @@ import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -private enum class SelectedSection { - MEMBERS, - BANNED -} - @Composable fun RoomMemberListView( state: RoomMemberListState, navigator: RoomMemberListNavigator, modifier: Modifier = Modifier, - initialSelectedSectionIndex: Int = 0, ) { fun onSelectUser(roomMember: RoomMember) { state.eventSink(RoomMemberListEvents.RoomMemberSelected(roomMember)) @@ -96,12 +88,6 @@ fun RoomMemberListView( } } ) { padding -> - var selectedSection by remember { mutableStateOf(SelectedSection.entries[initialSelectedSectionIndex]) } - if (!state.moderationState.canBan && selectedSection == SelectedSection.BANNED) { - SideEffect { - selectedSection = SelectedSection.MEMBERS - } - } Column( modifier = Modifier .fillMaxWidth() @@ -117,7 +103,7 @@ fun RoomMemberListView( onActiveChange = { state.eventSink(RoomMemberListEvents.OnSearchActiveChanged(it)) }, onTextChange = { state.eventSink(RoomMemberListEvents.UpdateSearchQuery(it)) }, onSelectUser = ::onSelectUser, - selectedSection = selectedSection, + selectedSection = state.selectedSection, modifier = Modifier.fillMaxWidth(), ) @@ -126,8 +112,8 @@ fun RoomMemberListView( roomMembers = state.roomMembers, showMembersCount = true, canDisplayBannedUsersControls = state.moderationState.canBan, - selectedSection = selectedSection, - onSelectedSectionChange = { selectedSection = it }, + selectedSection = state.selectedSection, + onSelectedSectionChange = { state.eventSink(RoomMemberListEvents.ChangeSelectedSection(it)) }, onSelectUser = ::onSelectUser, ) } @@ -380,9 +366,13 @@ private fun RoomMemberSearchBar( selectedSection: SelectedSection, modifier: Modifier = Modifier, ) { + var queryFieldState by textFieldState(query) SearchBar( - query = query, - onQueryChange = onTextChange, + query = queryFieldState, + onQueryChange = { newQuery -> + queryFieldState = newQuery + onTextChange(newQuery) + }, active = active, onActiveChange = onActiveChange, modifier = modifier, @@ -409,13 +399,3 @@ internal fun RoomMemberListViewPreview(@PreviewParameter(RoomMemberListStateProv navigator = object : RoomMemberListNavigator {}, ) } - -@PreviewsDayNight -@Composable -internal fun RoomMemberListViewBannedPreview(@PreviewParameter(RoomMemberListStateBannedProvider::class) state: RoomMemberListState) = ElementPreview { - RoomMemberListView( - initialSelectedSectionIndex = 1, - state = state, - navigator = object : RoomMemberListNavigator {}, - ) -}