Merge branch 'develop' into feature/fga/space_ui_tweaks

This commit is contained in:
ganfra 2026-02-10 09:31:50 +01:00
commit 0dec3a1cb6
174 changed files with 2905 additions and 1323 deletions

View file

@ -3,16 +3,33 @@
<string name="screen_create_room_action_create_room">"Nová místnost"</string>
<string name="screen_create_room_add_people_title">"Pozvat přátele"</string>
<string name="screen_create_room_error_creating_room">"Při vytváření místnosti došlo k chybě"</string>
<string name="screen_create_room_private_option_description">"Do této místnosti mají přístup pouze pozvaní lidé. Všechny zprávy jsou koncově šifrovány."</string>
<string name="screen_create_room_error_creating_space">"Prostor se nepodařilo vytvořit kvůli neznámé chybě. Zkuste to znovu později."</string>
<string name="screen_create_room_name_placeholder">"Přidat název…"</string>
<string name="screen_create_room_new_room_title">"Nová místnost"</string>
<string name="screen_create_room_new_space_title">"Nový prostor"</string>
<string name="screen_create_room_private_option_description">"Do této místnosti mohou vstoupit pouze pozvaní."</string>
<string name="screen_create_room_private_option_title">"Soukromý"</string>
<string name="screen_create_room_public_option_description">"Tuto místnost může najít kdokoli.
To můžete kdykoli změnit v nastavení místnosti."</string>
<string name="screen_create_room_public_option_short_description">"Vstoupit může kdokoli."</string>
<string name="screen_create_room_public_option_title">"Veřejná místnost"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Kdokoli může požádat o vstup do místnosti, ale správce nebo moderátor bude muset žádost přijmout"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Požádat o připojení"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Do této místnosti může vstoupit kdokoli"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Kdokoli může požádat o vstup do místnosti, ale správce nebo moderátor bude muset žádost přijmout."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Povolit žádost o vstup"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Kdokoli v %1$s může vstoupit, ale všichni ostatní si musí o přístup požádat."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Požádat o vstup"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Vstoupit mohou pouze pozvaní lidé."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Soukromý"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Vstoupit může kdokoli."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Kdokoliv"</string>
<string name="screen_create_room_room_address_section_footer">"Aby byla tato místnost viditelná v adresáři veřejných místností, budete potřebovat adresu místnosti."</string>
<string name="screen_create_room_room_address_section_title">"Adresa místnosti"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Kdokoli může vstoupit do %1$s."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"Standard"</string>
<string name="screen_create_room_room_access_section_title">"Kdo má přístup"</string>
<string name="screen_create_room_room_address_section_footer">"Budete potřebovat adresu, aby se zobrazovala ve veřejném adresáři."</string>
<string name="screen_create_room_room_address_section_title">"Adresa"</string>
<string name="screen_create_room_room_visibility_section_title">"Viditelnost místnosti"</string>
<string name="screen_create_room_space_selection_no_space_description">"(bez prostoru)"</string>
<string name="screen_create_room_space_selection_no_space_title">"Domov"</string>
<string name="screen_create_room_space_selection_sheet_title">"Přidat do prostoru"</string>
<string name="screen_create_room_topic_label">"Téma (nepovinné)"</string>
<string name="screen_create_room_topic_placeholder">"Přidat popis…"</string>
</resources>

View file

@ -3,20 +3,31 @@
<string name="screen_create_room_action_create_room">"Uus jututuba"</string>
<string name="screen_create_room_add_people_title">"Kutsu osalejaid"</string>
<string name="screen_create_room_error_creating_room">"Jututoa loomisel tekkis viga"</string>
<string name="screen_create_room_error_creating_space">"Kogukonda polnud tundmatu vea tõttu võimalik luua. Palun proovi hiljem uuesti."</string>
<string name="screen_create_room_name_placeholder">"Sisesta nimi…"</string>
<string name="screen_create_room_new_room_title">"Uus jututuba"</string>
<string name="screen_create_room_new_space_title">"Uus kogukond"</string>
<string name="screen_create_room_private_option_description">"Ligipääs siia jututuppa on vaid kutse alusel."</string>
<string name="screen_create_room_private_option_title">"Privaatne"</string>
<string name="screen_create_room_public_option_description">"Kõik saavad seda jututuba leida.
Sa võid seda jututoa seadistustest alati muuta."</string>
<string name="screen_create_room_public_option_short_description">"Kõik võivad selle jututoaga liituda."</string>
<string name="screen_create_room_public_option_title">"Avalik jututuba"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Kõik võivad paluda selle jututoaga liitumist, kuid peakasutaja või moderaator peavad selle kinnitama."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Luba küsida liitumisvõimalust"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Kõik „%1$s“ kogukonna liikmed võivad liituda, kuid kõik teised peavad liitumiseks küsima luba."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Küsi võimalust liitumiseks"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Ligipääs siia jututuppa on vaid kutse alusel."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Privaatne"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Kõik võivad selle jututoaga liituda."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Kõik kasutajad"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Liituda võivad kõik „%1$s“ kogukonna liikmed."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"Standardne"</string>
<string name="screen_create_room_room_access_section_title">"Kellel on ligipääs"</string>
<string name="screen_create_room_room_address_section_footer">"Selleks, et jututuba oleks nähtav jututubade avalikus kataloogis, vajab ta aadressi."</string>
<string name="screen_create_room_room_address_section_title">"Aadress"</string>
<string name="screen_create_room_room_visibility_section_title">"Jututoa nähtavus"</string>
<string name="screen_create_room_space_selection_no_space_description">"(kogukonda pole)"</string>
<string name="screen_create_room_space_selection_no_space_title">"Avaleht"</string>
<string name="screen_create_room_space_selection_sheet_title">"Lisa kogukonda"</string>
<string name="screen_create_room_topic_label">"Teema (kui soovid lisada)"</string>

View file

@ -20,7 +20,7 @@ Vous pouvez modifier cela à tout moment dans les paramètres du salon."</string
<string name="screen_create_room_room_access_section_private_option_description">"Seules les personnes invitées peuvent joindre."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Privé"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Tout le monde peut joindre"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Tout le monde"</string>
<string name="screen_create_room_room_access_section_public_option_title">"Public"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Toute membre de %1$s peut joindre le salon."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"Standard"</string>
<string name="screen_create_room_room_access_section_title">"Qui a accès"</string>
@ -28,7 +28,8 @@ Vous pouvez modifier cela à tout moment dans les paramètres du salon."</string
<string name="screen_create_room_room_address_section_title">"Adresse"</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilité du salon"</string>
<string name="screen_create_room_space_selection_no_space_description">"(pas despace)"</string>
<string name="screen_create_room_space_selection_no_space_title">"Accueil"</string>
<string name="screen_create_room_space_selection_no_space_option">"Ne pas ajouter à un espace"</string>
<string name="screen_create_room_space_selection_no_space_title">"Aucun espace sélectionné"</string>
<string name="screen_create_room_space_selection_sheet_title">"Ajouter à lespace"</string>
<string name="screen_create_room_topic_label">"Sujet (facultatif)"</string>
<string name="screen_create_room_topic_placeholder">"Ajouter une description…"</string>

View file

@ -3,17 +3,25 @@
<string name="screen_create_room_action_create_room">"Nytt rom"</string>
<string name="screen_create_room_add_people_title">"Inviter folk"</string>
<string name="screen_create_room_error_creating_room">"Det oppsto en feil under opprettelsen av rommet"</string>
<string name="screen_create_room_name_placeholder">"Legg til navn…"</string>
<string name="screen_create_room_new_room_title">"Nytt rom"</string>
<string name="screen_create_room_new_space_title">"Nytt område"</string>
<string name="screen_create_room_private_option_description">"Bare inviterte personer kan bli med."</string>
<string name="screen_create_room_private_option_title">"Privat"</string>
<string name="screen_create_room_public_option_description">"Alle kan finne dette rommet.
Du kan endre dette når som helst i rominnstillingene."</string>
<string name="screen_create_room_public_option_short_description">"Alle kan bli med."</string>
<string name="screen_create_room_public_option_title">"Offentlig rom"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Alle kan be om å få bli med, men en administrator eller moderator må godta forespørselen."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Be om å bli med"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Bare inviterte personer kan bli med."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Privat"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Alle kan bli med."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Alle"</string>
<string name="screen_create_room_room_access_section_title">"Hvem har tilgang"</string>
<string name="screen_create_room_room_address_section_footer">"Du trenger en adresse for å gjøre den synlig i den offentlige katalogen."</string>
<string name="screen_create_room_room_address_section_title">"Adresse"</string>
<string name="screen_create_room_room_visibility_section_title">"Romsynlighet"</string>
<string name="screen_create_room_topic_label">"Emne (valgfritt)"</string>
<string name="screen_create_room_topic_placeholder">"Legg til beskrivelse…"</string>
</resources>

View file

@ -7,12 +7,12 @@
<string name="screen_create_room_public_option_description">"Vem som helst kan hitta det här rummet.
Du kan ändra detta när som helst i rumsinställningarna."</string>
<string name="screen_create_room_public_option_title">"Offentligt rum"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Vem som helst kan be om att gå med i rummet men en administratör eller en moderator måste acceptera begäran"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Vem som helst kan be om att gå med men en administratör eller en moderator måste acceptera begäran"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Tillåt att be om att gå med"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Vem som helst kan gå med i det här rummet"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Vem som helst kan gå med."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Offentligt"</string>
<string name="screen_create_room_room_address_section_footer">"För att detta rum ska vara synligt i den allmänna rumskatalogen behöver du en rumsadress."</string>
<string name="screen_create_room_room_address_section_title">"Rumsadress"</string>
<string name="screen_create_room_room_address_section_footer">"Du behöver en adress för att den ska synas i den offentliga katalogen."</string>
<string name="screen_create_room_room_address_section_title">"Adress"</string>
<string name="screen_create_room_room_visibility_section_title">"Rumssynlighet"</string>
<string name="screen_create_room_topic_label">"Ämne (valfritt)"</string>
</resources>

View file

