diff --git a/.editorconfig b/.editorconfig index 231d35cfe4..4f23d46afd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -770,7 +770,7 @@ ij_kotlin_align_multiline_extends_list = false ij_kotlin_align_multiline_method_parentheses = false ij_kotlin_align_multiline_parameters = true ij_kotlin_align_multiline_parameters_in_calls = false -ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma = true ij_kotlin_allow_trailing_comma_on_call_site = false ij_kotlin_assignment_wrap = off ij_kotlin_blank_lines_after_class_header = 0 diff --git a/changelog.d/6200.bugfix b/changelog.d/6200.bugfix new file mode 100644 index 0000000000..ee204b4567 --- /dev/null +++ b/changelog.d/6200.bugfix @@ -0,0 +1 @@ +Fixes room not being in space after upgrade diff --git a/changelog.d/6437.feature b/changelog.d/6437.feature new file mode 100644 index 0000000000..fb24819daf --- /dev/null +++ b/changelog.d/6437.feature @@ -0,0 +1 @@ +[Location sharing] - Delete action on a live message diff --git a/changelog.d/6487.feature b/changelog.d/6487.feature new file mode 100644 index 0000000000..3d58e80bd5 --- /dev/null +++ b/changelog.d/6487.feature @@ -0,0 +1 @@ +[Timeline] - Collapse redacted events diff --git a/changelog.d/6537.bugfix b/changelog.d/6537.bugfix new file mode 100644 index 0000000000..688fd5104c --- /dev/null +++ b/changelog.d/6537.bugfix @@ -0,0 +1 @@ +[Location Share] - Wrong room live location status bar visibility in timeline diff --git a/changelog.d/6585.bugfix b/changelog.d/6585.bugfix new file mode 100644 index 0000000000..63bf5a0af6 --- /dev/null +++ b/changelog.d/6585.bugfix @@ -0,0 +1 @@ +Fix backup saving several times the same keys diff --git a/changelog.d/6587.bugfix b/changelog.d/6587.bugfix new file mode 100644 index 0000000000..0273689cfd --- /dev/null +++ b/changelog.d/6587.bugfix @@ -0,0 +1 @@ +Check user power level before sharing live location diff --git a/changelog.d/6596.bugfix b/changelog.d/6596.bugfix new file mode 100644 index 0000000000..8cf97778c8 --- /dev/null +++ b/changelog.d/6596.bugfix @@ -0,0 +1 @@ +[Location Share] - Live is considered as ended while still active diff --git a/fastlane/metadata/android/ar/short_description.txt b/fastlane/metadata/android/ar/short_description.txt index 48df6f2b0c..2b0789b376 100644 --- a/fastlane/metadata/android/ar/short_description.txt +++ b/fastlane/metadata/android/ar/short_description.txt @@ -1 +1 @@ -مُحادثة آمنة لا مركزية و VoIP. حافظ على بياناتك آمنة من الأطراف الثالثة. +برنامج المراسلة الجماعية - الرسائل المشفرة والدردشة الجماعية ومكالمات الفيديو diff --git a/fastlane/metadata/android/ar/title.txt b/fastlane/metadata/android/ar/title.txt index 11992d355d..c2ac1b2876 100644 --- a/fastlane/metadata/android/ar/title.txt +++ b/fastlane/metadata/android/ar/title.txt @@ -1 +1 @@ -‏Element (‏Riot.im سابقًا) +إيليمنت - تطبيق محادثات أمن diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40104260.txt b/fastlane/metadata/android/cs-CZ/changelogs/40104260.txt new file mode 100644 index 0000000000..721c24555b --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40104260.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Podpora UnifiedPush a možnost používat push bez FCM. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40104270.txt b/fastlane/metadata/android/cs-CZ/changelogs/40104270.txt new file mode 100644 index 0000000000..578549ce6c --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40104270.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Opravy různých chyb a vylepšení stability. +Úplný seznam změn: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40104260.txt b/fastlane/metadata/android/et/changelogs/40104260.txt new file mode 100644 index 0000000000..2241582817 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40104260.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: võimalus kasutada tõukesõnumite jaoks FCM'i asemel UnifiedPush'i. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/et/changelogs/40104270.txt b/fastlane/metadata/android/et/changelogs/40104270.txt new file mode 100644 index 0000000000..1df5ac4176 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40104270.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: erinevate vigade parandused ja stabiilsust edendavad kohendused. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40104260.txt b/fastlane/metadata/android/fa/changelogs/40104260.txt new file mode 100644 index 0000000000..2e6de40015 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40104260.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: استفاده از UnifiedPush و اجازه به کاربر برای داشتن آگاهی‌های ارسالی بدون FCM. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40104270.txt b/fastlane/metadata/android/fa/changelogs/40104270.txt new file mode 100644 index 0000000000..29efb95925 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40104270.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: رفع اشکال‌های مختلف و بهبودهای پایداری. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104260.txt b/fastlane/metadata/android/fr-FR/changelogs/40104260.txt new file mode 100644 index 0000000000..2abff13ba4 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40104260.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Utilisation de UnifiedPush qui permet aux utilisateur d’utiliser « push » sans FCM. +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/40104270.txt b/fastlane/metadata/android/fr-FR/changelogs/40104270.txt new file mode 100644 index 0000000000..fe61fd021c --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40104270.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Plusieurs corrections de bogues et d’améliorations de stabilité. +Intégralité des changements : https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/gl/changelogs/40104160.txt b/fastlane/metadata/android/gl/changelogs/40104160.txt new file mode 100644 index 0000000000..ee2cde15ad --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/40104160.txt @@ -0,0 +1,2 @@ +Principais cambios nesta versión: melloras na xestión das mensaxes cifradas. Varios arranxos e melloras na estabilidade. +Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/gl/changelogs/40104180.txt b/fastlane/metadata/android/gl/changelogs/40104180.txt new file mode 100644 index 0000000000..532464f402 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/40104180.txt @@ -0,0 +1,2 @@ +Principais cambios nesta versión: varios arranxos e melloras na estabilidade. +Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/gl/changelogs/40104190.txt b/fastlane/metadata/android/gl/changelogs/40104190.txt new file mode 100644 index 0000000000..532464f402 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/40104190.txt @@ -0,0 +1,2 @@ +Principais cambios nesta versión: varios arranxos e melloras na estabilidade. +Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/gl/changelogs/40104200.txt b/fastlane/metadata/android/gl/changelogs/40104200.txt new file mode 100644 index 0000000000..532464f402 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/40104200.txt @@ -0,0 +1,2 @@ +Principais cambios nesta versión: varios arranxos e melloras na estabilidade. +Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/gl/changelogs/40104220.txt b/fastlane/metadata/android/gl/changelogs/40104220.txt new file mode 100644 index 0000000000..532464f402 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/40104220.txt @@ -0,0 +1,2 @@ +Principais cambios nesta versión: varios arranxos e melloras na estabilidade. +Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/gl/changelogs/40104230.txt b/fastlane/metadata/android/gl/changelogs/40104230.txt new file mode 100644 index 0000000000..532464f402 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/40104230.txt @@ -0,0 +1,2 @@ +Principais cambios nesta versión: varios arranxos e melloras na estabilidade. +Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/gl/changelogs/40104240.txt b/fastlane/metadata/android/gl/changelogs/40104240.txt new file mode 100644 index 0000000000..532464f402 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/40104240.txt @@ -0,0 +1,2 @@ +Principais cambios nesta versión: varios arranxos e melloras na estabilidade. +Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/gl/changelogs/40104250.txt b/fastlane/metadata/android/gl/changelogs/40104250.txt new file mode 100644 index 0000000000..532464f402 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/40104250.txt @@ -0,0 +1,2 @@ +Principais cambios nesta versión: varios arranxos e melloras na estabilidade. +Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/gl/changelogs/40104260.txt b/fastlane/metadata/android/gl/changelogs/40104260.txt new file mode 100644 index 0000000000..a863d73cc4 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/40104260.txt @@ -0,0 +1,2 @@ +Principais cambios nesta versión: Utiliza UnifiedPush e permite á usuaria obter notificacións sen FCM. +Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/gl/changelogs/40104270.txt b/fastlane/metadata/android/gl/changelogs/40104270.txt new file mode 100644 index 0000000000..532464f402 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/40104270.txt @@ -0,0 +1,2 @@ +Principais cambios nesta versión: varios arranxos e melloras na estabilidade. +Rexistro completo dos cambios: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/gl/full_description.txt b/fastlane/metadata/android/gl/full_description.txt new file mode 100644 index 0000000000..3502e3ba58 --- /dev/null +++ b/fastlane/metadata/android/gl/full_description.txt @@ -0,0 +1,42 @@ +Element é tanto unha mensaxería segura e unha app de productividade para o traballo en equipo, perfecta para conversas de grupos con traballo remoto. Esta app de chat usa cifrado de extremo-a-extremo para proporcionar video conferencias seguras, compartición de ficheiros e chamadas de voz. + +Características de Element incluídas: +- Ferramentas avanzadas para a comunicación en liña +- Mensaxes completamente cifradas para permitir a comunicación corporativa, incluso para traballo remoto +- Chat descentralizado baseado no sistema de código aberto Matrix +- Compartición segura de ficheiros con datos cifrados na xestión de proxectos +- Chats de vídeo con Voz sobre IP en compartición de pantalla +- Integración doada con outras ferramentas de colaboración en liña, ferramentas de xestión de proxectos, servizos VoIP e outras apps de mensaxería para equipos + +Element é completamente diferente a outras apps de mensaxería e traballo en equipo. Funciona grazas a Matrix, unha rede aberta para mensaxería segura e descentralizada. Permite a hospedaxe na infraestructura propia para proporcionar o maior grao de propiedade e control sobre os teus datos e mensaxes. + +Mensaxería privada e cifrada +Element protéxete da publicidade non solicitada, minería de datos e burbullas de contido. Tamén protexe os teus datos, chamadas de vídeo e voz cifradas de extremo-a-extremo así como verificación con sinatura dos dispositivos. + +Element pon baixo o teu control a túa privacidade permitíndoche comunicarte de xeito seguro con calquera a través da rede Matrix, ou en outras ferramentas de colaboración para empresas ao estar integrada en apps como Slack. + +Element na túa infraestructura +Para un maior control sobre os teus datos sensibles e comunicacións, podes hospedar Element ou elexir calquera hóspede baseado en Matrix - un estándar para comunicación descentralizado e de código aberto. Element proporciona privacidade e seguridade así como flexibilidade para a integración. + +Os teus datos +Ti decides onde gardas os teus datos e mensaxes. Sen o risco da minería de datos ou acceso por terceiras partes. + +Element ponte ao mando de varios xeitos: +1. Consigue unha conta gratuíta no servidor público matrix.org hospedado polos desenvolvedores de Matrix, ou elixe entre miles de servidores públicos xestionados por voluntarias +2. Hospeda a túa conta na túa propia infraestructura IT +3. Crea unha conta nun servidor personalizado simplemente subscribíndote á plataforma de hospedaxe Element Matrix Services + +Mensaxería e Colaboración abertas +Podes conversar con calquera na rede Matrix, tanto se usan Element ou outra app Matrix ou incluso unha mensaxería diferente. + +Super segura +Cifrado real de extremo-a-extremo (só quen participa na conversa pode descifrar as mensaxes), e verificación con sinatura cruzada dos dispositivos. + +Comunicación e integración completas +Mensaxería, chamadas de voz e vídeo, compartición de ficheiros, compartición de pantalla e moitas máis integracións, bots e widgets. Crea salas, comunidades, mantén o contacto e saca adiante o traballo. + +Continúa onde o deixaches +Sigue en contacto alá onde estés grazas ao historial sincronizado de mensaxería entre tódolos dispositivos e na web https://app.element.io + +Código aberto +Element Android é un proxecto de código aberto, hospedado en GitHub. Informa de fallos e/ou contribúe ao seu desenvolvemento en https://github.com/vector-im/element-android diff --git a/fastlane/metadata/android/gl/short_description.txt b/fastlane/metadata/android/gl/short_description.txt new file mode 100644 index 0000000000..7c7f65bf61 --- /dev/null +++ b/fastlane/metadata/android/gl/short_description.txt @@ -0,0 +1 @@ +Mensaxería en grupo - mensaxería cifrada, chat en grupo e videochamadas diff --git a/fastlane/metadata/android/gl/title.txt b/fastlane/metadata/android/gl/title.txt new file mode 100644 index 0000000000..0fb73bc324 --- /dev/null +++ b/fastlane/metadata/android/gl/title.txt @@ -0,0 +1 @@ +Element - Mensaxería Segura diff --git a/fastlane/metadata/android/id/changelogs/40104260.txt b/fastlane/metadata/android/id/changelogs/40104260.txt new file mode 100644 index 0000000000..bfd9637fda --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40104260.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Dukungan UnifiedPush, memungkinkan pengguna untuk diberitahukan tanpa FCM. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/id/changelogs/40104270.txt b/fastlane/metadata/android/id/changelogs/40104270.txt new file mode 100644 index 0000000000..1017951d47 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40104270.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Banyak perbaikan kutu dan perbaikan stabilitas. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40104260.txt b/fastlane/metadata/android/it-IT/changelogs/40104260.txt new file mode 100644 index 0000000000..d52ed6b769 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40104260.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: utilizza UnifiedPush e consente all'utente di avere notifiche push senza FCM. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40104270.txt b/fastlane/metadata/android/it-IT/changelogs/40104270.txt new file mode 100644 index 0000000000..556a6fc7ea --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40104270.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: varie correzioni di errori e miglioramenti della stabilità. +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40104260.txt b/fastlane/metadata/android/sk/changelogs/40104260.txt new file mode 100644 index 0000000000..6980671174 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40104260.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Použitie UnifiedPush a umožňuje používateľovi používať push bez FCM. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sk/changelogs/40104270.txt b/fastlane/metadata/android/sk/changelogs/40104270.txt new file mode 100644 index 0000000000..50670f18c2 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40104270.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Rôzne opravy chýb a vylepšenia stability. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/40104260.txt b/fastlane/metadata/android/sv-SE/changelogs/40104260.txt new file mode 100644 index 0000000000..99185d8562 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40104260.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Använd UnifiedPush och tillåt användare att ha push utan FCM. +Full ändringslogg: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/40104270.txt b/fastlane/metadata/android/sv-SE/changelogs/40104270.txt new file mode 100644 index 0000000000..d8db452b51 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40104270.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Diverse buggfixar och stabilitetsförbättringar. +Full ändringslogg: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40101000.txt b/fastlane/metadata/android/uk/changelogs/40101000.txt index 36870f5ef7..1b7d25f1b9 100644 --- a/fastlane/metadata/android/uk/changelogs/40101000.txt +++ b/fastlane/metadata/android/uk/changelogs/40101000.txt @@ -1,2 +1,2 @@ -Основні зміни в цій версії: поліпшення VoIP (аудіо та відео дзвінки в DM) та виправлення помилок! +Основні зміни в цій версії: поліпшення VoIP (аудіо та відеовиклики у ПП) та виправлення помилок! Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/uk/changelogs/40104260.txt b/fastlane/metadata/android/uk/changelogs/40104260.txt new file mode 100644 index 0000000000..3e3219946a --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40104260.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Застосовано UnifiedPush і дозволено користувачам отримувати push-сповіщення без FCM. +Перелік усіх змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/uk/changelogs/40104270.txt b/fastlane/metadata/android/uk/changelogs/40104270.txt new file mode 100644 index 0000000000..9664c615c1 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40104270.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Усунуто різні вади й поліпшено стабільність. +Перелік усіх змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104260.txt b/fastlane/metadata/android/zh-TW/changelogs/40104260.txt new file mode 100644 index 0000000000..7569b4f491 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40104260.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:使用 UnifiedPush 並允許使用者在沒有 FCM 的情況下推送。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40104270.txt b/fastlane/metadata/android/zh-TW/changelogs/40104270.txt new file mode 100644 index 0000000000..4bcca9a0b8 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40104270.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:多個臭蟲修復與穩定性改善。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 6f1646ec53..ee58db748c 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -199,7 +199,7 @@ dependencies { implementation libs.apache.commonsImaging // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.51' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.52' testImplementation libs.tests.junit // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt index e160938721..2439119f01 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt @@ -24,7 +24,6 @@ import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.FixMethodOrder -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -56,7 +55,6 @@ import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) @LargeTest -@Ignore class KeysBackupTest : InstrumentedTest { @get:Rule val rule = RetryTestRule(3) 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 7f9ab4c6dd..59dc6c434d 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 @@ -202,7 +202,7 @@ data class Event( * It will return a decrypted text message or an empty string otherwise. */ fun getDecryptedTextSummary(): String? { - if (isRedacted()) return "Message Deleted" + if (isRedacted()) return "Message removed" val text = getDecryptedValue() ?: run { if (isPoll()) { return getPollQuestion() ?: "created a poll." @@ -371,6 +371,8 @@ fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClear fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER +fun Event.isLiveLocation(): Boolean = getClearType() in EventType.STATE_ROOM_BEACON_INFO + fun Event.getRelationContent(): RelationDefaultContent? { return if (isEncrypted()) { content.toModel()?.relatesTo diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt index 14095b67c0..cd8acbcccc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.api.session.room.location -import androidx.annotation.MainThread import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.util.Cancelable @@ -59,16 +58,21 @@ interface LocationSharingService { */ suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult + /** + * Redact (delete) the live associated to the given beacon info event id. + * @param beaconInfoEventId event id of the initial beacon info state event + * @param reason Optional reason string + */ + suspend fun redactLiveLocationShare(beaconInfoEventId: String, reason: String?) + /** * Returns a LiveData on the list of current running live location shares. */ - @MainThread fun getRunningLiveLocationShareSummaries(): LiveData> /** * Returns a LiveData on the live location share summary with the given eventId. * @param beaconInfoEventId event id of the initial beacon info state event */ - @MainThread fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt index 2e1668ebbb..8cfe3da031 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt @@ -33,5 +33,7 @@ enum class VersioningState { /** * The room has been upgraded, and the new room has been joined. */ - UPGRADED_ROOM_JOINED, + UPGRADED_ROOM_JOINED; + + fun isUpgraded() = this != NONE } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 9d8c8a13bd..d391abf1e6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.isEdition +import org.matrix.android.sdk.api.session.events.model.isLiveLocation import org.matrix.android.sdk.api.session.events.model.isPoll import org.matrix.android.sdk.api.session.events.model.isReply import org.matrix.android.sdk.api.session.events.model.isSticker @@ -165,6 +166,10 @@ fun TimelineEvent.isSticker(): Boolean { return root.isSticker() } +fun TimelineEvent.isLiveLocation(): Boolean { + return root.isLiveLocation() +} + /** * Returns whether or not the event is a root thread event. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt index 56425cbc74..46ebbb7b71 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt @@ -37,9 +37,9 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionData * This class execute the registration request and is responsible to keep the session of interactive authentication. */ internal class DefaultRegistrationWizard( - authAPI: AuthAPI, - private val sessionCreator: SessionCreator, - private val pendingSessionStore: PendingSessionStore + authAPI: AuthAPI, + private val sessionCreator: SessionCreator, + private val pendingSessionStore: PendingSessionStore ) : RegistrationWizard { private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") @@ -74,20 +74,20 @@ internal class DefaultRegistrationWizard( initialDeviceDisplayName: String? ): RegistrationResult { val params = RegistrationParams( - username = userName, - password = password, - initialDeviceDisplayName = initialDeviceDisplayName + username = userName, + password = password, + initialDeviceDisplayName = initialDeviceDisplayName ) return performRegistrationRequest(params, LoginType.PASSWORD) - .also { - pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true) - .also { pendingSessionStore.savePendingSessionData(it) } - } + .also { + pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true) + .also { pendingSessionStore.savePendingSessionData(it) } + } } override suspend fun performReCaptcha(response: String): RegistrationResult { val safeSession = pendingSessionData.currentSession - ?: throw IllegalStateException("developer error, call createAccount() method first") + ?: throw IllegalStateException("developer error, call createAccount() method first") val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response)) return performRegistrationRequest(params, LoginType.PASSWORD) @@ -95,7 +95,7 @@ internal class DefaultRegistrationWizard( override suspend fun acceptTerms(): RegistrationResult { val safeSession = pendingSessionData.currentSession - ?: throw IllegalStateException("developer error, call createAccount() method first") + ?: throw IllegalStateException("developer error, call createAccount() method first") val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession)) return performRegistrationRequest(params, LoginType.PASSWORD) @@ -103,14 +103,14 @@ internal class DefaultRegistrationWizard( override suspend fun addThreePid(threePid: RegisterThreePid): RegistrationResult { pendingSessionData = pendingSessionData.copy(currentThreePidData = null) - .also { pendingSessionStore.savePendingSessionData(it) } + .also { pendingSessionStore.savePendingSessionData(it) } return sendThreePid(threePid) } override suspend fun sendAgainThreePid(): RegistrationResult { val safeCurrentThreePid = pendingSessionData.currentThreePidData?.threePid - ?: throw IllegalStateException("developer error, call createAccount() method first") + ?: throw IllegalStateException("developer error, call createAccount() method first") return sendThreePid(safeCurrentThreePid) } @@ -126,7 +126,7 @@ internal class DefaultRegistrationWizard( ) pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1) - .also { pendingSessionStore.savePendingSessionData(it) } + .also { pendingSessionStore.savePendingSessionData(it) } val params = RegistrationParams( auth = if (threePid is RegisterThreePid.Email) { @@ -149,7 +149,7 @@ internal class DefaultRegistrationWizard( ) // Store data pendingSessionData = pendingSessionData.copy(currentThreePidData = ThreePidData.from(threePid, response, params)) - .also { pendingSessionStore.savePendingSessionData(it) } + .also { pendingSessionStore.savePendingSessionData(it) } // and send the sid a first time return performRegistrationRequest(params, LoginType.PASSWORD) @@ -157,7 +157,7 @@ internal class DefaultRegistrationWizard( override suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult { val safeParam = pendingSessionData.currentThreePidData?.registrationParams - ?: throw IllegalStateException("developer error, no pending three pid") + ?: throw IllegalStateException("developer error, no pending three pid") return performRegistrationRequest(safeParam, LoginType.PASSWORD, delayMillis) } @@ -168,13 +168,13 @@ internal class DefaultRegistrationWizard( private suspend fun validateThreePid(code: String): RegistrationResult { val registrationParams = pendingSessionData.currentThreePidData?.registrationParams - ?: throw IllegalStateException("developer error, no pending three pid") + ?: throw IllegalStateException("developer error, no pending three pid") val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first") val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url to send the code") val validationBody = ValidationCodeBody( - clientSecret = pendingSessionData.clientSecret, - sid = safeCurrentData.addThreePidRegistrationResponse.sid, - code = code + clientSecret = pendingSessionData.clientSecret, + sid = safeCurrentData.addThreePidRegistrationResponse.sid, + code = code ) val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody)) if (validationResponse.isSuccess()) { @@ -189,7 +189,7 @@ internal class DefaultRegistrationWizard( override suspend fun dummy(): RegistrationResult { val safeSession = pendingSessionData.currentSession - ?: throw IllegalStateException("developer error, call createAccount() method first") + ?: throw IllegalStateException("developer error, call createAccount() method first") val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession)) return performRegistrationRequest(params, LoginType.PASSWORD) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt index ab7cbb74b1..39dfb72149 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt @@ -21,13 +21,10 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import timber.log.Timber -import java.util.Timer -import java.util.TimerTask import javax.inject.Inject internal data class InboundGroupSessionHolder( @@ -57,18 +54,13 @@ internal class InboundGroupSessionStore @Inject constructor( if (oldValue != null) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}") - store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper }) + // store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper }) oldValue.wrapper.session.releaseSession() } } } } - private val timer = Timer() - private var timerTask: TimerTask? = null - - private val dirtySession = mutableListOf() - @Synchronized fun clear() { sessionCache.evictAll() @@ -90,7 +82,6 @@ internal class InboundGroupSessionStore @Inject constructor( @Synchronized fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) { Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}") - dirtySession.remove(old) store.removeInboundGroupSession(sessionId, senderKey) sessionCache.remove(CacheKey(sessionId, senderKey)) @@ -107,33 +98,14 @@ internal class InboundGroupSessionStore @Inject constructor( private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) { Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}") - // We want to batch this a bit for performances - dirtySession.add(holder) if (sessionCache[CacheKey(sessionId, senderKey)] == null) { // first time seen, put it in memory cache while waiting for batch insert // If it's already known, no need to update cache it's already there sessionCache.put(CacheKey(sessionId, senderKey), holder) } - - timerTask?.cancel() - timerTask = object : TimerTask() { - override fun run() { - batchSave() - } - } - timer.schedule(timerTask!!, 300) - } - - @Synchronized - private fun batchSave() { - val toSave = mutableListOf().apply { addAll(dirtySession) } - dirtySession.clear() cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession batching save of ${toSave.size}") - tryOrNull { - store.storeInboundGroupSessions(toSave.map { it.wrapper }) - } + store.storeInboundGroupSessions(listOf(holder.wrapper)) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index c4a6488258..96ccba51dc 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -604,14 +604,16 @@ internal class MXOlmDevice @Inject constructor( * @param sharedHistory MSC3061, this key is sharable on invite * @return true if the operation succeeds. */ - fun addInboundGroupSession(sessionId: String, - sessionKey: String, - roomId: String, - senderKey: String, - forwardingCurve25519KeyChain: List, - keysClaimed: Map, - exportFormat: Boolean, - sharedHistory: Boolean): AddSessionResult { + fun addInboundGroupSession( + sessionId: String, + sessionKey: String, + roomId: String, + senderKey: String, + forwardingCurve25519KeyChain: List, + keysClaimed: Map, + exportFormat: Boolean, + sharedHistory: Boolean + ): AddSessionResult { val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") { if (exportFormat) { OlmInboundGroupSession.importSession(sessionKey) @@ -701,8 +703,8 @@ internal class MXOlmDevice @Inject constructor( val senderKey = megolmSessionData.senderKey ?: continue val roomId = megolmSessionData.roomId - val candidateSessionToImport = try { - MXInboundMegolmSessionWrapper.newFromMegolmData(megolmSessionData, true) + val candidateSessionToImport = try { + MXInboundMegolmSessionWrapper.newFromMegolmData(megolmSessionData, true) } catch (e: Throwable) { Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Failed to import session $senderKey/$sessionId") continue @@ -806,7 +808,6 @@ internal class MXOlmDevice @Inject constructor( } replayAttackMap[messageIndexKey] = eventId } - inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey) val payload = try { val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt index 414416a0f6..38edbb7430 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt @@ -38,6 +38,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor( outgoingKeyRequestManager, cryptoStore, matrixConfiguration, - eventsManager) + eventsManager + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 48a25f2a8b..ceaee582c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -250,8 +250,10 @@ internal class MXMegolmEncryption( * @param sessionInfo the session info * @param devicesByUser the devices map */ - private suspend fun shareUserDevicesKey(sessionInfo: MXOutboundSessionInfo, - devicesByUser: Map>) { + private suspend fun shareUserDevicesKey( + sessionInfo: MXOutboundSessionInfo, + devicesByUser: Map> + ) { val sessionKey = olmDevice.getSessionKey(sessionInfo.sessionId) ?: return Unit.also { Timber.tag(loggerTag.value).v("shareUserDevicesKey() Failed to share session, failed to export") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt index 49cf60d051..8691c08779 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -1349,6 +1349,8 @@ internal class DefaultKeysBackupService @Inject constructor( // Mark keys as backed up cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers) + // we can release the sessions now + olmInboundGroupSessionWrappers.onEach { it.session.releaseSession() } if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) { Timber.v("backupKeys: All keys have been backed up") 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 20ca357d1a..f5468634cb 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 @@ -763,11 +763,17 @@ internal class RealmCryptoStore @Inject constructor( // } ?: false val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, wrapper.sessionData.senderKey) + val existing = realm.where() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findFirst() + val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { primaryKey = key store(wrapper) + backedUp = existing?.backedUp ?: false } - Timber.i("## CRYPTO | shouldShareHistory: ${wrapper.sessionData.sharedHistory} for $key") + + Timber.v("## CRYPTO | shouldShareHistory: ${wrapper.sessionData.sharedHistory} for $key") realm.insertOrUpdate(realmOlmInboundGroupSession) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index b54aec26b2..64e69bb3e9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -49,6 +49,8 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo029 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo030 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo031 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo032 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -57,7 +59,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 32L, + schemaVersion = 34L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -99,5 +101,7 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 30) MigrateSessionTo030(realm).perform() if (oldVersion < 31) MigrateSessionTo031(realm).perform() if (oldVersion < 32) MigrateSessionTo032(realm).perform() + if (oldVersion < 33) MigrateSessionTo033(realm).perform() + if (oldVersion < 34) MigrateSessionTo034(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo033.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo033.kt new file mode 100644 index 0000000000..0e3a8599c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo033.kt @@ -0,0 +1,33 @@ +/* + * 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.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +/** + * Migrating to: + * Live location sharing aggregated summary: adding new field relatedEventIds. + */ +internal class MigrateSessionTo033(realm: DynamicRealm) : RealmMigrator(realm, 33) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("LiveLocationShareAggregatedSummaryEntity") + ?.addRealmListField(LiveLocationShareAggregatedSummaryEntityFields.RELATED_EVENT_IDS.`$`, String::class.java) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo034.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo034.kt new file mode 100644 index 0000000000..b23e84706f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo034.kt @@ -0,0 +1,34 @@ +/* + * 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.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +/** + * Migrating to: + * Live location sharing aggregated summary: adding new field startOfLiveTimestampMillis. + */ +internal class MigrateSessionTo034(realm: DynamicRealm) : RealmMigrator(realm, 34) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("LiveLocationShareAggregatedSummaryEntity") + ?.addField(LiveLocationShareAggregatedSummaryEntityFields.START_OF_LIVE_TIMESTAMP_MILLIS, Long::class.java) + ?.setNullable(LiveLocationShareAggregatedSummaryEntityFields.START_OF_LIVE_TIMESTAMP_MILLIS, true) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt index c5df8e9338..ca793ffd8e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.database.model.livelocation +import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.PrimaryKey @@ -29,6 +30,11 @@ internal open class LiveLocationShareAggregatedSummaryEntity( @PrimaryKey var eventId: String = "", + /** + * List of event ids used to compute the aggregated summary data. + */ + var relatedEventIds: RealmList = RealmList(), + var roomId: String = "", var userId: String = "", @@ -38,6 +44,8 @@ internal open class LiveLocationShareAggregatedSummaryEntity( */ var isActive: Boolean? = null, + var startOfLiveTimestampMillis: Long? = null, + var endOfLiveTimestampMillis: Long? = null, /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt index 6caa832110..1c19c21de2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt @@ -23,6 +23,11 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { + return realm.where() + .equalTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId) +} + internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery { return realm.where() .equalTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId) @@ -44,3 +49,7 @@ internal fun EventAnnotationsSummaryEntity.Companion.getOrCreate(realm: Realm, r return EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst() ?: EventAnnotationsSummaryEntity.create(realm, roomId, eventId) } + +internal fun EventAnnotationsSummaryEntity.Companion.get(realm: Realm, eventId: String): EventAnnotationsSummaryEntity? { + return EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt index d69f251f6f..a1179ccdce 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt @@ -23,6 +23,14 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields +internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where( + realm: Realm, + eventId: String, +): RealmQuery { + return realm.where() + .equalTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId) +} + internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where( realm: Realm, roomId: String, @@ -72,17 +80,26 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get( return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst() } +internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get( + realm: Realm, + eventId: String, +): LiveLocationShareAggregatedSummaryEntity? { + return LiveLocationShareAggregatedSummaryEntity.where(realm, eventId).findFirst() +} + internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveInRoomForUser( realm: Realm, roomId: String, userId: String, ignoredEventId: String, + startOfLiveTimestampThreshold: Long, ): List { return LiveLocationShareAggregatedSummaryEntity .whereRoomId(realm, roomId = roomId) .equalTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, userId) .equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true) .notEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, ignoredEventId) + .lessThan(LiveLocationShareAggregatedSummaryEntityFields.START_OF_LIVE_TIMESTAMP_MILLIS, startOfLiveTimestampThreshold) .findAll() .toList() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index f8a52f0b7e..b9f56cbc9f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -88,6 +88,7 @@ import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationPro import org.matrix.android.sdk.internal.session.room.aggregation.poll.DefaultPollAggregationProcessor import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor +import org.matrix.android.sdk.internal.session.room.location.LiveLocationShareRedactionEventProcessor import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessorCoroutine @@ -321,6 +322,10 @@ internal abstract class SessionModule { @IntoSet abstract fun bindEventRedactionProcessor(processor: RedactionEventProcessor): EventInsertLiveProcessor + @Binds + @IntoSet + abstract fun bindLiveLocationShareRedactionEventProcessor(processor: LiveLocationShareRedactionEventProcessor): EventInsertLiveProcessor + @Binds @IntoSet abstract fun bindEventRelationsAggregationProcessor(processor: EventRelationsAggregationProcessor): EventInsertLiveProcessor diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index e1dd22a211..d01324a35f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -58,11 +58,13 @@ import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVi import org.matrix.android.sdk.internal.session.room.location.CheckIfExistingActiveLiveTask import org.matrix.android.sdk.internal.session.room.location.DefaultCheckIfExistingActiveLiveTask import org.matrix.android.sdk.internal.session.room.location.DefaultGetActiveBeaconInfoForUserTask +import org.matrix.android.sdk.internal.session.room.location.DefaultRedactLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.DefaultSendLiveLocationTask import org.matrix.android.sdk.internal.session.room.location.DefaultSendStaticLocationTask import org.matrix.android.sdk.internal.session.room.location.DefaultStartLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.DefaultStopLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.GetActiveBeaconInfoForUserTask +import org.matrix.android.sdk.internal.session.room.location.RedactLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.SendLiveLocationTask import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask @@ -339,4 +341,7 @@ internal abstract class RoomModule { @Binds abstract fun bindCheckIfExistingActiveLiveTask(task: DefaultCheckIfExistingActiveLiveTask): CheckIfExistingActiveLiveTask + + @Binds + abstract fun bindRedactLiveLocationShareTask(task: DefaultRedactLiveLocationShareTask): RedactLiveLocationShareTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt index 921749122b..510c20497b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation import androidx.work.ExistingWorkPolicy import io.realm.Realm +import io.realm.RealmList import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toContent @@ -73,16 +74,22 @@ internal class LiveLocationAggregationProcessor @Inject constructor( eventId = targetEventId ) + if (!isLive && !event.eventId.isNullOrEmpty()) { + // in this case, the received event is a new state event related to the previous one + addRelatedEventId(event.eventId, aggregatedSummary) + } + // remote event can stay with isLive == true while the local summary is no more active val isActive = aggregatedSummary.isActive.orTrue() && isLive val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) } Timber.d("updating summary of id=$targetEventId with isActive=$isActive and endTimestamp=$endOfLiveTimestampMillis") + aggregatedSummary.startOfLiveTimestampMillis = content.getBestTimestampMillis() aggregatedSummary.endOfLiveTimestampMillis = endOfLiveTimestampMillis aggregatedSummary.isActive = isActive aggregatedSummary.userId = event.senderId - deactivateAllPreviousBeacons(realm, roomId, event.senderId, targetEventId) + deactivateAllPreviousBeacons(realm, roomId, event.senderId, targetEventId, content.getBestTimestampMillis() ?: 0) if (isActive) { scheduleDeactivationAfterTimeout(targetEventId, roomId, endOfLiveTimestampMillis) @@ -144,6 +151,11 @@ internal class LiveLocationAggregationProcessor @Inject constructor( roomId = roomId, eventId = relatedEventId ) + + if (!event.eventId.isNullOrEmpty()) { + addRelatedEventId(event.eventId, aggregatedSummary) + } + val updatedLocationTimestamp = content.getBestTimestampMillis() ?: 0 val currentLocationTimestamp = ContentMapper .map(aggregatedSummary.lastLocationContent) @@ -160,13 +172,31 @@ internal class LiveLocationAggregationProcessor @Inject constructor( } } - private fun deactivateAllPreviousBeacons(realm: Realm, roomId: String, userId: String, currentEventId: String) { + private fun addRelatedEventId( + eventId: String, + aggregatedSummary: LiveLocationShareAggregatedSummaryEntity + ) { + Timber.d("adding related event id $eventId to summary of id ${aggregatedSummary.eventId}") + val updatedEventIds = aggregatedSummary.relatedEventIds.toMutableList().also { + it.add(eventId) + } + aggregatedSummary.relatedEventIds = RealmList(*updatedEventIds.toTypedArray()) + } + + private fun deactivateAllPreviousBeacons( + realm: Realm, + roomId: String, + userId: String, + currentEventId: String, + currentEventTimestamp: Long + ) { LiveLocationShareAggregatedSummaryEntity .findActiveLiveInRoomForUser( realm = realm, roomId = roomId, userId = userId, - ignoredEventId = currentEventId + ignoredEventId = currentEventId, + startOfLiveTimestampThreshold = currentEventTimestamp ) .forEach { it.isActive = false } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt index a8a9691ce9..60312071d7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -42,6 +42,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor( private val startLiveLocationShareTask: StartLiveLocationShareTask, private val stopLiveLocationShareTask: StopLiveLocationShareTask, private val checkIfExistingActiveLiveTask: CheckIfExistingActiveLiveTask, + private val redactLiveLocationShareTask: RedactLiveLocationShareTask, private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper, ) : LocationSharingService { @@ -102,6 +103,15 @@ internal class DefaultLocationSharingService @AssistedInject constructor( return stopLiveLocationShareTask.execute(params) } + override suspend fun redactLiveLocationShare(beaconInfoEventId: String, reason: String?) { + val params = RedactLiveLocationShareTask.Params( + roomId = roomId, + beaconInfoEventId = beaconInfoEventId, + reason = reason + ) + return redactLiveLocationShareTask.execute(params) + } + override fun getRunningLiveLocationShareSummaries(): LiveData> { return monarchy.findAllMappedWithChanges( { LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) }, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt new file mode 100644 index 0000000000..fa3479ed3c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt @@ -0,0 +1,65 @@ +/* + * Copyright 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.location + +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.LocalEcho +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import timber.log.Timber +import javax.inject.Inject + +/** + * Listens to the database for the insertion of any redaction event. + * Delete specifically the aggregated summary related to a redacted live location share event. + */ +internal class LiveLocationShareRedactionEventProcessor @Inject constructor() : EventInsertLiveProcessor { + + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { + return eventType == EventType.REDACTION && insertType != EventInsertType.LOCAL_ECHO + } + + override suspend fun process(realm: Realm, event: Event) { + if (event.redacts.isNullOrBlank() || LocalEcho.isLocalEchoId(event.eventId.orEmpty())) { + return + } + + val redactedEvent = EventEntity.where(realm, eventId = event.redacts).findFirst() + ?: return + + if (redactedEvent.type in EventType.STATE_ROOM_BEACON_INFO) { + val liveSummary = LiveLocationShareAggregatedSummaryEntity.get(realm, eventId = redactedEvent.eventId) + + if (liveSummary != null) { + Timber.d("deleting live summary with id: ${liveSummary.eventId}") + liveSummary.deleteFromRealm() + val annotationsSummary = EventAnnotationsSummaryEntity.get(realm, eventId = redactedEvent.eventId) + if (annotationsSummary != null) { + Timber.d("deleting annotation summary with id: ${annotationsSummary.eventId}") + annotationsSummary.deleteFromRealm() + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/RedactLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/RedactLiveLocationShareTask.kt new file mode 100644 index 0000000000..ac855b81e7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/RedactLiveLocationShareTask.kt @@ -0,0 +1,78 @@ +/* + * 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.location + +import io.realm.RealmConfiguration +import org.matrix.android.sdk.internal.database.awaitTransaction +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor +import org.matrix.android.sdk.internal.task.Task +import timber.log.Timber +import javax.inject.Inject + +internal interface RedactLiveLocationShareTask : Task { + data class Params( + val roomId: String, + val beaconInfoEventId: String, + val reason: String? + ) +} + +internal class DefaultRedactLiveLocationShareTask @Inject constructor( + @SessionDatabase private val realmConfiguration: RealmConfiguration, + private val localEchoEventFactory: LocalEchoEventFactory, + private val eventSenderProcessor: EventSenderProcessor, +) : RedactLiveLocationShareTask { + + override suspend fun execute(params: RedactLiveLocationShareTask.Params) { + val relatedEventIds = getRelatedEventIdsOfLive(params.beaconInfoEventId) + Timber.d("beacon with id ${params.beaconInfoEventId} has related event ids: ${relatedEventIds.joinToString(", ")}") + + postRedactionWithLocalEcho( + eventId = params.beaconInfoEventId, + roomId = params.roomId, + reason = params.reason + ) + relatedEventIds.forEach { eventId -> + postRedactionWithLocalEcho( + eventId = eventId, + roomId = params.roomId, + reason = params.reason + ) + } + } + + private suspend fun getRelatedEventIdsOfLive(beaconInfoEventId: String): List { + return awaitTransaction(realmConfiguration) { realm -> + val aggregatedSummaryEntity = LiveLocationShareAggregatedSummaryEntity.get( + realm = realm, + eventId = beaconInfoEventId + ) + aggregatedSummaryEntity?.relatedEventIds?.toList() ?: emptyList() + } + } + + private fun postRedactionWithLocalEcho(eventId: String, roomId: String, reason: String?) { + Timber.d("posting redaction for event of id $eventId") + val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, eventId, reason) + localEchoEventFactory.createLocalEcho(redactionEcho) + eventSenderProcessor.postRedaction(redactionEcho, reason) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt index e33fbb56b1..cc86679cbc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt @@ -74,6 +74,8 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr when (typeToPrune) { EventType.ENCRYPTED, EventType.MESSAGE, + in EventType.STATE_ROOM_BEACON_INFO, + in EventType.BEACON_LOCATION_DATA, in EventType.POLL_START -> { Timber.d("REDACTION for message ${eventToPrune.eventId}") val unsignedData = EventMapper.map(eventToPrune).unsignedData diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/fatal.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/fatal.kt index 323eee0b1c..7ed807d7cc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/fatal.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/fatal.kt @@ -22,7 +22,7 @@ import timber.log.Timber /** * Throws in debug, only log in production. * As this method does not always throw, next statement should be a return. -*/ + */ internal fun fatalError(message: String) { if (BuildConfig.DEBUG) { error(message) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt index 933087af2b..25d441ef5c 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation import androidx.work.ExistingWorkPolicy import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldContain import org.junit.Test import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.UnsignedData @@ -35,6 +36,7 @@ import org.matrix.android.sdk.test.fakes.FakeWorkManagerProvider import org.matrix.android.sdk.test.fakes.givenEqualTo import org.matrix.android.sdk.test.fakes.givenFindAll import org.matrix.android.sdk.test.fakes.givenFindFirst +import org.matrix.android.sdk.test.fakes.givenLessThan import org.matrix.android.sdk.test.fakes.givenNotEqualTo private const val A_SESSION_ID = "session_id" @@ -182,6 +184,7 @@ internal class LiveLocationAggregationProcessorTest { aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID aggregatedEntity.isActive shouldBeEqualTo true + aggregatedEntity.startOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS aggregatedEntity.lastLocationContent shouldBeEqualTo null previousEntities.forEach { entity -> @@ -199,9 +202,10 @@ internal class LiveLocationAggregationProcessorTest { age = 123, replacesState = AN_EVENT_ID ) + val stateEventId = "state-event-id" val event = Event( senderId = A_SENDER_ID, - eventId = "", + eventId = stateEventId, unsignedData = unsignedData ) val beaconInfo = MessageBeaconInfoContent( @@ -237,6 +241,7 @@ internal class LiveLocationAggregationProcessorTest { aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID aggregatedEntity.isActive shouldBeEqualTo false + aggregatedEntity.relatedEventIds shouldContain stateEventId aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS aggregatedEntity.lastLocationContent shouldBeEqualTo null previousEntities.forEach { entity -> @@ -324,7 +329,7 @@ internal class LiveLocationAggregationProcessorTest { val lastBeaconLocationContent = MessageBeaconLocationDataContent( unstableTimestampMillis = A_TIMESTAMP ) - givenLastSummaryQueryReturns( + val aggregatedEntity = givenLastSummaryQueryReturns( eventId = AN_EVENT_ID, roomId = A_ROOM_ID, beaconLocationContent = lastBeaconLocationContent @@ -340,6 +345,7 @@ internal class LiveLocationAggregationProcessorTest { ) result shouldBeEqualTo false + aggregatedEntity.relatedEventIds shouldContain AN_EVENT_ID } @Test @@ -353,7 +359,7 @@ internal class LiveLocationAggregationProcessorTest { val lastBeaconLocationContent = MessageBeaconLocationDataContent( unstableTimestampMillis = A_TIMESTAMP - 60_000 ) - val entity = givenLastSummaryQueryReturns( + val aggregatedEntity = givenLastSummaryQueryReturns( eventId = AN_EVENT_ID, roomId = A_ROOM_ID, beaconLocationContent = lastBeaconLocationContent @@ -369,7 +375,8 @@ internal class LiveLocationAggregationProcessorTest { ) result shouldBeEqualTo true - val savedLocationData = ContentMapper.map(entity.lastLocationContent).toModel() + aggregatedEntity.relatedEventIds shouldContain AN_EVENT_ID + val savedLocationData = ContentMapper.map(aggregatedEntity.lastLocationContent).toModel() savedLocationData?.getBestTimestampMillis() shouldBeEqualTo A_TIMESTAMP savedLocationData?.getBestLocationInfo()?.geoUri shouldBeEqualTo A_GEO_URI } @@ -399,6 +406,7 @@ internal class LiveLocationAggregationProcessorTest { .givenNotEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, AN_EVENT_ID) .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, A_SENDER_ID) .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true) + .givenLessThan(LiveLocationShareAggregatedSummaryEntityFields.START_OF_LIVE_TIMESTAMP_MILLIS, A_TIMESTAMP) .givenFindAll(summaryList) return summaryList } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt index ef9bde2c49..a01f51604c 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt @@ -22,8 +22,10 @@ import androidx.lifecycle.Transformations import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every +import io.mockk.just import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.runs import io.mockk.slot import io.mockk.unmockkAll import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -52,6 +54,7 @@ private const val A_LONGITUDE = 40.0 private const val AN_UNCERTAINTY = 5.0 private const val A_TIMEOUT = 15_000L private const val A_DESCRIPTION = "description" +private const val A_REASON = "reason" @ExperimentalCoroutinesApi internal class DefaultLocationSharingServiceTest { @@ -62,6 +65,7 @@ internal class DefaultLocationSharingServiceTest { private val startLiveLocationShareTask = mockk() private val stopLiveLocationShareTask = mockk() private val checkIfExistingActiveLiveTask = mockk() + private val redactLiveLocationShareTask = mockk() private val fakeLiveLocationShareAggregatedSummaryMapper = mockk() private val defaultLocationSharingService = DefaultLocationSharingService( @@ -72,6 +76,7 @@ internal class DefaultLocationSharingServiceTest { startLiveLocationShareTask = startLiveLocationShareTask, stopLiveLocationShareTask = stopLiveLocationShareTask, checkIfExistingActiveLiveTask = checkIfExistingActiveLiveTask, + redactLiveLocationShareTask = redactLiveLocationShareTask, liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper ) @@ -209,6 +214,20 @@ internal class DefaultLocationSharingServiceTest { coVerify { stopLiveLocationShareTask.execute(expectedParams) } } + @Test + fun `live location share can be redacted`() = runTest { + coEvery { redactLiveLocationShareTask.execute(any()) } just runs + + defaultLocationSharingService.redactLiveLocationShare(beaconInfoEventId = AN_EVENT_ID, reason = A_REASON) + + val expectedParams = RedactLiveLocationShareTask.Params( + roomId = A_ROOM_ID, + beaconInfoEventId = AN_EVENT_ID, + reason = A_REASON + ) + coVerify { redactLiveLocationShareTask.execute(expectedParams) } + } + @Test fun `livedata of live summaries is correctly computed`() { val entity = LiveLocationShareAggregatedSummaryEntity() diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultRedactLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultRedactLiveLocationShareTaskTest.kt new file mode 100644 index 0000000000..b8618d1a79 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultRedactLiveLocationShareTaskTest.kt @@ -0,0 +1,126 @@ +/* + * 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.location + +import io.mockk.unmockkAll +import io.realm.RealmList +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields +import org.matrix.android.sdk.test.fakes.FakeEventSenderProcessor +import org.matrix.android.sdk.test.fakes.FakeLocalEchoEventFactory +import org.matrix.android.sdk.test.fakes.FakeRealm +import org.matrix.android.sdk.test.fakes.FakeRealmConfiguration +import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenFindFirst + +private const val A_ROOM_ID = "room-id" +private const val AN_EVENT_ID = "event-id" +private const val AN_EVENT_ID_1 = "event-id-1" +private const val AN_EVENT_ID_2 = "event-id-2" +private const val AN_EVENT_ID_3 = "event-id-3" +private const val A_REASON = "reason" + +@ExperimentalCoroutinesApi +class DefaultRedactLiveLocationShareTaskTest { + + private val fakeRealmConfiguration = FakeRealmConfiguration() + private val fakeLocalEchoEventFactory = FakeLocalEchoEventFactory() + private val fakeEventSenderProcessor = FakeEventSenderProcessor() + private val fakeRealm = FakeRealm() + + private val defaultRedactLiveLocationShareTask = DefaultRedactLiveLocationShareTask( + realmConfiguration = fakeRealmConfiguration.instance, + localEchoEventFactory = fakeLocalEchoEventFactory.instance, + eventSenderProcessor = fakeEventSenderProcessor + ) + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given parameters when redacting then post redact events and related and creates redact local echos`() = runTest { + val params = createParams() + val relatedEventIds = listOf(AN_EVENT_ID_1, AN_EVENT_ID_2, AN_EVENT_ID_3) + val aggregatedSummaryEntity = createSummary(relatedEventIds) + givenSummaryForId(AN_EVENT_ID, aggregatedSummaryEntity) + fakeRealmConfiguration.givenAwaitTransaction>(fakeRealm.instance) + val redactEvents = givenCreateRedactEventWithLocalEcho(relatedEventIds + AN_EVENT_ID) + givenPostRedaction(redactEvents) + + defaultRedactLiveLocationShareTask.execute(params) + + verifyCreateRedactEventForEventIds(relatedEventIds + AN_EVENT_ID) + verifyCreateLocalEchoForEvents(redactEvents) + } + + private fun createParams() = RedactLiveLocationShareTask.Params( + roomId = A_ROOM_ID, + beaconInfoEventId = AN_EVENT_ID, + reason = A_REASON + ) + + private fun createSummary(relatedEventIds: List): LiveLocationShareAggregatedSummaryEntity { + return LiveLocationShareAggregatedSummaryEntity( + eventId = AN_EVENT_ID, + relatedEventIds = RealmList(*relatedEventIds.toTypedArray()), + ) + } + + private fun givenSummaryForId(eventId: String, aggregatedSummaryEntity: LiveLocationShareAggregatedSummaryEntity) { + fakeRealm.givenWhere() + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId) + .givenFindFirst(aggregatedSummaryEntity) + } + + private fun givenCreateRedactEventWithLocalEcho(eventIds: List): List { + return eventIds.map { eventId -> + fakeLocalEchoEventFactory.givenCreateRedactEvent( + eventId = eventId, + withLocalEcho = true + ) + } + } + + private fun givenPostRedaction(redactEvents: List) { + redactEvents.forEach { + fakeEventSenderProcessor.givenPostRedaction(event = it, reason = A_REASON) + } + } + + private fun verifyCreateRedactEventForEventIds(eventIds: List) { + eventIds.forEach { eventId -> + fakeLocalEchoEventFactory.verifyCreateRedactEvent( + roomId = A_ROOM_ID, + eventId = eventId, + reason = A_REASON + ) + } + } + + private fun verifyCreateLocalEchoForEvents(events: List) { + events.forEach { redactionEvent -> + fakeLocalEchoEventFactory.verifyCreateLocalEcho(redactionEvent) + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt new file mode 100644 index 0000000000..24d9c30039 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt @@ -0,0 +1,106 @@ +/* + * 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.location + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe +import org.junit.Test +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.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields +import org.matrix.android.sdk.test.fakes.FakeRealm +import org.matrix.android.sdk.test.fakes.givenDelete +import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenFindFirst + +private const val AN_EVENT_ID = "event-id" +private const val A_REDACTED_EVENT_ID = "redacted-event-id" + +@ExperimentalCoroutinesApi +class LiveLocationShareRedactionEventProcessorTest { + + private val liveLocationShareRedactionEventProcessor = LiveLocationShareRedactionEventProcessor() + private val fakeRealm = FakeRealm() + + @Test + fun `given an event when checking if it should be processed then only event of type REDACTED is processed`() { + val eventId = AN_EVENT_ID + val eventType = EventType.REDACTION + val insertType = EventInsertType.INCREMENTAL_SYNC + + val result = liveLocationShareRedactionEventProcessor.shouldProcess( + eventId = eventId, + eventType = eventType, + insertType = insertType + ) + + result shouldBe true + } + + @Test + fun `given an event when checking if it should be processed then local echo is not processed`() { + val eventId = AN_EVENT_ID + val eventType = EventType.REDACTION + val insertType = EventInsertType.LOCAL_ECHO + + val result = liveLocationShareRedactionEventProcessor.shouldProcess( + eventId = eventId, + eventType = eventType, + insertType = insertType + ) + + result shouldBe false + } + + @Test + fun `given a redacted live location share event when processing it then related summaries are deleted from database`() = runTest { + val event = Event(eventId = AN_EVENT_ID, redacts = A_REDACTED_EVENT_ID) + val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.first()) + fakeRealm.givenWhere() + .givenEqualTo(EventEntityFields.EVENT_ID, A_REDACTED_EVENT_ID) + .givenFindFirst(redactedEventEntity) + val liveSummary = mockk() + every { liveSummary.eventId } returns A_REDACTED_EVENT_ID + liveSummary.givenDelete() + fakeRealm.givenWhere() + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, A_REDACTED_EVENT_ID) + .givenFindFirst(liveSummary) + val annotationsSummary = mockk() + every { annotationsSummary.eventId } returns A_REDACTED_EVENT_ID + annotationsSummary.givenDelete() + fakeRealm.givenWhere() + .givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, A_REDACTED_EVENT_ID) + .givenFindFirst(annotationsSummary) + + liveLocationShareRedactionEventProcessor.process(fakeRealm.instance, event = event) + + verify { + liveSummary.deleteFromRealm() + annotationsSummary.deleteFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt index fbdcf5bfd7..db04b8b8cb 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt @@ -27,4 +27,8 @@ internal class FakeEventSenderProcessor : EventSenderProcessor by mockk() { fun givenPostEventReturns(event: Event, cancelable: Cancelable) { every { postEvent(event) } returns cancelable } + + fun givenPostRedaction(event: Event, reason: String?) { + every { postRedaction(event, reason) } returns mockk() + } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt index 50ec85f14a..f484e32149 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt @@ -46,24 +46,6 @@ internal class FakeLocalEchoEventFactory { return event } - fun givenCreateLiveLocationEvent(withLocalEcho: Boolean): Event { - val event = Event() - every { - instance.createLiveLocationEvent( - beaconInfoEventId = any(), - roomId = any(), - latitude = any(), - longitude = any(), - uncertainty = any() - ) - } returns event - - if (withLocalEcho) { - every { instance.createLocalEcho(event) } just runs - } - return event - } - fun verifyCreateStaticLocationEvent( roomId: String, latitude: Double, @@ -82,6 +64,24 @@ internal class FakeLocalEchoEventFactory { } } + fun givenCreateLiveLocationEvent(withLocalEcho: Boolean): Event { + val event = Event() + every { + instance.createLiveLocationEvent( + beaconInfoEventId = any(), + roomId = any(), + latitude = any(), + longitude = any(), + uncertainty = any() + ) + } returns event + + if (withLocalEcho) { + every { instance.createLocalEcho(event) } just runs + } + return event + } + fun verifyCreateLiveLocationEvent( roomId: String, beaconInfoEventId: String, @@ -100,6 +100,36 @@ internal class FakeLocalEchoEventFactory { } } + fun givenCreateRedactEvent(eventId: String, withLocalEcho: Boolean): Event { + val event = Event() + every { + instance.createRedactEvent( + roomId = any(), + eventId = eventId, + reason = any() + ) + } returns event + + if (withLocalEcho) { + every { instance.createLocalEcho(event) } just runs + } + return event + } + + fun verifyCreateRedactEvent( + roomId: String, + eventId: String, + reason: String? + ) { + verify { + instance.createRedactEvent( + roomId = roomId, + eventId = eventId, + reason = reason + ) + } + } + fun verifyCreateLocalEcho(event: Event) { verify { instance.createLocalEcho(event) } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt index 0ebff87278..afdcf111f8 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt @@ -18,10 +18,13 @@ package org.matrix.android.sdk.test.fakes import io.mockk.MockKVerificationScope import io.mockk.every +import io.mockk.just import io.mockk.mockk +import io.mockk.runs import io.mockk.verify import io.realm.Realm import io.realm.RealmModel +import io.realm.RealmObject import io.realm.RealmQuery import io.realm.RealmResults import io.realm.kotlin.where @@ -97,3 +100,18 @@ inline fun RealmQuery.givenIsNotNull( every { isNotNull(fieldName) } returns this return this } + +inline fun RealmQuery.givenLessThan( + fieldName: String, + value: Long +): RealmQuery { + every { lessThan(fieldName, value) } returns this + return this +} + +/** + * Should be called on a mocked RealmObject and not on a real RealmObject so that the underlying final method is mocked. + */ +fun RealmObject.givenDelete() { + every { deleteFromRealm() } just runs +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt new file mode 100644 index 0000000000..15a9823c79 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt @@ -0,0 +1,41 @@ +/* + * 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.test.fakes + +import io.mockk.coEvery +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.slot +import io.realm.Realm +import io.realm.RealmConfiguration +import org.matrix.android.sdk.internal.database.awaitTransaction + +internal class FakeRealmConfiguration { + + init { + mockkStatic("org.matrix.android.sdk.internal.database.AsyncTransactionKt") + } + + val instance = mockk() + + fun givenAwaitTransaction(realm: Realm) { + val transaction = slot T>() + coEvery { awaitTransaction(instance, capture(transaction)) } coAnswers { + secondArg T>().invoke(realm) + } + } +} diff --git a/vector/build.gradle b/vector/build.gradle index aba53bf89c..e4313770c4 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -437,7 +437,7 @@ dependencies { implementation 'com.facebook.stetho:stetho:1.6.0' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.51' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.52' // FlowBinding implementation libs.github.flowBinding diff --git a/vector/src/main/java/im/vector/app/features/analytics/extensions/SignUpExt.kt b/vector/src/main/java/im/vector/app/features/analytics/extensions/SignUpExt.kt index e63aafbfc4..d71e52de8c 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/extensions/SignUpExt.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/extensions/SignUpExt.kt @@ -21,11 +21,11 @@ import im.vector.app.features.onboarding.AuthenticationDescription fun AuthenticationDescription.AuthenticationType.toAnalyticsType() = when (this) { AuthenticationDescription.AuthenticationType.Password -> Signup.AuthenticationType.Password - AuthenticationDescription.AuthenticationType.Apple -> Signup.AuthenticationType.Apple + AuthenticationDescription.AuthenticationType.Apple -> Signup.AuthenticationType.Apple AuthenticationDescription.AuthenticationType.Facebook -> Signup.AuthenticationType.Facebook AuthenticationDescription.AuthenticationType.GitHub -> Signup.AuthenticationType.GitHub AuthenticationDescription.AuthenticationType.GitLab -> Signup.AuthenticationType.GitLab AuthenticationDescription.AuthenticationType.Google -> Signup.AuthenticationType.Google - AuthenticationDescription.AuthenticationType.SSO -> Signup.AuthenticationType.SSO - AuthenticationDescription.AuthenticationType.Other -> Signup.AuthenticationType.Other + AuthenticationDescription.AuthenticationType.SSO -> Signup.AuthenticationType.SSO + AuthenticationDescription.AuthenticationType.Other -> Signup.AuthenticationType.Other } diff --git a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index 4b32f3307f..f4ffbb826a 100644 --- a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt @@ -70,7 +70,7 @@ class AvatarRenderer @Inject constructor( render( GlideApp.with(imageView), matrixItem, - DrawableImageViewTarget(imageView) + DrawableImageViewTarget(imageView), ) } @@ -103,7 +103,7 @@ class AvatarRenderer @Inject constructor( render( glideRequests, matrixItem, - DrawableImageViewTarget(imageView) + DrawableImageViewTarget(imageView), ) } @@ -123,7 +123,7 @@ class AvatarRenderer @Inject constructor( val matrixItem = MatrixItem.UserItem( // Need an id starting with @ id = "@${mappedContact.displayName}", - displayName = mappedContact.displayName + displayName = mappedContact.displayName, ) val placeholder = getPlaceholderDrawable(matrixItem) @@ -140,7 +140,7 @@ class AvatarRenderer @Inject constructor( val matrixItem = MatrixItem.UserItem( // Need an id starting with @ id = profileInfo.matrixId, - displayName = profileInfo.displayName + displayName = profileInfo.displayName, ) val placeholder = getPlaceholderDrawable(matrixItem) @@ -215,7 +215,7 @@ class AvatarRenderer @Inject constructor( .bold() .endConfig() .buildRect(matrixItem.firstLetterOfDisplayName(), avatarColor) - .toBitmap(width = iconSize, height = iconSize) + .toBitmap(width = iconSize, height = iconSize), ) } } @@ -231,7 +231,7 @@ class AvatarRenderer @Inject constructor( addPlaceholder: Boolean ) { val transformations = mutableListOf>( - BlurTransformation(20, sampling) + BlurTransformation(20, sampling), ) if (colorFilter != null) { transformations.add(ColorFilterTransformation(colorFilter)) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 9d59ba1574..63808ba9fd 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -219,7 +219,7 @@ class HomeActivity : is HomeActivitySharedAction.ShowSpaceSettings -> showSpaceSettings(sharedAction.spaceId) is HomeActivitySharedAction.OpenSpaceInvite -> openSpaceInvite(sharedAction.spaceId) HomeActivitySharedAction.SendSpaceFeedBack -> bugReporter.openBugReportScreen(this, ReportType.SPACE_BETA_FEEDBACK) - HomeActivitySharedAction.OnCloseSpace -> onCloseSpace() + HomeActivitySharedAction.OnCloseSpace -> onCloseSpace() } } .launchIn(lifecycleScope) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 4d57647a1d..dcfee2d919 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -84,6 +84,4 @@ sealed class RoomDetailViewEvents : VectorViewEvents { data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents() object StopChatEffects : RoomDetailViewEvents() object RoomReplacementStarted : RoomDetailViewEvents() - - data class ChangeLocationIndicator(val isVisible: Boolean) : RoomDetailViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 3ec7166739..8500d1ed96 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -75,7 +75,8 @@ data class RoomDetailViewState( val switchToParentSpace: Boolean = false, val rootThreadEventId: String? = null, val threadNotificationBadgeState: ThreadNotificationBadgeState = ThreadNotificationBadgeState(), - val typingUsers: List? = null + val typingUsers: List? = null, + val isSharingLiveLocation: Boolean = false, ) : MavericksState { constructor(args: TimelineArgs) : this( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index e86a7fe227..31c1004ef9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -498,7 +498,6 @@ class TimelineFragment @Inject constructor( RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() - is RoomDetailViewEvents.ChangeLocationIndicator -> handleChangeLocationIndicator(it) } } @@ -663,10 +662,6 @@ class TimelineFragment @Inject constructor( ) } - private fun handleChangeLocationIndicator(event: RoomDetailViewEvents.ChangeLocationIndicator) { - views.locationLiveStatusIndicator.isVisible = event.isVisible - } - private fun displayErrorMessage(error: RoomDetailViewEvents.Failure) { if (error.showInDialog) displayErrorDialog(error.throwable) else showErrorInSnackbar(error.throwable) } @@ -1686,6 +1681,11 @@ class TimelineFragment @Inject constructor( } else if (mainState.asyncInviter.complete) { vectorBaseActivity.finish() } + updateLiveLocationIndicator(mainState.isSharingLiveLocation) + } + + private fun updateLiveLocationIndicator(isSharingLiveLocation: Boolean) { + views.locationLiveStatusIndicator.isVisible = isSharingLiveLocation } private fun FragmentTimelineBinding.hideComposerViews() { @@ -1706,7 +1706,7 @@ class TimelineFragment @Inject constructor( private fun renderToolbar(roomSummary: RoomSummary?) { when { - isLocalRoom() -> { + isLocalRoom() -> { views.includeRoomToolbar.roomToolbarContentView.isVisible = false views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false setupToolbar(views.roomToolbar) @@ -1724,7 +1724,7 @@ class TimelineFragment @Inject constructor( } views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title) } - else -> { + else -> { views.includeRoomToolbar.roomToolbarContentView.isVisible = true views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false if (roomSummary == null) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index d562609e42..0d03d21ad3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -48,6 +48,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider +import im.vector.app.features.home.room.detail.location.RedactLiveLocationShareEventUseCase import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever @@ -105,6 +106,7 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.read.ReadService 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.isLiveLocation import org.matrix.android.sdk.api.session.sync.SyncRequestState import org.matrix.android.sdk.api.session.threads.ThreadNotificationBadgeState import org.matrix.android.sdk.api.session.threads.ThreadNotificationState @@ -135,6 +137,7 @@ class TimelineViewModel @AssistedInject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val locationSharingServiceConnection: LocationSharingServiceConnection, private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase, + private val redactLiveLocationShareEventUseCase: RedactLiveLocationShareEventUseCase, timelineFactory: TimelineFactory, spaceStateHandler: SpaceStateHandler, ) : VectorViewModel(initialState), @@ -770,7 +773,13 @@ class TimelineViewModel @AssistedInject constructor( private fun handleRedactEvent(action: RoomDetailAction.RedactAction) { val event = room.getTimelineEvent(action.targetEventId) ?: return - room.sendService().redactEvent(event.root, action.reason) + if (event.isLiveLocation()) { + viewModelScope.launch { + redactLiveLocationShareEventUseCase.execute(event.root, room, action.reason) + } + } else { + room.sendService().redactEvent(event.root, action.reason) + } } private fun handleUndoReact(action: RoomDetailAction.UndoReaction) { @@ -1294,12 +1303,12 @@ class TimelineViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds)) } - override fun onLocationServiceRunning() { - _viewEvents.post(RoomDetailViewEvents.ChangeLocationIndicator(isVisible = true)) + override fun onLocationServiceRunning(roomIds: Set) { + setState { copy(isSharingLiveLocation = roomId in roomIds) } } override fun onLocationServiceStopped() { - _viewEvents.post(RoomDetailViewEvents.ChangeLocationIndicator(isVisible = false)) + setState { copy(isSharingLiveLocation = false) } // Bind again in case user decides to share live location without leaving the room locationSharingServiceConnection.bind(this) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/location/RedactLiveLocationShareEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/location/RedactLiveLocationShareEventUseCase.kt new file mode 100644 index 0000000000..ba91000b40 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/location/RedactLiveLocationShareEventUseCase.kt @@ -0,0 +1,30 @@ +/* + * 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.home.room.detail.location + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.Room +import javax.inject.Inject + +class RedactLiveLocationShareEventUseCase @Inject constructor() { + + suspend fun execute(event: Event, room: Room, reason: String?) { + event.eventId + ?.takeUnless { it.isEmpty() } + ?.let { room.locationSharingService().redactLiveLocationShare(it, reason) } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCase.kt new file mode 100644 index 0000000000..3bc3a5e351 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCase.kt @@ -0,0 +1,37 @@ +/* + * 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.home.room.detail.timeline.action + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import javax.inject.Inject + +class CheckIfCanRedactEventUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder +) { + + fun execute(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { + // Only some event types are supported for the moment + val canRedactEventTypes = listOf(EventType.MESSAGE, EventType.STICKER) + + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO + + return event.root.getClearType() in canRedactEventTypes && + // Message sent by the current user can always be redacted, else check permission for messages sent by other users + (event.root.senderId == activeSessionHolder.getActiveSession().myUserId || actionPermissions.canRedact) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 30786dc77a..3dfb6744e0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -82,6 +82,7 @@ class MessageActionsViewModel @AssistedInject constructor( private val pillsPostProcessorFactory: PillsPostProcessor.Factory, private val vectorPreferences: VectorPreferences, private val checkIfCanReplyEventUseCase: CheckIfCanReplyEventUseCase, + private val checkIfCanRedactEventUseCase: CheckIfCanRedactEventUseCase, ) : VectorViewModel(initialState) { private val informationData = initialState.informationData @@ -518,12 +519,7 @@ class MessageActionsViewModel @AssistedInject constructor( } private fun canRedact(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { - // Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment - if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START) return false - // Message sent by the current user can always be redacted - if (event.root.senderId == session.myUserId) return true - // Check permission for messages sent by other users - return actionPermissions.canRedact + return checkIfCanRedactEventUseCase.execute(event, actionPermissions) } private fun canRetry(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt index 5f32696334..69b4f6e039 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -63,10 +63,10 @@ class EncryptionItemFactory @Inject constructor( isDirect && RoomLocalEcho.isLocalEchoId(event.root.roomId.orEmpty()) -> { R.string.direct_room_encryption_enabled_tile_description_future } - isDirect -> { + isDirect -> { R.string.direct_room_encryption_enabled_tile_description } - else -> { + else -> { R.string.encryption_enabled_tile_description } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 800fc27e77..2149857ec2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -24,7 +24,6 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper -import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem @@ -35,6 +34,7 @@ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovement import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.query.QueryStringValue +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.content.EncryptionEventContent import org.matrix.android.sdk.api.session.events.model.toModel @@ -53,6 +53,7 @@ class MergedHeaderItemFactory @Inject constructor( private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper ) { + private val mergeableEventTypes = listOf(EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_SERVER_ACL) private val collapsedEventIds = linkedSetOf() private val mergeItemCollapseStates = HashMap() @@ -78,19 +79,65 @@ class MergedHeaderItemFactory @Inject constructor( callback: TimelineEventController.Callback?, requestModelBuild: () -> Unit ): BasedMergedItem<*>? { - return if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE && - event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel()?.creator)) { - // It's the first item before room.create - // Collapse all room configuration events - buildRoomCreationMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) - } else if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) { - null - } else { - buildMembershipEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) + return when { + isStartOfRoomCreationSummary(event, nextEvent) -> + buildRoomCreationMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) + isStartOfSameTypeEventsSummary(event, nextEvent, addDaySeparator) -> + buildSameTypeEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) + isStartOfRedactedEventsSummary(event, items, currentPosition, addDaySeparator) -> + buildRedactedEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) + else -> null } } - private fun buildMembershipEventsMergedSummary( + /** + * @param event the main timeline event + * @param nextEvent is an older event than event + */ + private fun isStartOfRoomCreationSummary( + event: TimelineEvent, + nextEvent: TimelineEvent?, + ): Boolean { + // It's the first item before room.create + // Collapse all room configuration events + return nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE && + event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel()?.creator) + } + + /** + * @param event the main timeline event + * @param nextEvent is an older event than event + * @param addDaySeparator true to add a day separator + */ + private fun isStartOfSameTypeEventsSummary( + event: TimelineEvent, + nextEvent: TimelineEvent?, + addDaySeparator: Boolean, + ): Boolean { + return event.root.getClearType() in mergeableEventTypes && + (nextEvent?.root?.getClearType() != event.root.getClearType() || addDaySeparator) + } + + /** + * @param event the main timeline event + * @param items all known items, sorted from newer event to oldest event + * @param currentPosition the current position + * @param addDaySeparator true to add a day separator + */ + private fun isStartOfRedactedEventsSummary( + event: TimelineEvent, + items: List, + currentPosition: Int, + addDaySeparator: Boolean, + ): Boolean { + val nextNonRedactionEvent = items + .subList(fromIndex = currentPosition + 1, toIndex = items.size) + .find { it.root.getClearType() != EventType.REDACTION } + return event.root.isRedacted() && + (!nextNonRedactionEvent?.root?.isRedacted().orFalse() || addDaySeparator) + } + + private fun buildSameTypeEventsMergedSummary( currentPosition: Int, items: List, partialState: TimelineEventController.PartialState, @@ -102,11 +149,42 @@ class MergedHeaderItemFactory @Inject constructor( val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents( items, currentPosition, - 2, + MIN_NUMBER_OF_MERGED_EVENTS, eventIdToHighlight, partialState.rootThreadEventId, partialState.isFromThreadTimeline() ) + return buildSimilarEventsMergedSummary(mergedEvents, partialState, event, eventIdToHighlight, requestModelBuild, callback) + } + + private fun buildRedactedEventsMergedSummary( + currentPosition: Int, + items: List, + partialState: TimelineEventController.PartialState, + event: TimelineEvent, + eventIdToHighlight: String?, + requestModelBuild: () -> Unit, + callback: TimelineEventController.Callback? + ): MergedSimilarEventsItem_? { + val mergedEvents = timelineEventVisibilityHelper.prevRedactedEvents( + items, + currentPosition, + MIN_NUMBER_OF_MERGED_EVENTS, + eventIdToHighlight, + partialState.rootThreadEventId, + partialState.isFromThreadTimeline() + ) + return buildSimilarEventsMergedSummary(mergedEvents, partialState, event, eventIdToHighlight, requestModelBuild, callback) + } + + private fun buildSimilarEventsMergedSummary( + mergedEvents: List, + partialState: TimelineEventController.PartialState, + event: TimelineEvent, + eventIdToHighlight: String?, + requestModelBuild: () -> Unit, + callback: TimelineEventController.Callback? + ): MergedSimilarEventsItem_? { return if (mergedEvents.isEmpty()) { null } else { @@ -127,7 +205,7 @@ class MergedHeaderItemFactory @Inject constructor( ) mergedData.add(data) } - val mergedEventIds = mergedEvents.map { it.localId } + val mergedEventIds = mergedEvents.map { it.localId }.toSet() // We try to find if one of the item id were used as mergeItemCollapseStates key // => handle case where paginating from mergeable events and we get more val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() @@ -140,12 +218,7 @@ class MergedHeaderItemFactory @Inject constructor( collapsedEventIds.removeAll(mergedEventIds) } val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } - val summaryTitleResId = when (event.root.getClearType()) { - EventType.STATE_ROOM_MEMBER -> R.plurals.membership_changes - EventType.STATE_ROOM_SERVER_ACL -> R.plurals.notice_room_server_acl_changes - else -> null - } - summaryTitleResId?.let { summaryTitle -> + getSummaryTitleResId(event.root)?.let { summaryTitle -> val attributes = MergedSimilarEventsItem.Attributes( summaryTitleResId = summaryTitle, isCollapsed = isCollapsed, @@ -168,6 +241,16 @@ class MergedHeaderItemFactory @Inject constructor( } } + private fun getSummaryTitleResId(event: Event): Int? { + val type = event.getClearType() + return when { + type == EventType.STATE_ROOM_MEMBER -> R.plurals.membership_changes + type == EventType.STATE_ROOM_SERVER_ACL -> R.plurals.notice_room_server_acl_changes + event.isRedacted() -> R.plurals.room_removed_messages + else -> null + } + } + private fun buildRoomCreationMergedSummary( currentPosition: Int, items: List, @@ -191,7 +274,7 @@ class MergedHeaderItemFactory @Inject constructor( tmpPos-- prevEvent = items.getOrNull(tmpPos) } - return if (mergedEvents.size > 2) { + return if (mergedEvents.size > MIN_NUMBER_OF_MERGED_EVENTS) { var highlighted = false val mergedData = ArrayList(mergedEvents.size) mergedEvents.reversed() @@ -264,4 +347,8 @@ class MergedHeaderItemFactory @Inject constructor( fun isCollapsed(localId: Long): Boolean { return collapsedEventIds.contains(localId) } + + companion object { + private const val MIN_NUMBER_OF_MERGED_EVENTS = 2 + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 97ae3b634e..6c5a66d39d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -113,8 +113,14 @@ class TimelineItemFactory @Inject constructor( EventType.CALL_NEGOTIATE, EventType.REACTION, in EventType.POLL_RESPONSE, - in EventType.POLL_END, - in EventType.BEACON_LOCATION_DATA -> noticeItemFactory.create(params) + in EventType.POLL_END -> noticeItemFactory.create(params) + in EventType.BEACON_LOCATION_DATA -> { + if (event.root.isRedacted()) { + messageItemFactory.create(params) + } else { + noticeItemFactory.create(params) + } + } // Calls EventType.CALL_INVITE, EventType.CALL_HANGUP, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 85ffd7295c..23db2a721c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -51,12 +51,7 @@ object TimelineDisplayableEvents { EventType.STATE_ROOM_JOIN_RULES, EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_CANCEL, - ) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO -} - -fun TimelineEvent.canBeMerged(): Boolean { - return root.getClearType() == EventType.STATE_ROOM_MEMBER || - root.getClearType() == EventType.STATE_ROOM_SERVER_ACL + ) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO + EventType.BEACON_LOCATION_DATA } fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt index 488aebde13..e6765bf35a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.helper import im.vector.app.core.extensions.localDateTime import im.vector.app.core.resources.UserPreferencesProvider +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.RelationType import org.matrix.android.sdk.api.session.events.model.getRelationContent @@ -30,25 +31,38 @@ import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject -class TimelineEventVisibilityHelper @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) { +class TimelineEventVisibilityHelper @Inject constructor( + private val userPreferencesProvider: UserPreferencesProvider, +) { + + private interface PredicateToStopSearch { + /** + * Indicate whether a search on events should stop by comparing 2 given successive events. + * @param oldEvent the oldest event between the 2 events to compare + * @param newEvent the more recent event between the 2 events to compare + */ + fun shouldStopSearch(oldEvent: Event, newEvent: Event): Boolean + } /** - * @param timelineEvents the events to search in + * @param timelineEvents the events to search in, sorted from oldest event to newer event * @param index the index to start computing (inclusive) * @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list * @param eventIdToHighlight used to compute visibility * @param rootThreadEventId the root thread event id if in a thread timeline * @param isFromThreadTimeline true if the timeline is a thread + * @param predicateToStop events are taken until this condition is met * - * @return a list of timeline events which have sequentially the same type following the next direction. + * @return a list of timeline events which meet sequentially the same criteria following the next direction. */ - private fun nextSameTypeEvents( + private fun nextEventsUntil( timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?, rootThreadEventId: String?, - isFromThreadTimeline: Boolean + isFromThreadTimeline: Boolean, + predicateToStop: PredicateToStopSearch ): List { if (index >= timelineEvents.size - 1) { return emptyList() @@ -65,13 +79,15 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen } else { nextSubList.subList(0, indexOfNextDay) } - val indexOfFirstDifferentEventType = nextSameDayEvents.indexOfFirst { it.root.getClearType() != timelineEvent.root.getClearType() } - val sameTypeEvents = if (indexOfFirstDifferentEventType == -1) { + val indexOfFirstDifferentEvent = nextSameDayEvents.indexOfFirst { + predicateToStop.shouldStopSearch(oldEvent = timelineEvent.root, newEvent = it.root) + } + val similarEvents = if (indexOfFirstDifferentEvent == -1) { nextSameDayEvents } else { - nextSameDayEvents.subList(0, indexOfFirstDifferentEventType) + nextSameDayEvents.subList(0, indexOfFirstDifferentEvent) } - val filteredSameTypeEvents = sameTypeEvents.filter { + val filteredSimilarEvents = similarEvents.filter { shouldShowEvent( timelineEvent = it, highlightedEventId = eventIdToHighlight, @@ -79,14 +95,11 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen rootThreadEventId = rootThreadEventId ) } - if (filteredSameTypeEvents.size < minSize) { - return emptyList() - } - return filteredSameTypeEvents + return if (filteredSimilarEvents.size < minSize) emptyList() else filteredSimilarEvents } /** - * @param timelineEvents the events to search in + * @param timelineEvents the events to search in, sorted from newer event to oldest event * @param index the index to start computing (inclusive) * @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list * @param eventIdToHighlight used to compute visibility @@ -107,7 +120,44 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen return prevSub .reversed() .let { - nextSameTypeEvents(it, 0, minSize, eventIdToHighlight, rootThreadEventId, isFromThreadTimeline) + nextEventsUntil(it, 0, minSize, eventIdToHighlight, rootThreadEventId, isFromThreadTimeline, object : PredicateToStopSearch { + override fun shouldStopSearch(oldEvent: Event, newEvent: Event): Boolean { + return oldEvent.getClearType() != newEvent.getClearType() + } + }) + } + } + + /** + * @param timelineEvents the events to search in, sorted from newer event to oldest event + * @param index the index to start computing (inclusive) + * @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list + * @param eventIdToHighlight used to compute visibility + * @param rootThreadEventId the root thread eventId + * @param isFromThreadTimeline true if the timeline is a thread + * + * @return a list of timeline events which are all redacted following the prev direction. + */ + fun prevRedactedEvents( + timelineEvents: List, + index: Int, + minSize: Int, + eventIdToHighlight: String?, + rootThreadEventId: String?, + isFromThreadTimeline: Boolean + ): List { + val prevSub = timelineEvents + .subList(0, index + 1) + // Ensure to not take the REDACTION events into account + .filter { it.root.getClearType() != EventType.REDACTION } + return prevSub + .reversed() + .let { + nextEventsUntil(it, 0, minSize, eventIdToHighlight, rootThreadEventId, isFromThreadTimeline, object : PredicateToStopSearch { + override fun shouldStopSearch(oldEvent: Event, newEvent: Event): Boolean { + return oldEvent.isRedacted() && !newEvent.isRedacted() + } + }) } } @@ -191,6 +241,10 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen } else root.eventId != rootThreadEventId } + if (root.getClearType() in EventType.BEACON_LOCATION_DATA) { + return !root.isRedacted() + } + return false } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/UpgradeRoomViewModelTask.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/UpgradeRoomViewModelTask.kt index 2f91b9a35a..1f0404d659 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/UpgradeRoomViewModelTask.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/UpgradeRoomViewModelTask.kt @@ -84,8 +84,6 @@ class UpgradeRoomViewModelTask @Inject constructor( // autoJoin = currentInfo.autoJoin ?: false, suggested = currentInfo.suggested ) - - parentSpace.removeChildren(params.roomId) } } } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt index 412b28862a..725f23cddd 100644 --- a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt @@ -85,27 +85,27 @@ class EventHtmlRenderer @Inject constructor( } else { builder } - .usePlugin( - MarkwonInlineParserPlugin.create( - /* Configuring the Markwon inline formatting processor. - * Default settings are all Markdown features. Turn those off, only using the - * inline HTML processor and HTML entities processor. - */ - MarkwonInlineParser.factoryBuilderNoDefaults() - .addInlineProcessor(HtmlInlineProcessor()) // use inline HTML processor - .addInlineProcessor(EntityInlineProcessor()) // use HTML entities processor + .usePlugin( + MarkwonInlineParserPlugin.create( + /* Configuring the Markwon inline formatting processor. + * Default settings are all Markdown features. Turn those off, only using the + * inline HTML processor and HTML entities processor. + */ + MarkwonInlineParser.factoryBuilderNoDefaults() + .addInlineProcessor(HtmlInlineProcessor()) // use inline HTML processor + .addInlineProcessor(EntityInlineProcessor()) // use HTML entities processor + ) ) - ) - .usePlugin(object : AbstractMarkwonPlugin() { - override fun configureParser(builder: Parser.Builder) { - /* Configuring the Markwon block formatting processor. - * Default settings are all Markdown blocks. Turn those off. - */ - builder.enabledBlockTypes(kotlin.collections.emptySet()) - } - }) - .textSetter(PrecomputedFutureTextSetterCompat.create()) - .build() + .usePlugin(object : AbstractMarkwonPlugin() { + override fun configureParser(builder: Parser.Builder) { + /* Configuring the Markwon block formatting processor. + * Default settings are all Markdown blocks. Turn those off. + */ + builder.enabledBlockTypes(kotlin.collections.emptySet()) + } + }) + .textSetter(PrecomputedFutureTextSetterCompat.create()) + .build() val plugins: List = markwon.plugins diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt index d86d97e6b0..264b1f0e0b 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt @@ -23,5 +23,6 @@ sealed class LocationSharingAction : VectorViewModelAction { data class PinnedLocationSharing(val locationData: LocationData?) : LocationSharingAction() data class LocationTargetChange(val locationData: LocationData) : LocationSharingAction() object ZoomToUserLocation : LocationSharingAction() + object LiveLocationSharingRequested : LocationSharingAction() data class StartLiveLocationSharing(val durationMillis: Long) : LocationSharingAction() } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt index 69ffc0e89e..dd18658059 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt @@ -26,9 +26,12 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.services.VectorAndroidService import im.vector.app.features.location.live.GetLiveLocationShareSummaryUseCase import im.vector.app.features.notifications.NotificationUtils +import im.vector.app.features.redaction.CheckIfEventIsRedactedUseCase import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn @@ -55,6 +58,7 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca @Inject lateinit var locationTracker: LocationTracker @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var getLiveLocationShareSummaryUseCase: GetLiveLocationShareSummaryUseCase + @Inject lateinit var checkIfEventIsRedactedUseCase: CheckIfEventIsRedactedUseCase private val binder = LocalBinder() @@ -66,6 +70,9 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca private val jobs = mutableListOf() private var startInProgress = false + private val _roomIdsOfActiveLives = MutableSharedFlow>(replay = 1) + val roomIdsOfActiveLives = _roomIdsOfActiveLives.asSharedFlow() + override fun onCreate() { super.onCreate() Timber.i("onCreate") @@ -193,24 +200,30 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca private fun addRoomArgs(beaconEventId: String, roomArgs: RoomArgs) { Timber.i("adding roomArgs for beaconEventId: $beaconEventId") roomArgsMap[beaconEventId] = roomArgs + launchWithActiveSession { _roomIdsOfActiveLives.emit(getRoomIdsOfActiveLives()) } } private fun removeRoomArgs(beaconEventId: String) { Timber.i("removing roomArgs for beaconEventId: $beaconEventId") roomArgsMap.remove(beaconEventId) + launchWithActiveSession { _roomIdsOfActiveLives.emit(getRoomIdsOfActiveLives()) } } private fun listenForLiveSummaryChanges(roomId: String, beaconEventId: String) { launchWithActiveSession { session -> val job = getLiveLocationShareSummaryUseCase.execute(roomId, beaconEventId) - .distinctUntilChangedBy { it.isActive } - .filter { it.isActive == false } + .distinctUntilChangedBy { it?.isActive } + .filter { it?.isActive == false || (it == null && isLiveRedacted(roomId, beaconEventId)) } .onEach { stopSharingLocation(beaconEventId) } .launchIn(session.coroutineScope) jobs.add(job) } } + private suspend fun isLiveRedacted(roomId: String, beaconEventId: String): Boolean { + return checkIfEventIsRedactedUseCase.execute(roomId = roomId, eventId = beaconEventId) + } + private fun launchWithActiveSession(block: suspend CoroutineScope.(Session) -> Unit) = activeSessionHolder .getSafeActiveSession() @@ -220,6 +233,10 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca ) } + fun getRoomIdsOfActiveLives(): Set { + return roomArgsMap.map { it.value.roomId }.toSet() + } + override fun onBind(intent: Intent?): IBinder { return binder } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt index be5f0aed6f..17e53e63d1 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -104,6 +104,9 @@ class LocationSharingFragment @Inject constructor( LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError() is LocationSharingViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(it) is LocationSharingViewEvents.StartLiveLocationService -> handleStartLiveLocationService(it) + LocationSharingViewEvents.ChooseLiveLocationDuration -> handleChooseLiveLocationDuration() + LocationSharingViewEvents.ShowLabsFlagPromotion -> handleShowLabsFlagPromotion() + LocationSharingViewEvents.LiveLocationSharingNotEnoughPermission -> handleLiveLocationSharingNotEnoughPermission() } } } @@ -168,6 +171,14 @@ class LocationSharingFragment @Inject constructor( .show() } + private fun handleLiveLocationSharingNotEnoughPermission() { + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(R.string.live_location_not_enough_permission_dialog_title) + .setMessage(R.string.live_location_not_enough_permission_dialog_description) + .setPositiveButton(R.string.ok, null) + .show() + } + private fun initLocateButton() { views.mapView.locateButton.setOnClickListener { viewModel.handle(LocationSharingAction.ZoomToUserLocation) @@ -201,7 +212,7 @@ class LocationSharingFragment @Inject constructor( viewModel.handle(LocationSharingAction.CurrentUserLocationSharing) } views.shareLocationOptionsPicker.optionUserLive.debouncedClicks { - tryStartLiveLocationSharing() + viewModel.handle(LocationSharingAction.LiveLocationSharingRequested) } } @@ -212,13 +223,13 @@ class LocationSharingFragment @Inject constructor( } } - private fun tryStartLiveLocationSharing() { - if (vectorPreferences.labsEnableLiveLocation()) { - startLiveLocationSharing() - } else { - LiveLocationLabsFlagPromotionBottomSheet.newInstance() - .show(requireActivity().supportFragmentManager, "DISPLAY_LIVE_LOCATION_LABS_FLAG_PROMOTION") - } + private fun handleChooseLiveLocationDuration() { + startLiveLocationSharing() + } + + private fun handleShowLabsFlagPromotion() { + LiveLocationLabsFlagPromotionBottomSheet.newInstance() + .show(requireActivity().supportFragmentManager, "DISPLAY_LIVE_LOCATION_LABS_FLAG_PROMOTION") } private val foregroundLocationResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt index 495b780188..3be73e9fd4 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt @@ -21,17 +21,22 @@ import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.IBinder +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import javax.inject.Inject import javax.inject.Singleton @Singleton class LocationSharingServiceConnection @Inject constructor( - private val context: Context -) : ServiceConnection, - LocationSharingAndroidService.Callback { + private val context: Context, + private val activeSessionHolder: ActiveSessionHolder +) : ServiceConnection, LocationSharingAndroidService.Callback { interface Callback { - fun onLocationServiceRunning() + fun onLocationServiceRunning(roomIds: Set) fun onLocationServiceStopped() fun onLocationServiceError(error: Throwable) } @@ -44,7 +49,7 @@ class LocationSharingServiceConnection @Inject constructor( addCallback(callback) if (isBound) { - callback.onLocationServiceRunning() + callback.onLocationServiceRunning(getRoomIdsOfActiveLives()) } else { Intent(context, LocationSharingAndroidService::class.java).also { intent -> context.bindService(intent, this, 0) @@ -56,12 +61,24 @@ class LocationSharingServiceConnection @Inject constructor( removeCallback(callback) } + private fun getRoomIdsOfActiveLives(): Set { + return locationSharingAndroidService?.getRoomIdsOfActiveLives() ?: emptySet() + } + override fun onServiceConnected(className: ComponentName, binder: IBinder) { - locationSharingAndroidService = (binder as LocationSharingAndroidService.LocalBinder).getService().also { - it.callback = this + locationSharingAndroidService = (binder as LocationSharingAndroidService.LocalBinder).getService().also { service -> + service.callback = this + getActiveSessionCoroutineScope()?.let { scope -> + service.roomIdsOfActiveLives + .onEach(::onRoomIdsUpdate) + .launchIn(scope) + } } isBound = true - onCallbackActionNoArg(Callback::onLocationServiceRunning) + } + + private fun getActiveSessionCoroutineScope(): CoroutineScope? { + return activeSessionHolder.getSafeActiveSession()?.coroutineScope } override fun onServiceDisconnected(className: ComponentName) { @@ -71,6 +88,10 @@ class LocationSharingServiceConnection @Inject constructor( onCallbackActionNoArg(Callback::onLocationServiceStopped) } + private fun onRoomIdsUpdate(roomIds: Set) { + forwardRoomIdsToCallbacks(roomIds) + } + override fun onServiceError(error: Throwable) { forwardErrorToCallbacks(error) } @@ -87,6 +108,10 @@ class LocationSharingServiceConnection @Inject constructor( callbacks.toList().forEach(action) } + private fun forwardRoomIdsToCallbacks(roomIds: Set) { + callbacks.toList().forEach { it.onLocationServiceRunning(roomIds) } + } + private fun forwardErrorToCallbacks(error: Throwable) { callbacks.toList().forEach { it.onLocationServiceError(error) } } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt index 1116003e41..c9e411c8d7 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt @@ -23,4 +23,7 @@ sealed class LocationSharingViewEvents : VectorViewEvents { object LocationNotAvailableError : LocationSharingViewEvents() data class ZoomToUserLocation(val userLocation: LocationData) : LocationSharingViewEvents() data class StartLiveLocationService(val sessionId: String, val roomId: String, val durationMillis: Long) : LocationSharingViewEvents() + object ChooseLiveLocationDuration : LocationSharingViewEvents() + object ShowLabsFlagPromotion : LocationSharingViewEvents() + object LiveLocationSharingNotEnoughPermission : LocationSharingViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt index b9a2dc830c..8056b72d57 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt @@ -26,6 +26,8 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.location.domain.usecase.CompareLocationsUseCase +import im.vector.app.features.powerlevel.PowerLevelsFlowFactory +import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.lastOrNull @@ -36,8 +38,10 @@ import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getUser +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.util.toMatrixItem import timber.log.Timber @@ -52,6 +56,7 @@ class LocationSharingViewModel @AssistedInject constructor( private val locationPinProvider: LocationPinProvider, private val session: Session, private val compareLocationsUseCase: CompareLocationsUseCase, + private val vectorPreferences: VectorPreferences, ) : VectorViewModel(initialState), LocationTracker.Callback { private val room = session.getRoom(initialState.roomId)!! @@ -70,6 +75,21 @@ class LocationSharingViewModel @AssistedInject constructor( setUserItem() updatePin() compareTargetAndUserLocation() + observePowerLevelsForLiveLocationSharing() + } + + private fun observePowerLevelsForLiveLocationSharing() { + PowerLevelsFlowFactory(room).createFlow() + .distinctUntilChanged() + .setOnEach { + val powerLevelsHelper = PowerLevelsHelper(it) + val canShareLiveLocation = EventType.STATE_ROOM_BEACON_INFO + .all { beaconInfoType -> + powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, beaconInfoType) + } + + copy(canShareLiveLocation = canShareLiveLocation) + } } private fun initLocationTracking() { @@ -130,10 +150,21 @@ class LocationSharingViewModel @AssistedInject constructor( is LocationSharingAction.PinnedLocationSharing -> handlePinnedLocationSharingAction(action) is LocationSharingAction.LocationTargetChange -> handleLocationTargetChangeAction(action) LocationSharingAction.ZoomToUserLocation -> handleZoomToUserLocationAction() + LocationSharingAction.LiveLocationSharingRequested -> handleLiveLocationSharingRequestedAction() is LocationSharingAction.StartLiveLocationSharing -> handleStartLiveLocationSharingAction(action.durationMillis) } } + private fun handleLiveLocationSharingRequestedAction() = withState { state -> + if (!state.canShareLiveLocation) { + _viewEvents.post(LocationSharingViewEvents.LiveLocationSharingNotEnoughPermission) + } else if (vectorPreferences.labsEnableLiveLocation()) { + _viewEvents.post(LocationSharingViewEvents.ChooseLiveLocationDuration) + } else { + _viewEvents.post(LocationSharingViewEvents.ShowLabsFlagPromotion) + } + } + private fun handleCurrentUserLocationSharingAction() = withState { state -> shareLocation(state.lastKnownUserLocation, isUserLocation = true) } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt index 64f324bc1b..d5226eacfb 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt @@ -34,7 +34,8 @@ data class LocationSharingViewState( val userItem: MatrixItem.UserItem? = null, val areTargetAndUserLocationEqual: Boolean? = null, val lastKnownUserLocation: LocationData? = null, - val locationTargetDrawable: Drawable? = null + val locationTargetDrawable: Drawable? = null, + val canShareLiveLocation: Boolean = false, ) : MavericksState { constructor(locationSharingArgs: LocationSharingArgs) : this( diff --git a/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt index 0d8b70ccda..bc38889d7f 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt @@ -19,7 +19,7 @@ package im.vector.app.features.location.live import androidx.lifecycle.asFlow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom @@ -31,13 +31,13 @@ class GetLiveLocationShareSummaryUseCase @Inject constructor( private val session: Session, ) { - suspend fun execute(roomId: String, eventId: String): Flow = withContext(session.coroutineDispatchers.main) { + suspend fun execute(roomId: String, eventId: String): Flow = withContext(session.coroutineDispatchers.main) { Timber.d("getting flow for roomId=$roomId and eventId=$eventId") session.getRoom(roomId) ?.locationSharingService() ?.getLiveLocationShareSummary(eventId) ?.asFlow() - ?.mapNotNull { it.getOrNull() } + ?.map { it.getOrNull() } ?: emptyFlow() } } diff --git a/vector/src/main/java/im/vector/app/features/location/live/LiveLocationLabsFlagPromotionBottomSheet.kt b/vector/src/main/java/im/vector/app/features/location/live/LiveLocationLabsFlagPromotionBottomSheet.kt index cf360ec277..be8e774ad0 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/LiveLocationLabsFlagPromotionBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/LiveLocationLabsFlagPromotionBottomSheet.kt @@ -29,7 +29,7 @@ import im.vector.app.databinding.BottomSheetLiveLocationLabsFlagPromotionBinding * This should not be shown if the user already enabled the labs flag. */ class LiveLocationLabsFlagPromotionBottomSheet : - VectorBaseBottomSheetDialogFragment() { + VectorBaseBottomSheetDialogFragment() { override val showExpanded = true diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt index 15c76b083e..44d39862f9 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt @@ -87,7 +87,7 @@ class LocationLiveMapViewModel @AssistedInject constructor( } } - override fun onLocationServiceRunning() { + override fun onLocationServiceRunning(roomIds: Set) { // NOOP } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/AuthenticationDescription.kt b/vector/src/main/java/im/vector/app/features/onboarding/AuthenticationDescription.kt index 3672b8eef3..c540871ab7 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/AuthenticationDescription.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/AuthenticationDescription.kt @@ -41,12 +41,12 @@ sealed interface AuthenticationDescription : Parcelable { } fun SsoIdentityProvider?.toAuthenticationType() = when (this?.brand) { - SsoIdentityProvider.BRAND_GOOGLE -> AuthenticationType.Google - SsoIdentityProvider.BRAND_GITHUB -> AuthenticationType.GitHub - SsoIdentityProvider.BRAND_APPLE -> AuthenticationType.Apple + SsoIdentityProvider.BRAND_GOOGLE -> AuthenticationType.Google + SsoIdentityProvider.BRAND_GITHUB -> AuthenticationType.GitHub + SsoIdentityProvider.BRAND_APPLE -> AuthenticationType.Apple SsoIdentityProvider.BRAND_FACEBOOK -> AuthenticationType.Facebook - SsoIdentityProvider.BRAND_GITLAB -> AuthenticationType.GitLab - SsoIdentityProvider.BRAND_TWITTER -> AuthenticationType.SSO - null -> AuthenticationType.SSO - else -> AuthenticationType.SSO + SsoIdentityProvider.BRAND_GITLAB -> AuthenticationType.GitLab + SsoIdentityProvider.BRAND_TWITTER -> AuthenticationType.SSO + null -> AuthenticationType.SSO + else -> AuthenticationType.SSO } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index add4bd3ab6..52c32d88e4 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -563,7 +563,7 @@ class OnboardingViewModel @AssistedInject constructor( setState { copy(isLoading = false, resetState = ResetState()) } val nextEvent = when { vectorFeatures.isOnboardingCombinedLoginEnabled() -> OnboardingViewEvents.OnResetPasswordComplete - else -> OnboardingViewEvents.OpenResetPasswordComplete + else -> OnboardingViewEvents.OpenResetPasswordComplete } _viewEvents.post(nextEvent) }, diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt index ea25d8ebcd..682cd96b56 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt @@ -31,6 +31,6 @@ fun SignMode.toAuthenticateAction(login: String, password: String, initialDevice } fun ThemeProvider.ftueBreakerBackground() = when (isLightTheme()) { - true -> R.drawable.bg_gradient_ftue_breaker + true -> R.drawable.bg_gradient_ftue_breaker false -> R.drawable.bg_color_background } diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricAuthError.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricAuthError.kt index 1b7d35879e..7a293d022e 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricAuthError.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricAuthError.kt @@ -33,6 +33,6 @@ class BiometricAuthError(val code: Int, message: String) : Throwable(message) { val isAuthPermanentlyDisabledError: Boolean get() = code == BiometricPrompt.ERROR_LOCKOUT_PERMANENT companion object { - private val LOCKOUT_ERROR_CODES = arrayOf(BiometricPrompt.ERROR_LOCKOUT, BiometricPrompt.ERROR_LOCKOUT_PERMANENT) + private val LOCKOUT_ERROR_CODES = arrayOf(BiometricPrompt.ERROR_LOCKOUT, BiometricPrompt.ERROR_LOCKOUT_PERMANENT) } } diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt index 1b510f5983..a34b284193 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt @@ -73,26 +73,30 @@ class BiometricHelper @Inject constructor( /** * Returns true if a weak biometric method (i.e.: some face or iris unlock implementations) can be used. */ - val canUseWeakBiometricAuth: Boolean get() = - configuration.isWeakBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_WEAK) == BIOMETRIC_SUCCESS + val canUseWeakBiometricAuth: Boolean + get() = + configuration.isWeakBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_WEAK) == BIOMETRIC_SUCCESS /** * Returns true if a strong biometric method (i.e.: fingerprint, some face or iris unlock implementations) can be used. */ - val canUseStrongBiometricAuth: Boolean get() = - configuration.isStrongBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS + val canUseStrongBiometricAuth: Boolean + get() = + configuration.isStrongBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS /** * Returns true if the device credentials can be used to unlock (system pin code, password, pattern, etc.). */ - val canUseDeviceCredentialsAuth: Boolean get() = - configuration.isDeviceCredentialUnlockEnabled && biometricManager.canAuthenticate(DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS + val canUseDeviceCredentialsAuth: Boolean + get() = + configuration.isDeviceCredentialUnlockEnabled && biometricManager.canAuthenticate(DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS /** * Returns true if any system authentication method (biometric weak/strong or device credentials) can be used. */ @VisibleForTesting(otherwise = PRIVATE) - internal val canUseAnySystemAuth: Boolean get() = canUseWeakBiometricAuth || canUseStrongBiometricAuth || canUseDeviceCredentialsAuth + internal val canUseAnySystemAuth: Boolean + get() = canUseWeakBiometricAuth || canUseStrongBiometricAuth || canUseDeviceCredentialsAuth /** * Returns true if any system authentication method and there is a valid associated key. @@ -153,9 +157,9 @@ class BiometricHelper @Inject constructor( @SuppressLint("NewApi") @OptIn(ExperimentalCoroutinesApi::class) private fun authenticateInternal( - activity: FragmentActivity, - checkSystemKeyExists: Boolean, - cryptoObject: BiometricPrompt.CryptoObject? = null, + activity: FragmentActivity, + checkSystemKeyExists: Boolean, + cryptoObject: BiometricPrompt.CryptoObject? = null, ): Flow { if (checkSystemKeyExists && !isSystemAuthEnabledAndValid) return flowOf(false) @@ -189,9 +193,9 @@ class BiometricHelper @Inject constructor( @VisibleForTesting(otherwise = PRIVATE) internal fun authenticateWithPromptInternal( - activity: FragmentActivity, - cryptoObject: BiometricPrompt.CryptoObject? = null, - channel: Channel, + activity: FragmentActivity, + cryptoObject: BiometricPrompt.CryptoObject? = null, + channel: Channel, ): BiometricPrompt { val executor = ContextCompat.getMainExecutor(context) val callback = createSuspendingAuthCallback(channel, executor.asCoroutineDispatcher()) diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepository.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepository.kt index a8690f69d2..4a7bce8a52 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepository.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepository.kt @@ -36,7 +36,7 @@ class LockScreenKeyRepository( private val systemKeyAlias = "$baseName.system" private val pinCodeCrypto: KeyStoreCrypto by lazy { - keyStoreCryptoFactory.provide(pinCodeKeyAlias, keyNeedsUserAuthentication = false) + keyStoreCryptoFactory.provide(pinCodeKeyAlias, keyNeedsUserAuthentication = false) } private val systemKeyCrypto: KeyStoreCrypto by lazy { keyStoreCryptoFactory.provide(systemKeyAlias, keyNeedsUserAuthentication = true) diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenFragment.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenFragment.kt index 764da8cceb..0e6fdfb61e 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenFragment.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenFragment.kt @@ -114,15 +114,15 @@ class LockScreenFragment : VectorBaseFragment() { private fun handleEvent(viewEvent: LockScreenViewEvent) { when (viewEvent) { is LockScreenViewEvent.CodeCreationComplete -> lockScreenListener?.onPinCodeCreated() - is LockScreenViewEvent.ClearPinCode -> { + is LockScreenViewEvent.ClearPinCode -> { if (viewEvent.confirmationFailed) { lockScreenListener?.onNewCodeValidationFailed() } views.codeView.clearCode() } - is LockScreenViewEvent.AuthSuccessful -> lockScreenListener?.onAuthenticationSuccess(viewEvent.method) - is LockScreenViewEvent.AuthFailure -> onAuthFailure(viewEvent.method) - is LockScreenViewEvent.AuthError -> onAuthError(viewEvent.method, viewEvent.throwable) + is LockScreenViewEvent.AuthSuccessful -> lockScreenListener?.onAuthenticationSuccess(viewEvent.method) + is LockScreenViewEvent.AuthFailure -> onAuthFailure(viewEvent.method) + is LockScreenViewEvent.AuthError -> onAuthError(viewEvent.method, viewEvent.throwable) } } diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenViewModel.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenViewModel.kt index a240af243a..39d0937323 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenViewModel.kt @@ -94,7 +94,7 @@ class LockScreenViewModel @AssistedInject constructor( override fun handle(action: LockScreenAction) { when (action) { - is LockScreenAction.PinCodeEntered -> onPinCodeEntered(action.value) + is LockScreenAction.PinCodeEntered -> onPinCodeEntered(action.value) is LockScreenAction.ShowBiometricPrompt -> showBiometricPrompt(action.callingActivity) } } diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/utils/DevicePromptCheck.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/utils/DevicePromptCheck.kt index fa1d7d5559..1760cd6c80 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/utils/DevicePromptCheck.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/utils/DevicePromptCheck.kt @@ -48,15 +48,15 @@ object DevicePromptCheck { * See [this OP forum thread](https://forums.oneplus.com/threads/oneplus-7-pro-fingerprint-biometricprompt-does-not-show.1035821/). */ private val isOnePlusDeviceWithNoBiometricUI: Boolean = - Build.BRAND.equals("OnePlus", ignoreCase = true) && - !onePlusModelsWithWorkingBiometricUI.contains(Build.MODEL) && - Build.VERSION.SDK_INT < Build.VERSION_CODES.R + Build.BRAND.equals("OnePlus", ignoreCase = true) && + !onePlusModelsWithWorkingBiometricUI.contains(Build.MODEL) && + Build.VERSION.SDK_INT < Build.VERSION_CODES.R /** * Some LG models don't seem to have a system biometric prompt at all. */ private val isLGDeviceWithNoBiometricUI: Boolean = - Build.BRAND.equals("LG", ignoreCase = true) && lgModelsWithoutBiometricUI.contains(Build.MODEL) + Build.BRAND.equals("LG", ignoreCase = true) && lgModelsWithoutBiometricUI.contains(Build.MODEL) /** * Check if this device is included in the list of devices with known Biometric Prompt issues. diff --git a/vector/src/main/java/im/vector/app/features/redaction/CheckIfEventIsRedactedUseCase.kt b/vector/src/main/java/im/vector/app/features/redaction/CheckIfEventIsRedactedUseCase.kt new file mode 100644 index 0000000000..ac77455d66 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/redaction/CheckIfEventIsRedactedUseCase.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.redaction + +import org.matrix.android.sdk.api.session.Session +import timber.log.Timber +import javax.inject.Inject + +class CheckIfEventIsRedactedUseCase @Inject constructor( + private val session: Session, +) { + + suspend fun execute(roomId: String, eventId: String): Boolean { + Timber.d("checking if event is redacted for roomId=$roomId and eventId=$eventId") + return try { + session.eventService() + .getEvent(roomId, eventId) + .isRedacted() + .also { Timber.d("event isRedacted=$it") } + } catch (error: Exception) { + Timber.e(error, "error when getting event, it may not exist yet") + false + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/font/FontScaleSettingFragment.kt b/vector/src/main/java/im/vector/app/features/settings/font/FontScaleSettingFragment.kt index d553c1a8ce..78c06d5969 100644 --- a/vector/src/main/java/im/vector/app/features/settings/font/FontScaleSettingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/font/FontScaleSettingFragment.kt @@ -31,7 +31,7 @@ import javax.inject.Inject class FontScaleSettingFragment @Inject constructor( private val fontListController: FontScaleSettingController -) : VectorBaseFragment(), FontScaleSettingController.Callback { +) : VectorBaseFragment(), FontScaleSettingController.Callback { private val viewModel: FontScaleSettingViewModel by fragmentViewModel() diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt index 265cf3199e..b1a240e942 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt @@ -94,10 +94,10 @@ class SoftLogoutController @Inject constructor( } private fun buildForm(state: SoftLogoutViewState) = when (state.asyncHomeServerLoginFlowRequest) { - is Fail -> buildLoginErrorWithRetryItem(state.asyncHomeServerLoginFlowRequest.error) - is Success -> buildLoginSuccessItem(state) + is Fail -> buildLoginErrorWithRetryItem(state.asyncHomeServerLoginFlowRequest.error) + is Success -> buildLoginSuccessItem(state) is Loading, Uninitialized -> buildLoadingItem() - is Incomplete -> Unit + is Incomplete -> Unit } private fun buildLoadingItem() { @@ -116,11 +116,11 @@ class SoftLogoutController @Inject constructor( } private fun buildLoginSuccessItem(state: SoftLogoutViewState) = when (state.asyncHomeServerLoginFlowRequest.invoke()) { - LoginMode.Password -> buildLoginPasswordForm(state) - is LoginMode.Sso -> buildLoginSSOForm() + LoginMode.Password -> buildLoginPasswordForm(state) + is LoginMode.Sso -> buildLoginSSOForm() is LoginMode.SsoAndPassword -> disambiguateLoginSSOAndPasswordForm(state) - LoginMode.Unsupported -> buildLoginUnsupportedForm() - LoginMode.Unknown, null -> Unit // Should not happen + LoginMode.Unsupported -> buildLoginUnsupportedForm() + LoginMode.Unknown, null -> Unit // Should not happen } private fun buildLoginPasswordForm(state: SoftLogoutViewState) { @@ -148,12 +148,12 @@ class SoftLogoutController @Inject constructor( private fun disambiguateLoginSSOAndPasswordForm(state: SoftLogoutViewState) { when (state.loginType) { - LoginType.PASSWORD -> buildLoginPasswordForm(state) - LoginType.SSO -> buildLoginSSOForm() + LoginType.PASSWORD -> buildLoginPasswordForm(state) + LoginType.SSO -> buildLoginSSOForm() LoginType.DIRECT, LoginType.CUSTOM, LoginType.UNSUPPORTED -> buildLoginUnsupportedForm() - LoginType.UNKNOWN -> Unit + LoginType.UNKNOWN -> Unit } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt index 4286c12058..5b362690fa 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt @@ -110,6 +110,7 @@ class SpaceDirectoryController @Inject constructor( ?.filter { it.parentRoomId == (data.hierarchyStack.lastOrNull() ?: data.spaceId) } + ?.filterNot { it.isUpgradedRoom(data) } ?: emptyList() if (flattenChildInfo.isEmpty()) { @@ -209,4 +210,7 @@ class SpaceDirectoryController @Inject constructor( } } } + + private fun SpaceChildInfo.isUpgradedRoom(data: SpaceDirectoryState) = + data.knownRoomSummaries.any { it.roomId == childRoomId && it.versioningState.isUpgraded() } } diff --git a/vector/src/main/res/values-ar/strings.xml b/vector/src/main/res/values-ar/strings.xml index 9c436d24f0..044653f4ba 100644 --- a/vector/src/main/res/values-ar/strings.xml +++ b/vector/src/main/res/values-ar/strings.xml @@ -97,14 +97,14 @@ • الخوادِم المُطابقة لـ %s أُزيلت مِن قائمة الحظر. • الخوادِم المُطابقة لـ %s محظورة الآن. • خوادِم مُطابقة IP الحرفية مسموحة الآن. - يَّرتَ خادِم الـACLs لهذه الغُرفة. - غيَّرَ %s خادِم الـACLs لهذه الغُرفة. - • الخوادِم المطابقة للقيم الحرفية للـIP محظورة. - • الخوادِم المُطابقة لـ %s مسموحة. + لقد قمت بتغيير قائمة الوصول لهذه الغُرفة. + قام %s بتغيير قائمة الوصول (ACL) لهذه الغُرفة. + • الخوادِم المتطابقة من حيث بروتوكول الإنترنت (IP) المستخدم محظورة. + • الخوادِم المُطابقة لـِ %s مسموحة. • الخوادِم المُطابقة لـ %s محظورة. - • الخوادِم المطابقة للقيم الحرفية للـIP مسموحة. - عيَّنتَ خادِم الـACLs لهذه الغُرفة. - عيَّنَ %s خادِم الـACLs لهذه الغُرفة. + الخوادِم المتطابقة من حيث بروتوكول الإنترنت (IP) المستخدم مسموحة. + لقد قمت بتعيين قائمة التحكم بالوصول لهذه الغُرفة. + %s قام بتعيين قائمة التحكم بالوصول لهذه الغرفة. رقيتَ هُنا. رقّى %s هُنا. جعلتَ الرسائل المُستقبلية مرئية لـ %1$s diff --git a/vector/src/main/res/values-ca/strings.xml b/vector/src/main/res/values-ca/strings.xml index 9491ebed77..05d10b0f5f 100644 --- a/vector/src/main/res/values-ca/strings.xml +++ b/vector/src/main/res/values-ca/strings.xml @@ -21,7 +21,7 @@ %1$s ha vetat %2$s %1$s ha retirat la invitació de %2$s %1$s ha canviat la seva foto - %1$s ha establert la visibilitat de l\'històric futur de la sala a %2$s + %1$s ha activat la visualització de l\'històric de la sala (a partir d\'ara) a %2$s tots els participants de la sala, des de que s\'hi uneixen. qualsevol. (també ha canviat la foto) @@ -153,7 +153,7 @@ %s ha actualitzat aquesta sala. Has establert la visibilitat dels missatges futurs a %1$s %1$s ha establert la visibilitat dels missatges futurs a %2$s - Has establert la visibilitat de l\'històric futur de la sala a %1$s + Has establert la visibilitat de l\'històric de la sala (a partir d\'ara) a %1$s Has finalitzat la trucada. Has respost a la trucada. Has enviat dades per configurar la trucada. @@ -1055,7 +1055,7 @@ Utilitza el xifrat per mantenir els teus xats privats Aquest xat és teu. Apropia-te\'n. Filtra xats… - Inici de l\'històric del xat personal amb %s. + Inici de l\'històric del xat directe amb %s. L\'administrador del servidor ha desactivat el xifrat d\'extrem a extrem per defecte en sales privades i en xats directes. Xat personal Els missatges d\'aquí estan xifrats d\'extrem a extrem. @@ -1064,7 +1064,7 @@ Crea un nou xat personal Envia un nou missatge Xats directes - Els missatges d\'aquesta sala estan xifrats d\'extrem a extrem. + Els missatges d\'aquest xat estan xifrats d\'extrem a extrem. Mostra els esdeveniments amagats a la cronologia No s\'ha pogut enviat el suggeriment (%s) Gràcies, el suggeriment s\'ha enviat correctament @@ -2482,7 +2482,7 @@ Obre càmera Mostra els contorns dels missatges Actualitzada fa %1$s - Implementació temporal: les ubicacions persisteixen a l\'historial de la sala + Implementació temporal: les ubicacions persisteixen a l\'històric de la sala Coses en aquest espai Reprodueix immediatament les imatges animades a la cronologia Deixa de compartir @@ -2529,4 +2529,10 @@ \'Endpoint\' registrat correctament al servidor. Registre d\'endpoint Següent - + Quan convidis algú a una sala xifrada que comparteix l\'històric de missatges, aquest serà visible. + Els resultats es mostraran quan acabi l\'enquesta + MSC3061: Compartició de claus de sala per missatges antics + Envia un primer missatge per convidar %s a parlar + Els missatges d\'aquest xat seran xifrats d\'extrem a extrem. + Crea + \ No newline at end of file diff --git a/vector/src/main/res/values-cs/strings.xml b/vector/src/main/res/values-cs/strings.xml index c15de7cc0d..cd66940316 100644 --- a/vector/src/main/res/values-cs/strings.xml +++ b/vector/src/main/res/values-cs/strings.xml @@ -2577,4 +2577,10 @@ Koncový bod byl úspěšně zaregistrován na domovském serveru. Registrace koncového bodu Další - + Výsledky se zobrazí po ukončení hlasování + Při pozvání do šifrované místnosti, která sdílí historii, bude zašifrovaná historie viditelná. + MSC3061: Sdílení klíčů místnosti pro minulé zprávy + Odesláním první zprávy pozvete %s do místnosti + Zprávy v této místnosti budou koncově šifrovány. + Přejít + \ No newline at end of file diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index bf69883e14..fcb82889f5 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -2528,4 +2528,5 @@ Profil personalisieren ${app_name} ist auch für den Arbeitsplatz geeignet. Die sichersten Organisationen der Welt vertrauen darauf. Threads sind noch in Arbeit, und es stehen neue, aufregende Funktionen an, wie z. B. verbesserte Benachrichtigungen. Wir würden uns sehr über Dein Feedback freuen! - + Nachrichten in diesem Chat werden End-zu-End-Verschlüsselt + \ No newline at end of file diff --git a/vector/src/main/res/values-et/strings.xml b/vector/src/main/res/values-et/strings.xml index af4c9d3b4c..80881f45c0 100644 --- a/vector/src/main/res/values-et/strings.xml +++ b/vector/src/main/res/values-et/strings.xml @@ -1705,7 +1705,7 @@ Kas sa unustasid või kaotasid kõik võimalused taastada ligipääsu oma kontole\? Lähtesta kõik seadistused Sina liitusid. %s liitus. - See jututuba on läbivalt krüptitud. + See vestlus on läbivalt krüptitud. Lahku Seadistused Sõnumid siin jututoas kasutavad läbivat krüptimist. @@ -2520,4 +2520,10 @@ Sünkroniseerimine taustal Google\'i teenused Vali teavituste laadimise viis - + Tulemused on näha siis, kui küsitlus on lõppenud + Kutsudes osalejaid krüptitud jututuppa, kus ajaloo jagamine on lubatud, on vanad sõnumid loetavad. + MSC3061: Minevikus saadetud sõnumite lugemiseks vajalike krüptovõtmete jagamine jututoas + Saada oma esimene sõnum kutsudes %s vestlusesse + See vestlus saab olema läbivalt krüptitud. + Mine + \ No newline at end of file diff --git a/vector/src/main/res/values-fa/strings.xml b/vector/src/main/res/values-fa/strings.xml index 95f9bedfae..a8dbe72b74 100644 --- a/vector/src/main/res/values-fa/strings.xml +++ b/vector/src/main/res/values-fa/strings.xml @@ -1095,7 +1095,7 @@ %s پیوست. اتاق را ساخته و پیکربندی کردید. رمزنگاری به کار نیفتاده - پیام‌های این اتاق، رمزنگاری سرتاسری دارند. + پیام‌های این گپ، رمزنگاری سرتاسری دارند. پیام‌های این اتاق رمزنگاری سرتاسری دارند. در نمایهٔ کاربران تأییدشان کرده و بیش‌تر بیاموزید. رمزنگاری به کار افتاد هنگام ارتقای اتاق‌ها @@ -1213,7 +1213,7 @@ شکست در ثبت ژتون FCM در کارساز خانگی: \n%1$s ژتون FCM با موفقیت در کارساز خانگی ثبت شد. - ثبت توکن + ثبت ژتون افزودن حساب کاربری [%1$s] \nاین خطا خارج از مهار ${app_name} است. هیچ حساب گوگلی روی تلفن نیست. لطفاً مدیر حساب را گشوده و حساب گوگلی بیفزایید. @@ -1221,11 +1221,11 @@ \nاین خطا خارج از مهار ${app_name} است و ممکن است به دلایل مختلفی رخ داده باشد. ممکن است اگر بعداً دوباره تلاش کنید، کار کند. می‌توانید بررسی کنید که مصرف دادهٔ خدمات پلی گوگل در تنظیمات سامانه محدود نشده باشد یا ساعت افزاره‌تان درست باشد. همچنین ممکن است روی رام‌های سفارشی اتفاق بیفتد. [%1$s] \nاین خطا، خارج از مهار ${app_name} است و طبق گفتهٔ گوگل، نشانگر ثبت بیش‌از حد کاره‌ها در FCM است. خطا فقط در مواردی که تعداد خیلی زیادی کاره وجود داشته باشد رخ می‌دهد، پس کاربران معمولی نباید تحت تأثیر قرار گیرند. - بازیابی توکن FCM با مشکل مواجه شد: + شکست در بازیابی ژتون FCM: \n%1$s - توکن FCM با موفقیت بازیابی شد: + ژتون FCM با موفقیت بازیابی شد: \n%1$s - توکن Firebase + ژتون فایربیس مشکل Google Play Services را برطرف کنید المنت برای ارسال آگاهی‌ها از خدمات پلی گوگل استفاده می‌کند اما به نظر می‌رسد به درستی پیکربندی نشده است: \n%1$s @@ -1494,7 +1494,7 @@ قطع ارتباط با سرور هویت‌سنجی به این معنی است که توسط کاربران دیگر قابل شناسایی نخواهید بود و نمی توانید دیگران را از طریق ایمیل یا تلفن دعوت کنید. در حال استفاده از کارساز هویتی نیستید. برای کشق و قابل کشف بودن به دست آشنایان موجودی که می‌شناسید، یک کارساز هویت در زیر پیکربندی کنید. دارید برای کشف و قابل کشف بودن به دست آشنایان موجودی که می‌شناسید از %1$s استفاده می‌کنید. - ثبت توکن + ثبت ژتون فرمت: آدرس: نام نشست: @@ -2521,4 +2521,18 @@ بازنشانی روش آگاهی برچسب نمایه: بعدی - + لطفاً به خاطر داشته باشید: این یک ویژگی آزمایشگاهی با پیاده‌سازی موقّتی است. یعنی نخواهید توانست تاریخجهٔ مکانتان را حذف کرده و کاربران پیش‌رفته خواهند توانست حتا پس از پایان هم‌رسانی مکان زنده‌تان با این اتاق، تاریخچهٔ مکانتان را ببینند. + نتوانست نقطهٔ پایانی را بیابد. + نقطهٔ پایانی کنونی: %s + نقطهٔ پایانی + هنگام پایان نظرسنجی، نتایج نمایان خواهند شد + هنگام دعوت به اتاقی رمزشده که تاریخچه‌ای هم‌رسانده دارد، تاریخچهٔ رمز شده نمایان خواهد بود. + MSC3061: هم‌رسانی کلیدهای اتاق برای پیام‌های گذشته + نخستین پیامتان را برای دعوت %s به گپ بفرستید + پیام‌های این گپ رمزنگاری سرتاسری خواهند شد. + رفتن + شکست در ثبت ژتون نقطهٔ پایانی روی کارساز خانگی: +\n%1$s + نقطهٔ پایانی با موفّقیت روی کارساز خانگی ثبت شد. + ثبت نقطهٔ پایانی + \ No newline at end of file diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index 01852278ef..3cfbd5f9bd 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -2529,4 +2529,10 @@ Le point de connexion a été correctement enregistré sur le serveur d’accueil. Enregistrement du point de connexion Suivant - + Les résultats seront visibles lorsque le sondage sera terminé + Lors de l’invitation dans un salon chiffré qui partage son historique, son historique chiffré sera visible. + MSC3061 : Partage des clés du salon pour les messages passés + Envoyez votre premier message pour inviter %s à discuter + Les messages de ce salon seront chiffrés de bout en bout. + C’est parti + \ No newline at end of file diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index 99caed9211..dc9e17fc75 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -1607,7 +1607,7 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Még nem adtál hozzá a fiókodhoz email címet Email címek Még nem adtál hozzá telefonszámot a fiókodhoz - Az üzenetek ebben a szobában végpontok közötti titkosítással védettek. + Az üzenetek ebben a beszélgetésben végpontok közötti titkosítással védettek. Kép hozzáadása Adj meg egy témát %s, hogy a többiek tudják, miről van szó ebben a szobában! @@ -2318,7 +2318,7 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze A változások életbelépéséhez indítsd újra az alkalmazást. LaTeX matematikai szintaxis engedélyezése Nem léphetsz be ebbe a szobába - Az ön beszélgetései csak az öné. + Birtokold a beszélgetéseid. Titkosítás visszafejtési hiba esemény alkalmával a rendszer automatikusan elküldi a logokat Titkosítás visszafejtési hibák automatikus jelentése. Szavazás létrehozása @@ -2436,7 +2436,7 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Válassz egy megjelenítési nevet A fiókod elkészült: %s. Gratulálunk! - Vigyél haza + A kezdőlapra Profil személyre szabása Tiltás Élő földrajzi helyzet meghatározás betöltése… @@ -2499,4 +2499,40 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze mperc perc ó - + Földrajzi hely megosztás engedélyezése + Figyelem: ez a labor lehetőség egy átmeneti megvalósítás. Ez azt jelenti, hogy a szobába már elküldött helyadatok az élő hely megosztás leállítása után is hozzáférhetők maradnak a szobában. + Élő földrajzi hely megosztása + Jelenlegi átjáró: %s + Átjáró (gateway) + Nem található végpont. + Jelenlegi végpont: %s + Végpont + Jelenleg használatban: %s. + Metódus + + %d beállítás található. + %d beállítás található. + + A háttér szinkronizációs szolgáltatástól eltérő beállítási lehetőség nem érhető el. + A Google Play szolgáltatástól eltérő beállítási lehetőség nem érhető el. + Elérhető beállítások + Értesítési beállítások + Szinkronizálás a háttérben + Google szolgáltatások + Válaszd ki, hogyan szeretnél értesítéseket kapni + Az eredmény a szavazás végeztével válik láthatóvá + Ha olyan titkosított szobába hívsz meg valakit ahol a régi üzenetek megosztása engedélyezett a régi titkosított üzenetek is láthatóak lesznek a meghívott számára. + MSC3061: Szoba kulcsok megosztása a régi üzenetekhez + A biometrikus azonosítást nem lehet engedélyezni. + A biometrikus azonosítás kikapcsolásra került mivel egy új biometrikus azonosítási metódus került hozzáadásra. Újra engedélyezheted a Beállításokban. + Az első üzeneteddel hívd meg ide őt: %s + Az üzenetek ebben a beszélgetésben végpontok közötti titkosítással lesznek védve. + Értesítési metódus visszaállítása + Profil címke: + Menj + Végpont token regisztrációja sikertelen a Matrix-kiszolgálón: +\n%1$s + Végpont sikeresen regisztrálva lett a matrix szerveren. + Végpont regisztráció + Következő + \ No newline at end of file diff --git a/vector/src/main/res/values-in/strings.xml b/vector/src/main/res/values-in/strings.xml index 104203bf32..11752f934f 100644 --- a/vector/src/main/res/values-in/strings.xml +++ b/vector/src/main/res/values-in/strings.xml @@ -2032,7 +2032,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Anda membuat dan mengatur ruangan ini. %s membuat dan mengatur ruangan ini. Enkripsi tidak diaktifkan - Pesan di ruangan ini dienkripsi secara ujung-ke-ujung. + Pesan di obrolan ini dienkripsi secara ujung-ke-ujung. Pesan di ruangan ini dienkripsi secara ujung-ke-ujung. Pelajari lebih lanjut & verifikasi pengguna di profil mereka. Enkripsi diaktifkan Jika Anda batalkan, Anda mungkin kehilangan pesan terenkripsi dan data Anda jika Anda kehilangan akses ke login Anda. @@ -2483,4 +2483,10 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Endpoint berhasil terdaftar ke homeserver. Pendaftaran Endpoint Lanjut - + Hasil akan ditampilkan ketika pemungutan suara berakhir + Ketika mengundang ke ruangan terenkripsi yang juga membagikan riwayat, riwayat terenkripsi akan dapat dilihat. + MSC3061: Pembagian kunci ruangan untuk pesan lama + Kirim pesan pertama Anda untuk mengundang %s ke obrolan + Pesan di obrolan ini akan dienkripsi secara ujung-ke-ujung. + Mulai + \ No newline at end of file diff --git a/vector/src/main/res/values-is/strings.xml b/vector/src/main/res/values-is/strings.xml index 88234a6364..53af7bff5c 100644 --- a/vector/src/main/res/values-is/strings.xml +++ b/vector/src/main/res/values-is/strings.xml @@ -764,7 +764,7 @@ Uppfærsla dulritunar tiltæk Sendir skilaboð sem óbreyttur texti án þess að túlka það sem markdown Dulritun ekki virk - Skilaboð í þessari spjallrás eru enda-í-enda dulrituð. + Skilaboð í þessu spjalli eru enda-í-enda dulrituð. Kerfisstjóri netþjónsins þíns hefur lokað á sjálfvirka dulritun í einkaspjallrásum og beinum skilaboðum. Stillingar spjallrásar Skilaboð í þessari spjallrás eru ekki enda-í-enda dulrituð. @@ -1292,7 +1292,7 @@ Lokið Tókst ! (Ítarlegt) - %d+ + +%d %1$s: %2$s fella saman fletta út @@ -2017,4 +2017,4 @@ Prófaðu það Gera óvirkt Upphafleg samstillingarbeiðni - + \ No newline at end of file diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index 3879e5a13c..140b63e4c8 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -1692,7 +1692,7 @@ Reimposta tutto Hai dimenticato o perso tutte le opzioni di ripristino\? Reimposta tutto Sei entrato. - I messaggi in questa stanza sono crittografati E2E. + I messaggi in questa conversazione sono cifrati end-to-end. Esci Impostazioni I messaggi qui sono cifrati E2E. @@ -2520,4 +2520,10 @@ Endpoint registrato con successo sull\'homeserver. Registrazione endpoint Avanti - + I risultati saranno visibili quando il sondaggio sarà terminato + Quando si invita in una stanza crittografata che condivide la cronologia, la cronologia cifrata sarà visibile. + MSC3061: Condivisione chiavi stanza per messaggi passati + Invia il primo messaggio per invitare %s a parlare + I messaggi in questa conversazione saranno cifrati end-to-end. + Vai + \ No newline at end of file diff --git a/vector/src/main/res/values-nl/strings.xml b/vector/src/main/res/values-nl/strings.xml index 9b5591d8d0..a9d9dd3658 100644 --- a/vector/src/main/res/values-nl/strings.xml +++ b/vector/src/main/res/values-nl/strings.xml @@ -1927,7 +1927,7 @@ %s heeft de kamer gemaakt en geconfigureerd. De versleuteling die door deze kamer wordt gebruikt, wordt niet ondersteund Versleuteling niet ingeschakeld - Berichten in deze kamer zijn end-to-end-versleuteld. + Berichten in deze chat zijn end-to-end-versleuteld. Berichten in deze kamer zijn eind-tot-eind-versleuteld. Lees meer en verifieer persoon in hun profiel. Als u nu annuleert, kunt u versleutelde berichten en gegevens kwijtraken als u de toegang tot uw aanmeldingen verliest. \n @@ -2529,4 +2529,10 @@ Eindpunt succesvol geregistreerd bij server. Registratie van eindpunt Volgende - + Resultaten zijn zichtbaar wanneer de poll is afgelopen + Bij het uitnodigen in een versleutelde ruimte die geschiedenis deelt, is de versleutelde geschiedenis zichtbaar. + MSC3061: Kamersleutels delen voor eerdere berichten + Stuur uw eerste bericht om %s uit te nodigen om te chatten + Berichten in deze chat worden eind-tot-eind versleuteld. + Ga + \ No newline at end of file diff --git a/vector/src/main/res/values-pl/strings.xml b/vector/src/main/res/values-pl/strings.xml index 26e85f2473..a9d1df49d8 100644 --- a/vector/src/main/res/values-pl/strings.xml +++ b/vector/src/main/res/values-pl/strings.xml @@ -1163,9 +1163,9 @@ Wyślij obrazów w oryginalnym rozmiarze Potwierdź Usunięcie - Jesteś pewny(-na), że chcesz usunąć to wydarzenie\? Jeżeli usuniesz nazwę pokoju lub zmienisz temat, wciąż będzie możliwe cofnięcie zmiany. + Czy na pewno chcesz usunąć to wydarzenie\? Pamiętaj, że skasowanie nazwy pokoju lub zmiana jego tematu może czasem je przywrócić. Podaj przyczynę - Przyczyna reakcji + Powód usunięcia Wydarzenie usunięte przez użytkownika, przyczyna: %1$s Wydarzenie moderowane przez administratora pokoju, przyczyna: %1$s Klucze są już aktualne! @@ -2605,4 +2605,4 @@ Dostępni dostawcy Dostawca powiadomień Wybierz, którego dostawcy powiadomień push chcesz używać - + \ No newline at end of file diff --git a/vector/src/main/res/values-sk/strings.xml b/vector/src/main/res/values-sk/strings.xml index edc4163d0c..9ddfa08aa4 100644 --- a/vector/src/main/res/values-sk/strings.xml +++ b/vector/src/main/res/values-sk/strings.xml @@ -1810,7 +1810,7 @@ Ahoj, ozvi sa mi na ${app_name}: %s Nemohli sme vytvoriť vašu priamu správu. Skontrolujte používateľov, ktorých chcete pozvať, a skúste to znova. Kvôli end-to-end šifrovaniu sa môže stať, že budete musieť chvíľu počkať, kým vám od niekoho príde správa, pretože vám neboli správne odoslané šifrovacie kľúče. - Správy v tejto miestnosti sú šifrované od vás až k príjemcovi. + Správy v tejto konverzácii sú šifrované od vás až k príjemcovi. Správy v tejto miestnosti sú end-to-end šifrované. Zistite viac a overte používateľov v ich profile. Správca vášho servera predvolene vypol end-to-end šifrovanie v súkromných miestnostiach a v priamych správach. Správy s týmto používateľom sú end-to-end šifrované a tretie strany ich nemôžu čítať. @@ -2577,4 +2577,10 @@ Koncové zariadenie sa úspešne zaregistrovalo na domovský server. Registrácia koncového bodu Ďalej - + Výsledky budú viditeľné po ukončení ankety + Pri pozvaní do zašifrovanej miestnosti, ktorá zdieľa históriu, bude zašifrovaná história viditeľná. + MSC3061: Zdieľanie kľúčov od miestnosti pre staršie správy + Odošlite svoju prvú správu a pozvite %s do konverzácie + Správy v tejto miestnosti sú šifrované od vás až k príjemcovi. + Spustiť + \ No newline at end of file diff --git a/vector/src/main/res/values-sv/strings.xml b/vector/src/main/res/values-sv/strings.xml index 84e90da44b..29ecdfe895 100644 --- a/vector/src/main/res/values-sv/strings.xml +++ b/vector/src/main/res/values-sv/strings.xml @@ -1706,7 +1706,7 @@ Lämna Återställ allt Du gick med. - Meddelanden i det här rummet är totalsträckskrypterade. + Meddelanden i den här chatten är totalsträckskrypterade. Inställningar Meddelanden här är totalsträckskrypterade. \n @@ -2529,4 +2529,10 @@ \n%1$s Lyckades registrera ändpunkten hos hemservern. Ändpunktsregistrering - + Resultat kommer att synas när omröstningen avslutas + När du bjuder in i ett krypterat rum som delar historik, så kommer krypterad historik att vara synlig. + MSC3061: Delar rumsnycklar för tidigare meddelanden + Skicka ditt första meddelande för att bjuda in %s att chatta + Meddelanden i den här chatten kommer att vara totalsträckskrypterade. + + \ No newline at end of file diff --git a/vector/src/main/res/values-uk/strings.xml b/vector/src/main/res/values-uk/strings.xml index a5b9571748..ed7a2c7438 100644 --- a/vector/src/main/res/values-uk/strings.xml +++ b/vector/src/main/res/values-uk/strings.xml @@ -22,7 +22,7 @@ %s починає відеовиклик. %s розпочинає голосовий виклик. %s відповідає на виклик. - %s завершує дзвінок. + %s завершує виклик. %1$s робить майбутню історію кімнати видимою для %2$s всіх учасників кімнати, з моменту їх запрошення. всіх учасників кімнати, з моменту їх приєднання. @@ -135,7 +135,7 @@ Ви зробили майбутню історію кімнати видимою для %1$s Ви зробили майбутні повідомлення видимими для %1$s %1$s робить майбутні повідомлення видимими для %2$s - Ви завершили дзвінок. + Ви завершили виклик. Ви відповіли на виклик. Ви надіслали дані для налаштування виклику. %s надсилає дані для налаштування виклику. @@ -230,8 +230,10 @@ Виконується виклик… Абонент не відповідає. Інформація - Для здійснення аудіодзвінків потрібен доступ до мікрофону. - Для здійснення відеодзвінків потрібен доступ до камери та мікрофону.\n\nБудь ласка, надайте його у наступних виринаючих вікнах, щоб мати змогу їх здійснити. + Для здійснення аудіовикликів потрібен доступ до мікрофона. + Для здійснення відеовикликів потрібен доступ до камери та мікрофону. +\n +\nНадайте його у наступних спливних вікнах, щоб мати змогу їх здійснити. ТАК НІ Продовжити @@ -406,7 +408,7 @@ Гучно Зашифроване повідомлення Використовувати рідну камеру - Щойно доданий вами пристрій \'%s\' править ключі шифрування. + Щойно доданий вами сеанс «%s», який запитує ключі шифрування. Ваш незвірений пристрій «%s» вимагає ключі шифрування. Почати перевірку Помилка виконання команди @@ -600,13 +602,13 @@ Пропустити Не вдалося видалити віджет Не вдалося додати віджет - Ви не можете здійснити дзвінок із самим собою + Ви не можете викликати самих себе Почати аудіо-зустріч Почати відеозустріч - У вас немає повноважень розпочати дзвінок - У вас немає повноважень на дзвінок у цій кімнаті - У вас немає повноважень розпочати груповий виклик - У вас немає повноважень розпочати груповий виклик у цій кімнаті + У вас немає дозволу розпочати виклик + У вас немає дозволу розпочати виклик у цій кімнаті + У вас немає дозволу розпочати груповий виклик + У вас немає дозволу розпочати груповий виклик у цій кімнаті Скинути Відхилити Грати @@ -628,7 +630,7 @@ Мені не потрібні мої зашифровані повідомлення Скасувати зміни Помилка SSL. - Ви не можете здійснити дзвінок із самим собою, дочекайтесь, доки інші учасники приймуть ваше запрошення + Ви не можете викликати себе самих, дочекайтесь, доки інші учасники погодяться на ваше запрошення ФАЙЛИ У цій кімнаті немає медіа МЕДІА @@ -1005,9 +1007,9 @@ \n \nВи можете будь-коли змінити цю дію в загальних налаштуваннях. Нехтувати користувача - Понизити - Ви не зможете скасувати цю зміну, оскільки понижуєте свої права, якщо ви останній привілейований користувач у кімнаті, неможливо буде повернути собі привілеї. - Понизитися\? + Зменшити + Ви не зможете скасувати цю зміну, оскільки зменшуєте свої повноваження, якщо ви останній привілейований користувач у кімнаті, неможливо буде повернути собі повноваження. + Зменшити свої повноваження\? Дозволити доступ до ваших контактів. Щоб сканувати QR-код необхідно дозволити доступ до камери. Триває відеовиклик… @@ -1074,7 +1076,7 @@ Повідомлення в цій кімнаті не захищено наскрізним шифруванням. Повідомлення тут не захищено наскрізним шифруванням. Увімкнути наскрізне шифрування… - Повідомлення в цій кімнаті захищені наскрізним шифруванням. + Повідомлення в цій бесіді захищені наскрізним шифруванням. Звірити цей сеанс Звірте цей сеанс, щоб позначити його надійним та надати йому доступ до зашифрованих повідомлень. Якщо ви не входили в цей сеанс, ваш обліковий запис може бути зламано: Назва або ID (#example:matrix.org) @@ -1152,8 +1154,8 @@ Наліпка Використовувати ботів, мости, віджети та пакунки наліпок Зв\'язок із сервером втрачено - Не знайдено жодної правки - Історія правок + Виправлень не знайдено + Історія виправлень Рівень довіри Не довірений Довірений @@ -2461,7 +2463,7 @@ З ким ви спілкуватиметеся найчастіше\? Ви вже переглядаєте цю гілку! Переглянути у кімнаті - Відповісти у гілку + Відповісти в гілці Команду «%s» розпізнано, але вона не підтримується в гілках. З гілки Порада: Торкніться й утримуйте повідомлення і виберіть «%s». @@ -2623,4 +2625,10 @@ Кінцеву точку успішно зареєстровано на домашньому сервері. Реєстрація кінцевої точки Далі - + Результати будуть видимі після завершення опитування + Коли запросити когось у зашифровану кімнату, яка поширює історію, зашифрована історія буде видимою. + MSC3061: Поширення ключів кімнати для минулих повідомлень + Надішліть своє перше повідомлення, щоб запросити %s до бесіди + Повідомлення в цій бесіді будуть захищені наскрізним шифруванням. + Уперед + \ No newline at end of file diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml index 89b2a723e1..c04e293787 100644 --- a/vector/src/main/res/values-zh-rTW/strings.xml +++ b/vector/src/main/res/values-zh-rTW/strings.xml @@ -1671,7 +1671,7 @@ 忘記或遺失所有復原選項?重設所有東西 您已加入。 %s 已加入。 - 此聊天室中的訊息有端到端加密。 + 此聊天中的訊息有端到端加密。 離開 設定 這裡的訊息有端到端加密。 @@ -2481,4 +2481,10 @@ 端點成功註冊至家伺服器。 端點註冊 下一步 - + 結果將在投票結束時可見 + 在分享歷史的加密聊天室中邀請時,加密歷史會是可見的。 + MSC3061:為過去的訊息分享聊天室金鑰 + 傳送您的第一則訊息以邀請 %s 來聊天 + 此聊天中的訊息將會是端到端加密。 + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index e99c1065e5..841ddc1c35 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1608,7 +1608,7 @@ View Reactions Reactions - Message deleted + Message removed Show removed messages Show a placeholder for removed messages Event deleted by user @@ -3116,6 +3116,8 @@ Stop sharing Updated %1$s ago + You don’t have permission to share live location + You need to have the right permissions in order to share live location in this room. Share location Show Message bubbles @@ -3166,4 +3168,8 @@ Please note: this is a labs feature using a temporary implementation. This means you will not be able to delete your location history, and advanced users will be able to see your location history even after you stop sharing your live location with this room. Enable location sharing + + %d message removed + %d messages removed + diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/location/RedactLiveLocationShareEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/location/RedactLiveLocationShareEventUseCaseTest.kt new file mode 100644 index 0000000000..2ca285ef50 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/location/RedactLiveLocationShareEventUseCaseTest.kt @@ -0,0 +1,51 @@ +/* + * 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.home.room.detail.location + +import im.vector.app.test.fakes.FakeRoom +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event + +private const val AN_EVENT_ID = "event-id" +private const val A_REASON = "reason" + +class RedactLiveLocationShareEventUseCaseTest { + + private val fakeRoom = FakeRoom() + + private val redactLiveLocationShareEventUseCase = RedactLiveLocationShareEventUseCase() + + @Test + fun `given an event with valid id when calling use case then event is redacted in the room`() = runTest { + val event = Event(eventId = AN_EVENT_ID) + fakeRoom.locationSharingService().givenRedactLiveLocationShare(beaconInfoEventId = AN_EVENT_ID, reason = A_REASON) + + redactLiveLocationShareEventUseCase.execute(event = event, room = fakeRoom, reason = A_REASON) + + fakeRoom.locationSharingService().verifyRedactLiveLocationShare(beaconInfoEventId = AN_EVENT_ID, reason = A_REASON) + } + + @Test + fun `given an event with empty id when calling use case then nothing is done`() = runTest { + val event = Event(eventId = "") + + redactLiveLocationShareEventUseCase.execute(event = event, room = fakeRoom, reason = A_REASON) + + fakeRoom.locationSharingService().verifyRedactLiveLocationShare(inverse = true, beaconInfoEventId = "", reason = A_REASON) + } +} diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCaseTest.kt new file mode 100644 index 0000000000..08dd5dac5b --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCaseTest.kt @@ -0,0 +1,115 @@ +/* + * 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.home.room.detail.timeline.action + +import im.vector.app.test.fakes.FakeActiveSessionHolder +import io.mockk.mockk +import org.amshove.kluent.shouldBe +import org.junit.Test +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.room.timeline.TimelineEvent + +class CheckIfCanRedactEventUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val checkIfCanRedactEventUseCase = CheckIfCanRedactEventUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance + ) + + @Test + fun `given an event which can be redacted and owned by user when use case executes then the result is true`() { + val canRedactEventTypes = listOf(EventType.MESSAGE, EventType.STICKER) + + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO + + canRedactEventTypes.forEach { eventType -> + val event = givenAnEvent( + eventType = eventType, + senderId = fakeActiveSessionHolder.fakeSession.myUserId + ) + + val actionPermissions = givenActionPermissions(canRedact = false) + + val result = checkIfCanRedactEventUseCase.execute(event, actionPermissions) + + result shouldBe true + } + } + + @Test + fun `given redact permission and an event which can be redacted and sent by another user when use case executes then the result is true`() { + val event = givenAnEvent( + eventType = EventType.MESSAGE, + senderId = "user-id" + ) + + val actionPermissions = givenActionPermissions(canRedact = true) + + val result = checkIfCanRedactEventUseCase.execute(event, actionPermissions) + + result shouldBe true + } + + @Test + fun `given an event which cannot be redacted when use case executes then the result is false`() { + val event = givenAnEvent( + eventType = EventType.CALL_ANSWER, + senderId = fakeActiveSessionHolder.fakeSession.myUserId + ) + + val actionPermissions = givenActionPermissions(canRedact = false) + + val result = checkIfCanRedactEventUseCase.execute(event, actionPermissions) + + result shouldBe false + } + + @Test + fun `given missing redact permission and an event which can be redacted and sent by another user when use case executes then the result is false`() { + val event = givenAnEvent( + eventType = EventType.MESSAGE, + senderId = "user-id" + ) + + val actionPermissions = givenActionPermissions(canRedact = false) + + val result = checkIfCanRedactEventUseCase.execute(event, actionPermissions) + + result shouldBe false + } + + private fun givenAnEvent(eventType: String, senderId: String): TimelineEvent { + val eventId = "event-id" + return TimelineEvent( + root = Event( + eventId = eventId, + type = eventType, + senderId = senderId + ), + localId = 123L, + eventId = eventId, + displayIndex = 1, + ownedByThreadChunk = false, + senderInfo = mockk() + ) + } + + private fun givenActionPermissions(canRedact: Boolean): ActionPermissions { + return ActionPermissions(canRedact = canRedact) + } +} diff --git a/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCaseTest.kt index fed825154c..ed1bcebf16 100644 --- a/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCaseTest.kt @@ -53,7 +53,7 @@ class GetLiveLocationShareSummaryUseCaseTest { } @Test - fun `given a room id and event id when calling use case then live data on summary is returned`() = runTest { + fun `given a room id and event id when calling use case then flow on summary is returned`() = runTest { val summary = LiveLocationShareAggregatedSummary( userId = "userId", isActive = true, @@ -70,4 +70,17 @@ class GetLiveLocationShareSummaryUseCaseTest { result shouldBeEqualTo summary } + + @Test + fun `given a room id, event id and a null summary when calling use case then null is emitted in the flow`() = runTest { + fakeSession.roomService() + .getRoom(A_ROOM_ID) + .locationSharingService() + .givenLiveLocationShareSummaryReturns(AN_EVENT_ID, null) + .givenAsFlowReturns(Optional(null)) + + val result = getLiveLocationShareSummaryUseCase.execute(A_ROOM_ID, AN_EVENT_ID).first() + + result shouldBeEqualTo null + } } diff --git a/vector/src/test/java/im/vector/app/features/redaction/CheckIfEventIsRedactedUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/redaction/CheckIfEventIsRedactedUseCaseTest.kt new file mode 100644 index 0000000000..7dffd78516 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/redaction/CheckIfEventIsRedactedUseCaseTest.kt @@ -0,0 +1,60 @@ +/* + * 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.redaction + +import im.vector.app.test.fakes.FakeSession +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.UnsignedData + +private const val A_ROOM_ID = "room_id" +private const val AN_EVENT_ID = "event_id" + +class CheckIfEventIsRedactedUseCaseTest { + + private val fakeSession = FakeSession() + + private val checkIfEventIsRedactedUseCase = CheckIfEventIsRedactedUseCase( + session = fakeSession + ) + + @Test + fun `given a room id and event id for redacted event when calling use case then true is returned`() = runTest { + val event = Event( + unsignedData = UnsignedData(age = 123, redactedEvent = Event()) + ) + fakeSession.eventService() + .givenGetEventReturns(event) + + val result = checkIfEventIsRedactedUseCase.execute(A_ROOM_ID, AN_EVENT_ID) + + result shouldBeEqualTo true + } + + @Test + fun `given a room id and event id for non redacted event when calling use case then false is returned`() = runTest { + val event = Event() + fakeSession.eventService() + .givenGetEventReturns(event) + + val result = checkIfEventIsRedactedUseCase.execute(A_ROOM_ID, AN_EVENT_ID) + + result shouldBeEqualTo false + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeEventService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeEventService.kt new file mode 100644 index 0000000000..167f1d624b --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeEventService.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2021 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.test.fakes + +import io.mockk.coEvery +import io.mockk.mockk +import org.matrix.android.sdk.api.session.events.EventService +import org.matrix.android.sdk.api.session.events.model.Event + +class FakeEventService : EventService by mockk() { + + fun givenGetEventReturns(event: Event) { + coEvery { getEvent(any(), any()) } returns event + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt index cebd45b2bb..ce498a715a 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt @@ -19,8 +19,11 @@ package im.vector.app.test.fakes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.every +import io.mockk.just import io.mockk.mockk +import io.mockk.runs import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary @@ -38,7 +41,7 @@ class FakeLocationSharingService : LocationSharingService by mockk() { fun givenLiveLocationShareSummaryReturns( eventId: String, - summary: LiveLocationShareAggregatedSummary + summary: LiveLocationShareAggregatedSummary? ): LiveData> { return MutableLiveData(Optional(summary)).also { every { getLiveLocationShareSummary(eventId) } returns it @@ -48,4 +51,17 @@ class FakeLocationSharingService : LocationSharingService by mockk() { fun givenStopLiveLocationShareReturns(result: UpdateLiveLocationShareResult) { coEvery { stopLiveLocationShare() } returns result } + + fun givenRedactLiveLocationShare(beaconInfoEventId: String, reason: String?) { + coEvery { redactLiveLocationShare(beaconInfoEventId, reason) } just runs + } + + /** + * @param inverse when true it will check redaction of the live did not happen + * @param beaconInfoEventId event id of the beacon related to the live + * @param reason reason explaining the redaction + */ + fun verifyRedactLiveLocationShare(inverse: Boolean = false, beaconInfoEventId: String, reason: String?) { + coVerify(inverse = inverse) { redactLiveLocationShare(beaconInfoEventId, reason) } + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt index e62214e310..18af88ba0f 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt @@ -38,6 +38,7 @@ class FakeSession( val fakeHomeServerCapabilitiesService: FakeHomeServerCapabilitiesService = FakeHomeServerCapabilitiesService(), val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService(), private val fakeRoomService: FakeRoomService = FakeRoomService(), + private val fakeEventService: FakeEventService = FakeEventService(), ) : Session by mockk(relaxed = true) { init { @@ -53,6 +54,7 @@ class FakeSession( override fun homeServerCapabilitiesService(): HomeServerCapabilitiesService = fakeHomeServerCapabilitiesService override fun sharedSecretStorageService() = fakeSharedSecretStorageService override fun roomService() = fakeRoomService + override fun eventService() = fakeEventService fun givenVectorStore(vectorSessionStore: VectorSessionStore) { coEvery { diff --git a/vector/src/test/java/org/matrix/android/sdk/api/session/room/model/VersioningStateTest.kt b/vector/src/test/java/org/matrix/android/sdk/api/session/room/model/VersioningStateTest.kt new file mode 100644 index 0000000000..473918d179 --- /dev/null +++ b/vector/src/test/java/org/matrix/android/sdk/api/session/room/model/VersioningStateTest.kt @@ -0,0 +1,50 @@ +/* + * 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 org.matrix.android.sdk.api.session.room.model + +import org.amshove.kluent.shouldBe +import org.junit.Test + +internal class VersioningStateTest { + + @Test + fun `when VersioningState is NONE, then isUpgraded returns false`() { + val versioningState = VersioningState.NONE + + val isUpgraded = versioningState.isUpgraded() + + isUpgraded shouldBe false + } + + @Test + fun `when VersioningState is UPGRADED_ROOM_NOT_JOINED, then isUpgraded returns true`() { + val versioningState = VersioningState.UPGRADED_ROOM_NOT_JOINED + + val isUpgraded = versioningState.isUpgraded() + + isUpgraded shouldBe true + } + + @Test + fun `when VersioningState is UPGRADED_ROOM_JOINED, then isUpgraded returns true`() { + val versioningState = VersioningState.UPGRADED_ROOM_JOINED + + val isUpgraded = versioningState.isUpgraded() + + isUpgraded shouldBe true + } +}