change(members): new empty state for search and hide banned tabs when there is none.

This commit is contained in:
ganfra
2025-11-24 10:42:03 +01:00
parent 1b0beda620
commit 8fd34051e0
5 changed files with 79 additions and 58 deletions

View File

@@ -49,7 +49,6 @@ import kotlinx.coroutines.withContext
@Inject
class RoomMemberListPresenter(
private val room: JoinedRoom,
private val roomMemberListDataSource: RoomMemberListDataSource,
private val coroutineDispatchers: CoroutineDispatchers,
private val roomMembersModerationPresenter: Presenter<RoomMemberModerationState>,
private val encryptionService: EncryptionService,
@@ -141,20 +140,21 @@ class RoomMemberListPresenter(
}
}
if (!roomModerationState.canBan && selectedSection == SelectedSection.BANNED) {
SideEffect {
selectedSection = SelectedSection.MEMBERS
}
}
return RoomMemberListState(
roomMembers = filteredRoomMembers,
val state = RoomMemberListState(
roomMembers = roomMembers,
filteredRoomMembers = filteredRoomMembers,
searchQuery = searchQuery,
canInvite = canInvite,
moderationState = roomModerationState,
selectedSection = selectedSection,
eventSink = ::handleEvent,
)
if (!state.showBannedSection && selectedSection == SelectedSection.BANNED) {
SideEffect {
selectedSection = SelectedSection.MEMBERS
}
}
return state
}
private suspend fun RoomMember.withIdentityState(identityStates: ImmutableMap<UserId, IdentityState>): RoomMemberWithIdentityState {

View File

@@ -17,13 +17,17 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
data class RoomMemberListState(
val roomMembers: AsyncData<RoomMembers>,
// Only used to know if we can show the banned section
private val roomMembers: AsyncData<RoomMembers>,
val filteredRoomMembers: AsyncData<RoomMembers>,
val searchQuery: String,
val canInvite: Boolean,
val selectedSection: SelectedSection,
val moderationState: RoomMemberModerationState,
val eventSink: (RoomMemberListEvents) -> Unit,
)
) {
val showBannedSection: Boolean = moderationState.canBan && roomMembers.dataOrNull()?.banned?.isNotEmpty() == true
}
enum class SelectedSection {
MEMBERS,
@@ -34,7 +38,14 @@ data class RoomMembers(
val invited: ImmutableList<RoomMemberWithIdentityState>,
val joined: ImmutableList<RoomMemberWithIdentityState>,
val banned: ImmutableList<RoomMemberWithIdentityState>,
){
) {
fun isEmpty(section: SelectedSection): Boolean {
return when (section) {
SelectedSection.MEMBERS -> invited.isEmpty() && joined.isEmpty()
SelectedSection.BANNED -> banned.isEmpty()
}
}
fun filter(query: String): RoomMembers {
if (query.isBlank()) {
return this

View File

@@ -132,6 +132,7 @@ internal fun aRoomMemberListState(
selectedSection: SelectedSection = SelectedSection.MEMBERS,
) = RoomMemberListState(
roomMembers = roomMembers,
filteredRoomMembers = roomMembers,
searchQuery = "",
canInvite = false,
moderationState = moderationState,

View File

@@ -9,14 +9,15 @@
package io.element.android.features.roomdetails.impl.members
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.consumeWindowInsets
@@ -43,6 +44,8 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.roomdetails.impl.R
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.BigIcon
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
@@ -87,9 +90,9 @@ fun RoomMemberListView(
) { padding ->
Column(
modifier = Modifier
.fillMaxWidth()
.padding(padding)
.consumeWindowInsets(padding),
.fillMaxWidth()
.padding(padding)
.consumeWindowInsets(padding),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
var searchQuery by textFieldState(state.searchQuery)
@@ -100,15 +103,16 @@ fun RoomMemberListView(
state.eventSink(RoomMemberListEvents.UpdateSearchQuery(newQuery))
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.fillMaxWidth()
.padding(horizontal = 16.dp),
placeholder = stringResource(CommonStrings.common_search_for_someone),
)
RoomMemberList(
roomMembers = state.roomMembers,
roomMembersData = state.filteredRoomMembers,
selectedSection = state.selectedSection,
showBannedSection = state.showBannedSection,
searchQuery = state.searchQuery,
onSelectedSectionChange = { state.eventSink(RoomMemberListEvents.ChangeSelectedSection(it)) },
showSections = state.moderationState.canBan,
onSelectUser = ::onSelectUser,
)
}
@@ -117,25 +121,26 @@ fun RoomMemberListView(
@Composable
private fun RoomMemberList(
roomMembers: AsyncData<RoomMembers>,
roomMembersData: AsyncData<RoomMembers>,
selectedSection: SelectedSection,
showSections: Boolean = true,
showBannedSection: Boolean,
searchQuery: String,
onSelectedSectionChange: (SelectedSection) -> Unit,
onSelectUser: (RoomMember) -> Unit,
) {
LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) {
stickyHeader {
Column {
if (showSections) {
AnimatedVisibility(visible = showBannedSection) {
val segmentedButtonTitles = persistentListOf(
stringResource(id = R.string.screen_room_member_list_mode_members),
stringResource(id = R.string.screen_room_member_list_mode_banned),
)
SingleChoiceSegmentedButtonRow(
modifier = Modifier
.background(ElementTheme.colors.bgCanvasDefault)
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
.background(ElementTheme.colors.bgCanvasDefault)
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
) {
for ((index, title) in segmentedButtonTitles.withIndex()) {
SegmentedButton(
@@ -148,23 +153,26 @@ private fun RoomMemberList(
}
}
}
AnimatedVisibility(
visible = roomMembers.isLoading(),
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
AnimatedVisibility(visible = roomMembersData.isLoading()) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
}
}
when (roomMembers) {
is AsyncData.Failure -> failureItem(roomMembers.error)
when (roomMembersData) {
is AsyncData.Failure -> failureItem(roomMembersData.error)
is AsyncData.Loading,
is AsyncData.Success -> memberItems(
roomMembers = roomMembers.dataOrNull() ?: return@LazyColumn,
selectedSection = selectedSection,
onSelectUser = onSelectUser,
)
is AsyncData.Success -> {
val roomMembers = roomMembersData.dataOrNull() ?: return@LazyColumn
if (roomMembers.isEmpty(selectedSection)) {
emptySearchItem(searchQuery)
} else {
memberItems(
roomMembers = roomMembers,
selectedSection = selectedSection,
onSelectUser = onSelectUser,
)
}
}
AsyncData.Uninitialized -> Unit
}
}
@@ -202,7 +210,7 @@ private fun LazyListScope.memberItems(
)
}
}
SelectedSection.BANNED -> { // Banned users
SelectedSection.BANNED -> {
if (roomMembers.banned.isNotEmpty()) {
roomMemberListSectionHeader(
text = {
@@ -215,23 +223,6 @@ private fun LazyListScope.memberItems(
members = roomMembers.banned,
onMemberSelected = { onSelectUser(it) }
)
} else {
item {
Box(
Modifier
.fillParentMaxSize()
.padding(horizontal = 16.dp)
) {
Text(
modifier = Modifier
.padding(bottom = 56.dp)
.align(Alignment.Center),
text = stringResource(id = R.string.screen_room_member_list_banned_empty),
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
}
}
}
}
}
@@ -241,8 +232,8 @@ private fun LazyListScope.failureItem(failure: Throwable) {
item {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 32.dp),
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 32.dp),
text = stringResource(id = CommonStrings.error_unknown) + "\n\n" + failure.localizedMessage,
color = ElementTheme.colors.textCriticalPrimary,
textAlign = TextAlign.Center,
@@ -278,6 +269,22 @@ private fun LazyListScope.roomMemberListSectionItems(
}
}
private fun LazyListScope.emptySearchItem(searchQuery: String) {
item {
IconTitleSubtitleMolecule(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 32.dp),
iconStyle = BigIcon.Style.Default(
vectorIcon = CompoundIcons.Search(),
contentDescription = null,
),
title = stringResource(R.string.screen_room_member_list_empty_search_title, searchQuery),
subTitle = stringResource(R.string.screen_room_member_list_empty_search_subtitle),
)
}
}
@Composable
private fun RoomMemberListItem(
roomMemberWithIdentity: RoomMemberWithIdentityState,

View File

@@ -72,6 +72,8 @@
<string name="screen_room_details_updating_room">"Updating room…"</string>
<string name="screen_room_member_list_banned_empty">"There are no banned users."</string>
<string name="screen_room_member_list_banned_header_title">"%1$d Banned"</string>
<string name="screen_room_member_list_empty_search_subtitle">"Check the spelling or try a new search"</string>
<string name="screen_room_member_list_empty_search_title">"No results for “%1$s”"</string>
<plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d Person"</item>
<item quantity="other">"%1$d People"</item>