Merge branch 'release/26.05.2'

This commit is contained in:
Benoit Marty 2026-05-19 14:44:04 +02:00
commit 678b203b96
162 changed files with 2089 additions and 1431 deletions

View file

@ -34,6 +34,13 @@
"/^org.jetbrains.kotlinx:kotlinx-datetime/", "/^org.jetbrains.kotlinx:kotlinx-datetime/",
], ],
}, },
{
// Keep Guava on the Android variant and ignore jre-only upgrades.
"matchPackageNames": [
"com.google.guava:guava",
],
"allowedVersions": "/-android$/",
},
{ {
// Limit PostHog Android upgrade to one PR per month, the first day of the month // Limit PostHog Android upgrade to one PR per month, the first day of the month
"matchPackageNames": [ "matchPackageNames": [

View file

@ -7,7 +7,7 @@ appId: ${MAESTRO_APP_ID}
- tapOn: - tapOn:
text: ${MAESTRO_INVITEE1_MXID} text: ${MAESTRO_INVITEE1_MXID}
index: 1 index: 1
- tapOn: "Send invite" - tapOn: "Continue"
- takeScreenshot: build/maestro/330-createAndDeleteDM - takeScreenshot: build/maestro/330-createAndDeleteDM
- tapOn: "maestroelement2" - tapOn: "maestroelement2"
- scroll - scroll

View file

@ -24,8 +24,16 @@ appId: ${MAESTRO_APP_ID}
text: ${MAESTRO_INVITEE2_MXID} text: ${MAESTRO_INVITEE2_MXID}
index: 1 index: 1
- tapOn: "Invite" - tapOn: "Invite"
- runFlow:
when:
visible: 'Invite new contact to this room?'
commands:
- tapOn:
id: "confirm_invite_unknown"
# Close the keyboard if it's still open
- tapOn: "Back"
# Go back to the room details screen
- tapOn: "Back" - tapOn: "Back"
- tapOn: "aRoomName"
- scrollUntilVisible: - scrollUntilVisible:
direction: DOWN direction: DOWN
element: element:

View file

@ -1,3 +1,47 @@
Changes in Element X v26.05.1
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.05.1 -->
## What's Changed
### ✨ Features
* Make Element Call screen work edge-to-edge by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6634
### 🙌 Improvements
* Stop removing the `logs` dir when clearing cache by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6765
* Adapt to new DM definition changes in the SDK by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6748
* feat: Update call started timeline item + declined support by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/6649
### 🐛 Bugfixes
* Improve pin code UX by @bmarty in https://github.com/element-hq/element-x-android/pull/6744
* Use just the other user's avatar for DM details by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6738
* Improve `FetchPushForegroundService`'s reliability by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6757
* Prevent user from starting Live Location Sharing in thread by @bmarty in https://github.com/element-hq/element-x-android/pull/6767
* Fix media playback from the timeline broken when exiting a thread by @bmarty in https://github.com/element-hq/element-x-android/pull/6771
* Pin code: remove the key if there is no pin code by @bmarty in https://github.com/element-hq/element-x-android/pull/6780
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6761
### 🚧 In development 🚧
* Feature : share live location by @ganfra in https://github.com/element-hq/element-x-android/pull/6741
### Dependency upgrades
* Update dependency org.matrix.rustcomponents:sdk-android to v26.05.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6746
* Update actions/add-to-project action to v2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6758
* Update dependency io.github.sergio-sastre.ComposablePreviewScanner:android to v0.9.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6759
* Update dependency io.element.android:element-call-embedded to v0.19.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6766
* Update metro to v1 (major) by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6720
* Update tspascoal/get-user-teams-membership action to v4.0.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6750
* Update plugin sonarqube to v7.3.0.8198 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6743
* Update plugin dependencycheck to v12.2.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6760
* Update dependency com.google.guava:guava to v33.6.0-android by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6646
* Update dependency org.matrix.rustcomponents:sdk-android to v26.05.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6779
### Others
* Render media captions formatting in the media viewer by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6729
* Reduce FeatureFlag `Knock` effect on room creation and room edition forms by @bmarty in https://github.com/element-hq/element-x-android/pull/6768
* Use the right analytics span as a parent in `checkNetworkConnection` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6751
* Add missing strings `theme.black` by @bmarty in https://github.com/element-hq/element-x-android/pull/6772
* Map back button in web view to esc (revive fixed version of: https://github.com/element-hq/element-x-android/pull/6724) by @toger5 in https://github.com/element-hq/element-x-android/pull/6725
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.05.0...v26.05.1
Changes in Element X v26.05.0 Changes in Element X v26.05.0
============================= =============================

View file

@ -382,9 +382,9 @@ class LoggedInFlowNode(
} }
is NavTarget.Room -> { is NavTarget.Room -> {
val joinedRoomCallback = object : JoinedRoomLoadedFlowNode.Callback { val joinedRoomCallback = object : JoinedRoomLoadedFlowNode.Callback {
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) { override fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean) {
lifecycleScope.launch { lifecycleScope.launch {
attachRoom(roomIdOrAlias = roomId.toRoomIdOrAlias(), serverNames = serverNames, clearBackstack = false) attachRoom(roomIdOrAlias = roomId.toRoomIdOrAlias(), serverNames = serverNames, clearBackstack = clearBackStack)
} }
} }

View file

@ -82,7 +82,7 @@ class JoinedRoomLoadedFlowNode(
plugins = plugins, plugins = plugins,
), DependencyInjectionGraphOwner { ), DependencyInjectionGraphOwner {
interface Callback : Plugin { interface Callback : Plugin {
fun navigateToRoom(roomId: RoomId, serverNames: List<String>) fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean = false)
fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
fun navigateToGlobalNotificationSettings() fun navigateToGlobalNotificationSettings()
fun navigateToDeveloperSettings() fun navigateToDeveloperSettings()
@ -150,7 +150,7 @@ class JoinedRoomLoadedFlowNode(
callback.navigateToDeveloperSettings() callback.navigateToDeveloperSettings()
} }
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) { override fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean) {
callback.navigateToRoom(roomId, serverNames) callback.navigateToRoom(roomId, serverNames)
} }

View file

@ -13,7 +13,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
class FakeJoinedRoomLoadedFlowNodeCallback : JoinedRoomLoadedFlowNode.Callback { class FakeJoinedRoomLoadedFlowNodeCallback : JoinedRoomLoadedFlowNode.Callback {
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) = lambdaError() override fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean) = lambdaError()
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError() override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError()
override fun navigateToGlobalNotificationSettings() = lambdaError() override fun navigateToGlobalNotificationSettings() = lambdaError()
override fun navigateToDeveloperSettings() = lambdaError() override fun navigateToDeveloperSettings() = lambdaError()

View file

@ -0,0 +1,2 @@
Main changes in this version: bug fixes and improvements.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -4,5 +4,5 @@
<string name="call_foreground_service_message_android">"Kõne juurde naasmiseks klõpsa"</string> <string name="call_foreground_service_message_android">"Kõne juurde naasmiseks klõpsa"</string>
<string name="call_foreground_service_title_android">"☎️ Kõne on pooleli"</string> <string name="call_foreground_service_title_android">"☎️ Kõne on pooleli"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Element Call ei võimalda selles Androidi versioonis Bluetoothi heliseadmete kasutamist. Palun vali mõni muu heliseade."</string> <string name="call_invalid_audio_device_bluetooth_devices_disabled">"Element Call ei võimalda selles Androidi versioonis Bluetoothi heliseadmete kasutamist. Palun vali mõni muu heliseade."</string>
<string name="screen_incoming_call_subtitle_android">"Sissetulev Element Calli kõne"</string> <string name="screen_incoming_call_subtitle_android">"Saabuv Element Calli kõne"</string>
</resources> </resources>

View file

@ -3,14 +3,34 @@
<string name="screen_create_room_action_create_room">"Cameră nouă"</string> <string name="screen_create_room_action_create_room">"Cameră nouă"</string>
<string name="screen_create_room_add_people_title">"Invitați prieteni"</string> <string name="screen_create_room_add_people_title">"Invitați prieteni"</string>
<string name="screen_create_room_error_creating_room">"A apărut o eroare la crearea camerei"</string> <string name="screen_create_room_error_creating_room">"A apărut o eroare la crearea camerei"</string>
<string name="screen_create_room_error_creating_space">"Spațiul nu a putut fi creat din cauza unei erori necunoscute. Încercați din nou mai târziu."</string>
<string name="screen_create_room_name_placeholder">"Adăugați un nume…"</string>
<string name="screen_create_room_new_room_title">"Cameră nouă"</string>
<string name="screen_create_room_new_space_title">"Spațiu nou"</string>
<string name="screen_create_room_private_option_description">"Doar persoanele invitate se pot alătura."</string> <string name="screen_create_room_private_option_description">"Doar persoanele invitate se pot alătura."</string>
<string name="screen_create_room_private_option_title">"Privat"</string>
<string name="screen_create_room_public_option_description">"Oricine poate găsi această cameră. <string name="screen_create_room_public_option_description">"Oricine poate găsi această cameră.
Puteți modifica acest lucru oricând în setări."</string> Puteți modifica acest lucru oricând în setări."</string>
<string name="screen_create_room_public_option_short_description">"Oricine se poate alătura."</string>
<string name="screen_create_room_public_option_title">"Public"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Oricine poate cere să se alăture camerei, dar un administrator sau un moderator va trebui să accepte cererea"</string> <string name="screen_create_room_room_access_section_knocking_option_description">"Oricine poate cere să se alăture camerei, dar un administrator sau un moderator va trebui să accepte cererea"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Permite solicitarea de alăturare"</string> <string name="screen_create_room_room_access_section_knocking_option_title">"Permite solicitarea de alăturare"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Oricine din %1$s se poate alătura, dar oricine altcineva trebuie să solicite acces."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Solicitați să vă alăturați"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Doar persoanele invitate se pot alătura."</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">"Oricine se poate alătura acestei camere"</string> <string name="screen_create_room_room_access_section_public_option_description">"Oricine se poate alătura acestei camere"</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">"Oricine din %1$s se poate alătura."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"Standard"</string>
<string name="screen_create_room_room_access_section_title">"Cine are acces"</string>
<string name="screen_create_room_room_address_section_footer">"Pentru ca această cameră să fie vizibilă în directorul de camere publice, veți avea nevoie de o adresă de cameră."</string> <string name="screen_create_room_room_address_section_footer">"Pentru ca această cameră să fie vizibilă în directorul de camere publice, veți avea nevoie de o adresă de cameră."</string>
<string name="screen_create_room_room_address_section_title">"Adresă"</string> <string name="screen_create_room_room_address_section_title">"Adresă"</string>
<string name="screen_create_room_room_visibility_section_title">"Vizibilitatea camerei"</string> <string name="screen_create_room_room_visibility_section_title">"Vizibilitatea camerei"</string>
<string name="screen_create_room_space_selection_no_space_description">"(nicun spațiu)"</string>
<string name="screen_create_room_space_selection_no_space_option">"Nu adăugați la un spațiu"</string>
<string name="screen_create_room_space_selection_no_space_title">"Niciun spațiu selectat"</string>
<string name="screen_create_room_space_selection_sheet_title">"Adăugați la spațiu"</string>
<string name="screen_create_room_topic_label">"Subiect (opțional)"</string> <string name="screen_create_room_topic_label">"Subiect (opțional)"</string>
<string name="screen_create_room_topic_placeholder">"Adăugați o descriere…"</string>
</resources> </resources>

View file

@ -1,14 +1,14 @@
<?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_deactivate_account_confirmation_dialog_content">"Potvrďte prosím, že chcete svůj účet deaktivovat. Tuto akci nelze vrátit zpět."</string> <string name="screen_deactivate_account_confirmation_dialog_content">"Potvrďte prosím, že chcete smazat svůj účet. Tuto akci nelze vrátit zpět."</string>
<string name="screen_deactivate_account_delete_all_messages">"Smazat všechny mé zprávy"</string> <string name="screen_deactivate_account_delete_all_messages">"Smazat všechny mé zprávy"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Upozornění: Budoucí uživatelé mohou vidět neúplné konverzace."</string> <string name="screen_deactivate_account_delete_all_messages_notice">"Upozornění: Budoucí uživatelé mohou vidět neúplné konverzace."</string>
<string name="screen_deactivate_account_description">"Deaktivace vašeho účtu je %1$s, což způsobí:"</string> <string name="screen_deactivate_account_description">"Smazání účtu je %1$s, dojde k:"</string>
<string name="screen_deactivate_account_description_bold_part">"nezvratná"</string> <string name="screen_deactivate_account_description_bold_part">"nezvratná"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s váš účet (nemůžete se znovu přihlásit a vaše ID nelze znovu použít)."</string> <string name="screen_deactivate_account_list_item_1">"%1$s váš účet (nemůžete se znovu přihlásit a vaše ID nelze znovu použít)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Trvale zakázat"</string> <string name="screen_deactivate_account_list_item_1_bold_part">"Trvale zakázat"</string>
<string name="screen_deactivate_account_list_item_2">"Odebere vás ze všech chatovacích místností."</string> <string name="screen_deactivate_account_list_item_2">"Odebere vás ze všech chatovacích místností."</string>
<string name="screen_deactivate_account_list_item_3">"Odstraní informace o vašem účtu z našeho serveru identit."</string> <string name="screen_deactivate_account_list_item_3">"Odstraní informace o vašem účtu z našeho serveru identit."</string>
<string name="screen_deactivate_account_list_item_4">"Vaše zprávy budou stále viditelné registrovaným uživatelům, ale nebudou dostupné novým ani neregistrovaným uživatelům, pokud se rozhodnete je smazat."</string> <string name="screen_deactivate_account_list_item_4">"Vaše zprávy budou stále viditelné registrovaným uživatelům, ale nebudou dostupné novým ani neregistrovaným uživatelům, pokud se rozhodnete je smazat."</string>
<string name="screen_deactivate_account_title">"Deaktivovat účet"</string> <string name="screen_deactivate_account_title">"Smazat účet"</string>
</resources> </resources>

View file

@ -3,7 +3,7 @@
<string name="screen_deactivate_account_confirmation_dialog_content">"Palun kinnita uuesti, et soovid kustutada oma kasutajakonto. Seda tegevust ei saa tagasi pöörata."</string> <string name="screen_deactivate_account_confirmation_dialog_content">"Palun kinnita uuesti, et soovid kustutada oma kasutajakonto. Seda tegevust ei saa tagasi pöörata."</string>
<string name="screen_deactivate_account_delete_all_messages">"Kustuta kõik minu sõnumid"</string> <string name="screen_deactivate_account_delete_all_messages">"Kustuta kõik minu sõnumid"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Hoiatus: tulevased kasutajad võivad näha poolikuid vestlusi."</string> <string name="screen_deactivate_account_delete_all_messages_notice">"Hoiatus: tulevased kasutajad võivad näha poolikuid vestlusi."</string>
<string name="screen_deactivate_account_description">"Sinu konto kasutusest eemaldamine on %1$s ja sellega:"</string> <string name="screen_deactivate_account_description">"Sinu konto kustutamine on %1$s ja sellega:"</string>
<string name="screen_deactivate_account_description_bold_part">"pöördumatu"</string> <string name="screen_deactivate_account_description_bold_part">"pöördumatu"</string>
<string name="screen_deactivate_account_list_item_1">"Sinu kasutajakonto %1$s (sa ei saa enam sellega võrku logida ning kasutajatunnust ei saa enam pruukida)."</string> <string name="screen_deactivate_account_list_item_1">"Sinu kasutajakonto %1$s (sa ei saa enam sellega võrku logida ning kasutajatunnust ei saa enam pruukida)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"jäädavalt eemaldatakse kasutusest"</string> <string name="screen_deactivate_account_list_item_1_bold_part">"jäädavalt eemaldatakse kasutusest"</string>

View file

@ -1,14 +1,14 @@
<?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_deactivate_account_confirmation_dialog_content">"Vă rugăm să confirmați că doriți să vă dezactivați contul. Această acțiune nu poate fi anulată."</string> <string name="screen_deactivate_account_confirmation_dialog_content">"Vă rugăm să confirmați că doriți să vă ștergeți contul. Această acțiune nu poate fi anulată."</string>
<string name="screen_deactivate_account_delete_all_messages">"Ștergeți toate mesajele mele"</string> <string name="screen_deactivate_account_delete_all_messages">"Ștergeți toate mesajele mele"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Avertisment: este posibil ca viitorii utilizatori să vadă conversații incomplete."</string> <string name="screen_deactivate_account_delete_all_messages_notice">"Avertisment: este posibil ca viitorii utilizatori să vadă conversații incomplete."</string>
<string name="screen_deactivate_account_description">"Dezactivarea contului dumneavoastră este %1$s, acesta va:"</string> <string name="screen_deactivate_account_description">"Ștergerea contului dumneavoastră este %1$s, acesta va:"</string>
<string name="screen_deactivate_account_description_bold_part">"ireversibilă"</string> <string name="screen_deactivate_account_description_bold_part">"ireversibilă"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s contul dumneavoastră (nu vă puteți conecta din nou, iar ID-ul dvs. nu poate fi reutilizat)."</string> <string name="screen_deactivate_account_list_item_1">"%1$s contul dumneavoastră (nu vă puteți conecta din nou, iar ID-ul dvs. nu poate fi reutilizat)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Dezactivați permanent"</string> <string name="screen_deactivate_account_list_item_1_bold_part">"Dezactivați permanent"</string>
<string name="screen_deactivate_account_list_item_2">"Îndepărta din toate camerele de chat."</string> <string name="screen_deactivate_account_list_item_2">"Îndepărta din toate camerele de chat."</string>
<string name="screen_deactivate_account_list_item_3">"Șterge informațiile contului dumneavoastră de pe serverul nostru de identitate."</string> <string name="screen_deactivate_account_list_item_3">"Șterge informațiile contului dumneavoastră de pe serverul nostru de identitate."</string>
<string name="screen_deactivate_account_list_item_4">"Mesajele dumneavoastră vor fi în continuare vizibile pentru utilizatorii înregistrați, dar nu vor fi disponibile pentru utilizatorii noi sau neînregistrați dacă alegeți să le ștergeți."</string> <string name="screen_deactivate_account_list_item_4">"Mesajele dumneavoastră vor fi în continuare vizibile pentru utilizatorii înregistrați, dar nu vor fi disponibile pentru utilizatorii noi sau neînregistrați dacă alegeți să le ștergeți."</string>
<string name="screen_deactivate_account_title">"Dezactivați contul"</string> <string name="screen_deactivate_account_title">"Ștergeți contul"</string>
</resources> </resources>

View file

@ -2,7 +2,7 @@
<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_identity_confirmation_cannot_confirm">"Kas kinnitamine pole võimalik?"</string> <string name="screen_identity_confirmation_cannot_confirm">"Kas kinnitamine pole võimalik?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Loo uus taastevõti"</string> <string name="screen_identity_confirmation_create_new_recovery_key">"Loo uus taastevõti"</string>
<string name="screen_identity_confirmation_subtitle">"Krüptitud sõnumivahetuse tagamiseks verifitseeri see seade."</string> <string name="screen_identity_confirmation_subtitle">"Turvalise sõnumside seadistamiseks vali verifitseerimise viis."</string>
<string name="screen_identity_confirmation_title">"Kinnita oma digitaalne identiteet"</string> <string name="screen_identity_confirmation_title">"Kinnita oma digitaalne identiteet"</string>
<string name="screen_identity_confirmation_use_another_device">"Kasuta teist seadet"</string> <string name="screen_identity_confirmation_use_another_device">"Kasuta teist seadet"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Kasuta taastevõtit"</string> <string name="screen_identity_confirmation_use_recovery_key">"Kasuta taastevõtit"</string>

View file

@ -83,8 +83,8 @@ class RoomListDataSource(
val loadingState = roomList.loadingState val loadingState = roomList.loadingState
fun launchIn(coroutineScope: CoroutineScope) { fun launchIn(coroutineScope: CoroutineScope): Job {
roomList return roomList
.summaries .summaries
.onEach { roomSummaries -> .onEach { roomSummaries ->
replaceWith(roomSummaries) replaceWith(roomSummaries)
@ -212,6 +212,7 @@ class RoomListDataSource(
private suspend fun rebuildAllRoomSummaries() { private suspend fun rebuildAllRoomSummaries() {
lock.withLock { lock.withLock {
roomList.summaries.replayCache.firstOrNull()?.let { roomSummaries -> roomList.summaries.replayCache.firstOrNull()?.let { roomSummaries ->
diffCacheUpdater.updateWith(roomSummaries)
buildAndEmitAllRooms(roomSummaries, useCache = false) buildAndEmitAllRooms(roomSummaries, useCache = false)
} }
} }

View file

@ -5,8 +5,8 @@
<string name="banner_battery_optimization_title_android">"Sa ei näe kõiki teavitusi?"</string> <string name="banner_battery_optimization_title_android">"Sa ei näe kõiki teavitusi?"</string>
<string name="banner_new_sound_message">"Sinu nutiseadme teavituste heli on uuenenud - see on nüüd selgem, kiirem ja vähem häiriv."</string> <string name="banner_new_sound_message">"Sinu nutiseadme teavituste heli on uuenenud - see on nüüd selgem, kiirem ja vähem häiriv."</string>
<string name="banner_new_sound_title">"Oleme sinu helisid värskendanud"</string> <string name="banner_new_sound_title">"Oleme sinu helisid värskendanud"</string>
<string name="banner_set_up_recovery_content">"Loo uus taastevõti, mida saad kasutada oma krüptitud sõnumite ajaloo taastamisel olukorras, kus kaotad ligipääsu oma seadmetele."</string> <string name="banner_set_up_recovery_content">"Sinu vestlused on automaatselt varundatud kasutades läbivat krüptimist. Kui peaksid kaotama ligipääsu kõikidele oma seadmetele, siis selle varukoopia taastamiseks ja oma digitaalse identiteedi säilitamiseks, on vaja taastevõtit."</string>
<string name="banner_set_up_recovery_submit">"Seadista andmete taastamine"</string> <string name="banner_set_up_recovery_submit">"Seadista taastevõti"</string>
<string name="banner_set_up_recovery_title">"Varunda oma vestlused"</string> <string name="banner_set_up_recovery_title">"Varunda oma vestlused"</string>
<string name="confirm_recovery_key_banner_message">"Säilitamaks ligipääsu vestluste ja krüptovõtmete varukoopiale, palun sisesta kinnituseks oma taastevõti."</string> <string name="confirm_recovery_key_banner_message">"Säilitamaks ligipääsu vestluste ja krüptovõtmete varukoopiale, palun sisesta kinnituseks oma taastevõti."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Sisesta oma taastevõti"</string> <string name="confirm_recovery_key_banner_primary_button_title">"Sisesta oma taastevõti"</string>

View file

@ -50,6 +50,7 @@ Nu aveți mesaje necitite!"</string>
<string name="screen_roomlist_mark_as_read">"Marcați ca citită"</string> <string name="screen_roomlist_mark_as_read">"Marcați ca citită"</string>
<string name="screen_roomlist_mark_as_unread">"Marcați ca necitită"</string> <string name="screen_roomlist_mark_as_unread">"Marcați ca necitită"</string>
<string name="screen_roomlist_tombstoned_room_description">"Această cameră a fost modernizată."</string> <string name="screen_roomlist_tombstoned_room_description">"Această cameră a fost modernizată."</string>
<string name="screen_roomlist_your_spaces">"Spațiile dumneavoastră"</string>
<string name="session_verification_banner_message">"Se pare că folosiți un dispozitiv nou. Verificați-vă identitatea cu un alt dispozitiv pentru a accesa mesajele dumneavoastră criptate."</string> <string name="session_verification_banner_message">"Se pare că folosiți un dispozitiv nou. Verificați-vă identitatea cu un alt dispozitiv pentru a accesa mesajele dumneavoastră criptate."</string>
<string name="session_verification_banner_title">"Verificați că sunteți dumneavoastră"</string> <string name="session_verification_banner_title">"Verificați că sunteți dumneavoastră"</string>
</resources> </resources>

View file

@ -14,6 +14,9 @@ import io.element.android.features.home.impl.FakeDateTimeObserver
import io.element.android.libraries.androidutils.system.DateTimeObserver import io.element.android.libraries.androidutils.system.DateTimeObserver
import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.A_ROOM_ID_3
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
@ -100,11 +103,169 @@ class RoomListDataSourceTest {
} }
} }
/**
* Tracking issue #4182: rooms duplicated in the room list around midnight.
*
* If the SDK ever leaks a list containing the same roomId twice (the suspected cause of #4182),
* the UI mapper's `distinctBy` safety net in [RoomListDataSource.buildAndEmitAllRooms] must
* remove the duplicate AND `analyticsService.trackError` must fire so the team can root-cause
* it via Sentry.
*/
@Test
fun `when SDK summaries source contains duplicate roomIds, UI layer dedupes and reports trackError`() = runTest {
val analyticsService = FakeAnalyticsService()
val duplicatedSummaries = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(roomId = A_ROOM_ID_2),
)
val roomList = FakeDynamicRoomList(summaries = MutableStateFlow(duplicatedSummaries))
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
).apply {
postState(RoomListService.State.Running)
}
val roomListDataSource = createRoomListDataSource(
roomListService = roomListService,
analyticsService = analyticsService,
)
roomListDataSource.roomSummariesFlow.test {
roomListDataSource.launchIn(backgroundScope)
val list = awaitItem()
assertThat(list.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder()
assertThat(analyticsService.trackedErrors).hasSize(1)
}
}
/**
* Tracking issue #4182.
*
* Targeted scenario: a `DateChanged` tick fires after an initial SDK emit, then a follow-up
* SDK emit lands (mimicking "midnight, then a new message arrives"). Even though the diffCache
* is bypassed during the rebuild (`useCache = false`), the final state must contain each
* roomId exactly once and trackError must not fire on a happy path.
*/
@Test
fun `interleaved date change and SDK update with overlapping content does not produce duplicates`() = runTest {
val analyticsService = FakeAnalyticsService()
val summariesFlow = MutableStateFlow(
listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(roomId = A_ROOM_ID_2),
)
)
val roomList = FakeDynamicRoomList(summaries = summariesFlow)
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
).apply {
postState(RoomListService.State.Running)
}
val dateTimeObserver = FakeDateTimeObserver()
val roomListDataSource = createRoomListDataSource(
roomListService = roomListService,
dateTimeObserver = dateTimeObserver,
analyticsService = analyticsService,
)
roomListDataSource.roomSummariesFlow.test {
roomListDataSource.launchIn(backgroundScope)
val initial = awaitItem()
assertThat(initial.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder()
// Midnight ticks while the cache holds [A_ROOM_ID, A_ROOM_ID_2]
dateTimeObserver.given(DateTimeObserver.Event.DateChanged(Instant.MIN, Instant.now()))
val afterMidnight = awaitItem()
assertThat(afterMidnight.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder()
// A new message bumps A_ROOM_ID — different unread count makes the StateFlow see this
// as a new value
summariesFlow.value = listOf(
aRoomSummary(roomId = A_ROOM_ID, numUnreadMessages = 1),
aRoomSummary(roomId = A_ROOM_ID_2),
)
val afterMessage = awaitItem()
assertThat(afterMessage.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder()
assertThat(afterMessage.map { it.roomId }.toSet()).hasSize(afterMessage.size)
// Second midnight rebuild after the new message
dateTimeObserver.given(DateTimeObserver.Event.DateChanged(Instant.MIN, Instant.now()))
val afterSecondMidnight = awaitItem()
assertThat(afterSecondMidnight.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder()
assertThat(afterSecondMidnight.map { it.roomId }.toSet()).hasSize(afterSecondMidnight.size)
assertThat(analyticsService.trackedErrors).isEmpty()
}
}
@Test
fun `regression test for race with DateTimeObserver and new items`() = runTest {
val roomList = FakeDynamicRoomList(summaries = MutableStateFlow(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2))))
val roomListService = FakeRoomListService(
createRoomListLambda = { roomList }
).apply {
postState(RoomListService.State.Running)
}
val dateTimeObserver = FakeDateTimeObserver()
var dateFormatterResult = "Today"
val dateFormatter = FakeDateFormatter({ _, _, _ -> dateFormatterResult })
val roomListDataSource = createRoomListDataSource(
roomListService = roomListService,
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
dateFormatter = dateFormatter,
),
dateTimeObserver = dateTimeObserver,
)
roomListDataSource.roomSummariesFlow.test {
// Observe room list items changes
val job = roomListDataSource.launchIn(backgroundScope)
// Get the initial room list
val initialRoomList = awaitItem()
assertThat(initialRoomList).hasSize(2)
assertThat(initialRoomList[0].roomId).isEqualTo(A_ROOM_ID)
assertThat(initialRoomList[0].timestamp).isEqualTo(dateFormatterResult)
assertThat(initialRoomList[1].roomId).isEqualTo(A_ROOM_ID_2)
assertThat(initialRoomList[1].timestamp).isEqualTo(dateFormatterResult)
// Stop processing room list updates so we can force a race condition with the date time observer updates
job.cancel()
// Trigger a date change and a new item at the same time
dateFormatterResult = "Yesterday"
roomList.summaries.tryEmit(listOf(aRoomSummary(roomId = A_ROOM_ID), aRoomSummary(roomId = A_ROOM_ID_3), aRoomSummary(roomId = A_ROOM_ID_2)))
dateTimeObserver.given(DateTimeObserver.Event.DateChanged(Instant.MIN, Instant.now()))
// The race condition would have caused the cache indices to be corrupted and only 2 items would be emitted
val rebuiltRoomList = awaitItem()
assertThat(rebuiltRoomList).hasSize(3)
assertThat(rebuiltRoomList[0].roomId).isEqualTo(A_ROOM_ID)
assertThat(rebuiltRoomList[0].timestamp).isEqualTo(dateFormatterResult)
assertThat(rebuiltRoomList[1].roomId).isEqualTo(A_ROOM_ID_3)
assertThat(rebuiltRoomList[1].timestamp).isEqualTo(dateFormatterResult)
assertThat(rebuiltRoomList[2].roomId).isEqualTo(A_ROOM_ID_2)
assertThat(rebuiltRoomList[2].timestamp).isEqualTo(dateFormatterResult)
// Restart processing room list updates
roomListDataSource.launchIn(backgroundScope)
// Check there is a new list and it's not the same as the previous one
val newRoomList = awaitItem()
assertThat(newRoomList).hasSize(3)
assertThat(newRoomList[0].roomId).isEqualTo(A_ROOM_ID)
assertThat(newRoomList[0].timestamp).isEqualTo(dateFormatterResult)
assertThat(newRoomList[1].roomId).isEqualTo(A_ROOM_ID_3)
assertThat(newRoomList[1].timestamp).isEqualTo(dateFormatterResult)
assertThat(newRoomList[2].roomId).isEqualTo(A_ROOM_ID_2)
assertThat(newRoomList[2].timestamp).isEqualTo(dateFormatterResult)
}
}
private fun TestScope.createRoomListDataSource( private fun TestScope.createRoomListDataSource(
roomListService: FakeRoomListService = FakeRoomListService(), roomListService: FakeRoomListService = FakeRoomListService(),
roomListRoomSummaryFactory: RoomListRoomSummaryFactory = aRoomListRoomSummaryFactory(), roomListRoomSummaryFactory: RoomListRoomSummaryFactory = aRoomListRoomSummaryFactory(),
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
dateTimeObserver: FakeDateTimeObserver = FakeDateTimeObserver(), dateTimeObserver: FakeDateTimeObserver = FakeDateTimeObserver(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
) = RoomListDataSource( ) = RoomListDataSource(
roomListService = roomListService, roomListService = roomListService,
roomListRoomSummaryFactory = roomListRoomSummaryFactory, roomListRoomSummaryFactory = roomListRoomSummaryFactory,
@ -112,6 +273,6 @@ class RoomListDataSourceTest {
notificationSettingsService = notificationSettingsService, notificationSettingsService = notificationSettingsService,
sessionCoroutineScope = backgroundScope, sessionCoroutineScope = backgroundScope,
dateTimeObserver = dateTimeObserver, dateTimeObserver = dateTimeObserver,
analyticsService = FakeAnalyticsService(), analyticsService = analyticsService,
) )
} }

View file

@ -11,4 +11,5 @@ package io.element.android.features.invitepeople.api
interface InvitePeopleEvents { interface InvitePeopleEvents {
data object SendInvites : InvitePeopleEvents data object SendInvites : InvitePeopleEvents
data object CloseSearch : InvitePeopleEvents data object CloseSearch : InvitePeopleEvents
data object ClearError : InvitePeopleEvents
} }

View file

@ -9,10 +9,12 @@
package io.element.android.features.invitepeople.api package io.element.android.features.invitepeople.api
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
interface InvitePeopleState { interface InvitePeopleState {
val canInvite: Boolean val canInvite: Boolean
val isSearchActive: Boolean val isSearchActive: Boolean
val sendInvitesAction: AsyncAction<Unit> val sendInvitesAction: AsyncAction<Unit>
val createRoomFromDmAction: AsyncAction<RoomId>
val eventSink: (InvitePeopleEvents) -> Unit val eventSink: (InvitePeopleEvents) -> Unit
} }

View file

@ -10,6 +10,7 @@ package io.element.android.features.invitepeople.api
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
class InvitePeopleStateProvider : PreviewParameterProvider<InvitePeopleState> { class InvitePeopleStateProvider : PreviewParameterProvider<InvitePeopleState> {
override val values: Sequence<InvitePeopleState> override val values: Sequence<InvitePeopleState>
@ -25,6 +26,7 @@ private data class PreviewInvitePeopleState(
override val canInvite: Boolean, override val canInvite: Boolean,
override val isSearchActive: Boolean, override val isSearchActive: Boolean,
override val sendInvitesAction: AsyncAction<Unit>, override val sendInvitesAction: AsyncAction<Unit>,
override val createRoomFromDmAction: AsyncAction<RoomId>,
override val eventSink: (InvitePeopleEvents) -> Unit, override val eventSink: (InvitePeopleEvents) -> Unit,
) : InvitePeopleState ) : InvitePeopleState
@ -32,10 +34,12 @@ private fun aPreviewInvitePeopleState(
canInvite: Boolean = false, canInvite: Boolean = false,
isSearchActive: Boolean = false, isSearchActive: Boolean = false,
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized, sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
createRoomFromDmAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
eventSink: (InvitePeopleEvents) -> Unit = {}, eventSink: (InvitePeopleEvents) -> Unit = {},
) = PreviewInvitePeopleState( ) = PreviewInvitePeopleState(
canInvite = canInvite, canInvite = canInvite,
isSearchActive = isSearchActive, isSearchActive = isSearchActive,
sendInvitesAction = sendInvitesAction, sendInvitesAction = sendInvitesAction,
createRoomFromDmAction = createRoomFromDmAction,
eventSink = eventSink eventSink = eventSink
) )

View file

@ -36,6 +36,7 @@ dependencies {
implementation(projects.libraries.uiUtils) implementation(projects.libraries.uiUtils)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.usersearch.api) implementation(projects.libraries.usersearch.api)
implementation(projects.libraries.testtags)
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(projects.services.apperror.api) implementation(projects.services.apperror.api)
implementation(projects.libraries.featureflag.api) implementation(projects.libraries.featureflag.api)

View file

@ -38,12 +38,17 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient 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.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.createroom.RoomPreset
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
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.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.filterMembers import io.element.android.libraries.matrix.api.room.filterMembers
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.recent.getRecentDirectRooms import io.element.android.libraries.matrix.api.room.recent.getRecentDirectRooms
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.usersearch.api.UserRepository import io.element.android.libraries.usersearch.api.UserRepository
@ -88,6 +93,7 @@ class DefaultInvitePeoplePresenter(
var searchActive by rememberSaveable { mutableStateOf(false) } var searchActive by rememberSaveable { mutableStateOf(false) }
val showSearchLoader = rememberSaveable { mutableStateOf(false) } val showSearchLoader = rememberSaveable { mutableStateOf(false) }
val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) } val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
val createRoomFromDmAction = remember { mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized) }
val recentDirectRooms by produceState(emptyList(), roomMembers.value) { val recentDirectRooms by produceState(emptyList(), roomMembers.value) {
if (roomMembers.value.isSuccess()) { if (roomMembers.value.isSuccess()) {
@ -208,7 +214,13 @@ class DefaultInvitePeoplePresenter(
) )
} else { } else {
room.dataOrNull()?.let { room.dataOrNull()?.let {
sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction) sessionCoroutineScope.launch {
if (it.isDm()) {
createRoomFromDm(it, selectedUsers.value, createRoomFromDmAction)
} else {
sendInvites(it, selectedUsers.value, sendInvitesAction)
}
}
} }
} }
} }
@ -216,6 +228,10 @@ class DefaultInvitePeoplePresenter(
searchActive = false searchActive = false
queryState.clearText() queryState.clearText()
} }
is InvitePeopleEvents.ClearError -> {
sendInvitesAction.value = AsyncAction.Uninitialized
createRoomFromDmAction.value = AsyncAction.Uninitialized
}
} }
} }
@ -228,6 +244,7 @@ class DefaultInvitePeoplePresenter(
searchResults = searchResults.value, searchResults = searchResults.value,
showSearchLoader = showSearchLoader.value, showSearchLoader = showSearchLoader.value,
sendInvitesAction = sendInvitesAction.value, sendInvitesAction = sendInvitesAction.value,
createRoomFromDmAction = createRoomFromDmAction.value,
suggestions = suggestions, suggestions = suggestions,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
@ -254,6 +271,35 @@ class DefaultInvitePeoplePresenter(
} }
} }
private fun CoroutineScope.createRoomFromDm(
currentRoom: JoinedRoom,
selectedUsers: List<MatrixUser>,
createRoomFromDmAction: MutableState<AsyncAction<RoomId>>,
) = launch {
createRoomFromDmAction.runUpdatingState {
val currentUsers = currentRoom.getMembers(limit = 100).getOrNull().orEmpty()
.filter { it.membership.isActive() }
val invitees = (currentUsers.map { it.userId } + selectedUsers.map { it.userId })
.filter { it != matrixClient.sessionId }
.distinct()
matrixClient.createRoom(
CreateRoomParameters(
name = null,
topic = null,
isEncrypted = true,
isDirect = false,
visibility = RoomVisibility.Private,
preset = RoomPreset.PRIVATE_CHAT,
invite = invitees,
avatar = null,
joinRuleOverride = JoinRule.Invite,
historyVisibilityOverride = RoomHistoryVisibility.Invited,
isSpace = false,
)
)
}
}
@JvmName("toggleUserInSelectedUsers") @JvmName("toggleUserInSelectedUsers")
private fun MutableState<ImmutableList<MatrixUser>>.toggleUser(user: MatrixUser) { private fun MutableState<ImmutableList<MatrixUser>>.toggleUser(user: MatrixUser) {
value = if (value.contains(user)) { value = if (value.contains(user)) {

View file

@ -14,6 +14,7 @@ import io.element.android.features.invitepeople.api.InvitePeopleState
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.RoomId
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
@ -26,6 +27,7 @@ data class DefaultInvitePeopleState(
val selectedUsers: ImmutableList<MatrixUser>, val selectedUsers: ImmutableList<MatrixUser>,
override val isSearchActive: Boolean, override val isSearchActive: Boolean,
override val sendInvitesAction: AsyncAction<Unit>, override val sendInvitesAction: AsyncAction<Unit>,
override val createRoomFromDmAction: AsyncAction<RoomId>,
val suggestions: ImmutableList<InvitableUser>, val suggestions: ImmutableList<InvitableUser>,
override val eventSink: (InvitePeopleEvents) -> Unit override val eventSink: (InvitePeopleEvents) -> Unit
) : InvitePeopleState ) : InvitePeopleState

View file

@ -18,6 +18,7 @@ import io.element.android.libraries.designsystem.preview.USER_NAME_CAROL
import io.element.android.libraries.designsystem.preview.USER_NAME_EVE import io.element.android.libraries.designsystem.preview.USER_NAME_EVE
import io.element.android.libraries.designsystem.preview.USER_NAME_JUSTIN import io.element.android.libraries.designsystem.preview.USER_NAME_JUSTIN
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.RoomId
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.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.matrix.ui.components.aMatrixUserList
@ -119,6 +120,7 @@ private fun aDefaultInvitePeopleState(
isSearchActive: Boolean = false, isSearchActive: Boolean = false,
showSearchLoader: Boolean = false, showSearchLoader: Boolean = false,
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized, sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
createRoomFromDmAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
suggestions: List<InvitableUser> = aMatrixUserList() suggestions: List<InvitableUser> = aMatrixUserList()
.take(5) .take(5)
.map { user -> anInvitableUser(matrixUser = user, isSelected = user in selectedUsers) }, .map { user -> anInvitableUser(matrixUser = user, isSelected = user in selectedUsers) },
@ -132,6 +134,7 @@ private fun aDefaultInvitePeopleState(
isSearchActive = isSearchActive, isSearchActive = isSearchActive,
showSearchLoader = showSearchLoader, showSearchLoader = showSearchLoader,
sendInvitesAction = sendInvitesAction, sendInvitesAction = sendInvitesAction,
createRoomFromDmAction = createRoomFromDmAction,
suggestions = suggestions.toImmutableList(), suggestions = suggestions.toImmutableList(),
eventSink = {}, eventSink = {},
) )

View file

@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@ -23,6 +24,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -54,6 +56,8 @@ import io.element.android.libraries.matrix.ui.components.MatrixUserRow
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.getBestName import io.element.android.libraries.matrix.ui.model.getBestName
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.strings.simplePluralStringResource import io.element.android.libraries.ui.utils.strings.simplePluralStringResource
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -102,7 +106,7 @@ private fun InvitePeopleContentView(
} }
InvitePeopleSearchBar( InvitePeopleSearchBar(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.imePadding().fillMaxWidth(),
queryState = state.searchQuery, queryState = state.searchQuery,
showLoader = state.showSearchLoader, showLoader = state.showSearchLoader,
selectedUsers = state.selectedUsers, selectedUsers = state.selectedUsers,
@ -298,7 +302,7 @@ private fun InvitePeopleConfirmModal(
text = stringResource(CommonStrings.action_remove), text = stringResource(CommonStrings.action_remove),
onClick = onRemove, onClick = onRemove,
leadingIcon = IconSource.Vector(CompoundIcons.Close()), leadingIcon = IconSource.Vector(CompoundIcons.Close()),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f).testTag(TestTags.confirmInviteUnknown),
) )
Button( Button(
text = stringResource(CommonStrings.action_invite), text = stringResource(CommonStrings.action_invite),

View file

@ -2,4 +2,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_invite_users_already_a_member">"Sa juba oled jututoa liige"</string> <string name="screen_invite_users_already_a_member">"Sa juba oled jututoa liige"</string>
<string name="screen_invite_users_already_invited">"Sa juba oled kutse saanud"</string> <string name="screen_invite_users_already_invited">"Sa juba oled kutse saanud"</string>
<string name="screen_invite_users_confirm_dialog_subtitle_multiple_users">"Sul pole hetkel nende kontaktidega ühtegi vestlust. Enne jätkamist kinnita neile siia jututuppa kutse saatmine."</string>
<string name="screen_invite_users_confirm_dialog_subtitle_one_user">"Sul pole hetkel selle kontaktiga ühtegi vestlust. Enne jätkamist kinnita talle siia jututuppa kutse saatmine."</string>
<string name="screen_invite_users_confirm_dialog_title_mutiple_users">"Kas kutsud uued kontaktid siia jututuppa?"</string>
<string name="screen_invite_users_confirm_dialog_title_one_user">"Kas kutsud uue kontakti siia jututuppa?"</string>
</resources> </resources>

View file

@ -2,4 +2,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_invite_users_already_a_member">"Deja membru"</string> <string name="screen_invite_users_already_a_member">"Deja membru"</string>
<string name="screen_invite_users_already_invited">"Deja invitat"</string> <string name="screen_invite_users_already_invited">"Deja invitat"</string>
<string name="screen_invite_users_confirm_dialog_subtitle_multiple_users">"În prezent, nu aveți nicio conversație cu aceste contacte. Confirmați invitarea lor în această cameră înainte de a continua."</string>
<string name="screen_invite_users_confirm_dialog_subtitle_one_user">"În prezent, nu aveți nicio conversație cu acest contact. Confirmați invitarea acestuia în cameră înainte de a continua."</string>
<string name="screen_invite_users_confirm_dialog_title_mutiple_users">"Invitați contactele noi în această cameră?"</string>
<string name="screen_invite_users_confirm_dialog_title_one_user">"Invitați contactul nou în această cameră?"</string>
</resources> </resources>

View file

@ -831,6 +831,54 @@ internal class DefaultInvitePeoplePresenterTest {
} }
} }
@Test
fun `present - inviting someone to a DM creates a new room`() = runTest {
val alice = aMatrixUser("@alice:example.com")
val matrixClient = FakeMatrixClient(
encryptionService = FakeEncryptionService(
getUserIdentityResult = lambdaRecorder { userId: UserId ->
Result.success(IdentityState.Pinned)
}
)
)
val presenter = createDefaultInvitePeoplePresenter(
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
matrixClient = matrixClient,
joinedRoom = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
initialRoomInfo = aRoomInfo(isDm = true),
getMembersResult = { Result.success(listOf(aRoomMember(userId = alice.userId, membership = RoomMembershipState.JOIN))) },
)
)
)
presenter.test {
val initialState = awaitItem()
skipItems(1)
// We want to add a new user to a DM
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
// And we send the invites
initialState.eventSink(InvitePeopleEvents.SendInvites)
skipItems(1)
awaitItemAsDefault().run {
assertThat(canInvite).isTrue()
assertThat(sendInvitesAction.isUninitialized()).isTrue()
// Inviting to a DM should trigger the creation of a new room
assertThat(createRoomFromDmAction.isLoading()).isTrue()
}
awaitItemAsDefault().run {
assertThat(sendInvitesAction.isUninitialized()).isTrue()
// Once the room is created, the action should be successful
assertThat(createRoomFromDmAction.isSuccess()).isTrue()
}
}
}
private suspend fun FakeUserRepository.emitStateWithUsers( private suspend fun FakeUserRepository.emitStateWithUsers(
users: List<MatrixUser>, users: List<MatrixUser>,
isSearching: Boolean = false isSearching: Boolean = false

View file

@ -26,6 +26,7 @@
<string name="screen_link_new_device_root_loading_qr_code">"Se încarcă codul QR…"</string> <string name="screen_link_new_device_root_loading_qr_code">"Se încarcă codul QR…"</string>
<string name="screen_link_new_device_root_mobile_device">"Dispozitiv mobil"</string> <string name="screen_link_new_device_root_mobile_device">"Dispozitiv mobil"</string>
<string name="screen_link_new_device_root_title">"Ce tip de dispozitiv doriți să conectați?"</string> <string name="screen_link_new_device_root_title">"Ce tip de dispozitiv doriți să conectați?"</string>
<string name="screen_link_new_device_wrong_number_subtitle">"Încercați din nou și asigurați-vă că ați introdus corect codul de 2 cifre. Dacă numerele tot nu se potrivesc, contactați furnizorul contului."</string>
<string name="screen_link_new_device_wrong_number_title">"Numerele nu se potrivesc"</string> <string name="screen_link_new_device_wrong_number_title">"Numerele nu se potrivesc"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Nu a putut fi făcută o conexiune sigură la noul dispozitiv. Dispozitivele existente sunt încă în siguranță și nu trebuie să vă faceți griji cu privire la ele."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Nu a putut fi făcută o conexiune sigură la noul dispozitiv. Dispozitivele existente sunt încă în siguranță și nu trebuie să vă faceți griji cu privire la ele."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Și acum?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Și acum?"</string>
@ -39,6 +40,8 @@
<string name="screen_qr_code_login_error_cancelled_title">"Cererea de autentificare a fost anulată"</string> <string name="screen_qr_code_login_error_cancelled_title">"Cererea de autentificare a fost anulată"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Autentificarea a fost refuzată pe celălalt dispozitiv."</string> <string name="screen_qr_code_login_error_declined_subtitle">"Autentificarea a fost refuzată pe celălalt dispozitiv."</string>
<string name="screen_qr_code_login_error_declined_title">"Autentificarea a fost refuzată"</string> <string name="screen_qr_code_login_error_declined_title">"Autentificarea a fost refuzată"</string>
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Nu trebuie să faceți nimic altceva."</string>
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Celălalt dispozitiv este deja conectat"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Autentificarea a expirat. Vă rugăm să încercați din nou."</string> <string name="screen_qr_code_login_error_expired_subtitle">"Autentificarea a expirat. Vă rugăm să încercați din nou."</string>
<string name="screen_qr_code_login_error_expired_title">"Autentificarea nu a fost finalizată la timp"</string> <string name="screen_qr_code_login_error_expired_title">"Autentificarea nu a fost finalizată la timp"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Celălalt dispozitiv nu acceptă autentificarea la %s cu un cod QR. <string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Celălalt dispozitiv nu acceptă autentificarea la %s cu un cod QR.

View file

@ -40,8 +40,6 @@ import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.core.extensions.flatMap import io.element.android.libraries.core.extensions.flatMap
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.dateformatter.api.DurationFormatter import io.element.android.libraries.dateformatter.api.DurationFormatter
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.MatrixClient
import io.element.android.libraries.matrix.api.room.CreateTimelineParams import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
@ -66,7 +64,6 @@ class ShareLocationPresenter(
private val messageComposerContext: MessageComposerContext, private val messageComposerContext: MessageComposerContext,
private val locationActions: LocationActions, private val locationActions: LocationActions,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val featureFlagService: FeatureFlagService,
private val client: MatrixClient, private val client: MatrixClient,
private val durationFormatter: DurationFormatter, private val durationFormatter: DurationFormatter,
private val liveLocationShareManager: ActiveLiveLocationShareManager, private val liveLocationShareManager: ActiveLiveLocationShareManager,
@ -83,9 +80,6 @@ class ShareLocationPresenter(
override fun present(): ShareLocationState { override fun present(): ShareLocationState {
val permissionsState: PermissionsState = permissionsPresenter.present() val permissionsState: PermissionsState = permissionsPresenter.present()
var trackUserPosition: Boolean by remember { mutableStateOf(permissionsState.isAnyGranted && locationActions.isLocationEnabled()) } var trackUserPosition: Boolean by remember { mutableStateOf(permissionsState.isAnyGranted && locationActions.isLocationEnabled()) }
val isLiveLocationSharingEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.LiveLocationSharing)
}.collectAsState(false)
val appName by remember { derivedStateOf { buildMeta.applicationName } } val appName by remember { derivedStateOf { buildMeta.applicationName } }
var dialogState: ShareLocationState.Dialog by remember { var dialogState: ShareLocationState.Dialog by remember {
mutableStateOf(ShareLocationState.Dialog.None) mutableStateOf(ShareLocationState.Dialog.None)
@ -171,7 +165,7 @@ class ShareLocationPresenter(
dialogState = dialogState, dialogState = dialogState,
trackUserLocation = trackUserPosition, trackUserLocation = trackUserPosition,
hasLocationPermission = permissionsState.isAnyGranted, hasLocationPermission = permissionsState.isAnyGranted,
canShareLiveLocation = isLiveLocationSharingEnabled && timelineMode.canShareLiveLocation(), canShareLiveLocation = timelineMode.canShareLiveLocation(),
appName = appName, appName = appName,
startLiveLocationAction = startLiveLocationAction.value, startLiveLocationAction = startLiveLocationAction.value,
eventSink = ::handleEvent, eventSink = ::handleEvent,

View file

@ -2,4 +2,5 @@
<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_share_location_live_location_disclaimer_title">"Vaše historie aktuální polohy bude uložena v místnosti a bude viditelná pro členy i po skončení relace."</string> <string name="screen_share_location_live_location_disclaimer_title">"Vaše historie aktuální polohy bude uložena v místnosti a bude viditelná pro členy i po skončení relace."</string>
<string name="screen_share_location_live_location_duration_picker_title">"Zvolte, jak dlouho chcete sdílet svou aktuální polohu."</string> <string name="screen_share_location_live_location_duration_picker_title">"Zvolte, jak dlouho chcete sdílet svou aktuální polohu."</string>
<string name="screen_share_location_live_location_missing_permissions">"Nemáte oprávnění sdílet svou aktuální polohu v této místnosti."</string>
</resources> </resources>

View file

@ -1,4 +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_share_location_live_location_disclaimer_title">"Sinu reaalajas jagatud asukoha ajalugu salvestub siin jututoas ja see on liikmetele nähtav ka pärast jagamissessiooni lõppu."</string>
<string name="screen_share_location_live_location_duration_picker_title">"Vali, kui kaua tahad oma reaalajas jagada."</string> <string name="screen_share_location_live_location_duration_picker_title">"Vali, kui kaua tahad oma reaalajas jagada."</string>
<string name="screen_share_location_live_location_missing_permissions">"Sul pole õigust jagada selles jututoas oma asukohta reaalajas"</string>
</resources> </resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_share_location_live_location_disclaimer_title">"Istoricul locațiilor dumneavoastră va fi stocat în cameră și va fi vizibil pentru membri după încheierea sesiunii."</string>
<string name="screen_share_location_live_location_duration_picker_title">"Alegeți cât timp doriți să vă partajați locația în timp real."</string>
<string name="screen_share_location_live_location_missing_permissions">"Nu aveți permisiunea de a vă partaja locația în această cameră."</string>
</resources>

View file

@ -1,6 +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_share_location_live_location_disclaimer_title">"你实时位置历史将存储在房间中,并于会话结束后对其他成员可见。"</string> <string name="screen_share_location_live_location_disclaimer_title">"你实时位置历史将存储在房间中,并于会话结束后对其他成员可见。"</string>
<string name="screen_share_location_live_location_duration_picker_title">"选择共享实时位置的时长。"</string> <string name="screen_share_location_live_location_duration_picker_title">"选择共享实时位置的时长。"</string>
<string name="screen_share_location_live_location_missing_permissions">"你无权在此房内共享实时位置。"</string> <string name="screen_share_location_live_location_missing_permissions">"你无权在此房内共享实时位置。"</string>
</resources> </resources>

View file

@ -17,7 +17,6 @@ import io.element.android.features.location.impl.live.LiveLocationStore
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.libraries.dateformatter.test.FakeDurationFormatter import io.element.android.libraries.dateformatter.test.FakeDurationFormatter
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
@ -50,7 +49,6 @@ class DefaultShareLocationEntryPointTest {
messageComposerContext = FakeMessageComposerContext(), messageComposerContext = FakeMessageComposerContext(),
locationActions = FakeLocationActions(), locationActions = FakeLocationActions(),
buildMeta = aBuildMeta(), buildMeta = aBuildMeta(),
featureFlagService = FakeFeatureFlagService(),
client = FakeMatrixClient(), client = FakeMatrixClient(),
durationFormatter = FakeDurationFormatter(), durationFormatter = FakeDurationFormatter(),
liveLocationShareManager = FakeActiveLiveLocationShareManager(), liveLocationShareManager = FakeActiveLiveLocationShareManager(),

View file

@ -29,8 +29,6 @@ import io.element.android.features.location.impl.live.LiveLocationStore
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.libraries.dateformatter.test.FakeDurationFormatter import io.element.android.libraries.dateformatter.test.FakeDurationFormatter
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
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.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
@ -77,7 +75,6 @@ class ShareLocationPresenterTest {
private val fakeMessageComposerContext = FakeMessageComposerContext() private val fakeMessageComposerContext = FakeMessageComposerContext()
private val fakeLocationActions = FakeLocationActions() private val fakeLocationActions = FakeLocationActions()
private val fakeBuildMeta = aBuildMeta(applicationName = "app name") private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
private val fakeFeatureFlagService = FakeFeatureFlagService()
private val fakeMatrixClient = FakeMatrixClient(sessionId = A_USER_ID) private val fakeMatrixClient = FakeMatrixClient(sessionId = A_USER_ID)
private val durationFormatter = FakeDurationFormatter() private val durationFormatter = FakeDurationFormatter()
@ -96,7 +93,6 @@ class ShareLocationPresenterTest {
messageComposerContext = fakeMessageComposerContext, messageComposerContext = fakeMessageComposerContext,
locationActions = locationActions, locationActions = locationActions,
buildMeta = fakeBuildMeta, buildMeta = fakeBuildMeta,
featureFlagService = fakeFeatureFlagService,
client = fakeMatrixClient, client = fakeMatrixClient,
durationFormatter = durationFormatter, durationFormatter = durationFormatter,
liveLocationShareManager = liveLocationShareManager, liveLocationShareManager = liveLocationShareManager,
@ -658,21 +654,7 @@ class ShareLocationPresenterTest {
} }
@Test @Test
fun `canShareLiveLocation is false when the feature is disabled`() = runTest { fun `canShareLiveLocation is true in live timeline`() = runTest {
fakeFeatureFlagService.setFeatureEnabled(FeatureFlags.LiveLocationSharing, false)
val shareLocationPresenter = createShareLocationPresenter(
timelineMode = Timeline.Mode.Live,
)
shareLocationPresenter.test {
skipItems(1)
val state = awaitItem()
assertThat(state.canShareLiveLocation).isFalse()
}
}
@Test
fun `canShareLiveLocation is true when the feature is enabled`() = runTest {
fakeFeatureFlagService.setFeatureEnabled(FeatureFlags.LiveLocationSharing, true)
val shareLocationPresenter = createShareLocationPresenter( val shareLocationPresenter = createShareLocationPresenter(
timelineMode = Timeline.Mode.Live, timelineMode = Timeline.Mode.Live,
) )
@ -685,7 +667,6 @@ class ShareLocationPresenterTest {
@Test @Test
fun `canShareLiveLocation is false in thread timeline`() = runTest { fun `canShareLiveLocation is false in thread timeline`() = runTest {
fakeFeatureFlagService.setFeatureEnabled(FeatureFlags.LiveLocationSharing, true)
val shareLocationPresenter = createShareLocationPresenter( val shareLocationPresenter = createShareLocationPresenter(
timelineMode = Timeline.Mode.Thread(A_THREAD_ID), timelineMode = Timeline.Mode.Thread(A_THREAD_ID),
) )

View file

@ -43,7 +43,7 @@ class DefaultLockScreenService(
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val sessionObserver: SessionObserver, private val sessionObserver: SessionObserver,
private val appForegroundStateService: AppForegroundStateService, private val appForegroundStateService: AppForegroundStateService,
biometricAuthenticatorManager: BiometricAuthenticatorManager, private val biometricAuthenticatorManager: BiometricAuthenticatorManager,
) : LockScreenService { ) : LockScreenService {
private val _lockState = MutableStateFlow<LockScreenLockState>(LockScreenLockState.Unlocked) private val _lockState = MutableStateFlow<LockScreenLockState>(LockScreenLockState.Unlocked)
override val lockState: StateFlow<LockScreenLockState> = _lockState override val lockState: StateFlow<LockScreenLockState> = _lockState
@ -81,6 +81,7 @@ class DefaultLockScreenService(
override suspend fun onSessionDeleted(userId: String, wasLastSession: Boolean) { override suspend fun onSessionDeleted(userId: String, wasLastSession: Boolean) {
if (wasLastSession) { if (wasLastSession) {
pinCodeManager.deletePinCode() pinCodeManager.deletePinCode()
biometricAuthenticatorManager.disable()
} }
} }
}) })

View file

@ -24,6 +24,11 @@ interface BiometricAuthenticatorManager {
fun addCallback(callback: BiometricAuthenticator.Callback) fun addCallback(callback: BiometricAuthenticator.Callback)
fun removeCallback(callback: BiometricAuthenticator.Callback) fun removeCallback(callback: BiometricAuthenticator.Callback)
/**
* Disable using the biometric unlock feature and remove any data associated with it.
*/
suspend fun disable()
/** /**
* Remember a biometric authenticator ready for unlocking the app. * Remember a biometric authenticator ready for unlocking the app.
*/ */

View file

@ -80,10 +80,7 @@ class DefaultBiometricAuthenticatorManager(
private val internalCallback = object : DefaultBiometricUnlockCallback() { private val internalCallback = object : DefaultBiometricUnlockCallback() {
override fun onBiometricSetupError() { override fun onBiometricSetupError() {
coroutineScope.launch { coroutineScope.launch { disable() }
lockScreenStore.setIsBiometricUnlockAllowed(false)
secretKeyRepository.deleteKey(SECRET_KEY_ALIAS)
}
} }
} }
@ -120,6 +117,11 @@ class DefaultBiometricAuthenticatorManager(
) )
} }
override suspend fun disable() {
lockScreenStore.setIsBiometricUnlockAllowed(false)
secretKeyRepository.deleteKey(SECRET_KEY_ALIAS)
}
@Composable @Composable
private fun rememberBiometricAuthenticator( private fun rememberBiometricAuthenticator(
isAvailable: Boolean, isAvailable: Boolean,

View file

@ -59,6 +59,7 @@ class LockScreenSettingsPresenter(
if (showRemovePinConfirmation) { if (showRemovePinConfirmation) {
showRemovePinConfirmation = false showRemovePinConfirmation = false
pinCodeManager.deletePinCode() pinCodeManager.deletePinCode()
biometricAuthenticatorManager.disable()
} }
} }
} }

View file

@ -34,5 +34,5 @@ Vali midagi, mis hästi meelde jääb. Kui unustad selle PIN-koodi, siis turvaka
</plurals> </plurals>
<string name="screen_app_lock_use_biometric_android">"Kasuta biomeetriat"</string> <string name="screen_app_lock_use_biometric_android">"Kasuta biomeetriat"</string>
<string name="screen_app_lock_use_pin_android">"Kasuta PIN-koodi"</string> <string name="screen_app_lock_use_pin_android">"Kasuta PIN-koodi"</string>
<string name="screen_signout_in_progress_dialog_content">"Logime välja…"</string> <string name="screen_signout_in_progress_dialog_content">"Eemaldan seadet…"</string>
</resources> </resources>

View file

@ -15,6 +15,7 @@ class FakeBiometricAuthenticatorManager(
override var isDeviceSecured: Boolean = true, override var isDeviceSecured: Boolean = true,
override var hasAvailableAuthenticator: Boolean = false, override var hasAvailableAuthenticator: Boolean = false,
private val createBiometricAuthenticator: () -> BiometricAuthenticator = { FakeBiometricAuthenticator() }, private val createBiometricAuthenticator: () -> BiometricAuthenticator = { FakeBiometricAuthenticator() },
private val disableLambda: suspend () -> Unit = { },
) : BiometricAuthenticatorManager { ) : BiometricAuthenticatorManager {
override fun addCallback(callback: BiometricAuthenticator.Callback) { override fun addCallback(callback: BiometricAuthenticator.Callback) {
// no-op // no-op
@ -37,4 +38,8 @@ class FakeBiometricAuthenticatorManager(
createBiometricAuthenticator() createBiometricAuthenticator()
} }
} }
override suspend fun disable() {
disableLambda()
}
} }

View file

@ -60,7 +60,6 @@ dependencies {
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
@ -81,7 +80,6 @@ dependencies {
testImplementation(projects.features.login.test) testImplementation(projects.features.login.test)
testImplementation(projects.features.enterprise.test) testImplementation(projects.features.enterprise.test)
testImplementation(projects.features.preferences.test) testImplementation(projects.features.preferences.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oauth.test) testImplementation(projects.libraries.oauth.test)
testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.permissions.test)

View file

@ -28,8 +28,6 @@ import io.element.android.libraries.androidutils.service.ServiceBinder
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.auth.ElementClassicSession import io.element.android.libraries.matrix.api.auth.ElementClassicSession
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@ -71,7 +69,6 @@ class DefaultElementClassicConnection(
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val matrixAuthenticationService: MatrixAuthenticationService, private val matrixAuthenticationService: MatrixAuthenticationService,
private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker, private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker,
private val featureFlagService: FeatureFlagService,
) : ElementClassicConnection { ) : ElementClassicConnection {
// Messenger for communicating with the service. // Messenger for communicating with the service.
private var messenger: Messenger? = null private var messenger: Messenger? = null
@ -119,10 +116,6 @@ class DefaultElementClassicConnection(
override fun start() { override fun start() {
Timber.tag(loggerTag.value).d("start()") Timber.tag(loggerTag.value).d("start()")
coroutineScope.launch { coroutineScope.launch {
if (!featureFlagService.isFeatureEnabled(FeatureFlags.SignInWithClassic)) {
Timber.tag(loggerTag.value).d("Login with Element Classic is disabled, not starting connection")
return@launch
}
// Establish a connection with the service. We use an explicit // Establish a connection with the service. We use an explicit
// class name because there is no reason to be able to let other // class name because there is no reason to be able to let other
// applications replace our component. // applications replace our component.
@ -158,11 +151,6 @@ class DefaultElementClassicConnection(
override fun requestSession() { override fun requestSession() {
Timber.tag(loggerTag.value).d("requestSession()") Timber.tag(loggerTag.value).d("requestSession()")
coroutineScope.launch { coroutineScope.launch {
if (!featureFlagService.isFeatureEnabled(FeatureFlags.SignInWithClassic)) {
Timber.tag(loggerTag.value).d("Login with Element Classic is disabled")
emitState(ElementClassicConnectionState.Error("The feature is disabled"))
return@launch
}
val finalMessenger = messenger val finalMessenger = messenger
if (finalMessenger == null) { if (finalMessenger == null) {
Timber.tag(loggerTag.value).d("The messenger is null, can't request data") Timber.tag(loggerTag.value).d("The messenger is null, can't request data")

View file

@ -28,7 +28,7 @@
<string name="screen_change_server_subtitle">"Jaká je adresa vašeho serveru?"</string> <string name="screen_change_server_subtitle">"Jaká je adresa vašeho serveru?"</string>
<string name="screen_change_server_title">"Vyberte váš server"</string> <string name="screen_change_server_title">"Vyberte váš server"</string>
<string name="screen_create_account_title">"Vytvořit účet"</string> <string name="screen_create_account_title">"Vytvořit účet"</string>
<string name="screen_login_error_deactivated_account">"Tento účet byl deaktivován."</string> <string name="screen_login_error_deactivated_account">"Tento účet byl smazán."</string>
<string name="screen_login_error_invalid_credentials">"Nesprávné uživatelské jméno nebo heslo"</string> <string name="screen_login_error_invalid_credentials">"Nesprávné uživatelské jméno nebo heslo"</string>
<string name="screen_login_error_invalid_user_id">"Toto není platný identifikátor uživatele. Očekávaný formát: \'@user:homeserver.org\'"</string> <string name="screen_login_error_invalid_user_id">"Toto není platný identifikátor uživatele. Očekávaný formát: \'@user:homeserver.org\'"</string>
<string name="screen_login_error_refresh_tokens">"Tento server je nakonfigurován tak, aby používal obnovovací tokeny. Ty nejsou podporovány při použití přihlašovacích údajů založených na hesle."</string> <string name="screen_login_error_refresh_tokens">"Tento server je nakonfigurován tak, aby používal obnovovací tokeny. Ty nejsou podporovány při použití přihlašovacích údajů založených na hesle."</string>

View file

@ -28,7 +28,7 @@
<string name="screen_change_server_subtitle">"Mis on sinu koduserveri aadress?"</string> <string name="screen_change_server_subtitle">"Mis on sinu koduserveri aadress?"</string>
<string name="screen_change_server_title">"Vali oma server"</string> <string name="screen_change_server_title">"Vali oma server"</string>
<string name="screen_create_account_title">"Loo kasutajakonto"</string> <string name="screen_create_account_title">"Loo kasutajakonto"</string>
<string name="screen_login_error_deactivated_account">"Konto on kasutusest eemaldatud."</string> <string name="screen_login_error_deactivated_account">"See kasutajakonto on kustutatud."</string>
<string name="screen_login_error_invalid_credentials">"Vigane kasutajanimi ja/või salasõna"</string> <string name="screen_login_error_invalid_credentials">"Vigane kasutajanimi ja/või salasõna"</string>
<string name="screen_login_error_invalid_user_id">"See ei ole korrektne kasutajanimi. Õige vorming on: „@kasutaja:koduserver.ee“"</string> <string name="screen_login_error_invalid_user_id">"See ei ole korrektne kasutajanimi. Õige vorming on: „@kasutaja:koduserver.ee“"</string>
<string name="screen_login_error_refresh_tokens">"See server on seadistatud kasutama tunnusloa põhist sisselogimist. Salasõnaga sisselogimisel see võimalus aga ei ole toetatud."</string> <string name="screen_login_error_refresh_tokens">"See server on seadistatud kasutama tunnusloa põhist sisselogimist. Salasõnaga sisselogimisel see võimalus aga ei ole toetatud."</string>
@ -39,7 +39,13 @@
<string name="screen_login_title_with_homeserver">"Logi sisse serverisse %1$s"</string> <string name="screen_login_title_with_homeserver">"Logi sisse serverisse %1$s"</string>
<string name="screen_missing_key_backup_open_element_classic">"Ava Element Classic"</string> <string name="screen_missing_key_backup_open_element_classic">"Ava Element Classic"</string>
<string name="screen_missing_key_backup_step_1">"Ava Element Classic oma seadmes"</string> <string name="screen_missing_key_backup_step_1">"Ava Element Classic oma seadmes"</string>
<string name="screen_missing_key_backup_step_2_android">"Ava „Seadistused“ → „Turvalisus ja privaatsus“"</string>
<string name="screen_missing_key_backup_step_3_android">"Krüptovõtmete halduses vali „Krüptitud sõnumite taastamine“"</string>
<string name="screen_missing_key_backup_step_4">"Võtmehoidla kasutuselevõtmiseks palun järgi juhendit"</string>
<string name="screen_missing_key_backup_step_5">"Tule tagasi rakendusse %1$s"</string>
<string name="screen_missing_key_backup_title">"Enne jätkamist rakenduses %1$s võta oma võtmehoidla kasutusele"</string>
<string name="screen_onboarding_app_version">"Versioon %1$s"</string> <string name="screen_onboarding_app_version">"Versioon %1$s"</string>
<string name="screen_onboarding_checking_account">"Kontrollin kasutajakontot"</string>
<string name="screen_onboarding_sign_in_manually">"Logi sisse käsitsi"</string> <string name="screen_onboarding_sign_in_manually">"Logi sisse käsitsi"</string>
<string name="screen_onboarding_sign_in_to">"Logi sisse serverisse %1$s"</string> <string name="screen_onboarding_sign_in_to">"Logi sisse serverisse %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Logi sisse QR-koodi alusel"</string> <string name="screen_onboarding_sign_in_with_qr_code">"Logi sisse QR-koodi alusel"</string>

View file

@ -28,7 +28,7 @@
<string name="screen_change_server_subtitle">"Care este adresa serverului dumneavoastră?"</string> <string name="screen_change_server_subtitle">"Care este adresa serverului dumneavoastră?"</string>
<string name="screen_change_server_title">"Selectați serverul dumneavoastra"</string> <string name="screen_change_server_title">"Selectați serverul dumneavoastra"</string>
<string name="screen_create_account_title">"Creați un cont"</string> <string name="screen_create_account_title">"Creați un cont"</string>
<string name="screen_login_error_deactivated_account">"Acest cont a fost dezactivat."</string> <string name="screen_login_error_deactivated_account">"Acest cont a fost șters."</string>
<string name="screen_login_error_invalid_credentials">"Utilizator și/sau parolă incorecte"</string> <string name="screen_login_error_invalid_credentials">"Utilizator și/sau parolă incorecte"</string>
<string name="screen_login_error_invalid_user_id">"Acesta nu este un identificator de utilizator valid. Format așteptat: „@user:homeserver.org”"</string> <string name="screen_login_error_invalid_user_id">"Acesta nu este un identificator de utilizator valid. Format așteptat: „@user:homeserver.org”"</string>
<string name="screen_login_error_refresh_tokens">"Acest server este configurat pentru a utiliza token-uri de reîmprospătare. Acestea nu sunt acceptate atunci când utilizați autentificare bazată pe parolă."</string> <string name="screen_login_error_refresh_tokens">"Acest server este configurat pentru a utiliza token-uri de reîmprospătare. Acestea nu sunt acceptate atunci când utilizați autentificare bazată pe parolă."</string>
@ -37,11 +37,20 @@
<string name="screen_login_subtitle">"Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată."</string> <string name="screen_login_subtitle">"Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată."</string>
<string name="screen_login_title">"Bine ați revenit!"</string> <string name="screen_login_title">"Bine ați revenit!"</string>
<string name="screen_login_title_with_homeserver">"Conectați-vă la %1$s"</string> <string name="screen_login_title_with_homeserver">"Conectați-vă la %1$s"</string>
<string name="screen_missing_key_backup_open_element_classic">"Deschideți Element Clasic"</string>
<string name="screen_missing_key_backup_step_1">"Deschideți Element Classic pe dispozitivul dumneavoastră"</string>
<string name="screen_missing_key_backup_step_2_android">"Accesați Setări &gt; Securitate și confidențialitate"</string>
<string name="screen_missing_key_backup_step_3_android">"În Gestionarea cheilor criptografice, selectați Recuperarea mesajelor criptate"</string>
<string name="screen_missing_key_backup_step_4">"Urmați instrucțiunile pentru a activa stocarea cheilor"</string>
<string name="screen_missing_key_backup_step_5">"Reveniți la %1$s"</string>
<string name="screen_missing_key_backup_title">"Activați stocarea cheilor înainte de a continua către %1$s"</string>
<string name="screen_onboarding_app_version">"Versiunea %1$s"</string> <string name="screen_onboarding_app_version">"Versiunea %1$s"</string>
<string name="screen_onboarding_checking_account">"Se verifică contul…"</string>
<string name="screen_onboarding_sign_in_manually">"Conectați-vă manual"</string> <string name="screen_onboarding_sign_in_manually">"Conectați-vă manual"</string>
<string name="screen_onboarding_sign_in_to">"Conectați-vă la %1$s"</string> <string name="screen_onboarding_sign_in_to">"Conectați-vă la %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Conectați-vă cu un cod QR"</string> <string name="screen_onboarding_sign_in_with_qr_code">"Conectați-vă cu un cod QR"</string>
<string name="screen_onboarding_sign_up">"Creați un cont"</string> <string name="screen_onboarding_sign_up">"Creați un cont"</string>
<string name="screen_onboarding_welcome_back">"Bine ați revenit"</string>
<string name="screen_onboarding_welcome_message">"Bine ați venit la cel mai rapid %1$s din toate timpurile. Supraalimentat pentru viteză și simplitate."</string> <string name="screen_onboarding_welcome_message">"Bine ați venit la cel mai rapid %1$s din toate timpurile. Supraalimentat pentru viteză și simplitate."</string>
<string name="screen_onboarding_welcome_subtitle">"Bun venit în %1$s. Supraalimentat, pentru viteză și simplitate."</string> <string name="screen_onboarding_welcome_subtitle">"Bun venit în %1$s. Supraalimentat, pentru viteză și simplitate."</string>
<string name="screen_onboarding_welcome_title">"Fii în Elementul tău"</string> <string name="screen_onboarding_welcome_title">"Fii în Elementul tău"</string>
@ -60,6 +69,8 @@
<string name="screen_qr_code_login_error_cancelled_title">"Cererea de autentificare a fost anulată"</string> <string name="screen_qr_code_login_error_cancelled_title">"Cererea de autentificare a fost anulată"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Autentificarea a fost refuzată pe celălalt dispozitiv."</string> <string name="screen_qr_code_login_error_declined_subtitle">"Autentificarea a fost refuzată pe celălalt dispozitiv."</string>
<string name="screen_qr_code_login_error_declined_title">"Autentificarea a fost refuzată"</string> <string name="screen_qr_code_login_error_declined_title">"Autentificarea a fost refuzată"</string>
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Nu trebuie să faceți nimic altceva."</string>
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Celălalt dispozitiv este deja conectat"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Autentificarea a expirat. Vă rugăm să încercați din nou."</string> <string name="screen_qr_code_login_error_expired_subtitle">"Autentificarea a expirat. Vă rugăm să încercați din nou."</string>
<string name="screen_qr_code_login_error_expired_title">"Autentificarea nu a fost finalizată la timp"</string> <string name="screen_qr_code_login_error_expired_title">"Autentificarea nu a fost finalizată la timp"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Celălalt dispozitiv nu acceptă autentificarea la %s cu un cod QR. <string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Celălalt dispozitiv nu acceptă autentificarea la %s cu un cod QR.

View file

@ -15,9 +15,6 @@ import androidx.core.graphics.createBitmap
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.androidutils.service.ServiceBinder import io.element.android.libraries.androidutils.service.ServiceBinder
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.auth.ElementClassicSession import io.element.android.libraries.matrix.api.auth.ElementClassicSession
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@ -112,21 +109,6 @@ class DefaultElementClassicConnectionTest {
} }
} }
@Test
fun `requestSession when the feature is disabled emits an error`() = runTest {
val connection = createDefaultElementClassicConnection(
matrixAuthenticationService = FakeMatrixAuthenticationService(
setElementClassicSessionResult = {},
),
isFeatureEnabled = false,
)
connection.stateFlow.test {
assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle)
connection.requestSession()
assertThat(awaitItem()).isInstanceOf(ElementClassicConnectionState.Error::class.java)
}
}
@Test @Test
fun `when an error is received, an error is emitted`() = runTest { fun `when an error is received, an error is emitted`() = runTest {
val connection = createDefaultElementClassicConnection( val connection = createDefaultElementClassicConnection(
@ -514,17 +496,10 @@ class DefaultElementClassicConnectionTest {
homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(
checkResult = { Result.success(true) } checkResult = { Result.success(true) }
), ),
isFeatureEnabled: Boolean = true,
featureFlagService: FeatureFlagService = FakeFeatureFlagService(
initialState = mapOf(
FeatureFlags.SignInWithClassic.key to isFeatureEnabled,
)
),
) = DefaultElementClassicConnection( ) = DefaultElementClassicConnection(
serviceBinder = serviceBinder, serviceBinder = serviceBinder,
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
matrixAuthenticationService = matrixAuthenticationService, matrixAuthenticationService = matrixAuthenticationService,
homeServerLoginCompatibilityChecker = homeServerLoginCompatibilityChecker, homeServerLoginCompatibilityChecker = homeServerLoginCompatibilityChecker,
featureFlagService = featureFlagService,
) )
} }

View file

@ -1,18 +1,18 @@
<?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_signout_confirmation_dialog_content">"Kas sa oled kindel, et soovid välja logida?"</string> <string name="screen_signout_confirmation_dialog_content">"Kas sa oled kindel, et soovid selle seadme eemaldada?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Eemalda see seade"</string> <string name="screen_signout_confirmation_dialog_submit">"Eemalda see seade"</string>
<string name="screen_signout_confirmation_dialog_title">"Eemalda see seade"</string> <string name="screen_signout_confirmation_dialog_title">"Eemalda see seade"</string>
<string name="screen_signout_in_progress_dialog_content">"Logime välja…"</string> <string name="screen_signout_in_progress_dialog_content">"Eemaldan seadet…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Oled oma viimasest seansist välja logimas. Kui logid nüüd välja, kaotad ligipääsu oma krüptitud sõnumitele."</string> <string name="screen_signout_key_backup_disabled_subtitle">"See on sinu ainus seade. Kui sa selle eemaldad, vajad taastamisvõtit, et kinnitada oma digitaalset identiteeti ja taastada järgmisel sisselogimisel oma krüptitud vestlused."</string>
<string name="screen_signout_key_backup_disabled_title">"Sa oled varukoopiate tegemise välja lülitanud"</string> <string name="screen_signout_key_backup_disabled_title">"Sa kaotad peagi juurdepääsu oma krüptitud vestlustele"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Kui su võrguühendus katkes, siis sinu krüptovõtmed oli parasjagu varundamisel. Loo võrguühendus uuesti, oota kuni krüptovõtmete varundamine lõppeb ja alles siis logi rakendusest välja."</string> <string name="screen_signout_key_backup_offline_subtitle">"Kui su võrguühendus katkes, siis sinu krüptovõtmed oli parasjagu varundamisel. Loo võrguühendus uuesti, oota kuni krüptovõtmete varundamine lõppeb ja alles siis eemalda see seade."</string>
<string name="screen_signout_key_backup_offline_title">"Sinu krüptovõtmed on veel varundamisel"</string> <string name="screen_signout_key_backup_offline_title">"Sinu krüptovõtmed on veel varundamisel"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Enne väljalogimist palun oota, et pooleliolev toiming lõppeb."</string> <string name="screen_signout_key_backup_ongoing_subtitle">"Enne selle seadme eemaldamist palun oota, et pooleliolev toiming lõppeb."</string>
<string name="screen_signout_key_backup_ongoing_title">"Sinu krüptovõtmed on veel varundamisel"</string> <string name="screen_signout_key_backup_ongoing_title">"Sinu krüptovõtmed on veel varundamisel"</string>
<string name="screen_signout_preference_item">"Eemalda see seade"</string> <string name="screen_signout_preference_item">"Eemalda see seade"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Sa oled logimas välja oma viimasest sessioonist. Kui teed seda nüüd, siis kaotad ligipääsu oma krüptitud sõnumitele."</string> <string name="screen_signout_recovery_disabled_subtitle">"See on sinu ainus seade. Kui sa selle eemaldad, vajad taastamisvõtit, et kinnitada oma digitaalset identiteeti ja taastada järgmisel sisselogimisel oma krüptitud vestlused."</string>
<string name="screen_signout_recovery_disabled_title">"Andmete taastamine on seadistamata"</string> <string name="screen_signout_recovery_disabled_title">"Andmete taastamine on seadistamata"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Sa oled logimas välja oma viimasest sessioonist. Kui teed seda nüüd, siis ilmselt kaotad ligipääsu oma krüptitud sõnumitele."</string> <string name="screen_signout_save_recovery_key_subtitle">"Sa oled logimas välja oma viimasest sessioonist. Kui teed seda nüüd, siis ilmselt kaotad ligipääsu oma krüptitud sõnumitele."</string>
<string name="screen_signout_save_recovery_key_title">"Kas sa oled oma taastevõtme salvestanud?"</string> <string name="screen_signout_save_recovery_key_title">"Enne selle seadme eemaldamist veendu, et sul on juurdepääs taastevõtmele"</string>
</resources> </resources>

View file

@ -1,7 +1,7 @@
<?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_event_authenticity_mismatched_sender">"Odesílatel události se neshoduje s vlastníkem zařízení, které ji odeslalo."</string> <string name="crypto_event_authenticity_mismatched_sender">"Odesílatel události se neshoduje s vlastníkem zařízení, které ji odeslalo."</string>
<string name="crypto_event_authenticity_not_guaranteed">"Autenticitu této zašifrované zprávy nelze na tomto zařízení zaručit."</string> <string name="crypto_event_authenticity_not_guaranteed">"Pravost této šifrované zprávy nelze na tomto zařízení zaručit."</string>
<string name="crypto_event_authenticity_previously_verified">"Zašifrováno dříve ověřeným uživatelem."</string> <string name="crypto_event_authenticity_previously_verified">"Zašifrováno dříve ověřeným uživatelem."</string>
<string name="crypto_event_authenticity_sent_in_clear">"Není zašifrováno."</string> <string name="crypto_event_authenticity_sent_in_clear">"Není zašifrováno."</string>
<string name="crypto_event_authenticity_unknown_device">"Šifrováno neznámým nebo smazaným zařízením."</string> <string name="crypto_event_authenticity_unknown_device">"Šifrováno neznámým nebo smazaným zařízením."</string>

View file

@ -35,7 +35,7 @@
<string name="screen_room_attachment_source_camera_video">"Salvesta video"</string> <string name="screen_room_attachment_source_camera_video">"Salvesta video"</string>
<string name="screen_room_attachment_source_files">"Manus"</string> <string name="screen_room_attachment_source_files">"Manus"</string>
<string name="screen_room_attachment_source_gallery">"Fotode ja videote galerii"</string> <string name="screen_room_attachment_source_gallery">"Fotode ja videote galerii"</string>
<string name="screen_room_attachment_source_location">"Asukoht"</string> <string name="screen_room_attachment_source_location">"Jaga asukohta"</string>
<string name="screen_room_attachment_source_poll">"Küsitlus"</string> <string name="screen_room_attachment_source_poll">"Küsitlus"</string>
<string name="screen_room_attachment_text_formatting">"Tekstivorming"</string> <string name="screen_room_attachment_text_formatting">"Tekstivorming"</string>
<string name="screen_room_encrypted_history_banner">"Sõnumite ajalugu pole hetkel saadaval"</string> <string name="screen_room_encrypted_history_banner">"Sõnumite ajalugu pole hetkel saadaval"</string>

View file

@ -32,7 +32,7 @@
<string name="screen_report_content_hint">"Powód zgłoszenia treści"</string> <string name="screen_report_content_hint">"Powód zgłoszenia treści"</string>
<string name="screen_room_attachment_source_camera">"Kamera"</string> <string name="screen_room_attachment_source_camera">"Kamera"</string>
<string name="screen_room_attachment_source_camera_photo">"Zrób zdjęcie"</string> <string name="screen_room_attachment_source_camera_photo">"Zrób zdjęcie"</string>
<string name="screen_room_attachment_source_camera_video">"Nagraj film"</string> <string name="screen_room_attachment_source_camera_video">"Nagraj wideo"</string>
<string name="screen_room_attachment_source_files">"Załącznik"</string> <string name="screen_room_attachment_source_files">"Załącznik"</string>
<string name="screen_room_attachment_source_gallery">"Zdjęcia i filmy"</string> <string name="screen_room_attachment_source_gallery">"Zdjęcia i filmy"</string>
<string name="screen_room_attachment_source_location">"Udostępnij lokalizację"</string> <string name="screen_room_attachment_source_location">"Udostępnij lokalizację"</string>

View file

@ -25,11 +25,8 @@ import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Inject @Inject
@ -56,17 +53,8 @@ class AdvancedSettingsPresenter(
appPreferencesStore.getThemeFlow().mapToTheme(isBlackThemeAllowed) appPreferencesStore.getThemeFlow().mapToTheme(isBlackThemeAllowed)
}.collectAsState(initial = Theme.System) }.collectAsState(initial = Theme.System)
@OptIn(ExperimentalCoroutinesApi::class)
val liveLocationMinimumDistanceUpdate by produceState<Int?>(null) { val liveLocationMinimumDistanceUpdate by produceState<Int?>(null) {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.LiveLocationSharing) appPreferencesStore.getLiveLocationMinimumDistanceInMetersUpdateFlow().collect { value = it }
.flatMapLatest { isEnabled ->
if (isEnabled) {
appPreferencesStore.getLiveLocationMinimumDistanceInMetersUpdateFlow()
} else {
emptyFlow()
}
}
.collect { value = it }
} }
val mediaPreviewConfigState = mediaPreviewConfigStateStore.state() val mediaPreviewConfigState = mediaPreviewConfigStateStore.state()

View file

@ -87,6 +87,7 @@ Pokud budete pokračovat, některá nastavení se mohou změnit."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"systémová nastavení"</string> <string name="screen_notification_settings_system_notifications_action_required_content_link">"systémová nastavení"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Systémová oznámení byla vypnuta"</string> <string name="screen_notification_settings_system_notifications_turned_off">"Systémová oznámení byla vypnuta"</string>
<string name="screen_notification_settings_title">"Oznámení"</string> <string name="screen_notification_settings_title">"Oznámení"</string>
<string name="theme_black">"Černý"</string>
<string name="theme_dark">"Tmavé"</string> <string name="theme_dark">"Tmavé"</string>
<string name="theme_light">"Světlý"</string> <string name="theme_light">"Světlý"</string>
<string name="theme_system">"Systém"</string> <string name="theme_system">"Systém"</string>

View file

@ -11,7 +11,10 @@
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Peida jututubade kutsetest tunnuspildid"</string> <string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Peida jututubade kutsetest tunnuspildid"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Peida meedia eelvaated ajajoonel"</string> <string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Peida meedia eelvaated ajajoonel"</string>
<string name="screen_advanced_settings_labs">"Katsed"</string> <string name="screen_advanced_settings_labs">"Katsed"</string>
<string name="screen_advanced_settings_live_location_section_description">"Vahemaa, mille pead andmete uuenduse käivitamiseks läbima."</string>
<string name="screen_advanced_settings_live_location_section_footer">"Veendu, et sellel rakendusel on õigus kasutada funktsionaalsust „Täpne asukoht“. Õiguste muutmiseks ava %1$s."</string>
<string name="screen_advanced_settings_live_location_section_footer_link">"Rakenduse seadistused"</string> <string name="screen_advanced_settings_live_location_section_footer_link">"Rakenduse seadistused"</string>
<string name="screen_advanced_settings_live_location_section_title">"Andmete uuendused reaalajas asukoha jagamisel"</string>
<plurals name="screen_advanced_settings_live_location_update_distance"> <plurals name="screen_advanced_settings_live_location_update_distance">
<item quantity="one">"Iga %1$d meeter"</item> <item quantity="one">"Iga %1$d meeter"</item>
<item quantity="other">"Iga %1$d meetrit"</item> <item quantity="other">"Iga %1$d meetrit"</item>
@ -81,6 +84,7 @@ Kui sa jätkad muutmist, siis võivad muutuda ka need peidetud eelistused."</str
<string name="screen_notification_settings_system_notifications_action_required_content_link">"süsteemi seadistusi"</string> <string name="screen_notification_settings_system_notifications_action_required_content_link">"süsteemi seadistusi"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Süsteemi teavitused on välja lülitatud"</string> <string name="screen_notification_settings_system_notifications_turned_off">"Süsteemi teavitused on välja lülitatud"</string>
<string name="screen_notification_settings_title">"Teavitused"</string> <string name="screen_notification_settings_title">"Teavitused"</string>
<string name="theme_black">"Süsimust kujundus"</string>
<string name="theme_dark">"Tume"</string> <string name="theme_dark">"Tume"</string>
<string name="theme_light">"Hele"</string> <string name="theme_light">"Hele"</string>
<string name="theme_system">"Süsteem"</string> <string name="theme_system">"Süsteem"</string>

View file

@ -84,6 +84,7 @@ Ha folytatja, egyes beállítások megváltozhatnak."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"rendszerbeállításokat"</string> <string name="screen_notification_settings_system_notifications_action_required_content_link">"rendszerbeállításokat"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"A rendszerértesítések ki vannak kapcsolva"</string> <string name="screen_notification_settings_system_notifications_turned_off">"A rendszerértesítések ki vannak kapcsolva"</string>
<string name="screen_notification_settings_title">"Értesítések"</string> <string name="screen_notification_settings_title">"Értesítések"</string>
<string name="theme_black">"Fekete"</string>
<string name="theme_dark">"Sötét"</string> <string name="theme_dark">"Sötét"</string>
<string name="theme_light">"Világos"</string> <string name="theme_light">"Világos"</string>
<string name="theme_system">"Rendszer"</string> <string name="theme_system">"Rendszer"</string>

View file

@ -85,6 +85,7 @@ Niektóre ustawienia mogą ulec zmianie, jeśli kontynuujesz."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"ustawienia systemowe"</string> <string name="screen_notification_settings_system_notifications_action_required_content_link">"ustawienia systemowe"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Powiadomienia systemowe wyłączone"</string> <string name="screen_notification_settings_system_notifications_turned_off">"Powiadomienia systemowe wyłączone"</string>
<string name="screen_notification_settings_title">"Powiadomienia"</string> <string name="screen_notification_settings_title">"Powiadomienia"</string>
<string name="theme_black">"Czarny"</string>
<string name="theme_dark">"Ciemny"</string> <string name="theme_dark">"Ciemny"</string>
<string name="theme_light">"Jasny"</string> <string name="theme_light">"Jasny"</string>
<string name="theme_system">"System"</string> <string name="theme_system">"System"</string>

View file

@ -11,6 +11,15 @@
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Ascundeți avatarele din invitațiile pentru camere"</string> <string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Ascundeți avatarele din invitațiile pentru camere"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Ascundeți previzualizările media în lista de mesaje"</string> <string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Ascundeți previzualizările media în lista de mesaje"</string>
<string name="screen_advanced_settings_labs">"Laboratoare"</string> <string name="screen_advanced_settings_labs">"Laboratoare"</string>
<string name="screen_advanced_settings_live_location_section_description">"Distanța pe care trebuie să o parcurgeți pentru a declanșa o actualizare."</string>
<string name="screen_advanced_settings_live_location_section_footer">"Asigurați-vă că este activată opțiunea „Locație precisă” pentru această aplicație. Pentru a schimba permisiunea, accesați %1$s."</string>
<string name="screen_advanced_settings_live_location_section_footer_link">"Setările aplicației"</string>
<string name="screen_advanced_settings_live_location_section_title">"Actualizări in timp real ale locației"</string>
<plurals name="screen_advanced_settings_live_location_update_distance">
<item quantity="one">"La fiecare %1$d metru"</item>
<item quantity="few">"La fiecare %1$d metri"</item>
<item quantity="other">"La fiecare %1$d metri"</item>
</plurals>
<string name="screen_advanced_settings_media_compression_description">"Încărcați fotografii și videoclipuri mai rapid și reduceți consumul de date"</string> <string name="screen_advanced_settings_media_compression_description">"Încărcați fotografii și videoclipuri mai rapid și reduceți consumul de date"</string>
<string name="screen_advanced_settings_media_compression_title">"Optimizați calitatea media"</string> <string name="screen_advanced_settings_media_compression_title">"Optimizați calitatea media"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderare și siguranță"</string> <string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderare și siguranță"</string>
@ -78,6 +87,7 @@ Dacă continuați, unele dintre setările dumneavoastră pot fi modificate."</st
<string name="screen_notification_settings_system_notifications_action_required_content_link">"Setări de sistem"</string> <string name="screen_notification_settings_system_notifications_action_required_content_link">"Setări de sistem"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Notificările de sistem sunt dezactivate"</string> <string name="screen_notification_settings_system_notifications_turned_off">"Notificările de sistem sunt dezactivate"</string>
<string name="screen_notification_settings_title">"Notificări"</string> <string name="screen_notification_settings_title">"Notificări"</string>
<string name="theme_black">"Negru"</string>
<string name="theme_dark">"Întunecat"</string> <string name="theme_dark">"Întunecat"</string>
<string name="theme_light">"Deschis"</string> <string name="theme_light">"Deschis"</string>
<string name="theme_system">"Sistem"</string> <string name="theme_system">"Sistem"</string>

View file

@ -78,11 +78,12 @@
<string name="screen_notification_settings_mode_all">"全部"</string> <string name="screen_notification_settings_mode_all">"全部"</string>
<string name="screen_notification_settings_mode_mentions">"提及"</string> <string name="screen_notification_settings_mode_mentions">"提及"</string>
<string name="screen_notification_settings_notification_section_title">"通知我以下类型"</string> <string name="screen_notification_settings_notification_section_title">"通知我以下类型"</string>
<string name="screen_notification_settings_room_mention_label">"我在房间中被提及时通知我"</string> <string name="screen_notification_settings_room_mention_label">"提及所有成员(@room时通知我"</string>
<string name="screen_notification_settings_system_notifications_action_required">"要接收通知,请更改 %1$s。"</string> <string name="screen_notification_settings_system_notifications_action_required">"要接收通知,请更改 %1$s。"</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"系统设置"</string> <string name="screen_notification_settings_system_notifications_action_required_content_link">"系统设置"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"系统通知已关闭"</string> <string name="screen_notification_settings_system_notifications_turned_off">"系统通知已关闭"</string>
<string name="screen_notification_settings_title">"通知"</string> <string name="screen_notification_settings_title">"通知"</string>
<string name="theme_black">"纯黑"</string>
<string name="theme_dark">"深色"</string> <string name="theme_dark">"深色"</string>
<string name="theme_light">"浅色"</string> <string name="theme_light">"浅色"</string>
<string name="theme_system">"系统"</string> <string name="theme_system">"系统"</string>

View file

@ -210,35 +210,12 @@ class AdvancedSettingsPresenterTest {
} }
} }
@Test
fun `present - live location minimum distance is null when feature is disabled`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore(
liveLocationMinimumDistanceUpdate = 50,
)
val featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.LiveLocationSharing, false)
}
val presenter = createAdvancedSettingsPresenter(appPreferencesStore = appPreferencesStore, featureFlagService = featureFlagService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
with(awaitItem()) {
assertThat(liveLocationMinimumDistanceUpdate).isNull()
}
}
}
@Test @Test
fun `present - exposes live location minimum distance from app preferences`() = runTest { fun `present - exposes live location minimum distance from app preferences`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore( val appPreferencesStore = InMemoryAppPreferencesStore(
liveLocationMinimumDistanceUpdate = 50, liveLocationMinimumDistanceUpdate = 50,
) )
val featureFlagService = FakeFeatureFlagService().apply { val presenter = createAdvancedSettingsPresenter(appPreferencesStore = appPreferencesStore)
setFeatureEnabled(FeatureFlags.LiveLocationSharing, true)
}
val presenter = createAdvancedSettingsPresenter(appPreferencesStore = appPreferencesStore, featureFlagService = featureFlagService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -256,10 +233,7 @@ class AdvancedSettingsPresenterTest {
val appPreferencesStore = InMemoryAppPreferencesStore( val appPreferencesStore = InMemoryAppPreferencesStore(
liveLocationMinimumDistanceUpdate = 10, liveLocationMinimumDistanceUpdate = 10,
) )
val featureFlagService = FakeFeatureFlagService().apply { val presenter = createAdvancedSettingsPresenter(appPreferencesStore = appPreferencesStore)
setFeatureEnabled(FeatureFlags.LiveLocationSharing, true)
}
val presenter = createAdvancedSettingsPresenter(appPreferencesStore = appPreferencesStore, featureFlagService = featureFlagService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()

View file

@ -2,7 +2,7 @@
<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_report_room_leave_failed_alert_message">"你的举报已成功提交,但在尝试退出房间时遇到问题。请重试。"</string> <string name="screen_report_room_leave_failed_alert_message">"你的举报已成功提交,但在尝试退出房间时遇到问题。请重试。"</string>
<string name="screen_report_room_leave_failed_alert_title">"无法离开房间"</string> <string name="screen_report_room_leave_failed_alert_title">"无法离开房间"</string>
<string name="screen_report_room_reason_footer">"向管理员举报此房间。如果息已加密,管理员将无法读取。"</string> <string name="screen_report_room_reason_footer">"向管理员举报此房间。如果息已加密,管理员将无法读取。"</string>
<string name="screen_report_room_reason_placeholder">"描述举报的理由…"</string> <string name="screen_report_room_reason_placeholder">"描述举报的理由…"</string>
<string name="screen_report_room_title">"举报房间"</string> <string name="screen_report_room_title">"举报房间"</string>
</resources> </resources>

View file

@ -6,6 +6,7 @@
<string name="screen_room_change_permissions_delete_messages">"Odstranit zprávy"</string> <string name="screen_room_change_permissions_delete_messages">"Odstranit zprávy"</string>
<string name="screen_room_change_permissions_everyone">"Člen"</string> <string name="screen_room_change_permissions_everyone">"Člen"</string>
<string name="screen_room_change_permissions_invite_people">"Pozvat přátele"</string> <string name="screen_room_change_permissions_invite_people">"Pozvat přátele"</string>
<string name="screen_room_change_permissions_live_location">"Sdílet aktuální polohu"</string>
<string name="screen_room_change_permissions_manage_space">"Správa prostoru"</string> <string name="screen_room_change_permissions_manage_space">"Správa prostoru"</string>
<string name="screen_room_change_permissions_manage_space_rooms">"Spravovat místnosti"</string> <string name="screen_room_change_permissions_manage_space_rooms">"Spravovat místnosti"</string>
<string name="screen_room_change_permissions_member_moderation">"Spravovat členy"</string> <string name="screen_room_change_permissions_member_moderation">"Spravovat členy"</string>

View file

@ -6,6 +6,7 @@
<string name="screen_room_change_permissions_delete_messages">"Eemalda sõnumid"</string> <string name="screen_room_change_permissions_delete_messages">"Eemalda sõnumid"</string>
<string name="screen_room_change_permissions_everyone">"Liikmed"</string> <string name="screen_room_change_permissions_everyone">"Liikmed"</string>
<string name="screen_room_change_permissions_invite_people">"Osalejate kutsumine"</string> <string name="screen_room_change_permissions_invite_people">"Osalejate kutsumine"</string>
<string name="screen_room_change_permissions_live_location">"Jaga asukohta reaalajas"</string>
<string name="screen_room_change_permissions_manage_space">"Halda kogukonda"</string> <string name="screen_room_change_permissions_manage_space">"Halda kogukonda"</string>
<string name="screen_room_change_permissions_manage_space_rooms">"Halda jututuba"</string> <string name="screen_room_change_permissions_manage_space_rooms">"Halda jututuba"</string>
<string name="screen_room_change_permissions_member_moderation">"Liikmete haldus"</string> <string name="screen_room_change_permissions_member_moderation">"Liikmete haldus"</string>

View file

@ -6,6 +6,7 @@
<string name="screen_room_change_permissions_delete_messages">"Ștergeți mesajele"</string> <string name="screen_room_change_permissions_delete_messages">"Ștergeți mesajele"</string>
<string name="screen_room_change_permissions_everyone">"Membru"</string> <string name="screen_room_change_permissions_everyone">"Membru"</string>
<string name="screen_room_change_permissions_invite_people">"Invitați persoane"</string> <string name="screen_room_change_permissions_invite_people">"Invitați persoane"</string>
<string name="screen_room_change_permissions_live_location">"Partajați locația în timp real"</string>
<string name="screen_room_change_permissions_manage_space">"Gestionați spațiul"</string> <string name="screen_room_change_permissions_manage_space">"Gestionați spațiul"</string>
<string name="screen_room_change_permissions_manage_space_rooms">"Gestionați camerele"</string> <string name="screen_room_change_permissions_manage_space_rooms">"Gestionați camerele"</string>
<string name="screen_room_change_permissions_member_moderation">"Gestionați membrii"</string> <string name="screen_room_change_permissions_member_moderation">"Gestionați membrii"</string>

View file

@ -40,7 +40,7 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint {
interface Callback : Plugin { interface Callback : Plugin {
fun navigateToGlobalNotificationSettings() fun navigateToGlobalNotificationSettings()
fun navigateToDeveloperSettings() fun navigateToDeveloperSettings()
fun navigateToRoom(roomId: RoomId, serverNames: List<String>) fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean = false)
fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean)
} }

View file

@ -263,7 +263,20 @@ class RoomDetailsFlowNode(
} }
NavTarget.InviteMembers -> { NavTarget.InviteMembers -> {
createNode<RoomInviteMembersNode>(buildContext) val callback = object : RoomInviteMembersNode.Callback {
override fun openCreatedRoom(roomId: RoomId) {
navigateUp()
room.roomCoroutineScope.launch {
callback.navigateToRoom(
roomId = roomId,
serverNames = emptyList(),
// Remove the invite screen from the backstack to avoid navigating back to it after the new room has been created
clearBackStack = true,
)
}
}
}
createNode<RoomInviteMembersNode>(buildContext, plugins = listOf(callback))
} }
is NavTarget.RoomNotificationSettings -> { is NavTarget.RoomNotificationSettings -> {

View file

@ -180,6 +180,7 @@ fun aDmRoomDetailsState(
roomName = roomName, roomName = roomName,
isPublic = false, isPublic = false,
isEncrypted = isEncrypted, isEncrypted = isEncrypted,
canInvite = true,
roomType = RoomDetailsType.Dm(otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored)), roomType = RoomDetailsType.Dm(otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored)),
roomMemberDetailsState = aUserProfileState( roomMemberDetailsState = aUserProfileState(
isBlocked = AsyncData.Success(isDmMemberIgnored), isBlocked = AsyncData.Success(isDmMemberIgnored),

View file

@ -208,8 +208,15 @@ fun RoomDetailsView(
onClick = onSecurityAndPrivacyClick onClick = onSecurityAndPrivacyClick
) )
} }
}
state.roomMemberDetailsState?.let { dmMemberDetails -> state.roomMemberDetailsState?.let { dmMemberDetails ->
if (state.canInvite) {
PreferenceCategory {
InviteItem(onClick = invitePeople)
}
}
PreferenceCategory {
ProfileItem( ProfileItem(
verificationState = dmMemberDetails.verificationState, verificationState = dmMemberDetails.verificationState,
onClick = { onProfileClick(dmMemberDetails.userId) } onClick = { onProfileClick(dmMemberDetails.userId) }
@ -374,14 +381,14 @@ private fun MainActionsSection(
onClick = { onCall(CallIntent.VIDEO) }, onClick = { onCall(CallIntent.VIDEO) },
) )
} }
if (state.canInvite && state.roomType !is RoomDetailsType.Dm) {
MainActionButton(
title = stringResource(CommonStrings.action_invite),
imageVector = CompoundIcons.UserAdd(),
onClick = onInvitePeople,
)
}
if (state.roomType is RoomDetailsType.Room) { if (state.roomType is RoomDetailsType.Room) {
if (state.canInvite) {
MainActionButton(
title = stringResource(CommonStrings.action_invite),
imageVector = CompoundIcons.UserAdd(),
onClick = onInvitePeople,
)
}
// Share CTA should be hidden for DMs // Share CTA should be hidden for DMs
MainActionButton( MainActionButton(
title = stringResource(CommonStrings.action_share), title = stringResource(CommonStrings.action_share),
@ -693,6 +700,17 @@ private fun MembersItem(
) )
} }
@Composable
private fun InviteItem(
onClick: () -> Unit,
) {
ListItem(
headlineContent = { Text(stringResource(R.string.screen_room_details_invite_title)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserAdd())),
onClick = onClick,
)
}
@Composable @Composable
private fun PinnedMessagesItem( private fun PinnedMessagesItem(
pinnedMessagesCount: Int?, pinnedMessagesCount: Int?,

View file

@ -11,6 +11,7 @@ package io.element.android.features.roomdetails.impl.invite
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
@ -19,10 +20,16 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.features.invitepeople.api.InvitePeoplePresenter import io.element.android.features.invitepeople.api.InvitePeoplePresenter
import io.element.android.features.invitepeople.api.InvitePeopleRenderer import io.element.android.features.invitepeople.api.InvitePeopleRenderer
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(RoomScope::class) @ContributesNode(RoomScope::class)
@ -35,6 +42,10 @@ class RoomInviteMembersNode(
room: JoinedRoom, room: JoinedRoom,
invitePeoplePresenterFactory: InvitePeoplePresenter.Factory, invitePeoplePresenterFactory: InvitePeoplePresenter.Factory,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun openCreatedRoom(roomId: RoomId)
}
init { init {
lifecycle.subscribe( lifecycle.subscribe(
onResume = { onResume = {
@ -48,6 +59,8 @@ class RoomInviteMembersNode(
roomId = room.roomId, roomId = room.roomId,
) )
private val callback = plugins.callback<Callback>()
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = invitePeoplePresenter.present() val state = invitePeoplePresenter.present()
@ -59,6 +72,19 @@ class RoomInviteMembersNode(
} }
} }
AsyncActionView(
async = state.createRoomFromDmAction,
onSuccess = { roomId ->
callback.openCreatedRoom(roomId)
},
progressDialog = {
ProgressDialog(text = stringResource(CommonStrings.common_creating_room))
},
onErrorDismiss = {
state.eventSink(InvitePeopleEvents.ClearError)
}
)
RoomInviteMembersView( RoomInviteMembersView(
state = state, state = state,
modifier = modifier, modifier = modifier,

View file

@ -108,8 +108,8 @@
<string name="screen_room_notification_settings_allow_custom">"Zezwalaj na ustawienia niestandardowe"</string> <string name="screen_room_notification_settings_allow_custom">"Zezwalaj na ustawienia niestandardowe"</string>
<string name="screen_room_notification_settings_allow_custom_footnote">"Włączenie tej opcji nadpisze ustawienie domyślne"</string> <string name="screen_room_notification_settings_allow_custom_footnote">"Włączenie tej opcji nadpisze ustawienie domyślne"</string>
<string name="screen_room_notification_settings_custom_settings_title">"Powiadamiaj mnie o tym czacie przez"</string> <string name="screen_room_notification_settings_custom_settings_title">"Powiadamiaj mnie o tym czacie przez"</string>
<string name="screen_room_notification_settings_default_setting_footnote">"Możesz to zmienić w swoim %1$s."</string> <string name="screen_room_notification_settings_default_setting_footnote">"Możesz to zmienić w %1$s."</string>
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"ustawienia globalne"</string> <string name="screen_room_notification_settings_default_setting_footnote_content_link">"ustawieniach globalnych"</string>
<string name="screen_room_notification_settings_default_setting_title">"Ustawienie domyślne"</string> <string name="screen_room_notification_settings_default_setting_title">"Ustawienie domyślne"</string>
<string name="screen_room_notification_settings_edit_remove_setting">"Usuń ustawienia własne"</string> <string name="screen_room_notification_settings_edit_remove_setting">"Usuń ustawienia własne"</string>
<string name="screen_room_notification_settings_error_loading_settings">"Wystąpił błąd podczas ładowania ustawień powiadomień."</string> <string name="screen_room_notification_settings_error_loading_settings">"Wystąpił błąd podczas ładowania ustawień powiadomień."</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">"Membrii noi nu pot vedea istoricul"</string>
<string name="crypto_history_sharing_room_info_shared_badge_content">"Membrii noi pot vedea istoricul"</string>
<string name="crypto_history_sharing_room_info_world_readable_badge_content">"Oricine poate vedea istoricul"</string>
<string name="screen_edit_room_address_room_address_section_footer">"Veți avea nevoie de o adresă pentru a o face vizibilă în directorul public."</string> <string name="screen_edit_room_address_room_address_section_footer">"Veți avea nevoie de o adresă pentru a o face vizibilă în directorul public."</string>
<string name="screen_edit_room_address_title">"Editați adresa"</string> <string name="screen_edit_room_address_title">"Editați adresa"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"A apărut o eroare în timpul actualizării setărilor pentru notificari."</string> <string name="screen_notification_settings_edit_failed_updating_default_mode">"A apărut o eroare în timpul actualizării setărilor pentru notificari."</string>
@ -135,6 +138,7 @@
<string name="screen_security_and_privacy_add_room_address_action">"Adăugați o adresă"</string> <string name="screen_security_and_privacy_add_room_address_action">"Adăugați o adresă"</string>
<string name="screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description">"Oricine se află în spațiile autorizate se poate alătura, dar toți ceilalți trebuie să solicite accesul."</string> <string name="screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description">"Oricine se află în spațiile autorizate se poate alătura, dar toți ceilalți trebuie să solicite accesul."</string>
<string name="screen_security_and_privacy_ask_to_join_option_description">"Toată lumea trebuie să solicite acces."</string> <string name="screen_security_and_privacy_ask_to_join_option_description">"Toată lumea trebuie să solicite acces."</string>
<string name="screen_security_and_privacy_ask_to_join_option_title">"Solicitați să vă alăturați"</string>
<string name="screen_security_and_privacy_ask_to_join_single_space_members_option_description">"Oricine în %1$s se poate alătura, dar toți ceilalți trebuie să solicite acces."</string> <string name="screen_security_and_privacy_ask_to_join_single_space_members_option_description">"Oricine în %1$s se poate alătura, dar toți ceilalți trebuie să solicite acces."</string>
<string name="screen_security_and_privacy_enable_encryption_alert_confirm_button_title">"Da, activați criptarea"</string> <string name="screen_security_and_privacy_enable_encryption_alert_confirm_button_title">"Da, activați criptarea"</string>
<string name="screen_security_and_privacy_enable_encryption_alert_description">"Odată activată, criptarea pentru o cameră nu poate fi dezactivată. Mesajele anterioare vor fi vizibile numai pentru membrii camerei de la momentul la care au fost invitați sau de la momentul la care s-au alăturat camerei. <string name="screen_security_and_privacy_enable_encryption_alert_description">"Odată activată, criptarea pentru o cameră nu poate fi dezactivată. Mesajele anterioare vor fi vizibile numai pentru membrii camerei de la momentul la care au fost invitați sau de la momentul la care s-au alăturat camerei.
@ -145,22 +149,26 @@ Nu recomandăm activarea criptării pentru camerele pe care oricine le poate gă
<string name="screen_security_and_privacy_encryption_section_header">"Criptare"</string> <string name="screen_security_and_privacy_encryption_section_header">"Criptare"</string>
<string name="screen_security_and_privacy_encryption_toggle_title">"Activați criptarea end-to-end"</string> <string name="screen_security_and_privacy_encryption_toggle_title">"Activați criptarea end-to-end"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Oricine se poate alătura."</string> <string name="screen_security_and_privacy_room_access_anyone_option_description">"Oricine se poate alătura."</string>
<string name="screen_security_and_privacy_room_access_footer">"Alegeți membrii căror spații se pot alătura acestei camere fără invitație. %1$s"</string> <string name="screen_security_and_privacy_room_access_anyone_option_title">"Oricine"</string>
<string name="screen_security_and_privacy_room_access_footer">"Alegeți membrii căror spații se pot alătura acestei cameră fără invitație. %1$s"</string>
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Gestionați spațiile"</string> <string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Gestionați spațiile"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Doar persoanele invitate se pot alătura."</string> <string name="screen_security_and_privacy_room_access_invite_only_option_description">"Doar persoanele invitate se pot alătura."</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Doar pe bază de invitație"</string> <string name="screen_security_and_privacy_room_access_invite_only_option_title">"Doar pe bază de invitație"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Acces"</string> <string name="screen_security_and_privacy_room_access_section_header">"Acces"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_multiple_parents_description">"Oricine se află într-un spațiu autorizat poate participa."</string> <string name="screen_security_and_privacy_room_access_space_members_option_multiple_parents_description">"Oricine se află într-un spațiu autorizat poate participa."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_single_parent_description">"Oricine din %1$s se poate alătura."</string> <string name="screen_security_and_privacy_room_access_space_members_option_single_parent_description">"Oricine din %1$s se poate alătura."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Membrii spațiului"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Spațiile nu sunt momentan suportate."</string> <string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Spațiile nu sunt momentan suportate."</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Veți avea nevoie de o adresă pentru a o face vizibilă în directorul public."</string> <string name="screen_security_and_privacy_room_address_section_footer">"Veți avea nevoie de o adresă pentru a o face vizibilă în directorul public."</string>
<string name="screen_security_and_privacy_room_address_section_header">"Adresă"</string> <string name="screen_security_and_privacy_room_address_section_header">"Adresă"</string>
<string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Permiteți găsirea acestei camere prin căutarea în directorul de camere publice al %1$s"</string> <string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Permiteți găsirea acestei camere prin căutarea în directorul de camere publice al %1$s"</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_description">"Permiteți găsirea prin căutarea în directorul public."</string> <string name="screen_security_and_privacy_room_directory_visibility_toggle_description">"Permiteți găsirea prin căutarea în directorul public."</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Vizibilă în directorul de camere publice"</string> <string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Vizibilă în directorul de camere publice"</string>
<string name="screen_security_and_privacy_room_history_anyone_option_title">"Oricine (istoricul este public)"</string>
<string name="screen_security_and_privacy_room_history_section_footer">"Modificările nu vor afecta mesajele anterioare, ci doar pe cele noi. %1$s"</string>
<string name="screen_security_and_privacy_room_history_section_header">"Cine poate citi mesajele anterioare"</string> <string name="screen_security_and_privacy_room_history_section_header">"Cine poate citi mesajele anterioare"</string>
<string name="screen_security_and_privacy_room_history_since_invite_option_title">"Doar pentru membri, de la momentul în care au fost invitați"</string> <string name="screen_security_and_privacy_room_history_since_invite_option_title">"Membri de la momentul invitației"</string>
<string name="screen_security_and_privacy_room_history_since_selecting_option_title">"Doar pentru membri, după selectarea acestei opțiuni"</string> <string name="screen_security_and_privacy_room_history_since_selecting_option_title">"Membri (istoric complet)"</string>
<string name="screen_security_and_privacy_room_publishing_section_footer">"Adresele camerelor sunt modalități de a găsi și accesa camere. Acest lucru vă asigură, de asemenea, că puteți partaja cu ușurință camera dumneavoastră cu alte persoane. <string name="screen_security_and_privacy_room_publishing_section_footer">"Adresele camerelor sunt modalități de a găsi și accesa camere. Acest lucru vă asigură, de asemenea, că puteți partaja cu ușurință camera dumneavoastră cu alte persoane.
Puteți alege să publicați camera în directorul public al camerelor serverului dumneavoastră."</string> Puteți alege să publicați camera în directorul public al camerelor serverului dumneavoastră."</string>
<string name="screen_security_and_privacy_room_publishing_section_header">"Publicare cameră"</string> <string name="screen_security_and_privacy_room_publishing_section_header">"Publicare cameră"</string>

View file

@ -70,7 +70,7 @@ class DefaultRoomDetailsEntryPointTest {
val callback = object : RoomDetailsEntryPoint.Callback { val callback = object : RoomDetailsEntryPoint.Callback {
override fun navigateToGlobalNotificationSettings() = lambdaError() override fun navigateToGlobalNotificationSettings() = lambdaError()
override fun navigateToDeveloperSettings() = lambdaError() override fun navigateToDeveloperSettings() = lambdaError()
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) = lambdaError() override fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean) = lambdaError()
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError() override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError()
override fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError() override fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError()
} }

View file

@ -13,6 +13,8 @@ package io.element.android.features.roomdetails.impl
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.AndroidComposeUiTest import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onLast
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
@ -339,6 +341,25 @@ class RoomDetailsViewTest {
clickOn(R.string.screen_room_details_profile_row_title) clickOn(R.string.screen_room_details_profile_row_title)
} }
} }
@Config(qualifiers = "h1024dp")
@Test
fun `click on invite invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback ->
setRoomDetailView(
state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false),
roomType = RoomDetailsType.Dm(
aDmRoomMember(userId = UserId("@other:local.org")),
),
roomMemberDetailsState = aUserProfileState(userId = A_USER_ID),
canInvite = true,
),
invitePeople = callback,
)
onAllNodesWithText(activity!!.getString(R.string.screen_room_details_invite_title)).onLast().performClick()
}
}
} }
private fun AndroidComposeUiTest<ComponentActivity>.setRoomDetailView( private fun AndroidComposeUiTest<ComponentActivity>.setRoomDetailView(

View file

@ -2,16 +2,17 @@
<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_chat_backup_key_backup_action_disable">"Lülita võtmete varundamine välja"</string> <string name="screen_chat_backup_key_backup_action_disable">"Lülita võtmete varundamine välja"</string>
<string name="screen_chat_backup_key_backup_action_enable">"Lülita võtmete varundamine sisse"</string> <string name="screen_chat_backup_key_backup_action_enable">"Lülita võtmete varundamine sisse"</string>
<string name="screen_chat_backup_key_backup_description">"Salvesta oma krüptoidentiteet ja sõnumite krüptovõtmed turvaliselt serveris. See tagab, et sinu sõnumite ajalugu on alati loetav, ka kõikides uutes seadmetes. %1$s."</string> <string name="screen_chat_backup_key_backup_description">"Salvesta oma digitaalne identiteet ja sõnumite krüptovõtmed turvaliselt serveris. See tagab, et sinu sõnumite ajalugu on alati loetav, ka kõikides uutes seadmetes. %1$s."</string>
<string name="screen_chat_backup_key_backup_title">"Krüptovõtmete varundus"</string> <string name="screen_chat_backup_key_backup_title">"Krüptovõtmete varundus"</string>
<string name="screen_chat_backup_key_storage_disabled_error">"Taastamise seadistamiseks peab võtmehoidla olema sisselülitatud."</string> <string name="screen_chat_backup_key_storage_disabled_error">"Sinu vestluste varundamiseks peab võtmehoidla olema sisselülitatud."</string>
<string name="screen_chat_backup_key_storage_toggle_description">"Laadi siin seadmes leiduvad võtmed üles"</string> <string name="screen_chat_backup_key_storage_toggle_description">"Laadi siin seadmes leiduvad võtmed üles"</string>
<string name="screen_chat_backup_key_storage_toggle_title">"Luba krüptovõtmete salvestamine"</string> <string name="screen_chat_backup_key_storage_toggle_title">"Luba krüptovõtmete salvestamine"</string>
<string name="screen_chat_backup_recovery_action_change">"Muuda taastevõtit"</string> <string name="screen_chat_backup_recovery_action_change">"Muuda taastevõtit"</string>
<string name="screen_chat_backup_recovery_action_change_description">"Kui sa oled kaotanud ligipääsu kõikidele oma olemasolevatele seadmetele, siis sa saad taastevõtme abil taastada ligipääsu oma krüptoidentiteedile ja sõnumite ajaloole."</string> <string name="screen_chat_backup_recovery_action_change_description">"Kui sa oled kaotanud ligipääsu kõikidele oma olemasolevatele seadmetele, siis sa saad taastevõtme abil taastada ligipääsu oma digitaalsele identiteedile ja sõnumite ajaloole."</string>
<string name="screen_chat_backup_recovery_action_confirm">"Sisesta taastevõti"</string> <string name="screen_chat_backup_recovery_action_confirm">"Sisesta taastevõti"</string>
<string name="screen_chat_backup_recovery_action_confirm_description">"Sinu krüptovõtmete varundus pole hetkel enam sünkroonis."</string> <string name="screen_chat_backup_recovery_action_confirm_description">"Sinu krüptovõtmete varundus pole hetkel enam sünkroonis."</string>
<string name="screen_chat_backup_recovery_action_setup">"Seadista andmete taastamine"</string> <string name="screen_chat_backup_recovery_action_setup">"Seadista taastevõti"</string>
<string name="screen_chat_backup_recovery_action_setup_description">"Sinu vestlused on automaatselt varundatud kasutades läbivat krüptimist. Kui peaksid kaotama ligipääsu kõikidele oma seadmetele, siis selle varukoopia taastamiseks ja oma digitaalse identiteedi säilitamiseks, on vaja taastevõtit."</string>
<string name="screen_create_new_recovery_key_list_item_1">"Ava %1$s töölauaga seadmes"</string> <string name="screen_create_new_recovery_key_list_item_1">"Ava %1$s töölauaga seadmes"</string>
<string name="screen_create_new_recovery_key_list_item_2">"Logi uuesti sisse oma kasutajakontole"</string> <string name="screen_create_new_recovery_key_list_item_2">"Logi uuesti sisse oma kasutajakontole"</string>
<string name="screen_create_new_recovery_key_list_item_3">"Kui sul palutakse seadet verifitseerida, vali %1$s"</string> <string name="screen_create_new_recovery_key_list_item_3">"Kui sul palutakse seadet verifitseerida, vali %1$s"</string>
@ -23,12 +24,12 @@
<string name="screen_encryption_reset_bullet_1">"Sinu kasutajakonto andmed, kontaktid, eelistused ja vestluste loend säiluvad"</string> <string name="screen_encryption_reset_bullet_1">"Sinu kasutajakonto andmed, kontaktid, eelistused ja vestluste loend säiluvad"</string>
<string name="screen_encryption_reset_bullet_2">"Sa kaotad seniste sõnumite ajaloo"</string> <string name="screen_encryption_reset_bullet_2">"Sa kaotad seniste sõnumite ajaloo"</string>
<string name="screen_encryption_reset_bullet_3">"Sa pead kõik oma olemasolevad seadmed ja kontaktid uuesti verifitseerima"</string> <string name="screen_encryption_reset_bullet_3">"Sa pead kõik oma olemasolevad seadmed ja kontaktid uuesti verifitseerima"</string>
<string name="screen_encryption_reset_footer">"Lähtesta oma identiteet vaid siis, kui sul pole ligipääsu mitte ühelegi oma seadmele ja sa oled kaotanud oma taastevõtme."</string> <string name="screen_encryption_reset_footer">"Lähtesta oma digitaalne identiteet vaid siis, kui sul pole ligipääsu mitte ühelegi oma seadmele ja sa oled kaotanud oma taastevõtme."</string>
<string name="screen_encryption_reset_title">"Kui sa ühtegi muud võimalust ei leia, siis lähtesta oma identiteet."</string> <string name="screen_encryption_reset_title">"Kui sa ühtegi muud võimalust ei leia, siis lähtesta oma digitaalne identiteet."</string>
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Lülita välja"</string> <string name="screen_key_backup_disable_confirmation_action_turn_off">"Kustuta"</string>
<string name="screen_key_backup_disable_confirmation_description">"Kui sa logid välja kõikidest oma seadmetest, siis sa kaotad ligipääsu oma krüptitud sõnumitele."</string> <string name="screen_key_backup_disable_confirmation_description">"Kui sa eemaldad kõik oma seadmed, siis sa kaotad ligipääsu oma krüptitud sõnumitele ja pead oma digitaalse identiteedi lähtestama."</string>
<string name="screen_key_backup_disable_confirmation_title">"Kas sa oled kindel, et soovid varukoopiate tegemise välja lülitada?"</string> <string name="screen_key_backup_disable_confirmation_title">"Kas oled kindel, et soovid võtmehoidla kustutada?"</string>
<string name="screen_key_backup_disable_description">"Varunduse väljalülitamisel kustutatakse hetkel olemasolev sinu krüptovõtmete varukoopia ning lülituvad välja veel mõned turvafunktsionaalsused. Sellisel juhul sul:"</string> <string name="screen_key_backup_disable_description">"Võtmehoidla kustutamine eemaldab sinu digitaalse identiteedi ja sõnumivõtmed serverist ning lülitab välja järgmised turvafunktsionaalsused:"</string>
<string name="screen_key_backup_disable_description_point_1">"sul ei ole krüptitud sõnumite ajalugu uutes seadmetes"</string> <string name="screen_key_backup_disable_description_point_1">"sul ei ole krüptitud sõnumite ajalugu uutes seadmetes"</string>
<string name="screen_key_backup_disable_description_point_2">"sa kaotad ligipääsu oma krüptitud sõnumitele, kui sa logid kõikjal välja rakendusest %1$s"</string> <string name="screen_key_backup_disable_description_point_2">"sa kaotad ligipääsu oma krüptitud sõnumitele, kui sa logid kõikjal välja rakendusest %1$s"</string>
<string name="screen_key_backup_disable_title">"Kas sa oled kindel, et soovid varunduse välja lülitada?"</string> <string name="screen_key_backup_disable_title">"Kas sa oled kindel, et soovid varunduse välja lülitada?"</string>
@ -58,12 +59,12 @@
<string name="screen_recovery_key_setup_generate_key">"Loo oma taastevõti"</string> <string name="screen_recovery_key_setup_generate_key">"Loo oma taastevõti"</string>
<string name="screen_recovery_key_setup_generate_key_description">"Ära jaga seda kellegagi"</string> <string name="screen_recovery_key_setup_generate_key_description">"Ära jaga seda kellegagi"</string>
<string name="screen_recovery_key_setup_success">"Andmete taastamise seadistamine õnnestus"</string> <string name="screen_recovery_key_setup_success">"Andmete taastamise seadistamine õnnestus"</string>
<string name="screen_recovery_key_setup_title">"Seadista andmete taastamine"</string> <string name="screen_recovery_key_setup_title">"Seadista taastevõti"</string>
<string name="screen_reset_encryption_confirmation_alert_action">"Jah, lähtesta nüüd"</string> <string name="screen_reset_encryption_confirmation_alert_action">"Jah, lähtesta nüüd"</string>
<string name="screen_reset_encryption_confirmation_alert_subtitle">"See tegevus on tagasipöördumatu."</string> <string name="screen_reset_encryption_confirmation_alert_subtitle">"See tegevus on tagasipöördumatu."</string>
<string name="screen_reset_encryption_confirmation_alert_title">"Kas sa oled kindel, et soovid oma võrguidentiteeti lähtestada?"</string> <string name="screen_reset_encryption_confirmation_alert_title">"Kas sa oled kindel, et soovid oma digitaalse identiteedi lähtestada?"</string>
<string name="screen_reset_encryption_password_error">"Tekkis teadmata viga. Palun kontrolli, kas sinu kasutajakonto salasõna on õige ja proovi uuesti."</string> <string name="screen_reset_encryption_password_error">"Tekkis teadmata viga. Palun kontrolli, kas sinu kasutajakonto salasõna on õige ja proovi uuesti."</string>
<string name="screen_reset_encryption_password_placeholder">"Sisesta…"</string> <string name="screen_reset_encryption_password_placeholder">"Sisesta…"</string>
<string name="screen_reset_encryption_password_subtitle">"Palun kinnita, et soovid oma võrguidentiteedi lähtestada."</string> <string name="screen_reset_encryption_password_subtitle">"Palun kinnita, et soovid oma digitaalse identiteedi lähtestada."</string>
<string name="screen_reset_encryption_password_title">"Jätkamaks sisesta oma kasutajakonto salasõna"</string> <string name="screen_reset_encryption_password_title">"Jätkamaks sisesta oma kasutajakonto salasõna"</string>
</resources> </resources>

View file

@ -30,7 +30,7 @@
<string name="screen_key_backup_disable_confirmation_description">"Jeśli usuniesz wszystkie swoje urządzenia, stracisz zaszyfrowaną historię wiadomości i będziesz musiał zresetować swoją tożsamość cyfrową."</string> <string name="screen_key_backup_disable_confirmation_description">"Jeśli usuniesz wszystkie swoje urządzenia, stracisz zaszyfrowaną historię wiadomości i będziesz musiał zresetować swoją tożsamość cyfrową."</string>
<string name="screen_key_backup_disable_confirmation_title">"Czy na pewno chcesz usunąć magazyn kluczy?"</string> <string name="screen_key_backup_disable_confirmation_title">"Czy na pewno chcesz usunąć magazyn kluczy?"</string>
<string name="screen_key_backup_disable_description">"Usunięcie magazynu kluczy usunie Twoją tożsamość cyfrową i klucze wiadomości z serwera, wyłączając następujące funkcje bezpieczeństwa:"</string> <string name="screen_key_backup_disable_description">"Usunięcie magazynu kluczy usunie Twoją tożsamość cyfrową i klucze wiadomości z serwera, wyłączając następujące funkcje bezpieczeństwa:"</string>
<string name="screen_key_backup_disable_description_point_1">"Posiadał historii wiadomości szyfrowanych na nowych urządzeniach"</string> <string name="screen_key_backup_disable_description_point_1">"Stracisz dostęp do zaszyfrowanej historii wiadomości na nowych urządzeniach"</string>
<string name="screen_key_backup_disable_description_point_2">"Utracisz dostęp do wiadomości szyfrowanych, jeśli zostaniesz wszędzie wylogowany z %1$s"</string> <string name="screen_key_backup_disable_description_point_2">"Utracisz dostęp do wiadomości szyfrowanych, jeśli zostaniesz wszędzie wylogowany z %1$s"</string>
<string name="screen_key_backup_disable_title">"Czy na pewno chcesz wyłączyć backup?"</string> <string name="screen_key_backup_disable_title">"Czy na pewno chcesz wyłączyć backup?"</string>
<string name="screen_recovery_key_change_description">"Uzyskaj nowy klucz przywracania, jeśli straciłeś dostęp do obecnego. Po zmianie klucza przywracania stary nie będzie już działał."</string> <string name="screen_recovery_key_change_description">"Uzyskaj nowy klucz przywracania, jeśli straciłeś dostęp do obecnego. Po zmianie klucza przywracania stary nie będzie już działał."</string>

View file

@ -12,6 +12,7 @@
<string name="screen_chat_backup_recovery_action_confirm">"Introduceți cheia de recuperare"</string> <string name="screen_chat_backup_recovery_action_confirm">"Introduceți cheia de recuperare"</string>
<string name="screen_chat_backup_recovery_action_confirm_description">"Backup-ul pentru chat nu este sincronizat în prezent."</string> <string name="screen_chat_backup_recovery_action_confirm_description">"Backup-ul pentru chat nu este sincronizat în prezent."</string>
<string name="screen_chat_backup_recovery_action_setup">"Obțineți cheia de recuperare"</string> <string name="screen_chat_backup_recovery_action_setup">"Obțineți cheia de recuperare"</string>
<string name="screen_chat_backup_recovery_action_setup_description">"Chaturile dumneavoastră sunt salvate automat cu criptare end-to-end. Pentru a restaura această copie de rezervă și a vă păstra identitatea digitală atunci când pierdeți accesul la toate dispozitivele dumneavoastră, veți avea nevoie de cheia de recuperare."</string>
<string name="screen_create_new_recovery_key_list_item_1">"Deschideți %1$s pe un dispozitiv desktop"</string> <string name="screen_create_new_recovery_key_list_item_1">"Deschideți %1$s pe un dispozitiv desktop"</string>
<string name="screen_create_new_recovery_key_list_item_2">"Conectați-vă din nou la contul dumneavoastră"</string> <string name="screen_create_new_recovery_key_list_item_2">"Conectați-vă din nou la contul dumneavoastră"</string>
<string name="screen_create_new_recovery_key_list_item_3">"Când vi se cere să vă verificați dispozitivul, selectați%1$s"</string> <string name="screen_create_new_recovery_key_list_item_3">"Când vi se cere să vă verificați dispozitivul, selectați%1$s"</string>

View file

@ -10,6 +10,7 @@
<string name="screen_security_and_privacy_add_room_address_action">"Adăugați o adresă"</string> <string name="screen_security_and_privacy_add_room_address_action">"Adăugați o adresă"</string>
<string name="screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description">"Oricine se află în spațiile autorizate se poate alătura, dar toți ceilalți trebuie să solicite accesul."</string> <string name="screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description">"Oricine se află în spațiile autorizate se poate alătura, dar toți ceilalți trebuie să solicite accesul."</string>
<string name="screen_security_and_privacy_ask_to_join_option_description">"Toată lumea trebuie să solicite acces."</string> <string name="screen_security_and_privacy_ask_to_join_option_description">"Toată lumea trebuie să solicite acces."</string>
<string name="screen_security_and_privacy_ask_to_join_option_title">"Solicitați să vă alăturați"</string>
<string name="screen_security_and_privacy_ask_to_join_single_space_members_option_description">"Oricine în %1$s se poate alătura, dar toți ceilalți trebuie să solicite acces."</string> <string name="screen_security_and_privacy_ask_to_join_single_space_members_option_description">"Oricine în %1$s se poate alătura, dar toți ceilalți trebuie să solicite acces."</string>
<string name="screen_security_and_privacy_enable_encryption_alert_confirm_button_title">"Da, activați criptarea"</string> <string name="screen_security_and_privacy_enable_encryption_alert_confirm_button_title">"Da, activați criptarea"</string>
<string name="screen_security_and_privacy_enable_encryption_alert_description">"Odată activată, criptarea pentru o cameră nu poate fi dezactivată. Mesajele anterioare vor fi vizibile numai pentru membrii camerei de la momentul la care au fost invitați sau de la momentul la care s-au alăturat camerei. <string name="screen_security_and_privacy_enable_encryption_alert_description">"Odată activată, criptarea pentru o cameră nu poate fi dezactivată. Mesajele anterioare vor fi vizibile numai pentru membrii camerei de la momentul la care au fost invitați sau de la momentul la care s-au alăturat camerei.
@ -20,22 +21,26 @@ Nu recomandăm activarea criptării pentru camerele pe care oricine le poate gă
<string name="screen_security_and_privacy_encryption_section_header">"Criptare"</string> <string name="screen_security_and_privacy_encryption_section_header">"Criptare"</string>
<string name="screen_security_and_privacy_encryption_toggle_title">"Activați criptarea end-to-end"</string> <string name="screen_security_and_privacy_encryption_toggle_title">"Activați criptarea end-to-end"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Oricine se poate alătura."</string> <string name="screen_security_and_privacy_room_access_anyone_option_description">"Oricine se poate alătura."</string>
<string name="screen_security_and_privacy_room_access_footer">"Alegeți membrii căror spații se pot alătura acestei camere fără invitație. %1$s"</string> <string name="screen_security_and_privacy_room_access_anyone_option_title">"Oricine"</string>
<string name="screen_security_and_privacy_room_access_footer">"Alegeți membrii căror spații se pot alătura acestei cameră fără invitație. %1$s"</string>
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Gestionați spațiile"</string> <string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Gestionați spațiile"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Doar persoanele invitate se pot alătura."</string> <string name="screen_security_and_privacy_room_access_invite_only_option_description">"Doar persoanele invitate se pot alătura."</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Doar pe bază de invitație"</string> <string name="screen_security_and_privacy_room_access_invite_only_option_title">"Doar pe bază de invitație"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Acces"</string> <string name="screen_security_and_privacy_room_access_section_header">"Acces"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_multiple_parents_description">"Oricine se află într-un spațiu autorizat poate participa."</string> <string name="screen_security_and_privacy_room_access_space_members_option_multiple_parents_description">"Oricine se află într-un spațiu autorizat poate participa."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_single_parent_description">"Oricine din %1$s se poate alătura."</string> <string name="screen_security_and_privacy_room_access_space_members_option_single_parent_description">"Oricine din %1$s se poate alătura."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Membrii spațiului"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Spațiile nu sunt momentan suportate."</string> <string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Spațiile nu sunt momentan suportate."</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Veți avea nevoie de o adresă pentru a o face vizibilă în directorul public."</string> <string name="screen_security_and_privacy_room_address_section_footer">"Veți avea nevoie de o adresă pentru a o face vizibilă în directorul public."</string>
<string name="screen_security_and_privacy_room_address_section_header">"Adresă"</string> <string name="screen_security_and_privacy_room_address_section_header">"Adresă"</string>
<string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Permiteți găsirea acestei camere prin căutarea în directorul de camere publice al %1$s"</string> <string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Permiteți găsirea acestei camere prin căutarea în directorul de camere publice al %1$s"</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_description">"Permiteți găsirea prin căutarea în directorul public."</string> <string name="screen_security_and_privacy_room_directory_visibility_toggle_description">"Permiteți găsirea prin căutarea în directorul public."</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Vizibilă în directorul de camere publice"</string> <string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Vizibilă în directorul de camere publice"</string>
<string name="screen_security_and_privacy_room_history_anyone_option_title">"Oricine (istoricul este public)"</string>
<string name="screen_security_and_privacy_room_history_section_footer">"Modificările nu vor afecta mesajele anterioare, ci doar pe cele noi. %1$s"</string>
<string name="screen_security_and_privacy_room_history_section_header">"Cine poate citi mesajele anterioare"</string> <string name="screen_security_and_privacy_room_history_section_header">"Cine poate citi mesajele anterioare"</string>
<string name="screen_security_and_privacy_room_history_since_invite_option_title">"Doar pentru membri, de la momentul în care au fost invitați"</string> <string name="screen_security_and_privacy_room_history_since_invite_option_title">"Membri de la momentul invitației"</string>
<string name="screen_security_and_privacy_room_history_since_selecting_option_title">"Doar pentru membri, după selectarea acestei opțiuni"</string> <string name="screen_security_and_privacy_room_history_since_selecting_option_title">"Membri (istoric complet)"</string>
<string name="screen_security_and_privacy_room_publishing_section_footer">"Adresele camerelor sunt modalități de a găsi și accesa camere. Acest lucru vă asigură, de asemenea, că puteți partaja cu ușurință camera dumneavoastră cu alte persoane. <string name="screen_security_and_privacy_room_publishing_section_footer">"Adresele camerelor sunt modalități de a găsi și accesa camere. Acest lucru vă asigură, de asemenea, că puteți partaja cu ușurință camera dumneavoastră cu alte persoane.
Puteți alege să publicați camera în directorul public al camerelor serverului dumneavoastră."</string> Puteți alege să publicați camera în directorul public al camerelor serverului dumneavoastră."</string>
<string name="screen_security_and_privacy_room_publishing_section_header">"Publicare cameră"</string> <string name="screen_security_and_privacy_room_publishing_section_header">"Publicare cameră"</string>

View file

@ -9,10 +9,21 @@
</plurals> </plurals>
<string name="screen_leave_space_subtitle">"Selectați camerele pe care doriți să le părăsiți și în care nu sunteți singurul administrator:"</string> <string name="screen_leave_space_subtitle">"Selectați camerele pe care doriți să le părăsiți și în care nu sunteți singurul administrator:"</string>
<string name="screen_leave_space_subtitle_last_admin">"Trebuie să desemnați un alt administrator pentru acest spațiu înainte de a-l părăsi."</string> <string name="screen_leave_space_subtitle_last_admin">"Trebuie să desemnați un alt administrator pentru acest spațiu înainte de a-l părăsi."</string>
<string name="screen_leave_space_subtitle_last_owner">"Sunteți singurul proprietar al %1$s. Trebuie să transferați dreptul de proprietate către altcineva înainte de a parăsi camera."</string>
<string name="screen_leave_space_subtitle_only_last_admin">"Nu veți părăsi următoarele camere deoarece sunteți singurul administrator:"</string> <string name="screen_leave_space_subtitle_only_last_admin">"Nu veți părăsi următoarele camere deoarece sunteți singurul administrator:"</string>
<string name="screen_leave_space_title">"Părăsiți %1$s?"</string> <string name="screen_leave_space_title">"Părăsiți %1$s?"</string>
<string name="screen_leave_space_title_last_admin">"Sunteți singurul administrator pentru %1$s"</string> <string name="screen_leave_space_title_last_admin">"Sunteți singurul administrator pentru %1$s"</string>
<string name="screen_leave_space_title_last_owner">"Transferați proprietatea"</string>
<string name="screen_space_add_room_action">"Cameră"</string>
<string name="screen_space_add_rooms_room_access_description">"Adăugarea unei camere nu va afecta accesul la cameră. Pentru a modifica accesul, accesați Setări cameră &gt; Securitate și confidențialitate."</string>
<string name="screen_space_empty_state_title">"Adăugați prima dumneavoastră cameră"</string>
<string name="screen_space_menu_action_members">"Vizualizați membrii"</string> <string name="screen_space_menu_action_members">"Vizualizați membrii"</string>
<string name="screen_space_remove_rooms_confirmation_content">"Eliminarea unei camere nu va afecta accesul la aceasta. Pentru a modifica accesul, accesați Informații despre cameră &gt; Confidențialitate și securitate."</string>
<plurals name="screen_space_remove_rooms_confirmation_title">
<item quantity="one">"Eliminați camera %1$d din %2$s"</item>
<item quantity="few">"Eliminați camerele %1$d din %2$s"</item>
<item quantity="other">"Eliminați camerele %1$d din %2$s"</item>
</plurals>
<string name="screen_space_settings_leave_space">"Părăsiți spațiul"</string> <string name="screen_space_settings_leave_space">"Părăsiți spațiul"</string>
<string name="screen_space_settings_roles_and_permissions">"Roluri și permisiuni"</string> <string name="screen_space_settings_roles_and_permissions">"Roluri și permisiuni"</string>
<string name="screen_space_settings_security_and_privacy">"Securitate &amp; confidențialitate"</string> <string name="screen_space_settings_security_and_privacy">"Securitate &amp; confidențialitate"</string>

View file

@ -2,7 +2,7 @@
<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_identity_confirmation_cannot_confirm">"Kas kinnitamine pole võimalik?"</string> <string name="screen_identity_confirmation_cannot_confirm">"Kas kinnitamine pole võimalik?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Loo uus taastevõti"</string> <string name="screen_identity_confirmation_create_new_recovery_key">"Loo uus taastevõti"</string>
<string name="screen_identity_confirmation_subtitle">"Krüptitud sõnumivahetuse tagamiseks verifitseeri see seade."</string> <string name="screen_identity_confirmation_subtitle">"Turvalise sõnumside seadistamiseks vali verifitseerimise viis."</string>
<string name="screen_identity_confirmation_title">"Kinnita oma digitaalne identiteet"</string> <string name="screen_identity_confirmation_title">"Kinnita oma digitaalne identiteet"</string>
<string name="screen_identity_confirmation_use_another_device">"Kasuta teist seadet"</string> <string name="screen_identity_confirmation_use_another_device">"Kasuta teist seadet"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Kasuta taastevõtit"</string> <string name="screen_identity_confirmation_use_recovery_key">"Kasuta taastevõtit"</string>
@ -17,7 +17,7 @@
<string name="screen_session_verification_compare_numbers_subtitle">"Kinnita, et kõik järgnevalt kuvatud numbrid on täpselt samad, mida sa näed oma teises sessioonis."</string> <string name="screen_session_verification_compare_numbers_subtitle">"Kinnita, et kõik järgnevalt kuvatud numbrid on täpselt samad, mida sa näed oma teises sessioonis."</string>
<string name="screen_session_verification_compare_numbers_title">"Võrdle numbreid"</string> <string name="screen_session_verification_compare_numbers_title">"Võrdle numbreid"</string>
<string name="screen_session_verification_complete_subtitle">"Võid nüüd sõnumeid oma teises seadmes turvaliselt saata ja vastu võtta."</string> <string name="screen_session_verification_complete_subtitle">"Võid nüüd sõnumeid oma teises seadmes turvaliselt saata ja vastu võtta."</string>
<string name="screen_session_verification_complete_user_subtitle">"Nüüd sa võid sõnumite vastuvõtmisel ja saatmisel selle kasutaja identiteeti usaldada."</string> <string name="screen_session_verification_complete_user_subtitle">"Nüüd sa võid sõnumite vastuvõtmisel ja saatmisel selle kasutaja digitaalset identiteeti usaldada."</string>
<string name="screen_session_verification_device_verified">"Seade on verifitseeritud"</string> <string name="screen_session_verification_device_verified">"Seade on verifitseeritud"</string>
<string name="screen_session_verification_enter_recovery_key">"Sisesta taastevõti"</string> <string name="screen_session_verification_enter_recovery_key">"Sisesta taastevõti"</string>
<string name="screen_session_verification_failed_subtitle">"Kas verifitseerimine aegus, teine osapool keeldus vastamast või tekkis vastuste mittevastavus."</string> <string name="screen_session_verification_failed_subtitle">"Kas verifitseerimine aegus, teine osapool keeldus vastamast või tekkis vastuste mittevastavus."</string>
@ -42,7 +42,7 @@
<string name="screen_session_verification_use_another_device_title">"Ava rakendus teises verifitseeritud seadmes"</string> <string name="screen_session_verification_use_another_device_title">"Ava rakendus teises verifitseeritud seadmes"</string>
<string name="screen_session_verification_user_initiator_subtitle">"Lisaturvalisuse nimel verifitseeri seee kasutaja, võrreldes oma seadmetes olevaid emojisid. Tee seda, kasutades usaldusväärset suhtlusviisi."</string> <string name="screen_session_verification_user_initiator_subtitle">"Lisaturvalisuse nimel verifitseeri seee kasutaja, võrreldes oma seadmetes olevaid emojisid. Tee seda, kasutades usaldusväärset suhtlusviisi."</string>
<string name="screen_session_verification_user_initiator_title">"Kas verifitseerime selle kasutaja?"</string> <string name="screen_session_verification_user_initiator_title">"Kas verifitseerime selle kasutaja?"</string>
<string name="screen_session_verification_user_responder_subtitle">"Lisaturvalisuse nimel soovib teine kasutaja sinu identiteeti verifitseerida. Järgmiseks näed sa emojisid, mida peate omavahel võrdlema."</string> <string name="screen_session_verification_user_responder_subtitle">"Lisaturvalisuse nimel soovib teine kasutaja sinu digitaalse identiteeti verifitseerida. Järgmiseks näed sa emojisid, mida peate omavahel võrdlema."</string>
<string name="screen_session_verification_waiting_another_device_subtitle">"Sa peaksid teises seadmes nägema hüpikakent. Palun alusta sealt verifitseerimist."</string> <string name="screen_session_verification_waiting_another_device_subtitle">"Sa peaksid teises seadmes nägema hüpikakent. Palun alusta sealt verifitseerimist."</string>
<string name="screen_session_verification_waiting_another_device_title">"Alusta verifitseerimist teises seadmes"</string> <string name="screen_session_verification_waiting_another_device_title">"Alusta verifitseerimist teises seadmes"</string>
<string name="screen_session_verification_waiting_other_device_title">"Alusta verifitseerimist teises seadmes"</string> <string name="screen_session_verification_waiting_other_device_title">"Alusta verifitseerimist teises seadmes"</string>
@ -50,5 +50,5 @@
<string name="screen_session_verification_waiting_subtitle">"Kui oled nõustunud, siis saad sa verifitseerimist jätkata."</string> <string name="screen_session_verification_waiting_subtitle">"Kui oled nõustunud, siis saad sa verifitseerimist jätkata."</string>
<string name="screen_session_verification_waiting_to_accept_subtitle">"Jätkamaks nõustu verifitseerimisprotsessi alustamisega oma teises sessioonis."</string> <string name="screen_session_verification_waiting_to_accept_subtitle">"Jätkamaks nõustu verifitseerimisprotsessi alustamisega oma teises sessioonis."</string>
<string name="screen_session_verification_waiting_to_accept_title">"Ootame nõustumist verifitseerimispäringuga"</string> <string name="screen_session_verification_waiting_to_accept_title">"Ootame nõustumist verifitseerimispäringuga"</string>
<string name="screen_signout_in_progress_dialog_content">"Logime välja…"</string> <string name="screen_signout_in_progress_dialog_content">"Eemaldan seadet…"</string>
</resources> </resources>

View file

@ -17,7 +17,7 @@
<string name="screen_session_verification_compare_numbers_subtitle">"Confirmați că numerele de mai jos se potrivesc cu cele afișate în cealaltă sesiune."</string> <string name="screen_session_verification_compare_numbers_subtitle">"Confirmați că numerele de mai jos se potrivesc cu cele afișate în cealaltă sesiune."</string>
<string name="screen_session_verification_compare_numbers_title">"Comparați numerele"</string> <string name="screen_session_verification_compare_numbers_title">"Comparați numerele"</string>
<string name="screen_session_verification_complete_subtitle">"Noua dumneavoastră sesiune este acum verificată. Are acces la mesajele dumneavoastră criptate, iar ceilalti utilizatori vă vor vedea ca fiind de încredere."</string> <string name="screen_session_verification_complete_subtitle">"Noua dumneavoastră sesiune este acum verificată. Are acces la mesajele dumneavoastră criptate, iar ceilalti utilizatori vă vor vedea ca fiind de încredere."</string>
<string name="screen_session_verification_complete_user_subtitle">"Acum puteți avea încredere în identitatea acestui utilizator atunci când trimiteți sau primiți mesaje."</string> <string name="screen_session_verification_complete_user_subtitle">"Acum puteți avea încredere în identitatea digitală a acestui utilizator atunci când trimiteți sau primiți mesaje."</string>
<string name="screen_session_verification_device_verified">"Dispozitiv verificat"</string> <string name="screen_session_verification_device_verified">"Dispozitiv verificat"</string>
<string name="screen_session_verification_enter_recovery_key">"Introduceți cheia de recuperare"</string> <string name="screen_session_verification_enter_recovery_key">"Introduceți cheia de recuperare"</string>
<string name="screen_session_verification_failed_subtitle">"Fie cererea a expirat, cererea a fost respinsă, fie a existat o nepotrivire de verificare."</string> <string name="screen_session_verification_failed_subtitle">"Fie cererea a expirat, cererea a fost respinsă, fie a existat o nepotrivire de verificare."</string>
@ -42,7 +42,7 @@
<string name="screen_session_verification_use_another_device_title">"Deschideți aplicația pe un alt dispozitiv verificat"</string> <string name="screen_session_verification_use_another_device_title">"Deschideți aplicația pe un alt dispozitiv verificat"</string>
<string name="screen_session_verification_user_initiator_subtitle">"Pentru securitate suplimentară, verificați acest utilizator comparând un set de emoji-uri pe dispozitivele dvs. Faceți acest lucru utilizând o metodă de comunicare de încredere."</string> <string name="screen_session_verification_user_initiator_subtitle">"Pentru securitate suplimentară, verificați acest utilizator comparând un set de emoji-uri pe dispozitivele dvs. Faceți acest lucru utilizând o metodă de comunicare de încredere."</string>
<string name="screen_session_verification_user_initiator_title">"Verificați acest utilizator?"</string> <string name="screen_session_verification_user_initiator_title">"Verificați acest utilizator?"</string>
<string name="screen_session_verification_user_responder_subtitle">"Pentru o securitate suplimentară, un alt utilizator dorește să vă verifice identitatea. Vi se va afișa un set de emoji-uri pentru comparație."</string> <string name="screen_session_verification_user_responder_subtitle">"Pentru o securitate sporită, un alt utilizator dorește să vă verifice identitatea digitală. Vi se va afișa un set de emoji-uri pentru comparație."</string>
<string name="screen_session_verification_waiting_another_device_subtitle">"Ar trebui să vedeți o fereastră pop-up pe celălalt dispozitiv. Începeți verificarea de acolo acum."</string> <string name="screen_session_verification_waiting_another_device_subtitle">"Ar trebui să vedeți o fereastră pop-up pe celălalt dispozitiv. Începeți verificarea de acolo acum."</string>
<string name="screen_session_verification_waiting_another_device_title">"Începeți verificarea pe celălalt dispozitiv"</string> <string name="screen_session_verification_waiting_another_device_title">"Începeți verificarea pe celălalt dispozitiv"</string>
<string name="screen_session_verification_waiting_other_device_title">"Începeți verificarea pe celălalt dispozitiv"</string> <string name="screen_session_verification_waiting_other_device_title">"Începeți verificarea pe celălalt dispozitiv"</string>

View file

@ -18,14 +18,14 @@ constraintlayout_compose = "1.1.1"
lifecycle = "2.10.0" lifecycle = "2.10.0"
activity = "1.13.0" activity = "1.13.0"
media3 = "1.10.0" media3 = "1.10.0"
camera = "1.6.0" camera = "1.6.1"
work = "2.11.2" work = "2.11.2"
# Compose # Compose
compose_bom = "2026.04.01" compose_bom = "2026.05.00"
# Coroutines # Coroutines
coroutines = "1.10.2" coroutines = "1.11.0"
# Accompanist # Accompanist
accompanist = "0.37.3" accompanist = "0.37.3"
@ -35,7 +35,7 @@ test_core = "1.7.0"
roborazzi = "1.60.0" roborazzi = "1.60.0"
# Jetbrain # Jetbrain
datetime = "0.7.1" datetime = "0.8.0"
serialization_json = "1.11.0" serialization_json = "1.11.0"
#other #other
@ -80,7 +80,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.12.0" google_firebase_bom = "com.google.firebase:firebase-bom:34.13.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" }
@ -111,13 +111,14 @@ androidx_media3_ui = { module = "androidx.media3:media3-ui", version.ref = "medi
androidx_media3_transformer = { module = "androidx.media3:media3-transformer", version.ref = "media3" } androidx_media3_transformer = { module = "androidx.media3:media3-transformer", version.ref = "media3" }
androidx_media3_effect = { module = "androidx.media3:media3-effect", version.ref = "media3" } androidx_media3_effect = { module = "androidx.media3:media3-effect", version.ref = "media3" }
androidx_media3_common = { module = "androidx.media3:media3-common", version.ref = "media3" } androidx_media3_common = { module = "androidx.media3:media3-common", version.ref = "media3" }
androidx_media3_exoplayer_midi = { module = "androidx.media3:media3-exoplayer-midi", version.ref = "media3" }
androidx_biometric = "androidx.biometric:biometric-ktx:1.4.0-alpha02" androidx_biometric = "androidx.biometric:biometric-ktx:1.4.0-alpha02"
androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" } androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" }
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" } androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
androidx_startup = "androidx.startup:startup-runtime:1.2.0" androidx_startup = "androidx.startup:startup-runtime:1.2.0"
androidx_preference = "androidx.preference:preference:1.2.1" androidx_preference = "androidx.preference:preference:1.2.1"
androidx_webkit = "androidx.webkit:webkit:1.15.0" androidx_webkit = "androidx.webkit:webkit:1.16.0"
androidx_compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom" } androidx_compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom" }
androidx_compose_material3 = { module = "androidx.compose.material3:material3", version = '1.5.0-alpha15' } androidx_compose_material3 = { module = "androidx.compose.material3:material3", version = '1.5.0-alpha15' }
@ -178,7 +179,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.05.13" matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.05.18"
# Others # Others
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
@ -222,7 +223,7 @@ color_picker = "io.mhssn:colorpicker:1.0.0"
# Analytics # Analytics
posthog = "com.posthog:posthog-android:3.43.0" posthog = "com.posthog:posthog-android:3.43.0"
sentry = "io.sentry:sentry-android:8.40.0" sentry = "io.sentry:sentry-android:8.41.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.33.2" matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.33.2"

View file

@ -29,7 +29,7 @@
<string name="state_event_room_invite_by_you">"Zaprosiłeś %1$s"</string> <string name="state_event_room_invite_by_you">"Zaprosiłeś %1$s"</string>
<string name="state_event_room_invite_you">"%1$s zaprosił Cię"</string> <string name="state_event_room_invite_you">"%1$s zaprosił Cię"</string>
<string name="state_event_room_join">"%1$s dołączył do pokoju"</string> <string name="state_event_room_join">"%1$s dołączył do pokoju"</string>
<string name="state_event_room_join_by_you">"Dołączyłeś(aś) do pokoju"</string> <string name="state_event_room_join_by_you">"Dołączyłeś do pokoju"</string>
<string name="state_event_room_knock">"%1$s prosi o możliwość dołączenia"</string> <string name="state_event_room_knock">"%1$s prosi o możliwość dołączenia"</string>
<string name="state_event_room_knock_accepted">"%1$s zezwolił %2$s na dołączenie"</string> <string name="state_event_room_knock_accepted">"%1$s zezwolił %2$s na dołączenie"</string>
<string name="state_event_room_knock_accepted_by_you">"Zezwoliłeś %1$s na dołączenie"</string> <string name="state_event_room_knock_accepted_by_you">"Zezwoliłeś %1$s na dołączenie"</string>

View file

@ -93,13 +93,6 @@ enum class FeatureFlags(
defaultValue = { false }, defaultValue = { false },
isFinished = false, isFinished = false,
), ),
SignInWithClassic(
key = "feature.signin_with_classic",
title = "Sign in with Element Classic",
description = "Allow the application to sign in to the current Element Classic account.",
defaultValue = { false },
isFinished = false,
),
AllowBlackTheme( AllowBlackTheme(
key = "feature.allow_black_theme", key = "feature.allow_black_theme",
title = "Black theme", title = "Black theme",
@ -107,13 +100,6 @@ enum class FeatureFlags(
defaultValue = { false }, defaultValue = { false },
isFinished = false, isFinished = false,
), ),
LiveLocationSharing(
key = "feature.liveLocationSharing",
title = "Live location sharing",
description = "Allow sharing live location in rooms.",
defaultValue = { false },
isFinished = false,
),
ValidateNetworkWhenSchedulingNotificationFetching( ValidateNetworkWhenSchedulingNotificationFetching(
key = "feature.validate_network_when_scheduling_notification_fetching", key = "feature.validate_network_when_scheduling_notification_fetching",
title = "Validate internet connectivity when scheduling notification fetching", title = "Validate internet connectivity when scheduling notification fetching",

View file

@ -434,13 +434,30 @@ class RustMatrixAuthenticationService(
qrCodeData: QrCodeData, qrCodeData: QrCodeData,
): Client { ): Client {
Timber.d("Creating client for QR Code login with simplified sliding sync") Timber.d("Creating client for QR Code login with simplified sliding sync")
// The 2025 version of MSC4108 provides baseUrl; the 2024 version has null baseUrl and uses
// serverName instead, which can be null or malformed. We only enforce presence/non-blankness
// here and rely on serverNameOrHomeserverUrl()/the Rust builder layer to validate structure.
val baseUrlOrServerName = qrCodeData.baseUrl() ?: qrCodeData.serverName()
if (baseUrlOrServerName == null) {
// With the 2024 version of MSC4108 we treat the absence of serverName as meaning that
// the other device is not signed in.
Timber.e("The QR code is from a device that is not yet signed in")
throw HumanQrLoginException.OtherDeviceNotSignedIn()
}
if (baseUrlOrServerName.isBlank()) {
Timber.e("The QR code contains an empty base URL or server name, which is invalid")
throw HumanQrLoginException.Unknown()
}
return rustMatrixClientFactory return rustMatrixClientFactory
.getBaseClientBuilder( .getBaseClientBuilder(
sessionPaths = sessionPaths, sessionPaths = sessionPaths,
passphrase = pendingPassphrase, passphrase = pendingPassphrase,
slidingSyncType = ClientBuilderSlidingSync.Discovered, slidingSyncType = ClientBuilderSlidingSync.Discovered,
) )
.serverNameOrHomeserverUrl(qrCodeData.serverName()!!) .serverNameOrHomeserverUrl(baseUrlOrServerName)
.build() .build()
} }

View file

@ -16,25 +16,25 @@ import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
internal fun RoomListEntriesUpdate.describe(): String { internal fun RoomListEntriesUpdate.describe(): String {
return when (this) { return when (this) {
is RoomListEntriesUpdate.Set -> { is RoomListEntriesUpdate.Set -> {
"Set #$index to '${value.displayName()}'" "Set #$index to '${value.id()}'"
} }
is RoomListEntriesUpdate.Append -> { is RoomListEntriesUpdate.Append -> {
"Append ${values.map { "'" + it.displayName() + "'" }}" "Append ${values.map { "'" + it.id() + "'" }}"
} }
is RoomListEntriesUpdate.PushBack -> { is RoomListEntriesUpdate.PushBack -> {
"PushBack '${value.displayName()}'" "PushBack '${value.id()}'"
} }
is RoomListEntriesUpdate.PushFront -> { is RoomListEntriesUpdate.PushFront -> {
"PushFront '${value.displayName()}'" "PushFront '${value.id()}'"
} }
is RoomListEntriesUpdate.Insert -> { is RoomListEntriesUpdate.Insert -> {
"Insert at #$index: '${value.displayName()}'" "Insert at #$index: '${value.id()}'"
} }
is RoomListEntriesUpdate.Remove -> { is RoomListEntriesUpdate.Remove -> {
"Remove #$index" "Remove #$index"
} }
is RoomListEntriesUpdate.Reset -> { is RoomListEntriesUpdate.Reset -> {
"Reset all to ${values.map { "'" + it.displayName() + "'" }}" "Reset all to ${values.map { "'" + it.id() + "'" }}"
} }
RoomListEntriesUpdate.PopBack -> { RoomListEntriesUpdate.PopBack -> {
"PopBack" "PopBack"

View file

@ -112,6 +112,11 @@ class RoomSummaryListProcessor(
private suspend fun updateRoomSummaries(updates: List<RoomListEntriesUpdate>, block: suspend MutableList<RoomSummary>.() -> Unit) = withContext( private suspend fun updateRoomSummaries(updates: List<RoomListEntriesUpdate>, block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(
coroutineContext coroutineContext
) { ) {
// Capture the description before applying updates: applyUpdate consumes each Room via
// `entry.use { ... }` which destroys it, and the duplicate-detection branch below reads
// id() through `describe()`. Without this capture the trackError call crashes before it
// can be reported.
val updatesDescription = updates.description()
mutex.withLock { mutex.withLock {
val current = roomSummaries.replayCache.lastOrNull() val current = roomSummaries.replayCache.lastOrNull()
val mutableRoomSummaries = current.orEmpty().toMutableList() val mutableRoomSummaries = current.orEmpty().toMutableList()
@ -126,7 +131,7 @@ class RoomSummaryListProcessor(
analyticsService.trackError( analyticsService.trackError(
IllegalStateException( IllegalStateException(
"Found duplicates in room summaries after a list update from the SDK: $duplicates. " + "Found duplicates in room summaries after a list update from the SDK: $duplicates. " +
"Updates: ${updates.description()}" "Updates: $updatesDescription"
) )
) )
} }

View file

@ -40,7 +40,7 @@ class FakeFfiSpaceRoomList(
return paginationStateResult() return paginationStateResult()
} }
override fun rooms(): List<SpaceRoom> { override suspend fun rooms(): List<SpaceRoom> {
return roomsResult() return roomsResult()
} }
@ -53,7 +53,7 @@ class FakeFfiSpaceRoomList(
spaceRoomListPaginationStateListener?.onUpdate(state) spaceRoomListPaginationStateListener?.onUpdate(state)
} }
override fun subscribeToRoomUpdate(listener: SpaceRoomListEntriesListener): TaskHandle { override suspend fun subscribeToRoomUpdate(listener: SpaceRoomListEntriesListener): TaskHandle {
spaceRoomListEntriesListener = listener spaceRoomListEntriesListener = listener
return FakeFfiTaskHandle() return FakeFfiTaskHandle()
} }

View file

@ -173,16 +173,68 @@ class RoomSummaryListProcessorTest {
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_3) assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_3)
} }
/**
* Tracking issue #4182 / #5031: rooms duplicated in the room list.
*
* If duplicates are present in the upstream summaries flow, the dedupe safety net in
* [RoomSummaryListProcessor.updateRoomSummaries] must remove them and report the incident via
* [analyticsService.trackError]. Uses an empty update to drive the dedupe path without
* passing a Rust Room through the destroy-on-use path.
*/
@Test
fun `pre-existing duplicates in summaries are deduped on next update and trackError fires`() = runTest {
summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(roomId = A_ROOM_ID), // simulated SDK-side leak
aRoomSummary(roomId = A_ROOM_ID_2),
)
val analyticsService = FakeAnalyticsService()
val processor = createProcessor(analyticsService = analyticsService)
processor.postUpdate(emptyList())
assertThat(summaries.value.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder()
assertThat(analyticsService.trackedErrors).hasSize(1)
}
/**
* Tracking issue #4182 / #5031.
*
* Insert is the most likely Rust-SDK trigger for a duplicate-room report: it blindly inserts
* a new entry at an index without checking whether the roomId already exists. Before the
* describe-capture fix, the dedupe branch in [updateRoomSummaries] would call `Room.id()`
* on an already-destroyed Room (because [applyUpdate] consumes each value via
* `entry.use { ... }`) and crash before [trackError] could be invoked. This test guards the
* fix: the Insert is processed, the list is emitted deduplicated, and the tracked error
* message carries the human-readable description of the offending update.
*/
@Test
fun `Insert that triggers dedupe is reported via trackError without crashing`() = runTest {
summaries.value = listOf(aRoomSummary(roomId = A_ROOM_ID))
val analyticsService = FakeAnalyticsService()
val processor = createProcessor(analyticsService = analyticsService)
processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(0u, aRustRoom(A_ROOM_ID))))
assertThat(summaries.value.map { it.roomId }).containsExactly(A_ROOM_ID)
assertThat(analyticsService.trackedErrors).hasSize(1)
val message = analyticsService.trackedErrors.single().message.orEmpty()
assertThat(message).contains("Found duplicates")
assertThat(message).contains("Insert at #0")
}
private fun aRustRoom(roomId: RoomId = A_ROOM_ID) = FakeFfiRoom( private fun aRustRoom(roomId: RoomId = A_ROOM_ID) = FakeFfiRoom(
roomId = roomId, roomId = roomId,
latestEventLambda = { LatestEventValue.None } latestEventLambda = { LatestEventValue.None }
) )
private fun TestScope.createProcessor() = RoomSummaryListProcessor( private fun TestScope.createProcessor(
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
) = RoomSummaryListProcessor(
summaries, summaries,
FakeFfiRoomListService(), FakeFfiRoomListService(),
coroutineContext = StandardTestDispatcher(testScheduler), coroutineContext = StandardTestDispatcher(testScheduler),
roomSummaryFactory = RoomSummaryFactory(), roomSummaryFactory = RoomSummaryFactory(),
analyticsService = FakeAnalyticsService(), analyticsService = analyticsService,
) )
} }

View file

@ -3,6 +3,7 @@
<string name="screen_bottom_sheet_create_dm_confirmation_button_title">"Saada kutse"</string> <string name="screen_bottom_sheet_create_dm_confirmation_button_title">"Saada kutse"</string>
<string name="screen_bottom_sheet_create_dm_message">"Kas sa soovid alustada vestlust kasutajaga %1$s?"</string> <string name="screen_bottom_sheet_create_dm_message">"Kas sa soovid alustada vestlust kasutajaga %1$s?"</string>
<string name="screen_bottom_sheet_create_dm_title">"Kas saadame kutse?"</string> <string name="screen_bottom_sheet_create_dm_title">"Kas saadame kutse?"</string>
<string name="screen_bottom_sheet_create_dm_unknown_user_content">"Sul pole hetkel selle inimesega ühtegi vestlust. Enne jätkamist kinnita talle kutse saatmine."</string>
<string name="screen_bottom_sheet_create_dm_unknown_user_title">"Kas alustad vestlust selle uue kontaktiga?"</string> <string name="screen_bottom_sheet_create_dm_unknown_user_title">"Kas alustad vestlust selle uue kontaktiga?"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) saatis sulle kutse"</string> <string name="screen_invites_invited_you">"%1$s (%2$s) saatis sulle kutse"</string>
</resources> </resources>

View file

@ -3,5 +3,7 @@
<string name="screen_bottom_sheet_create_dm_confirmation_button_title">"Trimiteți invitația"</string> <string name="screen_bottom_sheet_create_dm_confirmation_button_title">"Trimiteți invitația"</string>
<string name="screen_bottom_sheet_create_dm_message">"Doriți să începeți o discuție cu %1$s?"</string> <string name="screen_bottom_sheet_create_dm_message">"Doriți să începeți o discuție cu %1$s?"</string>
<string name="screen_bottom_sheet_create_dm_title">"Trimiteți invitația?"</string> <string name="screen_bottom_sheet_create_dm_title">"Trimiteți invitația?"</string>
<string name="screen_bottom_sheet_create_dm_unknown_user_content">"În prezent, nu aveți nicio chat cu această persoană. Confirmați invitația înainte de a continua."</string>
<string name="screen_bottom_sheet_create_dm_unknown_user_title">"Începeți o conversație cu acest nou contact?"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) v-a invitat."</string> <string name="screen_invites_invited_you">"%1$s (%2$s) v-a invitat."</string>
</resources> </resources>

View file

@ -31,6 +31,7 @@ dependencies {
implementation(libs.coroutines.core) implementation(libs.coroutines.core)
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.androidx.media3.exoplayer) implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.exoplayer.midi)
implementation(libs.androidx.media3.ui) implementation(libs.androidx.media3.ui)
implementation(libs.telephoto.zoomableimage) implementation(libs.telephoto.zoomableimage)
implementation(libs.vanniktech.blurhash) implementation(libs.vanniktech.blurhash)

View file

@ -88,7 +88,7 @@ fun MediaAudioView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isDisplayed: Boolean = true, isDisplayed: Boolean = true,
) { ) {
val exoPlayer = rememberExoPlayer() val exoPlayer = rememberExoPlayer(forAudioOnly = true)
ExoPlayerMediaAudioView( ExoPlayerMediaAudioView(
isDisplayed = isDisplayed, isDisplayed = isDisplayed,
localMediaViewState = localMediaViewState, localMediaViewState = localMediaViewState,

View file

@ -8,14 +8,18 @@
package io.element.android.libraries.mediaviewer.impl.local.player package io.element.android.libraries.mediaviewer.impl.local.player
import androidx.annotation.OptIn
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
@OptIn(UnstableApi::class)
@Composable @Composable
fun rememberExoPlayer(): ExoPlayer { fun rememberExoPlayer(forAudioOnly: Boolean): ExoPlayer {
return if (LocalInspectionMode.current) { return if (LocalInspectionMode.current) {
remember { remember {
ExoPlayerForPreview() ExoPlayerForPreview()
@ -23,7 +27,14 @@ fun rememberExoPlayer(): ExoPlayer {
} else { } else {
val context = LocalContext.current val context = LocalContext.current
remember { remember {
ExoPlayer.Builder(context).build() if (forAudioOnly) {
// Required for media3-exoplayer-midi to decode MIDI samples produced by DefaultExtractorsFactory.
val renderersFactory = DefaultRenderersFactory(context)
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
ExoPlayer.Builder(context, renderersFactory).build()
} else {
ExoPlayer.Builder(context).build()
}
} }
} }
} }

View file

@ -73,7 +73,7 @@ fun MediaVideoView(
audioFocus: AudioFocus?, audioFocus: AudioFocus?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val exoPlayer = rememberExoPlayer() val exoPlayer = rememberExoPlayer(forAudioOnly = false)
ExoPlayerMediaVideoView( ExoPlayerMediaVideoView(
isDisplayed = isDisplayed, isDisplayed = isDisplayed,
localMediaViewState = localMediaViewState, localMediaViewState = localMediaViewState,

View file

@ -19,6 +19,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.core.extensions.mapCatchingExceptions
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint.MediaViewerMode import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint.MediaViewerMode
import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMedia
@ -40,6 +41,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
class MediaViewerDataSource( class MediaViewerDataSource(
mode: MediaViewerMode, mode: MediaViewerMode,
@ -51,7 +53,7 @@ class MediaViewerDataSource(
private val pagerKeysHandler: PagerKeysHandler, private val pagerKeysHandler: PagerKeysHandler,
) { ) {
// List of media files that are currently being loaded // List of media files that are currently being loaded
private val mediaFiles: MutableList<MediaFile> = mutableListOf() private val mediaFiles: ConcurrentHashMap<MediaSource, MediaFile> = ConcurrentHashMap()
private val galleryMode = when (mode) { private val galleryMode = when (mode) {
MediaViewerMode.SingleMedia, MediaViewerMode.SingleMedia,
@ -69,7 +71,7 @@ class MediaViewerDataSource(
fun dispose() { fun dispose() {
Timber.d("Disposing MediaViewerDataSource, closing ${mediaFiles.size} media files") Timber.d("Disposing MediaViewerDataSource, closing ${mediaFiles.size} media files")
mediaFiles.forEach { it.close() } mediaFiles.values.forEach { it.close() }
mediaFiles.clear() mediaFiles.clear()
localMediaStates.clear() localMediaStates.clear()
} }
@ -163,6 +165,12 @@ class MediaViewerDataSource(
} }
suspend fun loadMedia(data: MediaViewerPageData.MediaViewerData) { suspend fun loadMedia(data: MediaViewerPageData.MediaViewerData) {
val currentState = localMediaStates[data.mediaSource.safeUrl]?.value
// If the media is already loading or has been loaded successfully, do nothing
if (currentState?.isLoading() == true || currentState?.isSuccess() == true) {
return
}
Timber.d("loadMedia for ${data.eventId}") Timber.d("loadMedia for ${data.eventId}")
val localMediaState = localMediaStates.getOrPut(data.mediaSource.safeUrl) { val localMediaState = localMediaStates.getOrPut(data.mediaSource.safeUrl) {
mutableStateOf(AsyncData.Uninitialized) mutableStateOf(AsyncData.Uninitialized)
@ -175,7 +183,7 @@ class MediaViewerDataSource(
filename = data.mediaInfo.filename filename = data.mediaInfo.filename
) )
.onSuccess { mediaFile -> .onSuccess { mediaFile ->
mediaFiles.add(mediaFile) mediaFiles[data.mediaSource] = mediaFile
} }
.mapCatchingExceptions { mediaFile -> .mapCatchingExceptions { mediaFile ->
localMediaFactory.createFromMediaFile( localMediaFactory.createFromMediaFile(
@ -190,4 +198,12 @@ class MediaViewerDataSource(
localMediaState.value = AsyncData.Failure(it) localMediaState.value = AsyncData.Failure(it)
} }
} }
fun cancelLoadingMedia(data: MediaViewerPageData.MediaViewerData) {
if (localMediaStates[data.mediaSource.safeUrl]?.value?.isLoading() == true) {
Timber.d("cancelLoadingMedia for ${data.eventId}")
mediaFiles.remove(data.mediaSource)?.close()
localMediaStates[data.mediaSource.safeUrl]?.value = AsyncData.Uninitialized
}
}
} }

View file

@ -29,4 +29,5 @@ sealed interface MediaViewerEvent {
data class Delete(val eventId: EventId) : MediaViewerEvent data class Delete(val eventId: EventId) : MediaViewerEvent
data class OnNavigateTo(val index: Int) : MediaViewerEvent data class OnNavigateTo(val index: Int) : MediaViewerEvent
data class LoadMore(val direction: Timeline.PaginationDirection) : MediaViewerEvent data class LoadMore(val direction: Timeline.PaginationDirection) : MediaViewerEvent
data class CancelLoadingMedia(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvent
} }

View file

@ -100,6 +100,9 @@ class MediaViewerPresenter(
is MediaViewerEvent.LoadMedia -> { is MediaViewerEvent.LoadMedia -> {
coroutineScope.downloadMedia(data = event.data) coroutineScope.downloadMedia(data = event.data)
} }
is MediaViewerEvent.CancelLoadingMedia -> {
dataSource.cancelLoadingMedia(event.data)
}
is MediaViewerEvent.ClearLoadingError -> { is MediaViewerEvent.ClearLoadingError -> {
dataSource.clearLoadingError(event.data) dataSource.clearLoadingError(event.data)
} }

View file

@ -52,6 +52,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.onVisibilityChanged
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -208,11 +209,16 @@ fun MediaViewerView(
} }
is MediaViewerPageData.MediaViewerData -> { is MediaViewerPageData.MediaViewerData -> {
var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) } var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) }
LaunchedEffect(Unit) {
state.eventSink(MediaViewerEvent.LoadMedia(dataForPage))
}
Box( Box(
modifier = Modifier.fillMaxSize() modifier = Modifier
.onVisibilityChanged(minDurationMs = 200L) { isVisible ->
if (isVisible) {
state.eventSink(MediaViewerEvent.LoadMedia(dataForPage))
} else {
state.eventSink(MediaViewerEvent.CancelLoadingMedia(dataForPage))
}
}
.fillMaxSize()
) { ) {
val isDisplayed = remember(pagerState.settledPage) { val isDisplayed = remember(pagerState.settledPage) {
// This 'item provider' lambda will be called when the data source changes with an outdated `settlePage` value // This 'item provider' lambda will be called when the data source changes with an outdated `settlePage` value

View file

@ -16,6 +16,7 @@
<string name="screen_media_details_filename">"Název souboru"</string> <string name="screen_media_details_filename">"Název souboru"</string>
<string name="screen_media_details_no_more_files_to_show">"Žádné další soubory k zobrazení"</string> <string name="screen_media_details_no_more_files_to_show">"Žádné další soubory k zobrazení"</string>
<string name="screen_media_details_no_more_media_to_show">"Žádná další média k zobrazení"</string> <string name="screen_media_details_no_more_media_to_show">"Žádná další média k zobrazení"</string>
<string name="screen_media_details_title">"Informace o souboru"</string>
<string name="screen_media_details_uploaded_by">"Nahrál(a)"</string> <string name="screen_media_details_uploaded_by">"Nahrál(a)"</string>
<string name="screen_media_details_uploaded_on">"Nahráno"</string> <string name="screen_media_details_uploaded_on">"Nahráno"</string>
</resources> </resources>

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