@ -9,6 +9,7 @@
package io.element.android.features.home.impl
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
@ -31,6 +32,7 @@ data class HomeState(
val directLogoutState: DirectLogoutState,
val eventSink: (HomeEvent) -> Unit,
) {
val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected
val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats
val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters
val showNavigationBar = homeSpacesState.canCreateSpaces || homeSpacesState.spaceRooms.isNotEmpty()

View file

@ -50,6 +50,9 @@ import io.element.android.features.home.impl.roomlist.RoomListDeclineInviteMenu
import io.element.android.features.home.impl.roomlist.RoomListEvent
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.search.RoomListSearchView
import io.element.android.features.home.impl.spacefilters.SpaceFiltersEvent
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersView
import io.element.android.features.home.impl.spaces.HomeSpacesView
import io.element.android.libraries.androidutils.throttler.FirstThrottler
import io.element.android.libraries.designsystem.preview.ElementPreview
@ -153,10 +156,15 @@ private fun HomeScaffold(
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
val roomListState: RoomListState = state.roomListState
BackHandler(
enabled = state.currentHomeNavigationBarItem != HomeNavigationBarItem.Chats,
) {
state.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Chats))
BackHandler(enabled = state.isBackHandlerEnabled) {
if (state.currentHomeNavigationBarItem != HomeNavigationBarItem.Chats) {
state.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Chats))
} else {
val spaceFiltersState = state.roomListState.spaceFiltersState
if (spaceFiltersState is SpaceFiltersState.Selected) {
spaceFiltersState.eventSink(SpaceFiltersEvent.Selected.ClearSelection)
}
}
}
val hazeState = rememberHazeState()
@ -168,7 +176,6 @@ private fun HomeScaffold(
topBar = {
HomeTopBar(
selectedNavigationItem = state.currentHomeNavigationBarItem,
title = stringResource(state.currentHomeNavigationBarItem.labelRes),
currentUserAndNeighbors = state.currentUserAndNeighbors,
showAvatarIndicator = state.showAvatarIndicator,
areSearchResultsDisplayed = roomListState.searchState.isSearchActive,
@ -182,6 +189,7 @@ private fun HomeScaffold(
scrollBehavior = scrollBehavior,
displayFilters = state.displayRoomListFilters,
filtersState = roomListState.filtersState,
spaceFiltersState = roomListState.spaceFiltersState,
canCreateSpaces = state.homeSpacesState.canCreateSpaces,
canReportBug = state.canReportBug,
modifier = Modifier.hazeEffect(
@ -227,6 +235,7 @@ private fun HomeScaffold(
RoomListContentView(
contentState = roomListState.contentState,
filtersState = roomListState.filtersState,
spaceFiltersState = roomListState.spaceFiltersState,
lazyListState = roomsLazyListState,
hideInvitesAvatars = roomListState.hideInvitesAvatars,
eventSink = roomListState.eventSink,
@ -256,6 +265,7 @@ private fun HomeScaffold(
.consumeWindowInsets(padding)
.hazeSource(state = hazeState)
)
SpaceFiltersView(roomListState.spaceFiltersState)
}
HomeNavigationBarItem.Spaces -> {
HomeSpacesView(

View file

@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
@ -44,6 +45,10 @@ import io.element.android.features.home.impl.R
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.filters.RoomListFiltersView
import io.element.android.features.home.impl.filters.aRoomListFiltersState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersEvent
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.home.impl.spacefilters.aSelectedSpaceFiltersState
import io.element.android.features.home.impl.spacefilters.anUnselectedSpaceFiltersState
import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
import io.element.android.libraries.designsystem.components.TopAppBarScrollBehaviorLayout
import io.element.android.libraries.designsystem.components.avatar.Avatar
@ -75,7 +80,6 @@ import kotlinx.collections.immutable.toImmutableList
@Composable
fun HomeTopBar(
selectedNavigationItem: HomeNavigationBarItem,
title: String,
currentUserAndNeighbors: ImmutableList<MatrixUser>,
showAvatarIndicator: Boolean,
areSearchResultsDisplayed: Boolean,
@ -89,6 +93,7 @@ fun HomeTopBar(
canReportBug: Boolean,
displayFilters: Boolean,
filtersState: RoomListFiltersState,
spaceFiltersState: SpaceFiltersState,
modifier: Modifier = Modifier,
) {
Column(modifier) {
@ -103,12 +108,21 @@ fun HomeTopBar(
scrolledContainerColor = Color.Transparent,
),
title = {
val displayTitle = when (selectedNavigationItem) {
HomeNavigationBarItem.Chats -> {
when (spaceFiltersState) {
is SpaceFiltersState.Selected -> spaceFiltersState.selectedFilter.spaceRoom.displayName
else -> stringResource(selectedNavigationItem.labelRes)
}
}
HomeNavigationBarItem.Spaces -> stringResource(selectedNavigationItem.labelRes)
}
Text(
modifier = Modifier.semantics {
heading()
},
style = ElementTheme.typography.aliasScreenTitle,
text = title,
text = displayTitle,
)
},
navigationIcon = {
@ -124,7 +138,8 @@ fun HomeTopBar(
HomeNavigationBarItem.Chats -> RoomListMenuItems(
onToggleSearch = onToggleSearch,
onMenuActionClick = onMenuActionClick,
canReportBug = canReportBug
canReportBug = canReportBug,
spaceFiltersState = spaceFiltersState,
)
HomeNavigationBarItem.Spaces -> SpacesMenuItems(
canCreateSpaces = canCreateSpaces,
@ -154,6 +169,7 @@ private fun RoomListMenuItems(
onToggleSearch: () -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
canReportBug: Boolean,
spaceFiltersState: SpaceFiltersState,
) {
IconButton(
onClick = onToggleSearch,
@ -163,6 +179,7 @@ private fun RoomListMenuItems(
contentDescription = stringResource(CommonStrings.action_search),
)
}
SpaceFilterButton(spaceFiltersState = spaceFiltersState)
if (RoomListConfig.HAS_DROP_DOWN_MENU) {
var showMenu by remember { mutableStateOf(false) }
IconButton(
@ -228,6 +245,38 @@ private fun SpacesMenuItems(
}
}
@Composable
private fun SpaceFilterButton(
spaceFiltersState: SpaceFiltersState,
) {
if (spaceFiltersState == SpaceFiltersState.Disabled) return
fun onClick() {
when (spaceFiltersState) {
is SpaceFiltersState.Unselected -> spaceFiltersState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters)
is SpaceFiltersState.Selected -> spaceFiltersState.eventSink(SpaceFiltersEvent.Selected.ClearSelection)
else -> Unit
}
}
val isSelected = spaceFiltersState is SpaceFiltersState.Selected
IconButton(
onClick = ::onClick,
colors = if (isSelected) {
IconButtonDefaults.iconButtonColors(
containerColor = ElementTheme.colors.bgAccentRest,
contentColor = ElementTheme.colors.iconOnSolidPrimary,
)
} else {
IconButtonDefaults.iconButtonColors()
},
) {
Icon(
imageVector = CompoundIcons.Filter(),
contentDescription = stringResource(R.string.screen_roomlist_your_spaces),
)
}
}
@Composable
private fun NavigationIcon(
currentUserAndNeighbors: ImmutableList<MatrixUser>,
@ -309,7 +358,6 @@ private fun AccountIcon(
internal fun HomeTopBarPreview() = ElementPreview {
HomeTopBar(
selectedNavigationItem = HomeNavigationBarItem.Chats,
title = stringResource(R.string.screen_roomlist_main_space_title),
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
showAvatarIndicator = false,
areSearchResultsDisplayed = false,
@ -322,6 +370,30 @@ internal fun HomeTopBarPreview() = ElementPreview {
canReportBug = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),
spaceFiltersState = anUnselectedSpaceFiltersState(),
onMenuActionClick = {},
)
}
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight
@Composable
internal fun HomeTopBarSpaceFiltersSelectedPreview() = ElementPreview {
HomeTopBar(
selectedNavigationItem = HomeNavigationBarItem.Chats,
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
showAvatarIndicator = false,
areSearchResultsDisplayed = false,
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
onOpenSettings = {},
onAccountSwitch = {},
onToggleSearch = {},
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),
spaceFiltersState = aSelectedSpaceFiltersState(),
onMenuActionClick = {},
)
}
@ -332,7 +404,6 @@ internal fun HomeTopBarPreview() = ElementPreview {
internal fun HomeTopBarSpacesPreview() = ElementPreview {
HomeTopBar(
selectedNavigationItem = HomeNavigationBarItem.Spaces,
title = stringResource(R.string.screen_home_tab_spaces),
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
showAvatarIndicator = false,
areSearchResultsDisplayed = false,
@ -345,6 +416,7 @@ internal fun HomeTopBarSpacesPreview() = ElementPreview {
canReportBug = true,
displayFilters = false,
filtersState = aRoomListFiltersState(),
spaceFiltersState = anUnselectedSpaceFiltersState(),
onMenuActionClick = {},
)
}
@ -355,7 +427,6 @@ internal fun HomeTopBarSpacesPreview() = ElementPreview {
internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
HomeTopBar(
selectedNavigationItem = HomeNavigationBarItem.Chats,
title = stringResource(R.string.screen_roomlist_main_space_title),
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
showAvatarIndicator = true,
areSearchResultsDisplayed = false,
@ -368,6 +439,7 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
canReportBug = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),
spaceFiltersState = anUnselectedSpaceFiltersState(),
onMenuActionClick = {},
)
}
@ -378,7 +450,6 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
internal fun HomeTopBarMultiAccountPreview() = ElementPreview {
HomeTopBar(
selectedNavigationItem = HomeNavigationBarItem.Chats,
title = stringResource(R.string.screen_roomlist_main_space_title),
currentUserAndNeighbors = aMatrixUserList().take(3).toImmutableList(),
showAvatarIndicator = false,
areSearchResultsDisplayed = false,
@ -391,6 +462,7 @@ internal fun HomeTopBarMultiAccountPreview() = ElementPreview {
canReportBug = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),
spaceFiltersState = anUnselectedSpaceFiltersState(),
onMenuActionClick = {},
)
}

View file

@ -45,6 +45,8 @@ import io.element.android.features.home.impl.roomlist.RoomListContentState
import io.element.android.features.home.impl.roomlist.RoomListContentStateProvider
import io.element.android.features.home.impl.roomlist.RoomListEvent
import io.element.android.features.home.impl.roomlist.SecurityBannerState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.home.impl.spacefilters.anUnselectedSpaceFiltersState
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
@ -59,6 +61,7 @@ import kotlinx.collections.immutable.ImmutableList
fun RoomListContentView(
contentState: RoomListContentState,
filtersState: RoomListFiltersState,
spaceFiltersState: SpaceFiltersState,
lazyListState: LazyListState,
hideInvitesAvatars: Boolean,
eventSink: (RoomListEvent) -> Unit,
@ -93,6 +96,7 @@ fun RoomListContentView(
state = contentState,
hideInvitesAvatars = hideInvitesAvatars,
filtersState = filtersState,
spaceFiltersState = spaceFiltersState,
eventSink = eventSink,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
@ -172,6 +176,7 @@ private fun RoomsView(
state: RoomListContentState.Rooms,
hideInvitesAvatars: Boolean,
filtersState: RoomListFiltersState,
spaceFiltersState: SpaceFiltersState,
eventSink: (RoomListEvent) -> Unit,
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
@ -180,9 +185,12 @@ private fun RoomsView(
lazyListState: LazyListState,
modifier: Modifier = Modifier,
) {
if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
val isSpaceFilterSelected = spaceFiltersState is SpaceFiltersState.Selected
val hasAnyFilterSelected = filtersState.hasAnyFilterSelected || isSpaceFilterSelected
if (state.summaries.isEmpty() && hasAnyFilterSelected) {
EmptyViewForFilterStates(
selectedFilters = filtersState.selectedFilters(),
isSpaceFilterSelected = isSpaceFilterSelected,
modifier = modifier.fillMaxSize()
)
} else {
@ -278,9 +286,10 @@ private fun RoomsViewList(
@Composable
private fun EmptyViewForFilterStates(
selectedFilters: ImmutableList<RoomListFilter>,
isSpaceFilterSelected: Boolean,
modifier: Modifier = Modifier,
) {
val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) ?: return
val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected) ?: return
EmptyScaffold(
title = emptyStateResources.title,
subtitle = emptyStateResources.subtitle,
@ -331,6 +340,7 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
)
}
),
spaceFiltersState = anUnselectedSpaceFiltersState(),
hideInvitesAvatars = false,
eventSink = {},
onSetUpRecoveryClick = {},

View file

@ -95,6 +95,7 @@ class RoomListRoomSummaryFactory(
content = content,
)
}
is LatestEventValue.RoomInvite -> LatestEvent.None
}
}
}

View file

@ -17,6 +17,8 @@ import io.element.android.features.home.impl.roomlist.RoomListPresenter
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.search.RoomListSearchPresenter
import io.element.android.features.home.impl.search.RoomListSearchState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersPresenter
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
@ -31,4 +33,7 @@ interface RoomListModule {
@Binds
fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter<RoomListFiltersState>
@Binds
fun bindSpaceFiltersPresenter(presenter: SpaceFiltersPresenter): Presenter<SpaceFiltersState>
}

View file

@ -9,6 +9,7 @@
package io.element.android.features.home.impl.filters
import io.element.android.features.home.impl.R
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
/**
* Enum class representing the different filters that can be applied to the room list.
@ -30,3 +31,13 @@ enum class RoomListFilter(val stringResource: Int) {
Invites -> setOf(Rooms, People, Unread, Favourites)
}
}
fun RoomListFilter.into(): MatrixRoomListFilter {
return when (this) {
RoomListFilter.Rooms -> MatrixRoomListFilter.Category.Group
RoomListFilter.People -> MatrixRoomListFilter.Category.People
RoomListFilter.Unread -> MatrixRoomListFilter.Unread
RoomListFilter.Favourites -> MatrixRoomListFilter.Favorite
RoomListFilter.Invites -> MatrixRoomListFilter.Invite
}
}

View file

@ -24,8 +24,12 @@ data class RoomListFiltersEmptyStateResources(
/**
* Create a [RoomListFiltersEmptyStateResources] from a list of selected filters.
*/
fun fromSelectedFilters(selectedFilters: List<RoomListFilter>): RoomListFiltersEmptyStateResources? {
fun fromSelectedFilters(selectedFilters: List<RoomListFilter>, isSpaceFilterSelected: Boolean): RoomListFiltersEmptyStateResources? {
return when {
isSpaceFilterSelected -> RoomListFiltersEmptyStateResources(
title = R.string.screen_roomlist_filter_mixed_empty_state_title,
subtitle = R.string.screen_roomlist_filter_mixed_empty_state_subtitle
)
selectedFilters.isEmpty() -> null
selectedFilters.size == 1 -> {
when (selectedFilters.first()) {

View file

@ -9,24 +9,17 @@
package io.element.android.features.home.impl.filters
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import dev.zacsweers.metro.Inject
import io.element.android.features.home.impl.datasource.RoomListDataSource
import io.element.android.features.home.impl.filters.selection.FilterSelectionStrategy
import io.element.android.libraries.architecture.Presenter
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
@Inject
class RoomListFiltersPresenter(
private val roomListDataSource: RoomListDataSource,
private val filterSelectionStrategy: FilterSelectionStrategy,
) : Presenter<RoomListFiltersState> {
private val initialFilters = filterSelectionStrategy.filterSelectionStates.value.toImmutableList()
@Composable
override fun present(): RoomListFiltersState {
fun handleEvent(event: RoomListFiltersEvent) {
@ -40,31 +33,9 @@ class RoomListFiltersPresenter(
}
}
val filters by produceState(initialValue = initialFilters) {
filterSelectionStrategy.filterSelectionStates
.map { filters ->
value = filters.toImmutableList()
filters.mapNotNull { filterState ->
if (!filterState.isSelected) {
return@mapNotNull null
}
when (filterState.filter) {
RoomListFilter.Rooms -> MatrixRoomListFilter.Category.Group
RoomListFilter.People -> MatrixRoomListFilter.Category.People
RoomListFilter.Unread -> MatrixRoomListFilter.Unread
RoomListFilter.Favourites -> MatrixRoomListFilter.Favorite
RoomListFilter.Invites -> MatrixRoomListFilter.Invite
}
}
}
.collectLatest { filters ->
val result = MatrixRoomListFilter.All(filters)
roomListDataSource.updateFilter(result)
}
}
val filters by filterSelectionStrategy.filterSelectionStates.collectAsState()
return RoomListFiltersState(
filterSelectionStates = filters,
filterSelectionStates = filters.toImmutableList(),
eventSink = ::handleEvent,
)
}

View file

@ -16,6 +16,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
@ContributesBinding(SessionScope::class)
class DefaultFilterSelectionStrategy : FilterSelectionStrategy {
private val selectedFilters = LinkedHashSet<RoomListFilter>()
private val availableFilters
get() = RoomListFilter.entries.toSet()
override val filterSelectionStates = MutableStateFlow(buildFilters())
@ -45,7 +47,7 @@ class DefaultFilterSelectionStrategy : FilterSelectionStrategy {
isSelected = true
)
}
val unselectedFilters = RoomListFilter.entries - selectedFilters - selectedFilters.flatMap { it.incompatibleFilters }.toSet()
val unselectedFilters = availableFilters - selectedFilters - selectedFilters.flatMap { it.incompatibleFilters }.toSet()
val unselectedFilterStates = unselectedFilters.map {
FilterSelectionState(
filter = it,

View file

@ -13,7 +13,6 @@ import kotlinx.coroutines.flow.StateFlow
interface FilterSelectionStrategy {
val filterSelectionStates: StateFlow<Set<FilterSelectionState>>
fun select(filter: RoomListFilter)
fun deselect(filter: RoomListFilter)
fun isSelected(filter: RoomListFilter): Boolean

View file

@ -28,9 +28,14 @@ import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.datasource.RoomListDataSource
import io.element.android.features.home.impl.filters.RoomListFilter.Rooms
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.filters.into
import io.element.android.features.home.impl.search.RoomListSearchEvent
import io.element.android.features.home.impl.search.RoomListSearchState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.home.impl.spacefilters.into
import io.element.android.features.home.impl.spacefilters.selectedFilter
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.AcceptInvite
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.DeclineInvite
@ -44,6 +49,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
@ -83,6 +89,7 @@ class RoomListPresenter(
private val seenInvitesStore: SeenInvitesStore,
private val announcementService: AnnouncementService,
private val coldStartWatcher: AnalyticsColdStartWatcher,
private val spaceFiltersPresenter: Presenter<SpaceFiltersState>,
) : Presenter<RoomListState> {
private val encryptionService = client.encryptionService
@ -92,6 +99,7 @@ class RoomListPresenter(
val leaveRoomState = leaveRoomPresenter.present()
val filtersState = filtersPresenter.present()
val searchState = searchPresenter.present()
val spaceFiltersState = spaceFiltersPresenter.present()
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
LaunchedEffect(Unit) {
@ -150,6 +158,13 @@ class RoomListPresenter(
}
}
LaunchedEffect(filtersState.filterSelectionStates, spaceFiltersState.selectedFilter()) {
val selectedFilters = filtersState.selectedFilters().map { filter -> filter.into() }
val selectedSpaceFilter = spaceFiltersState.selectedFilter().into()
val allFilters = RoomListFilter.All(selectedFilters + listOfNotNull(selectedSpaceFilter))
roomListDataSource.updateFilter(allFilters)
}
val contentState = roomListContentState(
securityBannerDismissed,
showNewNotificationSoundBanner,
@ -163,6 +178,7 @@ class RoomListPresenter(
leaveRoomState = leaveRoomState,
filtersState = filtersState,
searchState = searchState,
spaceFiltersState = spaceFiltersState,
contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState,
hideInvitesAvatars = hideInvitesAvatar,

View file

@ -12,6 +12,7 @@ import androidx.compose.runtime.Immutable
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.search.RoomListSearchState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
@ -26,6 +27,7 @@ data class RoomListState(
val leaveRoomState: LeaveRoomState,
val filtersState: RoomListFiltersState,
val searchState: RoomListSearchState,
val spaceFiltersState: SpaceFiltersState,
val contentState: RoomListContentState,
val acceptDeclineInviteState: AcceptDeclineInviteState,
val hideInvitesAvatars: Boolean,

View file

@ -18,6 +18,8 @@ import io.element.android.features.home.impl.model.aRoomListRoomSummary
import io.element.android.features.home.impl.model.anInviteSender
import io.element.android.features.home.impl.search.RoomListSearchState
import io.element.android.features.home.impl.search.aRoomListSearchState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.home.impl.spacefilters.anUnselectedSpaceFiltersState
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
import io.element.android.features.leaveroom.api.LeaveRoomEvent
@ -52,6 +54,7 @@ internal fun aRoomListState(
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
searchState: RoomListSearchState = aRoomListSearchState(),
filtersState: RoomListFiltersState = aRoomListFiltersState(),
spaceFiltersState: SpaceFiltersState = anUnselectedSpaceFiltersState(),
contentState: RoomListContentState = aRoomsContentState(),
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
hideInvitesAvatars: Boolean = false,
@ -63,6 +66,7 @@ internal fun aRoomListState(
leaveRoomState = leaveRoomState,
filtersState = filtersState,
searchState = searchState,
spaceFiltersState = spaceFiltersState,
contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState,
hideInvitesAvatars = hideInvitesAvatars,

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2026 Element Creations 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.home.impl.spacefilters
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
sealed interface SpaceFiltersEvent {
// Only valid in Unselected state
sealed interface Unselected : SpaceFiltersEvent {
data object ShowFilters : Unselected
}
// Only valid in Selecting state
sealed interface Selecting : SpaceFiltersEvent {
data object Cancel : Selecting
data class SelectFilter(val spaceFilter: SpaceServiceFilter) : Selecting
}
// Only valid in Selected state
sealed interface Selected : SpaceFiltersEvent {
data object ClearSelection : Selected
}
}

View file

@ -0,0 +1,113 @@
/*
* Copyright (c) 2026 Element Creations 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.home.impl.spacefilters
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.map
@Inject
class SpaceFiltersPresenter(
private val featureFlagService: FeatureFlagService,
private val matrixClient: MatrixClient,
) : Presenter<SpaceFiltersState> {
@Composable
override fun present(): SpaceFiltersState {
val isFeatureEnabled by featureFlagService
.isFeatureEnabledFlow(FeatureFlags.RoomListSpaceFilters)
.collectAsState(initial = false)
val availableFilters by remember {
matrixClient.spaceService.spaceFiltersFlow.map { it.toImmutableList() }
}.collectAsState(initial = persistentListOf())
if (!isFeatureEnabled || availableFilters.isEmpty()) {
return SpaceFiltersState.Disabled
}
var selectionMode by remember { mutableStateOf<SelectionMode>(SelectionMode.Unselected) }
fun handleUnselectedEvent(event: SpaceFiltersEvent.Unselected) {
when (event) {
SpaceFiltersEvent.Unselected.ShowFilters -> {
selectionMode = SelectionMode.Selecting
}
}
}
fun handleSelectingEvent(event: SpaceFiltersEvent.Selecting) {
when (event) {
SpaceFiltersEvent.Selecting.Cancel -> {
selectionMode = SelectionMode.Unselected
}
is SpaceFiltersEvent.Selecting.SelectFilter -> {
selectionMode = SelectionMode.Selected(event.spaceFilter)
}
}
}
fun handleSelectedEvent(event: SpaceFiltersEvent.Selected) {
when (event) {
SpaceFiltersEvent.Selected.ClearSelection -> {
selectionMode = SelectionMode.Unselected
}
}
}
return when (val mode = selectionMode) {
SelectionMode.Unselected -> SpaceFiltersState.Unselected(
eventSink = ::handleUnselectedEvent,
)
SelectionMode.Selecting -> {
val searchQuery = rememberTextFieldState()
SpaceFiltersState.Selecting(
availableFilters = availableFilters,
searchQuery = searchQuery,
eventSink = ::handleSelectingEvent,
)
}
is SelectionMode.Selected -> {
var selectedFilter by remember { mutableStateOf(mode.filter) }
// Makes sure the selectedFilter stays in sync with the available filters
LaunchedEffect(availableFilters) {
val upToDateFilter = availableFilters
.firstOrNull { it.spaceRoom.roomId == mode.filter.spaceRoom.roomId }
if (upToDateFilter == null) {
selectionMode = SelectionMode.Unselected
} else {
selectedFilter = upToDateFilter
}
}
SpaceFiltersState.Selected(
selectedFilter = selectedFilter,
eventSink = ::handleSelectedEvent,
)
}
}
}
}
private sealed interface SelectionMode {
data object Unselected : SelectionMode
data object Selecting : SelectionMode
data class Selected(val filter: SpaceServiceFilter) : SelectionMode
}

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2026 Element Creations 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.home.impl.spacefilters
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Immutable
sealed interface SpaceFiltersState {
data object Disabled : SpaceFiltersState
data class Unselected(
val eventSink: (SpaceFiltersEvent.Unselected) -> Unit,
) : SpaceFiltersState
data class Selecting(
val availableFilters: ImmutableList<SpaceServiceFilter>,
val searchQuery: TextFieldState,
val eventSink: (SpaceFiltersEvent.Selecting) -> Unit,
) : SpaceFiltersState {
val visibleFilters: ImmutableList<SpaceServiceFilter>
get() {
val query = searchQuery.text.toString()
if (query.isBlank()) return availableFilters
return availableFilters.filter { filter ->
filter.spaceRoom.displayName.contains(query, ignoreCase = true) ||
(filter.spaceRoom.canonicalAlias?.value ?: "").contains(query, ignoreCase = true)
}.toImmutableList()
}
}
data class Selected(
val selectedFilter: SpaceServiceFilter,
val eventSink: (SpaceFiltersEvent.Selected) -> Unit,
) : SpaceFiltersState
}
fun SpaceFiltersState.selectedFilter(): SpaceServiceFilter? {
return when (this) {
is SpaceFiltersState.Selected -> this.selectedFilter
else -> null
}
}
fun SpaceServiceFilter?.into(): RoomListFilter? {
return this?.let { RoomListFilter.Identifiers(descendants) }
}

View file

@ -0,0 +1,81 @@
/*
* Copyright (c) 2026 Element Creations 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.home.impl.spacefilters
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
import io.element.android.libraries.previewutils.room.aSpaceRoom
import kotlinx.collections.immutable.toImmutableList
class SpaceFiltersStateProvider : PreviewParameterProvider<SpaceFiltersState> {
override val values: Sequence<SpaceFiltersState>
get() = sequenceOf(
aSelectingSpaceFiltersState(),
aSelectingSpaceFiltersState(searchQuery = "Pr")
)
}
fun aDisabledSpaceFiltersState() = SpaceFiltersState.Disabled
fun anUnselectedSpaceFiltersState(
eventSink: (SpaceFiltersEvent.Unselected) -> Unit = {},
) = SpaceFiltersState.Unselected(
eventSink = eventSink,
)
fun aSelectingSpaceFiltersState(
availableFilters: List<SpaceServiceFilter> = listOf(
aSpaceServiceFilter(
displayName = "Work",
canonicalAlias = RoomAlias("#work:example.com"),
),
aSpaceServiceFilter(
displayName = "Personal",
roomId = RoomId("!personal:example.com"),
),
aSpaceServiceFilter(
displayName = "Projects",
roomId = RoomId("!projects:example.com"),
canonicalAlias = RoomAlias("#projects:example.com"),
level = 1,
),
aSpaceServiceFilter(
displayName = "Gaming",
roomId = RoomId("!gaming:example.com"),
),
),
searchQuery: String = "",
eventSink: (SpaceFiltersEvent.Selecting) -> Unit = {},
) = SpaceFiltersState.Selecting(
availableFilters = availableFilters.toImmutableList(),
searchQuery = TextFieldState(searchQuery),
eventSink = eventSink,
)
fun aSelectedSpaceFiltersState(
selectedFilter: SpaceServiceFilter = aSpaceServiceFilter(displayName = "Work"),
eventSink: (SpaceFiltersEvent.Selected) -> Unit = {},
) = SpaceFiltersState.Selected(
selectedFilter = selectedFilter,
eventSink = eventSink,
)
fun aSpaceServiceFilter(
displayName: String = "Space",
roomId: RoomId = RoomId("!space:example.com"),
canonicalAlias: RoomAlias? = null,
level: Int = 0,
descendants: List<RoomId> = emptyList(),
) = SpaceServiceFilter(
spaceRoom = aSpaceRoom(displayName = displayName, roomId = roomId, canonicalAlias = canonicalAlias),
level = level,
descendants = descendants,
)

View file

@ -0,0 +1,190 @@
/*
* Copyright (c) 2026 Element Creations 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.home.impl.spacefilters
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetValue
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.home.impl.R
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.SearchField
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SpaceFiltersView(
state: SpaceFiltersState,
modifier: Modifier = Modifier
) {
val isSelecting by rememberUpdatedState(state is SpaceFiltersState.Selecting)
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
confirmValueChange = { sheetValueTarget ->
// This ensures the hide animation is not cancelled
when (sheetValueTarget) {
SheetValue.Expanded -> isSelecting
else -> true
}
}
)
LaunchedEffect(isSelecting) {
if (!isSelecting) {
sheetState.hide()
}
}
if (sheetState.isVisible || isSelecting) {
ModalBottomSheet(
modifier = modifier
.systemBarsPadding()
.navigationBarsPadding(),
sheetState = sheetState,
onDismissRequest = {
if (state is SpaceFiltersState.Selecting) {
state.eventSink(SpaceFiltersEvent.Selecting.Cancel)
}
}
) {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.9f)
) {
if (state is SpaceFiltersState.Selecting) {
SpaceFiltersBottomSheetContent(
filters = state.visibleFilters,
searchQuery = state.searchQuery,
onFilterSelected = { filter ->
state.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(filter))
}
)
}
}
}
}
}
@Composable
private fun SpaceFiltersBottomSheetContent(
filters: List<SpaceServiceFilter>,
searchQuery: TextFieldState,
onFilterSelected: (SpaceServiceFilter) -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier.padding(vertical = 16.dp)
) {
Text(
text = stringResource(R.string.screen_roomlist_your_spaces),
style = ElementTheme.typography.fontHeadingSmMedium,
modifier = Modifier.padding(horizontal = 16.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Spacer(modifier = Modifier.height(12.dp))
SearchField(
state = searchQuery,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
placeholder = stringResource(CommonStrings.action_search),
)
Spacer(modifier = Modifier.height(16.dp))
LazyColumn {
items(filters) { filter ->
SpaceFilterItem(
filter = filter,
onClick = { onFilterSelected(filter) }
)
}
}
}
}
@Composable
private fun SpaceFilterItem(
filter: SpaceServiceFilter,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val spaceRoom = filter.spaceRoom
val supportingText = spaceRoom.canonicalAlias?.value
Row(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
// Level-based indentation
Spacer(modifier = Modifier.width((16 * filter.level).dp))
Avatar(
avatarData = spaceRoom.getAvatarData(AvatarSize.RoomSelectRoomListItem),
avatarType = AvatarType.Space(),
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = spaceRoom.displayName,
style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.colors.textPrimary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (supportingText != null) {
Text(
text = supportingText,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
@PreviewsDayNight
@Composable
internal fun SpaceFiltersViewPreview(@PreviewParameter(SpaceFiltersStateProvider::class) state: SpaceFiltersState) = ElementPreview {
SpaceFiltersView(state = state)
}

View file

@ -36,7 +36,7 @@ class HomeSpacesPresenter(
val canCreateSpaces by featureFlagsService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false)
val hideInvitesAvatar by client.rememberHideInvitesAvatar()
val spaceRooms by remember {
client.spaceService.spaceRoomsFlow.map { it.toImmutableList() }
client.spaceService.topLevelSpacesFlow.map { it.toImmutableList() }
}.collectAsState(persistentListOf())
val seenSpaceInvites by remember {

View file

@ -50,6 +50,7 @@ Nemáte žádné nepřečtené zprávy!"</string>
<string name="screen_roomlist_mark_as_read">"Označit jako přečtené"</string>
<string name="screen_roomlist_mark_as_unread">"Označit jako nepřečtené"</string>
<string name="screen_roomlist_tombstoned_room_description">"Tato místnost byla aktualizována"</string>
<string name="screen_roomlist_your_spaces">"Vaše prostory"</string>
<string name="session_verification_banner_message">"Zdá se, že používáte nové zařízení. Ověřte přihlášení, abyste měli přístup k zašifrovaným zprávám."</string>
<string name="session_verification_banner_title">"Ověřte, že jste to vy"</string>
</resources>

View file

@ -16,14 +16,14 @@ class RoomListFiltersEmptyStateResourcesTest {
@Test
fun `fromSelectedFilters should return null when selectedFilters is empty`() {
val selectedFilters = emptyList<RoomListFilter>()
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
assertThat(result).isNull()
}
@Test
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only unread filter`() {
val selectedFilters = listOf(RoomListFilter.Unread)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
assertThat(result).isNotNull()
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_unreads_empty_state_title)
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
@ -32,7 +32,7 @@ class RoomListFiltersEmptyStateResourcesTest {
@Test
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only people filter`() {
val selectedFilters = listOf(RoomListFilter.People)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
assertThat(result).isNotNull()
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_people_empty_state_title)
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
@ -41,7 +41,7 @@ class RoomListFiltersEmptyStateResourcesTest {
@Test
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only rooms filter`() {
val selectedFilters = listOf(RoomListFilter.Rooms)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
assertThat(result).isNotNull()
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_rooms_empty_state_title)
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
@ -50,7 +50,7 @@ class RoomListFiltersEmptyStateResourcesTest {
@Test
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only favourites filter`() {
val selectedFilters = listOf(RoomListFilter.Favourites)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
assertThat(result).isNotNull()
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_favourites_empty_state_title)
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_favourites_empty_state_subtitle)
@ -59,7 +59,7 @@ class RoomListFiltersEmptyStateResourcesTest {
@Test
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only invites filter`() {
val selectedFilters = listOf(RoomListFilter.Invites)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
assertThat(result).isNotNull()
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_invites_empty_state_title)
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
@ -68,7 +68,15 @@ class RoomListFiltersEmptyStateResourcesTest {
@Test
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has multiple filters`() {
val selectedFilters = listOf(RoomListFilter.Unread, RoomListFilter.People, RoomListFilter.Rooms, RoomListFilter.Favourites)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
assertThat(result).isNotNull()
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_title)
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
}
@Test
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when isSpaceFilterSelected is true`() {
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(emptyList(), isSpaceFilterSelected = true)
assertThat(result).isNotNull()
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_title)
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)

View file

@ -9,23 +9,10 @@
package io.element.android.features.home.impl.filters
import com.google.common.truth.Truth.assertThat
import io.element.android.features.home.impl.FakeDateTimeObserver
import io.element.android.features.home.impl.datasource.RoomListDataSource
import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFactory
import io.element.android.features.home.impl.filters.selection.DefaultFilterSelectionStrategy
import io.element.android.features.home.impl.filters.selection.FilterSelectionState
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
@ -54,8 +41,7 @@ class RoomListFiltersPresenterTest {
@Test
@OptIn(ExperimentalCoroutinesApi::class)
fun `present - toggle rooms filter`() = runTest {
val roomListService = FakeRoomListService()
val presenter = createRoomListFiltersPresenter(roomListService)
val presenter = createRoomListFiltersPresenter()
presenter.test {
awaitItem().eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms))
awaitLastSequentialItem().let { state ->
@ -89,8 +75,7 @@ class RoomListFiltersPresenterTest {
@Test
@OptIn(ExperimentalCoroutinesApi::class)
fun `present - clear filters event`() = runTest {
val roomListService = FakeRoomListService()
val presenter = createRoomListFiltersPresenter(roomListService)
val presenter = createRoomListFiltersPresenter()
presenter.test {
awaitItem().eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms))
awaitLastSequentialItem().let { state ->
@ -110,25 +95,8 @@ private fun filterSelectionState(filter: RoomListFilter, selected: Boolean) = Fi
isSelected = selected,
)
private fun TestScope.createRoomListFiltersPresenter(
roomListService: RoomListService = FakeRoomListService(),
notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
dateFormatter: DateFormatter = FakeDateFormatter(),
roomLatestEventFormatter: RoomLatestEventFormatter = FakeRoomLatestEventFormatter(),
): RoomListFiltersPresenter {
private fun TestScope.createRoomListFiltersPresenter(): RoomListFiltersPresenter {
return RoomListFiltersPresenter(
roomListDataSource = RoomListDataSource(
roomListService = roomListService,
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
dateFormatter = dateFormatter,
roomLatestEventFormatter = roomLatestEventFormatter,
),
coroutineDispatchers = testCoroutineDispatchers(),
notificationSettingsService = notificationSettingsService,
sessionCoroutineScope = backgroundScope,
dateTimeObserver = FakeDateTimeObserver(),
analyticsService = FakeAnalyticsService(),
),
filterSelectionStrategy = DefaultFilterSelectionStrategy(),
)
}

View file

@ -21,6 +21,8 @@ import io.element.android.features.home.impl.model.createRoomListRoomSummary
import io.element.android.features.home.impl.search.RoomListSearchEvent
import io.element.android.features.home.impl.search.RoomListSearchState
import io.element.android.features.home.impl.search.aRoomListSearchState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.home.impl.spacefilters.aDisabledSpaceFiltersState
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
@ -660,6 +662,7 @@ class RoomListPresenterTest {
analyticsService: AnalyticsService = FakeAnalyticsService(),
filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() },
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
spaceFiltersPresenter: Presenter<SpaceFiltersState> = Presenter { aDisabledSpaceFiltersState() },
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
@ -683,6 +686,7 @@ class RoomListPresenterTest {
searchPresenter = searchPresenter,
sessionPreferencesStore = sessionPreferencesStore,
filtersPresenter = filtersPresenter,
spaceFiltersPresenter = spaceFiltersPresenter,
analyticsService = analyticsService,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() },

View file

@ -0,0 +1,313 @@
/*
* Copyright (c) 2026 Element Creations 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.home.impl.spacefilters
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
import io.element.android.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class SpaceFiltersPresenterTest {
@Test
fun `present - when feature flag is disabled returns Disabled state`() = runTest {
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to false)
)
)
presenter.test {
val state = awaitItem()
assertThat(state).isEqualTo(SpaceFiltersState.Disabled)
}
}
@Test
fun `present - when available filters is empty returns Disabled state`() = runTest {
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
)
)
presenter.test {
val state = awaitLastSequentialItem()
assertThat(state).isEqualTo(SpaceFiltersState.Disabled)
}
}
@Test
fun `present - when feature flag is enabled and filters exist returns Unselected state`() = runTest {
val spaceFilter = aSpaceServiceFilter(displayName = "Test Space")
val spaceService = FakeSpaceService()
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
// Emit filters
spaceService.emitSpaceFilters(listOf(spaceFilter))
val state = awaitLastSequentialItem()
assertThat(state).isInstanceOf(SpaceFiltersState.Unselected::class.java)
}
}
@Test
fun `present - ShowFilters event transitions from Unselected to Selecting`() = runTest {
val spaceFilter = aSpaceServiceFilter(displayName = "Test Space")
val spaceService = FakeSpaceService()
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
// Emit filters first
spaceService.emitSpaceFilters(listOf(spaceFilter))
val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected
unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters)
val selectingState = awaitLastSequentialItem()
assertThat(selectingState).isInstanceOf(SpaceFiltersState.Selecting::class.java)
}
}
@Test
fun `present - Cancel event in Selecting state transitions back to Unselected`() = runTest {
val spaceFilter = aSpaceServiceFilter(displayName = "Test Space")
val spaceService = FakeSpaceService()
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
// Emit filters first
spaceService.emitSpaceFilters(listOf(spaceFilter))
// Start in Unselected
val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected
unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters)
// Now in Selecting
val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting
selectingState.eventSink(SpaceFiltersEvent.Selecting.Cancel)
// Back to Unselected
val finalState = awaitLastSequentialItem()
assertThat(finalState).isInstanceOf(SpaceFiltersState.Unselected::class.java)
}
}
@Test
fun `present - SelectFilter event in Selecting state transitions to Selected`() = runTest {
val spaceFilter = aSpaceServiceFilter(displayName = "Test Space")
val spaceService = FakeSpaceService()
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
// Emit filters first
spaceService.emitSpaceFilters(listOf(spaceFilter))
// Start in Unselected
val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected
unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters)
// Now in Selecting
val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting
selectingState.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(spaceFilter))
// Now in Selected
val selectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected
assertThat(selectedState.selectedFilter).isEqualTo(spaceFilter)
}
}
@Test
fun `present - ClearSelection event in Selected state transitions back to Unselected`() = runTest {
val spaceFilter = aSpaceServiceFilter(displayName = "Test Space")
val spaceService = FakeSpaceService()
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
// Emit filters first
spaceService.emitSpaceFilters(listOf(spaceFilter))
// Start in Unselected
val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected
unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters)
// Now in Selecting
val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting
selectingState.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(spaceFilter))
// Now in Selected
val selectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected
selectedState.eventSink(SpaceFiltersEvent.Selected.ClearSelection)
// Back to Unselected
val finalState = awaitLastSequentialItem()
assertThat(finalState).isInstanceOf(SpaceFiltersState.Unselected::class.java)
}
}
@Test
fun `present - available filters are passed from SpaceService`() = runTest {
val spaceFilter1 = aSpaceServiceFilter(displayName = "Work", roomId = RoomId("!work:example.com"))
val spaceFilter2 = aSpaceServiceFilter(displayName = "Personal", roomId = RoomId("!personal:example.com"))
val spaceFilters = listOf(spaceFilter1, spaceFilter2)
val spaceService = FakeSpaceService()
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
// Emit space filters
spaceService.emitSpaceFilters(spaceFilters)
// Start in Unselected
val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected
unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters)
// Now in Selecting with available filters
val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting
assertThat(selectingState.availableFilters).containsExactly(spaceFilter1, spaceFilter2).inOrder()
}
}
@Test
fun `present - selected filter is cleared when space is removed from available filters`() = runTest {
val spaceFilter = aSpaceServiceFilter(displayName = "Work", roomId = RoomId("!work:example.com"))
val otherSpaceFilter = aSpaceServiceFilter(displayName = "Personal", roomId = RoomId("!personal:example.com"))
val spaceService = FakeSpaceService()
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
// Emit filters first
spaceService.emitSpaceFilters(listOf(spaceFilter, otherSpaceFilter))
// Go to Selecting
val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected
unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters)
// Select the filter
val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting
selectingState.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(spaceFilter))
// Verify in Selected state
val selectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected
assertThat(selectedState.selectedFilter).isEqualTo(spaceFilter)
// Remove the selected space from available filters (but keep other spaces)
spaceService.emitSpaceFilters(listOf(otherSpaceFilter))
// Should auto-transition to Unselected
val finalState = awaitLastSequentialItem()
assertThat(finalState).isInstanceOf(SpaceFiltersState.Unselected::class.java)
}
}
@Test
fun `present - selected filter stays in sync when available filters update`() = runTest {
val originalFilter = aSpaceServiceFilter(
displayName = "Work",
roomId = RoomId("!work:example.com"),
descendants = listOf(RoomId("!room1:example.com"))
)
val updatedFilter = aSpaceServiceFilter(
displayName = "Work",
roomId = RoomId("!work:example.com"),
descendants = listOf(RoomId("!room1:example.com"), RoomId("!room2:example.com"))
)
val spaceService = FakeSpaceService()
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
// Emit initial space filters
spaceService.emitSpaceFilters(listOf(originalFilter))
// Start in Unselected
val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected
unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters)
// Now in Selecting
val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting
selectingState.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(originalFilter))
// Now in Selected
val selectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected
assertThat(selectedState.selectedFilter.descendants).hasSize(1)
// Emit updated space filters
spaceService.emitSpaceFilters(listOf(updatedFilter))
// Selected filter should be updated
val updatedSelectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected
assertThat(updatedSelectedState.selectedFilter.descendants).hasSize(2)
}
}
private fun createSpaceFiltersPresenter(
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
matrixClient: FakeMatrixClient = FakeMatrixClient(),
): SpaceFiltersPresenter {
return SpaceFiltersPresenter(
featureFlagService = featureFlagService,
matrixClient = matrixClient,
)
}
}

View file

@ -0,0 +1,80 @@
/*
* Copyright (c) 2026 Element Creations 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.home.impl.spacefilters
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
import io.element.android.tests.testutils.EventsRecorder
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SpaceFiltersViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on a filter with alias shows display name and alias`() {
val filter = aSpaceServiceFilter(
displayName = "Test Space",
canonicalAlias = A_ROOM_ALIAS,
)
val eventsRecorder = EventsRecorder<SpaceFiltersEvent.Selecting>()
rule.setSpaceFiltersView(
state = aSelectingSpaceFiltersState(
availableFilters = listOf(filter),
eventSink = eventsRecorder,
)
)
// Both display name and alias should be visible
rule.onNodeWithText(filter.spaceRoom.displayName).assertExists()
rule.onNodeWithText(A_ROOM_ALIAS.value).assertExists()
rule.onNodeWithText(filter.spaceRoom.displayName).performClick()
eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter))
}
@Test
fun `multiple filters are displayed and clickable`() {
val filter1 = aSpaceServiceFilter(displayName = "Space One")
val filter2 = aSpaceServiceFilter(displayName = "Space Two")
val eventsRecorder = EventsRecorder<SpaceFiltersEvent.Selecting>()
rule.setSpaceFiltersView(
state = aSelectingSpaceFiltersState(
availableFilters = listOf(filter1, filter2),
eventSink = eventsRecorder,
)
)
// Both filters should be visible
rule.onNodeWithText(filter1.spaceRoom.displayName).assertExists()
rule.onNodeWithText(filter2.spaceRoom.displayName).assertExists()
// Click on second filter
rule.onNodeWithText(filter2.spaceRoom.displayName).performClick()
eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter2))
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceFiltersView(
state: SpaceFiltersState,
) {
setContent {
SpaceFiltersView(state = state)
}
}

View file

@ -1,16 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_link_new_device_desktop_scanning_title">"Skann QR-koden"</string>
<string name="screen_link_new_device_desktop_step1">"Åpne %1$s på en bærbar eller stasjonær datamaskin"</string>
<string name="screen_link_new_device_desktop_step3">"Skann QR-koden med denne enheten"</string>
<string name="screen_link_new_device_desktop_submit">"Klar til å skanne"</string>
<string name="screen_link_new_device_desktop_title">"Åpne %1$s på en datamaskin for å få QR-koden"</string>
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Tallene stemmer ikke overens"</string>
<string name="screen_link_new_device_enter_number_notice">"Skriv inn 2-sifret kode"</string>
<string name="screen_link_new_device_enter_number_subtitle">"Dette vil bekrefte at forbindelsen til den andre enheten din er sikker."</string>
<string name="screen_link_new_device_enter_number_title">"Skriv inn nummeret som vises på den andre enheten din"</string>
<string name="screen_link_new_device_error_app_not_supported_subtitle">"Kontotilbyderen din støtter ikke %1$s."</string>
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s støttes ikke"</string>
<string name="screen_link_new_device_error_not_supported_subtitle">"Din kontoleverandør støtter ikke pålogging på en ny enhet med QR-kode."</string>
<string name="screen_link_new_device_error_not_supported_title">"QR-kode støttes ikke"</string>
<string name="screen_link_new_device_error_request_cancelled_subtitle">"Påloggingen ble kansellert på den andre enheten."</string>
<string name="screen_link_new_device_error_request_cancelled_title">"Påloggingsforespørsel kansellert"</string>
<string name="screen_link_new_device_error_request_timeout_subtitle">"Påloggingen er utløpt. Vennligst prøv igjen."</string>
<string name="screen_link_new_device_error_request_timeout_title">"Påloggingen ble ikke fullført i tide"</string>
<string name="screen_link_new_device_mobile_step1">"Åpne %1$s på den andre enheten"</string>
<string name="screen_link_new_device_mobile_step2">"Velg %1$s"</string>
<string name="screen_link_new_device_mobile_step2_action">"Logg inn med QR-kode"</string>
<string name="screen_link_new_device_mobile_step3">"Skann QR-koden som vises her med den andre enheten"</string>
<string name="screen_link_new_device_mobile_title">"Åpne %1$s på den andre enheten"</string>
<string name="screen_link_new_device_root_desktop_computer">"Datamaskin"</string>
<string name="screen_link_new_device_root_loading_qr_code">"Laster QR-kode…"</string>
<string name="screen_link_new_device_root_mobile_device">"Mobil enhet"</string>
<string name="screen_link_new_device_root_title">"Hvilken type enhet ønsker du å koble til?"</string>
<string name="screen_link_new_device_wrong_number_subtitle">"Prøv igjen og påse at du har tastet inn den tosifrede koden riktig. Hvis tallene fortsatt ikke stemmer, må du kontakte kontoleverandøren din."</string>
<string name="screen_link_new_device_wrong_number_title">"Tallene stemmer ikke overens"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"En sikker tilkobling kunne ikke opprettes til den nye enheten. Dine eksisterende enheter er fortsatt trygge, og du trenger ikke å bekymre deg for dem."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Hva nå?"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Prøv å logge på igjen med en QR-kode i tilfelle dette var et nettverksproblem"</string>
@ -21,6 +38,8 @@
<string name="screen_qr_code_login_error_cancelled_title">"Påloggingsforespørsel kansellert"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Påloggingen ble avvist på den andre enheten."</string>
<string name="screen_qr_code_login_error_declined_title">"Pålogging avslått"</string>
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Du trenger ikke å gjøre noe annet."</string>
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Din andre enhet er allerede logget inn"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Påloggingen er utløpt. Vennligst prøv igjen."</string>
<string name="screen_qr_code_login_error_expired_title">"Påloggingen ble ikke fullført i tide"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Den andre enheten din støtter ikke pålogging på %s med en QR-kode.

View file

@ -119,7 +119,8 @@ class QrCodeLoginFlowNode(
is QrLoginException.Cancelled -> {
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.Cancelled))
}
is QrLoginException.Expired -> {
is QrLoginException.Expired,
is QrLoginException.NotFound -> {
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.Expired))
}
is QrLoginException.Declined -> {
@ -138,7 +139,9 @@ class QrCodeLoginFlowNode(
Timber.e(error, "OIDC metadata is invalid")
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.UnknownError))
}
else -> {
QrLoginException.CheckCodeAlreadySent,
QrLoginException.CheckCodeCannotBeSent,
QrLoginException.Unknown -> {
Timber.e(error, "Unknown error found")
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.UnknownError))
}

View file

@ -60,6 +60,8 @@
<string name="screen_qr_code_login_error_cancelled_title">"Påloggingsforespørsel kansellert"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Påloggingen ble avvist på den andre enheten."</string>
<string name="screen_qr_code_login_error_declined_title">"Pålogging avslått"</string>
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Du trenger ikke å gjøre noe annet."</string>
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Din andre enhet er allerede logget inn"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Påloggingen er utløpt. Vennligst prøv igjen."</string>
<string name="screen_qr_code_login_error_expired_title">"Påloggingen ble ikke fullført i tide"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Den andre enheten din støtter ikke pålogging på %s med en QR-kode.

View file

@ -61,6 +61,9 @@ class QrCodeLoginFlowNodeTest {
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.Expired)
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.Expired))
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.NotFound)
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.Expired))
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.Declined)
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.Declined))

View file

@ -17,6 +17,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
@ -69,6 +70,7 @@ class DefaultVoiceMessageComposerPresenter(
}
private val permissionsPresenter = permissionsPresenterFactory.create(Manifest.permission.RECORD_AUDIO)
private var pendingEvent: VoiceMessageRecorderEvent.Start? = null
private val mediaSender = mediaSenderFactory.create(timelineMode)
@Composable
@ -77,8 +79,7 @@ class DefaultVoiceMessageComposerPresenter(
val recorderState by voiceRecorder.state.collectAsState(initial = VoiceRecorderState.Idle)
val playerState by player.state.collectAsState(initial = VoiceMessageComposerPlayer.State.Initial)
val keepScreenOn by remember { derivedStateOf { recorderState is VoiceRecorderState.Recording } }
val permissionState = permissionsPresenter.present()
val permissionState by rememberUpdatedState(permissionsPresenter.present())
var isSending by remember { mutableStateOf(false) }
var showSendFailureDialog by remember { mutableStateOf(false) }
@ -88,6 +89,15 @@ class DefaultVoiceMessageComposerPresenter(
player.setMedia(recording.file.path)
}
LaunchedEffect(permissionState.permissionGranted) {
if (permissionState.permissionGranted) {
pendingEvent?.let {
localCoroutineScope.startRecording()
pendingEvent = null
}
}
}
fun handleLifecycleEvent(event: Lifecycle.Event) {
when (event) {
Lifecycle.Event.ON_PAUSE -> {
@ -102,6 +112,7 @@ class DefaultVoiceMessageComposerPresenter(
}
fun handleVoiceMessageRecorderEvent(event: VoiceMessageRecorderEvent) {
pendingEvent = null
when (event) {
VoiceMessageRecorderEvent.Start -> {
Timber.v("Voice message record button pressed")
@ -111,6 +122,7 @@ class DefaultVoiceMessageComposerPresenter(
}
else -> {
Timber.i("Voice message permission needed")
pendingEvent = VoiceMessageRecorderEvent.Start
permissionState.eventSink(PermissionsEvent.RequestPermissions)
}
}

View file

@ -522,7 +522,9 @@ class DefaultVoiceMessageComposerPresenterTest {
permissionsPresenter.setPermissionGranted()
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
val finalState = awaitItem()
advanceUntilIdle()
val finalState = expectMostRecentItem()
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
voiceRecorder.assertCalls(stopped = 1, started = 1)
@ -547,14 +549,16 @@ class DefaultVoiceMessageComposerPresenterTest {
assertThat(it.showPermissionRationaleDialog).isTrue()
it.eventSink(VoiceMessageComposerEvent.AcceptPermissionRationale)
}
skipItems(1)
// Dialog is hidden, user accepts permissions
assertThat(awaitItem().showPermissionRationaleDialog).isFalse()
// Permission is granted, recording starts automatically
permissionsPresenter.setPermissionGranted()
advanceUntilIdle()
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
val finalState = awaitItem()
val finalState = expectMostRecentItem()
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
voiceRecorder.assertCalls(started = 1)
@ -579,12 +583,14 @@ class DefaultVoiceMessageComposerPresenterTest {
assertThat(it.showPermissionRationaleDialog).isTrue()
it.eventSink(VoiceMessageComposerEvent.DismissPermissionsRationale)
}
skipItems(1)
// Dialog is hidden, user tries to record again
awaitItem().also {
assertThat(it.showPermissionRationaleDialog).isFalse()
it.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
}
skipItems(1)
// Dialog is shown once again
val finalState = awaitItem().also {
@ -593,6 +599,7 @@ class DefaultVoiceMessageComposerPresenterTest {
}
voiceRecorder.assertCalls(started = 0)
cancelAndIgnoreRemainingEvents()
testPauseAndDestroy(finalState)
}
}

View file

@ -38,6 +38,12 @@
<string name="screen_room_change_role_unsaved_changes_description">"Du har endringer som ikke er lagret."</string>
<string name="screen_room_change_role_unsaved_changes_title">"Lagre endringer?"</string>
<string name="screen_room_member_list_banned_empty">"Det er ingen utestengte brukere."</string>
<plurals name="screen_room_member_list_banned_header_title">
<item quantity="one">"%1$d utestengt"</item>
<item quantity="other">"%1$d utestengt"</item>
</plurals>
<string name="screen_room_member_list_empty_search_subtitle">"Sjekk stavemåten eller prøv et nytt søk"</string>
<string name="screen_room_member_list_empty_search_title">"Ingen resultater for \"%1$s\""</string>
<plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d person"</item>
<item quantity="other">"%1$d personer"</item>
@ -49,6 +55,11 @@
<string name="screen_room_member_list_manage_member_unban_title">"Fjern utestengelsen fra rommet"</string>
<string name="screen_room_member_list_mode_banned">"Utestengt"</string>
<string name="screen_room_member_list_mode_members">"Medlemmer"</string>
<plurals name="screen_room_member_list_pending_header_title">
<item quantity="one">"%1$d invitert"</item>
<item quantity="other">"%1$d invitert"</item>
</plurals>
<string name="screen_room_member_list_pending_status">"Venter"</string>
<string name="screen_room_member_list_role_administrator">"Admin"</string>
<string name="screen_room_member_list_role_moderator">"Moderator"</string>
<string name="screen_room_member_list_role_owner">"Eier"</string>

View file

@ -168,6 +168,8 @@ class RoomDetailsPresenter(
val canReportRoom by produceState(false) { value = client.canReportRoom() }
val enableKeyShareOnInvite by featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(initial = false)
return RoomDetailsState(
roomId = room.roomId,
roomName = roomName,
@ -197,6 +199,8 @@ class RoomDetailsPresenter(
isTombstoned = roomInfo.successorRoom != null,
showDebugInfo = isDeveloperModeEnabled,
roomVersion = roomInfo.roomVersion,
enableKeyShareOnInvite = enableKeyShareOnInvite,
roomHistoryVisibility = roomInfo.historyVisibility,
eventSink = ::handleEvent,
)
}

View file

@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -50,6 +51,8 @@ data class RoomDetailsState(
val isTombstoned: Boolean,
val showDebugInfo: Boolean,
val roomVersion: String?,
val enableKeyShareOnInvite: Boolean,
val roomHistoryVisibility: RoomHistoryVisibility,
val eventSink: (RoomDetailsEvent) -> Unit
) {
val roomBadges = buildList {
@ -61,6 +64,14 @@ data class RoomDetailsState(
if (isPublic) {
add(RoomBadge.PUBLIC)
}
if (enableKeyShareOnInvite && isEncrypted) {
when (roomHistoryVisibility) {
RoomHistoryVisibility.Invited, RoomHistoryVisibility.Joined -> add(RoomBadge.SHARED_HISTORY_HIDDEN)
RoomHistoryVisibility.Shared -> add(RoomBadge.SHARED_HISTORY_SHARED)
RoomHistoryVisibility.WorldReadable -> add(RoomBadge.SHARED_HISTORY_WORLD_READABLE)
else -> {}
}
}
}.toImmutableList()
}
@ -84,4 +95,7 @@ enum class RoomBadge {
ENCRYPTED,
NOT_ENCRYPTED,
PUBLIC,
SHARED_HISTORY_HIDDEN,
SHARED_HISTORY_SHARED,
SHARED_HISTORY_WORLD_READABLE
}

View file

@ -26,6 +26,7 @@ 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.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import kotlinx.collections.immutable.toImmutableList
@ -57,6 +58,9 @@ open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState>
aRoomDetailsState(isTombstoned = true),
aDmRoomDetailsState(dmRoomMemberVerificationState = UserProfileVerificationState.VERIFIED),
aDmRoomDetailsState(dmRoomMemberVerificationState = UserProfileVerificationState.VERIFICATION_VIOLATION),
aSharedHistoryRoomDetailsState(roomHistoryVisibility = RoomHistoryVisibility.Joined),
aSharedHistoryRoomDetailsState(roomHistoryVisibility = RoomHistoryVisibility.Shared),
aSharedHistoryRoomDetailsState(roomHistoryVisibility = RoomHistoryVisibility.WorldReadable),
// Add other state here
)
}
@ -117,6 +121,8 @@ fun aRoomDetailsState(
canReportRoom: Boolean = true,
isTombstoned: Boolean = false,
showDebugInfo: Boolean = false,
enableKeyShareOnInvite: Boolean = false,
roomHistoryVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Shared,
eventSink: (RoomDetailsEvent) -> Unit = {},
) = RoomDetailsState(
roomId = roomId,
@ -147,6 +153,8 @@ fun aRoomDetailsState(
isTombstoned = isTombstoned,
showDebugInfo = showDebugInfo,
roomVersion = "12",
enableKeyShareOnInvite = enableKeyShareOnInvite,
roomHistoryVisibility = roomHistoryVisibility,
eventSink = eventSink,
)
@ -182,3 +190,11 @@ fun aDmRoomDetailsState(
verificationState = dmRoomMemberVerificationState,
)
)
fun aSharedHistoryRoomDetailsState(
roomHistoryVisibility: RoomHistoryVisibility
) = aRoomDetailsState(
isEncrypted = true,
enableKeyShareOnInvite = true,
roomHistoryVisibility = roomHistoryVisibility,
)

View file

@ -518,6 +518,27 @@ private fun RoomBadge.toMatrixBadgeData(): MatrixBadgeAtom.MatrixBadgeData {
type = MatrixBadgeAtom.Type.Info,
)
}
RoomBadge.SHARED_HISTORY_HIDDEN -> {
MatrixBadgeAtom.MatrixBadgeData(
text = stringResource(R.string.crypto_history_sharing_room_info_hidden_badge_content),
icon = CompoundIcons.VisibilityOff(),
type = MatrixBadgeAtom.Type.Info
)
}
RoomBadge.SHARED_HISTORY_SHARED -> {
MatrixBadgeAtom.MatrixBadgeData(
text = stringResource(R.string.crypto_history_sharing_room_info_shared_badge_content),
icon = CompoundIcons.History(),
type = MatrixBadgeAtom.Type.Info
)
}
RoomBadge.SHARED_HISTORY_WORLD_READABLE -> {
MatrixBadgeAtom.MatrixBadgeData(
text = stringResource(R.string.crypto_history_sharing_room_info_world_readable_badge_content),
icon = CompoundIcons.UserProfileSolid(),
type = MatrixBadgeAtom.Type.Info
)
}
}
}

View file

@ -145,7 +145,7 @@ Nedoporučujeme povolovat šifrování pro místnosti, které může kdokoli naj
<string name="screen_security_and_privacy_encryption_section_header">"Šifrování"</string>
<string name="screen_security_and_privacy_encryption_toggle_title">"Povolit koncové šifrování"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Vstoupit může kdokoli."</string>
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Kdokoliv"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Kdokoli"</string>
<string name="screen_security_and_privacy_room_access_footer">"Vyberte, kteří členové prostorů mohou vstoupit do této místnosti bez pozvánky. %1$s"</string>
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Spravovat prostory"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Vstoupit mohou pouze pozvaní lidé."</string>

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crypto_history_sharing_room_info_hidden_badge_content">"Uued liikmed ei näe ajalugu"</string>
<string name="crypto_history_sharing_room_info_shared_badge_content">"Uued liikmed näevad ajalugu"</string>
<string name="crypto_history_sharing_room_info_world_readable_badge_content">"Kõik võivad ajalugu näha"</string>
<string name="screen_edit_room_address_room_address_section_footer">"Selleks, et jututuba oleks nähtav jututubade avalikus kataloogis, vajab ta aadressi."</string>
<string name="screen_edit_room_address_title">"Muuda aadressi"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Teavituste seadistamisel tekkis viga"</string>

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crypto_history_sharing_room_info_hidden_badge_content">"Les nouveaux membres ne voient pas lhistorique."</string>
<string name="crypto_history_sharing_room_info_shared_badge_content">"Les nouveaux membres voient lhistorique"</string>
<string name="crypto_history_sharing_room_info_world_readable_badge_content">"Tout le monde voit lhistorique"</string>
<string name="screen_edit_room_address_room_address_section_footer">"Vous aurez besoin dune adresse pour le rendre visible dans lannuaire public."</string>
<string name="screen_edit_room_address_title">"Modifier ladresse"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Une erreur sest produite lors de la mise à jour du paramètre de notification."</string>

View file

@ -63,6 +63,7 @@
<string name="screen_room_details_profile_row_title">"Profil"</string>
<string name="screen_room_details_requests_to_join_title">"Forespørsler om å bli med"</string>
<string name="screen_room_details_roles_and_permissions">"Roller og tillatelser"</string>
<string name="screen_room_details_room_name_label">"Navn"</string>
<string name="screen_room_details_security_and_privacy_title">"Sikkerhet og personvern"</string>
<string name="screen_room_details_security_title">"Sikkerhet"</string>
<string name="screen_room_details_share_room_title">"Del rom"</string>
@ -70,6 +71,12 @@
<string name="screen_room_details_topic_title">"Emne"</string>
<string name="screen_room_details_updating_room">"Oppdaterer rommet …"</string>
<string name="screen_room_member_list_banned_empty">"Det er ingen utestengte brukere."</string>
<plurals name="screen_room_member_list_banned_header_title">
<item quantity="one">"%1$d utestengt"</item>
<item quantity="other">"%1$d utestengt"</item>
</plurals>
<string name="screen_room_member_list_empty_search_subtitle">"Sjekk stavemåten eller prøv et nytt søk"</string>
<string name="screen_room_member_list_empty_search_title">"Ingen resultater for \"%1$s\""</string>
<plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d person"</item>
<item quantity="other">"%1$d personer"</item>
@ -81,6 +88,11 @@
<string name="screen_room_member_list_manage_member_unban_title">"Fjern utestengelsen fra rommet"</string>
<string name="screen_room_member_list_mode_banned">"Utestengt"</string>
<string name="screen_room_member_list_mode_members">"Medlemmer"</string>
<plurals name="screen_room_member_list_pending_header_title">
<item quantity="one">"%1$d invitert"</item>
<item quantity="other">"%1$d invitert"</item>
</plurals>
<string name="screen_room_member_list_pending_status">"Venter"</string>
<string name="screen_room_member_list_role_administrator">"Admin"</string>
<string name="screen_room_member_list_role_moderator">"Moderator"</string>
<string name="screen_room_member_list_role_owner">"Eier"</string>
@ -117,8 +129,10 @@
<string name="screen_room_roles_and_permissions_room_details">"Romdetaljer"</string>
<string name="screen_room_roles_and_permissions_title">"Roller og tillatelser"</string>
<string name="screen_security_and_privacy_add_room_address_action">"Legg til adresse"</string>
<string name="screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description">"Alle i autoriserte områder kan bli med, men alle andre må be om tilgang."</string>
<string name="screen_security_and_privacy_ask_to_join_option_description">"Alle må be om tilgang."</string>
<string name="screen_security_and_privacy_ask_to_join_option_title">"Be om å få bli med"</string>
<string name="screen_security_and_privacy_ask_to_join_single_space_members_option_description">"Alle i %1$s kan bli med, men alle andre må be om tilgang."</string>
<string name="screen_security_and_privacy_enable_encryption_alert_confirm_button_title">"Ja, aktiver kryptering"</string>
<string name="screen_security_and_privacy_enable_encryption_alert_description">"Når kryptering for et rom er aktivert, kan den ikke deaktiveres. Meldingshistorikken vil bare være synlig for rommedlemmer siden de ble invitert eller siden de ble med i rommet.
Ingen andre enn rommedlemmene vil kunne lese meldingene. Dette kan føre til at bots og broer ikke fungerer som de skal.
@ -129,22 +143,28 @@ Vi anbefaler ikke å aktivere kryptering for rom som hvem som helst kan finne og
<string name="screen_security_and_privacy_encryption_toggle_title">"Aktiver ende-til-ende-kryptering"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Alle kan bli med."</string>
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Hvem som helst"</string>
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Administrer områder"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Bare inviterte personer kan bli med."</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Kun for inviterte"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Tilgang"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_multiple_parents_description">"Alle i autoriserte områder kan bli med."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_single_parent_description">"Alle i %1$s kan bli med."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Medlemmer av område"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Områder støttes ikke for øyeblikket"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Du trenger en adresse for å gjøre den synlig i den offentlige katalogen."</string>
<string name="screen_security_and_privacy_room_address_section_header">"Adresse"</string>
<string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Tillat at dette rommet blir funnet ved å søke %1$s offentlig romkatalog"</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_description">"Tillat å bli funnet ved søk i den offentlige katalogen."</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Synlig i offentlig katalog"</string>
<string name="screen_security_and_privacy_room_history_anyone_option_title">"Alle (historikken er offentlig)"</string>
<string name="screen_security_and_privacy_room_history_section_footer">"Endringene vil ikke påvirke tidligere meldinger, kun nye. %1$s"</string>
<string name="screen_security_and_privacy_room_history_section_header">"Hvem kan lese historikk"</string>
<string name="screen_security_and_privacy_room_history_since_invite_option_title">"Medlemmer siden de ble invitert"</string>
<string name="screen_security_and_privacy_room_history_since_selecting_option_title">"Medlemmer (full historikk)"</string>
<string name="screen_security_and_privacy_room_publishing_section_footer">"Romadresser er måter å finne og få tilgang til rom på. Dette sikrer også at du enkelt kan dele rommet ditt med andre.
Du kan velge å publisere rommet ditt i hjemeserverens offentlige romkatalog."</string>
<string name="screen_security_and_privacy_room_publishing_section_header">"Publisering av rom"</string>
<string name="screen_security_and_privacy_room_visibility_section_footer">"Adresser er en måte å finne og få tilgang til rom og områder. Dette sikrer også at du enkelt kan dele dem med andre."</string>
<string name="screen_security_and_privacy_room_visibility_section_header">"Synlighet"</string>
<string name="screen_security_and_privacy_title">"Sikkerhet og personvern"</string>
</resources>

View file

@ -114,7 +114,7 @@
<string name="screen_room_roles_and_permissions_room_details">"Rumsdetaljer"</string>
<string name="screen_room_roles_and_permissions_title">"Roller och behörigheter"</string>
<string name="screen_security_and_privacy_add_room_address_action">"Lägg till rumsadress"</string>
<string name="screen_security_and_privacy_ask_to_join_option_description">"Vem som helst kan be om att gå med i rummet men en administratör eller moderator måste acceptera begäran."</string>
<string name="screen_security_and_privacy_ask_to_join_option_description">"Alla måste begära åtkomst."</string>
<string name="screen_security_and_privacy_ask_to_join_option_title">"Be om att gå med"</string>
<string name="screen_security_and_privacy_enable_encryption_alert_confirm_button_title">"Ja, aktivera kryptering"</string>
<string name="screen_security_and_privacy_enable_encryption_alert_description">"När det är aktiverat kan kryptering för ett rum inte inaktiveras, meddelandehistoriken visas bara för rumsmedlemmar sedan de blev inbjudna eller sedan de gick med i rummet.
@ -126,9 +126,9 @@ Vi rekommenderar inte att aktivera kryptering för rum som vem som helst kan hit
<string name="screen_security_and_privacy_encryption_toggle_title">"Aktivera totalsträckskryptering"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Vem som helst kan hitta och gå med"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Vem som helst"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Användare kan bara gå med om de är inbjudna"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Endast inbjudna personer kan gå med."</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Endast inbjudan"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Tillgång till rum"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Åtkomst"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Utrymmesmedlemmar"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Utrymmen stöds för närvarande inte"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Du behöver en rumsadress för att göra den synlig i katalogen."</string>

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crypto_history_sharing_room_info_hidden_badge_content">"New members don\'t see history"</string>
<string name="crypto_history_sharing_room_info_shared_badge_content">"New members see history"</string>
<string name="crypto_history_sharing_room_info_world_readable_badge_content">"Anyone can see history"</string>
<string name="screen_edit_room_address_room_address_section_footer">"Youll need an address in order to make it visible in the public directory."</string>
<string name="screen_edit_room_address_title">"Edit address"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"An error occurred while updating the notification setting."</string>

View file

@ -9,6 +9,7 @@
package io.element.android.features.roomdetails.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import kotlinx.collections.immutable.persistentListOf
import org.junit.Test
@ -56,4 +57,52 @@ class RoomDetailsStateTest {
persistentListOf(RoomBadge.ENCRYPTED)
)
}
@Test
fun `room public not encrypted should not have history sharing badges`() {
val sut = aRoomDetailsState(
isEncrypted = false,
enableKeyShareOnInvite = true,
roomHistoryVisibility = RoomHistoryVisibility.Shared
)
assertThat(sut.roomBadges).isEqualTo(
persistentListOf(RoomBadge.NOT_ENCRYPTED, RoomBadge.PUBLIC)
)
}
@Test
fun `room public encrypted should have history sharing hidden badge`() {
val sut = aRoomDetailsState(
isEncrypted = true,
enableKeyShareOnInvite = true,
roomHistoryVisibility = RoomHistoryVisibility.Joined
)
assertThat(sut.roomBadges).isEqualTo(
persistentListOf(RoomBadge.ENCRYPTED, RoomBadge.PUBLIC, RoomBadge.SHARED_HISTORY_HIDDEN)
)
}
@Test
fun `room public encrypted should have history sharing shared badge`() {
val sut = aRoomDetailsState(
isEncrypted = true,
enableKeyShareOnInvite = true,
roomHistoryVisibility = RoomHistoryVisibility.Shared
)
assertThat(sut.roomBadges).isEqualTo(
persistentListOf(RoomBadge.ENCRYPTED, RoomBadge.PUBLIC, RoomBadge.SHARED_HISTORY_SHARED)
)
}
@Test
fun `room public encrypted should have history sharing world_readable badge`() {
val sut = aRoomDetailsState(
isEncrypted = true,
enableKeyShareOnInvite = true,
roomHistoryVisibility = RoomHistoryVisibility.WorldReadable
)
assertThat(sut.roomBadges).isEqualTo(
persistentListOf(RoomBadge.ENCRYPTED, RoomBadge.PUBLIC, RoomBadge.SHARED_HISTORY_WORLD_READABLE)
)
}
}

View file

@ -21,7 +21,7 @@ Nedoporučujeme povolovat šifrování pro místnosti, které může kdokoli naj
<string name="screen_security_and_privacy_encryption_section_header">"Šifrování"</string>
<string name="screen_security_and_privacy_encryption_toggle_title">"Povolit koncové šifrování"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Vstoupit může kdokoli."</string>
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Kdokoliv"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Kdokoli"</string>
<string name="screen_security_and_privacy_room_access_footer">"Vyberte, kteří členové prostorů mohou vstoupit do této místnosti bez pozvánky. %1$s"</string>
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Spravovat prostory"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Vstoupit mohou pouze pozvaní lidé."</string>

View file

@ -2,9 +2,16 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_edit_room_address_room_address_section_footer">"Du trenger en adresse for å gjøre den synlig i den offentlige katalogen."</string>
<string name="screen_edit_room_address_title">"Rediger adresse"</string>
<string name="screen_manage_authorized_spaces_header">"Områder hvor medlemmer kan bli med i rommet uten invitasjon."</string>
<string name="screen_manage_authorized_spaces_title">"Administrer områder"</string>
<string name="screen_manage_authorized_spaces_unknown_space">"(Ukjent område)"</string>
<string name="screen_manage_authorized_spaces_unknown_spaces_section_title">"Andre områder du ikke er medlem i"</string>
<string name="screen_manage_authorized_spaces_your_spaces_section_title">"Dine områder"</string>
<string name="screen_security_and_privacy_add_room_address_action">"Legg til adresse"</string>
<string name="screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description">"Alle i autoriserte områder kan bli med, men alle andre må be om tilgang."</string>
<string name="screen_security_and_privacy_ask_to_join_option_description">"Alle må be om tilgang."</string>
<string name="screen_security_and_privacy_ask_to_join_option_title">"Be om å få bli med"</string>
<string name="screen_security_and_privacy_ask_to_join_single_space_members_option_description">"Alle i %1$s kan bli med, men alle andre må be om tilgang."</string>
<string name="screen_security_and_privacy_enable_encryption_alert_confirm_button_title">"Ja, aktiver kryptering"</string>
<string name="screen_security_and_privacy_enable_encryption_alert_description">"Når kryptering for et rom er aktivert, kan den ikke deaktiveres. Meldingshistorikken vil bare være synlig for rommedlemmer siden de ble invitert eller siden de ble med i rommet.
Ingen andre enn rommedlemmene vil kunne lese meldingene. Dette kan føre til at bots og broer ikke fungerer som de skal.
@ -15,22 +22,28 @@ Vi anbefaler ikke å aktivere kryptering for rom som hvem som helst kan finne og
<string name="screen_security_and_privacy_encryption_toggle_title">"Aktiver ende-til-ende-kryptering"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Alle kan bli med."</string>
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Hvem som helst"</string>
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Administrer områder"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Bare inviterte personer kan bli med."</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Kun for inviterte"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Tilgang"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_multiple_parents_description">"Alle i autoriserte områder kan bli med."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_single_parent_description">"Alle i %1$s kan bli med."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Medlemmer av område"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Områder støttes ikke for øyeblikket"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Du trenger en adresse for å gjøre den synlig i den offentlige katalogen."</string>
<string name="screen_security_and_privacy_room_address_section_header">"Adresse"</string>
<string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Tillat at dette rommet blir funnet ved å søke %1$s offentlig romkatalog"</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_description">"Tillat å bli funnet ved søk i den offentlige katalogen."</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Synlig i offentlig katalog"</string>
<string name="screen_security_and_privacy_room_history_anyone_option_title">"Alle (historikken er offentlig)"</string>
<string name="screen_security_and_privacy_room_history_section_footer">"Endringene vil ikke påvirke tidligere meldinger, kun nye. %1$s"</string>
<string name="screen_security_and_privacy_room_history_section_header">"Hvem kan lese historikk"</string>
<string name="screen_security_and_privacy_room_history_since_invite_option_title">"Medlemmer siden de ble invitert"</string>
<string name="screen_security_and_privacy_room_history_since_selecting_option_title">"Medlemmer (full historikk)"</string>
<string name="screen_security_and_privacy_room_publishing_section_footer">"Romadresser er måter å finne og få tilgang til rom på. Dette sikrer også at du enkelt kan dele rommet ditt med andre.
Du kan velge å publisere rommet ditt i hjemeserverens offentlige romkatalog."</string>
<string name="screen_security_and_privacy_room_publishing_section_header">"Publisering av rom"</string>
<string name="screen_security_and_privacy_room_visibility_section_footer">"Adresser er en måte å finne og få tilgang til rom og områder. Dette sikrer også at du enkelt kan dele dem med andre."</string>
<string name="screen_security_and_privacy_room_visibility_section_header">"Synlighet"</string>
<string name="screen_security_and_privacy_title">"Sikkerhet og personvern"</string>
</resources>

View file

@ -3,7 +3,7 @@
<string name="screen_edit_room_address_room_address_section_footer">"Du behöver en rumsadress för att göra den synlig i katalogen."</string>
<string name="screen_edit_room_address_title">"Rumsadress"</string>
<string name="screen_security_and_privacy_add_room_address_action">"Lägg till rumsadress"</string>
<string name="screen_security_and_privacy_ask_to_join_option_description">"Vem som helst kan be om att gå med i rummet men en administratör eller moderator måste acceptera begäran."</string>
<string name="screen_security_and_privacy_ask_to_join_option_description">"Alla måste begära åtkomst."</string>
<string name="screen_security_and_privacy_ask_to_join_option_title">"Be om att gå med"</string>
<string name="screen_security_and_privacy_enable_encryption_alert_confirm_button_title">"Ja, aktivera kryptering"</string>
<string name="screen_security_and_privacy_enable_encryption_alert_description">"När det är aktiverat kan kryptering för ett rum inte inaktiveras, meddelandehistoriken visas bara för rumsmedlemmar sedan de blev inbjudna eller sedan de gick med i rummet.
@ -15,9 +15,9 @@ Vi rekommenderar inte att aktivera kryptering för rum som vem som helst kan hit
<string name="screen_security_and_privacy_encryption_toggle_title">"Aktivera totalsträckskryptering"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Vem som helst kan hitta och gå med"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Vem som helst"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Användare kan bara gå med om de är inbjudna"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Endast inbjudna personer kan gå med."</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Endast inbjudan"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Tillgång till rum"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Åtkomst"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Utrymmesmedlemmar"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Utrymmen stöds för närvarande inte"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Du behöver en rumsadress för att göra den synlig i katalogen."</string>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_leave_space_choose_owners_action">"Vyberte vlastníky"</string>
<string name="screen_leave_space_last_admin_info">"%1$s (Správce)"</string>
<plurals name="screen_leave_space_submit">
<item quantity="one">"Opustit %1$d místnost a prostor"</item>
@ -8,10 +9,21 @@
</plurals>
<string name="screen_leave_space_subtitle">"Tím budete také odstraněni ze všech místností v tomto prostoru."</string>
<string name="screen_leave_space_subtitle_last_admin">"Než budete moci odejít, musíte pro tento prostor přiřadit jiného správce."</string>
<string name="screen_leave_space_subtitle_last_owner">"Jste jediným vlastníkem %1$s. Před odchodem musíte převést vlastnictví na někoho jiného."</string>
<string name="screen_leave_space_subtitle_only_last_admin">"Z následujících místností nebudete odstraněni, protože jste jediným administrátorem:"</string>
<string name="screen_leave_space_title">"Opustit %1$s?"</string>
<string name="screen_leave_space_title_last_admin">"Jste jediným administrátorem pro %1$s"</string>
<string name="screen_leave_space_title_last_owner">"Převést vlastnictví"</string>
<string name="screen_space_add_room_action">"Místnost"</string>
<string name="screen_space_add_rooms_room_access_description">"Přidání místnosti neovlivní přístup k místnosti. Chcete-li přístup změnit, přejděte do Nastavení místnosti &gt; Zabezpečení a soukromí."</string>
<string name="screen_space_empty_state_title">"Přidejte svou první místnost"</string>
<string name="screen_space_menu_action_members">"Zobrazit členy"</string>
<string name="screen_space_remove_rooms_confirmation_content">"Odebrání místnosti neovlivní přístup k místnosti. Chcete-li přístup změnit, přejděte do sekce Informace o místnosti &gt; Soukromí a zabezpečení."</string>
<plurals name="screen_space_remove_rooms_confirmation_title">
<item quantity="one">"Odstranit %1$d místnost od %2$s"</item>
<item quantity="few">"Odstranit %1$d místnosti od %2$s"</item>
<item quantity="other">"Odstranit %1$d místností od %2$s"</item>
</plurals>
<string name="screen_space_settings_leave_space">"Opustit prostor"</string>
<string name="screen_space_settings_roles_and_permissions">"Role a oprávnění"</string>
<string name="screen_space_settings_security_and_privacy">"Zabezpečení a soukromí"</string>

View file

@ -8,13 +8,16 @@
</plurals>
<string name="screen_leave_space_subtitle">"Sellega eemaldad end ka kõikidest antud kogukonna jututubadest."</string>
<string name="screen_leave_space_subtitle_last_admin">"Enne lahkumist pead sa selle kogukonna jaoks lisama vähemalt ühe täiendava peakasutaja."</string>
<string name="screen_leave_space_subtitle_last_owner">"Sa oled „%1$s“ kogukonna viimane omanik. Enne lahkumist pead omandi kellelegi teisele üle andma."</string>
<string name="screen_leave_space_subtitle_only_last_admin">"Sind ei saa järgnevatest jututubadest eemaldada, kuna oled seal/neis ainus peakasutaja:"</string>
<string name="screen_leave_space_title">"Kas lahkud %1$s kogukonnast?"</string>
<string name="screen_leave_space_title_last_admin">"Sa oled siin ainus peakasutaja: %1$s"</string>
<string name="screen_leave_space_title_last_owner">"Anna omand üle"</string>
<string name="screen_space_add_room_action">"Jututuba"</string>
<string name="screen_space_add_rooms_room_access_description">"Jututoa lisamine ei mõjuta ligipääsu jututuppa. Selle muutmiseks ava „Jututoa seadistused“ → „Turvalisus ja privaatsus“."</string>
<string name="screen_space_empty_state_title">"Lisa oma esimene jututuba"</string>
<string name="screen_space_menu_action_members">"Vaata liikmeid"</string>
<string name="screen_space_remove_rooms_confirmation_content">"Jututoa eemaldamine ei mõjuta ligipääsu jututuppa. Selle muutmiseks ava „Jututoa seadistused“ → „Turvalisus ja privaatsus“."</string>
<plurals name="screen_space_remove_rooms_confirmation_title">
<item quantity="one">"Eemalda %1$d jututuba „%2$s“ kogukonnast"</item>
<item quantity="other">"Eemalda %1$d jututuba „%2$s“ kogukonnast"</item>