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 44e0aa168f..3d59b3d922 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 @@ -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, 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): RoomMemberWithIdentityState { 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 b59f67a915..007d276dd3 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 @@ -17,13 +17,17 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList data class RoomMemberListState( - val roomMembers: AsyncData, + // Only used to know if we can show the banned section + private val roomMembers: AsyncData, + val filteredRoomMembers: AsyncData, 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, val joined: ImmutableList, val banned: ImmutableList, -){ +) { + 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 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 dafe2546d5..b03faa97cb 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 @@ -132,6 +132,7 @@ internal fun aRoomMemberListState( selectedSection: SelectedSection = SelectedSection.MEMBERS, ) = RoomMemberListState( roomMembers = roomMembers, + filteredRoomMembers = roomMembers, searchQuery = "", canInvite = false, moderationState = moderationState, 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 74d3305ff5..41db02ee9d 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 @@ -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, + roomMembersData: AsyncData, 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, diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index 4d8e634e81..a88f910de9 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -72,6 +72,8 @@ "Updating room…" "There are no banned users." "%1$d Banned" + "Check the spelling or try a new search" + "No results for “%1$s”" "%1$d Person" "%1$d People"