Merge branch 'release/26.05.2'

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

View file

@ -34,6 +34,13 @@
"/^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
"matchPackageNames": [

View file

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

View file

@ -24,8 +24,16 @@ appId: ${MAESTRO_APP_ID}
text: ${MAESTRO_INVITEE2_MXID}
index: 1
- 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: "aRoomName"
- scrollUntilVisible:
direction: DOWN
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
=============================

View file

@ -382,9 +382,9 @@ class LoggedInFlowNode(
}
is NavTarget.Room -> {
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 {
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,
), DependencyInjectionGraphOwner {
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 navigateToGlobalNotificationSettings()
fun navigateToDeveloperSettings()
@ -150,7 +150,7 @@ class JoinedRoomLoadedFlowNode(
callback.navigateToDeveloperSettings()
}
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) {
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean) {
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
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 navigateToGlobalNotificationSettings() = 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_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="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>

View file

@ -3,14 +3,34 @@
<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_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_title">"Privat"</string>
<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>
<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_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_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_title">"Adresă"</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_placeholder">"Adăugați o descriere…"</string>
</resources>

View file

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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_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_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_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>

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_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_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_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>

View file

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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_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_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_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>

View file

@ -2,7 +2,7 @@
<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_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_use_another_device">"Kasuta teist seadet"</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
fun launchIn(coroutineScope: CoroutineScope) {
roomList
fun launchIn(coroutineScope: CoroutineScope): Job {
return roomList
.summaries
.onEach { roomSummaries ->
replaceWith(roomSummaries)
@ -212,6 +212,7 @@ class RoomListDataSource(
private suspend fun rebuildAllRoomSummaries() {
lock.withLock {
roomList.summaries.replayCache.firstOrNull()?.let { roomSummaries ->
diffCacheUpdater.updateWith(roomSummaries)
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_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_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_submit">"Seadista andmete taastamine"</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 taastevõti"</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_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_unread">"Marcați ca necitită"</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_title">"Verificați că sunteți dumneavoastră"</string>
</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.dateformatter.test.FakeDateFormatter
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.room.aRoomSummary
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(
roomListService: FakeRoomListService = FakeRoomListService(),
roomListRoomSummaryFactory: RoomListRoomSummaryFactory = aRoomListRoomSummaryFactory(),
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
dateTimeObserver: FakeDateTimeObserver = FakeDateTimeObserver(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
) = RoomListDataSource(
roomListService = roomListService,
roomListRoomSummaryFactory = roomListRoomSummaryFactory,
@ -112,6 +273,6 @@ class RoomListDataSourceTest {
notificationSettingsService = notificationSettingsService,
sessionCoroutineScope = backgroundScope,
dateTimeObserver = dateTimeObserver,
analyticsService = FakeAnalyticsService(),
analyticsService = analyticsService,
)
}

View file

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

View file

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

View file

@ -36,6 +36,7 @@ dependencies {
implementation(projects.libraries.uiUtils)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.usersearch.api)
implementation(projects.libraries.testtags)
implementation(libs.coil.compose)
implementation(projects.services.apperror.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.matrix.api.MatrixClient
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.room.JoinedRoom
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.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.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.usersearch.api.UserRepository
@ -88,6 +93,7 @@ class DefaultInvitePeoplePresenter(
var searchActive by rememberSaveable { mutableStateOf(false) }
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
val createRoomFromDmAction = remember { mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized) }
val recentDirectRooms by produceState(emptyList(), roomMembers.value) {
if (roomMembers.value.isSuccess()) {
@ -208,7 +214,13 @@ class DefaultInvitePeoplePresenter(
)
} else {
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
queryState.clearText()
}
is InvitePeopleEvents.ClearError -> {
sendInvitesAction.value = AsyncAction.Uninitialized
createRoomFromDmAction.value = AsyncAction.Uninitialized
}
}
}
@ -228,6 +244,7 @@ class DefaultInvitePeoplePresenter(
searchResults = searchResults.value,
showSearchLoader = showSearchLoader.value,
sendInvitesAction = sendInvitesAction.value,
createRoomFromDmAction = createRoomFromDmAction.value,
suggestions = suggestions,
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")
private fun MutableState<ImmutableList<MatrixUser>>.toggleUser(user: MatrixUser) {
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.AsyncData
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 kotlinx.collections.immutable.ImmutableList
@ -26,6 +27,7 @@ data class DefaultInvitePeopleState(
val selectedUsers: ImmutableList<MatrixUser>,
override val isSearchActive: Boolean,
override val sendInvitesAction: AsyncAction<Unit>,
override val createRoomFromDmAction: AsyncAction<RoomId>,
val suggestions: ImmutableList<InvitableUser>,
override val eventSink: (InvitePeopleEvents) -> Unit
) : 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_JUSTIN
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.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
@ -119,6 +120,7 @@ private fun aDefaultInvitePeopleState(
isSearchActive: Boolean = false,
showSearchLoader: Boolean = false,
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
createRoomFromDmAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
suggestions: List<InvitableUser> = aMatrixUserList()
.take(5)
.map { user -> anInvitableUser(matrixUser = user, isSelected = user in selectedUsers) },
@ -132,6 +134,7 @@ private fun aDefaultInvitePeopleState(
isSearchActive = isSearchActive,
showSearchLoader = showSearchLoader,
sendInvitesAction = sendInvitesAction,
createRoomFromDmAction = createRoomFromDmAction,
suggestions = suggestions.toImmutableList(),
eventSink = {},
)

View file

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

View file

@ -2,4 +2,8 @@
<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_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>

View file

@ -2,4 +2,8 @@
<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_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>

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(
users: List<MatrixUser>,
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_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_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_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>
@ -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_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_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_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.

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

View file

@ -2,4 +2,5 @@
<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_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>

View file

@ -1,4 +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">"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_missing_permissions">"Sul pole õigust jagada selles jututoas oma asukohta reaalajas"</string>
</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"?>
<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_missing_permissions">"你无权在此房内共享实时位置。"</string>
</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.messages.test.FakeMessageComposerContext
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.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta
@ -50,7 +49,6 @@ class DefaultShareLocationEntryPointTest {
messageComposerContext = FakeMessageComposerContext(),
locationActions = FakeLocationActions(),
buildMeta = aBuildMeta(),
featureFlagService = FakeFeatureFlagService(),
client = FakeMatrixClient(),
durationFormatter = FakeDurationFormatter(),
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.messages.test.FakeMessageComposerContext
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.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
@ -77,7 +75,6 @@ class ShareLocationPresenterTest {
private val fakeMessageComposerContext = FakeMessageComposerContext()
private val fakeLocationActions = FakeLocationActions()
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
private val fakeFeatureFlagService = FakeFeatureFlagService()
private val fakeMatrixClient = FakeMatrixClient(sessionId = A_USER_ID)
private val durationFormatter = FakeDurationFormatter()
@ -96,7 +93,6 @@ class ShareLocationPresenterTest {
messageComposerContext = fakeMessageComposerContext,
locationActions = locationActions,
buildMeta = fakeBuildMeta,
featureFlagService = fakeFeatureFlagService,
client = fakeMatrixClient,
durationFormatter = durationFormatter,
liveLocationShareManager = liveLocationShareManager,
@ -658,21 +654,7 @@ class ShareLocationPresenterTest {
}
@Test
fun `canShareLiveLocation is false when the feature is disabled`() = 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)
fun `canShareLiveLocation is true in live timeline`() = runTest {
val shareLocationPresenter = createShareLocationPresenter(
timelineMode = Timeline.Mode.Live,
)
@ -685,7 +667,6 @@ class ShareLocationPresenterTest {
@Test
fun `canShareLiveLocation is false in thread timeline`() = runTest {
fakeFeatureFlagService.setFeatureEnabled(FeatureFlags.LiveLocationSharing, true)
val shareLocationPresenter = createShareLocationPresenter(
timelineMode = Timeline.Mode.Thread(A_THREAD_ID),
)

View file

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

View file

@ -24,6 +24,11 @@ interface BiometricAuthenticatorManager {
fun addCallback(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.
*/

View file

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

View file

@ -59,6 +59,7 @@ class LockScreenSettingsPresenter(
if (showRemovePinConfirmation) {
showRemovePinConfirmation = false
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>
<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_signout_in_progress_dialog_content">"Logime välja…"</string>
<string name="screen_signout_in_progress_dialog_content">"Eemaldan seadet…"</string>
</resources>

View file

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

View file

@ -60,7 +60,6 @@ dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem)
@ -81,7 +80,6 @@ dependencies {
testImplementation(projects.features.login.test)
testImplementation(projects.features.enterprise.test)
testImplementation(projects.features.preferences.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oauth.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.uri.ensureProtocol
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.HomeServerLoginCompatibilityChecker
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@ -71,7 +69,6 @@ class DefaultElementClassicConnection(
private val coroutineScope: CoroutineScope,
private val matrixAuthenticationService: MatrixAuthenticationService,
private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker,
private val featureFlagService: FeatureFlagService,
) : ElementClassicConnection {
// Messenger for communicating with the service.
private var messenger: Messenger? = null
@ -119,10 +116,6 @@ class DefaultElementClassicConnection(
override fun start() {
Timber.tag(loggerTag.value).d("start()")
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
// class name because there is no reason to be able to let other
// applications replace our component.
@ -158,11 +151,6 @@ class DefaultElementClassicConnection(
override fun requestSession() {
Timber.tag(loggerTag.value).d("requestSession()")
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
if (finalMessenger == null) {
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_title">"Vyberte váš server"</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_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>

View file

@ -28,7 +28,7 @@
<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_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_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>
@ -39,7 +39,13 @@
<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_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_checking_account">"Kontrollin kasutajakontot"</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_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_title">"Selectați serverul dumneavoastra"</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_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>
@ -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_title">"Bine ați revenit!"</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_checking_account">"Se verifică contul…"</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_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_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_subtitle">"Bun venit în %1$s. Supraalimentat, pentru viteză și simplitate."</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_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_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_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.

View file

@ -15,9 +15,6 @@ import androidx.core.graphics.createBitmap
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
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.HomeServerLoginCompatibilityChecker
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
fun `when an error is received, an error is emitted`() = runTest {
val connection = createDefaultElementClassicConnection(
@ -514,17 +496,10 @@ class DefaultElementClassicConnectionTest {
homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(
checkResult = { Result.success(true) }
),
isFeatureEnabled: Boolean = true,
featureFlagService: FeatureFlagService = FakeFeatureFlagService(
initialState = mapOf(
FeatureFlags.SignInWithClassic.key to isFeatureEnabled,
)
),
) = DefaultElementClassicConnection(
serviceBinder = serviceBinder,
coroutineScope = coroutineScope,
matrixAuthenticationService = matrixAuthenticationService,
homeServerLoginCompatibilityChecker = homeServerLoginCompatibilityChecker,
featureFlagService = featureFlagService,
)
}

View file

@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<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_title">"Eemalda see seade"</string>
<string name="screen_signout_in_progress_dialog_content">"Logime välja…"</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_title">"Sa oled varukoopiate tegemise välja lülitanud"</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_in_progress_dialog_content">"Eemaldan seadet…"</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 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 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_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_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_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>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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_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>

View file

@ -35,7 +35,7 @@
<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_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_text_formatting">"Tekstivorming"</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_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_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_gallery">"Zdjęcia i filmy"</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 kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
@Inject
@ -56,17 +53,8 @@ class AdvancedSettingsPresenter(
appPreferencesStore.getThemeFlow().mapToTheme(isBlackThemeAllowed)
}.collectAsState(initial = Theme.System)
@OptIn(ExperimentalCoroutinesApi::class)
val liveLocationMinimumDistanceUpdate by produceState<Int?>(null) {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.LiveLocationSharing)
.flatMapLatest { isEnabled ->
if (isEnabled) {
appPreferencesStore.getLiveLocationMinimumDistanceInMetersUpdateFlow()
} else {
emptyFlow()
}
}
.collect { value = it }
appPreferencesStore.getLiveLocationMinimumDistanceInMetersUpdateFlow().collect { value = it }
}
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_turned_off">"Systémová oznámení byla vypnuta"</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_light">"Světlý"</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_timeline_media_toggle_title">"Peida meedia eelvaated ajajoonel"</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_title">"Andmete uuendused reaalajas asukoha jagamisel"</string>
<plurals name="screen_advanced_settings_live_location_update_distance">
<item quantity="one">"Iga %1$d meeter"</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_turned_off">"Süsteemi teavitused on välja lülitatud"</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_light">"Hele"</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_turned_off">"A rendszerértesítések ki vannak kapcsolva"</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_light">"Világos"</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_turned_off">"Powiadomienia systemowe wyłączone"</string>
<string name="screen_notification_settings_title">"Powiadomienia"</string>
<string name="theme_black">"Czarny"</string>
<string name="theme_dark">"Ciemny"</string>
<string name="theme_light">"Jasny"</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_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_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_title">"Optimizați calitatea media"</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_turned_off">"Notificările de sistem sunt dezactivate"</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_light">"Deschis"</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_mentions">"提及"</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_content_link">"系统设置"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"系统通知已关闭"</string>
<string name="screen_notification_settings_title">"通知"</string>
<string name="theme_black">"纯黑"</string>
<string name="theme_dark">"深色"</string>
<string name="theme_light">"浅色"</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
fun `present - exposes live location minimum distance from app preferences`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore(
liveLocationMinimumDistanceUpdate = 50,
)
val featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.LiveLocationSharing, true)
}
val presenter = createAdvancedSettingsPresenter(appPreferencesStore = appPreferencesStore, featureFlagService = featureFlagService)
val presenter = createAdvancedSettingsPresenter(appPreferencesStore = appPreferencesStore)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -256,10 +233,7 @@ class AdvancedSettingsPresenterTest {
val appPreferencesStore = InMemoryAppPreferencesStore(
liveLocationMinimumDistanceUpdate = 10,
)
val featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.LiveLocationSharing, true)
}
val presenter = createAdvancedSettingsPresenter(appPreferencesStore = appPreferencesStore, featureFlagService = featureFlagService)
val presenter = createAdvancedSettingsPresenter(appPreferencesStore = appPreferencesStore)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()

View file

@ -2,7 +2,7 @@
<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_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_title">"举报房间"</string>
</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_everyone">"Člen"</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_rooms">"Spravovat místnosti"</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_everyone">"Liikmed"</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_rooms">"Halda jututuba"</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_everyone">"Membru"</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_rooms">"Gestionați camerele"</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 {
fun navigateToGlobalNotificationSettings()
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 startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean)
}

View file

@ -263,7 +263,20 @@ class RoomDetailsFlowNode(
}
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 -> {

View file

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

View file

@ -208,8 +208,15 @@ fun RoomDetailsView(
onClick = onSecurityAndPrivacyClick
)
}
}
state.roomMemberDetailsState?.let { dmMemberDetails ->
state.roomMemberDetailsState?.let { dmMemberDetails ->
if (state.canInvite) {
PreferenceCategory {
InviteItem(onClick = invitePeople)
}
}
PreferenceCategory {
ProfileItem(
verificationState = dmMemberDetails.verificationState,
onClick = { onProfileClick(dmMemberDetails.userId) }
@ -374,14 +381,14 @@ private fun MainActionsSection(
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.canInvite) {
MainActionButton(
title = stringResource(CommonStrings.action_invite),
imageVector = CompoundIcons.UserAdd(),
onClick = onInvitePeople,
)
}
// Share CTA should be hidden for DMs
MainActionButton(
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
private fun PinnedMessagesItem(
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.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
@ -19,10 +20,16 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
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.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.matrix.api.core.RoomId
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
@ContributesNode(RoomScope::class)
@ -35,6 +42,10 @@ class RoomInviteMembersNode(
room: JoinedRoom,
invitePeoplePresenterFactory: InvitePeoplePresenter.Factory,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun openCreatedRoom(roomId: RoomId)
}
init {
lifecycle.subscribe(
onResume = {
@ -48,6 +59,8 @@ class RoomInviteMembersNode(
roomId = room.roomId,
)
private val callback = plugins.callback<Callback>()
@Composable
override fun View(modifier: Modifier) {
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(
state = state,
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_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_default_setting_footnote">"Możesz to zmienić w swoim %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">"Możesz to zmienić w %1$s."</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_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>

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crypto_history_sharing_room_info_hidden_badge_content">"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_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>
@ -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_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_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_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.
@ -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_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_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_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_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_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_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_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_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_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_selecting_option_title">"Doar pentru membri, după selectarea acestei opțiuni"</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">"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.
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>

View file

@ -70,7 +70,7 @@ class DefaultRoomDetailsEntryPointTest {
val callback = object : RoomDetailsEntryPoint.Callback {
override fun navigateToGlobalNotificationSettings() = 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 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.compose.ui.test.AndroidComposeUiTest
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.onNodeWithTag
import androidx.compose.ui.test.performClick
@ -339,6 +341,25 @@ class RoomDetailsViewTest {
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(

View file

@ -2,16 +2,17 @@
<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_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_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_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_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_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_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>
@ -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_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_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_title">"Kui sa ühtegi muud võimalust ei leia, siis lähtesta oma identiteet."</string>
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Lülita välja"</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_title">"Kas sa oled kindel, et soovid varukoopiate tegemise välja lülitada?"</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_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 digitaalne identiteet."</string>
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Kustuta"</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 oled kindel, et soovid võtmehoidla kustutada?"</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_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>
@ -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_description">"Ära jaga seda kellegagi"</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_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_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>
</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_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_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_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>

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_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_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_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>

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_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_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_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.
@ -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_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_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_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_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_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_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_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_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_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_selecting_option_title">"Doar pentru membri, după selectarea acestei opțiuni"</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">"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.
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>

View file

@ -9,10 +9,21 @@
</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_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_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_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_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_roles_and_permissions">"Roluri și permisiuni"</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">
<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_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_use_another_device">"Kasuta teist seadet"</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_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_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_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>
@ -42,7 +42,7 @@
<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_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_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_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_signout_in_progress_dialog_content">"Logime välja…"</string>
<string name="screen_signout_in_progress_dialog_content">"Eemaldan seadet…"</string>
</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_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_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_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>
@ -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_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_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_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"
activity = "1.13.0"
media3 = "1.10.0"
camera = "1.6.0"
camera = "1.6.1"
work = "2.11.2"
# Compose
compose_bom = "2026.04.01"
compose_bom = "2026.05.00"
# Coroutines
coroutines = "1.10.2"
coroutines = "1.11.0"
# Accompanist
accompanist = "0.37.3"
@ -35,7 +35,7 @@ test_core = "1.7.0"
roborazzi = "1.60.0"
# Jetbrain
datetime = "0.7.1"
datetime = "0.8.0"
serialization_json = "1.11.0"
#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" }
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
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" }
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" }
@ -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_effect = { module = "androidx.media3:media3-effect", 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_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" }
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
androidx_startup = "androidx.startup:startup-runtime:1.2.0"
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_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
# 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.
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.05.13"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.05.18"
# Others
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
@ -222,7 +223,7 @@ color_picker = "io.mhssn:colorpicker:1.0.0"
# Analytics
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
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_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_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_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>

View file

@ -93,13 +93,6 @@ enum class FeatureFlags(
defaultValue = { 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(
key = "feature.allow_black_theme",
title = "Black theme",
@ -107,13 +100,6 @@ enum class FeatureFlags(
defaultValue = { false },
isFinished = false,
),
LiveLocationSharing(
key = "feature.liveLocationSharing",
title = "Live location sharing",
description = "Allow sharing live location in rooms.",
defaultValue = { false },
isFinished = false,
),
ValidateNetworkWhenSchedulingNotificationFetching(
key = "feature.validate_network_when_scheduling_notification_fetching",
title = "Validate internet connectivity when scheduling notification fetching",

View file

@ -434,13 +434,30 @@ class RustMatrixAuthenticationService(
qrCodeData: QrCodeData,
): Client {
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
.getBaseClientBuilder(
sessionPaths = sessionPaths,
passphrase = pendingPassphrase,
slidingSyncType = ClientBuilderSlidingSync.Discovered,
)
.serverNameOrHomeserverUrl(qrCodeData.serverName()!!)
.serverNameOrHomeserverUrl(baseUrlOrServerName)
.build()
}

View file

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

View file

@ -112,6 +112,11 @@ class RoomSummaryListProcessor(
private suspend fun updateRoomSummaries(updates: List<RoomListEntriesUpdate>, block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(
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 {
val current = roomSummaries.replayCache.lastOrNull()
val mutableRoomSummaries = current.orEmpty().toMutableList()
@ -126,7 +131,7 @@ class RoomSummaryListProcessor(
analyticsService.trackError(
IllegalStateException(
"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()
}
override fun rooms(): List<SpaceRoom> {
override suspend fun rooms(): List<SpaceRoom> {
return roomsResult()
}
@ -53,7 +53,7 @@ class FakeFfiSpaceRoomList(
spaceRoomListPaginationStateListener?.onUpdate(state)
}
override fun subscribeToRoomUpdate(listener: SpaceRoomListEntriesListener): TaskHandle {
override suspend fun subscribeToRoomUpdate(listener: SpaceRoomListEntriesListener): TaskHandle {
spaceRoomListEntriesListener = listener
return FakeFfiTaskHandle()
}

View file

@ -173,16 +173,68 @@ class RoomSummaryListProcessorTest {
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(
roomId = roomId,
latestEventLambda = { LatestEventValue.None }
)
private fun TestScope.createProcessor() = RoomSummaryListProcessor(
private fun TestScope.createProcessor(
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
) = RoomSummaryListProcessor(
summaries,
FakeFfiRoomListService(),
coroutineContext = StandardTestDispatcher(testScheduler),
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_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_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_invites_invited_you">"%1$s (%2$s) saatis sulle kutse"</string>
</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_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_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>
</resources>

View file

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

View file

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

View file

@ -8,14 +8,18 @@
package io.element.android.libraries.mediaviewer.impl.local.player
import androidx.annotation.OptIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
@OptIn(UnstableApi::class)
@Composable
fun rememberExoPlayer(): ExoPlayer {
fun rememberExoPlayer(forAudioOnly: Boolean): ExoPlayer {
return if (LocalInspectionMode.current) {
remember {
ExoPlayerForPreview()
@ -23,7 +27,14 @@ fun rememberExoPlayer(): ExoPlayer {
} else {
val context = LocalContext.current
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?,
modifier: Modifier = Modifier,
) {
val exoPlayer = rememberExoPlayer()
val exoPlayer = rememberExoPlayer(forAudioOnly = false)
ExoPlayerMediaVideoView(
isDisplayed = isDisplayed,
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.matrix.api.media.MatrixMediaLoader
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.mediaviewer.api.MediaViewerEntryPoint.MediaViewerMode
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.withContext
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
class MediaViewerDataSource(
mode: MediaViewerMode,
@ -51,7 +53,7 @@ class MediaViewerDataSource(
private val pagerKeysHandler: PagerKeysHandler,
) {
// 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) {
MediaViewerMode.SingleMedia,
@ -69,7 +71,7 @@ class MediaViewerDataSource(
fun dispose() {
Timber.d("Disposing MediaViewerDataSource, closing ${mediaFiles.size} media files")
mediaFiles.forEach { it.close() }
mediaFiles.values.forEach { it.close() }
mediaFiles.clear()
localMediaStates.clear()
}
@ -163,6 +165,12 @@ class MediaViewerDataSource(
}
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}")
val localMediaState = localMediaStates.getOrPut(data.mediaSource.safeUrl) {
mutableStateOf(AsyncData.Uninitialized)
@ -175,7 +183,7 @@ class MediaViewerDataSource(
filename = data.mediaInfo.filename
)
.onSuccess { mediaFile ->
mediaFiles.add(mediaFile)
mediaFiles[data.mediaSource] = mediaFile
}
.mapCatchingExceptions { mediaFile ->
localMediaFactory.createFromMediaFile(
@ -190,4 +198,12 @@ class MediaViewerDataSource(
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 OnNavigateTo(val index: Int) : 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 -> {
coroutineScope.downloadMedia(data = event.data)
}
is MediaViewerEvent.CancelLoadingMedia -> {
dataSource.cancelLoadingMedia(event.data)
}
is MediaViewerEvent.ClearLoadingError -> {
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.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.onVisibilityChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
@ -208,11 +209,16 @@ fun MediaViewerView(
}
is MediaViewerPageData.MediaViewerData -> {
var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) }
LaunchedEffect(Unit) {
state.eventSink(MediaViewerEvent.LoadMedia(dataForPage))
}
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) {
// 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_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_title">"Informace o souboru"</string>
<string name="screen_media_details_uploaded_by">"Nahrál(a)"</string>
<string name="screen_media_details_uploaded_on">"Nahráno"</string>
</resources>

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