diff --git a/CHANGES.md b/CHANGES.md index e742d79c1e..15b0a76b23 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,30 @@ +Changes in Element v1.5.20 (2023-01-10) +======================================= + +Features ✨ +---------- + - "[Rich text editor] Add list formatting buttons to the rich text editor" ([#7887](https://github.com/vector-im/element-android/issues/7887)) + +Bugfixes 🐛 +---------- + - ReplyTo are not updated if the original message is edited or deleted. ([#5546](https://github.com/vector-im/element-android/issues/5546)) + - Observe ViewEvents only when resumed and ensure ViewEvents are not lost. ([#7724](https://github.com/vector-im/element-android/issues/7724)) + - [Session manager] Missing info when a session does not support encryption ([#7853](https://github.com/vector-im/element-android/issues/7853)) + - Reduce number of crypto database transactions when handling the sync response ([#7879](https://github.com/vector-im/element-android/issues/7879)) + - [Voice Broadcast] Stop listening if we reach the last received chunk and there is no last sequence number ([#7899](https://github.com/vector-im/element-android/issues/7899)) + - Handle network error on API `rooms/{roomId}/threads` ([#7913](https://github.com/vector-im/element-android/issues/7913)) + +In development 🚧 +---------------- + - [Poll] Render active polls list of a room + - [Poll] Render past polls list of a room ([#7864](https://github.com/vector-im/element-android/issues/7864)) + +Other changes +------------- + - fix: increase font size for messages ([#5717](https://github.com/vector-im/element-android/issues/5717)) + - Add trim to username input on the app side and SDK side when sign-in ([#7111](https://github.com/vector-im/element-android/issues/7111)) + + Changes in Element v1.5.18 (2023-01-02) ======================================= diff --git a/Gemfile.lock b/Gemfile.lock index 276f4ae66a..33ebbc1b70 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -127,7 +127,8 @@ GEM xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) gh_inspector (1.1.3) - git (1.11.0) + git (1.13.0) + addressable (~> 2.8) rchardet (~> 1.8) google-apis-androidpublisher_v3 (0.25.0) google-apis-core (>= 0.7, < 2.a) diff --git a/build.gradle b/build.gradle index 0f94fc418c..1ebe910e80 100644 --- a/build.gradle +++ b/build.gradle @@ -27,9 +27,9 @@ buildscript { classpath 'com.google.firebase:firebase-appdistribution-gradle:3.1.1' classpath 'com.google.gms:google-services:4.3.14' classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730' - classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' + classpath 'com.google.android.gms:oss-licenses-plugin:0.10.6' classpath "com.likethesalad.android:stem-plugin:2.2.3" - classpath 'org.owasp:dependency-check-gradle:7.4.1' + classpath 'org.owasp:dependency-check-gradle:7.4.4' classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.20" classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0" classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3' diff --git a/dependencies.gradle b/dependencies.gradle index b6af5d39d0..2041d0d0c3 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -11,6 +11,7 @@ def gradle = "7.3.1" def kotlin = "1.7.22" def kotlinCoroutines = "1.6.4" def dagger = "2.44.2" +def firebaseBom = "31.1.1" def appDistribution = "16.0.0-beta05" def retrofit = "2.9.0" def markwon = "4.6.2" @@ -81,10 +82,12 @@ ext.libs = [ ], google : [ 'material' : "com.google.android.material:material:1.7.0", + 'firebaseBom' : "com.google.firebase:firebase-bom:$firebaseBom", + 'messaging' : "com.google.firebase:firebase-messaging", //'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution", //'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution", // Phone number https://github.com/google/libphonenumber - 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.3" + 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.4" ], dagger : [ 'dagger' : "com.google.dagger:dagger:$dagger", @@ -99,7 +102,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:0.10.0" + 'wysiwyg' : "io.element.android:wysiwyg:0.14.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105110.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105110.txt new file mode 100644 index 0000000000..8c51742e06 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40105110.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Nová implementace celoobrazovkového režimu pro editor formátovaného textu a opravy chyb. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105120.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105120.txt new file mode 100644 index 0000000000..b7f38f629f --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40105120.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Vlákna jsou nyní ve výchozím nastavení povolena. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105130.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105130.txt new file mode 100644 index 0000000000..69c2b3304c --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40105130.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Vlákna jsou nyní povolena ve výchozím nastavení. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105140.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105140.txt new file mode 100644 index 0000000000..69c2b3304c --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40105140.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Vlákna jsou nyní povolena ve výchozím nastavení. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105160.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105160.txt new file mode 100644 index 0000000000..69c2b3304c --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40105160.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Vlákna jsou nyní povolena ve výchozím nastavení. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105180.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105180.txt new file mode 100644 index 0000000000..69c2b3304c --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40105180.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Vlákna jsou nyní povolena ve výchozím nastavení. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40105110.txt b/fastlane/metadata/android/de-DE/changelogs/40105110.txt new file mode 100644 index 0000000000..de5f4d90e8 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40105110.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Der Vollbildmodus des Textverarbeitungseditors wurde neu umgesetzt und es wurden diverse Fehler behoben. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40105120.txt b/fastlane/metadata/android/de-DE/changelogs/40105120.txt new file mode 100644 index 0000000000..901b4c2a70 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40105120.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Threads sind nun automatisch aktiviert. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40105130.txt b/fastlane/metadata/android/de-DE/changelogs/40105130.txt new file mode 100644 index 0000000000..901b4c2a70 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40105130.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Threads sind nun automatisch aktiviert. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40105140.txt b/fastlane/metadata/android/de-DE/changelogs/40105140.txt new file mode 100644 index 0000000000..c55d8d998f --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40105140.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Threads sind nun standardmäßig aktiviert. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40105160.txt b/fastlane/metadata/android/de-DE/changelogs/40105160.txt new file mode 100644 index 0000000000..c55d8d998f --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40105160.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Threads sind nun standardmäßig aktiviert. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/de-DE/changelogs/40105180.txt b/fastlane/metadata/android/de-DE/changelogs/40105180.txt new file mode 100644 index 0000000000..c55d8d998f --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40105180.txt @@ -0,0 +1,2 @@ +Die wichtigsten Änderungen in dieser Version: Threads sind nun standardmäßig aktiviert. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/40105200.txt b/fastlane/metadata/android/en-US/changelogs/40105200.txt new file mode 100644 index 0000000000..6f549d094a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40105200.txt @@ -0,0 +1,2 @@ +Main changes in this version: Mainly bugfixing! +Full changelog: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/eo/short_description.txt b/fastlane/metadata/android/eo/short_description.txt index 33013ce78f..05a4aaf191 100644 --- a/fastlane/metadata/android/eo/short_description.txt +++ b/fastlane/metadata/android/eo/short_description.txt @@ -1 +1 @@ -Sekura kaj sencentrigita vokado kaj babilado. Tenu viajn datumojn sekuraj. +Grupa mesaĝisto - ĉifrita mesaĝado, grupa babilejo kaj videovokoj diff --git a/fastlane/metadata/android/eo/title.txt b/fastlane/metadata/android/eo/title.txt index f56927e529..85b92c693b 100644 --- a/fastlane/metadata/android/eo/title.txt +++ b/fastlane/metadata/android/eo/title.txt @@ -1 +1 @@ -Element (antaŭe Riot.im) +Element - Sekura Tujmesaĝilo diff --git a/fastlane/metadata/android/et/changelogs/40105110.txt b/fastlane/metadata/android/et/changelogs/40105110.txt new file mode 100644 index 0000000000..833a567ce2 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40105110.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: tekstitoimeti täisekraanivaate uus versioon ja erinevate vigade parandused. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40105120.txt b/fastlane/metadata/android/et/changelogs/40105120.txt new file mode 100644 index 0000000000..9aadf5dae8 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40105120.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: jutulõngad on vaikimisi kasutusel. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40105130.txt b/fastlane/metadata/android/et/changelogs/40105130.txt new file mode 100644 index 0000000000..c8f1e98c3d --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40105130.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: jutulõngad on nüüd vaikimisi kasutusel. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40105140.txt b/fastlane/metadata/android/et/changelogs/40105140.txt new file mode 100644 index 0000000000..c8f1e98c3d --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40105140.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: jutulõngad on nüüd vaikimisi kasutusel. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40105160.txt b/fastlane/metadata/android/et/changelogs/40105160.txt new file mode 100644 index 0000000000..9aadf5dae8 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40105160.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: jutulõngad on vaikimisi kasutusel. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40105180.txt b/fastlane/metadata/android/et/changelogs/40105180.txt new file mode 100644 index 0000000000..9aadf5dae8 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40105180.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: jutulõngad on vaikimisi kasutusel. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40105100.txt b/fastlane/metadata/android/fa/changelogs/40105100.txt new file mode 100644 index 0000000000..b6a96e2fe8 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40105100.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: پیاده‌سازی جدید حالت تمام‌صفحه برای ویرایشگر متن غنی و رفع اشکال‌ها. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40105110.txt b/fastlane/metadata/android/fa/changelogs/40105110.txt new file mode 100644 index 0000000000..b6a96e2fe8 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40105110.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: پیاده‌سازی جدید حالت تمام‌صفحه برای ویرایشگر متن غنی و رفع اشکال‌ها. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40105120.txt b/fastlane/metadata/android/fa/changelogs/40105120.txt new file mode 100644 index 0000000000..0c3cc5aa31 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40105120.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: رشته‌ها اکنون به صورت پیش‌گزیده به کار افتاده‌اند. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40105130.txt b/fastlane/metadata/android/fa/changelogs/40105130.txt new file mode 100644 index 0000000000..0c3cc5aa31 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40105130.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: رشته‌ها اکنون به صورت پیش‌گزیده به کار افتاده‌اند. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40105140.txt b/fastlane/metadata/android/fa/changelogs/40105140.txt new file mode 100644 index 0000000000..0c3cc5aa31 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40105140.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: رشته‌ها اکنون به صورت پیش‌گزیده به کار افتاده‌اند. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40105160.txt b/fastlane/metadata/android/fa/changelogs/40105160.txt new file mode 100644 index 0000000000..0c3cc5aa31 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40105160.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: رشته‌ها اکنون به صورت پیش‌گزیده به کار افتاده‌اند. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40105180.txt b/fastlane/metadata/android/fa/changelogs/40105180.txt new file mode 100644 index 0000000000..0c3cc5aa31 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40105180.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: رشته‌ها اکنون به صورت پیش‌گزیده به کار افتاده‌اند. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fi-FI/full_description.txt b/fastlane/metadata/android/fi-FI/full_description.txt index ac02bc3b42..a1defe8131 100644 --- a/fastlane/metadata/android/fi-FI/full_description.txt +++ b/fastlane/metadata/android/fi-FI/full_description.txt @@ -1,4 +1,4 @@ -Element on turvallinen pikaviesti- ja tiimityösovellus joka sopii mainiosti ryhmäkeskusteluihin etätöissä. Sovellus käyttää päästä päähän -salausta ja tarjoaa videoneuvottelun, tiedostojen jakamisen ja äänipuhelut. +Element on turvallinen pikaviesti- ja tiimityösovellus, joka sopii mainiosti ryhmäkeskusteluihin etätöissä. Sovellus käyttää läpisalausta ja tarjoaa videoneuvottelun, tiedostojen jakamisen ja äänipuhelut. Elementin ominaisuuksia: - Edistyneet viestintätyökalut @@ -35,5 +35,8 @@ Real end-to-end encryption (only those in the conversation can decrypt messages) Kattavaa viestintää ja integraatioita Viestit, ääni- ja videopuhelut, tiedostojen jakaminen, näytön jakaminen ja koko joukko integraatioita, botteja ja sovelmia. Luo huoneita ja yhteisöjä, pidä yhteyttä ja hoida asiasi. -Jatka siitä mihin jäit -Stay in touch wherever you are with fully synchronised message history across all your devices and on the web at https://app.element.io +Jatka siitä, mihin jäit +Täysin synkronoitu viestihistoria kaikkien laitteidesi välillä ja verkkoselaimessa: https://app.element.io + +Avointa lähdekoodia +Element Android on avoimen lähdekoodin projekti GitHubissa. Ilmoita virheistä ja osallistu kehittämiseen osoitteessa https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105100.txt b/fastlane/metadata/android/fr-FR/changelogs/40105100.txt new file mode 100644 index 0000000000..9f0d0823b4 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40105100.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Nouvelle implémentation du mode plein écran pour l’éditeur de texte formaté, et correction de bogues. +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105110.txt b/fastlane/metadata/android/fr-FR/changelogs/40105110.txt new file mode 100644 index 0000000000..9f0d0823b4 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40105110.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Nouvelle implémentation du mode plein écran pour l’éditeur de texte formaté, et correction de bogues. +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105120.txt b/fastlane/metadata/android/fr-FR/changelogs/40105120.txt new file mode 100644 index 0000000000..4101bb0c86 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40105120.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Fils de discussion activés par défaut. +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105130.txt b/fastlane/metadata/android/fr-FR/changelogs/40105130.txt new file mode 100644 index 0000000000..4101bb0c86 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40105130.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Fils de discussion activés par défaut. +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105140.txt b/fastlane/metadata/android/fr-FR/changelogs/40105140.txt new file mode 100644 index 0000000000..4101bb0c86 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40105140.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Fils de discussion activés par défaut. +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105160.txt b/fastlane/metadata/android/fr-FR/changelogs/40105160.txt new file mode 100644 index 0000000000..4101bb0c86 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40105160.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Fils de discussion activés par défaut. +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105180.txt b/fastlane/metadata/android/fr-FR/changelogs/40105180.txt new file mode 100644 index 0000000000..4101bb0c86 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40105180.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Fils de discussion activés par défaut. +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105000.txt b/fastlane/metadata/android/hu-HU/changelogs/40105000.txt new file mode 100644 index 0000000000..7298a41794 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40105000.txt @@ -0,0 +1,2 @@ +Legnagyobb változtatás ebben a verzióban: A késleltetett DM alapból engedélyezve van. +Teljes változási napló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105020.txt b/fastlane/metadata/android/hu-HU/changelogs/40105020.txt new file mode 100644 index 0000000000..1686d93813 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40105020.txt @@ -0,0 +1,2 @@ +Legnagyobb változtatás ebben a verzióban: Új app layout alapból bekapcsolva! +Teljes változási napló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105040.txt b/fastlane/metadata/android/hu-HU/changelogs/40105040.txt new file mode 100644 index 0000000000..9821c573de --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40105040.txt @@ -0,0 +1,2 @@ +Legnagyobb változtatás ebben a verzióban: Új funkciók a laboratórium beállítások alatt: Gazdag szöveg kompózer, új eszköz kezelése, hangközvetítés. Még mindig aktív fejlesztés alatt! +Teljes változási napló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105060.txt b/fastlane/metadata/android/hu-HU/changelogs/40105060.txt new file mode 100644 index 0000000000..f62af5db6b --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Legnagyobb változtatás ebben a verzióban: Új felhasználói felület a mellékletek kiválasztására. +Teljes változási napló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105070.txt b/fastlane/metadata/android/hu-HU/changelogs/40105070.txt new file mode 100644 index 0000000000..f62af5db6b --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Legnagyobb változtatás ebben a verzióban: Új felhasználói felület a mellékletek kiválasztására. +Teljes változási napló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105080.txt b/fastlane/metadata/android/hu-HU/changelogs/40105080.txt new file mode 100644 index 0000000000..de2fe57fe1 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40105080.txt @@ -0,0 +1,2 @@ +Legnagyobb változtatás ebben a verzióban: bugfixek és javítások +Teljes változási napló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105100.txt b/fastlane/metadata/android/hu-HU/changelogs/40105100.txt new file mode 100644 index 0000000000..560103b913 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40105100.txt @@ -0,0 +1,2 @@ +Legnagyobb változtatás ebben a verzióban: A teljes képernyős mód új megvalósítása a Rich Text Editor számára és hibajavítások. +Teljes változási napló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105110.txt b/fastlane/metadata/android/hu-HU/changelogs/40105110.txt new file mode 100644 index 0000000000..560103b913 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40105110.txt @@ -0,0 +1,2 @@ +Legnagyobb változtatás ebben a verzióban: A teljes képernyős mód új megvalósítása a Rich Text Editor számára és hibajavítások. +Teljes változási napló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105120.txt b/fastlane/metadata/android/hu-HU/changelogs/40105120.txt new file mode 100644 index 0000000000..a4e7c9acb9 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40105120.txt @@ -0,0 +1,2 @@ +Legnagyobb változtatás ebben a verzióban: Threadek már alapból engedélyezve vannak. +Teljes változási napló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105130.txt b/fastlane/metadata/android/hu-HU/changelogs/40105130.txt new file mode 100644 index 0000000000..a4e7c9acb9 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40105130.txt @@ -0,0 +1,2 @@ +Legnagyobb változtatás ebben a verzióban: Threadek már alapból engedélyezve vannak. +Teljes változási napló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105140.txt b/fastlane/metadata/android/hu-HU/changelogs/40105140.txt new file mode 100644 index 0000000000..a4e7c9acb9 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40105140.txt @@ -0,0 +1,2 @@ +Legnagyobb változtatás ebben a verzióban: Threadek már alapból engedélyezve vannak. +Teljes változási napló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105160.txt b/fastlane/metadata/android/hu-HU/changelogs/40105160.txt new file mode 100644 index 0000000000..c5dc38bc8f --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40105160.txt @@ -0,0 +1,2 @@ +Legnagyobb változtatás ebben a verzióban: Új üzenetszálak alapból bekapcsolva! +Teljes változási napló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105180.txt b/fastlane/metadata/android/hu-HU/changelogs/40105180.txt new file mode 100644 index 0000000000..cc70967e58 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40105180.txt @@ -0,0 +1,2 @@ +Legnagyobb változtatás ebben a verzióban: Az üzenetszálak alapból bekapcsolva! +Teljes változási napló: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40105110.txt b/fastlane/metadata/android/id/changelogs/40105110.txt new file mode 100644 index 0000000000..0c7d2f5262 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40105110.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Penerapan baru mode layar penuh untuk Penyunting Teks Kaya dan perbaikan kutu. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40105120.txt b/fastlane/metadata/android/id/changelogs/40105120.txt new file mode 100644 index 0000000000..173a1bfb1b --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40105120.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Utasan sekarang diaktifkan secara bawaan. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40105130.txt b/fastlane/metadata/android/id/changelogs/40105130.txt new file mode 100644 index 0000000000..173a1bfb1b --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40105130.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Utasan sekarang diaktifkan secara bawaan. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40105140.txt b/fastlane/metadata/android/id/changelogs/40105140.txt new file mode 100644 index 0000000000..173a1bfb1b --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40105140.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Utasan sekarang diaktifkan secara bawaan. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40105160.txt b/fastlane/metadata/android/id/changelogs/40105160.txt new file mode 100644 index 0000000000..173a1bfb1b --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40105160.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Utasan sekarang diaktifkan secara bawaan. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40105180.txt b/fastlane/metadata/android/id/changelogs/40105180.txt new file mode 100644 index 0000000000..173a1bfb1b --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40105180.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Utasan sekarang diaktifkan secara bawaan. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40105100.txt b/fastlane/metadata/android/it-IT/changelogs/40105100.txt new file mode 100644 index 0000000000..7dc50eab33 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40105100.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: nuova implementazione della modalità a schermo intero per l'editor in Rich Text e correzione di errori. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40105110.txt b/fastlane/metadata/android/it-IT/changelogs/40105110.txt new file mode 100644 index 0000000000..7dc50eab33 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40105110.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: nuova implementazione della modalità a schermo intero per l'editor in Rich Text e correzione di errori. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40105120.txt b/fastlane/metadata/android/it-IT/changelogs/40105120.txt new file mode 100644 index 0000000000..ab24842e82 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40105120.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: i messaggi in conversazioni sono attivi in modo predefinito. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40105130.txt b/fastlane/metadata/android/it-IT/changelogs/40105130.txt new file mode 100644 index 0000000000..ab24842e82 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40105130.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: i messaggi in conversazioni sono attivi in modo predefinito. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40105140.txt b/fastlane/metadata/android/it-IT/changelogs/40105140.txt new file mode 100644 index 0000000000..ab24842e82 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40105140.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: i messaggi in conversazioni sono attivi in modo predefinito. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40105160.txt b/fastlane/metadata/android/it-IT/changelogs/40105160.txt new file mode 100644 index 0000000000..ab24842e82 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40105160.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: i messaggi in conversazioni sono attivi in modo predefinito. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40105180.txt b/fastlane/metadata/android/it-IT/changelogs/40105180.txt new file mode 100644 index 0000000000..ab24842e82 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40105180.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: i messaggi in conversazioni sono attivi in modo predefinito. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/40105100.txt b/fastlane/metadata/android/pt-BR/changelogs/40105100.txt new file mode 100644 index 0000000000..c03bb2b140 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40105100.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Nova implementação do modo de tela cheia para o Editor de Texto Rico e consertos de bugs. +Changelog completo: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/40105110.txt b/fastlane/metadata/android/pt-BR/changelogs/40105110.txt new file mode 100644 index 0000000000..c03bb2b140 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40105110.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Nova implementação do modo de tela cheia para o Editor de Texto Rico e consertos de bugs. +Changelog completo: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/40105120.txt b/fastlane/metadata/android/pt-BR/changelogs/40105120.txt new file mode 100644 index 0000000000..0e5e1a9401 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40105120.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Threads são agora habilitadas por padrão. +Changelog completo: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/40105130.txt b/fastlane/metadata/android/pt-BR/changelogs/40105130.txt new file mode 100644 index 0000000000..0e5e1a9401 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40105130.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Threads são agora habilitadas por padrão. +Changelog completo: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/40105140.txt b/fastlane/metadata/android/pt-BR/changelogs/40105140.txt new file mode 100644 index 0000000000..0e5e1a9401 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40105140.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Threads são agora habilitadas por padrão. +Changelog completo: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105080.txt b/fastlane/metadata/android/ru-RU/changelogs/40105080.txt new file mode 100644 index 0000000000..42f8b2263d --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40105080.txt @@ -0,0 +1,2 @@ +Главные изменения в этой версии: исправления ошибок и улучшения. +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105100.txt b/fastlane/metadata/android/ru-RU/changelogs/40105100.txt new file mode 100644 index 0000000000..14fb635403 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40105100.txt @@ -0,0 +1,2 @@ +Главные изменения в этой версии: Новая имплементация полноэкранного режима для расширенного режима текстового редактора и исправления ошибок. +Полный список всех изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105120.txt b/fastlane/metadata/android/ru-RU/changelogs/40105120.txt new file mode 100644 index 0000000000..047e0254ae --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40105120.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Обсуждения теперь включены по умолчанию. +Перечень всех изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105130.txt b/fastlane/metadata/android/ru-RU/changelogs/40105130.txt new file mode 100644 index 0000000000..047e0254ae --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40105130.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Обсуждения теперь включены по умолчанию. +Перечень всех изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40105110.txt b/fastlane/metadata/android/sk/changelogs/40105110.txt new file mode 100644 index 0000000000..af32da6d29 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40105110.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Nová implementácia režimu celej obrazovky pre rozšírený textový editor a opravy chýb. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40105120.txt b/fastlane/metadata/android/sk/changelogs/40105120.txt new file mode 100644 index 0000000000..d5b5ad330d --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40105120.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Vlákna sú teraz predvolene zapnuté. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40105130.txt b/fastlane/metadata/android/sk/changelogs/40105130.txt new file mode 100644 index 0000000000..d5b5ad330d --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40105130.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Vlákna sú teraz predvolene zapnuté. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40105140.txt b/fastlane/metadata/android/sk/changelogs/40105140.txt new file mode 100644 index 0000000000..d5b5ad330d --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40105140.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Vlákna sú teraz predvolene zapnuté. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40105160.txt b/fastlane/metadata/android/sk/changelogs/40105160.txt new file mode 100644 index 0000000000..d5b5ad330d --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40105160.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Vlákna sú teraz predvolene zapnuté. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40105180.txt b/fastlane/metadata/android/sk/changelogs/40105180.txt new file mode 100644 index 0000000000..d5b5ad330d --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40105180.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Vlákna sú teraz predvolene zapnuté. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40105100.txt b/fastlane/metadata/android/sq/changelogs/40105100.txt new file mode 100644 index 0000000000..a103977a73 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105100.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Sendërtim i ri i mënyrës “Sa krejt ekrani”, për Përpunues Teksti të Pasur, si dhe ndreqje të metash. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40105110.txt b/fastlane/metadata/android/sq/changelogs/40105110.txt new file mode 100644 index 0000000000..78d3d9785b --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105110.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Sendërtim i ri i mënyrë “Sa krejt ekrani” për Përpunuesin Tekst i Pasur, si dhe ndreqje të metash. +Regjistër ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40105120.txt b/fastlane/metadata/android/sq/changelogs/40105120.txt new file mode 100644 index 0000000000..51350f5c00 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105120.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Rrjedhat tanimë janë të aktivizuara, si parazgjedhje. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40105130.txt b/fastlane/metadata/android/sq/changelogs/40105130.txt new file mode 100644 index 0000000000..51350f5c00 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105130.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Rrjedhat tanimë janë të aktivizuara, si parazgjedhje. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40105140.txt b/fastlane/metadata/android/sq/changelogs/40105140.txt new file mode 100644 index 0000000000..51350f5c00 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105140.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Rrjedhat tanimë janë të aktivizuara, si parazgjedhje. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105100.txt b/fastlane/metadata/android/sv-SE/changelogs/40105100.txt new file mode 100644 index 0000000000..3c4b0f2297 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40105100.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Ny implementering av fullskärmsläget för rik-textredigeraren och buggfixar. +Full ändringslogg: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105110.txt b/fastlane/metadata/android/sv-SE/changelogs/40105110.txt new file mode 100644 index 0000000000..3c4b0f2297 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40105110.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Ny implementering av fullskärmsläget för rik-textredigeraren och buggfixar. +Full ändringslogg: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105120.txt b/fastlane/metadata/android/sv-SE/changelogs/40105120.txt new file mode 100644 index 0000000000..d0f9c996af --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40105120.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Trådar är nu aktivt som förval. +Full ändringslogg: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40105110.txt b/fastlane/metadata/android/uk/changelogs/40105110.txt new file mode 100644 index 0000000000..5f348b8a50 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40105110.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Нова реалізація повноекранного режиму для редактора розширеного тексту й виправлення помилок. +Перелік усіх змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40105120.txt b/fastlane/metadata/android/uk/changelogs/40105120.txt new file mode 100644 index 0000000000..edbd209d17 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40105120.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Гілки відтепер типово ввімкнено. +Перелік усіх змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40105130.txt b/fastlane/metadata/android/uk/changelogs/40105130.txt new file mode 100644 index 0000000000..edbd209d17 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40105130.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Гілки відтепер типово ввімкнено. +Перелік усіх змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40105140.txt b/fastlane/metadata/android/uk/changelogs/40105140.txt new file mode 100644 index 0000000000..edbd209d17 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40105140.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Гілки відтепер типово ввімкнено. +Перелік усіх змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40105160.txt b/fastlane/metadata/android/uk/changelogs/40105160.txt new file mode 100644 index 0000000000..edbd209d17 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40105160.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Гілки відтепер типово ввімкнено. +Перелік усіх змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40105180.txt b/fastlane/metadata/android/uk/changelogs/40105180.txt new file mode 100644 index 0000000000..edbd209d17 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40105180.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Гілки відтепер типово ввімкнено. +Перелік усіх змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105110.txt b/fastlane/metadata/android/zh-TW/changelogs/40105110.txt new file mode 100644 index 0000000000..20341b84fe --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40105110.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:格式化文字編輯器的全螢幕模式新實作與臭蟲修復。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105120.txt b/fastlane/metadata/android/zh-TW/changelogs/40105120.txt new file mode 100644 index 0000000000..9c66f3c2ad --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40105120.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:討論串現在預設啟用。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105130.txt b/fastlane/metadata/android/zh-TW/changelogs/40105130.txt new file mode 100644 index 0000000000..9c66f3c2ad --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40105130.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:討論串現在預設啟用。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105140.txt b/fastlane/metadata/android/zh-TW/changelogs/40105140.txt new file mode 100644 index 0000000000..9c66f3c2ad --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40105140.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:討論串現在預設啟用。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105160.txt b/fastlane/metadata/android/zh-TW/changelogs/40105160.txt new file mode 100644 index 0000000000..9c66f3c2ad --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40105160.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:討論串現在預設啟用。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105180.txt b/fastlane/metadata/android/zh-TW/changelogs/40105180.txt new file mode 100644 index 0000000000..9c66f3c2ad --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40105180.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:討論串現在預設啟用。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/gradle.properties b/gradle.properties index 7fbe2c0fc0..369472d856 100644 --- a/gradle.properties +++ b/gradle.properties @@ -43,3 +43,7 @@ signing.element.keyPassword=Secret signing.element.nightly.storePassword=Secret signing.element.nightly.keyId=Secret signing.element.nightly.keyPassword=Secret + +# Customise the Lint version to use a more recent version than the one bundled with AGP +# https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html +android.experimental.lint.version=8.0.0-alpha10 diff --git a/library/ui-strings/src/main/res/values-az/strings.xml b/library/ui-strings/src/main/res/values-az/strings.xml index 6fe322bdd0..b8341cd2b4 100644 --- a/library/ui-strings/src/main/res/values-az/strings.xml +++ b/library/ui-strings/src/main/res/values-az/strings.xml @@ -35,7 +35,7 @@ ** Şifrəni aça bilmir: %s ** Göndərənin cihazı bu mesaj üçün açarları bizə göndərməyib. Mesaj göndərmək olmur - Matris xətası + Matrix xətası Şifrəli mesaj Elektron poçt ünvanı Telefon nömrəsi @@ -136,4 +136,124 @@ • %s ilə uyğunlaşan serverlərə icazə verildi. Siz %1$s üçün otağa qoşulmaq dəvətin ləğv etdiniz %1$s-ı dəvət etdiniz + Tam ekran rejimini dəyiş + buna cavab olaraq + fayl göndərdi. + səs faylı göndərdi. + səsli mesaj göndərdi. + şəkil göndərdi. + video göndərdi. + stiker göndərdi. + sorğu yaratdı. + Digər cihazda giriş ləğv edildi. + Ev serveri QR kodu ilə daxil olmağı dəstəkləmir. + Tətbiqi digər cihazınızda açın + Tənzimləmələr -> Təhlükəsizlik & Məxfilik bölməsinə keç + \'QR kodunu göstər\' seç + Giriş ekranında başlat + \'QR kodu ilə daxil ol\' seç + Giriş ekranında başlat + \'QR kodunu skan et\' seç + QR kodunu bu cihazda göstər + Mobil cihazda daxil olursunuz\? + QR kodunu skan et + Cihaza qoşulur + Daxil olursunuz + Uyğunluq yoxdur\? + Bir daha cəhd et + Təsdiqlə + Bu kodun mənbəyini bildiyinizə əmin olun. Cihazları əlaqələndirməklə siz kiməsə hesabınıza tam giriş imkanı təmin edəcəksiniz. + Qalın format tətbiq et + Kursiv formatını tətbiq et + Alt xətt formatını tətbiq et + Bu otaq üçün əsas ünvanı sildiniz. + %1$s, %2$s əlavə etdi və %3$s-nı bu otaq üçün ünvan kimi sildi. + + Bu otaq üçün ünvan kimi %1$s-nı sildiniz. + Bu otaq üçün ünvan kimi %1$s-nı sildiniz. + + + %1$s, bu otaq üçün %2$s ünvanını sildi. + %1$s, bu otaq üçün %2$s ünvanını sildi. + + + Bu otaq üçün ünvan olaraq %1$s əlavə etdiniz. + Bu otaq üçün ünvan olaraq %1$s əlavə etdiniz. + + + %1$s, bu otaq üçün %2$s ünvanını əlavə etdi. + %1$s, bu otaq üçün %2$s ünvanını əlavə etdi. + + %1$s-ın⁴ dəvətini geri götürdünüz. Səbəb: %2$s + %1$s üçün dəvəti qəbul etdiniz. Səbəb: %2$s + %1$s-ı qadağan etdiniz. Səbəb: %2$s + Siz %1$s qadağanını sildiniz. Səbəb: %2$s + %1$s-ı sildiniz. Səbəb: %2$s + Siz dəvəti rədd etdiniz. Səbəb: %1$s + Siz getdiniz. Səbəb: %1$s + %1$s tərk etdi. Səbəb: %2$s + Otağı tərk etdiniz. Səbəb: %1$s + Siz qoşuldunuz. Əsas: %1$s + %1$s qoşuldu. Əsas: %2$s + Otağa qoşuldunuz. Əsas: %1$s + %1$s-ı dəvət etdiniz. Əsas: %2$s + Sizin dəvətiniz. Əsas: %1$s + Mesaj göndərildi + - Bəzi istifadəçilər nəzərə alınmayıb + ${app_name} aşağıdakı səbəbə görə: +\n%s +\n +\nGüncəl olmaq üçün təmiz keşi yerinə yetirməlidir. +\nNəzərə alın ki, bu əməliyyat tətbiqi yenidən işə salacaq və bir az vaxt apara bilər. + İlkin sinxronizasiya sorğusu + İlkin sinxronizasiya: +\nMəlumat endirilir… + İlkin sinxronizasiya: +\n Server cavabı gözlənilir… + Boş otaq (%s idi) + + %1$s, %2$s, %3$s və digər %4$d + %1$s, %2$s, %3$s və digər %4$d + + %1$s, %2$s, %3$s və %4$s + %1$s, %2$s və %3$s + Bu otağa qoşulmağa icazəniz yoxdur + %s uşağı yay + %s uşağı yığışdır + Otaqları Araşdır + Yeri Dəyiş + Otaq Yarat + Söhbət Başlat + Bütün Söhbətlər + Səsli yayımı bitirdiniz. + %1$s səsli yayımı bitirdi. + %1$s, %2$s - %3$s + %1$s, %2$s üçün güc səviyyəsini dəyişdi. + Siz %1$s üçün güc səviyyəsini dəyişdirdiniz. + Xüsusi + Fərdi (%1$d) + Defolt + Münsif + Müdir + %1$s vidcetini dəyişdirdiniz + %1$s, %2$s vidcetini dəyişdirdi + %1$s vidcetini sildiniz + %1$s, %2$s vidcetini sildi + %1$s vidceti əlavə etdiniz + %1$s, %2$s vidceti əlavə etdi + %1$s üçün dəvəti qəbul etdiniz + %1$s üçün dəvəti ləğv etdiniz + %1$s, %2$s üçün dəvəti ləğv etdi + • IP literallarına uyğunlaşan serverlər indi qadağan edilib. + • IP literalları uyğunlaşan serverlərə indi icazə verilir. + • %s ilə uyğunlaşan serverlər icazə verilən siyahıdan təmizləndi. + • %s ilə uyğunlaşan serverlərə indi icazə verilir. + • %s ilə uyğunlaşan serverlər qadağa siyahısından təmizləndi. + • %s ilə uyğunlaşan serverlər indi qadağan edilib. + + %d server ACLs dəyişiklik + %d server ACLs dəyişiklik + + • IP literallarına uyğunlaşan serverlər qadağan edildi. + • IP literallarına uyğunlaşan serverlərə icazə verilir. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-ca/strings.xml b/library/ui-strings/src/main/res/values-ca/strings.xml index 7e3e019ee5..b86a834a27 100644 --- a/library/ui-strings/src/main/res/values-ca/strings.xml +++ b/library/ui-strings/src/main/res/values-ca/strings.xml @@ -2790,7 +2790,7 @@ 2 1 Permet enregistrar i enviar emissions de veu dins una sala. - Activa l\'emissió de veu (en desenvolupament) + Activa l\'emissió de veu Activa la gravació d\'informació de client Desa el nom de client, la versió i l\'URL per reconèixer les sessions més fàcilment dins el gestor de sessions. Obté un millor control i visibilitat de totes les teves sessions. diff --git a/library/ui-strings/src/main/res/values-cs/strings.xml b/library/ui-strings/src/main/res/values-cs/strings.xml index 67cc3353aa..0a7998deaa 100644 --- a/library/ui-strings/src/main/res/values-cs/strings.xml +++ b/library/ui-strings/src/main/res/values-cs/strings.xml @@ -441,7 +441,7 @@ Žádný Zrušit Přihlásit se se single sign-on - To není platná adresa Matrix serveru + Toto není platná adresa Matrix serveru Domovský server není dostupný na této adrese, zkontrolujte ji prosím Odstraňování problémů s oznámeními Řešit diagnostiku @@ -2924,4 +2924,26 @@ Úpravy Zobrazit poslední chaty v nabídce sdílení systému Povolit přímé sdílení + Zkontrolujte, zda je váš účet v bezpečí + Máte neověřené relace + Tato relace nepodporuje šifrování, takže ji nelze ověřit. +\n +\nPři použití této relace se nebudete moci účastnit místností, kde je šifrování povoleno. +\n +\nPro dosažení nejlepšího zabezpečení a soukromí se doporučuje používat klienty Matrix, které šifrování podporují. + Odhlásit se ze všech ostatních relací + Tato relace nepodporuje šifrování, a proto ji nelze ověřit. + Získejte nejnovější sestavení (poznámka: můžete mít potíže s přihlášením) + Noční sestavení + Živé vysílání + Ukončili jste hlasové vysílání. + %1$s ukončil(a) hlasové vysílání. + Jste si jisti, že chcete ukončit živé vysílání\? Tím se vysílání ukončí a v místnosti bude k dispozici celý záznam. + Zastavit živé vysílání\? + Ano, zastavit + Upravit odkaz + Vytvořit odkaz + Odkaz + Text + Nastavit odkaz \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-da/strings.xml b/library/ui-strings/src/main/res/values-da/strings.xml index 13d53b7bb2..35c93949f9 100644 --- a/library/ui-strings/src/main/res/values-da/strings.xml +++ b/library/ui-strings/src/main/res/values-da/strings.xml @@ -39,7 +39,6 @@ Telefonnummer Invitation til rum %1$s og %2$s - Tomt rum Lyst Tema Mørkt Tema @@ -82,7 +81,6 @@ Kun Matrix kontakter Ingen resultater Rum - Send logfiler Send crashlogfiler Send screenshot @@ -110,10 +108,8 @@ Dette ligner ikke en gyldig emailadresse Den emailadresse er allerede i brug. Glemt adgangskode? - Denne Home Server vil gerne være sikker på du ikke er en robot Kunne ikke verificere emailadresse: vær sikker på du klikkede på linket i emailen - Skriv gyldig URL Fejlformet JSON Indeholdt ikke gyldig JSON @@ -134,15 +130,10 @@ Opkald I Gang Den anden side tog den ikke. Information - - ${app_name} skal bruge tilladelse til at bruge din mikrofon for at lave lydopkald. - ${app_name} skal bruge tilladelse til at bruge dit kamera og din mikrofon for at lave videoopkald. Giv venligst tilladelse ved næste pop-up for at lave opkaldet. - - JA NEJ Fortsæt @@ -150,7 +141,6 @@ Giv venligst tilladelse ved næste pop-up for at lave opkaldet. Forbind Afvis Spring til første ulæste besked. - Forlad rum Er du sikker på at du vil forlade rummet? DIREKTE CHATS @@ -163,7 +153,7 @@ Giv venligst tilladelse ved næste pop-up for at lave opkaldet. Du vil ikke kunne omgøre denne ændring da du forfremmer brugeren til at have samme magt niveau som dig selv. Er du sikker? %s skriver… - "%1$s & %2$s skriver…" + %1$s & %2$s skriver… %1$s, %2$s og andre skriver… Du har ikke tilladelse til at skrive i dette rum Stol på @@ -186,7 +176,6 @@ Er du sikker? %d medlemsændringer Medlemsoversigt - 1 medlem %d medlemmer @@ -199,8 +188,6 @@ Er du sikker? Søg Filtrer medlemmer i rum Ingen resultater - - Alle meddelelser Opret genvej på startskærm Profilbillede @@ -291,4 +278,4 @@ Er du sikker? %1$s oprettede rummet Din invitation Forbind denne email med din konto - + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml index 809ee477fc..52b8f0c716 100644 --- a/library/ui-strings/src/main/res/values-de/strings.xml +++ b/library/ui-strings/src/main/res/values-de/strings.xml @@ -471,7 +471,7 @@ Raum %s ist nicht sichtbar. Integrationen hinzufügen Benachrichtigungston - Anfrage konnte nicht gesendet werden. + Übertragung der Anfrage fehlgeschlagen. user_id fehlt in der Anfrage. Helles Design Dunkles Design @@ -2805,7 +2805,7 @@ Mit QR-Code anmelden QR-Code einlesen Zeichne Sprachnachrichten auf, während du sie in Echtzeit in den Raumverlauf sendest. - Sprachübertragung aktivieren (in aktiver Entwicklung) + Sprachübertragung aktivieren Der Heim-Server unterstützt Anmelden per QR-Code nicht. Die Anmeldung wurde vom anderen Gerät abgebrochen. Der QR-Code ist ungültig. @@ -2867,4 +2867,35 @@ IP-Adresse anzeigen Kürzliche Unterhaltungen im Teilen-Menü des Systems anzeigen Direktes Teilen aktivieren + Überprüfe sie, um ein sicheres Konto gewährleisten zu können + Du hast nicht verifizierte Sitzungen + Hol dir den neuesten Build (Achtung: Du kannst dich eventuell nicht anmelden) + Nightly-Build + Diese Sitzung unterstützt keine Verschlüsselung, weshalb sie nicht verifiziert werden kann. +\n +\nDu wirst dich mit dieser Sitzung nicht an Unterhaltungen in Räumen mit aktivierter Verschlüsselung beteiligen können. +\n +\nAus Sicherheits- und Datenschutzgründen, wird die Nutzung von verschlüsselungsfähigen Matrix-Anwendungen empfohlen. + Von allen anderen Sitzungen abmelden + Diese Sitzung unterstützt keine Verschlüsselung und kann deshalb nicht verifiziert werden. + Echtzeit-Übertragung + Du hast eine Sprachübertragung beendet. + %1$s beendete eine Sprachübertragung. + Möchtest du die Übertragung wirklich beenden\? Dies wird die Übertragung beenden und die vollständige Aufnahme im Raum bereitstellen. + Live-Übertragung beenden\? + Ja, beende + Link setzen + Link bearbeiten + Link erstellen + Link + Text + Dein Zugriffstoken gewährt vollen Zugriff auf dein Konto. Teile ihn mit niemandem. + Zugriffstoken + Unsortierte Liste umschalten + Nummerierte Liste umschalten + In diesem Raum gibt es noch keine abgeschlossenen Umfragen + Vergangene Umfragen + In diesem Raum gibt es keine aktiven Umfragen + Aktive Umfragen + Umfrageverlauf \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-eo/strings.xml b/library/ui-strings/src/main/res/values-eo/strings.xml index f536ca00f9..4521e840a6 100644 --- a/library/ui-strings/src/main/res/values-eo/strings.xml +++ b/library/ui-strings/src/main/res/values-eo/strings.xml @@ -718,8 +718,8 @@ Bonvolu enigi la URL-on de identiga servilo Ne povis konektiĝi al identiga servilo Enigu URL-on de identiga servilo - Ni sendis al vi konfirman retleteron al %s, bonvolu unue kontroli vian retpoŝton kaj klaki la konfirman ligilon - Ni sendis al vi konfirman retleteron al %s; kontrolu vian retpoŝton kaj klaku la konfirman ligilon + Ni sendis retleteron al %s, bonvolu unue kontroli vian retpoŝton kaj klaki la konfirman ligilon + Ni sendis retleteron al %s; kontrolu vian retpoŝton kaj klaku la konfirman ligilon Troveblaj telefonnumeroj Malkonekto de via identiga servilo signifas, ke vi ne estos trovebla de aliaj uzantoj kaj ne povos inviti aliulojn per retpoŝtadreso aŭ telefono. Elektebloj pri trovado aperos post aldono de telefonnumero. @@ -2201,4 +2201,4 @@ Sonorante… Aroj - Iom uzantoj reatentita - + \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-es/strings.xml b/library/ui-strings/src/main/res/values-es/strings.xml index 3d10997233..c06442b5d0 100644 --- a/library/ui-strings/src/main/res/values-es/strings.xml +++ b/library/ui-strings/src/main/res/values-es/strings.xml @@ -50,7 +50,7 @@ %1$s ha invitado a %2$s. Razón: %3$s %1$s te ha invitado. Razón: %2$s %1$s se ha unido. Razón: %2$s - %1$s se ha ido. Razón: %2$s + %1$s dejó la sala. Razón: %2$s %1$s ha rechadazo la invitación. Razón: %2$s %1$s expulsó a %2$s. Razón: %3$s %1$s ha baneado a %2$s. Razón: %3$s @@ -81,17 +81,17 @@ %1$s ha permitido que los invitados se unan a la sala. %1$s ha impedido que los invitados se unan a la sala. %1$s ha activado el cifrado Extremo-a-Extremo. - %1$s ha activado el cifrado Extremo-a-Extremo (algoritmo no reconocido %2$s). + %1$s ha activado el cifrado extremo-a-extremo (algoritmo no reconocido %2$s). Tu invitación %1$s creó la sala Creaste la sala Invitaste a %1$s Te uniste a la Sala - Dejaste la Sala + Dejaste la sala Rechazaste la invitación Tu pateaste a %1$s Tu desbanaste a %1$s - Usted prohibió a %1$s + Excluiste a %1$s Retiró la invitación de %1$s\'s Cambiaste tu avatar Establece su nombre de visualización en %1$s @@ -152,10 +152,10 @@ Agregaste %1$s y quitaste %2$s como direcciones para esta sala. Estableciste la dirección principal de esta sala en %1$s. Quitaste la dirección principal de esta sala. - Ha permitido que los invitados se unan a la sala. - Ha impedido que los invitados se unan a la sala. + Has permitido que los invitados se unan a la sala. + Has impedido que los invitados se unan a la sala. Has activado el cifrado Extremo-a-Extremo. - Has activado el cifrado Extremo-a-Extremo (algoritmo %1$s no reconocido). + Has activado el cifrado extremo-a-extremo (algoritmo %1$s no reconocido). Has impedido que invitados se unan a la sala. Has permitido a invitados unirse aquí. Te has ido. Razón: %1$s @@ -163,7 +163,7 @@ Has invitado a %1$s Has actualizado aquí. Has hecho futuros mensajes visibles a %1$s - Te saliste de la sala + Has dejado la sala Te uniste Creaste la conversación %1$s ha impedido que invitados se unan a la sala. @@ -255,7 +255,7 @@ Salas y Grupos Filtrar salas Invitaciones - Prioridad baja + Baja prioridad Conversaciones Solo contactos de Matrix No hay resultados @@ -429,7 +429,7 @@ Importar Cifrar solo a sesiones verificadas Nunca enviar mensajes cifrados a sesiones sin verificar desde esta sesión. - SIN Verificar + Sin Verificar Verificado Verificar Para verificar que esta sesión es confiable, por favor contacta a su dueño por algún otro medio (ej. cara a cara o por teléfono) y pregúntale si la clave que ve en sus Ajustes de Usuario para esta sesión coincide con la clave a continuación: @@ -819,7 +819,7 @@ La copia de seguridad tiene una firma valida de la sesión no verificada %s La copia de seguridad tiene una firma inválida de la sesión verificada %s La copia de seguridad tiene una firma inválida de la sesión no verificada %s - Para usar la copia de seguridad de la clave en esta sesión introduzca su contraseña o su clave de recuperación ahora. + Para usar la copia de seguridad de la clave en esta sesión introduce tu contraseña o tu clave de recuperación ahora. ¿Deseas borrar tus claves de cifrado guardadas en el servidor\? No podrás usar tu clave de recuperación para leer el historial de mensajes cifrados. Reproducir sonido de cámara ip desconocida @@ -1182,7 +1182,7 @@ %s cancelada Cancelado por usted %s aceptada - Aceptado por usted + Aceptaste Verificacion enviada Solicitud de verificación Verifica esta Sesion @@ -1239,7 +1239,7 @@ Precaucion Error al obtener sesiones Sesiones - Confirmado + Confiable No es confiable Inicializar Firmas Cruzadas Restablecer claves @@ -1255,7 +1255,7 @@ Razón para redactar ${app_name} Android Refrescar - Nuevo inicio de sesión detectado . ¿Fue usted\? + Nuevo inicio de sesión detectado . ¿Has sido tú\? Este no era yo Su cuenta puede estar comprometida Verificación cancelada @@ -1263,7 +1263,7 @@ Clave de mensaje ¡Listo! Cifrado habilitado - Sala creada y configurada por usted. + Creaste y configuraste la sala. Esperando por %s… Ajuste de Notificaciones Mensaje… @@ -1279,7 +1279,7 @@ Si decea resetear su PIN, toque Olvidé PIN para cerrar sesión y restablecer. Numeros telefonicos Correos y numeros telefonicos - Administre el correo y numero telefonico de su cuenta + Administra las direcciones de correo y/o números telefónicos relacionados a tu cuenta de Matrix Mostrar mensajes eliminados Indicar marca de mensaje eliminado ARCHIVOS @@ -1357,7 +1357,7 @@ Hiciste la sala solo por invitación. Únase gratis a millones de personas en el mayor servidor público Continuar con SSO - Dirección de servicios de Element Matrix + Dirección de Element Matrix Services Ingrese la dirección del servidor que desea utilizar Se enviará un correo electrónico de verificación a su bandeja de entrada para confirmar la configuración de su nueva contraseña. Siguiente @@ -1407,7 +1407,7 @@ Advertencia Tu cuenta aún no está creada. ¿Detener el proceso de registro\? Seleccione matrix.org - Seleccionar servicios de matriz de elementos + Seleccionar Element Matrix Services Seleccione un servidor doméstico personalizado Realiza el desafío de captcha Acepta los términos para continuar @@ -1453,7 +1453,7 @@ \nVuelva a iniciar sesión para acceder a los datos y mensajes de su cuenta. Perderás el acceso a los mensajes seguros a menos que inicies sesión para recuperar tus claves de cifrado. La sesión actual es para el usuario %1$s y usted proporciona las credenciales para el usuario %2$s. Esto no está suportado por ${app_name}. -\nPrimero borre los datos, luego inicie sesión nuevamente con otra cuenta. +\nPrimero borra los datos, luego inicia sesión nuevamente con otra cuenta. Su enlace matrix.to estaba mal formado El modo desarrollador activa funciones ocultas y también puede hacer que la aplicación sea menos estable. ¡Solo para desarrolladores! Uno de los siguientes puede verse comprometido: @@ -1462,9 +1462,9 @@ \n- El servidor privado al que está conectado el usuario que estás verificando \n- Su conexión a internet o la de otros usuarios \n- Su dispositivo o el de otros usuarios - Los mensajes de esta sala están cifrados Extremo-a-Extremo. + Los mensajes de esta sala están cifrados de extremo-a-extremo. \n -\nSus mensajes están protegidos y sólo usted y el destinatario tienen las claves únicas para descifrarlos. +\nTus mensajes están protegidos y sólo tu y el destinatario tienen las claves únicas para descifrarlos. Esta sesión no puede compartir esta verificación con sus otras sesiones. \nLa verificación se guardará localmente y se compartirá en una versión futura de la aplicación. Envía el emote dado coloreado como un arcoíris @@ -1474,7 +1474,7 @@ Verifica si los mismos emojis aparecen en el mismo orden en ambos usuarios. Compare el código con el que se muestra en la pantalla del otro usuario. Los mensajes con este usuario están cifrados Extremo-a-Extremo y no pueden ser leídos por terceros. - Su nueva sesión ahora está verificada. Tiene acceso a sus mensajes cifrados y otros usuarios lo verán como de confianza. + Tu nueva sesión acaba de verificarse y ahora tiene acceso a tus mensajes cifrados y otros usuarios la verán como de confianza. La firma cruzada está habilitada \n Claves privadas en el dispositivo. La firma cruzada está habilitada @@ -1484,8 +1484,8 @@ \nLas claves no son de confianza El administrador de su servidor ha desactivado el cifrado Extremo-a-Extremo de forma predeterminada en salas privadas y mensajes directos. No hay información criptográfica disponible - Esta sesión es confiable para mensajería segura porque usted la verificó: - Verifique esta sesión para marcarla como confiable y otorgarle acceso a mensajes cifrados. Si no inició sesión en esta sesión, su cuenta puede verse comprometida: + Esta sesión es confiable para mensajería segura porque la verificaste: + Verifica esta sesión para marcarla como confiable y otorgarle acceso a mensajes cifrados. Si no iniciaste sesión en esta sesión, su cuenta puede haber sido comprometida: %d sesión activa %d sesiones activas @@ -1510,8 +1510,8 @@ Solicitudes clave Desbloquear el historial de mensajes cifrados Utilice esta sesión para verificar su nuevo, otorgándole acceso a mensajes cifrados. - Si cancela, no podrá leer mensajes cifrados en este dispositivo y otros usuarios no confiarán en él - Si cancela, no podrá leer mensajes cifrados en su nuevo dispositivo y otros usuarios no confiarán en él + Si cancelas, no podrás leer mensajes cifrados en este dispositivo y otros usuarios no confiarán en este + Si cancelas, no podrás leer mensajes cifrados en tu nuevo dispositivo y otros usuarios no confiarán en este No verificarás %1$s (%2$s) si cancelas ahora. Comience de nuevo en su perfil de usuario. Uno de los siguientes puede verse comprometido: \n @@ -1524,7 +1524,7 @@ Se canceló la verificación. Puede iniciar la verificación de nuevo. Ingrese su %s para continuar. No use la contraseña de su cuenta. - Ingrese una frase de seguridad que solo usted conozca, que se usa para proteger secretos en su servidor. + Ingresa una frase de seguridad que solo tú conozcas, que se usa para proteger secretos en tu servidor. Esto puede tardar varios segundos, tenga paciencia. Configurando la recuperación. Manténlo seguro @@ -1561,7 +1561,7 @@ Nombre de usuario y / o contraseña incorrectos. La contraseña ingresada comienza o termina con espacios, verifíquela. Esta cuenta ha sido desactivada. Mejora de cifrado disponible - Verifíquese a usted mismo y a los demás para mantener sus chats seguros + Verifícate a ti mismo y a los demás para mantener tus chats seguros No es una clave de recuperación válida Por favor introduce una clave de recuperación Comprobando la clave de respaldo @@ -1622,11 +1622,11 @@ Usa una llave de seguridad Genere una clave de seguridad para almacenar en un lugar seguro, como un administrador de contraseñas o una caja fuerte. Utilice una frase de seguridad - Ingrese una frase secreta que solo usted conozca y genere una clave de respaldo. + Ingresa una frase secreta que solo tú conozcas y genera una clave para tu copia de respaldo. Guarde su llave de seguridad Guarde su llave de seguridad en un lugar seguro, como un administrador de contraseñas o una caja fuerte. Establecer una frase de seguridad - Ingrese una frase de seguridad que solo usted conozca, que se usa para proteger secretos en su servidor. + Ingresa una frase de seguridad que sólo tú conozcas, que se usa para proteger secretos en tu servidor. Frase de seguridad Ingrese su Frase de seguridad nuevamente para confirmarla. Nombre de la Sala @@ -1636,7 +1636,7 @@ Esperando este mensaje, esto puede tardar un poco Debido al cifrado Extremo-a-Extremo, es posible que deba esperar a que llegue el mensaje de alguien porque las claves de cifrado no se le enviaron correctamente. No puede acceder a este mensaje porque ha sido bloqueado por el remitente - No puede acceder a este mensaje porque el remitente no confía en su sesión + No puedes acceder a este mensaje porque el remitente no confía en tu sesión No puede acceder a este mensaje porque el remitente no envió las claves a propósito Esperando al historial de cifrado ¡Nos complace anunciar que hemos cambiado de nombre! Tu aplicación está actualizada y accediste a tu cuenta. @@ -1711,7 +1711,7 @@ Mostrar el dispositivo con el que puede verificar ahora Mostrar %d dispositivos con los que puede verificar ahora - Reiniciará sin historia, mensajes, dispositivos o usuarios verificados + Reiniciarás sin historial, ni mensajes, ni dispositivos o usuarios verificados Si resetea todo Solo haga esto si no tiene otro dispositivo con el que verificar éste. Resetear todo @@ -1748,7 +1748,7 @@ Se necesita una nueva autenticación ¡Código QR no escaneado! Código QR no válido (URL no válida)! - No puede DM usted mismo! + No puedes MD a ti mismo! Compartir por texto Cambiar PIN Cambie su PIN actual @@ -1960,7 +1960,7 @@ Acceso a la sala Siempre preguntar Espacios - mostrar todas las salas en el directorio de salas, incluyendo salas con contenido explícito. + Mostrar todas las salas en el directorio de salas, incluyendo salas con contenido explícito. Mostrar salas con contenido explícito Directorio de la sala Salas sugeridas @@ -2181,7 +2181,7 @@ Agregar nuevas palabra clave Tus palabras clave Habilitar notificación por correo electrónico para %s - Para recibir un correo electrónico con una notificación, asocie un correo electrónico a su cuenta de matrix + Para recibir notificaciones por correo electrónico, asocia una direccion de correo electrónico a tu cuenta de Matrix Notificación de correo electrónico Ninguno Solo menciones y palabras clave @@ -2219,7 +2219,7 @@ No se puede grabar un mensaje de voz No se puede reproducir este mensaje de voz Toca tu grabación para detenerla o escucharla - %1$ds dejado + Restan %1$ds Mantenga presionado para grabar, suelte para enviar Eliminar grabación Grabación de mensaje de voz @@ -2238,7 +2238,7 @@ ¡Se ha cerrado la sesión! ¡Se ha abandonado la sala! Consejo: Pulse prolongadamente un mensaje y use \"%s\" . - Mantén las conversaciones organizadas con hilos + Mantén las conversaciones organizadas usando hilos Muestra todos los hilos en que has participado Mis Hilos Muestra todos los hilos de la sala actual @@ -2457,7 +2457,7 @@ BETA Comentarios de la beta de hilos Beta de hilos - - Algunos usuarios han sido dejados de ignorar + - Algunos usuarios han dejado de ser ignorados La compartición de pantalla está en progreso ${app_name} Compartición de pantalla Dejar de compartir pantalla @@ -2474,7 +2474,7 @@ Actualizado hace %1$s Implementación temporal: las ubicaciones persisten en el historial de la sala Activar compartir ubicación en tiempo real - Queda %1$s + Restan %1$s Compartiendo hasta %1$s Ver ubicación en tiempo real La ubicación en tiempo real ha terminado @@ -2625,15 +2625,15 @@ \nPor favor, inténtelo de nuevo.%s Usar ajustes por defecto del sistema Escoger manualmente - Tamaño automático de fuente - Escoger tamaño de la fuente + Tamaño automático + Escoge tamaño del tipo de letra %1$s y %2$d otro %1$s y %2$d otros %1$s y %2$s - Email no verificado, comprueba tu bandeja de entrada - Aquí es donde tus nuevas solicitudes y invitaciones estarán. + Correo electrónico no verificado, comprueba tu bandeja de entrada + Aquí es donde se encontrarán tus nuevas solicitudes e invitaciones. Nada nuevo. Invitaciones Los espacios son una nueva forma de agrupar salas y personas. Crea un espacio para empezar. @@ -2655,4 +2655,37 @@ %1$d seleccionado %1$d seleccionados + Habilitar compartición directa + Otorgar permiso + ${app_name} necesita permiso para mostrar notificaciones. +\nPor favor, otórgalo. + ${app_name} necesita permiso para mostrar notificaciones. Las notificaciones pueden mostrar tus mensajes, invitaciones, etc. +\n +\nPor favor, a continuacion, en las ventanas emergentes, permite el acceso para poder visualizar notificaciones. + Prueba el editor de texto enriquecido (pronto llegará la opción de texto simple, sin formato) + Habilitar editor de texto enriquecido (rich text) + Crear MD únicamente al primer mensaje + Una versión simplificada de Element con pestañas opcionales + Habilitar nueva disposición + Sí, Detener + Deseleccionar todo + Ocultar los subespacios de %s + Mostrar los subespacios de %s + Has finalizado una transmisión de voz. + %1$s ha finalizado una transmisión de voz. + Element Matrix Services (EMS) es un servicio de alojamiento para tus comunicaciones en tiempo real. Robusto, confiable, rápido y seguro. Para saber cómo, ve a <a href=\"${ftue_ems_url}\">element.io/ems</a> + Difusión de voz + Habilitado: + ID de sesión: + Algo falló. Por favor, comprueba tu conexión de red e inténtalo nuevamente. + Citando + Respondiendo a %s + Editando + Abrir pantalla de herramientas de desarrollador + 🔒 Tienes habilitado el cifrado a sesiones verificadas sólo para todas las salas en Ajustes de Seguridad. + ⚠ Hay dispositivos sin verificar en esta sala, los cuales no seran capaces de descifrar los mensajes que envías. + Habilita MDs pospuestos + Mostrar chats recientes en el menú de compartir sistema + No enviar nunca mensajes cifrados a sesiones sin verificar en esta sala. + Restan %1$s \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-et/strings.xml b/library/ui-strings/src/main/res/values-et/strings.xml index 96d9650ceb..1e8e2b989e 100644 --- a/library/ui-strings/src/main/res/values-et/strings.xml +++ b/library/ui-strings/src/main/res/values-et/strings.xml @@ -1078,7 +1078,7 @@ Ootamatu viga Kas sa oled kindel\? Kui sa logid välja või kaotad seadme, siis sa ei saa enam lugeda oma krüptitud sõnumeid. - Laen varukoopia versiooni… + Laadin varukoopia versiooni… Selleks, et krüptitud sõnumite ajalugu lukust lahti võtta, kasuta oma taastamiseks mõeldud paroolifraasi kasuta oma taastevõtit Kas sa ei tea oma taastamiseks mõeldud paroolifraasi\? Siis sa võid %s. @@ -1379,7 +1379,7 @@ Selge lugu Vaata lisateavet Salvesta taastevõti järgnevalt - Laen sinu kontaktide loendit… + Laadin sinu kontaktide loendit… Sinu kontaktide loend on tühi Sinu kontaktide loend Tühista kutse @@ -2797,7 +2797,7 @@ \n \nJärgmistes vaadetes palun anna sellele rakendusele teavituste kuvamiseks vajalikud õigused. Võimalus salvestada ja postitada ringhäälingukõnesid jututoa ajajoonele. - Võta kasutusele ringhäälingukõned (aktiivses arenduses) + Võta kasutusele ringhäälingukõned Koduserver ei toeta muude seadmete võrku logimise võimalust. Sisselogimine katkestati teises seadmes. See QR-kood on vigane. @@ -2859,4 +2859,35 @@ koostas küsitluse. Kasuta otsejagamist Näita viimaseid vestlusi süsteemses jagamisvaates + Tagamaks, et su konto on sinu kontrolli all, vaata andmed üle + Sul on verifitseerimata sessioone + Igaöine arendusversioon + Kasuta viimast arendusversiooni (aga võib tekkida erinevaid vigu, sealhulgas sisselogimisega) + Seda sessiooni ei saa verifitseerida, sest seal puudub krüptimise tugi. + Logi välja kõikidest oma muudest sessioonidest + Seda sessiooni ei saa verifitseerida, sest seal puudub krüptimise tugi. +\n +\nSelle sessiooniga ei saa sa osaleda krüptitud jututubades. +\n +\nParima turvalisuse ja privaatsuse nimel palun kasuta selliseid Matrix\'i kliente, mis toetavad krüptimist. + Ringhäälingukõne on eetris + Sa lõpetasid ringhäälingukõne. + %1$s lõpetas ringhäälingukõne. + Kas sa oled kindel, et soovid otseeetri lõpetada\? Sellega ringhäälingukõne salvestamine lõppeb ja salvestis on kättesaadav kõigile jututoas. + Kas lõpetame otseeetri\? + Jah, lõpetame + Seadista linki + Tekst + Link + Loo link + Muuda linki + Küsitluste ajalugu + Käimasolevad küsitlused + Selles jututoas pole käimasolevaid küsitlusi + Varasemad küsitlused + Selles jututoas pole varasemaid küsitlusi + Lülita nummerdatud loend sisse/välja + Lülita täpploend sisse/välja + Pääsuluba + Sinu pääsuluba annab täismahulise ligipääsu sinu kasutajakontole. Palun ära jaga seda teistega. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-fa/strings.xml b/library/ui-strings/src/main/res/values-fa/strings.xml index a3a74df10f..4db3812237 100644 --- a/library/ui-strings/src/main/res/values-fa/strings.xml +++ b/library/ui-strings/src/main/res/values-fa/strings.xml @@ -1,5 +1,5 @@ - + دعوت %s ‫%1$s، %2$s را دعوت کرد %1$s دعوتتان کرد @@ -2737,7 +2737,7 @@ به تنظیمات -> امنیت و محرمانگی بروید کاره را روی افزارهٔ دیگرتان بگشایید پیوند دادن با این افزاره پشتیبانی نمی‌شود. - به کار انداختن پخش صدا (زیر توسعهٔ فعّال) + به کار انداختن پخش صدا نمایش کد QR روی این افزاره آغاز در صفحهٔ ورود گزینش‌«ورود با کد QR» @@ -2849,4 +2849,54 @@ لطفاً مطمئن شوید که مبدأ این کد را می‌دانید. با پیوند دادن افزاره‌ها، دسترسی کامل را به حسابتان می‌دهید. نمایش گپ‌های اخیر در فهرست هم رسانی سامانه به کار انداختن هم‌رسانی مستقیم + درخواست این که صفحه‌کلید نباید هیچ دادهٔ شخصی‌ شده‌ای را مانند نوشتن تاریخچه و واژه‌نامه برپایهٔ آن‌چه در گفت‌وگوها می‌نویسید به‌روز کند. توجّه داشته باشید که ممکن است برخی صفحه‌کلیدها به این تنظیمات احترام نگذارند. + ${app_name} برای نمایش آگاهی‌ها نیازمند اجازه است. آگاهی می‌تواندد پیام‌ها، دعوت‌ها و…تان را نشان دهند. +\n +\nلطفا در بیرون‌پریدنی بعدی اجازهٔ دسترسی بدهید تا بتوانید آگاهی‌ها را ببینید. + کاربران دیگر در پیام‌های مستقیم و اتاق‌هایی که می‌پیوندید قادر خواهند بود سیاهه‌ای کامل از نشست‌هایتان را ببینند. +\n +\nاین کار مطمئنشان می‌کند که دارند واقعاً با شما صحبت می‌کنند؛ ولی همچنین به این معنیست که می‌توانند نام نشست‌هایی که این‌جا وارد کرده‌اید را هم ببینند. + نشست‌های تأیید شده آن‌هاییند که پس از ورود عبارت عبورتان یا تأیید هویتتان با نشست تأیید شده‌ای دیگر، واردشان شده‌اید. +\n +\nیعنی تمامی کلیدهای لارم برای رمزگشایی پیام‌های رمزنگاشته‌تان را داشته و این تأیید را به دیگران می‌دهند که به این نشست اطمینان دارید. + نشست‌های تأیید شده به حسابتان وارد و با عبارت عبور امنتان یا تأیید متقابل تأیید شده‌اند. +\n +\nیعنی کلیدهای رمزنگاری پیام‌های پیشینتان را داشته و به دیگر کاربران این تأیید را می‌دهند که این نشست، خودتان هستید. + نشست‌های تأیید نشده نشست‌هاییند که به آن‌ها وارد شده‌اید، ولی تأیید متقبالشان نکرده‌اید. +\n +\nباید به طور خاص مطمئن شوید که این نشست‌ها را می‌شناسید؛ چرا که می‌توانند نشان‌دهندهٔ استفادهٔ تأییدنشده از حسابتان باشند. + نشست‌های غیرفعّال نشست‌هاییند که مدّتیست استفاده نکرده‌اید، ولی به دریافت کلیدهای رمزنگاری ادامه می‌دهند. +\n +\nبرداشتن نشست‌های غیرفعّال امنیت و کارایی را بهبود داده و تشخیص مشکوک بودن نشست‌های جدید را برایتان راحت‌تر می‌کند. + بازبینی برای اطمینان از امن بودن حسابتان + نسشت‌هایی تأیید نشده دارید + این نشست از رمزنگاری پشتیبانی نمی‌کند؛ پس نمی‌تواند تأیید شود. +\n +\nهنگام استفاده از اسن نشست نخواهید توانست در اتاق‌هایی که رمزنگاریشان به کار افتاده شرکت کنید. +\n +\nبرای بهترین امنیت و محرمانگی، پیشنهاد می‌شود از کارخواه‌های ماتریکس دارای رمزنگاری استفاده کنید. + خروج از تمامی نشست‌های دیگر + این نشست از رمزنگاری پشتیبانی نکرده و بنابراین نمی‌تواند تأیید شود. + رفتن به جدیدترین ساخت (نکته: ممکن است برای ورود به مشکل بخورید) + ساخت شبانه + پخش زنده + به پخش صوتی پایان دادید. + %1$s به پخش صوتی پایان داد. + مطمئنید که می‌خواهید پخش زنده‌تان را قطع کنید؟ این کار پخش را پایان داده و ضبط کامل در اتاق موجود خواهد شد. + قطع پخش زنده؟ + بله، متوقّف شود + ویرایش پیوند + ایجاد پیوند + پیوند + متن + تنظیم پیوند + ژتون دسترسیتان، دسترسی کامل به حسابتان را می‌دهد. با هیچ‌کس هم‌رسانیش نکنید. + ژتون دسترسی + تغییر وضعیت سیاههٔ گلوله‌ای + تغییر وضعیت سیاههٔ شماره‌دار + هیچ‌ نظرسنجی قدیمی‌ای در این اتاق وجود ندارد + نظرسنجی‌های گذشته + هیچ نظرسنجی فعّالی در این اتاق وجود ندارد + نظرسنجی‌های فعّال + تاریخچهٔ نظرسنجی‌ها \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-fr/strings.xml b/library/ui-strings/src/main/res/values-fr/strings.xml index d74d3bac71..cb1684f834 100644 --- a/library/ui-strings/src/main/res/values-fr/strings.xml +++ b/library/ui-strings/src/main/res/values-fr/strings.xml @@ -2810,7 +2810,7 @@ 2 1 Pouvoir enregistrer et envoyer une diffusion audio dans l’historique du salon. - Activer la diffusion audio (en cours de développement) + Activer la diffusion audio Vous pouvez utiliser cet appareil pour connecter un appareil mobile ou un client web avec un QR code. Il y a deux façons de le faire : Se connecter avec un QR code Scanner le QR code @@ -2868,4 +2868,35 @@ Modification Affiche les conversations récentes dans le menu de partage du système Activer le partage direct + Vérifiez pour assurer la sécurité de votre compte + Vous avez des sessions non vérifiées + Cette session ne prend pas en charge le chiffrement, elle ne peut donc pas être vérifiée. +\n +\nVous ne pourrez pas participer dans les salons où le chiffrement est activé en utilisant cette session. +\n +\nPour de meilleures sécurité et confidentialité, il est recommandé d’utiliser des clients Matrix qui prennent en charge le chiffrement. + Déconnecter toutes les autres sessions + Cette session ne prend pas en charge le chiffrement, elle ne peut donc pas être vérifiée. + Diffusion en direct + Obtenir la toute dernière version (note : vous pourriez avoir des problèmes pour vous connecter) + Version Nightly + Vous avez terminé une diffusion audio. + %1$s a terminé une diffusion audio. + Êtes-vous sûr de vouloir arrêter votre diffusion en direct \? Cela terminera la diffusion et l’enregistrement complet sera disponible dans le salon. + Arrêter la diffusion en direct \? + Oui, arrêter + Éditer le lien + Crée un lien + Lien + Texte + Définir un lien + Votre jeton d’accès donne un accès intégral à votre compte. Ne le partagez avec personne. + Jeton d’accès + (Dés)activer la liste à puce + (Dés)activer la liste numérotée + Il n’y a aucun ancien sondage dans ce salon + Anciens sondages + Il n’y a aucun sondage en cours dans ce salon + Sondages actifs + Historique des sondages \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-hu/strings.xml b/library/ui-strings/src/main/res/values-hu/strings.xml index 1dd2134b90..1be136bb39 100644 --- a/library/ui-strings/src/main/res/values-hu/strings.xml +++ b/library/ui-strings/src/main/res/values-hu/strings.xml @@ -2702,7 +2702,7 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Munkamenet Jelenlegi munkamenet A jobb biztonság vagy megbízhatóság érdekében ellenőrizze vagy jelentkezzen ki ebből a munkamenetből. - Az aktuális munkamenet készen áll a biztonságos üzenetküldésre. + Ellenőrizd az aktuális munkamenetet a biztonságos üzenetküldéshez. Ez a munkamenet beállítva a biztonságos üzenetküldéshez. Az aktuális munkamenet készen áll a biztonságos üzenetküldésre. Közvetlen beszélgetés indítása csak az első üzenettel @@ -2813,8 +2813,8 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Biztonsági probléma lépett fel a biztonságos üzenetküldés beállításánál. Valamihez illetéktelenül fértek hozzá: Matrix szervered, Internet kapcsolatod, Eszközöd, A kérés sikertelen. Hang közvetítés felvételéhez és a szoba idővonalára küldéséhez. - Hang közvetítés engedélyezése (aktív fejlesztés alatt) - Pufferelés + Hang közvetítés engedélyezése + Pufferelés… Hang közvetítés szüneteltetése Hang közvetítés lejátszása vagy lejátszás folytatása Hang közvetítés felvétel leállítása @@ -2866,4 +2866,28 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Idézet Válasz erre: %s Szerkesztés + Tekintsd át, hogy meggyőződj arról, hogy a fiókod biztonságban van + Ellenőrizetlen bejelentkezéseid vannak + Friss beszélgetések megjelenítése a rendszer megosztó menüjében + Közvetlen megosztás engedélyezése + Szerezd be a napi összeállítást (megjegyzés: lehet, hogy problémáid lesznek a bejelentkezéssel) + Napi összeállítás + Ez a munkamenet nem támogatja a titkosítást, így nem lehet ellenőrizni sem. +\n +\nEzzel a munkamenettel nem tudsz részt venni olyan szobákban ahol a titkosítás be van kapcsolva. +\n +\nA biztonság és a adatbiztonsági okokból javasolt olyan Matrix kliens használata ami támogatja a titkosítást. + Kijelentkezés minden más munkamenetből + Ez a munkamenet nem támogatja a titkosítást, így nem lehet ellenőrizni sem. + Élő közvetítés + A hang közvetítést befejezted. + %1$s befejezte a hang közvetítést. + Biztos, hogy befejezed az élő közvetítést\? Ez befejezi a közvetítést és a felvétel az egész szoba számára elérhető lesz. + Megszakítod az élő közvetítést\? + Igen, befejez + Hivatkozás szerkesztése + Hivatkozás készítése + Hivatkozás + Szöveg + Hivatkozás beállítása \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-in/strings.xml b/library/ui-strings/src/main/res/values-in/strings.xml index da4c474689..8896037037 100644 --- a/library/ui-strings/src/main/res/values-in/strings.xml +++ b/library/ui-strings/src/main/res/values-in/strings.xml @@ -107,8 +107,8 @@ Tema Terang Tema Gelap Tema Hitam - Pemberitahuan Berisik - Pemberitahuan Tenteram + Pemberitahuan berisik + Pemberitahuan diam Laporan Gangguan Kirimkan Sticker Memuat… @@ -344,7 +344,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Nama Perangkat Terakhir terlihat %1$s @ %2$s - Otentikasi + Autentikasi Masuk sebagai Homeserver Server identitas @@ -384,17 +384,17 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Satu atau beberapa ujian gagal, coba saran yang kami tawarkan. Satu atau beberapa ujian gagal, mohon kirim laporan kutu untuk kami selidiki. Pengaturan Sistem. - Pemberitahuan diperbolehkan dalam pengaturan sistem. + Pemberitahuan diaktifkan dalam pengaturan sistem. Notifikasi dinonaktifkan dalam pengaturan sistem. \nMohon periksa pengaturan sistem anda. Buka Pengaturan Pengaturan Akun. - Pemberitahuan diperbolehkan dalam pengaturan akun Anda. + Pemberitahuan diaktifkan dalam pengaturan akun Anda. Notifikasi dinonaktifkan dalam pengaturan akun anda. \nMohon periksa pengaturan akun anda. Perbolehkan Pengaturan Perangkat. - Pemberitahuan diperbolehkan untuk sesi ini. + Pemberitahuan diaktifkan untuk sesi ini. Notifikasi tidak diaktifkan pada sesi ini. \nMohon periksa pengaturan ${app_name}. Perbolehkan @@ -439,9 +439,9 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Selesai Anda yakin ingin keluar\? Pengaturan Pemberitahuan Lanjutan - Urgensi pemberitahuan lewat kejadian + Kepentingan pemberitahuan berdasarkan peristiwa Pengaturan Sesukanya. - Perhatikan bahwa sebagian jenis pesan tersetel diam (mengeluarkan pemberitahuan tanpa suara). + Perhatikan bahwa sebagian jenis pesan disetel diam (mengeluarkan pemberitahuan tanpa suara). Sebagian pemberitahuan dimatikan dalam pengaturan Anda. [%1$s] \nError ini di luar kendali ${app_name} dan menurut Google, error ini muncul ketika terlalu banyak aplikasi terdaftar dengan FCM pada perangkat tersebut. Error ini tidak seharusnya mempengaruhi pengguna biasa. @@ -610,7 +610,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Meminta untuk konfirmasi sebelum memulai panggilan Cegah panggilan tidak disengaja - Tidak sah, tidak ada kredensial otentikasi yang absah + Tidak sah, tidak ada kredensial autentikasi yang absah Kesalahan SSL. Kesalahan SSL: identitas peer belum diverifikasi. Tidak dapat mencapai homeserver pada URL ini, silakan periksa @@ -1202,7 +1202,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Kode QR Tambah dengan kode QR Tautan disalin ke klipboard - Aktifkan geser untuk balas di linimasa + Aktifkan geser untuk balas di lini masa Cari Nama Nama atau ID (#contoh:matrix.org) Tampilkan direktori ruangan @@ -1218,7 +1218,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Mengompresi gambar… Mengirim file (%1$s / %2$s) Mengenkripsi file… - Tampilkan peristiwa tersembunyi di linimasa + Tampilkan peristiwa tersembunyi di lini masa Mengenkripsi gambar mini… Menunggu… Pesan Langsung @@ -1457,7 +1457,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Anda tidak memiliki izin untuk mengaktifkan enkripsi ujung ke ujung di ruangan ini. Aktifkan enkripsi ujung ke ujung… Editor pesan - Linimasa + Lini Masa Mengirim emote yang dicantum berwarna pelangi Mengirim pesan yang dicantum berwarna pelangi Sesi ini tidak dapat berbagi verifikasi ini dengan sesi Anda yang lain. @@ -2108,9 +2108,9 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Impor kunci dari file Buka widget Tangkap layar - Gagal mengotentikasi + Gagal mengautentikasi ${app_name} meminta Anda untuk memasukkan kredensial untuk melakukan tindakan ini. - Otentikasi Ulang Dibutuhkan + Autentikasi Ulang Dibutuhkan Geser untuk mengakhirkan panggilan Orang tak dikenal Pindah ke %1$s @@ -2119,7 +2119,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Sebuah kesalahan terjadi ketika memindahkan panggilan Pindahkan Sambungkan - Konsultasikan dulu + Konsultasi dahulu %1$s Ketuk untuk kembali Panggilan aktif (%1$s) · @@ -2314,7 +2314,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Gagal untuk memuat peta Peta Catatan: aplikasi akan dimulai ulang - Aktifkan Pesan Utasan + Aktifkan pesan utasan Hubungkan ke server Ingin bergabung ke server yang sudah ada\? Lewati pertanyaan ini @@ -2442,7 +2442,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Jangan tinggalkan apa pun Tinggalkan semuanya Hal-hal di space ini - Mainkan gambar beranimasi di linimasa ketika muncul + Mainkan gambar beranimasi di lini masa ketika muncul Mainkan gambar beranimasi secara otomatis dtk mnt @@ -2467,8 +2467,8 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Sinkronisasi latar belakang Layanan Google Pilih cara untuk menerima notifikasi - Tidak dapat mengaktifkan otentikasi biometrik. - Otentikasi biometrik dinonaktifkan karena sebuah otentikasi biometrik telah ditambahkan baru-baru ini. Anda dapat mengaktifkan ulang di Pengaturan. + Tidak dapat mengaktifkan autentikasi biometrik. + Autentikasi biometrik dinonaktifkan karena sebuah autentikasi biometrik telah ditambahkan baru-baru ini. Anda dapat mengaktifkan ulang di Pengaturan. Atur ulang metode notifikasi Tag profil: Gagal mendaftarkan token endpoint ke homeserver: @@ -2648,7 +2648,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Aktivitas terakhir %1$s Perangkat Sesi - Sesi Saat Ini + Sesi saat ini Verifikasi atau keluarkan sesi ini untuk keamanan dan keandalan yang terbaik. Verifikasi sesi Anda saat ini untuk perpesanan aman yang baik. Sesi ini siap untuk perpesanan aman. @@ -2760,8 +2760,8 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Perangkat yang lain sudah masuk. Homeserver tidak mendukung masuk dengan kode QR. Permintaan gagal. - Memungkinkan untuk merekam dan mengirim siaran suara dalam linimasa ruangan. - Aktifkan siaran suara (dalam pengembangan aktif) + Memungkinkan untuk merekam dan mengirim siaran suara dalam lini masa ruangan. + Aktifkan siaran suara Memuat… Jeda siaran suara Mainkan atau lanjutkan siaran suara @@ -2814,4 +2814,35 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Membalas ke %s Tampilkan obrolan terkini dalam menu pembagian sistem Aktifkan pembagian langsung + Periksa untuk memastikan akun Anda aman + Anda memilki sesi yang belum diverifikasi + Sesi ini tidak mendukung enkripsi, jadi ini tidak dapat diverifikasi. +\n +\nAnda tidak akan dapat berpartisipasi dalam ruangan di mana enkripsi diaktifkan ketika menggunakan sesi ini. +\n +\nUntuk keamanan dan privasi yang terbaik, kami menyarankan untuk menggunakan klien Matrix yang mendukung enkripsi. + Keluarkan semua sesi lain + Sesi ini tidak mendukung enkripsi dan tidak dapat diverifikasi. + Dapatkan bangunan terkini (catatan: Anda mungkin memiliki masalah saat masuk) + Bangunan nightly + Siaran langsung + Anda mengakhiri sebuah siaran suara. + %1$s mengakhiri sebuah siaran suara. + Apakah Anda ingin menghentikan siaran langsung Anda\? Ini akan mengakhiri siaran dan rekaman lengkap akan tersedia dalam ruangan. + Hentikan siaran langsung\? + Ya, Hentikan + Sunting tautan + Buat sebuah tautan + Tautan + Teks + Atur tautan + Token akses Anda memberikan akses penuh ke akun Anda. Jangan bagikan dengan siapa pun. + Token Akses + Saklar daftar bulat + Saklar daftar bernomor + Tidak ada pemungutan suara sebelumnya di ruangan ini + Pemungutan suara sebelumnya + Tidak ada pemungutan suara yang aktif di ruangan ini + Pemungutan suara aktif + Riwayat pemungutan suara \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-it/strings.xml b/library/ui-strings/src/main/res/values-it/strings.xml index d6a7858ebc..729b826982 100644 --- a/library/ui-strings/src/main/res/values-it/strings.xml +++ b/library/ui-strings/src/main/res/values-it/strings.xml @@ -2797,7 +2797,7 @@ Accedi con codice QR Scansiona codice QR Registra e invia trasmissioni vocali nella linea temporale della stanza. - Attiva trasmissione vocale (in sviluppo attivo) + Attiva trasmissione vocale L\'homeserver non supporta l\'accesso con codice QR. L\'accesso è stato annullato sull\'altro dispositivo. Quel codice QR non è valido. @@ -2859,4 +2859,35 @@ Modifica Mostra chat recenti nel menu di condivisione di sistema Attiva condivisione diretta + Controlla per assicurarti che l\'account sia sicuro + Hai sessioni non verificate + Questa sessione non supporta la crittografia, perciò non può essere verificata. +\n +\nNon potrai partecipare in stanze dove la crittografia è attiva mentre usi questa sessione. +\n +\nPer maggiore sicurezza e privacy, è consigliabile usare i client di Matrix che supportano la crittografia. + Disconnetti da tutte le altre sessioni + Questa sessione non supporta la crittografia, perciò non può essere verificata. + Trasmissione in diretta + Ottieni la build più recente (nota: potresti avere problemi nell\'accesso) + Nightly build + Hai terminato una trasmissione vocale. + %1$s ha terminato una trasmissione vocale. + Vuoi davvero fermare la tua trasmissione in diretta\? Verrà terminata la trasmissione e la registrazione completa sarà disponibile nella stanza. + Fermare la trasmissione in diretta\? + Sì, ferma + Modifica collegamento + Crea un collegamento + Collegamento + Testo + Imposta collegamento + Il tuo token di accesso ti dà l\'accesso al tuo account. Non condividerlo con nessuno. + Token di accesso + Attiva/disattiva elenco numerato + Attiva/disattiva elenco puntato + In questa stanza non ci sono sondaggi passati + Sondaggi passati + In questa stanza non ci sono sondaggi attivi + Sondaggi attivi + Cronologia sondaggi \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-nl/strings.xml b/library/ui-strings/src/main/res/values-nl/strings.xml index c5616ed761..5bc5305df4 100644 --- a/library/ui-strings/src/main/res/values-nl/strings.xml +++ b/library/ui-strings/src/main/res/values-nl/strings.xml @@ -47,7 +47,7 @@ \nKamers importeren Initiële synchronisatie: \nGesprekken worden geladen -\nAls je aan veel kamers deelneemt kan dit even duren +\nAls u aan veel kamers deelneemt kan dit even duren Initiële synchronisatie: \nUitgenodigde kamers worden geïmporteerd Initiële synchronisatie: @@ -73,12 +73,12 @@ %1$s heeft %2$s als kameradressen toegevoegd. - %1$s heeft %2$s als gespreksadres verwijderd. - %1$s heeft %2$s als gespreksadressen verwijderd. + %1$s heeft %2$s verwijderd als adres voor deze kamer. + %1$s heeft %2$s verwijderd als adressen voor deze kamer. - %1$s heeft %2$s als gespreksadres toegevoegd en %3$s verwijderd. - %1$s heeft het hoofdadres voor dit gesprek ingesteld op %2$s. - %1$s heeft het hoofdadres voor dit gesprek verwijderd. + %1$s heeft %2$s toegevoegd en %3$s verwijderd als adres voor deze kamer. + %1$s heeft %2$s ingesteld als het hoofdadres voor deze kamer. + %1$s heeft het hoofdadres van deze kamer verwijderd. %1$s heeft gasten de toegang tot dit gesprek verleend. %1$s heeft gasten de toegang tot het gesprek verhinderd. %1$s heeft eind-tot-eind-versleuteling ingeschakeld. @@ -787,7 +787,7 @@ Publiek Iedereen kan deelnemer worden van deze kamer Afspelen - Je hebt het hoofdadres voor dit gesprek verwijderd. + U heeft het hoofdadres van deze kamer verwijderd. Je hebt %1$s uitgenodigd. Reden: %2$s Jouw uitnodiging. Reden: %1$s Bericht verstuurd @@ -884,11 +884,11 @@ Kan stembericht niet opnemen Kan stembericht niet afspelen Je hebt gasten de toegang tot dit gesprek verleend. - Je hebt het hoofdadres voor dit gesprek ingesteld op %1$s. - Je hebt %1$s als gespreksadres toegevoegd en %2$s verwijderd. + U heeft %1$s ingesteld als hoofdadres van deze kamer. + U heeft %1$s toegevoegd en %2$s verwijderd als adressen voor deze kamer. - Je hebt %1$s als gespreksadres verwijderd. - Je hebt %1$s als gespreksadressen verwijderd. + U heeft %1$s verwijderd als adres voor deze kamer. + U heeft %1$s verwijderd als adressen voor deze kamer. Je hebt %1$s als kameradres toegevoegd. @@ -2778,7 +2778,7 @@ 2 1 In staat zijn om spraakuitzendingen op te nemen en te verzenden in de tijdlijn van de kamer. - Spraakuitzending inschakelen (in actieve ontwikkeling) + Spraakuitzending inschakelen Noteer de naam, versie en url van de applicatie om sessies gemakkelijker te herkennen in sessiebeheer. Opname van applicatie informatie inschakelen Meer zichtbaarheid en controle over al je sessies. @@ -2836,4 +2836,10 @@ %1$d geselecteerd %1$d geselecteerd + Nightly-versie + Quoten + Reageren op %s + Bewerking + Recente gesprekken in het deelmenu van het systeem tonen + Direct delen inschakelen \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-pl/strings.xml b/library/ui-strings/src/main/res/values-pl/strings.xml index 1c01c82189..4b26562b06 100644 --- a/library/ui-strings/src/main/res/values-pl/strings.xml +++ b/library/ui-strings/src/main/res/values-pl/strings.xml @@ -39,7 +39,7 @@ %1$s wysłał(a) zaproszenie do %2$s aby dołączył(a) do tego pokoju %1$s zaakceptował(a) zaproszenie dla %2$s Urządzenie nadawcy nie wysłało nam kluczy do tej wiadomości. - %s zakutalizował(a) ten pokój. + %s zaktualizował(a) ten pokój. Wstępna synchronizacja: \nImportowanie konta… Wstępna synchronizacja: @@ -345,7 +345,7 @@ Importuj klucze z lokalnego pliku Importuj Szyfruj wiadomości tylko do zaufanych sesji - Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych sesji w tym pokoju z tej sesji. + Nigdy nie wysyłaj szyfrowanych wiadomości do sesji (np urządzeń innych użytkowników) które nie zostały zweryfikowane. Aby sprawdzić czy ta sesja jest zaufana, skontaktuj się z jej właścicielem używając innych form (np. osobiście lub telefonicznie) i zapytaj czy klucz, który widzą w ustawieniach użytkownika dla tego urządzenia pasuje do klucza poniżej: Jeśli klucz pasuje, potwierdź to przyciskiem poniżej. Jeśli nie, to ktoś inny najprawdopodobniej przejmuje lub podszywa się pod tą sesję i powinieneś dodać tę sesję do czarnej listy. W przyszłości proces weryfikacji będzie bardziej skomplikowany. Wyślij naklejkę @@ -622,7 +622,7 @@ Brak sieci. Sprawdź swoje połączenie z Internetem. Proszę czekać… Tego pokoju nie można podejrzeć - Bezpieczeństwo i Prywatność + Bezpieczeństwo i prywatność Brak reguł push Oczekiwanie… Wysyłanie miniatury (%1$s / %2$s) @@ -834,8 +834,8 @@ Importowanie kluczy E2E z pliku \"%1$s\". Informacje o stronach trzecich Już wyświetlasz ten pokój! - id_aplikacji: - klucz_push: + App ID: + Push Key: wyświetlana_nazwa_aplikacji: nazwa_sesji: Url: @@ -1370,7 +1370,7 @@ Twój administrator serwera zablokował domyślne szyfrowanie punkt-punkt (e2e) w pokojach prywatnych w Wiadomościach Bezpośrednich. Nie masz uprawnień żeby uaktywnić szyfrowanie w tym pokoju. Wiadomość bezpośrednia - Domyślnie w %1$s + Zwykły w %1$s Opuść Ustawienia Operacje administratora @@ -2398,7 +2398,7 @@ Wybierz, gdzie prowadzone są Twoje rozmowy, dając Ci kontrolę i niezależność. Połączenie przez sieć Matrix. Bezpieczna i niezależna komunikacja, która zapewnia ten sam poziom prywatności, co rozmowa twarzą w twarz we własnym domu. Komunikacja dla Twojego zespołu. - Położenie + Lokalizacja Zagadnienia prawne Już przeglądasz ten wątek! Wyświetl w pokoju @@ -2743,4 +2743,59 @@ %s \nwygląda nieco pusto. Brak przestrzeni. + ⚠ W tym pokoju znajdują się użytkownicy ze niezweryfikowanymi urządzeniami, nie będą one mogły odszyfrować wiadomości które wysyłasz. + Udziel Uprawnień + ${app_name} potrzebuje uprawnień aby wyświetlać powiadomienia. +\nProszę udziel uprawnień. + ${app_name} potrzebuje uprawnień by wyświetlić powiadomienia. Będą one pokazywać twoje wiadomości, zaproszenia, itd. +\n +\nProszę zezwól na dostęp na następnym pop-upie aby móc zobaczyć powiadomienia. + Tutaj będą twoje nowe zaproszenia. + Brak zaproszeń. + Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych sesji w tym pokoju. + Wyloguj się z tej sesji + + Wyloguj się z %1$d sesji + Wyloguj się z %1$d sesji + + + + Wyloguj się + Wybierz sesje + Pokaż adres IP + Niezweryfikowane sesje + Brak niezweryfikowanych sesji. + Zweryfikuj swoje sesje dla zwiększenia bezpieczeństwa wiadomości lub wyloguj się z tych których nie rozpoznajesz lub już nie używasz. + Niezweryfikowane + Zweryfikuj te sesje lub wyloguj się z nich. + Niezweryfikowane sesje + Popraw swoje bezpieczeństwo stosując te zalecenia. + Zalecenia bezpieczenstwa + Pokaż ostatnie rozmowy w systemowym menu udostępniania + Bezpośrednie udostępnianie + Bądź w stanie nagrywać i wysyłać transmisje głosowe na osi czasu pokoju. + Włącz transmicje głosowe + Zachowuj nazwę aplikacji, wersję oraz jej url aby łatwiej rozpoznawać je w menedzerze sesji. + Włącz rejestrowanie informacji o kliencie + Miej lepszą kontrolę nad zalogowanymi sesjami. + Włącz nowy manager sesji + Wypróbuj zaawansowany edytor tekstu (tryb zwykłego tekstu dostępny wkrótce) + Włącz zaawansowany edytor tekstu + Formatowanie tekstu + Ankiety + Transmisja Głosowa + Załączniki + Naklejki + Galeria + Coś poszło nie tak. Sprawdź swoje połączenie i spróbuj ponownie. + Cytowanie + Odpowiadanie %s + Edytowanie + Otwórz ekran narzędzi programisty + 🔒 Włączyłeś ograniczenie szyfrowania tylko dla zweryfikowanych sesji dla wszystkich pokojów w ustawieniach bezpieczeństwa. + Odznacz wszystko + Zaznacz wszystko + Rozumiem + Zwiń %s pokojów + Rozwiń %s pokojów \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml index 8129a234fb..a5aa778156 100644 --- a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml +++ b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml @@ -3,26 +3,26 @@ Convite de %s %1$s convidou %2$s %1$s convidou você - %1$s juntou-se à sala + %1$s entrou na sala %1$s saiu da sala - %1$s rejeitou o convite + %1$s recusou o convite %1$s removeu %2$s %1$s desbaniu %2$s %1$s baniu %2$s - %1$s retirou o convite de %2$s - %1$s mudou o avatar dela(e) - %1$s definiu o nome de exibição dela(e) para %2$s - %1$s mudou o nome de exibição dela(e) de %2$s para %3$s - %1$s removeu o nome de exibição dela(e) (era %2$s) + %1$s desfez o convite para %2$s + %1$s mudou seu avatar + %1$s definiu seu nome de exibição para %2$s + %1$s mudou seu nome de exibição de %2$s para %3$s + %1$s removeu seu nome de exibição (era %2$s) %1$s mudou o tópico para: %2$s %1$s mudou o nome da sala para: %2$s %s começou uma chamada de vídeo. %s começou uma chamada de voz. %s atendeu a chamada. - %s terminou a chamada. - %1$s fez histórico futuro da sala visível para %2$s - todos os membros da sala, do ponto que foram convidados. - todos os membros da sala, do ponto que se juntaram. + %s encerrou a chamada. + %1$s tornou o histórico futuro da sala visível para %2$s + todos os membros da sala, a partir do ponto que foram convidados. + todos os membros da sala, a partir do ponto que entraram. todos os membros da sala. qualquer pessoa. (avatar mudou também) @@ -43,13 +43,13 @@ %1$s criou a sala Você criou a sala Você convidou %1$s - Você juntou-se à sala + Você entrou na sala Você saiu da sala - Você rejeitou o convite + Você recusou o convite Você removeu %1$s Você desbaniu %1$s Você baniu %1$s - Você retirou o convite de %1$s + Você desfez o convite para %1$s Você mudou seu avatar Você definiu seu nome de exibição para %1$s Você mudou seu nome de exibição de %1$s para %2$s @@ -63,8 +63,8 @@ %s enviou dados para configurar a chamada. Você enviou dados para configurar a chamada. Você atendeu a chamada. - Você terminou a chamada. - Você fez histórico futuro da sala visível para %1$s + Você encerrou a chamada. + Você tornou o histórico futuro da sala visível para %1$s %s fez o upgrade desta sala. Você fez o upgrade desta sala. Você removeu o nome da sala @@ -89,31 +89,31 @@ Você mudou o nível de poder de %1$s. %1$s mudou o nível de poder de %2$s. %1$s de %2$s para %3$s - Sinc inicial: + Sincronização inicial: \nImportando conta… - Sinc inicial: -\nImportando crypto - Sinc inicial: + Sincronização inicial: +\nImportando criptografia + Sincronização inicial: \nImportando salas - Sinc inicial: + Sincronização inicial: \nCarregando suas conversas -\nSe você tem se juntado a muitas salas, isto podia levar um tempo - Sinc inicial: -\nImportando salas convidadas - Sinc inicial: -\nImportando salas saídas - Sinc inicial: +\nSe você entrou em muitas salas, isso pode demorar + Sincronização inicial: +\nImportando salas para as quais foi convidado + Sincronização inicial: +\nImportando salas das quais saiu + Sincronização inicial: \nImportando dados de conta Enviando mensagem… - Convite de %1$s. Razão: %2$s - Seu convite. Razão: %1$s - %1$s convidou %2$s. Razão: %3$s - Você convidou %1$s. Razão: %2$s - %1$s convidou você. Razão: %2$s - %1$s juntou-se à sala. Razão: %2$s - Você juntou-se à sala. Razão: %1$s - %1$s saiu da sala. Razão: %2$s - Você saiu da sala. Razão: %1$s + Convite de %1$s. Motivo: %2$s + Seu convite. Motivo: %1$s + %1$s convidou %2$s. Motivo: %3$s + Você convidou %1$s. Motivo: %2$s + %1$s convidou você. Motivo: %2$s + %1$s entrou na sala. Motivo: %2$s + Você entrou na sala. Motivo: %1$s + %1$s saiu da sala. Motivo: %2$s + Você saiu da sala. Motivo: %1$s %1$s rejeitou o convite. Razão: %2$s Você rejeitou o convite. Razão: %1$s %1$s expulsou %2$s. Razão: %3$s @@ -160,22 +160,22 @@ %1$s tem prevenido visitantes de se juntarem à sala. Você tem permitido visitantes se juntarem aqui. %1$s tem permitido visitantes se juntarem aqui. - Você saiu. Razão: %1$s - %1$s saiu. Razão: %2$s - Você juntou-se. Razão: %1$s - %1$s juntou-se. Razão: %2$s + Você saiu. Motivo: %1$s + %1$s saiu. Motivo: %2$s + Você entrou. Motivo: %1$s + %1$s entrou. Motivo: %2$s Você revogou o convite para %1$s %1$s revogou o convite para %2$s Você convidou %1$s %1$s convidou %2$s Você fez o upgrade aqui. %s fez o upgrade aqui. - Você fez mensagens futuras visíveis para %1$s - %1$s fez mensagens futuras visíveis para %2$s + Você tornou as mensagens futuras visíveis para %1$s + %1$s tornou as mensagens futuras visíveis para %2$s Você saiu da sala %1$s saiu da sala - Você juntou-se - %1$s juntou-se + Você entrou + %1$s entrou Você criou a discussão %1$s criou a discussão Sala vazia (era %s) @@ -408,7 +408,7 @@ Qualquer pessoa Membros somente (desde o ponto no tempo de seleção desta opção) Membros somente (desde que eles foram convidados) - Membros somente (desde que eles se juntaram) + Membros somente (desde que eles entraram) Usuárias(os) banidas(os) Avançadas ID interno desta sala @@ -480,7 +480,7 @@ Som de notificação Mnsgns contendo meu nome de exibição Mnsgns contendo meu nome de usuária(o) - Previsualização de URL emlinha + Previsualização de URL inline Mostrar timestamps em formato de 12 horas Vibrar ao mencionar um/uma usuário(a) Analítica @@ -1698,7 +1698,7 @@ Somente faça isto se você não tem nenhum outro dispositivo com o qual você pode verificar este dispositivo. Resettar tudo Esqueceu ou perdeu todas as opções de recuperação\? Resette tudo - Você juntou-se. + Você entrou. Mensagens neste chat são encriptadas ponta-a-ponta. Sair Configurações @@ -1722,7 +1722,7 @@ Resettar %1$s fez isto somente convite. Você fez isto somente convite. - %s juntou-se. + %s entrou. Filtrar usuárias(os) banidas(os) Testar Push @@ -1898,10 +1898,10 @@ Diretório de salas Novo valor Alterar - Sinc inicial: + Sincronização inicial: \nFazendo download de dados… - Sinc inicial: -\nEsperando por resposta de servidor… + Sincronização inicial: +\nEsperando pela resposta do servidor… Nível de confiança confiado Nível de confiança alerta Você tem certeza que você quer deletar todas as mensagens não-enviadas nesta sala\? @@ -2009,8 +2009,8 @@ Adicionar salas Explorar salas - %d pessoa que você conhece já tem se juntado - %d pessoas que você conhece já têm se juntado + %d pessoa que você conhece já entrou + %d pessoas que você conhece já entraram Juntar-Se a Espaço Criar espaço @@ -2356,7 +2356,7 @@ Falha para carregar mapa Mapa Nota: app vai ser recomeçado - Habilitar Mensagens de Thread + Habilitar mensagens com threads Conectar a servidor Procurando se juntar a um servidor existente\? Pular esta pergunta @@ -2470,7 +2470,7 @@ \n%s \n \nNote que esta ação vai recomeçar o app e pode levar algum tempo. - Requisição de sinc inicial + Requisição de sincronização inicial Mostrar a info de perfil mais recente (avatar e nome de exibição) para todas as mensagens. Mostrar info de usuária(o) mais recente Ocupada(o) @@ -2700,7 +2700,7 @@ Última atividade %1$s Dispositivo Sessão - Sessão Atual + Sessão atual Verifique ou faça signout desta sessão para melhor segurança e fiabilidade. Verifique sua sessão atual para mensageria segura melhorada. Esta sessão está pronta para mensageria segura. @@ -2813,8 +2813,8 @@ Um problema de segurança foi encontrado ao configurar mensageria segura. Um dos seguintes pode ter sido comprometido: Seu servidorcasa; Sua(s) conexão(ões) de internet; Seu(s) dispositivo(s); A requisição falhou. Seja capaz de gravar e enviar broadcast de voz em timeline de sala. - Broadcast de voz (sob desenvolvimento ativo) - Buffering + Broadcast de voz + Buffering… Pausar broadcast de voz Tocar ou retomar broadcast de voz Parar gravação de broadcast de voz @@ -2833,8 +2833,8 @@ Desselecionar todas(os) Selecionar todas(os) - %1$d selecionada(o) - %1$d selecionadas(os) + %1$d selecionado(a) + %1$d selecionados(as) Alguma outra pessoa já está gravando um broadcast de voz. Espere que o broadcast de voz dela termine para começar um novo. Alternar modo de tela cheia @@ -2866,4 +2866,28 @@ Citando Respondendo a %s Editando + Mostrar chats recentes no menu de compartilhar do sistema + Habilitar compartilhar direto + Esta sessão não suporta encriptação, então ela não pode ser verificada. +\n +\nVocê não vai ser capaz de participar em salas onde encriptação está habilitada quando usando esta sessão. +\n +\nPara a melhor segurança e privacidade, é recomendado usar cliente Matrix que suportam encriptação. + Fazer signout de todas as outras sessões + Esta sessão não suporta encriptação e assim não pode ser verificada. + Revise para assegurar que sua conta está segura + Você tem sessões não-verificadas + Obtenha a build mais recente (note: você pode ter problema para fazer signin) + Build nightly + Broadcast ao vivo + Você terminou um broadcast de voz. + %1$s terminou um broadcast de voz. + Editar link + Criar um link + Link + Texto + Definir link + Tem certeza que você quer parar seu broadcast ao vivo\? Isto vai terminar o broadcast e a gravação completa vai estar disponível na sala. + Parar de fazer broadcasting ao vivo\? + Sim, Parar \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-ru/strings.xml b/library/ui-strings/src/main/res/values-ru/strings.xml index 39d1c8de2b..0d8f1103fe 100644 --- a/library/ui-strings/src/main/res/values-ru/strings.xml +++ b/library/ui-strings/src/main/res/values-ru/strings.xml @@ -3,7 +3,7 @@ приглашение %s %1$s пригласил(а) %2$s %1$s пригласил(а) вас - %1$s вошёл(ла) в комнату + %1$s вошёл(шла) в комнату %1$s покинул(а) комнату %1$s отклонил(а) приглашение %1$s выгнан %2$s @@ -60,7 +60,7 @@ Приглашение %1$s. Причина: %2$s %1$s приглашен %2$s. Причина: %3$s %1$s пригласил вас. Причина: %2$s - %1$s вошёл(ла) в комнату. Причина: %2$s + %1$s присоединился(лась) к комнате. Причина: %2$s %1$s покинул(а) комнату. Причина: %2$s %1$s отклонил приглашение. Причина: %2$s %1$s выгнали %2$s. Причина: %3$s @@ -256,7 +256,7 @@ Переименовать Пожаловаться на содержимое или - Приглашение + Пригласить Выйти из учётной записи Голосовой вызов Видео вызов @@ -368,7 +368,7 @@ Системные настройки приложения. Сведения о приложении Уведомления для этой учётной записи - Уведомления для этой сессии + Уведомления для этого сеанса В персональных чатах В групповых чатах Когда меня приглашают в комнату @@ -432,8 +432,8 @@ Сбросить основной адрес Ошибка дешифровки Публичное название - ID сессии - Ключ сессии + ID сеанса + Ключ сеанса Экспорт E2E ключей Экспорт ключей Экспорт ключей в локальный файл @@ -444,8 +444,8 @@ Импорт ключей Импортировать ключи из локального файла Импорт - Шифровать только для проверенных сессий - Не отправлять зашифрованные сообщения непроверенным сессиям с этой сессии. + Шифровать только для заверенных сеансов + Не отправлять зашифрованные сообщения незаверенным сеансам из этого сеанса. Не заверено Заверено Подтвердить @@ -494,7 +494,7 @@ Вызов Сообщения, содержащие мое имя пользователя Вы добавили новою сессию \'%s\', запрашивающую ключи шифрования. - Ваше непроверенная сессия \'%s\' запрашивает ключи шифрования. + Ваш незаверенный сеанс \'%s\' запрашивает ключи шифрования. Сообщения, содержащие моё отображаемое имя Начать проверку Звуковые уведомления @@ -661,9 +661,9 @@ Уведомления отключены для вашей учетной записи. \nПожалуйста, проверьте настройки аккаунта. Включить - Настройки сессии. + Настройки сеанса. Уведомления включены для этой сессии. - Уведомления не включены для этой сессии. + Уведомления не включены для этого сеанса. \nПожалуйста, проверьте настройки ${app_name}. Включить Проверка сервисов Play @@ -744,15 +744,15 @@ Удалить резервную копию ключей шифрования с сервера\? Вы больше не сможете использовать бумажный ключ для чтения истории зашифрованных сообщений. Удалить резервную копию Удаление резервной копии… - Чтобы использовать резервное копирование ключей в этой сессии, восстановите их с помощью мнемонической фразы или бумажного ключа. + Чтобы использовать резервное копирование ключей в этом сеансе, восстановите их с помощью мнемонической фразы или бумажного ключа. Резервная копия имеет недействительную подпись из подтвержденной сессии %s Резервная копия имеет действительную подпись из неподтвержденной сессии %s Резервная копия имеет действительную подпись из подтверждённой сессии %s. Резервная копия имеет действительную подпись с этой сессии. Резервная копия подписана сессией с идентификатором %s. Резервные копии ключей этой сессии не сохраняются. - Резервное копирование ключей не активировано в этой сессии. - Резервное копирование ключей успешно настроено для этой сессии. + Резервное копирование ключей не активировано в этом сеансе. + Резервное копирование ключей успешно настроено для этого сеанса. Удалить резервную копию Восстановить из резервной копии Пожалуйста, введите бумажный ключ @@ -866,14 +866,14 @@ неизвестный IP ** Отправить не удалось — пожалуйста, откройте комнату К сожалению, конференц-звонки с Jitsi не поддерживаются на старых устройствах (ниже Android OS - 6.0) - Новая сессия запрашивает ключи шифрования. -\nИмя сессии: %1$s -\nПоследний раз в сети: %2$s -\nЕсли вы не вошли с другой сессии, проигнорируйте этот запрос. - Непроверенная сессия запрашивает ключи шифрования. -\nИмя сессии: %1$s -\nПоследний раз в сети: %2$s -\nЕсли вы не открывали новую сессию - проигнорируйте этот запрос. + Новый сеанс запрашивает ключи шифрования. +\nНазвание сеанса: %1$s +\nПоследний раз в сети: %2$s +\nЕсли вы не входили в другой сеанс, проигнорируйте этот запрос. + Незаверенный сеанс запрашивает ключи шифрования. +\nНазвание сеанса: %1$s +\nПоследний раз в сети: %2$s +\nЕсли вы не входили в другой сеанс, проигнорируйте этот запрос. Поделиться Запрос поделится ключом Игнорировать @@ -932,7 +932,7 @@ ID приложения: Ключ Push: Отображаемое название приложения: - Отображаемое название сессии: + Отображаемое название сеанса: Url: Формат: Голос и видео @@ -1185,8 +1185,8 @@ Режим разработчика Режим разработчика активирует скрытые функции, а также может сделать приложение менее стабильным. Только для разработчиков! Настройки - Текущая сессия - Другие сессии + Текущий сеанс + Другие сеансы Включить шифрование Недоверенный вход Вложения @@ -1230,8 +1230,8 @@ Сообщение… Доступно обновление шифрования Проверьте себя и других для защиты ваших бесед - Подтвердите вход - Подтвердите свою личность и получите доступ к зашифрованным сообщениям, подтвердив этот вход в другой сессии. + Заверьте сеанс + Подтвердите свою личность и получите доступ к зашифрованным сообщениям, сверив этот сеанс с другим вашим сеансом. Лента сообщений Ключ сообщения Распечатайте его и храните в безопасном месте @@ -1390,7 +1390,7 @@ Вы приняли Подтверждение отправлено Запрос на подтверждение - Заверьте эту сессию + Заверьте этот сеанс Сканируйте код с помощью устройства другого пользователя, чтобы безопасно проверить друг друга Сканировать их код Невозможно сканировать @@ -1429,7 +1429,7 @@ Сравните уникальные эмодзи, убедившись, что они появились в том же порядке. Сравните код с тем, который отображается на экране другого пользователя. Сообщения от этого пользователя зашифрованы сквозным шифрованием и не смогут быть прочитаны третьими лицами. - Ваша новая сессия подтверждена. Она имеет доступ к вашим зашифрованным сообщениям, и другие пользователи будут воспринимать её как заверенную. + Ваш новый сеанс заверен. Он имеет доступ к вашим зашифрованным сообщениям, и другие пользователи будут воспринимать его как заверенный. Перекрёстная подпись Перекрёстная подпись включена \nЛичные ключи хранятся на устройстве. @@ -1440,17 +1440,17 @@ \nКлючи не являются доверенными Перекрестная подпись выключена Администратор вашего сервера отключил сквозное шифрование по умолчанию в приватных комнатах и личных сообщениях. - Активные сессии - Показать все сессии - Управление сессиями - Выйти из этой сессии + Активные сеансы + Показать все сеансы + Управление сеансами + Выйти из этого сеанса Нет доступной криптографической информации %d сессия активна %d сессии активны %d сессий активно - Заверьте эту сессию + Заверьте этот сеанс Используйте существующую сессию для подтверждения этой, предоставив ей доступ к зашифрованным сообщениям. Инструменты для разработчиков Данные учётной записи @@ -1477,7 +1477,7 @@ Заверено Предупреждение Не удалось получить список сессий - Сессии + Сеансы Заверенная Незаверенная Эта сессия является доверенной для безопасного обмена сообщениями, так как %1$s (%2$s) проверил(а) его: @@ -1744,7 +1744,7 @@ Сбросить всё Забыли или потеряли все варианты восстановления\? Сбросить всё Вы вошли. - %s вошёл(ла). + %s вошёл(шла). Сообщения в этой переписке защищены сквозным шифрованием. Покинуть Настройки @@ -2417,7 +2417,7 @@ \n \nВы можете ознакомиться со всеми нашими условиями %s. Помогите улучшить ${app_name} - Сессия завершена! + Сеанс завершён! Комната покинута! Шифрование неправильно настроено, поэтому вы не можете отправлять сообщения. Нажмите, чтобы открыть настройки. Шифрование настроено неправильно, поэтому вы не можете отправлять сообщения. Пожалуйста, обратитесь к администратору, чтобы восстановить работу шифрования. @@ -2634,7 +2634,7 @@ Где хранятся ваши переписки Где будут храниться ваши переписки Должно быть 8 или более символов - Не удалось подтвердить эту сессию + Не удалось заверить этот сеанс Невозможно открыть эту ссылку: сообщества были заменены пространствами Имя пользователя / Почта / Телефон Следуйте инструкциям, отправленным на %s @@ -2661,9 +2661,9 @@ Не удалось загрузить карту \nВозможно, этот домашний сервер не настроен для отображения карт. Все беседы - Для лучшей безопасности заверьте свои сессии и выйдите из тех, которые более не признаёте или не используете. - Другие сессии - Сессии + Для лучшей защиты заверьте свои сеансы и выйдите из тех, которые более не признаёте или не используете. + Другие сеансы + Сеансы Создать беседу или комнату ЛС Настройки вида @@ -2680,41 +2680,41 @@ Создать комнату Посмотреть все (%1$d) Повысьте безопасность учётной записи, следуя этим рекомендациям. - Заверенная · Последняя активность %1$s - Незаверенная сессия - Заверенная сессия + Заверенный · Последняя активность %1$s + Незаверенный сеанс + Заверенный сеанс Неизвестный тип устройства Компьютер Мобильный - Незаверенная · Последняя активность %1$s + Незаверенный · Последняя активность %1$s Рекомендации по безопасности - Незаверенные сессии - Неактивные сессии + Незаверенные сеансы + Неактивные сеансы Добро пожаловать в ${app_name}, \n%s. Оставить отзыв - Название сессии + Название сеанса Неактивные IP-адрес Последняя активность - Сведения о сессии - Для лучшей безопасности выйдите из всех сессий, которые более не признаёте или не используете. + Сведения о сеансе + Для лучшей безопасности выйдите из всех сеансов, которые более не признаёте или не используете. Заверенные - Все сессии + Все сеансы Последняя активность %1$s Устройство - Сессия - Текущая сессия - Заверить сессию + Сеанс + Текущий сеанс + Заверить сеанс Подробности - Эта сессия готова к безопасному обмену сообщениями. - Текущая сессия готова к безопасному обмену сообщениями. + Этот сеанс готов к защищенной переписке. + Текущий сеанс готов к защищенной переписке. Веб-браузер Пространства — это новый способ организации комнат и людей. Создайте пространство, чтобы начать. Новый вид Нечего отображать. Здесь будут отображаться непрочитанные сообщения, когда таковые будут. - Присущий системе + Как в системе Смена пространства Упрощённый Element с дополнительными вкладками Добро пожаловать в новый вид! @@ -2722,18 +2722,18 @@ \nвыглядит слегка пустовато. Попробовать Информация о приложении, устройстве и активности. - Подтвердите текущую сессию для более безопасного обмена сообщениями. + Заверьте текущий сеанс для усиления защиты переписки. Пока нет пространств. - Подтвердите свои сессии для более безопасного обмена сообщениями или выйдите из тех, которые более не признаёте или не используете. - Подтвердите или выйдите из незаверенных сессий. - Подтвердите или выйдите из этой сессии для лучшей безопасности и надёжности. + Заверьте свои сеансы для усиления защиты переписки или выйдите из тех, которые более не признаёте или не используете. + Заверьте или выйдите из незаверенных сеансов. + Заверьте или выйдите из этого сеанса для лучшей безопасности и надёжности. Ничего нового. - Заверенных сессий не обнаружено. - Незаверенных сессий не обнаружено. - Неактивных сессий не обнаружено. + Заверенных сеансов не обнаружено. + Незаверенных сеансов не обнаружено. + Неактивных сеансов не обнаружено. Очистить фильтр - Не готовы к безопасному обмену сообщениями - Готовы к безопасному обмену сообщениями + Не готовы к защищенной переписке + Готовы к защищенной переписке Неактивны %1$d день или дольше Неактивны %1$d дня или дольше @@ -2742,32 +2742,32 @@ Незаверенные Фильтр - Незаверенная · Текущая сессия - Переименовать сессию - Название сессии + Незаверенный · Текущий сеанс + Переименовать сеанс + Название сеанса Заверенные - Выйти из этой сессии + Выйти из этого сеанса Неактивные Незаверенные - Пожалуйста, имейте в виду, что названия сессий также видны людям, с которыми вы общаетесь. - Заверенные сессии - Незаверенные сессии - Неактивные сессии + Пожалуйста, имейте в виду, что названия сеансов также видны людям, с которыми вы общаетесь. + Заверенные сеансы + Незаверенные сеансы + Неактивные сеансы Добавляет (╯°□°)╯︵ ┻━┻ в начало сообщения Приватная клавиатура Запрещает клавиатуре обновлять персональные данные, такие как история набора текста и словарь, на основе того, что вы набрали при общении. Обратите внимание, что некоторые клавиатуры могут не соблюдать эту настройку. Понятно 🔒 В настройках безопасности вы включили шифрование только для заверенных сессий во всех комнатах. - Не отправлять зашифрованные сообщения незаверенным сессиям в этой комнате. - Неактивные сессии — это сессии, которыми вы не пользовались определённое время, но они продолжают получать ключи шифрования. + Не отправлять зашифрованные сообщения незаверенным сеансам в этой комнате. + Неактивные сеансы — это сеансы, которыми вы не пользовались определённое время, но они продолжают получать ключи шифрования. \n -\nУдаление неактивных сессий повышает безопасность и производительность, а также облегчает выявление подозрительных новых сессий. - Переименование сессий - Другие пользователи в личных сообщениях и комнатах, к которым вы присоединились, могут просматривать весь список ваших сессий. +\nУдаление неактивных сеансов повышает безопасность и производительность, а также облегчает выявление подозрительных новых сеансов. + Переименование сеансов + Другие пользователи в личных сообщениях и комнатах, к которым вы присоединились, могут просматривать весь перечень ваших сеансов. \n -\nЭто даёт им уверенность в том, что они действительно общаются с вами, но это также означает, что они могут видеть название сессии, которое вы ввели здесь. +\nЭто даёт им уверенность в том, что они действительно общаются с вами, но это также означает, что они могут видеть название сеанса, которое вы ввели здесь. Наглядный текстовый редактор - ID сессии: + ID сеанса: Уведомления Получать push-уведомления в этой сессии. URL-адрес @@ -2777,25 +2777,25 @@ Веб-браузер Модель Операционная система - Новый менеджер сессий + Новый менеджер сеансов - Рассмотрите возможность выхода из старых сессий (%1$d день или дольше), которые вы более не используете. - Рассмотрите возможность выхода из старых сессий (%1$d дня или дольше), которые вы более не используете. - Рассмотрите возможность выхода из старых сессий (%1$d дней или дольше), которые вы более не используете. - Рассмотрите возможность выхода из старых сессий (%1$d дней или дольше), которые вы более не используете. + Рассмотрите возможность выхода из старых сеансов (%1$d день или дольше), которые вы более не используете. + Рассмотрите возможность выхода из старых сеансов (%1$d дня или дольше), которые вы более не используете. + Рассмотрите возможность выхода из старых сеансов (%1$d дней или дольше), которые вы более не используете. + Рассмотрите возможность выхода из старых сеансов (%1$d дней или дольше), которые вы более не используете. Результаты будут видны после завершения опроса Доступ к пространствам (внизу справа) быстрее и проще, чем когда-либо прежде. Доступ к пространствам - Рассмотрите возможность выхода из старых сессий (%1$d день или дольше), которые вы более не используете. - Рассмотрите возможность выхода из старых сессий (%1$d дня или дольше), которые вы более не используете. - Рассмотрите возможность выхода из старых сессий (%1$d дней или дольше), которые вы более не используете. - Рассмотрите возможность выхода из старых сессий (%1$d дней или дольше), которые вы более не используете. + Рассмотрите возможность выхода из старых сеансов (%1$d день или дольше), которые вы более не используете. + Рассмотрите возможность выхода из старых сеансов (%1$d дня или дольше), которые вы более не используете. + Рассмотрите возможность выхода из старых сеансов (%1$d дней или дольше), которые вы более не используете. + Рассмотрите возможность выхода из старых сеансов (%1$d дней или дольше), которые вы более не используете. Голосовая трансляция - Голосовые трансляции (в активной разработке) - Записывает название клиента, версию и URL-адрес для более лёгкого распознавания сессий в менеджере сессий. + Голосовые трансляции + Записывает название клиента, версию и URL-адрес для более лёгкого распознавания сеансов в менеджере сеансов. Записывать информацию о клиенте Галерея Наклейки @@ -2847,12 +2847,12 @@ Показать QR-код на этом устройстве Выберите «Сканировать QR-код» Начните с экрана входа - Выберите «Войти при помощи QR-кода» + Выберите «Войти по QR-коду» Начните с экрана входа Выберите «Показать QR-код» Зайдите в Настройки -> Безопасность и Приватность Откройте приложение с другого устройства - Домашний сервер не поддерживает вход при помощи QR-кода. + Домашний сервер не поддерживает вход по QR-коду. Вход был отменён с другого устройства. Этот QR-код не работает. Другое устройство должно войти в учётную запись. @@ -2867,7 +2867,7 @@ Безопасное соединение установлено Сканируйте QR-код снизу при помощи устройства, с которого вы вышли с учётной записи. Используйте устройство, с которого вы вошли в учётную запись, чтобы сканировать QR-код снизу: - Войти при помощи QR-кода + Войти по QR-коду Используйте камеру на этом устройстве, чтобы сканировать QR-код, отображённый на вашем другом устройстве: Сканировать QR-код 3 @@ -2879,26 +2879,26 @@ Пространства — новый способ групировать комнаты и людей. Добавьте существующую комнату или создайте новую, используя кнопку слева снизу. Возможность записывать и отправлять голосовые трансляции в ленту комнаты. Получите лучший надзор и контроль над всеми вашими сессиями. - Подтверждённые сессии есть везде, где вы используете эту учётную запись, после введения вашего пароля или подтверждения вашей личности при помощи другой подтверждённой сессии. + Заверенные сеансы есть везде, где вы используете эту учётную запись после ввода своей мнемонической фразы или подтверждения своей личности с помощью другого заверенного сеанса. \n -\nЭто значит, что у вас есть все нужные ключи, чтобы разблокировать зашифрованные сообщения и даёте другим пользователям знать, что вы доверяете этой сессии. - Подтверждённые сессии вошли при помощи ваших учётных данных и были подтверждены, либо при помощи вашего безопасного пароля, либо при помощи подтверждения с другого устройства. +\nЭто означает, что у вас есть все ключи, необходимые для разблокировки ваших зашифрованных сообщений и подтверждения другим пользователям, что вы доверяете этому сеансу. + Заверенные сеансы — сеансы, которые вошли в систему с вашими учётными данными, а затем были заверены либо мнемонической фразой (бумажным ключом), либо путём перекрёстной сверки. \n -\nЭто значит, что на них находятся ключи шифрования для ваших предыдущих сообщений и дают другим пользователям знать, что эти сессии действительно принадлежат вам. - Неподтверждённые сессии — это сессии, которые вошли при помощи ваших учётных данных, но не были подтверждены. +\nЭто означает, что они хранят ключи шифрования от ваших предыдущих сообщений и подтверждают другим пользователям, с которыми вы общаетесь, что эти сеансы — действительно ваши. + Незаверенные сеансы — это сеансы, которые вошли в систему с вашими учётными данными, но не были перекрёстно заверены. \n -\nВы должны удостовериться, что узнаёте эти сессии, так как они могут быть несанкционированным входом в вашу учётную запись. +\nВы должны быть особенно уверены, что признаёте эти сеансы, поскольку они могут представлять собой несанкционированное использование вашей учётной записи. Вы можете использовать это устройство для входа с телефона или веб-устройства при помощи QR-кода. Для этого есть два способа: - Войти при помощи QR-кода + Войти по QR-коду Собственные названия сессий помогут вам легче распознать свои девайсы. - Выйти из %1$d сессии - Выйти из %1$d сессий - Выйти из %1$d сессий - Выйти из %1$d сессий + Выйти из %1$d сеанса + Выйти из %1$d сеансов + Выйти из %1$d сеансов + Выйти из %1$d сеансов Выйти - Выбрать сессии + Выбрать сеансы Фильтр Неактивен %1$d+ день (%2$s) @@ -2921,7 +2921,7 @@ Не получилось начать новую голосовую трансляцию Перемотать вперёд на 30 секунд Перемотать назад на 30 секунд - Буферизация + Буферизация… Приостановить голосовую трансляцию Проиграть или продолжить голосовую трансляцию Остановить запись голосовой трансляции @@ -2963,4 +2963,17 @@ Цитируя В ответ на %s Редактирование + Показывать последние беседы в системном меню распостранения + Включить прямое распостранение + Выйти из всех других сеансов + У вас есть незаверенные сеансы + Ночная сборка + Этот сеанс не поддерживает шифрование, поэтому его невозможно заверить. +\n +\nПри использовании этого сеанса вы не сможете участвовать в комнатах, где включено шифрование. +\n +\nДля лучшей защиты и приватности рекомендуется использовать клиенты Matrix, поддерживающие шифрование. + Этот сеанс не поддерживает шифрование и поэтому не может быть заверен. + %1$s завершил(а) голосовую трансляцию. + Вы завершили голосовую трансляцию. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml index f59073c5db..34155ba6a5 100644 --- a/library/ui-strings/src/main/res/values-sk/strings.xml +++ b/library/ui-strings/src/main/res/values-sk/strings.xml @@ -2867,7 +2867,7 @@ Pri nastavovaní zabezpečeného zasielania správ sa vyskytol bezpečnostný problém. Jedna z nasledujúcich možností môže byť kompromitovaná: Váš domovský server; Vaše internetové pripojenie (pripojenia); Vaše zariadenie (zariadenia); Žiadosť zlyhala. Možnosť nahrávania a odosielania hlasového vysielania v časovej osi miestnosti. - Zapnúť hlasové vysielanie (v štádiu aktívneho vývoja) + Zapnúť hlasové vysielanie Načítavanie do vyrovnávacej pamäte… Pozastaviť hlasové vysielanie Prehrať alebo pokračovať v nahrávaní hlasového vysielania @@ -2924,4 +2924,35 @@ Úprava Zobraziť posledné konverzácie v systémovej ponuke zdieľania Povoliť priame zdieľanie + Skontrolujte, či je vaše konto bezpečné + Máte neoverené relácie + Táto relácia nepodporuje šifrovanie, takže ju nemožno overiť. +\n +\nPri používaní tejto relácie sa nebudete môcť zúčastňovať konverzácií v miestnostiach, kde je zapnuté šifrovanie. +\n +\nNa dosiahnutie čo najlepšieho zabezpečenia a súkromia sa odporúča používať Matrix klientov, ktoré podporujú šifrovanie. + Odhlásiť zo všetkých ostatných relácií + Táto relácia nepodporuje šifrovanie, a preto ju nemožno overiť. + Získajte najnovšiu zostavu (poznámka: môžete mať problémy s prihlásením) + Nočná zostava + Živé vysielanie + Ukončili ste hlasové vysielanie. + %1$s ukončil/a hlasové vysielanie. + Určite chcete zastaviť vysielanie naživo\? Tým sa vysielanie ukončí a v miestnosti bude k dispozícii celý záznam. + Zastaviť vysielanie naživo\? + Áno, zastaviť + Upraviť odkaz + Vytvoriť odkaz + Odkaz + Text + Nastaviť odkaz + Váš prístupový token poskytuje úplný prístup k vášmu účtu. S nikým ho nezdieľajte. + Prístupový token + Prepnúť na číslovaný zoznam + Prepnúť zoznam s odrážkami + V tejto miestnosti nie sú žiadne predchádzajúce ankety + Predchádzajúce ankety + V tejto miestnosti nie sú žiadne aktívne ankety + Aktívne ankety + História ankety \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-sq/strings.xml b/library/ui-strings/src/main/res/values-sq/strings.xml index 58214e8a22..c3f9d53c99 100644 --- a/library/ui-strings/src/main/res/values-sq/strings.xml +++ b/library/ui-strings/src/main/res/values-sq/strings.xml @@ -2343,7 +2343,7 @@ S’u arrit të ngarkohej hartë Hartë Shënim: aplikacioni do të riniset - Aktivizoni Rrjedha Mesazhesh + Aktivizoni rrjedha mesazhesh Lidhu te shërbyesi Po shihni për të marrë pjesë në një shërbyes ekzistues\? anashkalojeni këtë pyetje @@ -2504,7 +2504,7 @@ %s \nduket paksa si i zbrazët. Jini në gjendje të incizoni dhe dërgoni transmetim zanor në rrjedhën kohore të dhomës. - Aktivizoni transmetim zanor (nën zhvillim aktiv) + Aktivizoni transmetim zanor Aktivizo regjistrim hollësish klienti Shihini më qartë dhe kontrolloni më mirë krejt sesionet tuaj. Aktivizo përgjegjës të ri sesionesh @@ -2573,7 +2573,7 @@ Filtroji Pajisje Sesion - Sesioni i Tanishëm + Sesioni i tanishëm Shihni mundësinë e daljes nga sesione të vjetër (%1$d ditë ose më tepër) të cilët s’i përdorni më. Shihni mundësinë e daljes nga sesione të vjetër (%1$d ditë ose më tepër) të cilët s’i përdorni më. @@ -2659,7 +2659,7 @@ \nKy shërbyes Home mund të mos jetë formësuar të shfaqë harta. Përfundimet do të jenë të dukshme pasi të ketë përfunduar pyetësori Kur bëhet ftesë në një dhomë të fshehtëzuar që ka historik ndarjesh me të tjerët, historiku i fshehtëzuar do të jetë i dukshëm. - + Ndal transmetim zanor Luani ose vazhdoni luajtje transmetimi zanor Ndal incizim transmetimi zanor @@ -2853,4 +2853,33 @@ Aktivizo MD të lënë për më vonë Tkurr pjella të %s Zgjero pjella të %s + Shqyrtojini, për të garantuar se llogaria juaj është e parrezik + Keni sesione të paverifikuar + Ky sesion nuk mbulon fshehtëzim, ndaj s’mund të verifikohet. +\n +\nS’do të jeni në gjendje të merrni pjesë në dhoma ku është i aktivizuar fshehtëzimi, kur përdorni këtë sesion. +\n +\nPër sigurinë dhe privatësinë më të mirë, rekomandohet të përdorni klientë Matrix që mbulojnë fshehtëzimin. + Dilni nga krejt sesionet e tjerë + Ky sesion nuk mbulon fshehtëzim dhe ndaj s’mund të verifikohet. + Transmetim i drejtpërdrejtë + Merrni montimin më të ri (shënim: mund të keni probleme të bëni hyrjen) + Montim i përnatshëm + Përfunduat një transmetim zanor. + %1$s përfundoi një transmetim zanor. + Jeni i sigurt se doni të ndalet transmetimi juaj i drejtpërdrejtë\? Kjo do të përfundojë transmetimin dhe regjistrimi i plotë do të jetë i passhëm te dhoma. + Të ndalet transmetimi i drejtpërdrejtë\? + Po, Ndale + Përpunoni lidhje + Krijoni një lidhje + Lidhje + Tekst + Tokeni juaj i hyrjeve jep hyrje të plotë në llogarinë tuaj. Mos ia jepni kujt. + Token Hyrjesh + S’ka pyetësorë të kaluar në këtë dhomë + Pyetësorë të kaluar + S’ka pyetësorë aktivë në këtë dhomë + Pyetësorë aktivë + mundësia fituese + Historik pyetësorësh \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-sv/strings.xml b/library/ui-strings/src/main/res/values-sv/strings.xml index 45cfe4338b..373165802a 100644 --- a/library/ui-strings/src/main/res/values-sv/strings.xml +++ b/library/ui-strings/src/main/res/values-sv/strings.xml @@ -2728,7 +2728,7 @@ %s \nser lite tom ut. Möjliggör att spela in och skicka röstsändning i rummets tidslinje. - Aktivera röstsändning (under aktiv utveckling) + Aktivera röstsändning Spara klientnamnet, versionen, och URL:en för att enklare känna igen sessioner i sessionehanteraren. Aktivera klientinforapportering Ha bättre insyn i och kontroll över alla dina sessioner. @@ -2804,7 +2804,7 @@ Använd din inloggade enhet för att skanna QR-koden nedan: Logga in med QR-kod Använd den här enhetens kamera för att skanna QR-koden på din andra enhet: - Buffrar + Buffrar… Pausa röstsändning Spela eller återuppta röstsändning Avsluta inspelning av röstsändning @@ -2866,4 +2866,20 @@ Citerar Besvarar %s Redigerar + Granska för att försäkra att ditt konto är säkert + Du har overifierade sessioner + Visa nyliga chattar i systemets delningsmeny + Aktivera direktdelning + Den här sessioner stöder inte kryptering, så den kan inte verifieras. +\n +\nDu kommer inte kunna delta i rum där kryptering är aktiverat när du använder den här sessionen. +\n +\nFör bäst säkerhet och sekretess så rekommenderas det att använda Matrix-klienter som stöder kryptering. + Logga ut alla andra sessioner + Den här sessioner stöder inte kryptering och kan därför inte verifieras. + Hämta det senaste bygget (obs: du kan ha problem med att logga in) + Nightly-bygge + Direktsändning + Du avslutade en röstsändning. + %1$s avslutade en röstsändning. \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml index 6a1c5355ab..2ee9685c76 100644 --- a/library/ui-strings/src/main/res/values-uk/strings.xml +++ b/library/ui-strings/src/main/res/values-uk/strings.xml @@ -2921,7 +2921,7 @@ Під час налаштування захищеного обміну повідомленнями виникла проблема з безпекою. Можливо, порушено одне з таких налаштувань: Ваш домашній сервер; Ваше інтернет-з\'єднання; Ваш пристрій; Запит не виконаний. Можливість записувати та надсилати голосові трансляції до стрічки кімнати. - Увімкнути голосові трансляції (в активній розробці) + Увімкнути голосові трансляції Буферизація… Призупинити голосову трансляцію Відтворити або поновити відтворення голосової трансляції @@ -2980,4 +2980,35 @@ Редагування Показувати останні бесіди в системному меню загального доступу Увімкнути пряме поширення + Перегляньте їх, щоб переконатися, що ваш обліковий запис у безпеці + У вас є незвірені сеанси + Цей сеанс не підтримує шифрування, тому його неможливо звірити. +\n +\nПід час користування цим сеансом ви не зможете брати участь у кімнатах, в яких увімкнено шифрування. +\n +\nДля кращої безпеки й приватності радимо використовувати клієнти Matrix, які підтримують шифрування. + Вийти з усіх інших сеансів + Цей сеанс не підтримує шифрування і тому не може бути звірений. + Отримати найновішу збірку (примітка: у вас можуть виникнути проблеми з входом в систему) + Збірка Nightly + Трансляція наживо + Ви завершили голосову трансляцію. + %1$s завершує голосову трансляцію. + Ви впевнені, що хочете припинити голосову трансляцію\? На цьому трансляція завершиться, і повний запис буде доступний у кімнаті. + Припинити голосову трансляцію\? + Так, припинити + Змінити посилання + Створити посилання + Посилання + Текст + Налаштувати посилання + Ваш токен доступу надає повний доступ до вашого облікового запису. Не передавайте його нікому. + Токен доступу + Перемкнути на маркований список + Перемкнути на нумерований список + У цій кімнаті ще не проводилися опитування + Минулі опитування + У цій кімнаті немає активних опитувань + Активні опитування + Історія опитувань \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml index 0a01610c36..9f975e61e4 100644 --- a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml @@ -94,12 +94,12 @@ 你撤回了对 %1$s 的邀请 你更换了你的头像 你将你的显示名称设置为 %1$s - 您将显示名称从 %1$s 更改为 %2$s - 您移除了您的显示名称(%1$s) - 您将话题更改为:%1$s + 你将显示名称从 %1$s 更改为 %2$s + 你移除了你的显示名称(%1$s) + 你将话题更改为:%1$s %1$s 更改了房间头像 你更改了房间头像 - 您将房间名称更改为:%1$s + 你将房间名称更改为:%1$s 你发起了一次视频通话。 你发起了一次语音通话。 %s 发送了数据以建立通话。 @@ -126,7 +126,7 @@ 默认 自定义(%1$d) 自定义 - 您更改了 %1$s 的权限等级。 + 你更改了 %1$s 的权限等级。 %1$s 更改了 %2$s 的权限等级。 %1$s 从 %2$s 到 %3$s 你的邀请。理由:%1$s @@ -340,7 +340,7 @@ \n \n请在接下来的弹出窗口中授权允许访问,以便进行通话。 移除 - 您将无法撤消此更改,因为您正在将用户提升为与您相同的权限级别。 + 你将无法撤消此更改,因为你正在将用户提升为与你相同的权限级别。 \n你确定吗? 这可能意味着有人正在恶意劫持你的流量,或者你的手机不信任远程服务器提供的数字证书。 如果服务器管理员说这是预期的情况,请确保下面的指纹与管理员提供的指纹相匹配。 @@ -444,7 +444,7 @@ 已加密消息 响铃通知 静默通知 - 数据收集 + 数据分析 响铃 正在加载…… 你确定要发起语音通话吗? @@ -485,8 +485,8 @@ %1$s 条在 %2$s 中 停用账户 停用我的账户 - 发送统计分析数据 - ${app_name} 会收集匿名统计数据来帮助我们改进程序。 + 发送数据分析数据 + ${app_name}收集匿名数据分析以允许我们改进应用。 停用账户 这将使你的账户永远不再可用。你将无法登录,也不能使用相同的用户 ID 重新注册。你的账户将退出所有已加入的房间,你在身份服务器上的账户信息也会被删除。<b>此操作是不可逆的。</b> \n @@ -628,7 +628,7 @@ 使用密钥备份 如果你此时登出账户,你将会失去你的已加密消息 密钥备份进行中。如果你此时登出账户将无法再访问你的已加密消息。 - 安全密钥备份应该在您的所有会话中都处于活动状态,以避免失去对加密消息的访问权限。 + 安全密钥备份应该在你的所有会话中都处于活动状态,以避免失去对加密消息的访问权限。 我不想要我的已加密消息 正在备份密钥…… 确定吗? @@ -786,7 +786,7 @@ 已验证! 了解了 验证请求 - %s 想验证您的会话 + %s 想验证你的会话 未知错误 编辑 回复 @@ -837,7 +837,7 @@ 通知 ${app_name} 呼叫失败 无法建立实时连接。 -\n请让您的主服务器的管理员配置一个 TURN 服务器,以便呼叫能够可靠地工作。 +\n请让你的主服务器的管理员配置一个 TURN 服务器,以便呼叫能够可靠地工作。 选择声音设备 电话 扬声器 @@ -882,7 +882,7 @@ 应用在后台时你不会收到消息通知。 集成 使用集成管理器来管理机器人、桥接、小部件和贴纸包。 -\n集成管理器接收配置数据,并可以代表您修改小部件、发送房间邀请和设置权限等级。 +\n集成管理器接收配置数据,并可以代表你修改小部件、发送房间邀请和设置权限等级。 安全备份 设置安全备份 重置安全备份 @@ -993,10 +993,10 @@ 可发现的电子邮件地址 发现选项将在你添加电子邮件地址后出现。 发现选项将在你添加电话号码后出现。 - 与您的身份服务器断开连接意味着您将不会被其他用户发现,并且您将无法通过电子邮件或电话邀请其他人。 + 与你的身份服务器断开连接意味着你将不会被其他用户发现,并且你将无法通过电子邮件或电话邀请其他人。 可发现电话号码 我们向%s发送了一封电子邮件,请检查你的电子邮件并点击确认链接 - 我们向 %s 发送了一封电子邮件,请先检查您的电子邮件并点击确认链接 + 我们向 %s 发送了一封电子邮件,请先检查你的电子邮件并点击确认链接 输入身份服务器 URL 无法连接到身份服务器 请输入身份服务器 url @@ -1004,10 +1004,10 @@ 你选择的身份服务器无任何服务条款。仅在你信任服务所有者时继续 已向 %s 发送文字消息。请输入它包含的验证码。 验证码不正确。 - 您当前在身份服务器 %1$s 上共享电子邮件地址或电话号码。您需要重新连接到 %2$s 才能停止共享它们。 + 你当前在身份服务器 %1$s 上共享电子邮件地址或电话号码。你需要重新连接到 %2$s 才能停止共享它们。 同意身份服务器 (%s) 服务条款使你可以通过电子邮件地址或电话号码被发现。 启用详细日志。 - 详细日志将通过在您发送愤怒摇动(RageShake)时提供更多日志来帮助开发人员。即使启用,应用程序也不会记录消息内容或任何其他私人数据。 + 详细日志将通过在你发送愤怒摇动(RageShake)时提供更多日志来帮助开发人员。即使启用,应用程序也不会记录消息内容或任何其他私人数据。 接收你的主服务器条款和条件后请重试。 服务器似乎响应时间太长,这可能是由于连接不良或服务器错误引起的。请稍后再试。 发送附件 @@ -1199,7 +1199,7 @@ 除非你登录以恢复加密密钥,否则你将无法访问安全消息。 当前会话用于用户 %1$s 而你提供了用户 %2$s 的凭证。${app_name} 不支持此功能。 \n请先清除数据,然后重新登录另一个账户。 - 您的 matrix.to 链接格式错误 + 你的 matrix.to 链接格式错误 描述太短 初始同步… 高级设置 @@ -1328,7 +1328,7 @@ 在此用户信任此会话之前,发送到该会话和从该会话发送的消息均标有警告。或者,你可以手动进行验证。 初始化交叉签名 重置密钥 - 二维码 + QR码 快要完成了!%s 显示对勾了吗? @@ -1368,7 +1368,7 @@ \n- 设备使用的网络连接 \n \n我们推荐你在设置中立即更换你的密码和恢复密钥。 - 已取消验证。 您可以重新开始验证。 + 已取消验证。 你可以重新开始验证。 验证已取消 恢复口令词组 消息密钥 @@ -1444,7 +1444,7 @@ 启用此设置会将 FLAG_SECURE 添加到所有活动项。重新启动应用程序以使更改生效。 无法保存媒体文件 设置新账户密码…… - 在您的其它设备上使用最新的 ${app_name}、${app_name} Web、${app_name} Desktop、${app_name} iOS、${app_name} for Android 或其他支持交叉签名的 Matrix 客户端 + 在你的其它设备上使用最新的 ${app_name}、${app_name} Web、${app_name} Desktop、${app_name} iOS、${app_name} for Android 或其他支持交叉签名的 Matrix 客户端 ${app_name} Web \n${app_name} Desktop ${app_name} iOS @@ -1531,7 +1531,7 @@ 你无法访问此消息因为发送者有意不发送密钥 正在等待加密历史 Riot 现已成为 Element! - 我们很高兴地宣布我们已经更名了!您的应用程序是最新的,并且您已登录到您的帐户。 + 我们很高兴地宣布我们已经更名了!你的应用程序是最新的,并且你已登录到你的账户。 明白了 了解更多 将恢复密钥保存到 @@ -1588,7 +1588,7 @@ 移除 %s? 请确认你已点击我们向你发送的电子邮件中的链接。 电子邮件和电话号码 - 管理与您的 Matrix 帐户链接的电子邮件地址和电话号码 + 管理与你的 Matrix 账户链接的电子邮件地址和电话号码 代码 请使用国际格式(电话号码必须以“+”开头) 验证此登录来确认你的身份,授权其访问加密消息。 @@ -1634,7 +1634,7 @@ \n \n你的消息受加密保护,并且只有你和消息接收者拥有唯一解密密钥。 此处的消息未经端到端加密。 - 此主服务器正在运行旧版本。 请让您的主服务器管理员升级。 您可以继续,但某些功能可能无法正常工作。 + 此主服务器正在运行旧版本。 请让你的主服务器管理员升级。 你可以继续,但某些功能可能无法正常工作。 你将此房间设为仅邀请。 %1$s 仅发出此邀请。 在加密房间显示完整历史 @@ -1656,7 +1656,7 @@ 你没有权限发起会议通话 重置 允许访问你的联系人。 - 如需扫描二维码,你须允许相机访问权限。 + 如需扫描QR码,你须允许相机访问权限。 没有更多结果 开始聊天 删除地址 \"%1$s\"? @@ -1713,8 +1713,8 @@ 发送电子邮件和电话号码 建议 已知用户 - 二维码 - 通过二维码添加 + QR码 + 通过QR码添加 房间设置 话题 房间话题(可选) @@ -1738,7 +1738,7 @@ 使用 /confetti 命令或发送包含 ❄️ 或 🎉 的消息 显示聊天效果 更改话题 - 更新房间 + 升级房间 发送 m.room.server_acl 事件 更改权限 更改房间名称 @@ -1758,7 +1758,7 @@ 你没有权限更新更改房间多个部分所需角色 选择更改房间各个部分所需的角色 权限 - 需要查看和更新角色以更改房间多个部分。 + 查看和更新更改房间各个部分所需的角色。 房间权限 此房间不公开。你没有邀请将无法重新加入。 你保持通话 @@ -1804,7 +1804,7 @@ %d 个条目 - 这不是有效的 Matrix 二维码 + 这不是有效的 Matrix QR码 扫描二维码 添加人员 邀请朋友 @@ -1904,7 +1904,7 @@ 正在创建空间…… 随机 一般性 - 让我们为他们每个人创建一个房间。 您也可以稍后添加更多内容,包括已经存在的内容。 + 让我们为他们每个人创建一个房间。 你也可以稍后添加更多内容,包括已经存在的内容。 你在做些什么? 我们将会为此创建房间。你也可以在稍后增加更多。 你希望在 %s 中进行哪些讨论? @@ -1935,7 +1935,7 @@ 未检查 打开小部件 屏幕截图 - ${app_name} 需要您输入凭据才能执行此操作。 + ${app_name} 需要你输入凭据才能执行此操作。 呼叫转移时发生错误 先询问 查找电话号码时发生了错误 @@ -1943,8 +1943,8 @@ %1$s 拒绝了此通话 有未保存的更改。要放弃更改吗? 房间尚未创建。取消创建房间? - 未扫描二维码! - 无效的二维码(无效的标识)! + 未扫描QR码! + 无效的QR码(无效的标识)! 无法向你自己发送私聊消息! 通过文字共享 更改你当前的 PIN @@ -2031,7 +2031,7 @@ 空间地址 升级到推荐的房间版本 这个房间运行房间版本 %s,此主服务器已将其标记为不稳定。 - 您需要权限才能升级房间 + 你需要权限才能升级房间 自动更新空间父级 自动邀请用户 你将把此房间从 %1$s 升级到 %2$s。 @@ -2061,13 +2061,13 @@ %d 个未接音频电话 请注意,升级将使房间焕然一新。 所有当前消息都将保留在此存档的房间中。 - 主空间中的任何人都可以找到并加入此房间 - 无需手动邀请所有人。 您可以随时在房间设置中更改此设置。 - %s 中的任何人将可以查找并加入此房间 - 无需手动邀请所有人。 您可以随时在房间设置中更改此设置。 + 主空间中的任何人都可以找到并加入此房间 - 无需手动邀请所有人。 你可以随时在房间设置中更改此设置。 + %s 中的任何人将可以查找并加入此房间 - 无需手动邀请所有人。 你可以随时在房间设置中更改此设置。 语音消息 (%1$s) 语音消息处于活动状态时无法回复或编辑 无法录制语音消息 无法播放此语音消息 - 点按您的录音以停止或收听 + 点按你的录音以停止或收听 剩余 %1$d秒 按住录音,松开发送 删除录音 @@ -2078,7 +2078,7 @@ 录制语音消息 需要升级 语音 - 您可能不知道的其它空间或房间 + 你可能不知道的其它空间或房间 你知道的包含这个房间的空间 决定谁能找到并加入这个房间。 点按即可编辑空间 @@ -2105,9 +2105,9 @@ 可用视频通话 可用语音通话 在 ${app_name} 中直接接收邀请的设置 %s。 - 将此电子邮件地址与您的帐户链接 - 此空间的邀请已发送至与您的帐户无关的 %s - 此房间的邀请已发送至与您的帐户无关的 %s + 将此电子邮件地址与你的账户链接 + 此空间的邀请已发送至与你的账户无关的 %s + 此房间的邀请已发送至与你的账户无关的 %s 你所在的全部房间将显示在主页上。 在主页上显示所有房间 滑动结束通话 @@ -2147,7 +2147,7 @@ 和 %s 视频通话 来电响铃中… 空间 - 将一个空间添加到您管理的任何空间。 + 将一个空间添加到你管理的任何空间。 添加现有空间 添加现有房间 你确定要离开 %s 吗? @@ -2171,14 +2171,14 @@ 空间访问 谁可以访问? 为 %s 启用电子邮件通知 - 要接收带有通知的电子邮件,请将电子邮件地址链接到您的 Matrix 帐户 + 要接收带有通知的电子邮件,请将电子邮件地址链接到你的 Matrix 账户 电子邮件通知 升级空间 更改空间名称 启用空间加密 更改空间主地址 更改空间头像 - 您没有权限更新更改该空间的各个部分所需的角色 + 你没有权限更新更改该空间的各个部分所需的角色 选择更改该空间的各个部分所需的角色 查看和更新更改空间的各个部分所需的角色。 空间权限 @@ -2193,9 +2193,9 @@ 隐藏身份服务器政策 显示身份服务器策略 显示用户信息 - 仅更改您在当前房间的头像 + 仅更改你在当前房间的头像 更改当前房间的头像 - 仅在当前房间更改您的显示昵称 + 仅在当前房间更改你的显示昵称 设置房间名称 停止忽略用户,继续显示他们的消息 忽略用户,隐藏他们的消息 @@ -2203,7 +2203,7 @@ 离线 在线 选择主服务器 - 无法访问 URL %s 上的主服务器。请检查您的链接或手动选择一个主服务器。 + 无法访问 URL %s 上的主服务器。请检查你的链接或手动选择一个主服务器。 侦听通知 需要至少 %1$s 个选项 @@ -2218,13 +2218,13 @@ 创建投票 投票 向%s发送电子邮件地址和电话号码 - 您的联系人是私密的。 要从您的联系人中发现用户,我们需要您的许可才能将联系信息发送到您的身份服务器。 + 你的联系人是私密的。 要从你的联系人中发现用户,我们需要你的许可才能将联系信息发送到你的身份服务器。 已登出此会话! 已离开此房间! 你同意发送此信息吗? - 要发现现有的联系人,你需要将联系人信息(电子邮件地址和电话号码)发送到你的身份服务器。出乎隐私考量,我们会在发送前对您的数据进行散列处理。 + 要发现现有的联系人,你需要将联系人信息(电子邮件地址和电话号码)发送到你的身份服务器。出乎隐私考量,我们会在发送前对你的数据进行散列处理。 不是现在 - 您确定要删除此投票吗?一旦移除,就无法恢复。 + 你确定要删除此投票吗?一旦移除,就无法恢复。 删除投票 投票已结束 投票 @@ -2260,7 +2260,7 @@ 你可以随时在设置中关闭它 我们与第三方共享信息 此处 - 通过共享匿名使用数据,帮助我们识别问题并改进 ${app_name}。为了理解人们如何使用多台设备,我们将生成一个随机标识符,由您的设备共享。 + 通过共享匿名使用数据,帮助我们识别问题并改进 ${app_name}。为了理解人们如何使用多台设备,我们将生成一个随机标识符,由你的设备共享。 \n \n你可以阅读我们所有的条款 %s。 帮助改进 ${app_name} @@ -2286,11 +2286,11 @@ %1$d 更多 - 请注意:这是使用临时实现的实验室功能。这意味着您将无法删除您的位置历史记录,即使您停止与此房间共享您的实时位置,高级用户也将能够看到您的位置历史记录。 + 请注意:这是使用临时实现的实验室功能。这意味着你将无法删除你的位置历史记录,即使你停止与此房间共享你的实时位置,高级用户也将能够看到你的位置历史记录。 当前网关:%s 网关 提供反馈 - 为您的团队发送消息。 + 为你的团队发送消息。 消息列 beta 消息列 为所有消息显示最新资料信息(头像和显示名称)。 @@ -2299,7 +2299,7 @@ 覆盖显示名称颜色 检查你的电子邮件。 电子邮件 - 一切由您掌控。 + 一切由你掌控。 BETA 共享你的实时位置 缩放到当前位置 @@ -2334,7 +2334,7 @@ 共享位置 显示消息气泡 共享位置 - 您需要拥有正确的权限才能在此房间中共享实时位置。 + 你需要拥有正确的权限才能在此房间中共享实时位置。 你没有权限共享实时位置 %1$s 前已更新 临时执行:地点在房间历史中持续存在 @@ -2424,7 +2424,7 @@ 安全传送消息。 向主服务器注册端点token失败: \n%1$s - 消息列有助于使您的对话保持话题并易于跟踪。%s 创建消息列将刷新应用程序。对于某些帐户,这可能需要更长的时间。 + 消息列有助于使你的对话保持话题并易于跟踪。%s 创建消息列将刷新应用程序。对于某些账户,这可能需要更长的时间。 重启应用以使更改生效。 启用 LaTeX 数学 (%1$s) @@ -2446,7 +2446,7 @@ 此空间里的东西 无法启用生物特征识别。 - 生物特征识别被禁用,因为最近添加了新的生物特征识别方法。 您可以在“设置”中再次启用它。 + 生物特征识别被禁用,因为最近添加了新的生物特征识别方法。 你可以在“设置”中再次启用它。 主服务器不接收仅有数字的用户名。 发送你的第一条消息邀请%s聊天 加密配置错误 @@ -2509,7 +2509,7 @@ 自动播放动画图片 端点成功注册到主服务器。 端点注册 - 您的主服务器当前不支持消息列,因此此功能可能不可靠。某些消息列的消息可能无法可靠地使用。 %s 您仍然要启用消息列吗? + 你的主服务器当前不支持消息列,因此此功能可能不可靠。某些消息列的消息可能无法可靠地使用。 %s 你仍然要启用消息列吗? Threads接近Beta了 🎉 来自消息列 实用提示:长按消息并使用“%s”。 @@ -2539,13 +2539,13 @@ 自动允许 Element 通话小部件并授予相机/麦克风访问权限 启用 Element 通话权限快捷方式 实时位置 - 此二维码看起来格式不正确。请尝试使用其它方法进行验证。 + 此QR码看起来格式不正确。请尝试使用其它方法进行验证。 你无法访问加密消息历史。重置你的安全消息备份和验证密钥以重新开始。 无法验证此设备 你的服务器地址是什么? 你的对话发生的地方 %1$s 和 %2$s - 电子邮件未验证,请检查您的收件箱 + 电子邮件未验证,请检查你的收件箱 无法加载地图 \n此主服务器可能没有设置好显示地图。 打开设置 @@ -2599,14 +2599,14 @@ 欢迎来到 ${app_name}, \n%s。 - 当您有一些未读消息时,这里会显示您的未读消息。 + 当你有一些未读消息时,这里会显示你的未读消息。 提供反馈 点击右上角查看反馈选项。 试用 空间是对房间和人进行分组的新方式。创建一个空间来开始吧。 启用新布局 IP地址 - 验证您的会话以增强安全消息传递或从您不再识别或不再使用的会话中登出。 + 验证你的会话以增强安全消息传递或从你不再识别或不再使用的会话中登出。 尚未准备好安全收发消息 准备好安全收发消息 已验证 @@ -2624,7 +2624,7 @@ 启用延迟的私聊消息 简化的 Element,带有可选的标签 无痕键盘 - 请求键盘不要根据您在对话中输入的内容更新任何个性化数据,例如输入历史记录和字典。 请注意,某些键盘可能不遵守此设置。 + 请求键盘不要根据你在对话中输入的内容更新任何个性化数据,例如输入历史记录和字典。 请注意,某些键盘可能不遵守此设置。 ${app_name}需要权限来显示通知。通知可以显示消息、邀请等。 \n \n请在下个弹窗允许访问以便查看通知。 @@ -2640,25 +2640,25 @@ %s \n看起来有点空荡荡的。 能够在房间时间线中录制和发送语音广播。 - 启用语音广播(正在积极开发中) + 启用语音广播 记录客户端名称、版本和网址,以便在会话管理器中更轻松地识别会话。 启用客户端信息记录 对所有会话有更好的可见性和控制。 - 您加入的直接消息和聊天室中的其他用户可以查看您的会话的完整列表。 + 你加入的私聊消息和房间中的其他用户可以查看你的会话的完整列表。 \n -\n这让他们确信他们真的在与您交谈,但这也意味着他们可以看到您在此处输入的会话名称。 +\n这让他们确信他们真的在与你交谈,但这也意味着他们可以看到你在此处输入的会话名称。 重命名会话 - 已验证会话已使用您的凭据登录,然后使用您的安全密码或通过交叉验证进行验证。 + 已验证会话已使用你的凭据登录,然后使用你的安全密码或通过交叉验证进行验证。 \n -\n这意味着他们持有您之前消息的加密密钥,并向您正在与之通信的其他用户确认这些会话确实是您。 - 闲置会话是您一段时间未使用的会话,但它们会继续接收加密密钥。 +\n这意味着他们持有你之前消息的加密密钥,并向你正在与之通信的其他用户确认这些会话确实是你。 + 闲置会话是你一段时间未使用的会话,但它们会继续接收加密密钥。 \n -\n删除闲置会话可以提高安全性和性能,并使您更容易识别新会话是否可疑。 +\n删除闲置会话可以提高安全性和性能,并使你更容易识别新会话是否可疑。 闲置会话 - 您可以使用此设备通过二维码登录移动设备或网络设备。 有两种方法可以做到这一点: - 使用二维码登录 - 请注意,与您交流的人也可以看到会话名称。 - 自定义会话名称可以帮助您更轻松地识别您的设备。 + 你可以使用此设备通过QR码登录移动设备或网络设备。 有两种方法可以做到这一点: + 使用QR码登录 + 请注意,与你交流的人也可以看到会话名称。 + 自定义会话名称可以帮助你更轻松地识别你的设备。 重命名会话 操作系统 型号 @@ -2679,19 +2679,19 @@ 未找到未验证的会话。 未找到已验证的会话。 - 考虑登出您不再使用的旧会话(%1$d 天或更长时间)。 + 考虑登出你不再使用的旧会话(%1$d 天或更长时间)。 闲置 未验证 - 为获得最佳安全性,请从您不认识或不再使用的任何会话中登出。 + 为获得最佳安全性,请从你不认识或不再使用的任何会话中登出。 已验证 过滤器 闲置 %1$d 天或更长时间 未验证 - 未验证 · 您当前的会话 - 验证您当前的会话以显示此会话的验证状态。 + 未验证 · 你当前的会话 + 验证你当前的会话以显示此会话的验证状态。 未知的验证状态 开始语音广播 正在缓冲…… @@ -2702,51 +2702,51 @@ 应用删除线格式 应用斜体格式 应用粗体格式 - 请确保您知道此代码的来源。 通过链接设备,您将为某人提供对您帐户的完全访问权限。 + 请确保你知道此代码的来源。 通过链接设备,你将为某人提供对你账户的完全访问权限。 确认 再试一次 不匹配? 登录 连接到设备 - 扫描二维码 + 扫描QR码 登录移动设备? - 在此设备中显示二维码 - 选择“扫描二维码” + 在此设备中显示QR码 + 选择“扫描QR码” 从登录屏幕开始 - 选择“使用二维码登录” + 选择“使用QR码登录” 从登录屏幕开始 - 选择“显示二维码” + 选择“显示QR码” 转到设置 -> 安全和隐私 - 在您的其它设备上打开应用程序 - 主服务器不支持二维码登录。 + 在你的其它设备上打开应用程序 + 主服务器不支持QR码登录。 登录已在另一台设备上取消。 - 该二维码无效。 + 该QR码无效。 另一台设备必须登录。 另一台设备已登录。 链接未在规定时间内完成。 - 设置安全消息传递时遇到安全问题。 以下其中一项可能会受到损害:您的家庭服务器; 您的互联网连接; 您的设备; + 设置安全消息传递时遇到安全问题。 以下其中一项可能会受到损害:你的家庭服务器; 你的互联网连接; 你的设备; 请求失败。 该请求在另一台设备上被拒绝。 不支持与此设备链接。 连接不成功 - 检查您已登录的设备,应显示以下代码。 确认以下代码与该设备匹配: + 检查你已登录的设备,应显示以下代码。 确认以下代码与该设备匹配: 已建立安全连接 - 使用已退出登录的设备扫描下方二维码。 - 使用您已登录的设备扫描下方二维码: - 使用二维码登录 - 使用此设备上的相机扫描其它设备上显示的二维码: - 扫描二维码 + 使用已退出登录的设备扫描下方QR码。 + 使用你已登录的设备扫描下方QR码: + 使用QR码登录 + 使用此设备上的相机扫描其它设备上显示的QR码: + 扫描QR码 3 2 1 - 为了简化您的 ${app_name},选项卡现在是可选的。 使用右上角的菜单管理它们。 + 为了简化你的 ${app_name},选项卡现在是可选的。 使用右上角的菜单管理它们。 无需报告。 - 适用于团队、朋友和组织的一体化安全聊天应用程序。 创建一个聊天室,或加入一个现有的房间,开始。 + 适用于团队、朋友和组织的一体化安全聊天应用程序。 创建一个聊天或加入一个现有的房间来开始。 空间是一种对房间和人员进行分组的新方式。 使用右下角的按钮添加现有房间或创建新房间。 已验证会话 - 未验证会话是使用您的凭据登录但未经交叉验证的会话。 + 未验证会话是使用你的凭据登录但未经交叉验证的会话。 \n -\n您应特别确定您识别这些会话,因为它们可能代表未经授权使用您的帐户。 +\n你应特别确定你识别这些会话,因为它们可能代表未经授权使用你的账户。 未验证会话 闲置 会话名称 @@ -2762,11 +2762,11 @@ 停止语音广播录制 暂停语音广播录制 继续语音广播录制 - 扫描二维码 + 扫描QR码 语音广播 已启用: 会话ID: - 出了点差错。请检查您的网络连接并重试。 + 出了点差错。请检查你的网络连接并重试。 联系人 切换全屏模式 选择会话 @@ -2778,9 +2778,9 @@ 附件 贴纸 照片库 - 您没有在此房间内开始语音广播所需的权限。联系房间管理员升级您的权限。 + 你没有在此房间内开始语音广播所需的权限。联系房间管理员升级你的权限。 其他人已经在录制语音广播。等待他们的语音广播结束以开始新的广播。 - 您已经在录制语音广播。请结束您当前的语音广播以开始新的语音广播。 + 你已经在录制语音广播。请结束你当前的语音广播以开始新的语音广播。 无法开始新的语音广播 快进 30 秒 快退 30 秒 @@ -2804,4 +2804,21 @@ 登出 剩余%1$s + 正在编辑 + 回复给%s + 引用 + 显示IP地址 + 隐藏IP地址 + 回复给 + 启用直接分享 + 在系统分享菜单中显示最近聊天 + 复查以确保你的账户是安全的 + 你有未验证的会话 + 登出全部其他会话 + 这个会话不支持加密,因此不能被验证。 + 获取最新构建(注意:你可能在登录时遇到麻烦) + 实时广播 + Nightly构建 + 你结束了一个语音广播。 + %1$s结束了一个语音广播。 \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml index 9a5439b2ae..14729c5b44 100644 --- a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml @@ -2759,7 +2759,7 @@ 設定安全訊息傳遞時遇到安全問題。以下其中一項可能已被駭入:您的家伺服器、您的網際網路連線、您的裝置; 請求失敗。 可以在聊天室時間軸中錄製並傳送語音廣播。 - 啟用語音廣播(正在積極開發中) + 啟用語音廣播 正在緩衝…… 暫停語音廣播 播放或繼續語音廣播 @@ -2812,4 +2812,35 @@ 正在編輯 在系統分享選單中顯示最近聊天 啟用直接分享 + 檢查以確保您的帳號安全 + 您有未驗證的工作階段 + 此工作階段不支援加密,因此無法驗證。 +\n +\n使用此工作階段時,您將無法參與啟用了加密的聊天室。 +\n +\n為了取得最佳的安全性與隱私,建議使用支援加密的 Matrix 客戶端。 + 登出其他所有工作階段 + 此工作階段不支援加密,因此無法驗證。 + 取得最新版本(注意:您可能會無法登入) + Nightly 版本 + 即時廣播 + 您結束了語音廣播。 + %1$s 結束了語音廣播。 + 您真的想要停止您的即時廣播嗎?這將會結束廣播,完整的錄音會在聊天室中提供。 + 停止即時廣播? + 是的,停止 + 編輯連結 + 建立連結 + 連結 + 文字 + 設定連結 + 您的存取權杖可以完整存取您的帳號。不要將其與其他人分享。 + 存取權杖 + 切換項目符號清單 + 切換編號清單 + 此聊天室沒有過去的投票 + 過去的投票 + 此聊天室沒有正在進行的投票 + 進行中的投票 + 投票歷史紀錄 \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 73cb60bb68..d9f94ba27b 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -794,6 +794,7 @@ Shows all threads you’ve participated in Keep discussions organized with threads Threads help keep your conversations on-topic and easy to track. + You\'re homeserver does not support listing threads yet. Tip: Long tap a message and use “%s”. From a Thread @@ -2335,6 +2336,7 @@ "One person" "%1$d people" + Poll history Uploads Leave Room Leave @@ -3190,6 +3192,10 @@ Voters see results as soon as they have voted Closed poll Results are only revealed when you end the poll + Active polls + There are no active polls in this room + Past polls + There are no past polls in this room Share location @@ -3415,7 +3421,7 @@ Have greater visibility and control over all your sessions. Enable client info recording Record the client name, version, and url to recognise sessions more easily in session manager. - Enable voice broadcast (under active development) + Enable voice broadcast Be able to record and send voice broadcast in room timeline. @@ -3485,6 +3491,8 @@ Apply strikethrough format Apply underline format Set link + Toggle numbered list + Toggle bullet list Toggle full screen mode Text @@ -3501,4 +3509,7 @@ sent a video. sent a sticker. created a poll. + + Access Token + Your access token gives full access to your account. Do not share it with anyone. diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index adeda73918..d132158615 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -62,7 +62,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.5.18\"" + buildConfigField "String", "SDK_VERSION", "\"1.5.20\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt index 5b41ddaaec..165dcf079e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt @@ -25,6 +25,9 @@ import java.io.IOException import java.net.UnknownHostException import javax.net.ssl.HttpsURLConnection +fun Throwable.is400() = this is Failure.ServerError && + httpCode == HttpsURLConnection.HTTP_BAD_REQUEST + fun Throwable.is401() = this is Failure.ServerError && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED && /* 401 */ error.code == MatrixError.M_UNAUTHORIZED diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserIdentity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserIdentity.kt new file mode 100644 index 0000000000..071db7f902 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserIdentity.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.crosssigning + +/** + * Container for the three cross signing keys: master, self signing and user signing. + */ +data class UserIdentity( + val masterKey: CryptoCrossSigningKey?, + val selfSigningKey: CryptoCrossSigningKey?, + val userSigningKey: CryptoCrossSigningKey?, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 9b5f4ac19f..9a928c61fb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -126,8 +126,37 @@ data class Event( /** * Copy all fields, including transient fields. */ - fun copyAll(): Event { - return copy().also { + + fun copyAll( + type: String? = this.type, + eventId: String? = this.eventId, + content: Content? = this.content, + prevContent: Content? = this.prevContent, + originServerTs: Long? = this.originServerTs, + senderId: String? = this.senderId, + stateKey: String? = this.stateKey, + roomId: String? = this.roomId, + unsignedData: UnsignedData? = this.unsignedData, + redacts: String? = this.redacts, + mxDecryptionResult: OlmDecryptionResult? = this.mxDecryptionResult, + mCryptoError: MXCryptoError.ErrorType? = this.mCryptoError, + mCryptoErrorReason: String? = this.mCryptoErrorReason, + sendState: SendState = this.sendState, + ageLocalTs: Long? = this.ageLocalTs, + threadDetails: ThreadDetails? = this.threadDetails, + ): Event { + return copy( + type = type, + eventId = eventId, + content = content, + prevContent = prevContent, + originServerTs = originServerTs, + senderId = senderId, + stateKey = stateKey, + roomId = roomId, + unsignedData = unsignedData, + redacts = redacts + ).also { it.mxDecryptionResult = mxDecryptionResult it.mCryptoError = mCryptoError it.mCryptoErrorReason = mCryptoErrorReason @@ -429,7 +458,7 @@ fun Event.isReplyRenderedInThread(): Boolean { return isReply() && getRelationContent()?.shouldRenderInThread() == true } -fun Event.isThread(): Boolean = getRelationContentForType(RelationType.THREAD)?.eventId != null +fun Event.isThread(): Boolean = getRootThreadEventId() != null fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.THREAD)?.eventId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt index 5d4d67a65e..e3c5deeee7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt @@ -19,5 +19,4 @@ package org.matrix.android.sdk.api.session.room.threads sealed class FetchThreadsResult { data class ShouldFetchMore(val nextBatch: String) : FetchThreadsResult() object ReachedEnd : FetchThreadsResult() - object Failed : FetchThreadsResult() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt index bb6f6b51d3..dfa6cdeec1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt @@ -23,7 +23,7 @@ import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary * This interface defines methods to interact with thread related features. * It's the dynamic threads implementation and the homeserver must return * a capability entry for threads. If the server do not support m.thread - * then [ThreadsLocalService] should be used instead + * then [org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService] should be used instead */ interface ThreadsService { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt index 468e998407..0a8c58de16 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt @@ -69,7 +69,7 @@ internal class DefaultLoginWizard( ) } else { PasswordLoginParams.userIdentifier( - user = login, + user = login.trim(), password = password, deviceDisplayName = initialDeviceName, deviceId = deviceId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 7862da1c17..50497e3a27 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -89,6 +89,7 @@ import org.matrix.android.sdk.internal.crypto.model.SessionInfo import org.matrix.android.sdk.internal.crypto.model.toRest import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask @@ -192,21 +193,21 @@ internal class DefaultCryptoService @Inject constructor( private val isStarting = AtomicBoolean(false) private val isStarted = AtomicBoolean(false) - fun onStateEvent(roomId: String, event: Event) { + fun onStateEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) { when (event.type) { EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) - EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event, cryptoStoreAggregator) } } - fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean) { + fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean, cryptoStoreAggregator: CryptoStoreAggregator?) { // handle state events if (event.isStateEvent()) { when (event.type) { EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) - EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event, cryptoStoreAggregator) } } @@ -430,8 +431,10 @@ internal class DefaultCryptoService @Inject constructor( * A sync response has been received. * * @param syncResponse the syncResponse + * @param cryptoStoreAggregator data aggregated during the sync response treatment to store */ - fun onSyncCompleted(syncResponse: SyncResponse) { + fun onSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) { + cryptoStore.storeData(cryptoStoreAggregator) cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { runCatching { if (syncResponse.deviceLists != null) { @@ -998,15 +1001,26 @@ internal class DefaultCryptoService @Inject constructor( } } - private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) { + private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) { if (!event.isStateEvent()) return val eventContent = event.content.toModel() val historyVisibility = eventContent?.historyVisibility if (historyVisibility == null) { - cryptoStore.setShouldShareHistory(roomId, false) + if (cryptoStoreAggregator != null) { + cryptoStoreAggregator.setShouldShareHistoryData[roomId] = false + } else { + // Store immediately + cryptoStore.setShouldShareHistory(roomId, false) + } } else { - cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED) - cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory()) + if (cryptoStoreAggregator != null) { + cryptoStoreAggregator.setShouldEncryptForInvitedMembersData[roomId] = historyVisibility != RoomHistoryVisibility.JOINED + cryptoStoreAggregator.setShouldShareHistoryData[roomId] = historyVisibility.shouldShareHistory() + } else { + // Store immediately + cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED) + cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory()) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt index 7e9e156003..364d77f7ac 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt @@ -25,11 +25,13 @@ import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.extensions.measureMetric import org.matrix.android.sdk.api.metrics.DownloadDeviceKeysMetricsPlugin import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.UserDataToStore import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.sync.SyncTokenStore @@ -371,6 +373,8 @@ internal class DeviceListManager @Inject constructor( Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") } + val userDataToStore = UserDataToStore() + for (userId in filteredUsers) { // al devices = val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) } @@ -404,7 +408,7 @@ internal class DeviceListManager @Inject constructor( } // Update the store // Note that devices which aren't in the response will be removed from the stores - cryptoStore.storeUserDevices(userId, workingCopy) + userDataToStore.userDevices[userId] = workingCopy } val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also { @@ -416,14 +420,15 @@ internal class DeviceListManager @Inject constructor( val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also { Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") } - cryptoStore.storeUserCrossSigningKeys( - userId, - masterKey, - selfSigningKey, - userSigningKey + userDataToStore.userIdentities[userId] = UserIdentity( + masterKey = masterKey, + selfSigningKey = selfSigningKey, + userSigningKey = userSigningKey ) } + cryptoStore.storeData(userDataToStore) + // Update devices trust for these users // dispatchDeviceChange(downloadUsers) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index 21e3342365..0305f73a7b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -22,9 +22,9 @@ import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig import org.matrix.android.sdk.api.session.crypto.NewSessionListener import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState -import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo +import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo @@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper +import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity import org.matrix.olm.OlmAccount import org.matrix.olm.OlmOutboundGroupSession @@ -230,11 +231,12 @@ internal interface IMXCryptoStore { */ fun storeUserDevices(userId: String, devices: Map?) - fun storeUserCrossSigningKeys( + /** + * Store the cross signing keys for the user userId. + */ + fun storeUserIdentity( userId: String, - masterKey: CryptoCrossSigningKey?, - selfSigningKey: CryptoCrossSigningKey?, - userSigningKey: CryptoCrossSigningKey? + userIdentity: UserIdentity ) /** @@ -290,6 +292,13 @@ internal interface IMXCryptoStore { fun shouldEncryptForInvitedMembers(roomId: String): Boolean + /** + * Sets a boolean flag that will determine whether or not this device should encrypt Events for + * invited members. + * + * @param roomId the room id + * @param shouldEncryptForInvitedMembers The boolean flag + */ fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) fun shouldShareHistory(roomId: String): Boolean @@ -580,4 +589,14 @@ internal interface IMXCryptoStore { fun areDeviceKeysUploaded(): Boolean fun tidyUpDataBase() fun getOutgoingRoomKeyRequests(inStates: Set): List + + /** + * Store a bunch of data collected during a sync response treatment. @See [CryptoStoreAggregator]. + */ + fun storeData(cryptoStoreAggregator: CryptoStoreAggregator) + + /** + * Store a bunch of data related to the users. @See [UserDataToStore]. + */ + fun storeData(userDataToStore: UserDataToStore) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/UserDataToStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/UserDataToStore.kt new file mode 100644 index 0000000000..914ce4704e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/UserDataToStore.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store + +import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo + +internal data class UserDataToStore( + /** + * Map of userId -> (Map of deviceId -> [CryptoDeviceInfo]). + */ + val userDevices: MutableMap> = mutableMapOf(), + /** + * Map of userId -> [UserIdentity]. + */ + val userIdentities: MutableMap = mutableMapOf(), +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/CryptoStoreAggregator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/CryptoStoreAggregator.kt new file mode 100644 index 0000000000..687ec95ec3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/CryptoStoreAggregator.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db + +data class CryptoStoreAggregator( + val setShouldShareHistoryData: MutableMap = mutableMapOf(), + val setShouldEncryptForInvitedMembersData: MutableMap = mutableMapOf(), +) { + fun isEmpty(): Boolean { + return setShouldShareHistoryData.isEmpty() && + setShouldEncryptForInvitedMembersData.isEmpty() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt index 2d66ce1488..6412df205f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt @@ -20,10 +20,12 @@ import android.util.Base64 import io.realm.Realm import io.realm.RealmConfiguration import io.realm.RealmObject +import timber.log.Timber import java.io.ByteArrayOutputStream import java.io.ObjectOutputStream import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream +import kotlin.system.measureTimeMillis /** * Get realm, invoke the action, close realm, and return the result of the action. @@ -55,10 +57,12 @@ internal fun doRealmQueryAndCopyList(realmConfiguration: Realm /** * Get realm instance, invoke the action in a transaction and close realm. */ -internal fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { - Realm.getInstance(realmConfiguration).use { realm -> - realm.executeTransaction { action.invoke(it) } - } +internal fun doRealmTransaction(tag: String, realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { + measureTimeMillis { + Realm.getInstance(realmConfiguration).use { realm -> + realm.executeTransaction { action.invoke(it) } + } + }.also { Timber.w("doRealmTransaction for $tag took $it millis") } } internal fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index 442dc0125f..b4368467a2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -33,9 +33,9 @@ import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig import org.matrix.android.sdk.api.session.crypto.NewSessionListener import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState -import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo +import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo @@ -55,6 +55,7 @@ import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrappe import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.UserDataToStore import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity @@ -147,7 +148,7 @@ internal class RealmCryptoStore @Inject constructor( init { // Ensure CryptoMetadataEntity is inserted in DB - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("init", realmConfiguration) { realm -> var currentMetadata = realm.where().findFirst() var deleteAll = false @@ -189,7 +190,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun deleteStore() { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("deleteStore", realmConfiguration) { it.deleteAll() } } @@ -218,7 +219,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun storeDeviceId(deviceId: String) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("storeDeviceId", realmConfiguration) { it.where().findFirst()?.deviceId = deviceId } } @@ -230,7 +231,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun saveOlmAccount() { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("saveOlmAccount", realmConfiguration) { it.where().findFirst()?.putOlmAccount(olmAccount) } } @@ -248,7 +249,7 @@ internal class RealmCryptoStore @Inject constructor( @Synchronized override fun getOrCreateOlmAccount(): OlmAccount { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("getOrCreateOlmAccount", realmConfiguration) { val metaData = it.where().findFirst() val existing = metaData!!.getOlmAccount() if (existing == null) { @@ -288,131 +289,139 @@ internal class RealmCryptoStore @Inject constructor( } override fun storeUserDevices(userId: String, devices: Map?) { - doRealmTransaction(realmConfiguration) { realm -> - if (devices == null) { - Timber.d("Remove user $userId") - // Remove the user - UserEntity.delete(realm, userId) - } else { - val userEntity = UserEntity.getOrCreate(realm, userId) - // First delete the removed devices - val deviceIds = devices.keys - val devicesToDelete = ArrayList() - userEntity.devices.iterator().forEach { deviceInfoEntity -> + doRealmTransaction("storeUserDevices", realmConfiguration) { realm -> + storeUserDevices(realm, userId, devices) + } + } + + private fun storeUserDevices(realm: Realm, userId: String, devices: Map?) { + if (devices == null) { + Timber.d("Remove user $userId") + // Remove the user + UserEntity.delete(realm, userId) + } else { + val userEntity = UserEntity.getOrCreate(realm, userId) + // First delete the removed devices + val deviceIds = devices.keys + userEntity.devices.toTypedArray().iterator().let { + while (it.hasNext()) { + val deviceInfoEntity = it.next() if (deviceInfoEntity.deviceId !in deviceIds) { - devicesToDelete.add(deviceInfoEntity) + Timber.d("Remove device ${deviceInfoEntity.deviceId} of user $userId") + deviceInfoEntity.deleteOnCascade() } } - while (devicesToDelete.isNotEmpty()) { - val device = devicesToDelete.removeAt(0) - Timber.d("Remove device ${device.deviceId} of user $userId") - device.deleteOnCascade() - } - // Then update existing devices or add new one - devices.values.forEach { cryptoDeviceInfo -> - val existingDeviceInfoEntity = userEntity.devices.firstOrNull { it.deviceId == cryptoDeviceInfo.deviceId } - if (existingDeviceInfoEntity == null) { - // Add the device - Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId") - val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo) - newEntity.firstTimeSeenLocalTs = clock.epochMillis() - userEntity.devices.add(newEntity) - } else { - // Update the device - Timber.d("Update device ${cryptoDeviceInfo.deviceId} of user $userId") - CryptoMapper.updateDeviceInfoEntity(existingDeviceInfoEntity, cryptoDeviceInfo) - } + } + // Then update existing devices or add new one + devices.values.forEach { cryptoDeviceInfo -> + val existingDeviceInfoEntity = userEntity.devices.firstOrNull { it.deviceId == cryptoDeviceInfo.deviceId } + if (existingDeviceInfoEntity == null) { + // Add the device + Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId") + val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo) + newEntity.firstTimeSeenLocalTs = clock.epochMillis() + userEntity.devices.add(newEntity) + } else { + // Update the device + Timber.d("Update device ${cryptoDeviceInfo.deviceId} of user $userId") + CryptoMapper.updateDeviceInfoEntity(existingDeviceInfoEntity, cryptoDeviceInfo) } } } } - override fun storeUserCrossSigningKeys( + override fun storeUserIdentity( userId: String, - masterKey: CryptoCrossSigningKey?, - selfSigningKey: CryptoCrossSigningKey?, - userSigningKey: CryptoCrossSigningKey? + userIdentity: UserIdentity, ) { - doRealmTransaction(realmConfiguration) { realm -> - UserEntity.getOrCreate(realm, userId) - .let { userEntity -> - if (masterKey == null || selfSigningKey == null) { - // The user has disabled cross signing? - userEntity.crossSigningInfoEntity?.deleteOnCascade() - userEntity.crossSigningInfoEntity = null - } else { - var shouldResetMyDevicesLocalTrust = false - CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo -> - // What should we do if we detect a change of the keys? - val existingMaster = signingInfo.getMasterKey() - if (existingMaster != null && existingMaster.publicKeyBase64 == masterKey.unpaddedBase64PublicKey) { - crossSigningKeysMapper.update(existingMaster, masterKey) - } else { - Timber.d("## CrossSigning MSK change for $userId") - val keyEntity = crossSigningKeysMapper.map(masterKey) - signingInfo.setMasterKey(keyEntity) - if (userId == this.userId) { - shouldResetMyDevicesLocalTrust = true - // my msk has changed! clear my private key - // Could we have some race here? e.g I am the one that did change the keys - // could i get this update to early and clear the private keys? - // -> initializeCrossSigning is guarding for that by storing all at once - realm.where().findFirst()?.apply { - xSignMasterPrivateKey = null - } + doRealmTransaction("storeUserIdentity", realmConfiguration) { realm -> + storeUserIdentity(realm, userId, userIdentity) + } + } + + private fun storeUserIdentity( + realm: Realm, + userId: String, + userIdentity: UserIdentity, + ) { + UserEntity.getOrCreate(realm, userId) + .let { userEntity -> + if (userIdentity.masterKey == null || userIdentity.selfSigningKey == null) { + // The user has disabled cross signing? + userEntity.crossSigningInfoEntity?.deleteOnCascade() + userEntity.crossSigningInfoEntity = null + } else { + var shouldResetMyDevicesLocalTrust = false + CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo -> + // What should we do if we detect a change of the keys? + val existingMaster = signingInfo.getMasterKey() + if (existingMaster != null && existingMaster.publicKeyBase64 == userIdentity.masterKey.unpaddedBase64PublicKey) { + crossSigningKeysMapper.update(existingMaster, userIdentity.masterKey) + } else { + Timber.d("## CrossSigning MSK change for $userId") + val keyEntity = crossSigningKeysMapper.map(userIdentity.masterKey) + signingInfo.setMasterKey(keyEntity) + if (userId == this.userId) { + shouldResetMyDevicesLocalTrust = true + // my msk has changed! clear my private key + // Could we have some race here? e.g I am the one that did change the keys + // could i get this update to early and clear the private keys? + // -> initializeCrossSigning is guarding for that by storing all at once + realm.where().findFirst()?.apply { + xSignMasterPrivateKey = null } } - - val existingSelfSigned = signingInfo.getSelfSignedKey() - if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == selfSigningKey.unpaddedBase64PublicKey) { - crossSigningKeysMapper.update(existingSelfSigned, selfSigningKey) - } else { - Timber.d("## CrossSigning SSK change for $userId") - val keyEntity = crossSigningKeysMapper.map(selfSigningKey) - signingInfo.setSelfSignedKey(keyEntity) - if (userId == this.userId) { - shouldResetMyDevicesLocalTrust = true - // my ssk has changed! clear my private key - realm.where().findFirst()?.apply { - xSignSelfSignedPrivateKey = null - } - } - } - - // Only for me - if (userSigningKey != null) { - val existingUSK = signingInfo.getUserSigningKey() - if (existingUSK != null && existingUSK.publicKeyBase64 == userSigningKey.unpaddedBase64PublicKey) { - crossSigningKeysMapper.update(existingUSK, userSigningKey) - } else { - Timber.d("## CrossSigning USK change for $userId") - val keyEntity = crossSigningKeysMapper.map(userSigningKey) - signingInfo.setUserSignedKey(keyEntity) - if (userId == this.userId) { - shouldResetMyDevicesLocalTrust = true - // my usk has changed! clear my private key - realm.where().findFirst()?.apply { - xSignUserPrivateKey = null - } - } - } - } - - // When my cross signing keys are reset, we consider clearing all existing device trust - if (shouldResetMyDevicesLocalTrust) { - realm.where() - .equalTo(UserEntityFields.USER_ID, this.userId) - .findFirst() - ?.devices?.forEach { - it?.trustLevelEntity?.crossSignedVerified = false - it?.trustLevelEntity?.locallyVerified = it.deviceId == deviceId - } - } - userEntity.crossSigningInfoEntity = signingInfo } + + val existingSelfSigned = signingInfo.getSelfSignedKey() + if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == userIdentity.selfSigningKey.unpaddedBase64PublicKey) { + crossSigningKeysMapper.update(existingSelfSigned, userIdentity.selfSigningKey) + } else { + Timber.d("## CrossSigning SSK change for $userId") + val keyEntity = crossSigningKeysMapper.map(userIdentity.selfSigningKey) + signingInfo.setSelfSignedKey(keyEntity) + if (userId == this.userId) { + shouldResetMyDevicesLocalTrust = true + // my ssk has changed! clear my private key + realm.where().findFirst()?.apply { + xSignSelfSignedPrivateKey = null + } + } + } + + // Only for me + if (userIdentity.userSigningKey != null) { + val existingUSK = signingInfo.getUserSigningKey() + if (existingUSK != null && existingUSK.publicKeyBase64 == userIdentity.userSigningKey.unpaddedBase64PublicKey) { + crossSigningKeysMapper.update(existingUSK, userIdentity.userSigningKey) + } else { + Timber.d("## CrossSigning USK change for $userId") + val keyEntity = crossSigningKeysMapper.map(userIdentity.userSigningKey) + signingInfo.setUserSignedKey(keyEntity) + if (userId == this.userId) { + shouldResetMyDevicesLocalTrust = true + // my usk has changed! clear my private key + realm.where().findFirst()?.apply { + xSignUserPrivateKey = null + } + } + } + } + + // When my cross signing keys are reset, we consider clearing all existing device trust + if (shouldResetMyDevicesLocalTrust) { + realm.where() + .equalTo(UserEntityFields.USER_ID, this.userId) + .findFirst() + ?.devices?.forEach { + it?.trustLevelEntity?.crossSignedVerified = false + it?.trustLevelEntity?.locallyVerified = it.deviceId == deviceId + } + } + userEntity.crossSigningInfoEntity = signingInfo } } - } + } } override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { @@ -482,7 +491,7 @@ internal class RealmCryptoStore @Inject constructor( override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) { Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}") - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("storePrivateKeysInfo", realmConfiguration) { realm -> realm.where().findFirst()?.apply { xSignMasterPrivateKey = msk xSignUserPrivateKey = usk @@ -492,7 +501,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("saveBackupRecoveryKey", realmConfiguration) { realm -> realm.where().findFirst()?.apply { keyBackupRecoveryKey = recoveryKey keyBackupRecoveryKeyVersion = version @@ -518,7 +527,7 @@ internal class RealmCryptoStore @Inject constructor( override fun storeMSKPrivateKey(msk: String?) { Timber.v("## CRYPTO | *** storeMSKPrivateKey ${msk != null} ") - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("storeMSKPrivateKey", realmConfiguration) { realm -> realm.where().findFirst()?.apply { xSignMasterPrivateKey = msk } @@ -527,7 +536,7 @@ internal class RealmCryptoStore @Inject constructor( override fun storeSSKPrivateKey(ssk: String?) { Timber.v("## CRYPTO | *** storeSSKPrivateKey ${ssk != null} ") - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("storeSSKPrivateKey", realmConfiguration) { realm -> realm.where().findFirst()?.apply { xSignSelfSignedPrivateKey = ssk } @@ -536,7 +545,7 @@ internal class RealmCryptoStore @Inject constructor( override fun storeUSKPrivateKey(usk: String?) { Timber.v("## CRYPTO | *** storeUSKPrivateKey ${usk != null} ") - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("storeUSKPrivateKey", realmConfiguration) { realm -> realm.where().findFirst()?.apply { xSignUserPrivateKey = usk } @@ -669,7 +678,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun storeRoomAlgorithm(roomId: String, algorithm: String?) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("storeRoomAlgorithm", realmConfiguration) { CryptoRoomEntity.getOrCreate(it, roomId).let { entity -> entity.algorithm = algorithm // store anyway the new algorithm, but mark the room @@ -710,7 +719,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("setShouldEncryptForInvitedMembers", realmConfiguration) { CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers } } @@ -718,7 +727,7 @@ internal class RealmCryptoStore @Inject constructor( override fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) { Timber.tag(loggerTag.value) .v("setShouldShareHistory for room $roomId is $shouldShareHistory") - doRealmTransaction(realmConfiguration) { + doRealmTransaction("setShouldShareHistory", realmConfiguration) { CryptoRoomEntity.getOrCreate(it, roomId).shouldShareHistory = shouldShareHistory } } @@ -735,7 +744,7 @@ internal class RealmCryptoStore @Inject constructor( if (sessionIdentifier != null) { val key = OlmSessionEntity.createPrimaryKey(sessionIdentifier, deviceKey) - doRealmTransaction(realmConfiguration) { + doRealmTransaction("storeSession", realmConfiguration) { val realmOlmSession = OlmSessionEntity().apply { primaryKey = key sessionId = sessionIdentifier @@ -792,7 +801,7 @@ internal class RealmCryptoStore @Inject constructor( return } - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("storeInboundGroupSessions", realmConfiguration) { realm -> sessions.forEach { wrapper -> val sessionIdentifier = try { @@ -916,7 +925,7 @@ internal class RealmCryptoStore @Inject constructor( override fun removeInboundGroupSession(sessionId: String, senderKey: String) { val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) - doRealmTransaction(realmConfiguration) { + doRealmTransaction("removeInboundGroupSession", realmConfiguration) { it.where() .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) .findAll() @@ -935,7 +944,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun setKeyBackupVersion(keyBackupVersion: String?) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("setKeyBackupVersion", realmConfiguration) { it.where().findFirst()?.backupVersion = keyBackupVersion } } @@ -947,7 +956,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("setKeysBackupData", realmConfiguration) { if (keysBackupData == null) { // Clear the table it.where() @@ -961,7 +970,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun resetBackupMarkers() { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("resetBackupMarkers", realmConfiguration) { it.where() .findAll() .map { inboundGroupSession -> @@ -975,7 +984,7 @@ internal class RealmCryptoStore @Inject constructor( return } - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("markBackupDoneForInboundGroupSessions", realmConfiguration) { realm -> olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> try { val sessionIdentifier = @@ -1034,13 +1043,13 @@ internal class RealmCryptoStore @Inject constructor( } override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("setGlobalBlacklistUnverifiedDevices", realmConfiguration) { it.where().findFirst()?.globalBlacklistUnverifiedDevices = block } } override fun enableKeyGossiping(enable: Boolean) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("enableKeyGossiping", realmConfiguration) { it.where().findFirst()?.globalEnableKeyGossiping = enable } } @@ -1064,13 +1073,13 @@ internal class RealmCryptoStore @Inject constructor( } override fun enableShareKeyOnInvite(enable: Boolean) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("enableShareKeyOnInvite", realmConfiguration) { it.where().findFirst()?.enableKeyForwardingOnInvite = enable } } override fun setDeviceKeysUploaded(uploaded: Boolean) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("setDeviceKeysUploaded", realmConfiguration) { it.where().findFirst()?.deviceKeysSentToServer = uploaded } } @@ -1117,7 +1126,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("blockUnverifiedDevicesInRoom", realmConfiguration) { realm -> CryptoRoomEntity.getById(realm, roomId) ?.blacklistUnverifiedDevices = block } @@ -1137,7 +1146,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("saveDeviceTrackingStatuses", realmConfiguration) { deviceTrackingStatuses .map { entry -> UserEntity.getOrCreate(it, entry.key) @@ -1270,7 +1279,7 @@ internal class RealmCryptoStore @Inject constructor( ): OutgoingKeyRequest { // Insert the request and return the one passed in parameter lateinit var request: OutgoingKeyRequest - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("getOrAddOutgoingRoomKeyRequest", realmConfiguration) { realm -> val existing = realm.where() .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId) @@ -1308,7 +1317,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("updateOutgoingRoomKeyRequestState", realmConfiguration) { realm -> realm.where() .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) .findFirst()?.apply { @@ -1322,7 +1331,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("updateOutgoingRoomKeyRequiredIndex", realmConfiguration) { realm -> realm.where() .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) .findFirst()?.apply { @@ -1339,7 +1348,7 @@ internal class RealmCryptoStore @Inject constructor( fromDevice: String?, event: Event ) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("updateOutgoingRoomKeyReply", realmConfiguration) { realm -> realm.where() .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId) .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId) @@ -1355,7 +1364,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun deleteOutgoingRoomKeyRequest(requestId: String) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("deleteOutgoingRoomKeyRequest", realmConfiguration) { realm -> realm.where() .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) .findFirst()?.deleteOnCascade() @@ -1363,7 +1372,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("deleteOutgoingRoomKeyRequestInState", realmConfiguration) { realm -> realm.where() .equalTo(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, state.name) .findAll() @@ -1499,7 +1508,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("setMyCrossSigningInfo", realmConfiguration) { realm -> realm.where().findFirst()?.userId?.let { userId -> addOrUpdateCrossSigningInfo(realm, userId, info) } @@ -1507,7 +1516,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun setUserKeysAsTrusted(userId: String, trusted: Boolean) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("setUserKeysAsTrusted", realmConfiguration) { realm -> val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java) .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) .findFirst() @@ -1527,7 +1536,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("setDeviceTrust", realmConfiguration) { realm -> realm.where(DeviceInfoEntity::class.java) .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) .findFirst()?.let { deviceInfoEntity -> @@ -1547,7 +1556,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun clearOtherUserTrust() { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("clearOtherUserTrust", realmConfiguration) { realm -> val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java) .findAll() xInfoEntities?.forEach { info -> @@ -1562,7 +1571,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun updateUsersTrust(check: (String) -> Boolean) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("updateUsersTrust", realmConfiguration) { realm -> val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java) .findAll() xInfoEntities?.forEach { xInfoEntity -> @@ -1670,13 +1679,13 @@ internal class RealmCryptoStore @Inject constructor( } override fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("setCrossSigningInfo", realmConfiguration) { realm -> addOrUpdateCrossSigningInfo(realm, userId, info) } } override fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("markMyMasterKeyAsLocallyTrusted", realmConfiguration) { realm -> realm.where().findFirst()?.userId?.let { myUserId -> CrossSigningInfoEntity.get(realm, myUserId)?.getMasterKey()?.let { xInfoEntity -> val level = xInfoEntity.trustLevelEntity @@ -1715,7 +1724,7 @@ internal class RealmCryptoStore @Inject constructor( val roomId = withHeldContent.roomId ?: return val sessionId = withHeldContent.sessionId ?: return if (withHeldContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("addWithHeldMegolmSession", realmConfiguration) { realm -> WithHeldSessionEntity.getOrCreate(realm, roomId, sessionId)?.let { it.code = withHeldContent.code it.senderKey = withHeldContent.senderKey @@ -1747,7 +1756,7 @@ internal class RealmCryptoStore @Inject constructor( deviceIdentityKey: String, chainIndex: Int ) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("markedSessionAsShared", realmConfiguration) { realm -> SharedSessionEntity.create( realm = realm, roomId = roomId, @@ -1796,7 +1805,7 @@ internal class RealmCryptoStore @Inject constructor( */ override fun tidyUpDataBase() { val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000 - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("tidyUpDataBase", realmConfiguration) { realm -> // Clean the old ones? realm.where() @@ -1817,4 +1826,31 @@ internal class RealmCryptoStore @Inject constructor( // Can we do something for WithHeldSessionEntity? } } + + override fun storeData(cryptoStoreAggregator: CryptoStoreAggregator) { + if (cryptoStoreAggregator.isEmpty()) { + return + } + doRealmTransaction("storeData - CryptoStoreAggregator", realmConfiguration) { realm -> + // setShouldShareHistory + cryptoStoreAggregator.setShouldShareHistoryData.forEach { + CryptoRoomEntity.getOrCreate(realm, it.key).shouldShareHistory = it.value + } + // setShouldEncryptForInvitedMembers + cryptoStoreAggregator.setShouldEncryptForInvitedMembersData.forEach { + CryptoRoomEntity.getOrCreate(realm, it.key).shouldEncryptForInvitedMembers = it.value + } + } + } + + override fun storeData(userDataToStore: UserDataToStore) { + doRealmTransaction("storeData - UserDataToStore", realmConfiguration) { realm -> + userDataToStore.userDevices.forEach { + storeUserDevices(realm, it.key, it.value) + } + userDataToStore.userIdentities.forEach { + storeUserIdentity(realm, it.key, it.value) + } + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt index 9bd197e42e..f89221b627 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.di -import androidx.annotation.Nullable import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonQualifier import com.squareup.moshi.Moshi @@ -28,7 +27,6 @@ import java.lang.reflect.Type internal annotation class SerializeNulls { companion object { val JSON_ADAPTER_FACTORY: JsonAdapter.Factory = object : JsonAdapter.Factory { - @Nullable override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { val nextAnnotations = Types.nextAnnotations(annotations, SerializeNulls::class.java) ?: return null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt index 4e0525536c..334a8c5076 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.network.interceptors -import androidx.annotation.NonNull import okhttp3.logging.HttpLoggingInterceptor import org.json.JSONArray import org.json.JSONException @@ -38,7 +37,7 @@ internal class FormattedJsonHttpLogger( * @param message */ @Synchronized - override fun log(@NonNull message: String) { + override fun log(message: String) { Timber.v(message) // Try to log formatted Json only if there is a chance that [message] contains Json. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt index 8b54978279..6c28b9fcce 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.network.parsing -import androidx.annotation.Nullable import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter @@ -32,14 +31,12 @@ internal interface CheckNumberType { companion object { val JSON_ADAPTER_FACTORY = object : JsonAdapter.Factory { - @Nullable override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { if (type !== Any::class.java) { return null } val delegate: JsonAdapter = moshi.nextAdapter(this, Any::class.java, emptySet()) return object : JsonAdapter() { - @Nullable @Throws(IOException::class) override fun fromJson(reader: JsonReader): Any? { return if (reader.peek() !== JsonReader.Token.NUMBER) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt index cfc26045a0..ce34b0430e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt @@ -42,14 +42,12 @@ internal class StreamEventsManager @Inject constructor() { listeners.remove(listener) } - fun dispatchLiveEventReceived(event: Event, roomId: String, initialSync: Boolean) { + fun dispatchLiveEventReceived(event: Event, roomId: String) { Timber.v("## dispatchLiveEventReceived ${event.eventId}") coroutineScope.launch { - if (!initialSync) { - listeners.forEach { - tryOrNull { - it.onLiveEvent(roomId, event) - } + listeners.forEach { + tryOrNull { + it.onLiveEvent(roomId, event) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesResponse.kt index 5f35c919fc..e359410f17 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesResponse.kt @@ -30,10 +30,4 @@ internal data class GetPushRulesResponse( */ @Json(name = "global") val global: RuleSet, - - /** - * Device specific rules, apply only to current device. - */ - @Json(name = "device") - val device: RuleSet? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt index 88c78aa460..4a46f56a70 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt @@ -42,7 +42,6 @@ internal class DefaultSavePushRulesTask @Inject constructor(@SessionDatabase pri .findAll() .forEach { it.deleteOnCascade() } - // Save only global rules for the moment val globalRules = params.pushRules.global val content = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.CONTENT } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt index 793c2573be..653069b3c8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt @@ -176,7 +176,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor( } // Give info to crypto module - cryptoService.onStateEvent(roomId, event) + cryptoService.onStateEvent(roomId, event, null) } roomMemberContentsByUser.getOrPut(event.senderId) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index e30c3183de..b552f9960d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -608,26 +608,22 @@ internal class LocalEchoEventFactory @Inject constructor( return clock.epochMillis() } - /** - * Creates a reply to a regular timeline Event or a thread Event if needed. - */ - fun createReplyTextEvent( - roomId: String, + fun createReplyTextContent( eventReplied: TimelineEvent, replyText: CharSequence, replyTextFormatted: CharSequence?, autoMarkdown: Boolean, rootThreadEventId: String? = null, showInThread: Boolean, - additionalContent: Content? = null - ): Event? { + isRedactedEvent: Boolean = false + ): MessageContent? { // Fallbacks and event representation // TODO Add error/warning logs when any of this is null val permalink = permalinkFactory.createPermalink(eventReplied.root, false) ?: return null val userId = eventReplied.root.senderId ?: return null val userLink = permalinkFactory.createPermalink(userId, false) ?: return null - val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply()) + val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply(), isRedactedEvent) // For inline images and user pills val replyTextProcessed = textPillsUtils.processSpecialSpansToHtml(replyText) ?: replyText @@ -648,7 +644,7 @@ internal class LocalEchoEventFactory @Inject constructor( val replyFallback = buildReplyFallback(body, userId, replyText.toString()) val eventId = eventReplied.root.eventId ?: return null - val content = MessageTextContent( + return MessageTextContent( msgType = MessageType.MSGTYPE_TEXT, format = MessageFormat.FORMAT_MATRIX_HTML, body = replyFallback, @@ -659,7 +655,25 @@ internal class LocalEchoEventFactory @Inject constructor( showInThread = showInThread ) ) - return createMessageEvent(roomId, content, additionalContent) + } + + /** + * Creates a reply to a regular timeline Event or a thread Event if needed. + */ + fun createReplyTextEvent( + roomId: String, + eventReplied: TimelineEvent, + replyText: CharSequence, + replyTextFormatted: CharSequence?, + autoMarkdown: Boolean, + rootThreadEventId: String? = null, + showInThread: Boolean, + additionalContent: Content? = null, + ): Event? { + val content = createReplyTextContent(eventReplied, replyText, replyTextFormatted, autoMarkdown, rootThreadEventId, showInThread) + return content?.let { + createMessageEvent(roomId, it, additionalContent) + } } private fun generateThreadRelationContent(rootThreadEventId: String) = @@ -728,7 +742,7 @@ internal class LocalEchoEventFactory @Inject constructor( * In case of an edit of a reply the last content is not * himself a reply, but it will contain the fallbacks, so we have to trim them. */ - private fun bodyForReply(content: MessageContent?, isReply: Boolean): TextContent { + fun bodyForReply(content: MessageContent?, isReply: Boolean, isRedactedEvent: Boolean = false): TextContent { when (content?.msgType) { MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_TEXT, @@ -737,7 +751,9 @@ internal class LocalEchoEventFactory @Inject constructor( if (content is MessageContentWithFormattedBody) { formattedText = content.matrixFormattedBody } - return if (isReply) { + return if (isRedactedEvent) { + TextContent("message removed.") + } else if (isReply) { TextContent(content.body, formattedText).removeInReplyFallbacks() } else { TextContent(content.body, formattedText) @@ -751,7 +767,11 @@ internal class LocalEchoEventFactory @Inject constructor( MessageType.MSGTYPE_POLL_START -> { return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "") } - else -> return TextContent(content?.body ?: "") + else -> { + return if (isRedactedEvent) { + TextContent("message removed.") + } else TextContent(content?.body ?: "") + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index a9f43ad3c8..f3102a974a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -46,6 +46,7 @@ import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler @@ -68,6 +69,7 @@ internal class DefaultTimeline( private val settings: TimelineSettings, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val clock: Clock, + localEchoEventFactory: LocalEchoEventFactory, stateEventDataSource: StateEventDataSource, paginationTask: PaginationTask, getEventTask: GetContextOfEventTask, @@ -123,6 +125,7 @@ internal class DefaultTimeline( onNewTimelineEvents = this::onNewTimelineEvents, stateEventDataSource = stateEventDataSource, matrixCoroutineDispatchers = coroutineDispatchers, + localEchoEventFactory = localEchoEventFactory ) private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Live) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index e163d24df4..af66ba42b4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler @@ -56,6 +57,7 @@ internal class DefaultTimelineService @AssistedInject constructor( private val timelineEventDataSource: TimelineEventDataSource, private val clock: Clock, private val stateEventDataSource: StateEventDataSource, + private val localEchoEventFactory: LocalEchoEventFactory ) : TimelineService { @AssistedFactory @@ -83,6 +85,7 @@ internal class DefaultTimelineService @AssistedInject constructor( lightweightSettingsStorage = lightweightSettingsStorage, clock = clock, stateEventDataSource = stateEventDataSource, + localEchoEventFactory = localEchoEventFactory ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index 2675ca5789..64c679bc8a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -47,7 +47,12 @@ import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource +import org.matrix.android.sdk.internal.session.room.timeline.decorator.TimelineEventDecorator +import org.matrix.android.sdk.internal.session.room.timeline.decorator.TimelineEventDecoratorChain +import org.matrix.android.sdk.internal.session.room.timeline.decorator.UiEchoDecorator +import org.matrix.android.sdk.internal.session.room.timeline.decorator.UpdatedReplyDecorator import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber @@ -118,6 +123,7 @@ internal class LoadTimelineStrategy constructor( val onNewTimelineEvents: (List) -> Unit, val stateEventDataSource: StateEventDataSource, val matrixCoroutineDispatchers: MatrixCoroutineDispatchers, + val localEchoEventFactory: LocalEchoEventFactory ) private var getContextLatch: CompletableDeferred? = null @@ -372,6 +378,19 @@ internal class LoadTimelineStrategy constructor( } private fun RealmResults.createTimelineChunk(): TimelineChunk? { + fun createTimelineEventDecorator(): TimelineEventDecorator { + val decorators = listOf( + UiEchoDecorator(uiEchoManager), + UpdatedReplyDecorator( + realm = dependencies.realm, + roomId = roomId, + localEchoEventFactory = dependencies.localEchoEventFactory, + timelineEventMapper = dependencies.timelineEventMapper + ) + ) + return TimelineEventDecoratorChain(decorators) + } + return firstOrNull()?.let { if (ENABLE_TIMELINE_EMPTY_CHUNK_CLEANUP) { // Before creating timeline chunks, make sure there are no empty chunks linking themselves, causing a stuck timeline @@ -398,6 +417,9 @@ internal class LoadTimelineStrategy constructor( initialEventId = mode.originEventId(), onBuiltEvents = dependencies.onEventsUpdated, onEventsDeleted = dependencies.onEventsDeleted, + realm = dependencies.realm, + localEchoEventFactory = dependencies.localEchoEventFactory, + decorator = createTimelineEventDecorator() ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index c67194616f..4683d88a7c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -20,6 +20,7 @@ import de.spiritcroc.matrixsdk.util.DbgUtil import de.spiritcroc.matrixsdk.util.Dimber import io.realm.OrderedCollectionChangeSet import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm import io.realm.RealmConfiguration import io.realm.RealmObjectChangeListener import io.realm.RealmQuery @@ -29,9 +30,11 @@ import kotlinx.coroutines.CompletableDeferred import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.isReply import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.api.session.room.timeline.getRelationContent import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.internal.database.mapper.EventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper @@ -41,10 +44,13 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.timeline.decorator.TimelineEventDecorator import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import timber.log.Timber import java.util.Collections import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference /** * This is a wrapper around a ChunkEntity in the database. @@ -68,6 +74,9 @@ internal class TimelineChunk( private val initialEventId: String?, private val onBuiltEvents: (Boolean) -> Unit, private val onEventsDeleted: () -> Unit, + private val realm: AtomicReference, + private val decorator: TimelineEventDecorator, + val localEchoEventFactory: LocalEchoEventFactory, ) { private val isLastForward = AtomicBoolean(chunkEntity.isLastForward) @@ -79,6 +88,13 @@ internal class TimelineChunk( private val dimber = Dimber("TimelineChunks", DbgUtil.DBG_TIMELINE_CHUNKS) + /** + Map of eventId -> eventId + The key holds the eventId of the repliedTo event. + The value holds a set of eventIds of all events replying to this event. + */ + private val repliedEventsMap = HashMap>() + private val chunkObjectListener = RealmObjectChangeListener { _, changeSet -> if (changeSet == null) return@RealmObjectChangeListener if (changeSet.isDeleted.orFalse()) { @@ -386,9 +402,6 @@ internal class TimelineChunk( builtTimelineEvents .mapIndexed { index, timelineEvent -> // timelineEventEntity -> //val timelineEvent = timelineEventEntity.buildAndDecryptIfNeeded() - if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) { - isLastBackward.set(true) - } if (direction == Timeline.Direction.FORWARDS) { builtEventsIndexes[timelineEvent.eventId] = index builtEvents.add(index, timelineEvent) @@ -431,26 +444,45 @@ internal class TimelineChunk( } private fun TimelineEventEntity.buildAndDecryptIfNeeded(): TimelineEvent { - val timelineEvent = buildTimelineEvent(this) - val transactionId = timelineEvent.root.unsignedData?.transactionId - uiEchoManager?.onSyncedEvent(transactionId) - if (timelineEvent.isEncrypted() && - timelineEvent.root.mxDecryptionResult == null) { - timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) } + /** + * Makes sure to update some internal state after a TimelineEvent is built. + */ + fun processTimelineEvent(timelineEvent: TimelineEvent) { + if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) { + isLastBackward.set(true) + } else if (timelineEvent.root.isReply()) { + val relatesEventId = timelineEvent.getRelationContent()?.inReplyTo?.eventId + if (relatesEventId != null) { + val relatedEvents = repliedEventsMap.getOrPut(relatesEventId) { mutableSetOf() } + relatedEvents.add(timelineEvent.eventId) + } + } + val transactionId = timelineEvent.root.unsignedData?.transactionId + uiEchoManager?.onSyncedEvent(transactionId) } - if (!timelineEvent.isEncrypted() && !lightweightSettingsStorage.areThreadMessagesEnabled()) { - // Thread aware for not encrypted events - timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) } + + fun decryptIfNeeded(timelineEvent: TimelineEvent) { + if (timelineEvent.isEncrypted() && + timelineEvent.root.mxDecryptionResult == null) { + timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) } + } + if (!timelineEvent.isEncrypted() && !lightweightSettingsStorage.areThreadMessagesEnabled()) { + // Thread aware for not encrypted events + timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) } + } + } + + return buildTimelineEvent(this).also { timelineEvent -> + decryptIfNeeded(timelineEvent) + processTimelineEvent(timelineEvent) } - return timelineEvent } private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map( timelineEventEntity = eventEntity, buildReadReceipts = timelineSettings.buildReadReceipts - ).let { - // eventually enhance with ui echo? - (uiEchoManager?.decorateEventWithReactionUiEcho(it) ?: it) + ).let { timelineEvent -> + decorator.decorate(timelineEvent) } /** @@ -530,13 +562,9 @@ internal class TimelineChunk( if (!validateInsertion(range, results)) continue val newItems = results .subList(range.startIndex, range.startIndex + range.length) - .map { it.buildAndDecryptIfNeeded() } - builtEventsIndexes.entries.filter { it.value >= range.startIndex }.forEach { it.setValue(it.value + range.length) } - newItems.mapIndexed { index, timelineEvent -> - if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) { - isLastBackward.set(true) - } + newItems.mapIndexed { index, timelineEventEntity -> + val timelineEvent = timelineEventEntity.buildAndDecryptIfNeeded() val correctedIndex = range.startIndex + index dimber.i{"TimelineChunk.handleDatabaseChangeSet.$dbgId: insert ${timelineEvent.eventId} at $correctedIndex (${range.startIndex} + $index)"} builtEvents.add(correctedIndex, timelineEvent) @@ -547,11 +575,17 @@ internal class TimelineChunk( for (range in modifications) { for (modificationIndex in (range.startIndex until range.startIndex + range.length)) { val updatedEntity = results[modificationIndex] ?: continue - val builtEventIndex = builtEventsIndexes[updatedEntity.eventId] ?: continue - try { - builtEvents[builtEventIndex] = updatedEntity.buildAndDecryptIfNeeded() - } catch (failure: Throwable) { - Timber.v("Fail to update items at index: $modificationIndex") + val updatedEventId = updatedEntity.eventId + val repliesOfUpdatedEvent = repliedEventsMap.getOrElse(updatedEventId) { emptySet() }.mapNotNull { eventId -> + results.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() + } + repliesOfUpdatedEvent.plus(updatedEntity).forEach { entityToRebuild -> + val builtEventIndex = builtEventsIndexes[entityToRebuild.eventId] ?: return@forEach + try { + builtEvents[builtEventIndex] = entityToRebuild.buildAndDecryptIfNeeded() + } catch (failure: Throwable) { + Timber.v("Fail to update items at index: $modificationIndex") + } } } } @@ -618,7 +652,10 @@ internal class TimelineChunk( lightweightSettingsStorage = lightweightSettingsStorage, initialEventId = null, onBuiltEvents = this.onBuiltEvents, - onEventsDeleted = this.onEventsDeleted + onEventsDeleted = this.onEventsDeleted, + decorator = this.decorator, + realm = realm, + localEchoEventFactory = localEchoEventFactory ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/TimelineEventDecorator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/TimelineEventDecorator.kt new file mode 100644 index 0000000000..261407ba5f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/TimelineEventDecorator.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline.decorator + +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +/** + * This interface can be used to make a copy of a TimelineEvent with new data, before the event is posted to the timeline. + */ +internal fun interface TimelineEventDecorator { + fun decorate(timelineEvent: TimelineEvent): TimelineEvent +} + +/** + * This is an implementation of [TimelineEventDecorator] which chains calls to decorators. + */ +internal class TimelineEventDecoratorChain(private val decorators: List) : TimelineEventDecorator { + + override fun decorate(timelineEvent: TimelineEvent): TimelineEvent { + var decorated = timelineEvent + val iterator = decorators.iterator() + while (iterator.hasNext()) { + val decorator = iterator.next() + decorated = decorator.decorate(decorated) + } + return decorated + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/UiEchoDecorator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/UiEchoDecorator.kt new file mode 100644 index 0000000000..778a9d27d9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/UiEchoDecorator.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline.decorator + +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.session.room.timeline.UIEchoManager + +internal class UiEchoDecorator(private val uiEchoManager: UIEchoManager?) : TimelineEventDecorator { + + override fun decorate(timelineEvent: TimelineEvent): TimelineEvent { + return uiEchoManager?.decorateEventWithReactionUiEcho(timelineEvent) ?: timelineEvent + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/UpdatedReplyDecorator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/UpdatedReplyDecorator.kt new file mode 100644 index 0000000000..2b12fe814c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/UpdatedReplyDecorator.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline.decorator + +import io.realm.Realm +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.isThread +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent +import org.matrix.android.sdk.api.session.room.timeline.getRelationContent +import org.matrix.android.sdk.api.session.room.timeline.isReply +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import java.util.concurrent.atomic.AtomicReference + +internal class UpdatedReplyDecorator( + private val realm: AtomicReference, + private val roomId: String, + private val localEchoEventFactory: LocalEchoEventFactory, + private val timelineEventMapper: TimelineEventMapper, +) : TimelineEventDecorator { + + override fun decorate(timelineEvent: TimelineEvent): TimelineEvent { + return if (timelineEvent.isReply() && !timelineEvent.root.isThread()) { + val newRepliedEvent = createNewRepliedEvent(timelineEvent) ?: return timelineEvent + timelineEvent.copy(root = newRepliedEvent) + } else { + timelineEvent + } + } + + private fun createNewRepliedEvent(currentTimelineEvent: TimelineEvent): Event? { + val relatesEventId = currentTimelineEvent.getRelationContent()?.inReplyTo?.eventId ?: return null + val timelineEventEntity = TimelineEventEntity.where( + realm.get(), + roomId, + relatesEventId + ).findFirst() ?: return null + + val isRedactedEvent = timelineEventEntity.root?.asDomain()?.isRedacted() ?: false + + val replyText = localEchoEventFactory + .bodyForReply(currentTimelineEvent.getLastMessageContent(), true).formattedText ?: "" + + val newContent = localEchoEventFactory.createReplyTextContent( + timelineEventMapper.map(timelineEventEntity), + replyText, + null, + false, + showInThread = false, + isRedactedEvent = isRedactedEvent + ).toContent() + + val decryptionResultToSet = currentTimelineEvent.root.mxDecryptionResult?.copy( + payload = mapOf( + "content" to newContent, + "type" to EventType.MESSAGE + ) + ) + + val contentToSet = if (currentTimelineEvent.isEncrypted()) { + // Keep encrypted content as is + currentTimelineEvent.root.content + } else { + // Use new content + newContent + } + + return currentTimelineEvent.root.copyAll( + content = contentToSet, + mxDecryptionResult = decryptionResultToSet, + mCryptoError = null, + mCryptoErrorReason = null + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt index 5ff5f293e6..908a4141f6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.session.SessionListeners @@ -92,7 +93,7 @@ internal class SyncResponseHandler @Inject constructor( postTreatmentSyncResponse(syncResponse, isInitialSync) - markCryptoSyncCompleted(syncResponse) + markCryptoSyncCompleted(syncResponse, aggregator.cryptoStoreAggregator) handlePostSync() @@ -218,10 +219,10 @@ internal class SyncResponseHandler @Inject constructor( } } - private fun markCryptoSyncCompleted(syncResponse: SyncResponse) { + private fun markCryptoSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) { relevantPlugins.measureSpan("task", "crypto_sync_handler_onSyncCompleted") { measureTimeMillis { - cryptoSyncHandler.onSyncCompleted(syncResponse) + cryptoSyncHandler.onSyncCompleted(syncResponse, cryptoStoreAggregator) }.also { Timber.i("cryptoSyncHandler.onSyncCompleted took $it ms") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt index 2b7f936fa8..af05e08da3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.internal.session.sync +import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator + internal class SyncResponsePostTreatmentAggregator { // List of RoomId val ephemeralFilesToDelete = mutableListOf() @@ -28,4 +30,7 @@ internal class SyncResponsePostTreatmentAggregator { // Set of users to call `crossSigningService.checkTrustAndAffectedRoomShields` once per sync val userIdsForCheckingTrustAndAffectedRoomShields = mutableSetOf() + + // For the crypto store + val cryptoStoreAggregator = CryptoStoreAggregator() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt index 551db52dbd..7224b0c29c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt @@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator import org.matrix.android.sdk.internal.crypto.tasks.toDeviceTracingId import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService import org.matrix.android.sdk.internal.session.sync.ProgressReporter @@ -85,8 +86,8 @@ internal class CryptoSyncHandler @Inject constructor( } } - fun onSyncCompleted(syncResponse: SyncResponse) { - cryptoService.onSyncCompleted(syncResponse) + fun onSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) { + cryptoService.onSyncCompleted(syncResponse, cryptoStoreAggregator) } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index c34e58fa03..969f6d2971 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -258,7 +258,7 @@ internal class RoomSyncHandler @Inject constructor( root = eventEntity } // Give info to crypto module - cryptoService.onStateEvent(roomId, event) + cryptoService.onStateEvent(roomId, event, aggregator.cryptoStoreAggregator) roomMemberEventHandler.handle(realm, roomId, event, isInitialSync, aggregator) } } @@ -377,8 +377,16 @@ internal class RoomSyncHandler @Inject constructor( roomEntity.chunks.clearWith { it.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true) } roomTypingUsersHandler.handle(realm, roomId, null) roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.LEAVE) - roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, - roomSync.unreadNotifications, roomSync.unreadCount, roomSync.unreadThreadNotifications, aggregator = aggregator) + roomSummaryUpdater.update( + realm, + roomId, + membership, + roomSync.summary, + roomSync.unreadNotifications, + roomSync.unreadCount, + roomSync.unreadThreadNotifications, + aggregator = aggregator, + ) return roomEntity } @@ -424,7 +432,9 @@ internal class RoomSyncHandler @Inject constructor( val isInitialSync = insertType == EventInsertType.INITIAL_SYNC eventIds.add(event.eventId) - liveEventService.get().dispatchLiveEventReceived(event, roomId, isInitialSync) + if (!isInitialSync) { + liveEventService.get().dispatchLiveEventReceived(event, roomId) + } if (event.isEncrypted() && !isInitialSync) { try { @@ -487,7 +497,7 @@ internal class RoomSyncHandler @Inject constructor( } } // Give info to crypto module - cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync) + cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync, aggregator.cryptoStoreAggregator) // Try to remove local echo event.unsignedData?.transactionId?.also { txId -> diff --git a/tools/lint/lint.xml b/tools/lint/lint.xml index 3d3b073749..dbe30f2267 100644 --- a/tools/lint/lint.xml +++ b/tools/lint/lint.xml @@ -77,6 +77,7 @@ + diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh index f91e11584c..553c02101c 100755 --- a/tools/release/releaseScript.sh +++ b/tools/release/releaseScript.sh @@ -87,6 +87,14 @@ fi printf "OK\n" +printf "\n================================================================================\n" +printf "Ensuring main and develop branches are up to date...\n" + +git checkout main +git pull +git checkout develop +git pull + printf "\n================================================================================\n" # Guessing version to propose a default version versionMajorCandidate=`grep "ext.versionMajor" ./vector-app/build.gradle | cut -d " " -f3` @@ -103,14 +111,6 @@ versionMinor=`echo ${version} | cut -d "." -f2` versionPatch=`echo ${version} | cut -d "." -f3` nextPatchVersion=$((versionPatch + 2)) -printf "\n================================================================================\n" -printf "Ensuring main and develop branches are up to date...\n" - -git checkout main -git pull -git checkout develop -git pull - printf "\n================================================================================\n" printf "Starting the release ${version}\n" git flow release start ${version} @@ -190,6 +190,9 @@ yes | towncrier build --version "v${version}" printf "\n================================================================================\n" read -p "Check the file CHANGES.md consistency. It's possible to reorder items (most important changes first) or change their section if relevant. Also an opportunity to fix some typo, or rewrite things. Do not commit your change. Press enter when it's done." +# Get the changes to use it to create the GitHub release +changelogUrlEncoded=`git diff CHANGES.md | grep ^+ | tail -n +2 | cut -c2- | jq -sRr @uri | sed s/\(/%28/g | sed s/\)/%29/g` + printf "\n================================================================================\n" printf "Committing...\n" git commit -a -m "Changelog for version ${version}" @@ -263,7 +266,7 @@ else fi printf "\n================================================================================\n" -printf "Wait for the GitHub action https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Amain to build the 'main' branch.\n" +printf "Wait for the GitHub action https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%%3Amain to build the 'main' branch.\n" read -p "After GHA is finished, please enter the artifact URL (for 'vector-gplay-release-unsigned'): " artifactUrl printf "\n================================================================================\n" @@ -354,10 +357,15 @@ apkPath="${targetPath}/vector-gplay-arm64-v8a-release-signed.apk" adb -d install ${apkPath} read -p "Please run the APK on your phone to check that the upgrade went well (no init sync, etc.). Press enter when it's done." -# TODO Get the block to copy from towncrier earlier (be may be edited by the release manager)? -read -p "Create the release on gitHub from the tag https://github.com/vector-im/element-android/tags, copy paste the block from the file CHANGES.md. Press enter when it's done." -read -p "Add the 4 signed APKs to the GitHub release. They are located at ${targetPath}. Press enter when it's done." +printf "\n================================================================================\n" +githubCreateReleaseLink="https://github.com/vector-im/element-android/releases/new?tag=v${version}&title=Element%%20Android%%20v${version}&body=${changelogUrlEncoded}" +printf "Creating the release on gitHub.\n" +printf "Open this link: ${githubCreateReleaseLink}\n" +printf "Then\n" +printf " - click on the 'Generate releases notes' button\n" +printf " - Add the 4 signed APKs to the GitHub release. They are located at ${targetPath}\n" +read -p ". Press enter when it's done. " printf "\n================================================================================\n" printf "Message for the Android internal room:\n\n" diff --git a/vector-app/build.gradle b/vector-app/build.gradle index eaad3b12b0..ec7b29bbcc 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -37,7 +37,7 @@ ext.versionMinor = 5 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 18 +ext.versionPatch = 20 ext.scVersion = 62 @@ -364,7 +364,8 @@ dependencies { gplayImplementation "com.google.android.gms:play-services-location:21.0.1" // UnifiedPush gplay flavor only - gplayImplementation('com.google.firebase:firebase-messaging:23.1.0') { + gplayImplementation platform(libs.google.firebaseBom) + gplayImplementation(libs.google.messaging) { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' diff --git a/vector/build.gradle b/vector/build.gradle index 9038fa6af0..b221e9e263 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -133,7 +133,7 @@ dependencies { implementation libs.androidx.biometric api "org.threeten:threetenbp:1.4.0:no-tzdb" - api "com.gabrielittner.threetenbp:lazythreetenbp:0.12.0" + api "com.gabrielittner.threetenbp:lazythreetenbp:0.13.0" implementation libs.squareup.moshi implementation libs.squareup.moshiKt @@ -310,7 +310,7 @@ dependencies { // Fix issue with Jitsi. Inspired from https://github.com/android/android-test/issues/861#issuecomment-872067868 // Error was lots of `Duplicate class org.checkerframework.common.reflection.qual.MethodVal found in modules jetified-checker-3.1 (org.checkerframework:checker:3.1.1) and jetified-checker-qual-3.12.0 (org.checkerframework:checker-qual:3.12.0) //noinspection GradleDependency Cannot use latest 3.15.0 since it required min API 26. - implementation "org.checkerframework:checker:3.27.0" + implementation "org.checkerframework:checker:3.29.0" androidTestImplementation libs.androidx.testCore androidTestImplementation libs.androidx.testRunner diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index d22ab51e7a..911bbfa4a3 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -84,6 +84,7 @@ import im.vector.app.features.roomprofile.banned.RoomBannedMemberListViewModel import im.vector.app.features.roomprofile.members.RoomMemberListViewModel import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsViewModel import im.vector.app.features.roomprofile.permissions.RoomPermissionsViewModel +import im.vector.app.features.roomprofile.polls.RoomPollsViewModel import im.vector.app.features.roomprofile.settings.RoomSettingsViewModel import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedViewModel import im.vector.app.features.roomprofile.uploads.RoomUploadsViewModel @@ -697,4 +698,9 @@ interface MavericksViewModelModule { @IntoMap @MavericksViewModelKey(SetLinkViewModel::class) fun setLinkViewModelFactory(factory: SetLinkViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(RoomPollsViewModel::class) + fun roomPollsViewModelFactory(factory: RoomPollsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> } diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index 4e5116eda9..1e29dfff5e 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -41,6 +41,7 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.preference.PreferenceManager import androidx.viewbinding.ViewBinding import com.airbnb.mvrx.MavericksView @@ -91,6 +92,7 @@ import im.vector.app.features.themes.ActivityOtherThemes import im.vector.app.features.themes.ThemeUtils import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.GlobalError @@ -123,14 +125,20 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver protected val viewModelProvider get() = ViewModelProvider(this, viewModelFactory) - fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { - viewEvents - .stream() - .onEach { - hideWaitingView() - observer(it) - } - .launchIn(lifecycleScope) + fun VectorViewModel<*, *, T>.observeViewEvents( + observer: (T) -> Unit, + ) { + val tag = this@VectorBaseActivity::class.simpleName.toString() + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + viewEvents + .stream(tag) + .collect { + hideWaitingView() + observer(it) + } + } + } } var toolbar: ToolbarConfig? = null diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt index ec6f3288f8..a44fb1c9ac 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -26,8 +26,10 @@ import android.view.ViewGroup import android.widget.FrameLayout import androidx.annotation.CallSuper import androidx.annotation.FloatRange +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.viewbinding.ViewBinding import com.airbnb.mvrx.MavericksView import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -43,6 +45,7 @@ import im.vector.app.features.analytics.plan.MobileScreen import io.github.hyuwah.draggableviewlib.Utils import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import reactivecircus.flowbinding.android.view.clicks import timber.log.Timber @@ -199,12 +202,18 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShe * ViewEvents * ========================================================================================== */ - protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { - viewEvents - .stream() - .onEach { - observer(it) - } - .launchIn(viewLifecycleOwner.lifecycleScope) + protected fun VectorViewModel<*, *, T>.observeViewEvents( + observer: (T) -> Unit, + ) { + val tag = this@VectorBaseBottomSheetDialogFragment::class.simpleName.toString() + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + viewEvents + .stream(tag) + .collect { + observer(it) + } + } + } } } diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt index 5a817b989e..34e233aa7a 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt @@ -23,8 +23,10 @@ import android.view.View import android.view.ViewGroup import androidx.annotation.CallSuper import androidx.fragment.app.DialogFragment +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.viewbinding.ViewBinding import com.airbnb.mvrx.MavericksView import dagger.hilt.android.EntryPointAccessors @@ -37,6 +39,7 @@ import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.themes.ThemeUtils import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import reactivecircus.flowbinding.android.view.clicks import timber.log.Timber @@ -145,11 +148,15 @@ abstract class VectorBaseDialogFragment : DialogFragment(), Ma * ========================================================================================== */ protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { - viewEvents - .stream() - .onEach { - observer(it) - } - .launchIn(viewLifecycleOwner.lifecycleScope) + val tag = this@VectorBaseDialogFragment::class.simpleName.toString() + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + viewEvents + .stream(tag) + .collect { + observer(it) + } + } + } } } diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt index 520a0c50b7..0c41673dd4 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt @@ -34,6 +34,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.viewbinding.ViewBinding import com.airbnb.mvrx.MavericksView import com.bumptech.glide.util.Util.assertMainThread @@ -53,6 +54,7 @@ import im.vector.app.features.navigation.Navigator import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import reactivecircus.flowbinding.android.view.clicks import timber.log.Timber @@ -273,14 +275,20 @@ abstract class VectorBaseFragment : Fragment(), MavericksView * ViewEvents * ========================================================================================== */ - protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { - viewEvents - .stream() - .onEach { - dismissLoadingDialog() - observer(it) - } - .launchIn(viewLifecycleOwner.lifecycleScope) + protected fun VectorViewModel<*, *, T>.observeViewEvents( + observer: (T) -> Unit, + ) { + val tag = this@VectorBaseFragment::class.simpleName.toString() + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + viewEvents + .stream(tag) + .collect { + dismissLoadingDialog() + observer(it) + } + } + } } /* ========================================================================================== diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt index c9d58f9545..3dd38c455f 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt @@ -18,15 +18,16 @@ package im.vector.app.core.platform import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksViewModel -import im.vector.app.core.utils.DataSource -import im.vector.app.core.utils.PublishDataSource +import im.vector.app.core.utils.EventQueue +import im.vector.app.core.utils.SharedEvents abstract class VectorViewModel(initialState: S) : MavericksViewModel(initialState) { // Used to post transient events to the View - protected val _viewEvents = PublishDataSource() - val viewEvents: DataSource = _viewEvents + protected val _viewEvents = EventQueue(capacity = 64) + val viewEvents: SharedEvents + get() = _viewEvents abstract fun handle(action: VA) } diff --git a/vector/src/main/java/im/vector/app/core/resources/StringArrayProvider.kt b/vector/src/main/java/im/vector/app/core/resources/StringArrayProvider.kt index 9ed3c02ba4..3c057e0635 100644 --- a/vector/src/main/java/im/vector/app/core/resources/StringArrayProvider.kt +++ b/vector/src/main/java/im/vector/app/core/resources/StringArrayProvider.kt @@ -18,7 +18,6 @@ package im.vector.app.core.resources import android.content.res.Resources import androidx.annotation.ArrayRes -import androidx.annotation.NonNull import javax.inject.Inject class StringArrayProvider @Inject constructor(private val resources: Resources) { @@ -31,7 +30,6 @@ class StringArrayProvider @Inject constructor(private val resources: Resources) * @return The string array associated with the resource, stripped of styled * text information. */ - @NonNull fun getStringArray(@ArrayRes resId: Int): Array { return resources.getStringArray(resId) } diff --git a/vector/src/main/java/im/vector/app/core/resources/StringProvider.kt b/vector/src/main/java/im/vector/app/core/resources/StringProvider.kt index 8a78461f65..5a3b71bb55 100644 --- a/vector/src/main/java/im/vector/app/core/resources/StringProvider.kt +++ b/vector/src/main/java/im/vector/app/core/resources/StringProvider.kt @@ -18,7 +18,6 @@ package im.vector.app.core.resources import android.content.res.Resources import android.view.View -import androidx.annotation.NonNull import androidx.annotation.PluralsRes import androidx.annotation.StringRes import javax.inject.Inject @@ -33,7 +32,6 @@ class StringProvider @Inject constructor(private val resources: Resources) { * @return The string data associated with the resource, stripped of styled * text information. */ - @NonNull fun getString(@StringRes resId: Int): String { return resources.getString(resId) } @@ -49,12 +47,10 @@ class StringProvider @Inject constructor(private val resources: Resources) { * @return The string data associated with the resource, formatted and * stripped of styled text information. */ - @NonNull fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String { return resources.getString(resId, *formatArgs) } - @NonNull fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any?): String { return resources.getQuantityString(resId, quantity, *formatArgs) } diff --git a/vector/src/main/java/im/vector/app/core/utils/SharedEvent.kt b/vector/src/main/java/im/vector/app/core/utils/SharedEvent.kt new file mode 100644 index 0000000000..081a4f6192 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/SharedEvent.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.utils + +import im.vector.app.core.platform.VectorViewEvents +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.transform +import java.util.concurrent.CopyOnWriteArraySet + +interface SharedEvents { + fun stream(consumerId: String): Flow +} + +class EventQueue(capacity: Int) : SharedEvents { + + private val innerQueue = MutableSharedFlow>(replay = capacity) + + fun post(event: T) { + innerQueue.tryEmit(OneTimeEvent(event)) + } + + override fun stream(consumerId: String): Flow = innerQueue + .onEach { + // Ensure that buffered Events will not be sent again to new subscribers. + innerQueue.resetReplayCache() + } + .filterNotHandledBy(consumerId) +} + +/** + * Event designed to be delivered only once to a concrete entity, + * but it can also be delivered to multiple different entities. + * + * Keeps track of who has already handled its content. + */ +private class OneTimeEvent(private val content: T) { + + private val handlers = CopyOnWriteArraySet() + + /** + * @param asker Used to identify, whether this "asker" has already handled this Event. + * @return Event content or null if it has been already handled by asker + */ + fun getIfNotHandled(asker: String): T? = if (handlers.add(asker)) content else null +} + +private fun Flow>.filterNotHandledBy(consumerId: String): Flow = transform { event -> + event.getIfNotHandled(consumerId)?.let { emit(it) } +} diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index 8ce375122e..cffb1577cf 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -55,8 +55,6 @@ import im.vector.app.features.themes.ActivityOtherThemes import im.vector.app.features.ui.UiStateRepository import im.vector.lib.core.utils.compat.getParcelableExtraCompat import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize @@ -142,9 +140,9 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity startAppViewModel.onEach { renderState(it) } - startAppViewModel.viewEvents.stream() - .onEach(::handleViewEvents) - .launchIn(lifecycleScope) + startAppViewModel.observeViewEvents { + handleViewEvents(it) + } startAppViewModel.handle(StartAppAction.StartApp) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt index d393636a8e..aea87beea9 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt @@ -59,7 +59,7 @@ class SharedSecureStorageActivity : views.toolbar.visibility = View.GONE - viewModel.observeViewEvents { observeViewEvents(it) } + viewModel.observeViewEvents { onViewEvents(it) } viewModel.onEach { renderState(it) } } @@ -85,7 +85,7 @@ class SharedSecureStorageActivity : showFragment(fragment) } - private fun observeViewEvents(it: SharedSecureStorageViewEvent?) { + private fun onViewEvents(it: SharedSecureStorageViewEvent) { when (it) { is SharedSecureStorageViewEvent.Dismiss -> { finish() diff --git a/vector/src/main/java/im/vector/app/features/home/NotificationPermissionManager.kt b/vector/src/main/java/im/vector/app/features/home/NotificationPermissionManager.kt index 33ba1be02b..4f585becca 100644 --- a/vector/src/main/java/im/vector/app/features/home/NotificationPermissionManager.kt +++ b/vector/src/main/java/im/vector/app/features/home/NotificationPermissionManager.kt @@ -21,7 +21,6 @@ import android.app.Activity import android.content.pm.PackageManager import android.os.Build import androidx.activity.result.ActivityResultLauncher -import androidx.annotation.ChecksSdkIntAtLeast import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import im.vector.app.R @@ -35,17 +34,12 @@ class NotificationPermissionManager @Inject constructor( private val vectorPreferences: VectorPreferences, ) { - @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU) + @RequiresApi(Build.VERSION_CODES.TIRAMISU) fun isPermissionGranted(activity: Activity): Boolean { - return if (sdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) { - ContextCompat.checkSelfPermission( - activity, - Manifest.permission.POST_NOTIFICATIONS - ) == PackageManager.PERMISSION_GRANTED - } else { - // No notification permission management before API 33. - true - } + return ContextCompat.checkSelfPermission( + activity, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED } fun eventuallyRequestPermission( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index 580a3167b1..f48a493cc8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -241,6 +241,12 @@ internal class RichTextComposerLayout @JvmOverloads constructor( } } } + addRichTextMenuItem(R.drawable.ic_composer_bullet_list, R.string.rich_text_editor_bullet_list, ComposerAction.UNORDERED_LIST) { + views.richTextComposerEditText.toggleList(ordered = false) + } + addRichTextMenuItem(R.drawable.ic_composer_numbered_list, R.string.rich_text_editor_numbered_list, ComposerAction.ORDERED_LIST) { + views.richTextComposerEditText.toggleList(ordered = true) + } } fun setLink(link: String?) = diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt index aa8893041c..7bfe999289 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -104,7 +104,7 @@ abstract class MessageTextItem : AbsMessageItem() { if (useBigFont) { holder.messageView.textSize = 44F } else { - holder.messageView.textSize = 14F + holder.messageView.textSize = 15.5F } if (searchForPills) { message?.charSequence?.findPillsAndProcess(coroutineScope) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewActions.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewActions.kt new file mode 100644 index 0000000000..7dda460a5e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewActions.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.threads.list.viewmodel + +import im.vector.app.core.platform.VectorViewModelAction + +sealed interface ThreadListViewActions : VectorViewModelAction { + object TryAgain : ThreadListViewActions +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewEvents.kt new file mode 100644 index 0000000000..3e9af034f4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewEvents.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.threads.list.viewmodel + +import im.vector.app.core.platform.VectorViewEvents + +sealed interface ThreadListViewEvents : VectorViewEvents { + data class ShowError(val throwable: Throwable) : ThreadListViewEvents +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt index 7124727bb7..f31f19849c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt @@ -27,8 +27,6 @@ import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import im.vector.app.core.platform.EmptyAction -import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.extensions.toAnalyticsInteraction @@ -52,7 +50,7 @@ class ThreadListViewModel @AssistedInject constructor( @Assisted val initialState: ThreadListViewState, private val analyticsTracker: AnalyticsTracker, private val session: Session, -) : VectorViewModel(initialState) { +) : VectorViewModel(initialState) { private val room = session.getRoom(initialState.roomId) @@ -93,7 +91,17 @@ class ThreadListViewModel @AssistedInject constructor( fetchAndObserveThreads() } - override fun handle(action: EmptyAction) {} + override fun handle(action: ThreadListViewActions) { + when (action) { + ThreadListViewActions.TryAgain -> handleTryAgain() + } + } + + private fun handleTryAgain() { + viewModelScope.launch { + fetchNextPage() + } + } /** * Observing thread list with respect to homeserver capabilities. @@ -181,21 +189,23 @@ class ThreadListViewModel @AssistedInject constructor( true -> ThreadFilter.PARTICIPATED false -> ThreadFilter.ALL } - room?.threadsService()?.fetchThreadList( - nextBatchId = nextBatchId, - limit = defaultPagedListConfig.pageSize, - filter = filter, - ).let { result -> - when (result) { - is FetchThreadsResult.ReachedEnd -> { - hasReachedEnd = true - } - is FetchThreadsResult.ShouldFetchMore -> { - nextBatchId = result.nextBatch - } - else -> { + try { + room?.threadsService()?.fetchThreadList( + nextBatchId = nextBatchId, + limit = defaultPagedListConfig.pageSize, + filter = filter, + )?.let { result -> + when (result) { + is FetchThreadsResult.ReachedEnd -> { + hasReachedEnd = true + } + is FetchThreadsResult.ShouldFetchMore -> { + nextBatchId = result.nextBatch + } } } + } catch (throwable: Throwable) { + _viewEvents.post(ThreadListViewEvents.ShowError(throwable)) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt index 318c250906..1e67941856 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt @@ -26,6 +26,7 @@ import androidx.core.view.isVisible import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.cleanup @@ -41,10 +42,14 @@ import im.vector.app.features.home.room.threads.arguments.ThreadListArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListController import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListPagedController +import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewActions +import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewEvents import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState import im.vector.app.features.rageshake.BugReporter import im.vector.app.features.rageshake.ReportType +import org.matrix.android.sdk.api.failure.is400 +import org.matrix.android.sdk.api.failure.is404 import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.MatrixItem @@ -126,11 +131,45 @@ class ThreadListFragment : views.threadListRecyclerView.configureWith(legacyThreadListController, TimelineItemAnimator(), hasFixedSize = false) legacyThreadListController.listener = this } + observeViewEvents() + } + + private fun observeViewEvents() { + threadListViewModel.observeViewEvents { + when (it) { + is ThreadListViewEvents.ShowError -> handleShowError(it) + } + } + } + + private fun handleShowError(event: ThreadListViewEvents.ShowError) { + val error = event.throwable + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .also { + if (error.is400() || error.is404()) { + // Outdated Homeserver + it.setMessage(R.string.thread_list_not_available) + it.setPositiveButton(R.string.ok) { _, _ -> + requireActivity().finish() + } + } else { + // Other error, can retry + // (Can happen on first request or on pagination request) + it.setMessage(errorFormatter.toHumanReadable(error)) + it.setPositiveButton(R.string.ok, null) + it.setNegativeButton(R.string.global_retry) { _, _ -> + threadListViewModel.handle(ThreadListViewActions.TryAgain) + } + } + } + .show() } override fun onDestroyView() { views.threadListRecyclerView.cleanup() threadListController.listener = null + legacyThreadListController.listener = null super.onDestroyView() } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt index b46f22c58f..8d520628f0 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt @@ -685,7 +685,7 @@ class LoginViewModel @AssistedInject constructor( currentJob = viewModelScope.launch { try { safeLoginWizard.login( - action.username, + action.username.trim(), action.password, action.initialDeviceName ) diff --git a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt index 089fdcebd4..0d240b376b 100644 --- a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt @@ -29,7 +29,9 @@ import androidx.core.transition.addListener import androidx.core.view.ViewCompat import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.transition.Transition import com.airbnb.mvrx.viewModel import dagger.hilt.android.AndroidEntryPoint @@ -50,8 +52,6 @@ import im.vector.lib.attachmentviewer.AttachmentViewerActivity import im.vector.lib.core.utils.compat.getParcelableArrayListExtraCompat import im.vector.lib.core.utils.compat.getParcelableExtraCompat import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize @@ -239,10 +239,15 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), AttachmentInt } private fun observeViewEvents() { - viewModel.viewEvents - .stream() - .onEach(::handleViewEvents) - .launchIn(lifecycleScope) + val tag = this::class.simpleName.toString() + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + viewModel + .viewEvents + .stream(tag) + .collect(::handleViewEvents) + } + } } private fun handleViewEvents(event: VectorAttachmentViewerViewEvents) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt index 526d676dee..3c37c92650 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt @@ -36,6 +36,7 @@ import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment import im.vector.app.features.roomprofile.members.RoomMemberListFragment import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsFragment import im.vector.app.features.roomprofile.permissions.RoomPermissionsFragment +import im.vector.app.features.roomprofile.polls.RoomPollsFragment import im.vector.app.features.roomprofile.settings.RoomSettingsFragment import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment import im.vector.lib.core.utils.compat.getParcelableCompat @@ -98,6 +99,7 @@ class RoomProfileActivity : RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings() RoomProfileSharedAction.OpenRoomAliasesSettings -> openRoomAlias() RoomProfileSharedAction.OpenRoomPermissionsSettings -> openRoomPermissions() + RoomProfileSharedAction.OpenRoomPolls -> openRoomPolls() RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads() RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers() RoomProfileSharedAction.OpenRoomNotificationSettings -> openRoomNotificationSettings() @@ -126,6 +128,10 @@ class RoomProfileActivity : finish() } + private fun openRoomPolls() { + addFragmentToBackstack(views.simpleFragmentContainer, RoomPollsFragment::class.java, roomProfileArgs) + } + private fun openRoomUploads() { addFragmentToBackstack(views.simpleFragmentContainer, RoomUploadsFragment::class.java, roomProfileArgs) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt index 0abfc6c896..f873dfbdde 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt @@ -18,6 +18,7 @@ package im.vector.app.features.roomprofile import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.epoxy.expandableTextItem import im.vector.app.core.epoxy.profiles.buildProfileAction @@ -56,6 +57,7 @@ class RoomProfileController @Inject constructor( fun onMemberListClicked() fun onBannedMemberListClicked() fun onNotificationsClicked() + fun onPollHistoryClicked() fun onUploadsClicked() fun createShortcut() fun onSettingsClicked() @@ -267,6 +269,15 @@ class RoomProfileController @Inject constructor( action = { callback?.onBannedMemberListClicked() } ) } + if (BuildConfig.DEBUG) { + // WIP, will be in release when related screens will be finished + buildProfileAction( + id = "poll_history", + title = stringProvider.getString(R.string.room_profile_section_more_polls), + icon = R.drawable.ic_attachment_poll, + action = { callback?.onPollHistoryClicked() } + ) + } buildProfileAction( id = "uploads", title = stringProvider.getString(R.string.room_profile_section_more_uploads), diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index f4394111ab..51885dbf39 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -269,6 +269,10 @@ class RoomProfileFragment : roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomNotificationSettings) } + override fun onPollHistoryClicked() { + roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomPolls) + } + override fun onUploadsClicked() { roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomUploads) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt index 7d62bb86a1..b243ceb206 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt @@ -25,6 +25,7 @@ sealed class RoomProfileSharedAction : VectorSharedAction { object OpenRoomSettings : RoomProfileSharedAction() object OpenRoomAliasesSettings : RoomProfileSharedAction() object OpenRoomPermissionsSettings : RoomProfileSharedAction() + object OpenRoomPolls : RoomProfileSharedAction() object OpenRoomUploads : RoomProfileSharedAction() object OpenRoomMembers : RoomProfileSharedAction() object OpenBannedRoomMembers : RoomProfileSharedAction() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/GetPollsUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/GetPollsUseCase.kt new file mode 100644 index 0000000000..6f2a757ed7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/GetPollsUseCase.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls + +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class GetPollsUseCase @Inject constructor() { + + fun execute(): Flow> { + // TODO unmock and add unit tests + return flowOf(getActivePolls() + getEndedPolls()) + .map { it.sortedByDescending { poll -> poll.creationTimestamp } } + } + + private fun getActivePolls(): List { + return listOf( + PollSummary.ActivePoll( + id = "id1", + // 2022/06/28 UTC+1 + creationTimestamp = 1656367200000, + title = "Which charity would you like to support?" + ), + PollSummary.ActivePoll( + id = "id2", + // 2022/06/26 UTC+1 + creationTimestamp = 1656194400000, + title = "Which sport should the pupils do this year?" + ), + PollSummary.ActivePoll( + id = "id3", + // 2022/06/24 UTC+1 + creationTimestamp = 1656021600000, + title = "What type of food should we have at the party?" + ), + PollSummary.ActivePoll( + id = "id4", + // 2022/06/22 UTC+1 + creationTimestamp = 1655848800000, + title = "What film should we show at the end of the year party?" + ), + ) + } + + private fun getEndedPolls(): List { + return listOf( + PollSummary.EndedPoll( + id = "id1-ended", + // 2022/06/28 UTC+1 + creationTimestamp = 1656367200000, + title = "Which charity would you like to support?", + totalVotes = 22, + winnerOptions = listOf( + PollOptionViewState.PollEnded( + optionId = "id1", + optionAnswer = "Cancer research", + voteCount = 13, + votePercentage = 13 / 22.0, + isWinner = true, + ) + ), + ), + PollSummary.EndedPoll( + id = "id2-ended", + // 2022/06/26 UTC+1 + creationTimestamp = 1656194400000, + title = "Where should we do the offsite?", + totalVotes = 92, + winnerOptions = listOf( + PollOptionViewState.PollEnded( + optionId = "id1", + optionAnswer = "Hawaii", + voteCount = 43, + votePercentage = 43 / 92.0, + isWinner = true, + ) + ), + ), + PollSummary.EndedPoll( + id = "id3-ended", + // 2022/06/24 UTC+1 + creationTimestamp = 1656021600000, + title = "What type of food should we have at the party?", + totalVotes = 22, + winnerOptions = listOf( + PollOptionViewState.PollEnded( + optionId = "id1", + optionAnswer = "Brazilian", + voteCount = 13, + votePercentage = 13 / 22.0, + isWinner = true, + ) + ), + ), + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/PollSummary.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/PollSummary.kt new file mode 100644 index 0000000000..f24ac8b8a6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/PollSummary.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls + +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState + +sealed interface PollSummary { + val id: String + val creationTimestamp: Long + val title: String + + data class ActivePoll( + override val id: String, + override val creationTimestamp: Long, + override val title: String, + ) : PollSummary + + data class EndedPoll( + override val id: String, + override val creationTimestamp: Long, + override val title: String, + val totalVotes: Int, + val winnerOptions: List, + ) : PollSummary +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsAction.kt new file mode 100644 index 0000000000..c18142a306 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsAction.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls + +import im.vector.app.core.platform.VectorViewModelAction + +sealed interface RoomPollsAction : VectorViewModelAction diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsFragment.kt new file mode 100644 index 0000000000..9f7e704135 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsFragment.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel +import com.google.android.material.tabs.TabLayoutMediator +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentRoomPollsBinding +import im.vector.app.features.roomprofile.RoomProfileArgs + +@AndroidEntryPoint +class RoomPollsFragment : VectorBaseFragment() { + + private val roomProfileArgs: RoomProfileArgs by args() + + private val viewModel: RoomPollsViewModel by fragmentViewModel() + + private var tabLayoutMediator: TabLayoutMediator? = null + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomPollsBinding { + return FragmentRoomPollsBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupToolbar() + setupTabs() + } + + override fun onDestroyView() { + views.roomPollsViewPager.adapter = null + tabLayoutMediator?.detach() + tabLayoutMediator = null + super.onDestroyView() + } + + private fun setupToolbar() { + setupToolbar(views.roomPollsToolbar) + .allowBack() + } + + private fun setupTabs() { + views.roomPollsViewPager.adapter = RoomPollsPagerAdapter(this) + + tabLayoutMediator = TabLayoutMediator(views.roomPollsTabs, views.roomPollsViewPager) { tab, position -> + when (position) { + RoomPollsType.ACTIVE.ordinal -> tab.text = getString(R.string.room_polls_active) + RoomPollsType.ENDED.ordinal -> tab.text = getString(R.string.room_polls_ended) + } + }.also { it.attach() } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsPagerAdapter.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsPagerAdapter.kt new file mode 100644 index 0000000000..c60fc5de27 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsPagerAdapter.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls + +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter +import im.vector.app.features.roomprofile.polls.active.RoomActivePollsFragment +import im.vector.app.features.roomprofile.polls.ended.RoomEndedPollsFragment + +class RoomPollsPagerAdapter( + private val fragment: Fragment +) : FragmentStateAdapter(fragment) { + + override fun getItemCount() = RoomPollsType.values().size + + override fun createFragment(position: Int): Fragment { + return when (position) { + RoomPollsType.ACTIVE.ordinal -> instantiateFragment(RoomActivePollsFragment::class.java.name) + RoomPollsType.ENDED.ordinal -> instantiateFragment(RoomEndedPollsFragment::class.java.name) + else -> throw IllegalArgumentException("position should be between 0 and ${itemCount - 1}, while it was $position") + } + } + + private fun instantiateFragment(fragmentName: String): Fragment { + return fragment.childFragmentManager.fragmentFactory.instantiate(fragment.requireContext().classLoader, fragmentName) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsType.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsType.kt new file mode 100644 index 0000000000..134ef9a195 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsType.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls + +enum class RoomPollsType { + ACTIVE, + ENDED, +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewEvent.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewEvent.kt new file mode 100644 index 0000000000..231123563a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewEvent.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls + +import im.vector.app.core.platform.VectorViewEvents + +sealed class RoomPollsViewEvent : VectorViewEvents diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt new file mode 100644 index 0000000000..95cb4717ca --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls + +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.platform.VectorViewModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +class RoomPollsViewModel @AssistedInject constructor( + @Assisted initialState: RoomPollsViewState, + private val getPollsUseCase: GetPollsUseCase, +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: RoomPollsViewState): RoomPollsViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + init { + observePolls() + } + + private fun observePolls() { + getPollsUseCase.execute() + .onEach { setState { copy(polls = it) } } + .launchIn(viewModelScope) + } + + override fun handle(action: RoomPollsAction) { + // do nothing for now + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt new file mode 100644 index 0000000000..74794c99b1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls + +import com.airbnb.mvrx.MavericksState +import im.vector.app.features.roomprofile.RoomProfileArgs + +data class RoomPollsViewState( + val roomId: String, + val polls: List = emptyList(), +) : MavericksState { + + constructor(roomProfileArgs: RoomProfileArgs) : this(roomId = roomProfileArgs.roomId) +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsFragment.kt new file mode 100644 index 0000000000..1c6a03c480 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsFragment.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.active + +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.features.roomprofile.polls.RoomPollsType +import im.vector.app.features.roomprofile.polls.list.RoomPollsListFragment + +@AndroidEntryPoint +class RoomActivePollsFragment : RoomPollsListFragment() { + + override fun getEmptyListTitle(): String { + return getString(R.string.room_polls_active_no_item) + } + + override fun getRoomPollsType(): RoomPollsType { + return RoomPollsType.ACTIVE + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/ended/RoomEndedPollsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/ended/RoomEndedPollsFragment.kt new file mode 100644 index 0000000000..8dd0cadadf --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/ended/RoomEndedPollsFragment.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.ended + +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.features.roomprofile.polls.RoomPollsType +import im.vector.app.features.roomprofile.polls.list.RoomPollsListFragment + +@AndroidEntryPoint +class RoomEndedPollsFragment : RoomPollsListFragment() { + + override fun getEmptyListTitle(): String { + return getString(R.string.room_polls_ended_no_item) + } + + override fun getRoomPollsType(): RoomPollsType { + return RoomPollsType.ENDED + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollItem.kt new file mode 100644 index 0000000000..da00fedddb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollItem.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.list + +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.epoxy.onClick +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.features.home.room.detail.timeline.item.PollOptionView +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState + +@EpoxyModelClass +abstract class RoomPollItem : VectorEpoxyModel(R.layout.item_poll) { + + @EpoxyAttribute + lateinit var formattedDate: String + + @EpoxyAttribute + lateinit var title: String + + @EpoxyAttribute + var winnerOptions: List = emptyList() + + @EpoxyAttribute + var totalVotesStatus: String? = null + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var clickListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.view.onClick(clickListener) + holder.date.text = formattedDate + holder.title.text = title + holder.winnerOptions.removeAllViews() + holder.winnerOptions.isVisible = winnerOptions.isNotEmpty() + for (winnerOption in winnerOptions) { + val optionView = PollOptionView(holder.view.context) + holder.winnerOptions.addView(optionView) + optionView.render(winnerOption) + } + holder.totalVotes.setTextOrHide(totalVotesStatus) + } + + class Holder : VectorEpoxyHolder() { + val date by bind(R.id.pollDate) + val title by bind(R.id.pollTitle) + val winnerOptions by bind(R.id.pollWinnerOptionsContainer) + val totalVotes by bind(R.id.pollTotalVotes) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsController.kt new file mode 100644 index 0000000000..f0e3b6b9a4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsController.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.list + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.R +import im.vector.app.core.date.DateFormatKind +import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.roomprofile.polls.PollSummary +import javax.inject.Inject + +class RoomPollsController @Inject constructor( + val dateFormatter: VectorDateFormatter, + val stringProvider: StringProvider, +) : TypedEpoxyController>() { + + interface Listener { + fun onPollClicked(pollId: String) + } + + var listener: Listener? = null + + override fun buildModels(data: List?) { + if (data.isNullOrEmpty()) { + return + } + + for (poll in data) { + when (poll) { + is PollSummary.ActivePoll -> buildActivePollItem(poll) + is PollSummary.EndedPoll -> buildEndedPollItem(poll) + } + } + } + + private fun buildActivePollItem(poll: PollSummary.ActivePoll) { + val host = this + roomPollItem { + id(poll.id) + formattedDate(host.dateFormatter.format(poll.creationTimestamp, DateFormatKind.TIMELINE_DAY_DIVIDER)) + title(poll.title) + clickListener { + host.listener?.onPollClicked(poll.id) + } + } + } + + private fun buildEndedPollItem(poll: PollSummary.EndedPoll) { + val host = this + roomPollItem { + id(poll.id) + formattedDate(host.dateFormatter.format(poll.creationTimestamp, DateFormatKind.TIMELINE_DAY_DIVIDER)) + title(poll.title) + winnerOptions(poll.winnerOptions) + totalVotesStatus(host.stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, poll.totalVotes, poll.totalVotes)) + clickListener { + host.listener?.onPollClicked(poll.id) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsListFragment.kt new file mode 100644 index 0000000000..0d97bd8dcb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsListFragment.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.polls.list + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentRoomPollsListBinding +import im.vector.app.features.roomprofile.polls.PollSummary +import im.vector.app.features.roomprofile.polls.RoomPollsType +import im.vector.app.features.roomprofile.polls.RoomPollsViewModel +import timber.log.Timber +import javax.inject.Inject + +abstract class RoomPollsListFragment : + VectorBaseFragment(), + RoomPollsController.Listener { + + @Inject + lateinit var roomPollsController: RoomPollsController + + private val viewModel: RoomPollsViewModel by parentFragmentViewModel(RoomPollsViewModel::class) + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomPollsListBinding { + return FragmentRoomPollsListBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupList() + } + + abstract fun getEmptyListTitle(): String + + abstract fun getRoomPollsType(): RoomPollsType + + private fun setupList() { + roomPollsController.listener = this + views.roomPollsList.configureWith(roomPollsController) + views.roomPollsEmptyTitle.text = getEmptyListTitle() + } + + override fun onDestroyView() { + cleanUpList() + super.onDestroyView() + } + + private fun cleanUpList() { + views.roomPollsList.cleanup() + roomPollsController.listener = null + } + + override fun invalidate() = withState(viewModel) { viewState -> + when (getRoomPollsType()) { + RoomPollsType.ACTIVE -> renderList(viewState.polls.filterIsInstance(PollSummary.ActivePoll::class.java)) + RoomPollsType.ENDED -> renderList(viewState.polls.filterIsInstance(PollSummary.EndedPoll::class.java)) + } + } + + private fun renderList(polls: List) { + roomPollsController.setData(polls) + views.roomPollsEmptyTitle.isVisible = polls.isEmpty() + } + + override fun onPollClicked(pollId: String) { + // TODO navigate to details + Timber.d("poll with id $pollId clicked") + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt index 7a156ca039..d0cb5096af 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt @@ -26,6 +26,7 @@ import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorPreferenceCategory import im.vector.app.core.preference.VectorSwitchPreference +import im.vector.app.core.utils.copyToClipboard import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.home.NightlyProxy import im.vector.app.features.rageshake.RageShake @@ -67,6 +68,14 @@ class VectorSettingsAdvancedSettingsFragment : override fun bindPref() { setupRageShakeSection() setupNightlySection() + setupDevToolsSection() + } + + private fun setupDevToolsSection() { + findPreference("SETTINGS_ACCESS_TOKEN")?.setOnPreferenceClickListener { + copyToClipboard(requireActivity(), session.sessionParams.credentials.accessToken) + true + } } private fun setupRageShakeSection() { diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt index 8f991ecd79..f003baac46 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt @@ -20,7 +20,9 @@ import android.content.Context import android.os.Bundle import android.view.View import androidx.annotation.CallSuper +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.preference.PreferenceFragmentCompat import com.airbnb.mvrx.MavericksView import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -36,6 +38,7 @@ import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.plan.MobileScreen import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import reactivecircus.flowbinding.android.view.clicks import timber.log.Timber @@ -67,13 +70,19 @@ abstract class VectorSettingsBaseFragment : ScPreferenceFragment(), MavericksVie * ViewEvents * ========================================================================================== */ - protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { - viewEvents - .stream() - .onEach { - observer(it) - } - .launchIn(viewLifecycleOwner.lifecycleScope) + protected fun VectorViewModel<*, *, T>.observeViewEvents( + observer: (T) -> Unit, + ) { + val tag = this@VectorSettingsBaseFragment::class.simpleName.toString() + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + viewEvents + .stream(tag) + .collect { + observer(it) + } + } + } } /* ========================================================================================== diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt index e779948b41..cbc02ba0f0 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt @@ -142,7 +142,7 @@ class DevicesViewModel @AssistedInject constructor( .map { deviceInfo -> val cryptoDeviceInfo = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId } val trustLevelForShield = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo) - val isInactive = checkIfSessionIsInactiveUseCase.execute(deviceInfo.lastSeenTs ?: 0) + val isInactive = checkIfSessionIsInactiveUseCase.execute(deviceInfo.lastSeenTs) DeviceFullInfo(deviceInfo, cryptoDeviceInfo, trustLevelForShield, isInactive) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index 232fcd50f7..f2e6b25f32 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -18,7 +18,6 @@ package im.vector.app.features.settings.devices.v2 import android.content.SharedPreferences import com.airbnb.mvrx.MavericksViewModelFactory -import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -32,10 +31,10 @@ import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthN import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import timber.log.Timber @@ -103,27 +102,27 @@ class DevicesViewModel @AssistedInject constructor( } private fun observeDevices() { - getDeviceFullInfoListUseCase.execute( + val allSessionsFlow = getDeviceFullInfoListUseCase.execute( filterType = DeviceManagerFilterType.ALL_SESSIONS, - excludeCurrentDevice = false + excludeCurrentDevice = false, + ) + val unverifiedSessionsFlow = getDeviceFullInfoListUseCase.execute( + filterType = DeviceManagerFilterType.UNVERIFIED, + excludeCurrentDevice = true, + ) + val inactiveSessionsFlow = getDeviceFullInfoListUseCase.execute( + filterType = DeviceManagerFilterType.INACTIVE, + excludeCurrentDevice = true, ) - .execute { async -> - if (async is Success) { - val deviceFullInfoList = async.invoke() - val unverifiedSessionsCount = deviceFullInfoList.count { !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse() } - val inactiveSessionsCount = deviceFullInfoList.count { it.isInactive } - copy( - devices = async, - unverifiedSessionsCount = unverifiedSessionsCount, - inactiveSessionsCount = inactiveSessionsCount, - ) - } else { - copy( - devices = async - ) - } - } + combine(allSessionsFlow, unverifiedSessionsFlow, inactiveSessionsFlow) { allSessions, unverifiedSessions, inactiveSessions -> + DeviceFullInfoList( + allSessions = allSessions, + unverifiedSessionsCount = unverifiedSessions.size, + inactiveSessionsCount = inactiveSessions.size, + ) + } + .execute { async -> copy(devices = async) } } private fun refreshDevicesOnCryptoDevicesChange() { @@ -185,6 +184,7 @@ class DevicesViewModel @AssistedInject constructor( private fun getDeviceIdsOfOtherSessions(state: DevicesViewState): List { val currentDeviceId = state.currentSessionCrossSigningInfo.deviceId return state.devices() + ?.allSessions ?.mapNotNull { fullInfo -> fullInfo.deviceInfo.deviceId.takeUnless { it == currentDeviceId } } .orEmpty() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewState.kt index e0531c34dc..75d0f132bb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewState.kt @@ -23,9 +23,13 @@ import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCro data class DevicesViewState( val currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo = CurrentSessionCrossSigningInfo(), - val devices: Async> = Uninitialized, - val unverifiedSessionsCount: Int = 0, - val inactiveSessionsCount: Int = 0, + val devices: Async = Uninitialized, val isLoading: Boolean = false, val isShowingIpAddress: Boolean = false, ) : MavericksState + +data class DeviceFullInfoList( + val allSessions: List, + val unverifiedSessionsCount: Int, + val inactiveSessionsCount: Int, +) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt index 6adb33d5ab..1ae7d8836f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt @@ -75,7 +75,7 @@ class GetDeviceFullInfoListUseCase @Inject constructor( .map { deviceInfo -> val cryptoDeviceInfo = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId } val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo) - val isInactive = checkIfSessionIsInactiveUseCase.execute(deviceInfo.lastSeenTs ?: 0) + val isInactive = checkIfSessionIsInactiveUseCase.execute(deviceInfo.lastSeenTs) val isCurrentDevice = currentSessionCrossSigningInfo.deviceId == cryptoDeviceInfo?.deviceId val deviceExtendedInfo = parseDeviceUserAgentUseCase.execute(deviceInfo.getBestLastSeenUserAgent()) val matrixClientInfo = deviceInfo.deviceId diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index 15375ef679..f83b00a75e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -55,7 +55,6 @@ import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDia import im.vector.app.features.workers.signout.SignOutUiWorker import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject /** @@ -282,13 +281,15 @@ class VectorSettingsDevicesFragment : override fun invalidate() = withState(viewModel) { state -> if (state.devices is Success) { - val devices = state.devices() + val deviceFullInfoList = state.devices() + val devices = deviceFullInfoList?.allSessions val currentDeviceId = state.currentSessionCrossSigningInfo.deviceId val currentDeviceInfo = devices?.firstOrNull { it.deviceInfo.deviceId == currentDeviceId } - val isCurrentSessionVerified = currentDeviceInfo?.roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Trusted val otherDevices = devices?.filter { it.deviceInfo.deviceId != currentDeviceId } + val inactiveSessionsCount = deviceFullInfoList?.inactiveSessionsCount ?: 0 + val unverifiedSessionsCount = deviceFullInfoList?.unverifiedSessionsCount ?: 0 - renderSecurityRecommendations(state.inactiveSessionsCount, state.unverifiedSessionsCount, isCurrentSessionVerified) + renderSecurityRecommendations(inactiveSessionsCount, unverifiedSessionsCount) renderCurrentSessionView(currentDeviceInfo, hasOtherDevices = otherDevices?.isNotEmpty().orFalse()) renderOtherSessionsView(otherDevices, state.isShowingIpAddress) } else { @@ -303,9 +304,8 @@ class VectorSettingsDevicesFragment : private fun renderSecurityRecommendations( inactiveSessionsCount: Int, unverifiedSessionsCount: Int, - isCurrentSessionVerified: Boolean, ) { - val isUnverifiedSectionVisible = unverifiedSessionsCount > 0 && isCurrentSessionVerified + val isUnverifiedSectionVisible = unverifiedSessionsCount > 0 val isInactiveSectionVisible = inactiveSessionsCount > 0 if (isUnverifiedSectionVisible.not() && isInactiveSectionVisible.not()) { hideSecurityRecommendations() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCase.kt index 8f23fd06cc..b1f23eb510 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCase.kt @@ -37,7 +37,9 @@ class FilterDevicesUseCase @Inject constructor() { // when current session is not verified, other session status cannot be trusted DeviceManagerFilterType.VERIFIED -> isCurrentSessionVerified && it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse() // when current session is not verified, other session status cannot be trusted - DeviceManagerFilterType.UNVERIFIED -> isCurrentSessionVerified && !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse() + DeviceManagerFilterType.UNVERIFIED -> + (isCurrentSessionVerified && !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse()) || + it.cryptoDeviceInfo == null DeviceManagerFilterType.INACTIVE -> it.isInactive } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCase.kt index 8991ad1e3d..f3670793bd 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCase.kt @@ -24,11 +24,13 @@ class CheckIfSessionIsInactiveUseCase @Inject constructor( private val clock: Clock, ) { - fun execute(lastSeenTs: Long): Boolean { - // In case of the server doesn't send the last seen date. - if (lastSeenTs == 0L) return true - - val diffMilliseconds = clock.epochMillis() - lastSeenTs - return diffMilliseconds >= TimeUnit.DAYS.toMillis(SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS.toLong()) + fun execute(lastSeenTsMillis: Long?): Boolean { + return if (lastSeenTsMillis == null || lastSeenTsMillis <= 0) { + // in these situations we cannot say anything about the inactivity of the session + false + } else { + val diffMilliseconds = clock.epochMillis() - lastSeenTsMillis + diffMilliseconds >= TimeUnit.DAYS.toMillis(SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS.toLong()) + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt index 5e2549f42a..e85e394681 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt @@ -63,12 +63,13 @@ class OtherSessionsController @Inject constructor( } val drawableColor = host.colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) val descriptionDrawable = if (device.isInactive) host.drawableProvider.getDrawable(R.drawable.ic_inactive_sessions, drawableColor) else null + val sessionName = device.deviceInfo.displayName ?: device.deviceInfo.deviceId otherSessionItem { id(device.deviceInfo.deviceId) deviceType(device.deviceExtendedInfo.deviceType) roomEncryptionTrustLevel(device.roomEncryptionTrustLevel) - sessionName(device.deviceInfo.displayName) + sessionName(sessionName) sessionDescription(description) sessionDescriptionDrawable(descriptionDrawable) sessionDescriptionColor(descriptionColor) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt index 5d2daf2941..81a8aae666 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt @@ -62,9 +62,10 @@ class SessionInfoView @JvmOverloads constructor( stringProvider: StringProvider, ) { renderDeviceInfo( - sessionInfoViewState.deviceFullInfo.deviceInfo.displayName.orEmpty(), - sessionInfoViewState.deviceFullInfo.deviceExtendedInfo.deviceType, - stringProvider, + sessionName = sessionInfoViewState.deviceFullInfo.deviceInfo.displayName + ?: sessionInfoViewState.deviceFullInfo.deviceInfo.deviceId.orEmpty(), + deviceType = sessionInfoViewState.deviceFullInfo.deviceExtendedInfo.deviceType, + stringProvider = stringProvider, ) renderVerificationStatus( sessionInfoViewState.deviceFullInfo.roomEncryptionTrustLevel, diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCase.kt index 140b55a30c..445d2309a4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCase.kt @@ -49,10 +49,10 @@ class GetDeviceFullInfoUseCase @Inject constructor( ) { currentSessionCrossSigningInfo, deviceInfo, cryptoDeviceInfo -> val info = deviceInfo.getOrNull() val cryptoInfo = cryptoDeviceInfo.getOrNull() - val fullInfo = if (info != null && cryptoInfo != null) { + val fullInfo = if (info != null) { val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoInfo) - val isInactive = checkIfSessionIsInactiveUseCase.execute(info.lastSeenTs ?: 0) - val isCurrentDevice = currentSessionCrossSigningInfo.deviceId == cryptoInfo.deviceId + val isInactive = checkIfSessionIsInactiveUseCase.execute(info.lastSeenTs) + val isCurrentDevice = currentSessionCrossSigningInfo.deviceId == info.deviceId val deviceUserAgent = parseDeviceUserAgentUseCase.execute(info.getBestLastSeenUserAgent()) val matrixClientInfo = info.deviceId ?.takeIf { it.isNotEmpty() } diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestSystemSettings.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestSystemSettings.kt index 0f362d5019..589edecad9 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestSystemSettings.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestSystemSettings.kt @@ -15,12 +15,14 @@ */ package im.vector.app.features.settings.troubleshoot +import android.os.Build import androidx.core.app.NotificationManagerCompat import androidx.fragment.app.FragmentActivity import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.startNotificationSettingsIntent import im.vector.app.features.home.NotificationPermissionManager +import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider import javax.inject.Inject /** @@ -30,6 +32,7 @@ import javax.inject.Inject class TestSystemSettings @Inject constructor( private val context: FragmentActivity, private val stringProvider: StringProvider, + private val sdkIntProvider: BuildVersionSdkIntProvider, private val notificationPermissionManager: NotificationPermissionManager, ) : TroubleshootTest(R.string.settings_troubleshoot_test_system_settings_title) { @@ -39,14 +42,7 @@ class TestSystemSettings @Inject constructor( quickFix = null status = TestStatus.SUCCESS } else { - if (notificationPermissionManager.isPermissionGranted(context)) { - description = stringProvider.getString(R.string.settings_troubleshoot_test_system_settings_failed) - quickFix = object : TroubleshootQuickFix(R.string.open_settings) { - override fun doFix() { - startNotificationSettingsIntent(context, testParameters.activityResultLauncher) - } - } - } else { + if (sdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU) && notificationPermissionManager.isPermissionGranted(context).not()) { // In this case, we can ask for user permission description = stringProvider.getString(R.string.settings_troubleshoot_test_system_settings_permission_failed) quickFix = object : TroubleshootQuickFix(R.string.grant_permission) { @@ -54,6 +50,13 @@ class TestSystemSettings @Inject constructor( notificationPermissionManager.askPermission(testParameters.permissionResultLauncher) } } + } else { + description = stringProvider.getString(R.string.settings_troubleshoot_test_system_settings_failed) + quickFix = object : TroubleshootQuickFix(R.string.open_settings) { + override fun doFix() { + startNotificationSettingsIntent(context, testParameters.activityResultLauncher) + } + } } status = TestStatus.FAILED } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index d56f4ad715..9cb894bb58 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -419,7 +419,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor( // Next media player is already attached to this player and will start playing automatically if (nextMediaPlayer != null) return - val hasEnded = !isLiveListening && mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence + val currentSequence = playlist.currentSequence ?: 0 + val lastChunkSequence = mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence ?: 0 + val hasEnded = !isLiveListening && currentSequence >= lastChunkSequence if (hasEnded) { // We'll not receive new chunks anymore so we can stop the live listening stop() diff --git a/vector/src/main/res/drawable/ic_composer_bullet_list.xml b/vector/src/main/res/drawable/ic_composer_bullet_list.xml new file mode 100644 index 0000000000..f6febc88f0 --- /dev/null +++ b/vector/src/main/res/drawable/ic_composer_bullet_list.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_composer_numbered_list.xml b/vector/src/main/res/drawable/ic_composer_numbered_list.xml new file mode 100644 index 0000000000..d6a860c4c8 --- /dev/null +++ b/vector/src/main/res/drawable/ic_composer_numbered_list.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/vector/src/main/res/layout/composer_rich_text_layout.xml b/vector/src/main/res/layout/composer_rich_text_layout.xml index 3484616c72..7cc2d48cda 100644 --- a/vector/src/main/res/layout/composer_rich_text_layout.xml +++ b/vector/src/main/res/layout/composer_rich_text_layout.xml @@ -180,6 +180,8 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:minHeight="52dp" + android:requiresFadingEdge="horizontal" + android:fadingEdgeLength="28dp" app:layout_constraintTop_toBottomOf="@id/composerEditTextOuterBorder" app:layout_constraintStart_toEndOf="@id/attachmentButton" app:layout_constraintEnd_toStartOf="@id/sendButton" diff --git a/vector/src/main/res/layout/fragment_room_polls.xml b/vector/src/main/res/layout/fragment_room_polls.xml new file mode 100644 index 0000000000..396d6fd8c5 --- /dev/null +++ b/vector/src/main/res/layout/fragment_room_polls.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_room_polls_list.xml b/vector/src/main/res/layout/fragment_room_polls_list.xml new file mode 100644 index 0000000000..8eb27e5e00 --- /dev/null +++ b/vector/src/main/res/layout/fragment_room_polls_list.xml @@ -0,0 +1,42 @@ + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_poll.xml b/vector/src/main/res/layout/item_poll.xml new file mode 100644 index 0000000000..17f3b5abf5 --- /dev/null +++ b/vector/src/main/res/layout/item_poll.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + diff --git a/vector/src/main/res/xml/vector_settings_advanced_settings.xml b/vector/src/main/res/xml/vector_settings_advanced_settings.xml index 8df55abf02..d5c60eb676 100644 --- a/vector/src/main/res/xml/vector_settings_advanced_settings.xml +++ b/vector/src/main/res/xml/vector_settings_advanced_settings.xml @@ -98,6 +98,11 @@ android:title="@string/settings_key_requests" app:fragment="im.vector.app.features.settings.devtools.KeyRequestsFragment" /> + () + private val initialState = RoomPollsViewState(ROOM_ID) + + private fun createViewModel(): RoomPollsViewModel { + return RoomPollsViewModel( + initialState = initialState, + getPollsUseCase = fakeGetPollsUseCase, + ) + } + + @Test + fun `given viewModel when created then polls list is observed and viewState is updated`() { + // Given + val polls = listOf(givenAPollSummary()) + every { fakeGetPollsUseCase.execute() } returns flowOf(polls) + val expectedViewState = initialState.copy(polls = polls) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + + // Then + viewModelTest + .assertLatestState(expectedViewState) + .finish() + verify { + fakeGetPollsUseCase.execute() + } + } + + private fun givenAPollSummary(): PollSummary { + return mockk() + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index 524858da77..79c998ff5c 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -21,6 +21,7 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule import im.vector.app.core.session.clientinfo.MatrixClientInfoContent import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.list.DeviceType import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo @@ -176,10 +177,7 @@ class DevicesViewModelTest { val viewModelTest = createViewModel().test() // Then - viewModelTest.assertLatestState { - it.devices is Success && it.devices.invoke() == deviceFullInfoList && - it.inactiveSessionsCount == 1 && it.unverifiedSessionsCount == 1 - } + viewModelTest.assertLatestState { it.devices is Success && it.devices.invoke() == deviceFullInfoList } viewModelTest.finish() } @@ -403,7 +401,7 @@ class DevicesViewModelTest { /** * Generate mocked deviceFullInfo list with 1 unverified and inactive + 1 verified and active. */ - private fun givenDeviceFullInfoList(deviceId1: String, deviceId2: String): List { + private fun givenDeviceFullInfoList(deviceId1: String, deviceId2: String): DeviceFullInfoList { val verifiedCryptoDeviceInfo = mockk() every { verifiedCryptoDeviceInfo.trustLevel } returns DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true) val unverifiedCryptoDeviceInfo = mockk() @@ -432,10 +430,15 @@ class DevicesViewModelTest { deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE), matrixClientInfo = MatrixClientInfoContent(), ) - val deviceFullInfoList = listOf(deviceFullInfo1, deviceFullInfo2) - val deviceFullInfoListFlow = flowOf(deviceFullInfoList) - every { getDeviceFullInfoListUseCase.execute(any(), any()) } returns deviceFullInfoListFlow - return deviceFullInfoList + val devices = listOf(deviceFullInfo1, deviceFullInfo2) + every { getDeviceFullInfoListUseCase.execute(DeviceManagerFilterType.ALL_SESSIONS, any()) } returns flowOf(devices) + every { getDeviceFullInfoListUseCase.execute(DeviceManagerFilterType.UNVERIFIED, any()) } returns flowOf(listOf(deviceFullInfo2)) + every { getDeviceFullInfoListUseCase.execute(DeviceManagerFilterType.INACTIVE, any()) } returns flowOf(listOf(deviceFullInfo1)) + return DeviceFullInfoList( + allSessions = devices, + unverifiedSessionsCount = 1, + inactiveSessionsCount = 1, + ) } private fun givenInitialViewState(deviceId1: String, deviceId2: String): DevicesViewState { @@ -444,8 +447,6 @@ class DevicesViewModelTest { return DevicesViewState( currentSessionCrossSigningInfo = currentSessionCrossSigningInfo, devices = Success(deviceFullInfoList), - unverifiedSessionsCount = 1, - inactiveSessionsCount = 1, isLoading = false, ) } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCaseTest.kt index 79dff5bc16..2e8a81bf54 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCaseTest.kt @@ -22,6 +22,7 @@ import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtende import im.vector.app.features.settings.devices.v2.list.DeviceType import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldContain import org.amshove.kluent.shouldContainAll import org.junit.Test import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel @@ -82,11 +83,22 @@ private val inactiveUnverifiedDevice = DeviceFullInfo( matrixClientInfo = MatrixClientInfoContent(), ) +private val deviceWithoutEncryptionSupport = DeviceFullInfo( + deviceInfo = DeviceInfo(deviceId = "DEVICE_WITHOUT_ENCRYPTION_SUPPORT"), + cryptoDeviceInfo = null, + roomEncryptionTrustLevel = null, + isInactive = false, + isCurrentDevice = false, + deviceExtendedInfo = DeviceExtendedInfo(DeviceType.UNKNOWN), + matrixClientInfo = MatrixClientInfoContent(), +) + private val devices = listOf( activeVerifiedDevice, inactiveVerifiedDevice, activeUnverifiedDevice, inactiveUnverifiedDevice, + deviceWithoutEncryptionSupport, ) class FilterDevicesUseCaseTest { @@ -123,8 +135,8 @@ class FilterDevicesUseCaseTest { val currentSessionCrossSigningInfo = givenCurrentSessionVerified(true) val filteredDeviceList = filterDevicesUseCase.execute(currentSessionCrossSigningInfo, devices, DeviceManagerFilterType.UNVERIFIED, emptyList()) - filteredDeviceList.size shouldBeEqualTo 2 - filteredDeviceList shouldContainAll listOf(activeUnverifiedDevice, inactiveUnverifiedDevice) + filteredDeviceList.size shouldBeEqualTo 3 + filteredDeviceList shouldContainAll listOf(activeUnverifiedDevice, inactiveUnverifiedDevice, deviceWithoutEncryptionSupport) } @Test @@ -132,7 +144,8 @@ class FilterDevicesUseCaseTest { val currentSessionCrossSigningInfo = givenCurrentSessionVerified(false) val filteredDeviceList = filterDevicesUseCase.execute(currentSessionCrossSigningInfo, devices, DeviceManagerFilterType.UNVERIFIED, emptyList()) - filteredDeviceList.size shouldBeEqualTo 0 + filteredDeviceList.size shouldBeEqualTo 1 + filteredDeviceList shouldContain deviceWithoutEncryptionSupport } @Test diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCaseTest.kt index b7d56a88bf..20291e5ee2 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCaseTest.kt @@ -17,43 +17,69 @@ package im.vector.app.features.settings.devices.v2.list import im.vector.app.test.fakes.FakeClock -import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue import org.junit.Test import java.util.concurrent.TimeUnit -private const val A_TIMESTAMP = 1654689143L +private const val A_TIMESTAMP_MILLIS = 1654689143000L class CheckIfSessionIsInactiveUseCaseTest { - private val clock = FakeClock().apply { givenEpoch(A_TIMESTAMP) } + private val clock = FakeClock().apply { givenEpoch(A_TIMESTAMP_MILLIS) } private val checkIfSessionIsInactiveUseCase = CheckIfSessionIsInactiveUseCase(clock) @Test fun `given an old last seen date then session is inactive`() { - val lastSeenDate = A_TIMESTAMP - TimeUnit.DAYS.toMillis(SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS.toLong()) - 1 + val lastSeenDate = A_TIMESTAMP_MILLIS - TimeUnit.DAYS.toMillis(SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS.toLong()) - 1 - checkIfSessionIsInactiveUseCase.execute(lastSeenDate) shouldBeEqualTo true + val result = checkIfSessionIsInactiveUseCase.execute(lastSeenDate) + + result.shouldBeTrue() } @Test fun `given a last seen date equal to the threshold then session is inactive`() { - val lastSeenDate = A_TIMESTAMP - TimeUnit.DAYS.toMillis(SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS.toLong()) + val lastSeenDate = A_TIMESTAMP_MILLIS - TimeUnit.DAYS.toMillis(SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS.toLong()) - checkIfSessionIsInactiveUseCase.execute(lastSeenDate) shouldBeEqualTo true + val result = checkIfSessionIsInactiveUseCase.execute(lastSeenDate) + + result.shouldBeTrue() } @Test fun `given a recent last seen date then session is active`() { - val lastSeenDate = A_TIMESTAMP - TimeUnit.DAYS.toMillis(SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS.toLong()) + 1 + val lastSeenDate = A_TIMESTAMP_MILLIS - TimeUnit.DAYS.toMillis(SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS.toLong()) + 1 - checkIfSessionIsInactiveUseCase.execute(lastSeenDate) shouldBeEqualTo false + val result = checkIfSessionIsInactiveUseCase.execute(lastSeenDate) + + result.shouldBeFalse() } @Test - fun `given a last seen date as zero then session is inactive`() { - // In case of the server doesn't send the last seen date. + fun `given a last seen date as zero then session is not inactive`() { val lastSeenDate = 0L - checkIfSessionIsInactiveUseCase.execute(lastSeenDate) shouldBeEqualTo true + val result = checkIfSessionIsInactiveUseCase.execute(lastSeenDate) + + result.shouldBeFalse() + } + + @Test + fun `given a last seen date as null then session is not inactive`() { + val lastSeenDate = null + + val result = checkIfSessionIsInactiveUseCase.execute(lastSeenDate) + + result.shouldBeFalse() + } + + @Test + fun `given a last seen date as negative then session is not inactive`() { + val lastSeenDate = -3L + + val result = checkIfSessionIsInactiveUseCase.execute(lastSeenDate) + + result.shouldBeFalse() } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCaseTest.kt index 2185c295d0..48c5ce74b4 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCaseTest.kt @@ -117,6 +117,45 @@ class GetDeviceFullInfoUseCaseTest { } } + @Test + fun `given current session and no crypto info for device when getting device info then the result is correct`() = runTest { + // Given + val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo() + val deviceInfo = givenADeviceInfo() + val cryptoDeviceInfo = null + val trustLevel = givenTrustLevel(currentSessionCrossSigningInfo, cryptoDeviceInfo) + val isInactive = false + val isCurrentDevice = true + every { checkIfSessionIsInactiveUseCase.execute(any()) } returns isInactive + every { parseDeviceUserAgentUseCase.execute(any()) } returns DeviceExtendedInfo(DeviceType.MOBILE) + val matrixClientInfo = givenAMatrixClientInfo() + fakeActiveSessionHolder.fakeSession.fakeCryptoService.cryptoDeviceInfoWithIdLiveData = MutableLiveData(Optional(null)) + fakeActiveSessionHolder.fakeSession.fakeCryptoService.cryptoDeviceInfoWithIdLiveData.givenAsFlow() + + // When + val deviceFullInfo = getDeviceFullInfoUseCase.execute(A_DEVICE_ID).firstOrNull() + + // Then + deviceFullInfo shouldBeEqualTo DeviceFullInfo( + deviceInfo = deviceInfo, + cryptoDeviceInfo = cryptoDeviceInfo, + roomEncryptionTrustLevel = trustLevel, + isInactive = isInactive, + isCurrentDevice = isCurrentDevice, + deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE), + matrixClientInfo = matrixClientInfo, + ) + verify { + fakeActiveSessionHolder.instance.getSafeActiveSession() + getCurrentSessionCrossSigningInfoUseCase.execute() + getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo) + fakeActiveSessionHolder.fakeSession.fakeCryptoService.getMyDevicesInfoLive(A_DEVICE_ID).asFlow() + fakeActiveSessionHolder.fakeSession.fakeCryptoService.getLiveCryptoDeviceInfoWithId(A_DEVICE_ID).asFlow() + checkIfSessionIsInactiveUseCase.execute(A_TIMESTAMP) + getMatrixClientInfoUseCase.execute(fakeActiveSessionHolder.fakeSession, A_DEVICE_ID) + } + } + @Test fun `given current session and no info for device when getting device info then the result is empty`() = runTest { // Given @@ -131,9 +170,11 @@ class GetDeviceFullInfoUseCaseTest { // Then deviceFullInfo.shouldBeNull() - verify { fakeActiveSessionHolder.instance.getSafeActiveSession() } - verify { fakeActiveSessionHolder.fakeSession.fakeCryptoService.getMyDevicesInfoLive(A_DEVICE_ID).asFlow() } - verify { fakeActiveSessionHolder.fakeSession.fakeCryptoService.getLiveCryptoDeviceInfoWithId(A_DEVICE_ID).asFlow() } + verify { + fakeActiveSessionHolder.instance.getSafeActiveSession() + fakeActiveSessionHolder.fakeSession.fakeCryptoService.getMyDevicesInfoLive(A_DEVICE_ID).asFlow() + fakeActiveSessionHolder.fakeSession.fakeCryptoService.getLiveCryptoDeviceInfoWithId(A_DEVICE_ID).asFlow() + } } @Test diff --git a/vector/src/test/java/im/vector/app/test/Extensions.kt b/vector/src/test/java/im/vector/app/test/Extensions.kt index 2fbab3b71b..0b1a22f75c 100644 --- a/vector/src/test/java/im/vector/app/test/Extensions.kt +++ b/vector/src/test/java/im/vector/app/test/Extensions.kt @@ -28,7 +28,7 @@ fun String.trimIndentOneLine() = trimIndent().replace("\n", "") fun VectorViewModel.test(): ViewModelTest { val testResultCollectingScope = CoroutineScope(Dispatchers.Unconfined) val state = stateFlow.test(testResultCollectingScope) - val viewEvents = viewEvents.stream().test(testResultCollectingScope) + val viewEvents = viewEvents.stream("test").test(testResultCollectingScope) return ViewModelTest(state, viewEvents) }