change(members): new empty state for search and hide banned tabs when there is none.
This commit is contained in:
parent
1b0beda620
commit
8fd34051e0
5 changed files with 79 additions and 58 deletions
|
|
@ -49,7 +49,6 @@ import kotlinx.coroutines.withContext
|
||||||
@Inject
|
@Inject
|
||||||
class RoomMemberListPresenter(
|
class RoomMemberListPresenter(
|
||||||
private val room: JoinedRoom,
|
private val room: JoinedRoom,
|
||||||
private val roomMemberListDataSource: RoomMemberListDataSource,
|
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
private val roomMembersModerationPresenter: Presenter<RoomMemberModerationState>,
|
private val roomMembersModerationPresenter: Presenter<RoomMemberModerationState>,
|
||||||
private val encryptionService: EncryptionService,
|
private val encryptionService: EncryptionService,
|
||||||
|
|
@ -141,20 +140,21 @@ class RoomMemberListPresenter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!roomModerationState.canBan && selectedSection == SelectedSection.BANNED) {
|
val state = RoomMemberListState(
|
||||||
SideEffect {
|
roomMembers = roomMembers,
|
||||||
selectedSection = SelectedSection.MEMBERS
|
filteredRoomMembers = filteredRoomMembers,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return RoomMemberListState(
|
|
||||||
roomMembers = filteredRoomMembers,
|
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
canInvite = canInvite,
|
canInvite = canInvite,
|
||||||
moderationState = roomModerationState,
|
moderationState = roomModerationState,
|
||||||
selectedSection = selectedSection,
|
selectedSection = selectedSection,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
|
if (!state.showBannedSection && selectedSection == SelectedSection.BANNED) {
|
||||||
|
SideEffect {
|
||||||
|
selectedSection = SelectedSection.MEMBERS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun RoomMember.withIdentityState(identityStates: ImmutableMap<UserId, IdentityState>): RoomMemberWithIdentityState {
|
private suspend fun RoomMember.withIdentityState(identityStates: ImmutableMap<UserId, IdentityState>): RoomMemberWithIdentityState {
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,17 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
data class RoomMemberListState(
|
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 searchQuery: String,
|
||||||
val canInvite: Boolean,
|
val canInvite: Boolean,
|
||||||
val selectedSection: SelectedSection,
|
val selectedSection: SelectedSection,
|
||||||
val moderationState: RoomMemberModerationState,
|
val moderationState: RoomMemberModerationState,
|
||||||
val eventSink: (RoomMemberListEvents) -> Unit,
|
val eventSink: (RoomMemberListEvents) -> Unit,
|
||||||
)
|
) {
|
||||||
|
val showBannedSection: Boolean = moderationState.canBan && roomMembers.dataOrNull()?.banned?.isNotEmpty() == true
|
||||||
|
}
|
||||||
|
|
||||||
enum class SelectedSection {
|
enum class SelectedSection {
|
||||||
MEMBERS,
|
MEMBERS,
|
||||||
|
|
@ -34,7 +38,14 @@ data class RoomMembers(
|
||||||
val invited: ImmutableList<RoomMemberWithIdentityState>,
|
val invited: ImmutableList<RoomMemberWithIdentityState>,
|
||||||
val joined: ImmutableList<RoomMemberWithIdentityState>,
|
val joined: ImmutableList<RoomMemberWithIdentityState>,
|
||||||
val banned: 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 {
|
fun filter(query: String): RoomMembers {
|
||||||
if (query.isBlank()) {
|
if (query.isBlank()) {
|
||||||
return this
|
return this
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,7 @@ internal fun aRoomMemberListState(
|
||||||
selectedSection: SelectedSection = SelectedSection.MEMBERS,
|
selectedSection: SelectedSection = SelectedSection.MEMBERS,
|
||||||
) = RoomMemberListState(
|
) = RoomMemberListState(
|
||||||
roomMembers = roomMembers,
|
roomMembers = roomMembers,
|
||||||
|
filteredRoomMembers = roomMembers,
|
||||||
searchQuery = "",
|
searchQuery = "",
|
||||||
canInvite = false,
|
canInvite = false,
|
||||||
moderationState = moderationState,
|
moderationState = moderationState,
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,15 @@
|
||||||
package io.element.android.features.roomdetails.impl.members
|
package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandHorizontally
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkHorizontally
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
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.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.roomdetails.impl.R
|
import io.element.android.features.roomdetails.impl.R
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
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.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.form.textFieldState
|
import io.element.android.libraries.designsystem.components.form.textFieldState
|
||||||
|
|
@ -87,9 +90,9 @@ fun RoomMemberListView(
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding),
|
.consumeWindowInsets(padding),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
var searchQuery by textFieldState(state.searchQuery)
|
var searchQuery by textFieldState(state.searchQuery)
|
||||||
|
|
@ -100,15 +103,16 @@ fun RoomMemberListView(
|
||||||
state.eventSink(RoomMemberListEvents.UpdateSearchQuery(newQuery))
|
state.eventSink(RoomMemberListEvents.UpdateSearchQuery(newQuery))
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
placeholder = stringResource(CommonStrings.common_search_for_someone),
|
placeholder = stringResource(CommonStrings.common_search_for_someone),
|
||||||
)
|
)
|
||||||
RoomMemberList(
|
RoomMemberList(
|
||||||
roomMembers = state.roomMembers,
|
roomMembersData = state.filteredRoomMembers,
|
||||||
selectedSection = state.selectedSection,
|
selectedSection = state.selectedSection,
|
||||||
|
showBannedSection = state.showBannedSection,
|
||||||
|
searchQuery = state.searchQuery,
|
||||||
onSelectedSectionChange = { state.eventSink(RoomMemberListEvents.ChangeSelectedSection(it)) },
|
onSelectedSectionChange = { state.eventSink(RoomMemberListEvents.ChangeSelectedSection(it)) },
|
||||||
showSections = state.moderationState.canBan,
|
|
||||||
onSelectUser = ::onSelectUser,
|
onSelectUser = ::onSelectUser,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -117,25 +121,26 @@ fun RoomMemberListView(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomMemberList(
|
private fun RoomMemberList(
|
||||||
roomMembers: AsyncData<RoomMembers>,
|
roomMembersData: AsyncData<RoomMembers>,
|
||||||
selectedSection: SelectedSection,
|
selectedSection: SelectedSection,
|
||||||
showSections: Boolean = true,
|
showBannedSection: Boolean,
|
||||||
|
searchQuery: String,
|
||||||
onSelectedSectionChange: (SelectedSection) -> Unit,
|
onSelectedSectionChange: (SelectedSection) -> Unit,
|
||||||
onSelectUser: (RoomMember) -> Unit,
|
onSelectUser: (RoomMember) -> Unit,
|
||||||
) {
|
) {
|
||||||
LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) {
|
LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) {
|
||||||
stickyHeader {
|
stickyHeader {
|
||||||
Column {
|
Column {
|
||||||
if (showSections) {
|
AnimatedVisibility(visible = showBannedSection) {
|
||||||
val segmentedButtonTitles = persistentListOf(
|
val segmentedButtonTitles = persistentListOf(
|
||||||
stringResource(id = R.string.screen_room_member_list_mode_members),
|
stringResource(id = R.string.screen_room_member_list_mode_members),
|
||||||
stringResource(id = R.string.screen_room_member_list_mode_banned),
|
stringResource(id = R.string.screen_room_member_list_mode_banned),
|
||||||
)
|
)
|
||||||
SingleChoiceSegmentedButtonRow(
|
SingleChoiceSegmentedButtonRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(ElementTheme.colors.bgCanvasDefault)
|
.background(ElementTheme.colors.bgCanvasDefault)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||||
) {
|
) {
|
||||||
for ((index, title) in segmentedButtonTitles.withIndex()) {
|
for ((index, title) in segmentedButtonTitles.withIndex()) {
|
||||||
SegmentedButton(
|
SegmentedButton(
|
||||||
|
|
@ -148,23 +153,26 @@ private fun RoomMemberList(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(visible = roomMembersData.isLoading()) {
|
||||||
visible = roomMembers.isLoading(),
|
|
||||||
enter = fadeIn() + expandVertically(),
|
|
||||||
exit = fadeOut() + shrinkVertically(),
|
|
||||||
) {
|
|
||||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (roomMembers) {
|
when (roomMembersData) {
|
||||||
is AsyncData.Failure -> failureItem(roomMembers.error)
|
is AsyncData.Failure -> failureItem(roomMembersData.error)
|
||||||
is AsyncData.Loading,
|
is AsyncData.Loading,
|
||||||
is AsyncData.Success -> memberItems(
|
is AsyncData.Success -> {
|
||||||
roomMembers = roomMembers.dataOrNull() ?: return@LazyColumn,
|
val roomMembers = roomMembersData.dataOrNull() ?: return@LazyColumn
|
||||||
selectedSection = selectedSection,
|
if (roomMembers.isEmpty(selectedSection)) {
|
||||||
onSelectUser = onSelectUser,
|
emptySearchItem(searchQuery)
|
||||||
)
|
} else {
|
||||||
|
memberItems(
|
||||||
|
roomMembers = roomMembers,
|
||||||
|
selectedSection = selectedSection,
|
||||||
|
onSelectUser = onSelectUser,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
AsyncData.Uninitialized -> Unit
|
AsyncData.Uninitialized -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +210,7 @@ private fun LazyListScope.memberItems(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SelectedSection.BANNED -> { // Banned users
|
SelectedSection.BANNED -> {
|
||||||
if (roomMembers.banned.isNotEmpty()) {
|
if (roomMembers.banned.isNotEmpty()) {
|
||||||
roomMemberListSectionHeader(
|
roomMemberListSectionHeader(
|
||||||
text = {
|
text = {
|
||||||
|
|
@ -215,23 +223,6 @@ private fun LazyListScope.memberItems(
|
||||||
members = roomMembers.banned,
|
members = roomMembers.banned,
|
||||||
onMemberSelected = { onSelectUser(it) }
|
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 {
|
item {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 32.dp),
|
.padding(horizontal = 16.dp, vertical = 32.dp),
|
||||||
text = stringResource(id = CommonStrings.error_unknown) + "\n\n" + failure.localizedMessage,
|
text = stringResource(id = CommonStrings.error_unknown) + "\n\n" + failure.localizedMessage,
|
||||||
color = ElementTheme.colors.textCriticalPrimary,
|
color = ElementTheme.colors.textCriticalPrimary,
|
||||||
textAlign = TextAlign.Center,
|
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
|
@Composable
|
||||||
private fun RoomMemberListItem(
|
private fun RoomMemberListItem(
|
||||||
roomMemberWithIdentity: RoomMemberWithIdentityState,
|
roomMemberWithIdentity: RoomMemberWithIdentityState,
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,8 @@
|
||||||
<string name="screen_room_details_updating_room">"Updating room…"</string>
|
<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_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_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">
|
<plurals name="screen_room_member_list_header_title">
|
||||||
<item quantity="one">"%1$d Person"</item>
|
<item quantity="one">"%1$d Person"</item>
|
||||||
<item quantity="other">"%1$d People"</item>
|
<item quantity="other">"%1$d People"</item>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue