change(members): make sure state is not lost when navigating

This commit is contained in:
ganfra
2025-11-20 16:41:43 +01:00
parent 80d799c4db
commit dcaac703fc
6 changed files with 153 additions and 125 deletions

View File

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

View File

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

View File

@@ -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<RoomMemberModerationState>,
private val encryptionService: EncryptionService,
) : Presenter<RoomMemberListState> {
private var roomMembers: AsyncData<RoomMembers> by mutableStateOf(AsyncData.Loading())
private val powerLevelRoomMemberComparator = PowerLevelRoomMemberComparator()
@Composable
@@ -77,6 +78,9 @@ class RoomMemberListPresenter(
.launchIn(this)
}
var roomMembers: AsyncData<RoomMembers> 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,
)
}

View File

@@ -21,10 +21,16 @@ data class RoomMemberListState(
val searchResults: SearchBarResultState<AsyncData<RoomMembers>>,
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<RoomMemberWithIdentityState>,
val joined: ImmutableList<RoomMemberWithIdentityState>,

View File

@@ -21,107 +21,131 @@ import kotlinx.collections.immutable.persistentListOf
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
override val values: Sequence<RoomMemberListState>
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<RoomMemberListState> {
override val values: Sequence<RoomMemberListState>
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<RoomMemberListState> = 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<RoomMemberListState> = 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<RoomMembers> = AsyncData.Loading(),
searchResults: SearchBarResultState<AsyncData<RoomMembers>> = 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 = {}
)

View File

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