diff --git a/features/rolesandpermissions/impl/src/main/res/values/localazy.xml b/features/rolesandpermissions/impl/src/main/res/values/localazy.xml index 2d98f43f97..77ab04749f 100644 --- a/features/rolesandpermissions/impl/src/main/res/values/localazy.xml +++ b/features/rolesandpermissions/impl/src/main/res/values/localazy.xml @@ -35,8 +35,8 @@ "Save changes?" "There are no banned users." - "%1$d person" - "%1$d people" + "%1$d Person" + "%1$d People" "Ban user" "Only remove member" @@ -45,7 +45,7 @@ "Unban user" "Banned" "Members" - "Pending" + "%1$d Invited" "Admin" "Moderator" "Owner" diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListDataSource.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListDataSource.kt deleted file mode 100644 index 5e00ce0937..0000000000 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListDataSource.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.roomdetails.impl.members - -import dev.zacsweers.metro.Inject -import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.matrix.api.room.BaseRoom -import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.RoomMembershipState -import io.element.android.libraries.matrix.api.room.roomMembers -import kotlinx.coroutines.withContext -import kotlin.collections.filter - -@Inject -class RoomMemberListDataSource( - private val room: BaseRoom, - private val coroutineDispatchers: CoroutineDispatchers, -) { - suspend fun search(query: String, selectedSection: SelectedSection): List = withContext(coroutineDispatchers.io) { - val roomMembersState = room.membersStateFlow.value - val displayableMembers = roomMembersState.roomMembers() - .orEmpty() - .filter { - when(selectedSection){ - SelectedSection.MEMBERS -> it.membership.isActive() - SelectedSection.BANNED -> it.membership == RoomMembershipState.BAN - } - } - - val filteredMembers = if (query.isBlank()) { - displayableMembers - } else { - displayableMembers.filter { member -> - member.userId.value.contains(query, ignoreCase = true) || - member.displayName?.contains(query, ignoreCase = true).orFalse() - } - } - filteredMembers - } -} 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 9a8798124e..a4b7b22bf3 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 @@ -13,6 +13,5 @@ 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/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 400432d331..44e0aa168f 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 @@ -23,6 +23,7 @@ import io.element.android.features.roommembermoderation.api.RoomMemberModeration import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.map import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.core.UserId @@ -41,7 +42,6 @@ import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext @@ -63,8 +63,6 @@ class RoomMemberListPresenter( var searchResults by remember { mutableStateOf>>(SearchBarResultState.Initial()) } - var isSearchActive by rememberSaveable { mutableStateOf(false) } - val membersState by room.membersStateFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val canInvite by room.canInviteAsState(syncUpdateFlow.value) @@ -78,8 +76,9 @@ class RoomMemberListPresenter( .launchIn(this) } - var roomMembers: AsyncData by remember { mutableStateOf(AsyncData.Loading())} - var selectedSection by remember { mutableStateOf(SelectedSection.MEMBERS)} + var roomMembers: AsyncData by remember { mutableStateOf(AsyncData.Loading()) } + var selectedSection by remember { mutableStateOf(SelectedSection.MEMBERS) } + var filteredRoomMembers: AsyncData by remember { mutableStateOf(AsyncData.Loading()) } // Update the room members when the screen is loaded LaunchedEffect(Unit) { @@ -98,7 +97,7 @@ class RoomMemberListPresenter( } withContext(coroutineDispatchers.io) { val members = membersState.roomMembers().orEmpty().groupBy { it.membership } - val info = room.roomInfoFlow.first() + val info = room.info() if (members.getOrDefault(RoomMembershipState.JOIN, emptyList()).size < info.joinedMembersCount / 2) { // Don't display initial room member list if we have less than half of the joined members: // This result will come from the timeline loading membership events and it'll be wrong. @@ -125,43 +124,16 @@ class RoomMemberListPresenter( } } - LaunchedEffect(membersState, searchQuery, isSearchActive) { - withContext(coroutineDispatchers.io) { - searchResults = if (searchQuery.isEmpty() || !isSearchActive) { - SearchBarResultState.Initial() - } else { - val results = roomMemberListDataSource.search(searchQuery, selectedSection).groupBy { it.membership } - if (results.isEmpty()) { - SearchBarResultState.NoResultsFound() - } else { - val result = RoomMembers( - invited = results.getOrDefault(RoomMembershipState.INVITE, emptyList()) - .map { it.withIdentityState(roomMemberIdentityStates) } - .toImmutableList(), - joined = results.getOrDefault(RoomMembershipState.JOIN, emptyList()) - .sortedWith(powerLevelRoomMemberComparator) - .map { it.withIdentityState(roomMemberIdentityStates) } - .toImmutableList(), - banned = results.getOrDefault(RoomMembershipState.BAN, emptyList()) - .sortedBy { it.userId.value } - .map { it.withIdentityState(roomMemberIdentityStates) } - .toImmutableList(), - ) - SearchBarResultState.Results( - if (membersState is RoomMembersState.Pending) { - AsyncData.Loading(result) - } else { - AsyncData.Success(result) - } - ) - } + LaunchedEffect(searchQuery, roomMembers) { + filteredRoomMembers = roomMembers.map { members -> + withContext(coroutineDispatchers.io) { + members.filter(searchQuery) } } } fun handleEvent(event: RoomMemberListEvents) { when (event) { - is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query is RoomMemberListEvents.RoomMemberSelected -> roomModerationState.eventSink(ShowActionsForUser(event.roomMember.toMatrixUser())) @@ -176,10 +148,8 @@ class RoomMemberListPresenter( } return RoomMemberListState( - roomMembers = roomMembers, + roomMembers = filteredRoomMembers, searchQuery = searchQuery, - searchResults = searchResults, - isSearchActive = isSearchActive, canInvite = canInvite, moderationState = roomModerationState, selectedSection = selectedSection, 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 498dfd0180..b59f67a915 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 @@ -10,16 +10,15 @@ package io.element.android.features.roomdetails.impl.members import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.room.RoomMember import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList data class RoomMemberListState( val roomMembers: AsyncData, val searchQuery: String, - val searchResults: SearchBarResultState>, - val isSearchActive: Boolean, val canInvite: Boolean, val selectedSection: SelectedSection, val moderationState: RoomMemberModerationState, @@ -35,7 +34,22 @@ data class RoomMembers( val invited: ImmutableList, val joined: ImmutableList, val banned: ImmutableList, -) +){ + fun filter(query: String): RoomMembers { + if (query.isBlank()) { + return this + } + val filterPredicate = { member: RoomMemberWithIdentityState -> + member.roomMember.userId.value.contains(query, ignoreCase = true) || + member.roomMember.displayName?.contains(query, ignoreCase = true).orFalse() + } + return RoomMembers( + invited = invited.filter(filterPredicate).toImmutableList(), + joined = joined.filter(filterPredicate).toImmutableList(), + banned = banned.filter(filterPredicate).toImmutableList(), + ) + } +} data class RoomMemberWithIdentityState( val roomMember: RoomMember, 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 312efef35c..dafe2546d5 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 @@ -59,36 +59,21 @@ private fun roomMemberListStates(): Sequence = sequenceOf( 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( @@ -143,17 +128,14 @@ private fun bannedRoomMemberListStates(): Sequence = sequen internal fun aRoomMemberListState( roomMembers: AsyncData = AsyncData.Loading(), - searchResults: SearchBarResultState> = SearchBarResultState.Initial(), moderationState: RoomMemberModerationState = aRoomMemberModerationState(), selectedSection: SelectedSection = SelectedSection.MEMBERS, ) = RoomMemberListState( roomMembers = roomMembers, searchQuery = "", - searchResults = searchResults, - isSearchActive = false, canInvite = false, moderationState = moderationState, - selectedSection = SelectedSection.MEMBERS, + selectedSection = selectedSection, 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 b2a51aa446..74d3305ff5 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 @@ -51,8 +51,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator import io.element.android.libraries.designsystem.theme.components.Scaffold -import io.element.android.libraries.designsystem.theme.components.SearchBar -import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.designsystem.theme.components.SearchField import io.element.android.libraries.designsystem.theme.components.SegmentedButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton @@ -79,44 +78,39 @@ fun RoomMemberListView( Scaffold( modifier = modifier, topBar = { - if (!state.isSearchActive) { - RoomMemberListTopBar( - canInvite = state.canInvite, - onBackClick = navigator::exitRoomMemberList, - onInviteClick = navigator::openInviteMembers, - ) - } + RoomMemberListTopBar( + canInvite = state.canInvite, + onBackClick = navigator::exitRoomMemberList, + onInviteClick = navigator::openInviteMembers, + ) } ) { padding -> Column( modifier = Modifier - .fillMaxWidth() - .padding(padding) - .consumeWindowInsets(padding), + .fillMaxWidth() + .padding(padding) + .consumeWindowInsets(padding), verticalArrangement = Arrangement.spacedBy(16.dp), ) { - RoomMemberSearchBar( - query = state.searchQuery, - state = state.searchResults, - active = state.isSearchActive, - placeHolderTitle = stringResource(CommonStrings.common_search_for_someone), - onActiveChange = { state.eventSink(RoomMemberListEvents.OnSearchActiveChanged(it)) }, - onTextChange = { state.eventSink(RoomMemberListEvents.UpdateSearchQuery(it)) }, - onSelectUser = ::onSelectUser, - selectedSection = state.selectedSection, - modifier = Modifier.fillMaxWidth(), + var searchQuery by textFieldState(state.searchQuery) + SearchField( + value = searchQuery, + onValueChange = { newQuery -> + searchQuery = newQuery + state.eventSink(RoomMemberListEvents.UpdateSearchQuery(newQuery)) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + placeholder = stringResource(CommonStrings.common_search_for_someone), + ) + RoomMemberList( + roomMembers = state.roomMembers, + selectedSection = state.selectedSection, + onSelectedSectionChange = { state.eventSink(RoomMemberListEvents.ChangeSelectedSection(it)) }, + showSections = state.moderationState.canBan, + onSelectUser = ::onSelectUser, ) - - if (!state.isSearchActive) { - RoomMemberList( - roomMembers = state.roomMembers, - showMembersCount = true, - canDisplayBannedUsersControls = state.moderationState.canBan, - selectedSection = state.selectedSection, - onSelectedSectionChange = { state.eventSink(RoomMemberListEvents.ChangeSelectedSection(it)) }, - onSelectUser = ::onSelectUser, - ) - } } } } @@ -124,25 +118,24 @@ fun RoomMemberListView( @Composable private fun RoomMemberList( roomMembers: AsyncData, - showMembersCount: Boolean, selectedSection: SelectedSection, + showSections: Boolean = true, onSelectedSectionChange: (SelectedSection) -> Unit, - canDisplayBannedUsersControls: Boolean, onSelectUser: (RoomMember) -> Unit, ) { LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) { stickyHeader { Column { - if (canDisplayBannedUsersControls) { + if (showSections) { 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( @@ -171,7 +164,6 @@ private fun RoomMemberList( roomMembers = roomMembers.dataOrNull() ?: return@LazyColumn, selectedSection = selectedSection, onSelectUser = onSelectUser, - showMembersCount = showMembersCount, ) AsyncData.Uninitialized -> Unit } @@ -182,27 +174,29 @@ private fun LazyListScope.memberItems( roomMembers: RoomMembers, selectedSection: SelectedSection, onSelectUser: (RoomMember) -> Unit, - showMembersCount: Boolean, ) { when (selectedSection) { SelectedSection.MEMBERS -> { if (roomMembers.invited.isNotEmpty()) { - roomMemberListSection( - headerText = { stringResource(id = R.string.screen_room_member_list_pending_header_title) }, + roomMemberListSectionHeader( + text = { + val memberCount = roomMembers.invited.count() + stringResource(id = R.string.screen_room_member_list_pending_header_title, memberCount) + }, + ) + roomMemberListSectionItems( members = roomMembers.invited, onMemberSelected = { onSelectUser(it) } ) } if (roomMembers.joined.isNotEmpty()) { - roomMemberListSection( - headerText = { - if (showMembersCount) { - val memberCount = roomMembers.joined.count() - pluralStringResource(id = R.plurals.screen_room_member_list_header_title, count = memberCount, memberCount) - } else { - stringResource(id = R.string.screen_room_member_list_room_members_header_title) - } + roomMemberListSectionHeader( + text = { + val memberCount = roomMembers.joined.count() + pluralStringResource(id = R.plurals.screen_room_member_list_header_title, count = memberCount, memberCount) }, + ) + roomMemberListSectionItems( members = roomMembers.joined, onMemberSelected = { onSelectUser(it) } ) @@ -210,8 +204,14 @@ private fun LazyListScope.memberItems( } SelectedSection.BANNED -> { // Banned users if (roomMembers.banned.isNotEmpty()) { - roomMemberListSection( - headerText = null, + roomMemberListSectionHeader( + text = { + val memberCount = roomMembers.banned.count() + stringResource(id = R.string.screen_room_member_list_banned_header_title, memberCount) + }, + isCritical = true, + ) + roomMemberListSectionItems( members = roomMembers.banned, onMemberSelected = { onSelectUser(it) } ) @@ -219,13 +219,13 @@ private fun LazyListScope.memberItems( item { Box( Modifier - .fillParentMaxSize() - .padding(horizontal = 16.dp) + .fillParentMaxSize() + .padding(horizontal = 16.dp) ) { Text( modifier = Modifier - .padding(bottom = 56.dp) - .align(Alignment.Center), + .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 +241,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, @@ -250,21 +250,25 @@ private fun LazyListScope.failureItem(failure: Throwable) { } } -private fun LazyListScope.roomMemberListSection( - headerText: @Composable (() -> String)?, +private fun LazyListScope.roomMemberListSectionHeader( + text: @Composable (() -> String), + modifier: Modifier = Modifier, + isCritical: Boolean = false, +) { + item { + Text( + modifier = modifier.padding(horizontal = 16.dp, vertical = 12.dp), + text = text(), + style = ElementTheme.typography.fontBodyLgMedium, + color = if (isCritical) ElementTheme.colors.textCriticalPrimary else ElementTheme.colors.textPrimary, + ) + } +} + +private fun LazyListScope.roomMemberListSectionItems( members: ImmutableList?, onMemberSelected: (RoomMember) -> Unit, ) { - headerText?.let { - item { - Text( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), - text = it(), - style = ElementTheme.typography.fontBodyLgRegular, - color = ElementTheme.colors.textSecondary, - ) - } - } items(members.orEmpty()) { matrixUser -> RoomMemberListItem( modifier = Modifier.fillMaxWidth(), @@ -353,44 +357,6 @@ private fun RoomMemberListTopBar( ) } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun RoomMemberSearchBar( - query: String, - state: SearchBarResultState>, - active: Boolean, - placeHolderTitle: String, - onActiveChange: (Boolean) -> Unit, - onTextChange: (String) -> Unit, - onSelectUser: (RoomMember) -> Unit, - selectedSection: SelectedSection, - modifier: Modifier = Modifier, -) { - var queryFieldState by textFieldState(query) - SearchBar( - query = queryFieldState, - onQueryChange = { newQuery -> - queryFieldState = newQuery - onTextChange(newQuery) - }, - active = active, - onActiveChange = onActiveChange, - modifier = modifier, - placeHolderTitle = placeHolderTitle, - resultState = state, - resultHandler = { results -> - RoomMemberList( - roomMembers = results, - showMembersCount = false, - onSelectUser = { onSelectUser(it) }, - canDisplayBannedUsersControls = false, - selectedSection = selectedSection, - onSelectedSectionChange = {}, - ) - }, - ) -} - @PreviewsDayNight @Composable internal fun RoomMemberListViewPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) = ElementPreview { diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index 2f9f2de9b7..4d8e634e81 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -71,9 +71,10 @@ "Topic" "Updating room…" "There are no banned users." + "%1$d Banned" - "%1$d person" - "%1$d people" + "%1$d Person" + "%1$d People" "Ban user" "Only remove member" @@ -82,7 +83,8 @@ "Unban user" "Banned" "Members" - "Pending" + "%1$d Invited" + "Pending" "Admin" "Moderator" "Owner" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 405451bb34..27ca1ecd12 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -198,6 +198,7 @@ "screen_room_details_.*", "screen\\.room_details\\..*", "screen_room_member_list_.*", + "screen\\.room_member_list\\..*", "screen_room_notification_settings_.*", "screen_notification_settings_edit_failed_updating_default_mode", "screen_polls_history_title",