Merge branch 'develop' into feature/fga/space_ui_tweaks

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

View file

@ -22,7 +22,8 @@
{ {
"versioning": "semver", "versioning": "semver",
"matchPackageNames": [ "matchPackageNames": [
"/^org.maplibre/" "/^org.maplibre/",
"/^org.jetbrains.kotlinx:kotlinx-datetime/"
] ]
} }
] ]

22
.github/workflows/stale-issues.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: Close stale issues that are missing info.
on:
schedule:
- cron: "30 1 * * *"
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v10
with:
only-labels: "X-Needs-Info"
days-before-issue-stale: 30
days-before-issue-close: 7
days-before-pr-stale: -1
stale-issue-label: "stale"
labels-to-remove-when-unstale: "X-Needs-Info"
stale-issue-message: "This issue has been awaiting further information for the past 30 days so will now be marked as stale. Please provide the requested information within the next 7 days to keep it open."
close-issue-message: "This issue is being closed due to inactivity after further information was requested."

4
.idea/kotlinc.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="2.3.0" /> <option name="version" value="2.3.10" />
</component> </component>
</project> </project>

View file

@ -9,29 +9,35 @@ appId: ${MAESTRO_APP_ID}
id: "login-continue" id: "login-continue"
## MAS page ## MAS page
## Conditional workflow to pass the Chrome first launch welcome page. ## Conditional workflow to pass the Chrome first launch welcome page.
- runFlow: - retry:
when: maxRetries: 3
visible: 'Use without an account'
commands: commands:
- tapOn: "Use without an account" - runFlow:
## For older chrome versions when:
- runFlow: visible: 'Use without an account'
when: commands:
visible: 'Accept & continue' - tapOn: "Use without an account"
commands: ## For older chrome versions
- tapOn: "Accept & continue" - runFlow:
- runFlow: when:
when: visible: 'Accept & continue'
visible: 'No thanks' commands:
commands: - tapOn: "Accept & continue"
- tapOn: "No thanks" - runFlow:
when:
visible: 'No thanks'
commands:
- tapOn: "No thanks"
## Working when running Maestro locally, but not on the CI yet. ## Working when running Maestro locally, but not on the CI yet.
- extendedWaitUntil: - retry:
visible: maxRetries: 3
id: "form-1" commands:
timeout: 10000 - extendedWaitUntil:
visible:
id: "form-1"
timeout: 10000
- tapOn: - tapOn:
id: "form-1" id: "form-1"
- inputText: ${MAESTRO_USERNAME} - inputText: ${MAESTRO_USERNAME}
- pressKey: Enter - pressKey: Enter
- tapOn: - tapOn:

View file

@ -13,3 +13,8 @@ appId: ${MAESTRO_APP_ID}
- scroll - scroll
- tapOn: "Leave room" - tapOn: "Leave room"
- tapOn: "Leave" - tapOn: "Leave"
- runFlow:
when:
visible: 'You need an invite in order to join'
commands:
- tapOn: "Back"

View file

@ -3,9 +3,9 @@ appId: ${MAESTRO_APP_ID}
# Purpose: Test the creation and deletion of a room # Purpose: Test the creation and deletion of a room
- tapOn: "Create a new conversation or room" - tapOn: "Create a new conversation or room"
- tapOn: "New room" - tapOn: "New room"
- tapOn: "e.g. your project name" - tapOn: "Add name…"
- inputText: "aRoomName" - inputText: "aRoomName"
- tapOn: "What is this room about?" - tapOn: "Add description…"
- inputText: "aRoomTopic" - inputText: "aRoomTopic"
- tapOn: "Create" - tapOn: "Create"
- takeScreenshot: build/maestro/320-createAndDeleteRoom - takeScreenshot: build/maestro/320-createAndDeleteRoom
@ -37,3 +37,8 @@ appId: ${MAESTRO_APP_ID}
- scroll - scroll
- tapOn: "Leave room" - tapOn: "Leave room"
- tapOn: "Leave" - tapOn: "Leave"
- runFlow:
when:
visible: 'You need an invite in order to join'
commands:
- tapOn: "Back"

View file

@ -3,16 +3,33 @@
<string name="screen_create_room_action_create_room">"Nová místnost"</string> <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_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_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. <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> 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_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_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_knocking_option_title">"Povolit žádost o vstup"</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_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_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_access_section_restricted_option_description">"Kdokoli může vstoupit do %1$s."</string>
<string name="screen_create_room_room_address_section_title">"Adresa místnosti"</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_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_label">"Téma (nepovinné)"</string>
<string name="screen_create_room_topic_placeholder">"Přidat popis…"</string>
</resources> </resources>

View file

@ -3,20 +3,31 @@
<string name="screen_create_room_action_create_room">"Uus jututuba"</string> <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_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_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_name_placeholder">"Sisesta nimi…"</string>
<string name="screen_create_room_new_room_title">"Uus jututuba"</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_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_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. <string name="screen_create_room_public_option_description">"Kõik saavad seda jututuba leida.
Sa võid seda jututoa seadistustest alati muuta."</string> 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_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_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_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_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_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_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_address_section_title">"Aadress"</string>
<string name="screen_create_room_room_visibility_section_title">"Jututoa nähtavus"</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_no_space_title">"Avaleht"</string>
<string name="screen_create_room_space_selection_sheet_title">"Lisa kogukonda"</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> <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_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_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_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_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_restricted_option_title">"Standard"</string>
<string name="screen_create_room_room_access_section_title">"Qui a accès"</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_address_section_title">"Adresse"</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilité du salon"</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_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_space_selection_sheet_title">"Ajouter à lespace"</string>
<string name="screen_create_room_topic_label">"Sujet (facultatif)"</string> <string name="screen_create_room_topic_label">"Sujet (facultatif)"</string>
<string name="screen_create_room_topic_placeholder">"Ajouter une description…"</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_action_create_room">"Nytt rom"</string>
<string name="screen_create_room_add_people_title">"Inviter folk"</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_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_description">"Bare inviterte personer kan bli med."</string>
<string name="screen_create_room_private_option_title">"Privat"</string> <string name="screen_create_room_private_option_title">"Privat"</string>
<string name="screen_create_room_public_option_description">"Alle kan finne dette rommet. <string name="screen_create_room_public_option_description">"Alle kan finne dette rommet.
Du kan endre dette når som helst i rominnstillingene."</string> 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_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_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_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_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_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_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_address_section_title">"Adresse"</string>
<string name="screen_create_room_room_visibility_section_title">"Romsynlighet"</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_label">"Emne (valgfritt)"</string>
<string name="screen_create_room_topic_placeholder">"Legg til beskrivelse…"</string>
</resources> </resources>

View file

@ -7,12 +7,12 @@
<string name="screen_create_room_public_option_description">"Vem som helst kan hitta det här rummet. <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> 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_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_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_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_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">"Rumsadress"</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_room_visibility_section_title">"Rumssynlighet"</string>
<string name="screen_create_room_topic_label">"Ämne (valfritt)"</string> <string name="screen_create_room_topic_label">"Ämne (valfritt)"</string>
</resources> </resources>

View file

@ -9,6 +9,7 @@
package io.element.android.features.home.impl package io.element.android.features.home.impl
import io.element.android.features.home.impl.roomlist.RoomListState 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.home.impl.spaces.HomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
@ -31,6 +32,7 @@ data class HomeState(
val directLogoutState: DirectLogoutState, val directLogoutState: DirectLogoutState,
val eventSink: (HomeEvent) -> Unit, val eventSink: (HomeEvent) -> Unit,
) { ) {
val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected
val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats
val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters
val showNavigationBar = homeSpacesState.canCreateSpaces || homeSpacesState.spaceRooms.isNotEmpty() 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.RoomListEvent
import io.element.android.features.home.impl.roomlist.RoomListState 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.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.features.home.impl.spaces.HomeSpacesView
import io.element.android.libraries.androidutils.throttler.FirstThrottler import io.element.android.libraries.androidutils.throttler.FirstThrottler
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
@ -153,10 +156,15 @@ private fun HomeScaffold(
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
val roomListState: RoomListState = state.roomListState val roomListState: RoomListState = state.roomListState
BackHandler( BackHandler(enabled = state.isBackHandlerEnabled) {
enabled = state.currentHomeNavigationBarItem != HomeNavigationBarItem.Chats, if (state.currentHomeNavigationBarItem != HomeNavigationBarItem.Chats) {
) { state.eventSink(HomeEvent.SelectHomeNavigationBarItem(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() val hazeState = rememberHazeState()
@ -168,7 +176,6 @@ private fun HomeScaffold(
topBar = { topBar = {
HomeTopBar( HomeTopBar(
selectedNavigationItem = state.currentHomeNavigationBarItem, selectedNavigationItem = state.currentHomeNavigationBarItem,
title = stringResource(state.currentHomeNavigationBarItem.labelRes),
currentUserAndNeighbors = state.currentUserAndNeighbors, currentUserAndNeighbors = state.currentUserAndNeighbors,
showAvatarIndicator = state.showAvatarIndicator, showAvatarIndicator = state.showAvatarIndicator,
areSearchResultsDisplayed = roomListState.searchState.isSearchActive, areSearchResultsDisplayed = roomListState.searchState.isSearchActive,
@ -182,6 +189,7 @@ private fun HomeScaffold(
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
displayFilters = state.displayRoomListFilters, displayFilters = state.displayRoomListFilters,
filtersState = roomListState.filtersState, filtersState = roomListState.filtersState,
spaceFiltersState = roomListState.spaceFiltersState,
canCreateSpaces = state.homeSpacesState.canCreateSpaces, canCreateSpaces = state.homeSpacesState.canCreateSpaces,
canReportBug = state.canReportBug, canReportBug = state.canReportBug,
modifier = Modifier.hazeEffect( modifier = Modifier.hazeEffect(
@ -227,6 +235,7 @@ private fun HomeScaffold(
RoomListContentView( RoomListContentView(
contentState = roomListState.contentState, contentState = roomListState.contentState,
filtersState = roomListState.filtersState, filtersState = roomListState.filtersState,
spaceFiltersState = roomListState.spaceFiltersState,
lazyListState = roomsLazyListState, lazyListState = roomsLazyListState,
hideInvitesAvatars = roomListState.hideInvitesAvatars, hideInvitesAvatars = roomListState.hideInvitesAvatars,
eventSink = roomListState.eventSink, eventSink = roomListState.eventSink,
@ -256,6 +265,7 @@ private fun HomeScaffold(
.consumeWindowInsets(padding) .consumeWindowInsets(padding)
.hazeSource(state = hazeState) .hazeSource(state = hazeState)
) )
SpaceFiltersView(roomListState.spaceFiltersState)
} }
HomeNavigationBarItem.Spaces -> { HomeNavigationBarItem.Spaces -> {
HomeSpacesView( HomeSpacesView(

View file

@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.pager.VerticalPager import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState 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.RoomListFiltersState
import io.element.android.features.home.impl.filters.RoomListFiltersView 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.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.atomic.atoms.RedIndicatorAtom
import io.element.android.libraries.designsystem.components.TopAppBarScrollBehaviorLayout import io.element.android.libraries.designsystem.components.TopAppBarScrollBehaviorLayout
import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.Avatar
@ -75,7 +80,6 @@ import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun HomeTopBar( fun HomeTopBar(
selectedNavigationItem: HomeNavigationBarItem, selectedNavigationItem: HomeNavigationBarItem,
title: String,
currentUserAndNeighbors: ImmutableList<MatrixUser>, currentUserAndNeighbors: ImmutableList<MatrixUser>,
showAvatarIndicator: Boolean, showAvatarIndicator: Boolean,
areSearchResultsDisplayed: Boolean, areSearchResultsDisplayed: Boolean,
@ -89,6 +93,7 @@ fun HomeTopBar(
canReportBug: Boolean, canReportBug: Boolean,
displayFilters: Boolean, displayFilters: Boolean,
filtersState: RoomListFiltersState, filtersState: RoomListFiltersState,
spaceFiltersState: SpaceFiltersState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column(modifier) { Column(modifier) {
@ -103,12 +108,21 @@ fun HomeTopBar(
scrolledContainerColor = Color.Transparent, scrolledContainerColor = Color.Transparent,
), ),
title = { 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( Text(
modifier = Modifier.semantics { modifier = Modifier.semantics {
heading() heading()
}, },
style = ElementTheme.typography.aliasScreenTitle, style = ElementTheme.typography.aliasScreenTitle,
text = title, text = displayTitle,
) )
}, },
navigationIcon = { navigationIcon = {
@ -124,7 +138,8 @@ fun HomeTopBar(
HomeNavigationBarItem.Chats -> RoomListMenuItems( HomeNavigationBarItem.Chats -> RoomListMenuItems(
onToggleSearch = onToggleSearch, onToggleSearch = onToggleSearch,
onMenuActionClick = onMenuActionClick, onMenuActionClick = onMenuActionClick,
canReportBug = canReportBug canReportBug = canReportBug,
spaceFiltersState = spaceFiltersState,
) )
HomeNavigationBarItem.Spaces -> SpacesMenuItems( HomeNavigationBarItem.Spaces -> SpacesMenuItems(
canCreateSpaces = canCreateSpaces, canCreateSpaces = canCreateSpaces,
@ -154,6 +169,7 @@ private fun RoomListMenuItems(
onToggleSearch: () -> Unit, onToggleSearch: () -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit, onMenuActionClick: (RoomListMenuAction) -> Unit,
canReportBug: Boolean, canReportBug: Boolean,
spaceFiltersState: SpaceFiltersState,
) { ) {
IconButton( IconButton(
onClick = onToggleSearch, onClick = onToggleSearch,
@ -163,6 +179,7 @@ private fun RoomListMenuItems(
contentDescription = stringResource(CommonStrings.action_search), contentDescription = stringResource(CommonStrings.action_search),
) )
} }
SpaceFilterButton(spaceFiltersState = spaceFiltersState)
if (RoomListConfig.HAS_DROP_DOWN_MENU) { if (RoomListConfig.HAS_DROP_DOWN_MENU) {
var showMenu by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) }
IconButton( 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 @Composable
private fun NavigationIcon( private fun NavigationIcon(
currentUserAndNeighbors: ImmutableList<MatrixUser>, currentUserAndNeighbors: ImmutableList<MatrixUser>,
@ -309,7 +358,6 @@ private fun AccountIcon(
internal fun HomeTopBarPreview() = ElementPreview { internal fun HomeTopBarPreview() = ElementPreview {
HomeTopBar( HomeTopBar(
selectedNavigationItem = HomeNavigationBarItem.Chats, selectedNavigationItem = HomeNavigationBarItem.Chats,
title = stringResource(R.string.screen_roomlist_main_space_title),
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
showAvatarIndicator = false, showAvatarIndicator = false,
areSearchResultsDisplayed = false, areSearchResultsDisplayed = false,
@ -322,6 +370,30 @@ internal fun HomeTopBarPreview() = ElementPreview {
canReportBug = true, canReportBug = true,
displayFilters = true, displayFilters = true,
filtersState = aRoomListFiltersState(), 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 = {}, onMenuActionClick = {},
) )
} }
@ -332,7 +404,6 @@ internal fun HomeTopBarPreview() = ElementPreview {
internal fun HomeTopBarSpacesPreview() = ElementPreview { internal fun HomeTopBarSpacesPreview() = ElementPreview {
HomeTopBar( HomeTopBar(
selectedNavigationItem = HomeNavigationBarItem.Spaces, selectedNavigationItem = HomeNavigationBarItem.Spaces,
title = stringResource(R.string.screen_home_tab_spaces),
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
showAvatarIndicator = false, showAvatarIndicator = false,
areSearchResultsDisplayed = false, areSearchResultsDisplayed = false,
@ -345,6 +416,7 @@ internal fun HomeTopBarSpacesPreview() = ElementPreview {
canReportBug = true, canReportBug = true,
displayFilters = false, displayFilters = false,
filtersState = aRoomListFiltersState(), filtersState = aRoomListFiltersState(),
spaceFiltersState = anUnselectedSpaceFiltersState(),
onMenuActionClick = {}, onMenuActionClick = {},
) )
} }
@ -355,7 +427,6 @@ internal fun HomeTopBarSpacesPreview() = ElementPreview {
internal fun HomeTopBarWithIndicatorPreview() = ElementPreview { internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
HomeTopBar( HomeTopBar(
selectedNavigationItem = HomeNavigationBarItem.Chats, selectedNavigationItem = HomeNavigationBarItem.Chats,
title = stringResource(R.string.screen_roomlist_main_space_title),
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
showAvatarIndicator = true, showAvatarIndicator = true,
areSearchResultsDisplayed = false, areSearchResultsDisplayed = false,
@ -368,6 +439,7 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
canReportBug = true, canReportBug = true,
displayFilters = true, displayFilters = true,
filtersState = aRoomListFiltersState(), filtersState = aRoomListFiltersState(),
spaceFiltersState = anUnselectedSpaceFiltersState(),
onMenuActionClick = {}, onMenuActionClick = {},
) )
} }
@ -378,7 +450,6 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
internal fun HomeTopBarMultiAccountPreview() = ElementPreview { internal fun HomeTopBarMultiAccountPreview() = ElementPreview {
HomeTopBar( HomeTopBar(
selectedNavigationItem = HomeNavigationBarItem.Chats, selectedNavigationItem = HomeNavigationBarItem.Chats,
title = stringResource(R.string.screen_roomlist_main_space_title),
currentUserAndNeighbors = aMatrixUserList().take(3).toImmutableList(), currentUserAndNeighbors = aMatrixUserList().take(3).toImmutableList(),
showAvatarIndicator = false, showAvatarIndicator = false,
areSearchResultsDisplayed = false, areSearchResultsDisplayed = false,
@ -391,6 +462,7 @@ internal fun HomeTopBarMultiAccountPreview() = ElementPreview {
canReportBug = true, canReportBug = true,
displayFilters = true, displayFilters = true,
filtersState = aRoomListFiltersState(), filtersState = aRoomListFiltersState(),
spaceFiltersState = anUnselectedSpaceFiltersState(),
onMenuActionClick = {}, 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.RoomListContentStateProvider
import io.element.android.features.home.impl.roomlist.RoomListEvent 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.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.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
@ -59,6 +61,7 @@ import kotlinx.collections.immutable.ImmutableList
fun RoomListContentView( fun RoomListContentView(
contentState: RoomListContentState, contentState: RoomListContentState,
filtersState: RoomListFiltersState, filtersState: RoomListFiltersState,
spaceFiltersState: SpaceFiltersState,
lazyListState: LazyListState, lazyListState: LazyListState,
hideInvitesAvatars: Boolean, hideInvitesAvatars: Boolean,
eventSink: (RoomListEvent) -> Unit, eventSink: (RoomListEvent) -> Unit,
@ -93,6 +96,7 @@ fun RoomListContentView(
state = contentState, state = contentState,
hideInvitesAvatars = hideInvitesAvatars, hideInvitesAvatars = hideInvitesAvatars,
filtersState = filtersState, filtersState = filtersState,
spaceFiltersState = spaceFiltersState,
eventSink = eventSink, eventSink = eventSink,
onSetUpRecoveryClick = onSetUpRecoveryClick, onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
@ -172,6 +176,7 @@ private fun RoomsView(
state: RoomListContentState.Rooms, state: RoomListContentState.Rooms,
hideInvitesAvatars: Boolean, hideInvitesAvatars: Boolean,
filtersState: RoomListFiltersState, filtersState: RoomListFiltersState,
spaceFiltersState: SpaceFiltersState,
eventSink: (RoomListEvent) -> Unit, eventSink: (RoomListEvent) -> Unit,
onSetUpRecoveryClick: () -> Unit, onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit,
@ -180,9 +185,12 @@ private fun RoomsView(
lazyListState: LazyListState, lazyListState: LazyListState,
modifier: Modifier = Modifier, 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( EmptyViewForFilterStates(
selectedFilters = filtersState.selectedFilters(), selectedFilters = filtersState.selectedFilters(),
isSpaceFilterSelected = isSpaceFilterSelected,
modifier = modifier.fillMaxSize() modifier = modifier.fillMaxSize()
) )
} else { } else {
@ -278,9 +286,10 @@ private fun RoomsViewList(
@Composable @Composable
private fun EmptyViewForFilterStates( private fun EmptyViewForFilterStates(
selectedFilters: ImmutableList<RoomListFilter>, selectedFilters: ImmutableList<RoomListFilter>,
isSpaceFilterSelected: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) ?: return val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected) ?: return
EmptyScaffold( EmptyScaffold(
title = emptyStateResources.title, title = emptyStateResources.title,
subtitle = emptyStateResources.subtitle, subtitle = emptyStateResources.subtitle,
@ -331,6 +340,7 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
) )
} }
), ),
spaceFiltersState = anUnselectedSpaceFiltersState(),
hideInvitesAvatars = false, hideInvitesAvatars = false,
eventSink = {}, eventSink = {},
onSetUpRecoveryClick = {}, onSetUpRecoveryClick = {},

View file

@ -95,6 +95,7 @@ class RoomListRoomSummaryFactory(
content = content, 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.roomlist.RoomListState
import io.element.android.features.home.impl.search.RoomListSearchPresenter 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.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.architecture.Presenter
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ -31,4 +33,7 @@ interface RoomListModule {
@Binds @Binds
fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter<RoomListFiltersState> 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 package io.element.android.features.home.impl.filters
import io.element.android.features.home.impl.R 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. * 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) 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. * 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 { 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.isEmpty() -> null
selectedFilters.size == 1 -> { selectedFilters.size == 1 -> {
when (selectedFilters.first()) { when (selectedFilters.first()) {

View file

@ -9,24 +9,17 @@
package io.element.android.features.home.impl.filters package io.element.android.features.home.impl.filters
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import dev.zacsweers.metro.Inject 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.features.home.impl.filters.selection.FilterSelectionStrategy
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import kotlinx.collections.immutable.toImmutableList 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 @Inject
class RoomListFiltersPresenter( class RoomListFiltersPresenter(
private val roomListDataSource: RoomListDataSource,
private val filterSelectionStrategy: FilterSelectionStrategy, private val filterSelectionStrategy: FilterSelectionStrategy,
) : Presenter<RoomListFiltersState> { ) : Presenter<RoomListFiltersState> {
private val initialFilters = filterSelectionStrategy.filterSelectionStates.value.toImmutableList()
@Composable @Composable
override fun present(): RoomListFiltersState { override fun present(): RoomListFiltersState {
fun handleEvent(event: RoomListFiltersEvent) { fun handleEvent(event: RoomListFiltersEvent) {
@ -40,31 +33,9 @@ class RoomListFiltersPresenter(
} }
} }
val filters by produceState(initialValue = initialFilters) { val filters by filterSelectionStrategy.filterSelectionStates.collectAsState()
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)
}
}
return RoomListFiltersState( return RoomListFiltersState(
filterSelectionStates = filters, filterSelectionStates = filters.toImmutableList(),
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }

View file

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

View file

@ -13,7 +13,6 @@ import kotlinx.coroutines.flow.StateFlow
interface FilterSelectionStrategy { interface FilterSelectionStrategy {
val filterSelectionStates: StateFlow<Set<FilterSelectionState>> val filterSelectionStates: StateFlow<Set<FilterSelectionState>>
fun select(filter: RoomListFilter) fun select(filter: RoomListFilter)
fun deselect(filter: RoomListFilter) fun deselect(filter: RoomListFilter)
fun isSelected(filter: RoomListFilter): Boolean 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.Announcement
import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.datasource.RoomListDataSource 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.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.RoomListSearchEvent
import io.element.android.features.home.impl.search.RoomListSearchState 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.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.AcceptInvite import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.AcceptInvite
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.DeclineInvite 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.core.RoomId
import io.element.android.libraries.matrix.api.encryption.RecoveryState 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.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.api.timeline.ReceiptType
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.AppPreferencesStore
@ -83,6 +89,7 @@ class RoomListPresenter(
private val seenInvitesStore: SeenInvitesStore, private val seenInvitesStore: SeenInvitesStore,
private val announcementService: AnnouncementService, private val announcementService: AnnouncementService,
private val coldStartWatcher: AnalyticsColdStartWatcher, private val coldStartWatcher: AnalyticsColdStartWatcher,
private val spaceFiltersPresenter: Presenter<SpaceFiltersState>,
) : Presenter<RoomListState> { ) : Presenter<RoomListState> {
private val encryptionService = client.encryptionService private val encryptionService = client.encryptionService
@ -92,6 +99,7 @@ class RoomListPresenter(
val leaveRoomState = leaveRoomPresenter.present() val leaveRoomState = leaveRoomPresenter.present()
val filtersState = filtersPresenter.present() val filtersState = filtersPresenter.present()
val searchState = searchPresenter.present() val searchState = searchPresenter.present()
val spaceFiltersState = spaceFiltersPresenter.present()
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present() val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
LaunchedEffect(Unit) { 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( val contentState = roomListContentState(
securityBannerDismissed, securityBannerDismissed,
showNewNotificationSoundBanner, showNewNotificationSoundBanner,
@ -163,6 +178,7 @@ class RoomListPresenter(
leaveRoomState = leaveRoomState, leaveRoomState = leaveRoomState,
filtersState = filtersState, filtersState = filtersState,
searchState = searchState, searchState = searchState,
spaceFiltersState = spaceFiltersState,
contentState = contentState, contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState, acceptDeclineInviteState = acceptDeclineInviteState,
hideInvitesAvatars = hideInvitesAvatar, 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.filters.RoomListFiltersState
import io.element.android.features.home.impl.model.RoomListRoomSummary 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.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.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
@ -26,6 +27,7 @@ data class RoomListState(
val leaveRoomState: LeaveRoomState, val leaveRoomState: LeaveRoomState,
val filtersState: RoomListFiltersState, val filtersState: RoomListFiltersState,
val searchState: RoomListSearchState, val searchState: RoomListSearchState,
val spaceFiltersState: SpaceFiltersState,
val contentState: RoomListContentState, val contentState: RoomListContentState,
val acceptDeclineInviteState: AcceptDeclineInviteState, val acceptDeclineInviteState: AcceptDeclineInviteState,
val hideInvitesAvatars: Boolean, 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.model.anInviteSender
import io.element.android.features.home.impl.search.RoomListSearchState 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.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.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomEvent
@ -52,6 +54,7 @@ internal fun aRoomListState(
leaveRoomState: LeaveRoomState = aLeaveRoomState(), leaveRoomState: LeaveRoomState = aLeaveRoomState(),
searchState: RoomListSearchState = aRoomListSearchState(), searchState: RoomListSearchState = aRoomListSearchState(),
filtersState: RoomListFiltersState = aRoomListFiltersState(), filtersState: RoomListFiltersState = aRoomListFiltersState(),
spaceFiltersState: SpaceFiltersState = anUnselectedSpaceFiltersState(),
contentState: RoomListContentState = aRoomsContentState(), contentState: RoomListContentState = aRoomsContentState(),
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
hideInvitesAvatars: Boolean = false, hideInvitesAvatars: Boolean = false,
@ -63,6 +66,7 @@ internal fun aRoomListState(
leaveRoomState = leaveRoomState, leaveRoomState = leaveRoomState,
filtersState = filtersState, filtersState = filtersState,
searchState = searchState, searchState = searchState,
spaceFiltersState = spaceFiltersState,
contentState = contentState, contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState, acceptDeclineInviteState = acceptDeclineInviteState,
hideInvitesAvatars = hideInvitesAvatars, 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 canCreateSpaces by featureFlagsService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false)
val hideInvitesAvatar by client.rememberHideInvitesAvatar() val hideInvitesAvatar by client.rememberHideInvitesAvatar()
val spaceRooms by remember { val spaceRooms by remember {
client.spaceService.spaceRoomsFlow.map { it.toImmutableList() } client.spaceService.topLevelSpacesFlow.map { it.toImmutableList() }
}.collectAsState(persistentListOf()) }.collectAsState(persistentListOf())
val seenSpaceInvites by remember { 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_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_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_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_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> <string name="session_verification_banner_title">"Ověřte, že jste to vy"</string>
</resources> </resources>

View file

@ -16,14 +16,14 @@ class RoomListFiltersEmptyStateResourcesTest {
@Test @Test
fun `fromSelectedFilters should return null when selectedFilters is empty`() { fun `fromSelectedFilters should return null when selectedFilters is empty`() {
val selectedFilters = emptyList<RoomListFilter>() val selectedFilters = emptyList<RoomListFilter>()
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
assertThat(result).isNull() assertThat(result).isNull()
} }
@Test @Test
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only unread filter`() { fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only unread filter`() {
val selectedFilters = listOf(RoomListFilter.Unread) val selectedFilters = listOf(RoomListFilter.Unread)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
assertThat(result).isNotNull() assertThat(result).isNotNull()
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_unreads_empty_state_title) 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) assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
@ -32,7 +32,7 @@ class RoomListFiltersEmptyStateResourcesTest {
@Test @Test
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only people filter`() { fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only people filter`() {
val selectedFilters = listOf(RoomListFilter.People) val selectedFilters = listOf(RoomListFilter.People)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
assertThat(result).isNotNull() assertThat(result).isNotNull()
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_people_empty_state_title) 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) assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
@ -41,7 +41,7 @@ class RoomListFiltersEmptyStateResourcesTest {
@Test @Test
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only rooms filter`() { fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only rooms filter`() {
val selectedFilters = listOf(RoomListFilter.Rooms) val selectedFilters = listOf(RoomListFilter.Rooms)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
assertThat(result).isNotNull() assertThat(result).isNotNull()
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_rooms_empty_state_title) 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) assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
@ -50,7 +50,7 @@ class RoomListFiltersEmptyStateResourcesTest {
@Test @Test
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only favourites filter`() { fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only favourites filter`() {
val selectedFilters = listOf(RoomListFilter.Favourites) val selectedFilters = listOf(RoomListFilter.Favourites)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
assertThat(result).isNotNull() assertThat(result).isNotNull()
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_favourites_empty_state_title) 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) assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_favourites_empty_state_subtitle)
@ -59,7 +59,7 @@ class RoomListFiltersEmptyStateResourcesTest {
@Test @Test
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only invites filter`() { fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only invites filter`() {
val selectedFilters = listOf(RoomListFilter.Invites) val selectedFilters = listOf(RoomListFilter.Invites)
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false)
assertThat(result).isNotNull() assertThat(result).isNotNull()
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_invites_empty_state_title) 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) assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
@ -68,7 +68,15 @@ class RoomListFiltersEmptyStateResourcesTest {
@Test @Test
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has multiple filters`() { fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has multiple filters`() {
val selectedFilters = listOf(RoomListFilter.Unread, RoomListFilter.People, RoomListFilter.Rooms, RoomListFilter.Favourites) 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).isNotNull()
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_title) 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) 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 package io.element.android.features.home.impl.filters
import com.google.common.truth.Truth.assertThat 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.DefaultFilterSelectionStrategy
import io.element.android.features.home.impl.filters.selection.FilterSelectionState 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.awaitLastSequentialItem
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.advanceUntilIdle
@ -54,8 +41,7 @@ class RoomListFiltersPresenterTest {
@Test @Test
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun `present - toggle rooms filter`() = runTest { fun `present - toggle rooms filter`() = runTest {
val roomListService = FakeRoomListService() val presenter = createRoomListFiltersPresenter()
val presenter = createRoomListFiltersPresenter(roomListService)
presenter.test { presenter.test {
awaitItem().eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms)) awaitItem().eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms))
awaitLastSequentialItem().let { state -> awaitLastSequentialItem().let { state ->
@ -89,8 +75,7 @@ class RoomListFiltersPresenterTest {
@Test @Test
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun `present - clear filters event`() = runTest { fun `present - clear filters event`() = runTest {
val roomListService = FakeRoomListService() val presenter = createRoomListFiltersPresenter()
val presenter = createRoomListFiltersPresenter(roomListService)
presenter.test { presenter.test {
awaitItem().eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms)) awaitItem().eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms))
awaitLastSequentialItem().let { state -> awaitLastSequentialItem().let { state ->
@ -110,25 +95,8 @@ private fun filterSelectionState(filter: RoomListFilter, selected: Boolean) = Fi
isSelected = selected, isSelected = selected,
) )
private fun TestScope.createRoomListFiltersPresenter( private fun TestScope.createRoomListFiltersPresenter(): RoomListFiltersPresenter {
roomListService: RoomListService = FakeRoomListService(),
notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
dateFormatter: DateFormatter = FakeDateFormatter(),
roomLatestEventFormatter: RoomLatestEventFormatter = FakeRoomLatestEventFormatter(),
): RoomListFiltersPresenter {
return 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(), 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.RoomListSearchEvent
import io.element.android.features.home.impl.search.RoomListSearchState 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.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.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
@ -660,6 +662,7 @@ class RoomListPresenterTest {
analyticsService: AnalyticsService = FakeAnalyticsService(), analyticsService: AnalyticsService = FakeAnalyticsService(),
filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() }, filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() },
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() }, searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
spaceFiltersPresenter: Presenter<SpaceFiltersState> = Presenter { aDisabledSpaceFiltersState() },
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }, acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(), notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
@ -683,6 +686,7 @@ class RoomListPresenterTest {
searchPresenter = searchPresenter, searchPresenter = searchPresenter,
sessionPreferencesStore = sessionPreferencesStore, sessionPreferencesStore = sessionPreferencesStore,
filtersPresenter = filtersPresenter, filtersPresenter = filtersPresenter,
spaceFiltersPresenter = spaceFiltersPresenter,
analyticsService = analyticsService, analyticsService = analyticsService,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() }, 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"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <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_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_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_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_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_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_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_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_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_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_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">"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_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_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> <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_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_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_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_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_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. <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 -> { is QrLoginException.Cancelled -> {
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.Cancelled)) backstack.replace(NavTarget.Error(QrCodeErrorScreenType.Cancelled))
} }
is QrLoginException.Expired -> { is QrLoginException.Expired,
is QrLoginException.NotFound -> {
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.Expired)) backstack.replace(NavTarget.Error(QrCodeErrorScreenType.Expired))
} }
is QrLoginException.Declined -> { is QrLoginException.Declined -> {
@ -138,7 +139,9 @@ class QrCodeLoginFlowNode(
Timber.e(error, "OIDC metadata is invalid") Timber.e(error, "OIDC metadata is invalid")
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.UnknownError)) backstack.replace(NavTarget.Error(QrCodeErrorScreenType.UnknownError))
} }
else -> { QrLoginException.CheckCodeAlreadySent,
QrLoginException.CheckCodeCannotBeSent,
QrLoginException.Unknown -> {
Timber.e(error, "Unknown error found") Timber.e(error, "Unknown error found")
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.UnknownError)) 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_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_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_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_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_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. <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) qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.Expired)
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.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) qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.Declined)
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.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.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -69,6 +70,7 @@ class DefaultVoiceMessageComposerPresenter(
} }
private val permissionsPresenter = permissionsPresenterFactory.create(Manifest.permission.RECORD_AUDIO) private val permissionsPresenter = permissionsPresenterFactory.create(Manifest.permission.RECORD_AUDIO)
private var pendingEvent: VoiceMessageRecorderEvent.Start? = null
private val mediaSender = mediaSenderFactory.create(timelineMode) private val mediaSender = mediaSenderFactory.create(timelineMode)
@Composable @Composable
@ -77,8 +79,7 @@ class DefaultVoiceMessageComposerPresenter(
val recorderState by voiceRecorder.state.collectAsState(initial = VoiceRecorderState.Idle) val recorderState by voiceRecorder.state.collectAsState(initial = VoiceRecorderState.Idle)
val playerState by player.state.collectAsState(initial = VoiceMessageComposerPlayer.State.Initial) val playerState by player.state.collectAsState(initial = VoiceMessageComposerPlayer.State.Initial)
val keepScreenOn by remember { derivedStateOf { recorderState is VoiceRecorderState.Recording } } val keepScreenOn by remember { derivedStateOf { recorderState is VoiceRecorderState.Recording } }
val permissionState by rememberUpdatedState(permissionsPresenter.present())
val permissionState = permissionsPresenter.present()
var isSending by remember { mutableStateOf(false) } var isSending by remember { mutableStateOf(false) }
var showSendFailureDialog by remember { mutableStateOf(false) } var showSendFailureDialog by remember { mutableStateOf(false) }
@ -88,6 +89,15 @@ class DefaultVoiceMessageComposerPresenter(
player.setMedia(recording.file.path) player.setMedia(recording.file.path)
} }
LaunchedEffect(permissionState.permissionGranted) {
if (permissionState.permissionGranted) {
pendingEvent?.let {
localCoroutineScope.startRecording()
pendingEvent = null
}
}
}
fun handleLifecycleEvent(event: Lifecycle.Event) { fun handleLifecycleEvent(event: Lifecycle.Event) {
when (event) { when (event) {
Lifecycle.Event.ON_PAUSE -> { Lifecycle.Event.ON_PAUSE -> {
@ -102,6 +112,7 @@ class DefaultVoiceMessageComposerPresenter(
} }
fun handleVoiceMessageRecorderEvent(event: VoiceMessageRecorderEvent) { fun handleVoiceMessageRecorderEvent(event: VoiceMessageRecorderEvent) {
pendingEvent = null
when (event) { when (event) {
VoiceMessageRecorderEvent.Start -> { VoiceMessageRecorderEvent.Start -> {
Timber.v("Voice message record button pressed") Timber.v("Voice message record button pressed")
@ -111,6 +122,7 @@ class DefaultVoiceMessageComposerPresenter(
} }
else -> { else -> {
Timber.i("Voice message permission needed") Timber.i("Voice message permission needed")
pendingEvent = VoiceMessageRecorderEvent.Start
permissionState.eventSink(PermissionsEvent.RequestPermissions) permissionState.eventSink(PermissionsEvent.RequestPermissions)
} }
} }

View file

@ -522,7 +522,9 @@ class DefaultVoiceMessageComposerPresenterTest {
permissionsPresenter.setPermissionGranted() permissionsPresenter.setPermissionGranted()
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
val finalState = awaitItem() advanceUntilIdle()
val finalState = expectMostRecentItem()
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
voiceRecorder.assertCalls(stopped = 1, started = 1) voiceRecorder.assertCalls(stopped = 1, started = 1)
@ -547,14 +549,16 @@ class DefaultVoiceMessageComposerPresenterTest {
assertThat(it.showPermissionRationaleDialog).isTrue() assertThat(it.showPermissionRationaleDialog).isTrue()
it.eventSink(VoiceMessageComposerEvent.AcceptPermissionRationale) it.eventSink(VoiceMessageComposerEvent.AcceptPermissionRationale)
} }
skipItems(1)
// Dialog is hidden, user accepts permissions // Dialog is hidden, user accepts permissions
assertThat(awaitItem().showPermissionRationaleDialog).isFalse() assertThat(awaitItem().showPermissionRationaleDialog).isFalse()
// Permission is granted, recording starts automatically
permissionsPresenter.setPermissionGranted() permissionsPresenter.setPermissionGranted()
advanceUntilIdle()
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) val finalState = expectMostRecentItem()
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
voiceRecorder.assertCalls(started = 1) voiceRecorder.assertCalls(started = 1)
@ -579,12 +583,14 @@ class DefaultVoiceMessageComposerPresenterTest {
assertThat(it.showPermissionRationaleDialog).isTrue() assertThat(it.showPermissionRationaleDialog).isTrue()
it.eventSink(VoiceMessageComposerEvent.DismissPermissionsRationale) it.eventSink(VoiceMessageComposerEvent.DismissPermissionsRationale)
} }
skipItems(1)
// Dialog is hidden, user tries to record again // Dialog is hidden, user tries to record again
awaitItem().also { awaitItem().also {
assertThat(it.showPermissionRationaleDialog).isFalse() assertThat(it.showPermissionRationaleDialog).isFalse()
it.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) it.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
} }
skipItems(1)
// Dialog is shown once again // Dialog is shown once again
val finalState = awaitItem().also { val finalState = awaitItem().also {
@ -593,6 +599,7 @@ class DefaultVoiceMessageComposerPresenterTest {
} }
voiceRecorder.assertCalls(started = 0) voiceRecorder.assertCalls(started = 0)
cancelAndIgnoreRemainingEvents()
testPauseAndDestroy(finalState) 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_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_change_role_unsaved_changes_title">"Lagre endringer?"</string>
<string name="screen_room_member_list_banned_empty">"Det er ingen utestengte brukere."</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"> <plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d person"</item> <item quantity="one">"%1$d person"</item>
<item quantity="other">"%1$d personer"</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_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_banned">"Utestengt"</string>
<string name="screen_room_member_list_mode_members">"Medlemmer"</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_administrator">"Admin"</string>
<string name="screen_room_member_list_role_moderator">"Moderator"</string> <string name="screen_room_member_list_role_moderator">"Moderator"</string>
<string name="screen_room_member_list_role_owner">"Eier"</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 canReportRoom by produceState(false) { value = client.canReportRoom() }
val enableKeyShareOnInvite by featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(initial = false)
return RoomDetailsState( return RoomDetailsState(
roomId = room.roomId, roomId = room.roomId,
roomName = roomName, roomName = roomName,
@ -197,6 +199,8 @@ class RoomDetailsPresenter(
isTombstoned = roomInfo.successorRoom != null, isTombstoned = roomInfo.successorRoom != null,
showDebugInfo = isDeveloperModeEnabled, showDebugInfo = isDeveloperModeEnabled,
roomVersion = roomInfo.roomVersion, roomVersion = roomInfo.roomVersion,
enableKeyShareOnInvite = enableKeyShareOnInvite,
roomHistoryVisibility = roomInfo.historyVisibility,
eventSink = ::handleEvent, 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.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember 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.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.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -50,6 +51,8 @@ data class RoomDetailsState(
val isTombstoned: Boolean, val isTombstoned: Boolean,
val showDebugInfo: Boolean, val showDebugInfo: Boolean,
val roomVersion: String?, val roomVersion: String?,
val enableKeyShareOnInvite: Boolean,
val roomHistoryVisibility: RoomHistoryVisibility,
val eventSink: (RoomDetailsEvent) -> Unit val eventSink: (RoomDetailsEvent) -> Unit
) { ) {
val roomBadges = buildList { val roomBadges = buildList {
@ -61,6 +64,14 @@ data class RoomDetailsState(
if (isPublic) { if (isPublic) {
add(RoomBadge.PUBLIC) 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() }.toImmutableList()
} }
@ -84,4 +95,7 @@ enum class RoomBadge {
ENCRYPTED, ENCRYPTED,
NOT_ENCRYPTED, NOT_ENCRYPTED,
PUBLIC, 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.RoomMembershipState
import io.element.android.libraries.matrix.api.room.RoomNotificationMode 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.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.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -57,6 +58,9 @@ open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState>
aRoomDetailsState(isTombstoned = true), aRoomDetailsState(isTombstoned = true),
aDmRoomDetailsState(dmRoomMemberVerificationState = UserProfileVerificationState.VERIFIED), aDmRoomDetailsState(dmRoomMemberVerificationState = UserProfileVerificationState.VERIFIED),
aDmRoomDetailsState(dmRoomMemberVerificationState = UserProfileVerificationState.VERIFICATION_VIOLATION), aDmRoomDetailsState(dmRoomMemberVerificationState = UserProfileVerificationState.VERIFICATION_VIOLATION),
aSharedHistoryRoomDetailsState(roomHistoryVisibility = RoomHistoryVisibility.Joined),
aSharedHistoryRoomDetailsState(roomHistoryVisibility = RoomHistoryVisibility.Shared),
aSharedHistoryRoomDetailsState(roomHistoryVisibility = RoomHistoryVisibility.WorldReadable),
// Add other state here // Add other state here
) )
} }
@ -117,6 +121,8 @@ fun aRoomDetailsState(
canReportRoom: Boolean = true, canReportRoom: Boolean = true,
isTombstoned: Boolean = false, isTombstoned: Boolean = false,
showDebugInfo: Boolean = false, showDebugInfo: Boolean = false,
enableKeyShareOnInvite: Boolean = false,
roomHistoryVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Shared,
eventSink: (RoomDetailsEvent) -> Unit = {}, eventSink: (RoomDetailsEvent) -> Unit = {},
) = RoomDetailsState( ) = RoomDetailsState(
roomId = roomId, roomId = roomId,
@ -147,6 +153,8 @@ fun aRoomDetailsState(
isTombstoned = isTombstoned, isTombstoned = isTombstoned,
showDebugInfo = showDebugInfo, showDebugInfo = showDebugInfo,
roomVersion = "12", roomVersion = "12",
enableKeyShareOnInvite = enableKeyShareOnInvite,
roomHistoryVisibility = roomHistoryVisibility,
eventSink = eventSink, eventSink = eventSink,
) )
@ -182,3 +190,11 @@ fun aDmRoomDetailsState(
verificationState = dmRoomMemberVerificationState, 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, 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_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_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_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">"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_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> <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"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <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_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_edit_room_address_title">"Muuda aadressi"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Teavituste seadistamisel tekkis viga"</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"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <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_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_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> <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_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_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_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_and_privacy_title">"Sikkerhet og personvern"</string>
<string name="screen_room_details_security_title">"Sikkerhet"</string> <string name="screen_room_details_security_title">"Sikkerhet"</string>
<string name="screen_room_details_share_room_title">"Del rom"</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_topic_title">"Emne"</string>
<string name="screen_room_details_updating_room">"Oppdaterer rommet …"</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> <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"> <plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d person"</item> <item quantity="one">"%1$d person"</item>
<item quantity="other">"%1$d personer"</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_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_banned">"Utestengt"</string>
<string name="screen_room_member_list_mode_members">"Medlemmer"</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_administrator">"Admin"</string>
<string name="screen_room_member_list_role_moderator">"Moderator"</string> <string name="screen_room_member_list_role_moderator">"Moderator"</string>
<string name="screen_room_member_list_role_owner">"Eier"</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_room_details">"Romdetaljer"</string>
<string name="screen_room_roles_and_permissions_title">"Roller og tillatelser"</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_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_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_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_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. <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. 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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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. <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> 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_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_room_visibility_section_header">"Synlighet"</string>
<string name="screen_security_and_privacy_title">"Sikkerhet og personvern"</string> <string name="screen_security_and_privacy_title">"Sikkerhet og personvern"</string>
</resources> </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_room_details">"Rumsdetaljer"</string>
<string name="screen_room_roles_and_permissions_title">"Roller och behörigheter"</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_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_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_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. <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_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_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_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_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_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_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> <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"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <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_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_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> <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 package io.element.android.features.roomdetails.impl
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import org.junit.Test import org.junit.Test
@ -56,4 +57,52 @@ class RoomDetailsStateTest {
persistentListOf(RoomBadge.ENCRYPTED) 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_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_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_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">"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_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> <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"> <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_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_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_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_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_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_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. <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. 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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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. <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> 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_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_room_visibility_section_header">"Synlighet"</string>
<string name="screen_security_and_privacy_title">"Sikkerhet og personvern"</string> <string name="screen_security_and_privacy_title">"Sikkerhet og personvern"</string>
</resources> </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_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_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_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_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_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. <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_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_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_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_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_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_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> <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"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <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> <string name="screen_leave_space_last_admin_info">"%1$s (Správce)"</string>
<plurals name="screen_leave_space_submit"> <plurals name="screen_leave_space_submit">
<item quantity="one">"Opustit %1$d místnost a prostor"</item> <item quantity="one">"Opustit %1$d místnost a prostor"</item>
@ -8,10 +9,21 @@
</plurals> </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">"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_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_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">"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_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_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_leave_space">"Opustit prostor"</string>
<string name="screen_space_settings_roles_and_permissions">"Role a oprávnění"</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> <string name="screen_space_settings_security_and_privacy">"Zabezpečení a soukromí"</string>

View file

@ -8,13 +8,16 @@
</plurals> </plurals>
<string name="screen_leave_space_subtitle">"Sellega eemaldad end ka kõikidest antud kogukonna jututubadest."</string> <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_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_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">"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_admin">"Sa oled siin ainus peakasutaja: %1$s"</string>
<string name="screen_leave_space_title_last_owner">"Anna omand üle"</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_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_empty_state_title">"Lisa oma esimene jututuba"</string>
<string name="screen_space_menu_action_members">"Vaata liikmeid"</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"> <plurals name="screen_space_remove_rooms_confirmation_title">
<item quantity="one">"Eemalda %1$d jututuba „%2$s“ kogukonnast"</item> <item quantity="one">"Eemalda %1$d jututuba „%2$s“ kogukonnast"</item>
<item quantity="other">"Eemalda %1$d jututuba „%2$s“ kogukonnast"</item> <item quantity="other">"Eemalda %1$d jututuba „%2$s“ kogukonnast"</item>

View file

@ -5,10 +5,10 @@
# Project # Project
android_gradle_plugin = "8.13.2" android_gradle_plugin = "8.13.2"
# When updateing this, please also update the version in the file ./idea/kotlinc.xml # When updateing this, please also update the version in the file ./idea/kotlinc.xml
kotlin = "2.3.0" kotlin = "2.3.10"
kotlinpoet = "2.2.0" kotlinpoet = "2.2.0"
ksp = "2.3.5" ksp = "2.3.5"
firebaseAppDistribution = "5.2.0" firebaseAppDistribution = "5.2.1"
# AndroidX # AndroidX
core = "1.17.0" core = "1.17.0"
@ -17,7 +17,7 @@ constraintlayout = "2.2.1"
constraintlayout_compose = "1.1.1" constraintlayout_compose = "1.1.1"
lifecycle = "2.10.0" lifecycle = "2.10.0"
activity = "1.12.3" activity = "1.12.3"
media3 = "1.9.1" media3 = "1.9.2"
camera = "1.5.3" camera = "1.5.3"
work = "2.11.1" work = "2.11.1"
@ -78,7 +78,7 @@ kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlin
kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" } kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" }
ksp_gradle_plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } ksp_gradle_plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
# https://firebase.google.com/docs/android/setup#available-libraries # https://firebase.google.com/docs/android/setup#available-libraries
google_firebase_bom = "com.google.firebase:firebase-bom:34.8.0" google_firebase_bom = "com.google.firebase:firebase-bom:34.9.0"
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" } firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" } autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
@ -177,7 +177,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version
# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt # https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt
# All new features should not be implemented in the pull request that upgrades the version, developers should # All new features should not be implemented in the pull request that upgrades the version, developers should
# only fix API breaks and may add some TODOs. # only fix API breaks and may add some TODOs.
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.1.27" matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.2.6"
# Others # Others
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
@ -218,8 +218,8 @@ haze_materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref =
color_picker = "io.mhssn:colorpicker:1.0.0" color_picker = "io.mhssn:colorpicker:1.0.0"
# Analytics # Analytics
posthog = "com.posthog:posthog-android:3.30.0" posthog = "com.posthog:posthog-android:3.31.0"
sentry = "io.sentry:sentry-android:8.31.0" sentry = "io.sentry:sentry-android:8.32.0"
# main branch can be tested replacing the version with main-SNAPSHOT # main branch can be tested replacing the version with main-SNAPSHOT
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.29.2" matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.29.2"

View file

@ -98,6 +98,18 @@ internal fun MatrixBadgeAtomNegativePreview() = ElementPreview {
) )
} }
@PreviewsDayNight
@Composable
internal fun MatrixBadgeAtomNeutralWrappingPreview() = ElementPreview {
MatrixBadgeAtom.View(
MatrixBadgeAtom.MatrixBadgeData(
text = "How much wood could a wood chuck chuck if a wood chuck could chuck wood",
icon = CompoundIcons.LockOff(),
type = MatrixBadgeAtom.Type.Info,
)
)
}
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun MatrixBadgeAtomInfoPreview() = ElementPreview { internal fun MatrixBadgeAtomInfoPreview() = ElementPreview {

View file

@ -9,9 +9,10 @@
package io.element.android.libraries.designsystem.atomic.molecules package io.element.android.libraries.designsystem.atomic.molecules
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.atomic.atoms.MatrixBadgeAtom import io.element.android.libraries.designsystem.atomic.atoms.MatrixBadgeAtom
@ -22,10 +23,11 @@ fun MatrixBadgeRowMolecule(
data: ImmutableList<MatrixBadgeAtom.MatrixBadgeData>, data: ImmutableList<MatrixBadgeAtom.MatrixBadgeData>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row( FlowRow(
modifier = modifier modifier = modifier
.padding(start = 16.dp, end = 16.dp, top = 8.dp), .padding(start = 16.dp, end = 16.dp, top = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
for (badge in data) { for (badge in data) {
MatrixBadgeAtom.View(badge) MatrixBadgeAtom.View(badge)

View file

@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
@ -63,6 +64,8 @@ fun Badge(
text = text, text = text,
style = ElementTheme.typography.fontBodySmRegular, style = ElementTheme.typography.fontBodySmRegular,
color = textColor, color = textColor,
overflow = TextOverflow.Ellipsis,
softWrap = false,
) )
} }
} }

View file

@ -84,6 +84,13 @@ enum class FeatureFlags(
defaultValue = { false }, defaultValue = { false },
isFinished = false, isFinished = false,
), ),
RoomListSpaceFilters(
key = "feature.roomListSpaceFilters",
title = "Room list space filters",
description = "Allow filtering the room list by space.",
defaultValue = { false },
isFinished = false,
),
PrintLogsToLogcat( PrintLogsToLogcat(
key = "feature.print_logs_to_logcat", key = "feature.print_logs_to_logcat",
title = "Print logs to logcat", title = "Print logs to logcat",

View file

@ -13,6 +13,7 @@ sealed class QrLoginException : Exception() {
data object ConnectionInsecure : QrLoginException() data object ConnectionInsecure : QrLoginException()
data object Declined : QrLoginException() data object Declined : QrLoginException()
data object Expired : QrLoginException() data object Expired : QrLoginException()
data object NotFound : QrLoginException()
data object LinkingNotSupported : QrLoginException() data object LinkingNotSupported : QrLoginException()
data object OidcMetadataInvalid : QrLoginException() data object OidcMetadataInvalid : QrLoginException()
data object SlidingSyncNotAvailable : QrLoginException() data object SlidingSyncNotAvailable : QrLoginException()
@ -20,5 +21,4 @@ sealed class QrLoginException : Exception() {
data object CheckCodeAlreadySent : QrLoginException() data object CheckCodeAlreadySent : QrLoginException()
data object CheckCodeCannotBeSent : QrLoginException() data object CheckCodeCannotBeSent : QrLoginException()
data object Unknown : QrLoginException() data object Unknown : QrLoginException()
data object NotFound : QrLoginException()
} }

View file

@ -8,6 +8,9 @@
package io.element.android.libraries.matrix.api.room.history package io.element.android.libraries.matrix.api.room.history
import androidx.compose.runtime.Immutable
@Immutable
sealed interface RoomHistoryVisibility { sealed interface RoomHistoryVisibility {
/** /**
* Previous events are accessible to newly joined members from the point * Previous events are accessible to newly joined members from the point

View file

@ -28,4 +28,10 @@ sealed interface LatestEventValue {
val senderProfile: ProfileDetails, val senderProfile: ProfileDetails,
val isSending: Boolean, val isSending: Boolean,
) : LatestEventValue ) : LatestEventValue
data class RoomInvite(
val timestamp: Long,
val inviterId: UserId?,
val invitedProfile: ProfileDetails,
) : LatestEventValue
} }

View file

@ -8,6 +8,8 @@
package io.element.android.libraries.matrix.api.roomlist package io.element.android.libraries.matrix.api.roomlist
import io.element.android.libraries.matrix.api.core.RoomId
sealed interface RoomListFilter { sealed interface RoomListFilter {
companion object { companion object {
/** /**
@ -41,6 +43,10 @@ sealed interface RoomListFilter {
val filters: List<RoomListFilter> val filters: List<RoomListFilter>
) : RoomListFilter ) : RoomListFilter
data class Identifiers(
val values: List<RoomId>,
) : RoomListFilter
/** /**
* A filter that matches rooms that are unread. * A filter that matches rooms that are unread.
*/ */

View file

@ -19,6 +19,7 @@ data class RoomSummary(
is LatestEventValue.None -> null is LatestEventValue.None -> null
is LatestEventValue.Local -> latestEvent.timestamp is LatestEventValue.Local -> latestEvent.timestamp
is LatestEventValue.Remote -> latestEvent.timestamp is LatestEventValue.Remote -> latestEvent.timestamp
is LatestEventValue.RoomInvite -> latestEvent.timestamp
} }
val isOneToOne get() = info.activeMembersCount == 2L val isOneToOne get() = info.activeMembersCount == 2L
} }

View file

@ -12,9 +12,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
interface SpaceService { interface SpaceService {
val spaceRoomsFlow: SharedFlow<List<SpaceRoom>> val topLevelSpacesFlow: SharedFlow<List<SpaceRoom>>
suspend fun joinedSpaces(): Result<List<SpaceRoom>> val spaceFiltersFlow: SharedFlow<List<SpaceServiceFilter>>
suspend fun joinedParents(spaceId: RoomId): Result<List<SpaceRoom>> suspend fun joinedParents(spaceId: RoomId): Result<List<SpaceRoom>>
suspend fun getSpaceRoom(spaceId: RoomId): SpaceRoom? suspend fun getSpaceRoom(spaceId: RoomId): SpaceRoom?

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.spaces
import io.element.android.libraries.matrix.api.core.RoomId
/**
* Represents a space filter for filtering rooms by space membership.
*
* @property spaceRoom The space room associated with this filter.
* @property level The nesting level of the space (0 = top level, 1 = first level child, etc.).
* @property descendants The list of room IDs that are descendants of this space.
*/
data class SpaceServiceFilter(
val spaceRoom: SpaceRoom,
val level: Int,
val descendants: List<RoomId>,
)

View file

@ -20,6 +20,7 @@ import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.ClientSessionDelegate import org.matrix.rustcomponents.sdk.ClientSessionDelegate
import org.matrix.rustcomponents.sdk.Session import org.matrix.rustcomponents.sdk.Session
import timber.log.Timber import timber.log.Timber
import uniffi.matrix_sdk_common.BackgroundTaskFailureReason
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -120,6 +121,11 @@ class RustClientSessionDelegate(
} }
} }
override fun onBackgroundTaskErrorReport(taskName: String, error: BackgroundTaskFailureReason) {
// TODO actually implement the missing logic to report to sentry and crash the app
Timber.tag(loggerTag.value).e("onBackgroundTaskErrorReport(taskName=$taskName, error=$error)")
}
override fun retrieveSessionFromKeychain(userId: String): Session { override fun retrieveSessionFromKeychain(userId: String): Session {
// This should never be called, as it's only used for multi-process setups // This should never be called, as it's only used for multi-process setups
error("retrieveSessionFromKeychain should never be called for Android") error("retrieveSessionFromKeychain should never be called for Android")

View file

@ -66,7 +66,7 @@ import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RoomInfoMapper import io.element.android.libraries.matrix.impl.room.RoomInfoMapper
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
import io.element.android.libraries.matrix.impl.room.RustRoomFactory import io.element.android.libraries.matrix.impl.room.RustRoomFactory
import io.element.android.libraries.matrix.impl.room.TimelineEventTypeFilterFactory import io.element.android.libraries.matrix.impl.room.TimelineEventFilterFactory
import io.element.android.libraries.matrix.impl.room.history.map import io.element.android.libraries.matrix.impl.room.history.map
import io.element.android.libraries.matrix.impl.room.join.map import io.element.android.libraries.matrix.impl.room.join.map
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper
@ -141,7 +141,7 @@ class RustMatrixClient(
dispatchers: CoroutineDispatchers, dispatchers: CoroutineDispatchers,
baseCacheDirectory: File, baseCacheDirectory: File,
clock: SystemClock, clock: SystemClock,
timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory, timelineEventFilterFactory: TimelineEventFilterFactory,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val workManagerScheduler: WorkManagerScheduler, private val workManagerScheduler: WorkManagerScheduler,
@ -225,7 +225,7 @@ class RustMatrixClient(
systemClock = clock, systemClock = clock,
roomContentForwarder = RoomContentForwarder(innerRoomListService), roomContentForwarder = RoomContentForwarder(innerRoomListService),
roomSyncSubscriber = roomSyncSubscriber, roomSyncSubscriber = roomSyncSubscriber,
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory, timelineEventFilterFactory = timelineEventFilterFactory,
roomMembershipObserver = roomMembershipObserver, roomMembershipObserver = roomMembershipObserver,
roomInfoMapper = roomInfoMapper, roomInfoMapper = roomInfoMapper,
featureFlagService = featureFlagService, featureFlagService = featureFlagService,

View file

@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.impl.certificates.UserCertificatesPro
import io.element.android.libraries.matrix.impl.paths.SessionPaths import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.impl.paths.getSessionPaths import io.element.android.libraries.matrix.impl.paths.getSessionPaths
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
import io.element.android.libraries.matrix.impl.room.TimelineEventTypeFilterFactory import io.element.android.libraries.matrix.impl.room.TimelineEventFilterFactory
import io.element.android.libraries.matrix.impl.storage.SqliteStoreBuilderProvider import io.element.android.libraries.matrix.impl.storage.SqliteStoreBuilderProvider
import io.element.android.libraries.matrix.impl.util.anonymizedTokens import io.element.android.libraries.matrix.impl.util.anonymizedTokens
import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.network.useragent.UserAgentProvider
@ -61,7 +61,7 @@ class RustMatrixClientFactory(
private val clock: SystemClock, private val clock: SystemClock,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory, private val timelineEventFilterFactory: TimelineEventFilterFactory,
private val clientBuilderProvider: ClientBuilderProvider, private val clientBuilderProvider: ClientBuilderProvider,
private val sqliteStoreBuilderProvider: SqliteStoreBuilderProvider, private val sqliteStoreBuilderProvider: SqliteStoreBuilderProvider,
private val workManagerScheduler: WorkManagerScheduler, private val workManagerScheduler: WorkManagerScheduler,
@ -115,7 +115,7 @@ class RustMatrixClientFactory(
dispatchers = coroutineDispatchers, dispatchers = coroutineDispatchers,
baseCacheDirectory = cacheDirectory, baseCacheDirectory = cacheDirectory,
clock = clock, clock = clock,
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory, timelineEventFilterFactory = timelineEventFilterFactory,
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
analyticsService = analyticsService, analyticsService = analyticsService,
workManagerScheduler = workManagerScheduler, workManagerScheduler = workManagerScheduler,

View file

@ -38,6 +38,7 @@ object QrErrorMapper {
is RustHumanQrLoginException.ConnectionInsecure -> QrLoginException.ConnectionInsecure is RustHumanQrLoginException.ConnectionInsecure -> QrLoginException.ConnectionInsecure
is RustHumanQrLoginException.Declined -> QrLoginException.Declined is RustHumanQrLoginException.Declined -> QrLoginException.Declined
is RustHumanQrLoginException.Expired -> QrLoginException.Expired is RustHumanQrLoginException.Expired -> QrLoginException.Expired
is RustHumanQrLoginException.NotFound -> QrLoginException.NotFound
is RustHumanQrLoginException.OtherDeviceNotSignedIn -> QrLoginException.OtherDeviceNotSignedIn is RustHumanQrLoginException.OtherDeviceNotSignedIn -> QrLoginException.OtherDeviceNotSignedIn
is RustHumanQrLoginException.LinkingNotSupported -> QrLoginException.LinkingNotSupported is RustHumanQrLoginException.LinkingNotSupported -> QrLoginException.LinkingNotSupported
is RustHumanQrLoginException.Unknown -> QrLoginException.Unknown is RustHumanQrLoginException.Unknown -> QrLoginException.Unknown
@ -45,6 +46,5 @@ object QrErrorMapper {
is RustHumanQrLoginException.SlidingSyncNotAvailable -> QrLoginException.SlidingSyncNotAvailable is RustHumanQrLoginException.SlidingSyncNotAvailable -> QrLoginException.SlidingSyncNotAvailable
is RustHumanQrLoginException.CheckCodeAlreadySent -> QrLoginException.CheckCodeAlreadySent is RustHumanQrLoginException.CheckCodeAlreadySent -> QrLoginException.CheckCodeAlreadySent
is RustHumanQrLoginException.CheckCodeCannotBeSent -> QrLoginException.CheckCodeCannotBeSent is RustHumanQrLoginException.CheckCodeCannotBeSent -> QrLoginException.CheckCodeCannotBeSent
is RustHumanQrLoginException.NotFound -> QrLoginException.NotFound
} }
} }

View file

@ -80,6 +80,7 @@ import org.matrix.rustcomponents.sdk.getElementCallRequiredPermissions
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import timber.log.Timber import timber.log.Timber
import uniffi.matrix_sdk.RoomPowerLevelChanges import uniffi.matrix_sdk.RoomPowerLevelChanges
import uniffi.matrix_sdk_ui.TimelineEventFocusThreadMode
import uniffi.matrix_sdk_ui.TimelineReadReceiptTracking import uniffi.matrix_sdk_ui.TimelineReadReceiptTracking
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
import org.matrix.rustcomponents.sdk.IdentityStatusChange as RustIdentityStateChange import org.matrix.rustcomponents.sdk.IdentityStatusChange as RustIdentityStateChange
@ -177,21 +178,18 @@ class JoinedRustRoom(
): Result<Timeline> = withContext(roomDispatcher) { ): Result<Timeline> = withContext(roomDispatcher) {
val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.Threads) val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.Threads)
val focus = when (createTimelineParams) { val focus = when (createTimelineParams) {
is CreateTimelineParams.PinnedOnly -> TimelineFocus.PinnedEvents( is CreateTimelineParams.PinnedOnly -> TimelineFocus.PinnedEvents
maxEventsToLoad = 100u,
maxConcurrentRequests = 10u,
)
is CreateTimelineParams.MediaOnly -> TimelineFocus.Live(hideThreadedEvents = hideThreadedEvents) is CreateTimelineParams.MediaOnly -> TimelineFocus.Live(hideThreadedEvents = hideThreadedEvents)
is CreateTimelineParams.Focused -> TimelineFocus.Event( is CreateTimelineParams.Focused -> TimelineFocus.Event(
eventId = createTimelineParams.focusedEventId.value, eventId = createTimelineParams.focusedEventId.value,
numContextEvents = 50u, numContextEvents = 50u,
hideThreadedEvents = hideThreadedEvents, threadMode = TimelineEventFocusThreadMode.Automatic(hideThreadedEvents),
) )
is CreateTimelineParams.MediaOnlyFocused -> TimelineFocus.Event( is CreateTimelineParams.MediaOnlyFocused -> TimelineFocus.Event(
eventId = createTimelineParams.focusedEventId.value, eventId = createTimelineParams.focusedEventId.value,
numContextEvents = 50u, numContextEvents = 50u,
// Never hide threaded events in media focused timeline // Never hide threaded events in media focused timeline
hideThreadedEvents = false, threadMode = TimelineEventFocusThreadMode.Automatic(false),
) )
is CreateTimelineParams.Threaded -> TimelineFocus.Thread( is CreateTimelineParams.Threaded -> TimelineFocus.Thread(
rootEventId = createTimelineParams.threadRootEventId.value, rootEventId = createTimelineParams.threadRootEventId.value,

View file

@ -57,7 +57,7 @@ class RustRoomFactory(
private val roomListService: RoomListService, private val roomListService: RoomListService,
private val innerRoomListService: InnerRoomListService, private val innerRoomListService: InnerRoomListService,
private val roomSyncSubscriber: RoomSyncSubscriber, private val roomSyncSubscriber: RoomSyncSubscriber,
private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory, private val timelineEventFilterFactory: TimelineEventFilterFactory,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val roomMembershipObserver: RoomMembershipObserver, private val roomMembershipObserver: RoomMembershipObserver,
private val roomInfoMapper: RoomInfoMapper, private val roomInfoMapper: RoomInfoMapper,
@ -70,7 +70,7 @@ class RustRoomFactory(
private val eventFilters = TimelineConfig.excludedEvents private val eventFilters = TimelineConfig.excludedEvents
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }
?.let { listStateEventType -> ?.let { listStateEventType ->
timelineEventTypeFilterFactory.create(listStateEventType) timelineEventFilterFactory.create(listStateEventType)
} }
suspend fun destroy() { suspend fun destroy() {
@ -133,7 +133,7 @@ class RustRoomFactory(
sdkRoom.timelineWithConfiguration( sdkRoom.timelineWithConfiguration(
TimelineConfiguration( TimelineConfiguration(
focus = TimelineFocus.Live(hideThreadedEvents = hideThreadedEvents), focus = TimelineFocus.Live(hideThreadedEvents = hideThreadedEvents),
filter = eventFilters?.let(TimelineFilter::EventTypeFilter) ?: TimelineFilter.All, filter = eventFilters?.let(TimelineFilter::EventFilter) ?: TimelineFilter.All,
internalIdPrefix = "live", internalIdPrefix = "live",
dateDividerMode = DateDividerMode.DAILY, dateDividerMode = DateDividerMode.DAILY,
trackReadReceipts = TimelineReadReceiptTracking.ALL_EVENTS, trackReadReceipts = TimelineReadReceiptTracking.ALL_EVENTS,

View file

@ -12,16 +12,16 @@ import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.StateEventType
import org.matrix.rustcomponents.sdk.FilterTimelineEventType import org.matrix.rustcomponents.sdk.FilterTimelineEventType
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter import org.matrix.rustcomponents.sdk.TimelineEventFilter
interface TimelineEventTypeFilterFactory { interface TimelineEventFilterFactory {
fun create(listStateEventType: List<StateEventType>): TimelineEventTypeFilter fun create(listStateEventType: List<StateEventType>): TimelineEventFilter
} }
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class RustTimelineEventTypeFilterFactory : TimelineEventTypeFilterFactory { class RustTimelineEventFilterFactory : TimelineEventFilterFactory {
override fun create(listStateEventType: List<StateEventType>): TimelineEventTypeFilter { override fun create(listStateEventType: List<StateEventType>): TimelineEventFilter {
return TimelineEventTypeFilter.exclude( return TimelineEventFilter.excludeEventTypes(
listStateEventType.map { stateEventType -> listStateEventType.map { stateEventType ->
FilterTimelineEventType.State(stateEventType.map()) FilterTimelineEventType.State(stateEventType.map())
} }

View file

@ -14,6 +14,7 @@ import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Any
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Category import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Category
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.DeduplicateVersions import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.DeduplicateVersions
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Favourite import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Favourite
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Identifiers
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Invite import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Invite
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NonLeft import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NonLeft
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NonSpace import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NonSpace
@ -60,6 +61,7 @@ internal object RoomListFilterMapper {
return when (filter) { return when (filter) {
is RoomListFilter.All -> All(filters = filter.filters.map { mapFilter(it) }) is RoomListFilter.All -> All(filters = filter.filters.map { mapFilter(it) })
is RoomListFilter.Any -> Any(filters = filter.filters.map { mapFilter(it) }) is RoomListFilter.Any -> Any(filters = filter.filters.map { mapFilter(it) })
is RoomListFilter.Identifiers -> Identifiers(identifiers = filter.values.map { it.value })
RoomListFilter.None -> None RoomListFilter.None -> None
RoomListFilter.Category.Group -> Category(RoomListFilterCategory.GROUP) RoomListFilter.Category.Group -> Category(RoomListFilterCategory.GROUP)
RoomListFilter.Category.People -> Category(RoomListFilterCategory.PEOPLE) RoomListFilter.Category.People -> Category(RoomListFilterCategory.PEOPLE)

View file

@ -53,6 +53,11 @@ class RoomSummaryFactory(
senderProfile = event.profile.map(), senderProfile = event.profile.map(),
isOwn = event.isOwn, isOwn = event.isOwn,
) )
is RustLatestEventValue.RemoteInvite -> LatestEventValue.RoomInvite(
timestamp = event.timestamp.toLong(),
inviterId = event.inviter?.let(::UserId),
invitedProfile = event.inviterProfile.map(),
)
} }
} }
return RoomSummary( return RoomSummary(

View file

@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import io.element.android.libraries.matrix.api.spaces.SpaceService import io.element.android.libraries.matrix.api.spaces.SpaceService
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
@ -31,9 +32,11 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.SpaceFilterUpdate
import org.matrix.rustcomponents.sdk.SpaceListUpdate import org.matrix.rustcomponents.sdk.SpaceListUpdate
import org.matrix.rustcomponents.sdk.SpaceServiceInterface import org.matrix.rustcomponents.sdk.SpaceServiceInterface
import org.matrix.rustcomponents.sdk.SpaceServiceJoinedSpacesListener import org.matrix.rustcomponents.sdk.SpaceServiceJoinedSpacesListener
import org.matrix.rustcomponents.sdk.SpaceServiceSpaceFiltersListener
import timber.log.Timber import timber.log.Timber
import org.matrix.rustcomponents.sdk.SpaceService as ClientSpaceService import org.matrix.rustcomponents.sdk.SpaceService as ClientSpaceService
@ -45,20 +48,20 @@ class RustSpaceService(
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
) : SpaceService { ) : SpaceService {
private val spaceRoomMapper = SpaceRoomMapper() private val spaceRoomMapper = SpaceRoomMapper()
override val spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = 1) private val spaceFilterMapper = SpaceServiceFilterMapper(spaceRoomMapper)
override val topLevelSpacesFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = 1)
private val spaceListUpdateProcessor = SpaceListUpdateProcessor( private val spaceListUpdateProcessor = SpaceListUpdateProcessor(
spaceRoomsFlow = spaceRoomsFlow, spaceRoomsFlow = topLevelSpacesFlow,
mapper = spaceRoomMapper, mapper = spaceRoomMapper,
analyticsService = analyticsService, analyticsService = analyticsService,
) )
override suspend fun joinedSpaces(): Result<List<SpaceRoom>> = withContext(sessionDispatcher) { override val spaceFiltersFlow = MutableSharedFlow<List<SpaceServiceFilter>>(replay = 1, extraBufferCapacity = 1)
runCatchingExceptions { private val spaceFilterUpdateProcessor = SpaceServiceFilterUpdateProcessor(
innerSpaceService spaceFiltersFlow = spaceFiltersFlow,
.topLevelJoinedSpaces() mapper = spaceFilterMapper,
.map(spaceRoomMapper::map) )
}
}
override suspend fun joinedParents(spaceId: RoomId): Result<List<SpaceRoom>> = withContext(sessionDispatcher) { override suspend fun joinedParents(spaceId: RoomId): Result<List<SpaceRoom>> = withContext(sessionDispatcher) {
runCatchingExceptions { runCatchingExceptions {
@ -123,6 +126,13 @@ class RustSpaceService(
spaceListUpdateProcessor.postUpdates(updates) spaceListUpdateProcessor.postUpdates(updates)
} }
.launchIn(sessionCoroutineScope) .launchIn(sessionCoroutineScope)
innerSpaceService
.spaceFilterListUpdate()
.onEach { updates ->
spaceFilterUpdateProcessor.postUpdates(updates)
}
.launchIn(sessionCoroutineScope)
} }
} }
@ -142,3 +152,20 @@ internal fun SpaceServiceInterface.spaceListUpdate(): Flow<List<SpaceListUpdate>
}.catch { }.catch {
Timber.d(it, "spaceDiffFlow() failed") Timber.d(it, "spaceDiffFlow() failed")
}.buffer(Channel.UNLIMITED) }.buffer(Channel.UNLIMITED)
internal fun SpaceServiceInterface.spaceFilterListUpdate(): Flow<List<SpaceFilterUpdate>> =
callbackFlow {
val listener = object : SpaceServiceSpaceFiltersListener {
override fun onUpdate(filterUpdates: List<SpaceFilterUpdate>) {
trySendBlocking(filterUpdates)
}
}
Timber.d("Open spaceFilterDiffFlow for SpaceServiceInterface ${this@spaceFilterListUpdate}")
val taskHandle = subscribeToSpaceFilters(listener)
awaitClose {
Timber.d("Close spaceFilterDiffFlow for SpaceServiceInterface ${this@spaceFilterListUpdate}")
taskHandle.cancelAndDestroy()
}
}.catch {
Timber.d(it, "spaceFilterListUpdate() failed")
}.buffer(Channel.UNLIMITED)

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.spaces
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
import org.matrix.rustcomponents.sdk.SpaceFilter as RustSpaceFilter
class SpaceServiceFilterMapper(
private val spaceRoomMapper: SpaceRoomMapper,
) {
fun map(spaceFilter: RustSpaceFilter): SpaceServiceFilter {
return SpaceServiceFilter(
spaceRoom = spaceRoomMapper.map(spaceFilter.spaceRoom),
level = spaceFilter.level.toInt(),
descendants = spaceFilter.descendants.map { RoomId(it) },
)
}
}

View file

@ -0,0 +1,85 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.spaces
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.matrix.rustcomponents.sdk.SpaceFilterUpdate
import timber.log.Timber
internal class SpaceServiceFilterUpdateProcessor(
private val spaceFiltersFlow: MutableSharedFlow<List<SpaceServiceFilter>>,
private val mapper: SpaceServiceFilterMapper,
) {
private val mutex = Mutex()
suspend fun postUpdates(updates: List<SpaceFilterUpdate>) {
Timber.v("Update space filters from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}")
updateSpaceFilters {
updates.forEach { update -> applyUpdate(update) }
}
}
private suspend fun updateSpaceFilters(block: MutableList<SpaceServiceFilter>.() -> Unit) =
mutex.withLock {
val spaceFilters = if (spaceFiltersFlow.replayCache.isNotEmpty()) {
spaceFiltersFlow.first().toMutableList()
} else {
mutableListOf()
}
block(spaceFilters)
spaceFiltersFlow.emit(spaceFilters)
}
private fun MutableList<SpaceServiceFilter>.applyUpdate(update: SpaceFilterUpdate) {
when (update) {
is SpaceFilterUpdate.Append -> {
val newFilters = update.values.map(mapper::map)
addAll(newFilters)
}
SpaceFilterUpdate.Clear -> clear()
is SpaceFilterUpdate.Insert -> {
val newFilter = mapper.map(update.value)
add(update.index.toInt(), newFilter)
}
SpaceFilterUpdate.PopBack -> {
removeAt(lastIndex)
}
SpaceFilterUpdate.PopFront -> {
removeAt(0)
}
is SpaceFilterUpdate.PushBack -> {
val newFilter = mapper.map(update.value)
add(newFilter)
}
is SpaceFilterUpdate.PushFront -> {
val newFilter = mapper.map(update.value)
add(0, newFilter)
}
is SpaceFilterUpdate.Remove -> {
removeAt(update.index.toInt())
}
is SpaceFilterUpdate.Reset -> {
clear()
val newFilters = update.values.map(mapper::map)
addAll(newFilters)
}
is SpaceFilterUpdate.Set -> {
val newFilter = mapper.map(update.value)
this[update.index.toInt()] = newFilter
}
is SpaceFilterUpdate.Truncate -> {
subList(update.length.toInt(), size).clear()
}
}
}
}

View file

@ -13,7 +13,7 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider
import io.element.android.libraries.matrix.impl.auth.FakeUserCertificatesProvider import io.element.android.libraries.matrix.impl.auth.FakeUserCertificatesProvider
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventTypeFilterFactory import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory
import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider
import io.element.android.libraries.network.useragent.SimpleUserAgentProvider import io.element.android.libraries.network.useragent.SimpleUserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
@ -63,7 +63,7 @@ fun TestScope.createRustMatrixClientFactory(
clock = FakeSystemClock(), clock = FakeSystemClock(),
analyticsService = FakeAnalyticsService(), analyticsService = FakeAnalyticsService(),
featureFlagService = FakeFeatureFlagService(), featureFlagService = FakeFeatureFlagService(),
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(), timelineEventFilterFactory = FakeTimelineEventFilterFactory(),
clientBuilderProvider = clientBuilderProvider, clientBuilderProvider = clientBuilderProvider,
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(), sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
workManagerScheduler = workManagerScheduler, workManagerScheduler = workManagerScheduler,

View file

@ -15,7 +15,7 @@ import io.element.android.libraries.core.data.bytes
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSyncService import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSyncService
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventTypeFilterFactory import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory
import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_DEVICE_ID import io.element.android.libraries.matrix.test.A_DEVICE_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
@ -150,7 +150,7 @@ class RustMatrixClientTest {
dispatchers = testCoroutineDispatchers(), dispatchers = testCoroutineDispatchers(),
baseCacheDirectory = File(""), baseCacheDirectory = File(""),
clock = FakeSystemClock(), clock = FakeSystemClock(),
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(), timelineEventFilterFactory = FakeTimelineEventFilterFactory(),
featureFlagService = FakeFeatureFlagService(), featureFlagService = FakeFeatureFlagService(),
analyticsService = FakeAnalyticsService(), analyticsService = FakeAnalyticsService(),
workManagerScheduler = FakeWorkManagerScheduler(submitLambda = {}), workManagerScheduler = FakeWorkManagerScheduler(submitLambda = {}),

View file

@ -30,6 +30,7 @@ internal fun aRustNotificationItem(
hasMention: Boolean? = false, hasMention: Boolean? = false,
threadId: ThreadId? = null, threadId: ThreadId? = null,
actions: List<Action>? = null, actions: List<Action>? = null,
rawEvent: String = "",
) = NotificationItem( ) = NotificationItem(
event = event, event = event,
senderInfo = senderInfo, senderInfo = senderInfo,
@ -38,6 +39,7 @@ internal fun aRustNotificationItem(
hasMention = hasMention, hasMention = hasMention,
threadId = threadId?.value, threadId = threadId?.value,
actions = actions, actions = actions,
rawEvent = rawEvent,
) )
internal fun aRustBatchNotificationResultOk( internal fun aRustBatchNotificationResultOk(

View file

@ -9,6 +9,6 @@
package io.element.android.libraries.matrix.impl.fixtures.fakes package io.element.android.libraries.matrix.impl.fixtures.fakes
import org.matrix.rustcomponents.sdk.NoHandle import org.matrix.rustcomponents.sdk.NoHandle
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter import org.matrix.rustcomponents.sdk.TimelineEventFilter
class FakeFfiTimelineEventTypeFilter : TimelineEventTypeFilter(NoHandle) class FakeFfiTimelineEventFilter : TimelineEventFilter(NoHandle)

View file

@ -9,11 +9,11 @@
package io.element.android.libraries.matrix.impl.room package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineEventTypeFilter import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineEventFilter
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter import org.matrix.rustcomponents.sdk.TimelineEventFilter
class FakeTimelineEventTypeFilterFactory : TimelineEventTypeFilterFactory { class FakeTimelineEventFilterFactory : TimelineEventFilterFactory {
override fun create(listStateEventType: List<StateEventType>): TimelineEventTypeFilter { override fun create(listStateEventType: List<StateEventType>): TimelineEventFilter {
return FakeFfiTimelineEventTypeFilter() return FakeFfiTimelineEventFilter()
} }
} }

View file

@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import io.element.android.libraries.matrix.api.spaces.SpaceService import io.element.android.libraries.matrix.api.spaces.SpaceService
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -20,7 +21,6 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
class FakeSpaceService( class FakeSpaceService(
private val joinedSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() }, private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() },
private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() }, private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() },
private val removeChildFromSpaceResult: (RoomId, RoomId) -> Result<Unit> = { _, _ -> lambdaError() }, private val removeChildFromSpaceResult: (RoomId, RoomId) -> Result<Unit> = { _, _ -> lambdaError() },
@ -29,16 +29,20 @@ class FakeSpaceService(
private val editableSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() }, private val editableSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
private val addChildToSpaceResult: (RoomId, RoomId) -> Result<Unit> = { _, _ -> lambdaError() }, private val addChildToSpaceResult: (RoomId, RoomId) -> Result<Unit> = { _, _ -> lambdaError() },
) : SpaceService { ) : SpaceService {
private val _spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>() private val _topLevelSpacesFlow = MutableSharedFlow<List<SpaceRoom>>()
override val spaceRoomsFlow: SharedFlow<List<SpaceRoom>> override val topLevelSpacesFlow: SharedFlow<List<SpaceRoom>>
get() = _spaceRoomsFlow.asSharedFlow() get() = _topLevelSpacesFlow.asSharedFlow()
suspend fun emitSpaceRoomList(value: List<SpaceRoom>) { suspend fun emitTopLevelSpaces(value: List<SpaceRoom>) {
_spaceRoomsFlow.emit(value) _topLevelSpacesFlow.emit(value)
} }
override suspend fun joinedSpaces(): Result<List<SpaceRoom>> = simulateLongTask { private val _spaceFiltersFlow = MutableSharedFlow<List<SpaceServiceFilter>>()
return joinedSpacesResult() override val spaceFiltersFlow: SharedFlow<List<SpaceServiceFilter>>
get() = _spaceFiltersFlow.asSharedFlow()
suspend fun emitSpaceFilters(value: List<SpaceServiceFilter>) {
_spaceFiltersFlow.emit(value)
} }
override suspend fun joinedParents(spaceId: RoomId): Result<List<SpaceRoom>> { override suspend fun joinedParents(spaceId: RoomId): Result<List<SpaceRoom>> {

View file

@ -13,6 +13,7 @@
<item quantity="one">"%d varsel"</item> <item quantity="one">"%d varsel"</item>
<item quantity="other">"%d varsler"</item> <item quantity="other">"%d varsler"</item>
</plurals> </plurals>
<string name="notification_error_unified_push_unregistered_android">"UnifiedPush-varslingsdistributøren kunne ikke registreres, så du vil ikke motta varsler lenger. Sjekk varslingsinnstillingene til appen og statusen til push-distributøren."</string>
<string name="notification_fallback_content">"Du har nye meldinger."</string> <string name="notification_fallback_content">"Du har nye meldinger."</string>
<string name="notification_incoming_call">"📹 Innkommende anrop"</string> <string name="notification_incoming_call">"📹 Innkommende anrop"</string>
<string name="notification_inline_reply_failed">"** Kunne ikke sende - vennligst åpne rommet"</string> <string name="notification_inline_reply_failed">"** Kunne ikke sende - vennligst åpne rommet"</string>
@ -37,6 +38,8 @@
<string name="notification_room_invite_body_with_sender">"%1$s inviterte deg til å bli med i rommet"</string> <string name="notification_room_invite_body_with_sender">"%1$s inviterte deg til å bli med i rommet"</string>
<string name="notification_sender_me">"Meg"</string> <string name="notification_sender_me">"Meg"</string>
<string name="notification_sender_mention_reply">"%1$s nevnt eller besvart"</string> <string name="notification_sender_mention_reply">"%1$s nevnt eller besvart"</string>
<string name="notification_space_invite_body">"Inviterte deg til å bli med i området"</string>
<string name="notification_space_invite_body_with_sender">"%1$s inviterte deg til å bli med i området"</string>
<string name="notification_test_push_notification_content">"Du ser på varselet! Klikk på meg!"</string> <string name="notification_test_push_notification_content">"Du ser på varselet! Klikk på meg!"</string>
<string name="notification_thread_in_room">"Tråd i %1$s"</string> <string name="notification_thread_in_room">"Tråd i %1$s"</string>
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string> <string name="notification_ticker_text_dm">"%1$s: %2$s"</string>

View file

@ -58,6 +58,7 @@
<string name="a11y_your_avatar">"Váš avatar"</string> <string name="a11y_your_avatar">"Váš avatar"</string>
<string name="action_accept">"Přijmout"</string> <string name="action_accept">"Přijmout"</string>
<string name="action_add_caption">"Přidat titulek"</string> <string name="action_add_caption">"Přidat titulek"</string>
<string name="action_add_existing_rooms">"Přidat stávající místnosti"</string>
<string name="action_add_to_timeline">"Přidat na časovou osu"</string> <string name="action_add_to_timeline">"Přidat na časovou osu"</string>
<string name="action_back">"Zpět"</string> <string name="action_back">"Zpět"</string>
<string name="action_call">"Hovor"</string> <string name="action_call">"Hovor"</string>
@ -77,6 +78,7 @@
<string name="action_copy_text">"Kopírovat text"</string> <string name="action_copy_text">"Kopírovat text"</string>
<string name="action_create">"Vytvořit"</string> <string name="action_create">"Vytvořit"</string>
<string name="action_create_room">"Vytvořit místnost"</string> <string name="action_create_room">"Vytvořit místnost"</string>
<string name="action_create_space">"Vytvořte prostor"</string>
<string name="action_deactivate">"Deaktivovat"</string> <string name="action_deactivate">"Deaktivovat"</string>
<string name="action_deactivate_account">"Deaktivovat účet"</string> <string name="action_deactivate_account">"Deaktivovat účet"</string>
<string name="action_decline">"Odmítnout"</string> <string name="action_decline">"Odmítnout"</string>
@ -93,6 +95,7 @@
<string name="action_enable">"Povolit"</string> <string name="action_enable">"Povolit"</string>
<string name="action_end_poll">"Ukončit hlasování"</string> <string name="action_end_poll">"Ukončit hlasování"</string>
<string name="action_enter_pin">"Zadejte PIN"</string> <string name="action_enter_pin">"Zadejte PIN"</string>
<string name="action_explore_public_spaces">"Prozkoumejte veřejné prostory"</string>
<string name="action_finish">"Dokončit"</string> <string name="action_finish">"Dokončit"</string>
<string name="action_forgot_password">"Zapomněli jste heslo?"</string> <string name="action_forgot_password">"Zapomněli jste heslo?"</string>
<string name="action_forward">"Přeposlat"</string> <string name="action_forward">"Přeposlat"</string>
@ -194,6 +197,7 @@
<string name="common_copied_to_clipboard">"Zkopírováno do schránky"</string> <string name="common_copied_to_clipboard">"Zkopírováno do schránky"</string>
<string name="common_copyright">"Autorská práva"</string> <string name="common_copyright">"Autorská práva"</string>
<string name="common_creating_room">"Vytváření místnosti…"</string> <string name="common_creating_room">"Vytváření místnosti…"</string>
<string name="common_creating_space">"Vytváření prostoru…"</string>
<string name="common_current_user_canceled_knock">"Žádost zrušena"</string> <string name="common_current_user_canceled_knock">"Žádost zrušena"</string>
<string name="common_current_user_left_room">"Místnost opuštěna"</string> <string name="common_current_user_left_room">"Místnost opuštěna"</string>
<string name="common_current_user_left_space">"Opustit prostor"</string> <string name="common_current_user_left_space">"Opustit prostor"</string>
@ -295,6 +299,7 @@ Důvod: %1$s."</string>
<string name="common_reason">"Důvod"</string> <string name="common_reason">"Důvod"</string>
<string name="common_recovery_key">"Klíč pro obnovení"</string> <string name="common_recovery_key">"Klíč pro obnovení"</string>
<string name="common_refreshing">"Obnovování…"</string> <string name="common_refreshing">"Obnovování…"</string>
<string name="common_removing">"Odstraňování…"</string>
<plurals name="common_replies"> <plurals name="common_replies">
<item quantity="other">"%1$d odpovědí"</item> <item quantity="other">"%1$d odpovědí"</item>
</plurals> </plurals>
@ -303,6 +308,7 @@ Důvod: %1$s."</string>
<string name="common_report_a_problem">"Nahlásit problém"</string> <string name="common_report_a_problem">"Nahlásit problém"</string>
<string name="common_report_submitted">"Zpráva odeslána"</string> <string name="common_report_submitted">"Zpráva odeslána"</string>
<string name="common_rich_text_editor">"Editor formátovaného textu"</string> <string name="common_rich_text_editor">"Editor formátovaného textu"</string>
<string name="common_role">"Role"</string>
<string name="common_room">"Místnost"</string> <string name="common_room">"Místnost"</string>
<string name="common_room_name">"Název místnosti"</string> <string name="common_room_name">"Název místnosti"</string>
<string name="common_room_name_placeholder">"např. název vašeho projektu"</string> <string name="common_room_name_placeholder">"např. název vašeho projektu"</string>
@ -319,6 +325,11 @@ Důvod: %1$s."</string>
<string name="common_security">"Zabezpečení"</string> <string name="common_security">"Zabezpečení"</string>
<string name="common_seen_by">"Viděno"</string> <string name="common_seen_by">"Viděno"</string>
<string name="common_select_account">"Vybrat účet"</string> <string name="common_select_account">"Vybrat účet"</string>
<plurals name="common_selected_count">
<item quantity="one">"%1$d vybraný"</item>
<item quantity="few">"%1$d vybrané"</item>
<item quantity="other">"%1$d vybraných"</item>
</plurals>
<string name="common_send_to">"Odeslat do"</string> <string name="common_send_to">"Odeslat do"</string>
<string name="common_sending">"Odesílání…"</string> <string name="common_sending">"Odesílání…"</string>
<string name="common_sending_failed">"Odeslání se nezdařilo"</string> <string name="common_sending_failed">"Odeslání se nezdařilo"</string>
@ -329,6 +340,7 @@ Důvod: %1$s."</string>
<string name="common_server_url">"URL serveru"</string> <string name="common_server_url">"URL serveru"</string>
<string name="common_settings">"Nastavení"</string> <string name="common_settings">"Nastavení"</string>
<string name="common_share_space">"Sdílet prostor"</string> <string name="common_share_space">"Sdílet prostor"</string>
<string name="common_shared_history">"Noví členové vidí historii"</string>
<string name="common_shared_location">"Sdílená poloha"</string> <string name="common_shared_location">"Sdílená poloha"</string>
<string name="common_shared_space">"Sdílený prostor"</string> <string name="common_shared_space">"Sdílený prostor"</string>
<string name="common_signing_out">"Odhlašování"</string> <string name="common_signing_out">"Odhlašování"</string>
@ -344,6 +356,7 @@ Důvod: %1$s."</string>
<string name="common_starting_chat">"Zahajování chatu…"</string> <string name="common_starting_chat">"Zahajování chatu…"</string>
<string name="common_sticker">"Nálepka"</string> <string name="common_sticker">"Nálepka"</string>
<string name="common_success">"Úspěch"</string> <string name="common_success">"Úspěch"</string>
<string name="common_suggested">"Doporučeno"</string>
<string name="common_suggestions">"Návrhy"</string> <string name="common_suggestions">"Návrhy"</string>
<string name="common_syncing">"Synchronizace"</string> <string name="common_syncing">"Synchronizace"</string>
<string name="common_system">"Systém"</string> <string name="common_system">"Systém"</string>
@ -380,7 +393,10 @@ Důvod: %1$s."</string>
<string name="common_voice_message">"Hlasová zpráva"</string> <string name="common_voice_message">"Hlasová zpráva"</string>
<string name="common_waiting">"Čekání…"</string> <string name="common_waiting">"Čekání…"</string>
<string name="common_waiting_for_decryption_key">"Čekání na dešifrovací klíč"</string> <string name="common_waiting_for_decryption_key">"Čekání na dešifrovací klíč"</string>
<string name="common_world_readable_history">"Kdokoli může vidět historii"</string>
<string name="common_you">"Vy"</string> <string name="common_you">"Vy"</string>
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"%1$s (%2$s) sdílel(a) tuto zprávu v době, kdy jste nebyli v místnosti."</string>
<string name="crypto_event_key_forwarded_unknown_profile_dialog_content">"%1$s sdílel(a) tuto zprávu v době, kdy jste nebyli v místnosti."</string>
<string name="crypto_history_visible">"Tato místnost byla nastavena tak, aby noví členové mohli číst historii. %1$s"</string> <string name="crypto_history_visible">"Tato místnost byla nastavena tak, aby noví členové mohli číst historii. %1$s"</string>
<string name="crypto_identity_change_pin_violation">"Identita uživatele %1$s se změnila. %2$s"</string> <string name="crypto_identity_change_pin_violation">"Identita uživatele %1$s se změnila. %2$s"</string>
<string name="crypto_identity_change_pin_violation_new">"Identita uživatele %1$s %2$s se změnila. %3$s"</string> <string name="crypto_identity_change_pin_violation_new">"Identita uživatele %1$s %2$s se změnila. %3$s"</string>
@ -474,6 +490,7 @@ Opravdu chcete pokračovat?"</string>
<string name="screen_share_this_location_action">"Sdílet tuto polohu"</string> <string name="screen_share_this_location_action">"Sdílet tuto polohu"</string>
<string name="screen_space_list_description">"Prostory, které jste vytvořili nebo se k nim připojili."</string> <string name="screen_space_list_description">"Prostory, které jste vytvořili nebo se k nim připojili."</string>
<string name="screen_space_list_details">"%1$s • %2$s"</string> <string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_empty_state_title">"Vytvořte prostory pro uspořádání místností"</string>
<string name="screen_space_list_parent_space">"%1$s prostor"</string> <string name="screen_space_list_parent_space">"%1$s prostor"</string>
<string name="screen_space_list_title">"Prostory"</string> <string name="screen_space_list_title">"Prostory"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Zpráva nebyla odeslána, protože ověřená identita uživatele %1$s se změnila."</string> <string name="screen_timeline_item_menu_send_failure_changed_identity">"Zpráva nebyla odeslána, protože ověřená identita uživatele %1$s se změnila."</string>

View file

@ -389,6 +389,7 @@ Põhjus: %1$s."</string>
<string name="common_world_readable_history">"Kõik võivad ajalugu näha"</string> <string name="common_world_readable_history">"Kõik võivad ajalugu näha"</string>
<string name="common_you">"Sina"</string> <string name="common_you">"Sina"</string>
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"Kuna sind polnud saatmise ajal jututoas, siis %1$s (%2$s) jagas seda sõnumit sinuga."</string> <string name="crypto_event_key_forwarded_known_profile_dialog_content">"Kuna sind polnud saatmise ajal jututoas, siis %1$s (%2$s) jagas seda sõnumit sinuga."</string>
<string name="crypto_event_key_forwarded_unknown_profile_dialog_content">"%1$s jagas seda sõnumit, kuna sind ei olnud selle algse saatmise ajal jututoas."</string>
<string name="crypto_history_visible">"See jututuba on seadistatud sedaviisi, et ka uued liikmed saavad lugeda varasemat ajalugu. %1$s"</string> <string name="crypto_history_visible">"See jututuba on seadistatud sedaviisi, et ka uued liikmed saavad lugeda varasemat ajalugu. %1$s"</string>
<string name="crypto_identity_change_pin_violation">"Kasutaja %1$s võrguidentiteet on lähtestatud. %2$s"</string> <string name="crypto_identity_change_pin_violation">"Kasutaja %1$s võrguidentiteet on lähtestatud. %2$s"</string>
<string name="crypto_identity_change_pin_violation_new">"Kasutaja %1$s %2$s võrguidentiteet on lähtestatud. %3$s"</string> <string name="crypto_identity_change_pin_violation_new">"Kasutaja %1$s %2$s võrguidentiteet on lähtestatud. %3$s"</string>

View file

@ -285,8 +285,10 @@ Raison : %1$s."</string>
</plurals> </plurals>
<string name="common_preparing">"Préparation…"</string> <string name="common_preparing">"Préparation…"</string>
<string name="common_privacy_policy">"Politique de confidentialité"</string> <string name="common_privacy_policy">"Politique de confidentialité"</string>
<string name="common_private">"Privé"</string>
<string name="common_private_room">"Salon privé"</string> <string name="common_private_room">"Salon privé"</string>
<string name="common_private_space">"Espace privé"</string> <string name="common_private_space">"Espace privé"</string>
<string name="common_public">"Public"</string>
<string name="common_public_room">"Salon public"</string> <string name="common_public_room">"Salon public"</string>
<string name="common_public_space">"Espace public"</string> <string name="common_public_space">"Espace public"</string>
<string name="common_reaction">"Réaction"</string> <string name="common_reaction">"Réaction"</string>
@ -341,6 +343,7 @@ Raison : %1$s."</string>
<string name="common_something_went_wrong">"Une erreur sest produite"</string> <string name="common_something_went_wrong">"Une erreur sest produite"</string>
<string name="common_something_went_wrong_message">"Nous avons rencontré un problème. Veuillez réessayer."</string> <string name="common_something_went_wrong_message">"Nous avons rencontré un problème. Veuillez réessayer."</string>
<string name="common_space">"Espace"</string> <string name="common_space">"Espace"</string>
<string name="common_space_members">"Membres de lespace"</string>
<string name="common_space_topic_placeholder">"Quel est le sujet de cet espace ?"</string> <string name="common_space_topic_placeholder">"Quel est le sujet de cet espace ?"</string>
<plurals name="common_spaces"> <plurals name="common_spaces">
<item quantity="one">"%1$d Espace"</item> <item quantity="one">"%1$d Espace"</item>

View file

@ -56,6 +56,7 @@
<string name="a11y_your_avatar">"Din avatar"</string> <string name="a11y_your_avatar">"Din avatar"</string>
<string name="action_accept">"Godta"</string> <string name="action_accept">"Godta"</string>
<string name="action_add_caption">"Legg til bildetekst"</string> <string name="action_add_caption">"Legg til bildetekst"</string>
<string name="action_add_existing_rooms">"Legg til eksisterende rom"</string>
<string name="action_add_to_timeline">"Legg til i tidslinjen"</string> <string name="action_add_to_timeline">"Legg til i tidslinjen"</string>
<string name="action_back">"Tilbake"</string> <string name="action_back">"Tilbake"</string>
<string name="action_call">"Ring"</string> <string name="action_call">"Ring"</string>
@ -75,6 +76,7 @@
<string name="action_copy_text">"Kopier tekst"</string> <string name="action_copy_text">"Kopier tekst"</string>
<string name="action_create">"Opprett"</string> <string name="action_create">"Opprett"</string>
<string name="action_create_room">"Opprett rom"</string> <string name="action_create_room">"Opprett rom"</string>
<string name="action_create_space">"Opprett område"</string>
<string name="action_deactivate">"Deaktiver"</string> <string name="action_deactivate">"Deaktiver"</string>
<string name="action_deactivate_account">"Deaktiver kontoen"</string> <string name="action_deactivate_account">"Deaktiver kontoen"</string>
<string name="action_decline">"Avslå"</string> <string name="action_decline">"Avslå"</string>
@ -162,6 +164,7 @@
<string name="action_static_map_load">"Trykk for å laste inn kart"</string> <string name="action_static_map_load">"Trykk for å laste inn kart"</string>
<string name="action_take_photo">"Ta bilde"</string> <string name="action_take_photo">"Ta bilde"</string>
<string name="action_tap_for_options">"Trykk for alternativer"</string> <string name="action_tap_for_options">"Trykk for alternativer"</string>
<string name="action_translate">"Oversett"</string>
<string name="action_try_again">"Prøv igjen"</string> <string name="action_try_again">"Prøv igjen"</string>
<string name="action_unpin">"Løsne"</string> <string name="action_unpin">"Løsne"</string>
<string name="action_view">"Vis"</string> <string name="action_view">"Vis"</string>
@ -191,6 +194,7 @@
<string name="common_copied_to_clipboard">"Kopiert til utklippstavlen"</string> <string name="common_copied_to_clipboard">"Kopiert til utklippstavlen"</string>
<string name="common_copyright">"Opphavsrett"</string> <string name="common_copyright">"Opphavsrett"</string>
<string name="common_creating_room">"Oppretter rom …"</string> <string name="common_creating_room">"Oppretter rom …"</string>
<string name="common_creating_space">"Oppretter område…"</string>
<string name="common_current_user_canceled_knock">"Forespørsel kansellert"</string> <string name="common_current_user_canceled_knock">"Forespørsel kansellert"</string>
<string name="common_current_user_left_room">"Forlot rommet"</string> <string name="common_current_user_left_room">"Forlot rommet"</string>
<string name="common_current_user_left_space">"Forlot område"</string> <string name="common_current_user_left_space">"Forlot område"</string>
@ -236,6 +240,7 @@
<string name="common_light">"Lys"</string> <string name="common_light">"Lys"</string>
<string name="common_line_copied_to_clipboard">"Linje kopiert til utklippstavlen"</string> <string name="common_line_copied_to_clipboard">"Linje kopiert til utklippstavlen"</string>
<string name="common_link_copied_to_clipboard">"Lenke kopiert til utklippstavlen"</string> <string name="common_link_copied_to_clipboard">"Lenke kopiert til utklippstavlen"</string>
<string name="common_link_new_device">"Koble til ny enhet"</string>
<string name="common_loading">"Laster inn…"</string> <string name="common_loading">"Laster inn…"</string>
<string name="common_loading_more">"Laster inn mer…"</string> <string name="common_loading_more">"Laster inn mer…"</string>
<plurals name="common_many_members"> <plurals name="common_many_members">
@ -248,10 +253,12 @@
</plurals> </plurals>
<string name="common_message">"Melding"</string> <string name="common_message">"Melding"</string>
<string name="common_message_actions">"Meldingshandlinger"</string> <string name="common_message_actions">"Meldingshandlinger"</string>
<string name="common_message_failed_to_send">"Sending av beskjed feilet"</string>
<string name="common_message_layout">"Meldingsoppsett"</string> <string name="common_message_layout">"Meldingsoppsett"</string>
<string name="common_message_removed">"Melding fjernet"</string> <string name="common_message_removed">"Melding fjernet"</string>
<string name="common_modern">"Moderne"</string> <string name="common_modern">"Moderne"</string>
<string name="common_mute">"Demp"</string> <string name="common_mute">"Demp"</string>
<string name="common_name">"Navn"</string>
<string name="common_name_and_id">"%1$s (%2$s)"</string> <string name="common_name_and_id">"%1$s (%2$s)"</string>
<string name="common_no_results">"Ingen resultater"</string> <string name="common_no_results">"Ingen resultater"</string>
<string name="common_no_room_name">"Ingen romnavn"</string> <string name="common_no_room_name">"Ingen romnavn"</string>
@ -324,6 +331,7 @@
<string name="common_something_went_wrong">"Noe gikk galt"</string> <string name="common_something_went_wrong">"Noe gikk galt"</string>
<string name="common_something_went_wrong_message">"Vi har støtt på et problem. Vennligst prøv igjen."</string> <string name="common_something_went_wrong_message">"Vi har støtt på et problem. Vennligst prøv igjen."</string>
<string name="common_space">"Område"</string> <string name="common_space">"Område"</string>
<string name="common_space_topic_placeholder">"Hva handler dette området om?"</string>
<plurals name="common_spaces"> <plurals name="common_spaces">
<item quantity="one">"%1$d Område"</item> <item quantity="one">"%1$d Område"</item>
<item quantity="other">"%1$d Områder"</item> <item quantity="other">"%1$d Områder"</item>
@ -331,6 +339,7 @@
<string name="common_starting_chat">"Starter chat…"</string> <string name="common_starting_chat">"Starter chat…"</string>
<string name="common_sticker">"Klistremerke"</string> <string name="common_sticker">"Klistremerke"</string>
<string name="common_success">"Suksess"</string> <string name="common_success">"Suksess"</string>
<string name="common_suggested">"Foreslått"</string>
<string name="common_suggestions">"Forslag"</string> <string name="common_suggestions">"Forslag"</string>
<string name="common_syncing">"Synkroniserer"</string> <string name="common_syncing">"Synkroniserer"</string>
<string name="common_system">"System"</string> <string name="common_system">"System"</string>
@ -368,6 +377,7 @@
<string name="common_waiting">"Venter…"</string> <string name="common_waiting">"Venter…"</string>
<string name="common_waiting_for_decryption_key">"Venter på denne meldingen"</string> <string name="common_waiting_for_decryption_key">"Venter på denne meldingen"</string>
<string name="common_you">"Du"</string> <string name="common_you">"Du"</string>
<string name="crypto_history_visible">"Dette rommet er konfigurert slik at nye medlemmer kan lese historikken.%1$s"</string>
<string name="crypto_identity_change_pin_violation">"%1$s\'s identitet ble tilbakestilt. %2$s"</string> <string name="crypto_identity_change_pin_violation">"%1$s\'s identitet ble tilbakestilt. %2$s"</string>
<string name="crypto_identity_change_pin_violation_new">"%1$ss %2$s-identitet ble tilbakestilt. %3$s"</string> <string name="crypto_identity_change_pin_violation_new">"%1$ss %2$s-identitet ble tilbakestilt. %3$s"</string>
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string> <string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string>
@ -459,6 +469,7 @@ Er du sikker på at du vil fortsette?"</string>
<string name="screen_share_this_location_action">"Del denne lokasjonen"</string> <string name="screen_share_this_location_action">"Del denne lokasjonen"</string>
<string name="screen_space_list_description">"Områder du har opprettet eller blitt med i."</string> <string name="screen_space_list_description">"Områder du har opprettet eller blitt med i."</string>
<string name="screen_space_list_details">"%1$s • %2$s"</string> <string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_empty_state_title">"Opprett område for å organisere rom"</string>
<string name="screen_space_list_parent_space">"%1$s område"</string> <string name="screen_space_list_parent_space">"%1$s område"</string>
<string name="screen_space_list_title">"Områder"</string> <string name="screen_space_list_title">"Områder"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Meldingen ble ikke sendt fordi %1$ss verifiserte identitet er tilbakestilt."</string> <string name="screen_timeline_item_menu_send_failure_changed_identity">"Meldingen ble ikke sendt fordi %1$ss verifiserte identitet er tilbakestilt."</string>

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:82d4ec8dfa27a109e3b6e09a989b57540ee24beacc325c32707b3fd2b6310821 oid sha256:adad85db73f043c52ad7218ec8d67375b784cbc268dd996e984b83a9821463bf
size 21869 size 22042

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:efe2ba437dadcc651834505f585101e8b69dd34fa7f5da3da000ee0a0021cac0
size 23051

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3e1492f5df9092f52dd9ecdc38d03aee7abda581bf7c12401561a6dea5a41f86 oid sha256:dfce9aca0a041ce2293cb075c5069a04fc0268494f0fdd4da6b6764060c5b812
size 22302 size 22470

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ec8262321fea4ad6cf9b648e262c3359557fa8dcfd127f70e62bc705d7ec5cd4 oid sha256:731442c83806cf4f45a9ebeddcc3124f595213673c3f8574da03f2948bb85802
size 21962 size 22131

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:55d1487d5eab42dba662fd813786894a674b406b43520f262036b1be675378f4
size 30753

Some files were not shown because too many files have changed in this diff Show more