diff --git a/build.gradle b/build.gradle
index 0f94fc418c..cdbfcc44b7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -29,7 +29,7 @@ buildscript {
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730'
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5'
classpath "com.likethesalad.android:stem-plugin:2.2.3"
- classpath 'org.owasp:dependency-check-gradle:7.4.1'
+ classpath 'org.owasp:dependency-check-gradle:7.4.4'
classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.20"
classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0"
classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
diff --git a/changelog.d/7724.bugfix b/changelog.d/7724.bugfix
new file mode 100644
index 0000000000..685f7ad4e2
--- /dev/null
+++ b/changelog.d/7724.bugfix
@@ -0,0 +1 @@
+ Observe ViewEvents only when resumed and ensure ViewEvents are not lost.
diff --git a/changelog.d/7864.wip b/changelog.d/7864.wip
new file mode 100644
index 0000000000..e1187ee1e7
--- /dev/null
+++ b/changelog.d/7864.wip
@@ -0,0 +1,2 @@
+[Poll] Render active polls list of a room
+[Poll] Render past polls list of a room
diff --git a/changelog.d/7879.bugfix b/changelog.d/7879.bugfix
new file mode 100644
index 0000000000..be828ec2cc
--- /dev/null
+++ b/changelog.d/7879.bugfix
@@ -0,0 +1 @@
+Reduce number of crypto database transactions when handling the sync response
diff --git a/changelog.d/7899.bugfix b/changelog.d/7899.bugfix
new file mode 100644
index 0000000000..d95af29d8d
--- /dev/null
+++ b/changelog.d/7899.bugfix
@@ -0,0 +1 @@
+[Voice Broadcast] Stop listening if we reach the last received chunk and there is no last sequence number
diff --git a/changelog.d/7913.bugfix b/changelog.d/7913.bugfix
new file mode 100644
index 0000000000..32b821f14d
--- /dev/null
+++ b/changelog.d/7913.bugfix
@@ -0,0 +1 @@
+Handle network error on API `rooms/{roomId}/threads`
diff --git a/dependencies.gradle b/dependencies.gradle
index 76ff9b0c6f..e970457e7c 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -101,7 +101,7 @@ ext.libs = [
],
element : [
'opusencoder' : "io.element.android:opusencoder:1.1.0",
- 'wysiwyg' : "io.element.android:wysiwyg:0.13.0"
+ 'wysiwyg' : "io.element.android:wysiwyg:0.14.0"
],
squareup : [
'moshi' : "com.squareup.moshi:moshi:$moshi",
diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105160.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105160.txt
new file mode 100644
index 0000000000..69c2b3304c
--- /dev/null
+++ b/fastlane/metadata/android/cs-CZ/changelogs/40105160.txt
@@ -0,0 +1,2 @@
+Hlavní změny v této verzi: Vlákna jsou nyní povolena ve výchozím nastavení.
+Úplný seznam změn: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105180.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105180.txt
new file mode 100644
index 0000000000..69c2b3304c
--- /dev/null
+++ b/fastlane/metadata/android/cs-CZ/changelogs/40105180.txt
@@ -0,0 +1,2 @@
+Hlavní změny v této verzi: Vlákna jsou nyní povolena ve výchozím nastavení.
+Úplný seznam změn: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/de-DE/changelogs/40105160.txt b/fastlane/metadata/android/de-DE/changelogs/40105160.txt
new file mode 100644
index 0000000000..c55d8d998f
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/40105160.txt
@@ -0,0 +1,2 @@
+Die wichtigsten Änderungen in dieser Version: Threads sind nun standardmäßig aktiviert.
+Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/de-DE/changelogs/40105180.txt b/fastlane/metadata/android/de-DE/changelogs/40105180.txt
new file mode 100644
index 0000000000..c55d8d998f
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/40105180.txt
@@ -0,0 +1,2 @@
+Die wichtigsten Änderungen in dieser Version: Threads sind nun standardmäßig aktiviert.
+Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/eo/short_description.txt b/fastlane/metadata/android/eo/short_description.txt
index 33013ce78f..05a4aaf191 100644
--- a/fastlane/metadata/android/eo/short_description.txt
+++ b/fastlane/metadata/android/eo/short_description.txt
@@ -1 +1 @@
-Sekura kaj sencentrigita vokado kaj babilado. Tenu viajn datumojn sekuraj.
+Grupa mesaĝisto - ĉifrita mesaĝado, grupa babilejo kaj videovokoj
diff --git a/fastlane/metadata/android/eo/title.txt b/fastlane/metadata/android/eo/title.txt
index f56927e529..85b92c693b 100644
--- a/fastlane/metadata/android/eo/title.txt
+++ b/fastlane/metadata/android/eo/title.txt
@@ -1 +1 @@
-Element (antaŭe Riot.im)
+Element - Sekura Tujmesaĝilo
diff --git a/fastlane/metadata/android/et/changelogs/40105160.txt b/fastlane/metadata/android/et/changelogs/40105160.txt
new file mode 100644
index 0000000000..9aadf5dae8
--- /dev/null
+++ b/fastlane/metadata/android/et/changelogs/40105160.txt
@@ -0,0 +1,2 @@
+Põhilised muutused selles versioonis: jutulõngad on vaikimisi kasutusel.
+Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/et/changelogs/40105180.txt b/fastlane/metadata/android/et/changelogs/40105180.txt
new file mode 100644
index 0000000000..9aadf5dae8
--- /dev/null
+++ b/fastlane/metadata/android/et/changelogs/40105180.txt
@@ -0,0 +1,2 @@
+Põhilised muutused selles versioonis: jutulõngad on vaikimisi kasutusel.
+Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fa/changelogs/40105160.txt b/fastlane/metadata/android/fa/changelogs/40105160.txt
new file mode 100644
index 0000000000..0c3cc5aa31
--- /dev/null
+++ b/fastlane/metadata/android/fa/changelogs/40105160.txt
@@ -0,0 +1,2 @@
+تغییرات عمده در این نگارش: رشتهها اکنون به صورت پیشگزیده به کار افتادهاند.
+گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fa/changelogs/40105180.txt b/fastlane/metadata/android/fa/changelogs/40105180.txt
new file mode 100644
index 0000000000..0c3cc5aa31
--- /dev/null
+++ b/fastlane/metadata/android/fa/changelogs/40105180.txt
@@ -0,0 +1,2 @@
+تغییرات عمده در این نگارش: رشتهها اکنون به صورت پیشگزیده به کار افتادهاند.
+گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105160.txt b/fastlane/metadata/android/fr-FR/changelogs/40105160.txt
new file mode 100644
index 0000000000..4101bb0c86
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40105160.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : Fils de discussion activés par défaut.
+Intégralité des changements : https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105180.txt b/fastlane/metadata/android/fr-FR/changelogs/40105180.txt
new file mode 100644
index 0000000000..4101bb0c86
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40105180.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : Fils de discussion activés par défaut.
+Intégralité des changements : https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105160.txt b/fastlane/metadata/android/hu-HU/changelogs/40105160.txt
new file mode 100644
index 0000000000..c5dc38bc8f
--- /dev/null
+++ b/fastlane/metadata/android/hu-HU/changelogs/40105160.txt
@@ -0,0 +1,2 @@
+Legnagyobb változtatás ebben a verzióban: Új üzenetszálak alapból bekapcsolva!
+Teljes változási napló: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/hu-HU/changelogs/40105180.txt b/fastlane/metadata/android/hu-HU/changelogs/40105180.txt
new file mode 100644
index 0000000000..cc70967e58
--- /dev/null
+++ b/fastlane/metadata/android/hu-HU/changelogs/40105180.txt
@@ -0,0 +1,2 @@
+Legnagyobb változtatás ebben a verzióban: Az üzenetszálak alapból bekapcsolva!
+Teljes változási napló: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/id/changelogs/40105160.txt b/fastlane/metadata/android/id/changelogs/40105160.txt
new file mode 100644
index 0000000000..173a1bfb1b
--- /dev/null
+++ b/fastlane/metadata/android/id/changelogs/40105160.txt
@@ -0,0 +1,2 @@
+Perubahan utama dalam versi ini: Utasan sekarang diaktifkan secara bawaan.
+Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/id/changelogs/40105180.txt b/fastlane/metadata/android/id/changelogs/40105180.txt
new file mode 100644
index 0000000000..173a1bfb1b
--- /dev/null
+++ b/fastlane/metadata/android/id/changelogs/40105180.txt
@@ -0,0 +1,2 @@
+Perubahan utama dalam versi ini: Utasan sekarang diaktifkan secara bawaan.
+Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/it-IT/changelogs/40105160.txt b/fastlane/metadata/android/it-IT/changelogs/40105160.txt
new file mode 100644
index 0000000000..ab24842e82
--- /dev/null
+++ b/fastlane/metadata/android/it-IT/changelogs/40105160.txt
@@ -0,0 +1,2 @@
+Modifiche principali in questa versione: i messaggi in conversazioni sono attivi in modo predefinito.
+Cronologia completa: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/it-IT/changelogs/40105180.txt b/fastlane/metadata/android/it-IT/changelogs/40105180.txt
new file mode 100644
index 0000000000..ab24842e82
--- /dev/null
+++ b/fastlane/metadata/android/it-IT/changelogs/40105180.txt
@@ -0,0 +1,2 @@
+Modifiche principali in questa versione: i messaggi in conversazioni sono attivi in modo predefinito.
+Cronologia completa: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sk/changelogs/40105160.txt b/fastlane/metadata/android/sk/changelogs/40105160.txt
new file mode 100644
index 0000000000..d5b5ad330d
--- /dev/null
+++ b/fastlane/metadata/android/sk/changelogs/40105160.txt
@@ -0,0 +1,2 @@
+Hlavné zmeny v tejto verzii: Vlákna sú teraz predvolene zapnuté.
+Úplný zoznam zmien: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/sk/changelogs/40105180.txt b/fastlane/metadata/android/sk/changelogs/40105180.txt
new file mode 100644
index 0000000000..d5b5ad330d
--- /dev/null
+++ b/fastlane/metadata/android/sk/changelogs/40105180.txt
@@ -0,0 +1,2 @@
+Hlavné zmeny v tejto verzii: Vlákna sú teraz predvolene zapnuté.
+Úplný zoznam zmien: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/uk/changelogs/40105160.txt b/fastlane/metadata/android/uk/changelogs/40105160.txt
new file mode 100644
index 0000000000..edbd209d17
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/40105160.txt
@@ -0,0 +1,2 @@
+Основні зміни в цій версії: Гілки відтепер типово ввімкнено.
+Перелік усіх змін: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/uk/changelogs/40105180.txt b/fastlane/metadata/android/uk/changelogs/40105180.txt
new file mode 100644
index 0000000000..edbd209d17
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/40105180.txt
@@ -0,0 +1,2 @@
+Основні зміни в цій версії: Гілки відтепер типово ввімкнено.
+Перелік усіх змін: https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105160.txt b/fastlane/metadata/android/zh-TW/changelogs/40105160.txt
new file mode 100644
index 0000000000..9c66f3c2ad
--- /dev/null
+++ b/fastlane/metadata/android/zh-TW/changelogs/40105160.txt
@@ -0,0 +1,2 @@
+此版本中的主要變動:討論串現在預設啟用。
+完整的變更紀錄:https://github.com/vector-im/element-android/releases
diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105180.txt b/fastlane/metadata/android/zh-TW/changelogs/40105180.txt
new file mode 100644
index 0000000000..9c66f3c2ad
--- /dev/null
+++ b/fastlane/metadata/android/zh-TW/changelogs/40105180.txt
@@ -0,0 +1,2 @@
+此版本中的主要變動:討論串現在預設啟用。
+完整的變更紀錄:https://github.com/vector-im/element-android/releases
diff --git a/library/ui-strings/src/main/res/values-da/strings.xml b/library/ui-strings/src/main/res/values-da/strings.xml
index 13d53b7bb2..35c93949f9 100644
--- a/library/ui-strings/src/main/res/values-da/strings.xml
+++ b/library/ui-strings/src/main/res/values-da/strings.xml
@@ -39,7 +39,6 @@
Telefonnummer
Invitation til rum
%1$s og %2$s
-
Tomt rum
Lyst Tema
Mørkt Tema
@@ -82,7 +81,6 @@
Kun Matrix kontakter
Ingen resultater
Rum
-
Send logfiler
Send crashlogfiler
Send screenshot
@@ -110,10 +108,8 @@
Dette ligner ikke en gyldig emailadresse
Den emailadresse er allerede i brug.
Glemt adgangskode?
-
Denne Home Server vil gerne være sikker på du ikke er en robot
Kunne ikke verificere emailadresse: vær sikker på du klikkede på linket i emailen
-
Skriv gyldig URL
Fejlformet JSON
Indeholdt ikke gyldig JSON
@@ -134,15 +130,10 @@
Opkald I Gang
Den anden side tog den ikke.
Information
-
-
${app_name} skal bruge tilladelse til at bruge din mikrofon for at lave lydopkald.
-
${app_name} skal bruge tilladelse til at bruge dit kamera og din mikrofon for at lave videoopkald.
Giv venligst tilladelse ved næste pop-up for at lave opkaldet.
-
-
JA
NEJ
Fortsæt
@@ -150,7 +141,6 @@ Giv venligst tilladelse ved næste pop-up for at lave opkaldet.
Forbind
Afvis
Spring til første ulæste besked.
-
Forlad rum
Er du sikker på at du vil forlade rummet?
DIREKTE CHATS
@@ -163,7 +153,7 @@ Giv venligst tilladelse ved næste pop-up for at lave opkaldet.
Du vil ikke kunne omgøre denne ændring da du forfremmer brugeren til at have samme magt niveau som dig selv.
Er du sikker?
%s skriver…
- "%1$s & %2$s skriver…"
+ %1$s & %2$s skriver…
%1$s, %2$s og andre skriver…
Du har ikke tilladelse til at skrive i dette rum
Stol på
@@ -186,7 +176,6 @@ Er du sikker?
- %d medlemsændringer
Medlemsoversigt
-
- 1 medlem
- %d medlemmer
@@ -199,8 +188,6 @@ Er du sikker?
Søg
Filtrer medlemmer i rum
Ingen resultater
-
-
Alle meddelelser
Opret genvej på startskærm
Profilbillede
@@ -291,4 +278,4 @@ Er du sikker?
%1$s oprettede rummet
Din invitation
Forbind denne email med din konto
-
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml
index 3f2ea694ca..52b8f0c716 100644
--- a/library/ui-strings/src/main/res/values-de/strings.xml
+++ b/library/ui-strings/src/main/res/values-de/strings.xml
@@ -2889,4 +2889,13 @@
Link erstellen
Link
Text
+ Dein Zugriffstoken gewährt vollen Zugriff auf dein Konto. Teile ihn mit niemandem.
+ Zugriffstoken
+ Unsortierte Liste umschalten
+ Nummerierte Liste umschalten
+ In diesem Raum gibt es noch keine abgeschlossenen Umfragen
+ Vergangene Umfragen
+ In diesem Raum gibt es keine aktiven Umfragen
+ Aktive Umfragen
+ Umfrageverlauf
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-eo/strings.xml b/library/ui-strings/src/main/res/values-eo/strings.xml
index f536ca00f9..4521e840a6 100644
--- a/library/ui-strings/src/main/res/values-eo/strings.xml
+++ b/library/ui-strings/src/main/res/values-eo/strings.xml
@@ -718,8 +718,8 @@
Bonvolu enigi la URL-on de identiga servilo
Ne povis konektiĝi al identiga servilo
Enigu URL-on de identiga servilo
- Ni sendis al vi konfirman retleteron al %s, bonvolu unue kontroli vian retpoŝton kaj klaki la konfirman ligilon
- Ni sendis al vi konfirman retleteron al %s; kontrolu vian retpoŝton kaj klaku la konfirman ligilon
+ Ni sendis retleteron al %s, bonvolu unue kontroli vian retpoŝton kaj klaki la konfirman ligilon
+ Ni sendis retleteron al %s; kontrolu vian retpoŝton kaj klaku la konfirman ligilon
Troveblaj telefonnumeroj
Malkonekto de via identiga servilo signifas, ke vi ne estos trovebla de aliaj uzantoj kaj ne povos inviti aliulojn per retpoŝtadreso aŭ telefono.
Elektebloj pri trovado aperos post aldono de telefonnumero.
@@ -2201,4 +2201,4 @@
Sonorante…
Aroj
- Iom uzantoj reatentita
-
+
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-es/strings.xml b/library/ui-strings/src/main/res/values-es/strings.xml
index e07c21d6a5..c06442b5d0 100644
--- a/library/ui-strings/src/main/res/values-es/strings.xml
+++ b/library/ui-strings/src/main/res/values-es/strings.xml
@@ -50,7 +50,7 @@
%1$s ha invitado a %2$s. Razón: %3$s
%1$s te ha invitado. Razón: %2$s
%1$s se ha unido. Razón: %2$s
- %1$s se ha ido. Razón: %2$s
+ %1$s dejó la sala. Razón: %2$s
%1$s ha rechadazo la invitación. Razón: %2$s
%1$s expulsó a %2$s. Razón: %3$s
%1$s ha baneado a %2$s. Razón: %3$s
@@ -81,17 +81,17 @@
%1$s ha permitido que los invitados se unan a la sala.
%1$s ha impedido que los invitados se unan a la sala.
%1$s ha activado el cifrado Extremo-a-Extremo.
- %1$s ha activado el cifrado Extremo-a-Extremo (algoritmo no reconocido %2$s).
+ %1$s ha activado el cifrado extremo-a-extremo (algoritmo no reconocido %2$s).
Tu invitación
%1$s creó la sala
Creaste la sala
Invitaste a %1$s
Te uniste a la Sala
- Dejaste la Sala
+ Dejaste la sala
Rechazaste la invitación
Tu pateaste a %1$s
Tu desbanaste a %1$s
- Usted prohibió a %1$s
+ Excluiste a %1$s
Retiró la invitación de %1$s\'s
Cambiaste tu avatar
Establece su nombre de visualización en %1$s
@@ -152,10 +152,10 @@
Agregaste %1$s y quitaste %2$s como direcciones para esta sala.
Estableciste la dirección principal de esta sala en %1$s.
Quitaste la dirección principal de esta sala.
- Ha permitido que los invitados se unan a la sala.
- Ha impedido que los invitados se unan a la sala.
+ Has permitido que los invitados se unan a la sala.
+ Has impedido que los invitados se unan a la sala.
Has activado el cifrado Extremo-a-Extremo.
- Has activado el cifrado Extremo-a-Extremo (algoritmo %1$s no reconocido).
+ Has activado el cifrado extremo-a-extremo (algoritmo %1$s no reconocido).
Has impedido que invitados se unan a la sala.
Has permitido a invitados unirse aquí.
Te has ido. Razón: %1$s
@@ -163,7 +163,7 @@
Has invitado a %1$s
Has actualizado aquí.
Has hecho futuros mensajes visibles a %1$s
- Te saliste de la sala
+ Has dejado la sala
Te uniste
Creaste la conversación
%1$s ha impedido que invitados se unan a la sala.
@@ -255,7 +255,7 @@
Salas y Grupos
Filtrar salas
Invitaciones
- Prioridad baja
+ Baja prioridad
Conversaciones
Solo contactos de Matrix
No hay resultados
@@ -429,7 +429,7 @@
Importar
Cifrar solo a sesiones verificadas
Nunca enviar mensajes cifrados a sesiones sin verificar desde esta sesión.
- SIN Verificar
+ Sin Verificar
Verificado
Verificar
Para verificar que esta sesión es confiable, por favor contacta a su dueño por algún otro medio (ej. cara a cara o por teléfono) y pregúntale si la clave que ve en sus Ajustes de Usuario para esta sesión coincide con la clave a continuación:
@@ -819,7 +819,7 @@
La copia de seguridad tiene una firma valida de la sesión no verificada %s
La copia de seguridad tiene una firma inválida de la sesión verificada %s
La copia de seguridad tiene una firma inválida de la sesión no verificada %s
- Para usar la copia de seguridad de la clave en esta sesión introduzca su contraseña o su clave de recuperación ahora.
+ Para usar la copia de seguridad de la clave en esta sesión introduce tu contraseña o tu clave de recuperación ahora.
¿Deseas borrar tus claves de cifrado guardadas en el servidor\? No podrás usar tu clave de recuperación para leer el historial de mensajes cifrados.
Reproducir sonido de cámara
ip desconocida
@@ -1182,7 +1182,7 @@
%s cancelada
Cancelado por usted
%s aceptada
- Aceptado por usted
+ Aceptaste
Verificacion enviada
Solicitud de verificación
Verifica esta Sesion
@@ -1239,7 +1239,7 @@
Precaucion
Error al obtener sesiones
Sesiones
- Confirmado
+ Confiable
No es confiable
Inicializar Firmas Cruzadas
Restablecer claves
@@ -1255,7 +1255,7 @@
Razón para redactar
${app_name} Android
Refrescar
- Nuevo inicio de sesión detectado . ¿Fue usted\?
+ Nuevo inicio de sesión detectado . ¿Has sido tú\?
Este no era yo
Su cuenta puede estar comprometida
Verificación cancelada
@@ -1263,7 +1263,7 @@
Clave de mensaje
¡Listo!
Cifrado habilitado
- Sala creada y configurada por usted.
+ Creaste y configuraste la sala.
Esperando por %s…
Ajuste de Notificaciones
Mensaje…
@@ -1279,7 +1279,7 @@
Si decea resetear su PIN, toque Olvidé PIN para cerrar sesión y restablecer.
Numeros telefonicos
Correos y numeros telefonicos
- Administre el correo y numero telefonico de su cuenta
+ Administra las direcciones de correo y/o números telefónicos relacionados a tu cuenta de Matrix
Mostrar mensajes eliminados
Indicar marca de mensaje eliminado
ARCHIVOS
@@ -1357,7 +1357,7 @@
Hiciste la sala solo por invitación.
Únase gratis a millones de personas en el mayor servidor público
Continuar con SSO
- Dirección de servicios de Element Matrix
+ Dirección de Element Matrix Services
Ingrese la dirección del servidor que desea utilizar
Se enviará un correo electrónico de verificación a su bandeja de entrada para confirmar la configuración de su nueva contraseña.
Siguiente
@@ -1407,7 +1407,7 @@
Advertencia
Tu cuenta aún no está creada. ¿Detener el proceso de registro\?
Seleccione matrix.org
- Seleccionar servicios de matriz de elementos
+ Seleccionar Element Matrix Services
Seleccione un servidor doméstico personalizado
Realiza el desafío de captcha
Acepta los términos para continuar
@@ -1453,7 +1453,7 @@
\nVuelva a iniciar sesión para acceder a los datos y mensajes de su cuenta.
Perderás el acceso a los mensajes seguros a menos que inicies sesión para recuperar tus claves de cifrado.
La sesión actual es para el usuario %1$s y usted proporciona las credenciales para el usuario %2$s. Esto no está suportado por ${app_name}.
-\nPrimero borre los datos, luego inicie sesión nuevamente con otra cuenta.
+\nPrimero borra los datos, luego inicia sesión nuevamente con otra cuenta.
Su enlace matrix.to estaba mal formado
El modo desarrollador activa funciones ocultas y también puede hacer que la aplicación sea menos estable. ¡Solo para desarrolladores!
Uno de los siguientes puede verse comprometido:
@@ -1462,9 +1462,9 @@
\n- El servidor privado al que está conectado el usuario que estás verificando
\n- Su conexión a internet o la de otros usuarios
\n- Su dispositivo o el de otros usuarios
- Los mensajes de esta sala están cifrados Extremo-a-Extremo.
+ Los mensajes de esta sala están cifrados de extremo-a-extremo.
\n
-\nSus mensajes están protegidos y sólo usted y el destinatario tienen las claves únicas para descifrarlos.
+\nTus mensajes están protegidos y sólo tu y el destinatario tienen las claves únicas para descifrarlos.
Esta sesión no puede compartir esta verificación con sus otras sesiones.
\nLa verificación se guardará localmente y se compartirá en una versión futura de la aplicación.
Envía el emote dado coloreado como un arcoíris
@@ -1474,7 +1474,7 @@
Verifica si los mismos emojis aparecen en el mismo orden en ambos usuarios.
Compare el código con el que se muestra en la pantalla del otro usuario.
Los mensajes con este usuario están cifrados Extremo-a-Extremo y no pueden ser leídos por terceros.
- Su nueva sesión ahora está verificada. Tiene acceso a sus mensajes cifrados y otros usuarios lo verán como de confianza.
+ Tu nueva sesión acaba de verificarse y ahora tiene acceso a tus mensajes cifrados y otros usuarios la verán como de confianza.
La firma cruzada está habilitada
\n Claves privadas en el dispositivo.
La firma cruzada está habilitada
@@ -1484,8 +1484,8 @@
\nLas claves no son de confianza
El administrador de su servidor ha desactivado el cifrado Extremo-a-Extremo de forma predeterminada en salas privadas y mensajes directos.
No hay información criptográfica disponible
- Esta sesión es confiable para mensajería segura porque usted la verificó:
- Verifique esta sesión para marcarla como confiable y otorgarle acceso a mensajes cifrados. Si no inició sesión en esta sesión, su cuenta puede verse comprometida:
+ Esta sesión es confiable para mensajería segura porque la verificaste:
+ Verifica esta sesión para marcarla como confiable y otorgarle acceso a mensajes cifrados. Si no iniciaste sesión en esta sesión, su cuenta puede haber sido comprometida:
- %d sesión activa
- %d sesiones activas
@@ -1510,8 +1510,8 @@
Solicitudes clave
Desbloquear el historial de mensajes cifrados
Utilice esta sesión para verificar su nuevo, otorgándole acceso a mensajes cifrados.
- Si cancela, no podrá leer mensajes cifrados en este dispositivo y otros usuarios no confiarán en él
- Si cancela, no podrá leer mensajes cifrados en su nuevo dispositivo y otros usuarios no confiarán en él
+ Si cancelas, no podrás leer mensajes cifrados en este dispositivo y otros usuarios no confiarán en este
+ Si cancelas, no podrás leer mensajes cifrados en tu nuevo dispositivo y otros usuarios no confiarán en este
No verificarás %1$s (%2$s) si cancelas ahora. Comience de nuevo en su perfil de usuario.
Uno de los siguientes puede verse comprometido:
\n
@@ -1524,7 +1524,7 @@
Se canceló la verificación. Puede iniciar la verificación de nuevo.
Ingrese su %s para continuar.
No use la contraseña de su cuenta.
- Ingrese una frase de seguridad que solo usted conozca, que se usa para proteger secretos en su servidor.
+ Ingresa una frase de seguridad que solo tú conozcas, que se usa para proteger secretos en tu servidor.
Esto puede tardar varios segundos, tenga paciencia.
Configurando la recuperación.
Manténlo seguro
@@ -1561,7 +1561,7 @@
Nombre de usuario y / o contraseña incorrectos. La contraseña ingresada comienza o termina con espacios, verifíquela.
Esta cuenta ha sido desactivada.
Mejora de cifrado disponible
- Verifíquese a usted mismo y a los demás para mantener sus chats seguros
+ Verifícate a ti mismo y a los demás para mantener tus chats seguros
No es una clave de recuperación válida
Por favor introduce una clave de recuperación
Comprobando la clave de respaldo
@@ -1622,11 +1622,11 @@
Usa una llave de seguridad
Genere una clave de seguridad para almacenar en un lugar seguro, como un administrador de contraseñas o una caja fuerte.
Utilice una frase de seguridad
- Ingrese una frase secreta que solo usted conozca y genere una clave de respaldo.
+ Ingresa una frase secreta que solo tú conozcas y genera una clave para tu copia de respaldo.
Guarde su llave de seguridad
Guarde su llave de seguridad en un lugar seguro, como un administrador de contraseñas o una caja fuerte.
Establecer una frase de seguridad
- Ingrese una frase de seguridad que solo usted conozca, que se usa para proteger secretos en su servidor.
+ Ingresa una frase de seguridad que sólo tú conozcas, que se usa para proteger secretos en tu servidor.
Frase de seguridad
Ingrese su Frase de seguridad nuevamente para confirmarla.
Nombre de la Sala
@@ -1636,7 +1636,7 @@
Esperando este mensaje, esto puede tardar un poco
Debido al cifrado Extremo-a-Extremo, es posible que deba esperar a que llegue el mensaje de alguien porque las claves de cifrado no se le enviaron correctamente.
No puede acceder a este mensaje porque ha sido bloqueado por el remitente
- No puede acceder a este mensaje porque el remitente no confía en su sesión
+ No puedes acceder a este mensaje porque el remitente no confía en tu sesión
No puede acceder a este mensaje porque el remitente no envió las claves a propósito
Esperando al historial de cifrado
¡Nos complace anunciar que hemos cambiado de nombre! Tu aplicación está actualizada y accediste a tu cuenta.
@@ -1711,7 +1711,7 @@
- Mostrar el dispositivo con el que puede verificar ahora
- Mostrar %d dispositivos con los que puede verificar ahora
- Reiniciará sin historia, mensajes, dispositivos o usuarios verificados
+ Reiniciarás sin historial, ni mensajes, ni dispositivos o usuarios verificados
Si resetea todo
Solo haga esto si no tiene otro dispositivo con el que verificar éste.
Resetear todo
@@ -1748,7 +1748,7 @@
Se necesita una nueva autenticación
¡Código QR no escaneado!
Código QR no válido (URL no válida)!
- No puede DM usted mismo!
+ No puedes MD a ti mismo!
Compartir por texto
Cambiar PIN
Cambie su PIN actual
@@ -1960,7 +1960,7 @@
Acceso a la sala
Siempre preguntar
Espacios
- mostrar todas las salas en el directorio de salas, incluyendo salas con contenido explícito.
+ Mostrar todas las salas en el directorio de salas, incluyendo salas con contenido explícito.
Mostrar salas con contenido explícito
Directorio de la sala
Salas sugeridas
@@ -2181,7 +2181,7 @@
Agregar nuevas palabra clave
Tus palabras clave
Habilitar notificación por correo electrónico para %s
- Para recibir un correo electrónico con una notificación, asocie un correo electrónico a su cuenta de matrix
+ Para recibir notificaciones por correo electrónico, asocia una direccion de correo electrónico a tu cuenta de Matrix
Notificación de correo electrónico
Ninguno
Solo menciones y palabras clave
@@ -2219,7 +2219,7 @@
No se puede grabar un mensaje de voz
No se puede reproducir este mensaje de voz
Toca tu grabación para detenerla o escucharla
- %1$ds dejado
+ Restan %1$ds
Mantenga presionado para grabar, suelte para enviar
Eliminar grabación
Grabación de mensaje de voz
@@ -2238,7 +2238,7 @@
¡Se ha cerrado la sesión!
¡Se ha abandonado la sala!
Consejo: Pulse prolongadamente un mensaje y use \"%s\" .
- Mantén las conversaciones organizadas con hilos
+ Mantén las conversaciones organizadas usando hilos
Muestra todos los hilos en que has participado
Mis Hilos
Muestra todos los hilos de la sala actual
@@ -2457,7 +2457,7 @@
BETA
Comentarios de la beta de hilos
Beta de hilos
- - Algunos usuarios han sido dejados de ignorar
+ - Algunos usuarios han dejado de ser ignorados
La compartición de pantalla está en progreso
${app_name} Compartición de pantalla
Dejar de compartir pantalla
@@ -2474,7 +2474,7 @@
Actualizado hace %1$s
Implementación temporal: las ubicaciones persisten en el historial de la sala
Activar compartir ubicación en tiempo real
- Queda %1$s
+ Restan %1$s
Compartiendo hasta %1$s
Ver ubicación en tiempo real
La ubicación en tiempo real ha terminado
@@ -2625,8 +2625,8 @@
\nPor favor, inténtelo de nuevo.%s
Usar ajustes por defecto del sistema
Escoger manualmente
- Tamaño automático de fuente
- Escoger tamaño de la fuente
+ Tamaño automático
+ Escoge tamaño del tipo de letra
- %1$s y %2$d otro
- %1$s y %2$d otros
@@ -2659,18 +2659,33 @@
Otorgar permiso
${app_name} necesita permiso para mostrar notificaciones.
\nPor favor, otórgalo.
- ${app_name} necesita permisos para mostrar notificaciones. Las notificaciones pueden mostrar tus mensajes, invitaciones, etc.
+ ${app_name} necesita permiso para mostrar notificaciones. Las notificaciones pueden mostrar tus mensajes, invitaciones, etc.
\n
-\nPor favor, permite el acceso en las siguientes ventanas emergentes para poder visualizar notificaciones.
- Prueba el editor de texto enriquecido (pronto llegará la opción de texto sin formato plain text)
+\nPor favor, a continuacion, en las ventanas emergentes, permite el acceso para poder visualizar notificaciones.
+ Prueba el editor de texto enriquecido (pronto llegará la opción de texto simple, sin formato)
Habilitar editor de texto enriquecido (rich text)
Crear MD únicamente al primer mensaje
Una versión simplificada de Element con pestañas opcionales
Habilitar nueva disposición
- Sí, Parar
+ Sí, Detener
Deseleccionar todo
- Ocultar los hijos de %s
- Mostrar los hijos de %s
+ Ocultar los subespacios de %s
+ Mostrar los subespacios de %s
Has finalizado una transmisión de voz.
%1$s ha finalizado una transmisión de voz.
+ Element Matrix Services (EMS) es un servicio de alojamiento para tus comunicaciones en tiempo real. Robusto, confiable, rápido y seguro. Para saber cómo, ve a <a href=\"${ftue_ems_url}\">element.io/ems</a>
+ Difusión de voz
+ Habilitado:
+ ID de sesión:
+ Algo falló. Por favor, comprueba tu conexión de red e inténtalo nuevamente.
+ Citando
+ Respondiendo a %s
+ Editando
+ Abrir pantalla de herramientas de desarrollador
+ 🔒 Tienes habilitado el cifrado a sesiones verificadas sólo para todas las salas en Ajustes de Seguridad.
+ ⚠ Hay dispositivos sin verificar en esta sala, los cuales no seran capaces de descifrar los mensajes que envías.
+ Habilita MDs pospuestos
+ Mostrar chats recientes en el menú de compartir sistema
+ No enviar nunca mensajes cifrados a sesiones sin verificar en esta sala.
+ Restan %1$s
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-et/strings.xml b/library/ui-strings/src/main/res/values-et/strings.xml
index 490145b5cc..1e8e2b989e 100644
--- a/library/ui-strings/src/main/res/values-et/strings.xml
+++ b/library/ui-strings/src/main/res/values-et/strings.xml
@@ -2881,4 +2881,13 @@
Link
Loo link
Muuda linki
+ Küsitluste ajalugu
+ Käimasolevad küsitlused
+ Selles jututoas pole käimasolevaid küsitlusi
+ Varasemad küsitlused
+ Selles jututoas pole varasemaid küsitlusi
+ Lülita nummerdatud loend sisse/välja
+ Lülita täpploend sisse/välja
+ Pääsuluba
+ Sinu pääsuluba annab täismahulise ligipääsu sinu kasutajakontole. Palun ära jaga seda teistega.
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-fa/strings.xml b/library/ui-strings/src/main/res/values-fa/strings.xml
index 56dbe912c1..4db3812237 100644
--- a/library/ui-strings/src/main/res/values-fa/strings.xml
+++ b/library/ui-strings/src/main/res/values-fa/strings.xml
@@ -2890,4 +2890,13 @@
پیوند
متن
تنظیم پیوند
+ ژتون دسترسیتان، دسترسی کامل به حسابتان را میدهد. با هیچکس همرسانیش نکنید.
+ ژتون دسترسی
+ تغییر وضعیت سیاههٔ گلولهای
+ تغییر وضعیت سیاههٔ شمارهدار
+ هیچ نظرسنجی قدیمیای در این اتاق وجود ندارد
+ نظرسنجیهای گذشته
+ هیچ نظرسنجی فعّالی در این اتاق وجود ندارد
+ نظرسنجیهای فعّال
+ تاریخچهٔ نظرسنجیها
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-fr/strings.xml b/library/ui-strings/src/main/res/values-fr/strings.xml
index 33f1410502..cb1684f834 100644
--- a/library/ui-strings/src/main/res/values-fr/strings.xml
+++ b/library/ui-strings/src/main/res/values-fr/strings.xml
@@ -2890,4 +2890,13 @@
Lien
Texte
Définir un lien
+ Votre jeton d’accès donne un accès intégral à votre compte. Ne le partagez avec personne.
+ Jeton d’accès
+ (Dés)activer la liste à puce
+ (Dés)activer la liste numérotée
+ Il n’y a aucun ancien sondage dans ce salon
+ Anciens sondages
+ Il n’y a aucun sondage en cours dans ce salon
+ Sondages actifs
+ Historique des sondages
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-in/strings.xml b/library/ui-strings/src/main/res/values-in/strings.xml
index 8715aa84e0..8896037037 100644
--- a/library/ui-strings/src/main/res/values-in/strings.xml
+++ b/library/ui-strings/src/main/res/values-in/strings.xml
@@ -2836,4 +2836,13 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.
Tautan
Teks
Atur tautan
+ Token akses Anda memberikan akses penuh ke akun Anda. Jangan bagikan dengan siapa pun.
+ Token Akses
+ Saklar daftar bulat
+ Saklar daftar bernomor
+ Tidak ada pemungutan suara sebelumnya di ruangan ini
+ Pemungutan suara sebelumnya
+ Tidak ada pemungutan suara yang aktif di ruangan ini
+ Pemungutan suara aktif
+ Riwayat pemungutan suara
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-it/strings.xml b/library/ui-strings/src/main/res/values-it/strings.xml
index b1721449ad..729b826982 100644
--- a/library/ui-strings/src/main/res/values-it/strings.xml
+++ b/library/ui-strings/src/main/res/values-it/strings.xml
@@ -2881,4 +2881,13 @@
Collegamento
Testo
Imposta collegamento
+ Il tuo token di accesso ti dà l\'accesso al tuo account. Non condividerlo con nessuno.
+ Token di accesso
+ Attiva/disattiva elenco numerato
+ Attiva/disattiva elenco puntato
+ In questa stanza non ci sono sondaggi passati
+ Sondaggi passati
+ In questa stanza non ci sono sondaggi attivi
+ Sondaggi attivi
+ Cronologia sondaggi
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml
index bc617c62f0..a5aa778156 100644
--- a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml
+++ b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml
@@ -5,24 +5,24 @@
%1$s convidou você
%1$s entrou na sala
%1$s saiu da sala
- %1$s rejeitou o convite
+ %1$s recusou o convite
%1$s removeu %2$s
%1$s desbaniu %2$s
%1$s baniu %2$s
- %1$s retirou o convite de %2$s
- %1$s mudou o avatar dela(e)
- %1$s definiu o nome de exibição dela(e) para %2$s
- %1$s mudou o nome de exibição dela(e) de %2$s para %3$s
- %1$s removeu o nome de exibição dela(e) (era %2$s)
+ %1$s desfez o convite para %2$s
+ %1$s mudou seu avatar
+ %1$s definiu seu nome de exibição para %2$s
+ %1$s mudou seu nome de exibição de %2$s para %3$s
+ %1$s removeu seu nome de exibição (era %2$s)
%1$s mudou o tópico para: %2$s
%1$s mudou o nome da sala para: %2$s
%s começou uma chamada de vídeo.
%s começou uma chamada de voz.
%s atendeu a chamada.
- %s terminou a chamada.
- %1$s fez histórico futuro da sala visível para %2$s
- todos os membros da sala, do ponto que foram convidados.
- todos os membros da sala, do ponto que se juntaram.
+ %s encerrou a chamada.
+ %1$s tornou o histórico futuro da sala visível para %2$s
+ todos os membros da sala, a partir do ponto que foram convidados.
+ todos os membros da sala, a partir do ponto que entraram.
todos os membros da sala.
qualquer pessoa.
(avatar mudou também)
@@ -45,11 +45,11 @@
Você convidou %1$s
Você entrou na sala
Você saiu da sala
- Você rejeitou o convite
+ Você recusou o convite
Você removeu %1$s
Você desbaniu %1$s
Você baniu %1$s
- Você retirou o convite de %1$s
+ Você desfez o convite para %1$s
Você mudou seu avatar
Você definiu seu nome de exibição para %1$s
Você mudou seu nome de exibição de %1$s para %2$s
@@ -63,8 +63,8 @@
%s enviou dados para configurar a chamada.
Você enviou dados para configurar a chamada.
Você atendeu a chamada.
- Você terminou a chamada.
- Você fez histórico futuro da sala visível para %1$s
+ Você encerrou a chamada.
+ Você tornou o histórico futuro da sala visível para %1$s
%s fez o upgrade desta sala.
Você fez o upgrade desta sala.
Você removeu o nome da sala
@@ -170,8 +170,8 @@
%1$s convidou %2$s
Você fez o upgrade aqui.
%s fez o upgrade aqui.
- Você fez mensagens futuras visíveis para %1$s
- %1$s fez mensagens futuras visíveis para %2$s
+ Você tornou as mensagens futuras visíveis para %1$s
+ %1$s tornou as mensagens futuras visíveis para %2$s
Você saiu da sala
%1$s saiu da sala
Você entrou
@@ -408,7 +408,7 @@
Qualquer pessoa
Membros somente (desde o ponto no tempo de seleção desta opção)
Membros somente (desde que eles foram convidados)
- Membros somente (desde que eles se juntaram)
+ Membros somente (desde que eles entraram)
Usuárias(os) banidas(os)
Avançadas
ID interno desta sala
@@ -2009,8 +2009,8 @@
Adicionar salas
Explorar salas
- - %d pessoa que você conhece já tem se juntado
- - %d pessoas que você conhece já têm se juntado
+ - %d pessoa que você conhece já entrou
+ - %d pessoas que você conhece já entraram
Juntar-Se a Espaço
Criar espaço
@@ -2833,8 +2833,8 @@
Desselecionar todas(os)
Selecionar todas(os)
- - %1$d selecionada(o)
- - %1$d selecionadas(os)
+ - %1$d selecionado(a)
+ - %1$d selecionados(as)
Alguma outra pessoa já está gravando um broadcast de voz. Espere que o broadcast de voz dela termine para começar um novo.
Alternar modo de tela cheia
diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml
index d46b4e8898..34155ba6a5 100644
--- a/library/ui-strings/src/main/res/values-sk/strings.xml
+++ b/library/ui-strings/src/main/res/values-sk/strings.xml
@@ -2946,4 +2946,13 @@
Odkaz
Text
Nastaviť odkaz
+ Váš prístupový token poskytuje úplný prístup k vášmu účtu. S nikým ho nezdieľajte.
+ Prístupový token
+ Prepnúť na číslovaný zoznam
+ Prepnúť zoznam s odrážkami
+ V tejto miestnosti nie sú žiadne predchádzajúce ankety
+ Predchádzajúce ankety
+ V tejto miestnosti nie sú žiadne aktívne ankety
+ Aktívne ankety
+ História ankety
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-sq/strings.xml b/library/ui-strings/src/main/res/values-sq/strings.xml
index 3b233c087c..c3f9d53c99 100644
--- a/library/ui-strings/src/main/res/values-sq/strings.xml
+++ b/library/ui-strings/src/main/res/values-sq/strings.xml
@@ -2874,4 +2874,12 @@
Krijoni një lidhje
Lidhje
Tekst
+ Tokeni juaj i hyrjeve jep hyrje të plotë në llogarinë tuaj. Mos ia jepni kujt.
+ Token Hyrjesh
+ S’ka pyetësorë të kaluar në këtë dhomë
+ Pyetësorë të kaluar
+ S’ka pyetësorë aktivë në këtë dhomë
+ Pyetësorë aktivë
+ mundësia fituese
+ Historik pyetësorësh
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml
index ebe2dcaf4d..2ee9685c76 100644
--- a/library/ui-strings/src/main/res/values-uk/strings.xml
+++ b/library/ui-strings/src/main/res/values-uk/strings.xml
@@ -3002,4 +3002,13 @@
Посилання
Текст
Налаштувати посилання
+ Ваш токен доступу надає повний доступ до вашого облікового запису. Не передавайте його нікому.
+ Токен доступу
+ Перемкнути на маркований список
+ Перемкнути на нумерований список
+ У цій кімнаті ще не проводилися опитування
+ Минулі опитування
+ У цій кімнаті немає активних опитувань
+ Активні опитування
+ Історія опитувань
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml
index 934f5aa47b..14729c5b44 100644
--- a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml
+++ b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml
@@ -2834,4 +2834,13 @@
連結
文字
設定連結
+ 您的存取權杖可以完整存取您的帳號。不要將其與其他人分享。
+ 存取權杖
+ 切換項目符號清單
+ 切換編號清單
+ 此聊天室沒有過去的投票
+ 過去的投票
+ 此聊天室沒有正在進行的投票
+ 進行中的投票
+ 投票歷史紀錄
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 146df5054a..2ce6f13b97 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -794,6 +794,7 @@
Shows all threads you’ve participated in
Keep discussions organized with threads
Threads help keep your conversations on-topic and easy to track.
+ You\'re homeserver does not support listing threads yet.
Tip: Long tap a message and use “%s”.
From a Thread
@@ -2338,6 +2339,7 @@
- "One person"
- "%1$d people"
+ Poll history
Uploads
Leave Room
Leave
@@ -3193,6 +3195,10 @@
Voters see results as soon as they have voted
Closed poll
Results are only revealed when you end the poll
+ Active polls
+ There are no active polls in this room
+ Past polls
+ There are no past polls in this room
Share location
@@ -3506,4 +3512,7 @@
sent a video.
sent a sticker.
created a poll.
+
+ Access Token
+ Your access token gives full access to your account. Do not share it with anyone.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
index 5b41ddaaec..165dcf079e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
@@ -25,6 +25,9 @@ import java.io.IOException
import java.net.UnknownHostException
import javax.net.ssl.HttpsURLConnection
+fun Throwable.is400() = this is Failure.ServerError &&
+ httpCode == HttpsURLConnection.HTTP_BAD_REQUEST
+
fun Throwable.is401() = this is Failure.ServerError &&
httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED && /* 401 */
error.code == MatrixError.M_UNAUTHORIZED
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserIdentity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserIdentity.kt
new file mode 100644
index 0000000000..071db7f902
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserIdentity.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.crypto.crosssigning
+
+/**
+ * Container for the three cross signing keys: master, self signing and user signing.
+ */
+data class UserIdentity(
+ val masterKey: CryptoCrossSigningKey?,
+ val selfSigningKey: CryptoCrossSigningKey?,
+ val userSigningKey: CryptoCrossSigningKey?,
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt
index 5d4d67a65e..e3c5deeee7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt
@@ -19,5 +19,4 @@ package org.matrix.android.sdk.api.session.room.threads
sealed class FetchThreadsResult {
data class ShouldFetchMore(val nextBatch: String) : FetchThreadsResult()
object ReachedEnd : FetchThreadsResult()
- object Failed : FetchThreadsResult()
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
index bb6f6b51d3..dfa6cdeec1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
@@ -23,7 +23,7 @@ import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
* This interface defines methods to interact with thread related features.
* It's the dynamic threads implementation and the homeserver must return
* a capability entry for threads. If the server do not support m.thread
- * then [ThreadsLocalService] should be used instead
+ * then [org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService] should be used instead
*/
interface ThreadsService {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
index 7862da1c17..50497e3a27 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
@@ -89,6 +89,7 @@ import org.matrix.android.sdk.internal.crypto.model.SessionInfo
import org.matrix.android.sdk.internal.crypto.model.toRest
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
+import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator
import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask
import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask
@@ -192,21 +193,21 @@ internal class DefaultCryptoService @Inject constructor(
private val isStarting = AtomicBoolean(false)
private val isStarted = AtomicBoolean(false)
- fun onStateEvent(roomId: String, event: Event) {
+ fun onStateEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) {
when (event.type) {
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
- EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
+ EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event, cryptoStoreAggregator)
}
}
- fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean) {
+ fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean, cryptoStoreAggregator: CryptoStoreAggregator?) {
// handle state events
if (event.isStateEvent()) {
when (event.type) {
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
- EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
+ EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event, cryptoStoreAggregator)
}
}
@@ -430,8 +431,10 @@ internal class DefaultCryptoService @Inject constructor(
* A sync response has been received.
*
* @param syncResponse the syncResponse
+ * @param cryptoStoreAggregator data aggregated during the sync response treatment to store
*/
- fun onSyncCompleted(syncResponse: SyncResponse) {
+ fun onSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) {
+ cryptoStore.storeData(cryptoStoreAggregator)
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
runCatching {
if (syncResponse.deviceLists != null) {
@@ -998,15 +1001,26 @@ internal class DefaultCryptoService @Inject constructor(
}
}
- private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) {
+ private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) {
if (!event.isStateEvent()) return
val eventContent = event.content.toModel()
val historyVisibility = eventContent?.historyVisibility
if (historyVisibility == null) {
- cryptoStore.setShouldShareHistory(roomId, false)
+ if (cryptoStoreAggregator != null) {
+ cryptoStoreAggregator.setShouldShareHistoryData[roomId] = false
+ } else {
+ // Store immediately
+ cryptoStore.setShouldShareHistory(roomId, false)
+ }
} else {
- cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED)
- cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory())
+ if (cryptoStoreAggregator != null) {
+ cryptoStoreAggregator.setShouldEncryptForInvitedMembersData[roomId] = historyVisibility != RoomHistoryVisibility.JOINED
+ cryptoStoreAggregator.setShouldShareHistoryData[roomId] = historyVisibility.shouldShareHistory()
+ } else {
+ // Store immediately
+ cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED)
+ cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory())
+ }
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
index 7e9e156003..364d77f7ac 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
@@ -25,11 +25,13 @@ import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.extensions.measureMetric
import org.matrix.android.sdk.api.metrics.DownloadDeviceKeysMetricsPlugin
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
+import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
+import org.matrix.android.sdk.internal.crypto.store.UserDataToStore
import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
@@ -371,6 +373,8 @@ internal class DeviceListManager @Inject constructor(
Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
}
+ val userDataToStore = UserDataToStore()
+
for (userId in filteredUsers) {
// al devices =
val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
@@ -404,7 +408,7 @@ internal class DeviceListManager @Inject constructor(
}
// Update the store
// Note that devices which aren't in the response will be removed from the stores
- cryptoStore.storeUserDevices(userId, workingCopy)
+ userDataToStore.userDevices[userId] = workingCopy
}
val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also {
@@ -416,14 +420,15 @@ internal class DeviceListManager @Inject constructor(
val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also {
Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
}
- cryptoStore.storeUserCrossSigningKeys(
- userId,
- masterKey,
- selfSigningKey,
- userSigningKey
+ userDataToStore.userIdentities[userId] = UserIdentity(
+ masterKey = masterKey,
+ selfSigningKey = selfSigningKey,
+ userSigningKey = userSigningKey
)
}
+ cryptoStore.storeData(userDataToStore)
+
// Update devices trust for these users
// dispatchDeviceChange(downloadUsers)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
index 21e3342365..0305f73a7b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
@@ -22,9 +22,9 @@ import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
-import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
+import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
@@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
+import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmOutboundGroupSession
@@ -230,11 +231,12 @@ internal interface IMXCryptoStore {
*/
fun storeUserDevices(userId: String, devices: Map?)
- fun storeUserCrossSigningKeys(
+ /**
+ * Store the cross signing keys for the user userId.
+ */
+ fun storeUserIdentity(
userId: String,
- masterKey: CryptoCrossSigningKey?,
- selfSigningKey: CryptoCrossSigningKey?,
- userSigningKey: CryptoCrossSigningKey?
+ userIdentity: UserIdentity
)
/**
@@ -290,6 +292,13 @@ internal interface IMXCryptoStore {
fun shouldEncryptForInvitedMembers(roomId: String): Boolean
+ /**
+ * Sets a boolean flag that will determine whether or not this device should encrypt Events for
+ * invited members.
+ *
+ * @param roomId the room id
+ * @param shouldEncryptForInvitedMembers The boolean flag
+ */
fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean)
fun shouldShareHistory(roomId: String): Boolean
@@ -580,4 +589,14 @@ internal interface IMXCryptoStore {
fun areDeviceKeysUploaded(): Boolean
fun tidyUpDataBase()
fun getOutgoingRoomKeyRequests(inStates: Set): List
+
+ /**
+ * Store a bunch of data collected during a sync response treatment. @See [CryptoStoreAggregator].
+ */
+ fun storeData(cryptoStoreAggregator: CryptoStoreAggregator)
+
+ /**
+ * Store a bunch of data related to the users. @See [UserDataToStore].
+ */
+ fun storeData(userDataToStore: UserDataToStore)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/UserDataToStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/UserDataToStore.kt
new file mode 100644
index 0000000000..914ce4704e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/UserDataToStore.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2023 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.crypto.store
+
+import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+
+internal data class UserDataToStore(
+ /**
+ * Map of userId -> (Map of deviceId -> [CryptoDeviceInfo]).
+ */
+ val userDevices: MutableMap> = mutableMapOf(),
+ /**
+ * Map of userId -> [UserIdentity].
+ */
+ val userIdentities: MutableMap = mutableMapOf(),
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/CryptoStoreAggregator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/CryptoStoreAggregator.kt
new file mode 100644
index 0000000000..687ec95ec3
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/CryptoStoreAggregator.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2023 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.crypto.store.db
+
+data class CryptoStoreAggregator(
+ val setShouldShareHistoryData: MutableMap = mutableMapOf(),
+ val setShouldEncryptForInvitedMembersData: MutableMap = mutableMapOf(),
+) {
+ fun isEmpty(): Boolean {
+ return setShouldShareHistoryData.isEmpty() &&
+ setShouldEncryptForInvitedMembersData.isEmpty()
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt
index 2d66ce1488..6412df205f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt
@@ -20,10 +20,12 @@ import android.util.Base64
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmObject
+import timber.log.Timber
import java.io.ByteArrayOutputStream
import java.io.ObjectOutputStream
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
+import kotlin.system.measureTimeMillis
/**
* Get realm, invoke the action, close realm, and return the result of the action.
@@ -55,10 +57,12 @@ internal fun doRealmQueryAndCopyList(realmConfiguration: Realm
/**
* Get realm instance, invoke the action in a transaction and close realm.
*/
-internal fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
- Realm.getInstance(realmConfiguration).use { realm ->
- realm.executeTransaction { action.invoke(it) }
- }
+internal fun doRealmTransaction(tag: String, realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
+ measureTimeMillis {
+ Realm.getInstance(realmConfiguration).use { realm ->
+ realm.executeTransaction { action.invoke(it) }
+ }
+ }.also { Timber.w("doRealmTransaction for $tag took $it millis") }
}
internal fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
index 1b52b79746..b4368467a2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
@@ -33,9 +33,9 @@ import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
-import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
+import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
@@ -55,6 +55,7 @@ import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrappe
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
+import org.matrix.android.sdk.internal.crypto.store.UserDataToStore
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity
@@ -147,7 +148,7 @@ internal class RealmCryptoStore @Inject constructor(
init {
// Ensure CryptoMetadataEntity is inserted in DB
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("init", realmConfiguration) { realm ->
var currentMetadata = realm.where().findFirst()
var deleteAll = false
@@ -189,7 +190,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun deleteStore() {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("deleteStore", realmConfiguration) {
it.deleteAll()
}
}
@@ -218,7 +219,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun storeDeviceId(deviceId: String) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("storeDeviceId", realmConfiguration) {
it.where().findFirst()?.deviceId = deviceId
}
}
@@ -230,7 +231,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun saveOlmAccount() {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("saveOlmAccount", realmConfiguration) {
it.where().findFirst()?.putOlmAccount(olmAccount)
}
}
@@ -248,7 +249,7 @@ internal class RealmCryptoStore @Inject constructor(
@Synchronized
override fun getOrCreateOlmAccount(): OlmAccount {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("getOrCreateOlmAccount", realmConfiguration) {
val metaData = it.where().findFirst()
val existing = metaData!!.getOlmAccount()
if (existing == null) {
@@ -288,129 +289,139 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun storeUserDevices(userId: String, devices: Map?) {
- doRealmTransaction(realmConfiguration) { realm ->
- if (devices == null) {
- Timber.d("Remove user $userId")
- // Remove the user
- UserEntity.delete(realm, userId)
- } else {
- val userEntity = UserEntity.getOrCreate(realm, userId)
- // First delete the removed devices
- val deviceIds = devices.keys
- userEntity.devices.toTypedArray().iterator().let {
- while (it.hasNext()) {
- val deviceInfoEntity = it.next()
- if (deviceInfoEntity.deviceId !in deviceIds) {
- Timber.d("Remove device ${deviceInfoEntity.deviceId} of user $userId")
- deviceInfoEntity.deleteOnCascade()
- }
+ doRealmTransaction("storeUserDevices", realmConfiguration) { realm ->
+ storeUserDevices(realm, userId, devices)
+ }
+ }
+
+ private fun storeUserDevices(realm: Realm, userId: String, devices: Map?) {
+ if (devices == null) {
+ Timber.d("Remove user $userId")
+ // Remove the user
+ UserEntity.delete(realm, userId)
+ } else {
+ val userEntity = UserEntity.getOrCreate(realm, userId)
+ // First delete the removed devices
+ val deviceIds = devices.keys
+ userEntity.devices.toTypedArray().iterator().let {
+ while (it.hasNext()) {
+ val deviceInfoEntity = it.next()
+ if (deviceInfoEntity.deviceId !in deviceIds) {
+ Timber.d("Remove device ${deviceInfoEntity.deviceId} of user $userId")
+ deviceInfoEntity.deleteOnCascade()
}
}
- // Then update existing devices or add new one
- devices.values.forEach { cryptoDeviceInfo ->
- val existingDeviceInfoEntity = userEntity.devices.firstOrNull { it.deviceId == cryptoDeviceInfo.deviceId }
- if (existingDeviceInfoEntity == null) {
- // Add the device
- Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId")
- val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo)
- newEntity.firstTimeSeenLocalTs = clock.epochMillis()
- userEntity.devices.add(newEntity)
- } else {
- // Update the device
- Timber.d("Update device ${cryptoDeviceInfo.deviceId} of user $userId")
- CryptoMapper.updateDeviceInfoEntity(existingDeviceInfoEntity, cryptoDeviceInfo)
- }
+ }
+ // Then update existing devices or add new one
+ devices.values.forEach { cryptoDeviceInfo ->
+ val existingDeviceInfoEntity = userEntity.devices.firstOrNull { it.deviceId == cryptoDeviceInfo.deviceId }
+ if (existingDeviceInfoEntity == null) {
+ // Add the device
+ Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId")
+ val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo)
+ newEntity.firstTimeSeenLocalTs = clock.epochMillis()
+ userEntity.devices.add(newEntity)
+ } else {
+ // Update the device
+ Timber.d("Update device ${cryptoDeviceInfo.deviceId} of user $userId")
+ CryptoMapper.updateDeviceInfoEntity(existingDeviceInfoEntity, cryptoDeviceInfo)
}
}
}
}
- override fun storeUserCrossSigningKeys(
+ override fun storeUserIdentity(
userId: String,
- masterKey: CryptoCrossSigningKey?,
- selfSigningKey: CryptoCrossSigningKey?,
- userSigningKey: CryptoCrossSigningKey?
+ userIdentity: UserIdentity,
) {
- doRealmTransaction(realmConfiguration) { realm ->
- UserEntity.getOrCreate(realm, userId)
- .let { userEntity ->
- if (masterKey == null || selfSigningKey == null) {
- // The user has disabled cross signing?
- userEntity.crossSigningInfoEntity?.deleteOnCascade()
- userEntity.crossSigningInfoEntity = null
- } else {
- var shouldResetMyDevicesLocalTrust = false
- CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo ->
- // What should we do if we detect a change of the keys?
- val existingMaster = signingInfo.getMasterKey()
- if (existingMaster != null && existingMaster.publicKeyBase64 == masterKey.unpaddedBase64PublicKey) {
- crossSigningKeysMapper.update(existingMaster, masterKey)
- } else {
- Timber.d("## CrossSigning MSK change for $userId")
- val keyEntity = crossSigningKeysMapper.map(masterKey)
- signingInfo.setMasterKey(keyEntity)
- if (userId == this.userId) {
- shouldResetMyDevicesLocalTrust = true
- // my msk has changed! clear my private key
- // Could we have some race here? e.g I am the one that did change the keys
- // could i get this update to early and clear the private keys?
- // -> initializeCrossSigning is guarding for that by storing all at once
- realm.where().findFirst()?.apply {
- xSignMasterPrivateKey = null
- }
+ doRealmTransaction("storeUserIdentity", realmConfiguration) { realm ->
+ storeUserIdentity(realm, userId, userIdentity)
+ }
+ }
+
+ private fun storeUserIdentity(
+ realm: Realm,
+ userId: String,
+ userIdentity: UserIdentity,
+ ) {
+ UserEntity.getOrCreate(realm, userId)
+ .let { userEntity ->
+ if (userIdentity.masterKey == null || userIdentity.selfSigningKey == null) {
+ // The user has disabled cross signing?
+ userEntity.crossSigningInfoEntity?.deleteOnCascade()
+ userEntity.crossSigningInfoEntity = null
+ } else {
+ var shouldResetMyDevicesLocalTrust = false
+ CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo ->
+ // What should we do if we detect a change of the keys?
+ val existingMaster = signingInfo.getMasterKey()
+ if (existingMaster != null && existingMaster.publicKeyBase64 == userIdentity.masterKey.unpaddedBase64PublicKey) {
+ crossSigningKeysMapper.update(existingMaster, userIdentity.masterKey)
+ } else {
+ Timber.d("## CrossSigning MSK change for $userId")
+ val keyEntity = crossSigningKeysMapper.map(userIdentity.masterKey)
+ signingInfo.setMasterKey(keyEntity)
+ if (userId == this.userId) {
+ shouldResetMyDevicesLocalTrust = true
+ // my msk has changed! clear my private key
+ // Could we have some race here? e.g I am the one that did change the keys
+ // could i get this update to early and clear the private keys?
+ // -> initializeCrossSigning is guarding for that by storing all at once
+ realm.where().findFirst()?.apply {
+ xSignMasterPrivateKey = null
}
}
-
- val existingSelfSigned = signingInfo.getSelfSignedKey()
- if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == selfSigningKey.unpaddedBase64PublicKey) {
- crossSigningKeysMapper.update(existingSelfSigned, selfSigningKey)
- } else {
- Timber.d("## CrossSigning SSK change for $userId")
- val keyEntity = crossSigningKeysMapper.map(selfSigningKey)
- signingInfo.setSelfSignedKey(keyEntity)
- if (userId == this.userId) {
- shouldResetMyDevicesLocalTrust = true
- // my ssk has changed! clear my private key
- realm.where().findFirst()?.apply {
- xSignSelfSignedPrivateKey = null
- }
- }
- }
-
- // Only for me
- if (userSigningKey != null) {
- val existingUSK = signingInfo.getUserSigningKey()
- if (existingUSK != null && existingUSK.publicKeyBase64 == userSigningKey.unpaddedBase64PublicKey) {
- crossSigningKeysMapper.update(existingUSK, userSigningKey)
- } else {
- Timber.d("## CrossSigning USK change for $userId")
- val keyEntity = crossSigningKeysMapper.map(userSigningKey)
- signingInfo.setUserSignedKey(keyEntity)
- if (userId == this.userId) {
- shouldResetMyDevicesLocalTrust = true
- // my usk has changed! clear my private key
- realm.where().findFirst()?.apply {
- xSignUserPrivateKey = null
- }
- }
- }
- }
-
- // When my cross signing keys are reset, we consider clearing all existing device trust
- if (shouldResetMyDevicesLocalTrust) {
- realm.where()
- .equalTo(UserEntityFields.USER_ID, this.userId)
- .findFirst()
- ?.devices?.forEach {
- it?.trustLevelEntity?.crossSignedVerified = false
- it?.trustLevelEntity?.locallyVerified = it.deviceId == deviceId
- }
- }
- userEntity.crossSigningInfoEntity = signingInfo
}
+
+ val existingSelfSigned = signingInfo.getSelfSignedKey()
+ if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == userIdentity.selfSigningKey.unpaddedBase64PublicKey) {
+ crossSigningKeysMapper.update(existingSelfSigned, userIdentity.selfSigningKey)
+ } else {
+ Timber.d("## CrossSigning SSK change for $userId")
+ val keyEntity = crossSigningKeysMapper.map(userIdentity.selfSigningKey)
+ signingInfo.setSelfSignedKey(keyEntity)
+ if (userId == this.userId) {
+ shouldResetMyDevicesLocalTrust = true
+ // my ssk has changed! clear my private key
+ realm.where().findFirst()?.apply {
+ xSignSelfSignedPrivateKey = null
+ }
+ }
+ }
+
+ // Only for me
+ if (userIdentity.userSigningKey != null) {
+ val existingUSK = signingInfo.getUserSigningKey()
+ if (existingUSK != null && existingUSK.publicKeyBase64 == userIdentity.userSigningKey.unpaddedBase64PublicKey) {
+ crossSigningKeysMapper.update(existingUSK, userIdentity.userSigningKey)
+ } else {
+ Timber.d("## CrossSigning USK change for $userId")
+ val keyEntity = crossSigningKeysMapper.map(userIdentity.userSigningKey)
+ signingInfo.setUserSignedKey(keyEntity)
+ if (userId == this.userId) {
+ shouldResetMyDevicesLocalTrust = true
+ // my usk has changed! clear my private key
+ realm.where().findFirst()?.apply {
+ xSignUserPrivateKey = null
+ }
+ }
+ }
+ }
+
+ // When my cross signing keys are reset, we consider clearing all existing device trust
+ if (shouldResetMyDevicesLocalTrust) {
+ realm.where()
+ .equalTo(UserEntityFields.USER_ID, this.userId)
+ .findFirst()
+ ?.devices?.forEach {
+ it?.trustLevelEntity?.crossSignedVerified = false
+ it?.trustLevelEntity?.locallyVerified = it.deviceId == deviceId
+ }
+ }
+ userEntity.crossSigningInfoEntity = signingInfo
}
}
- }
+ }
}
override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? {
@@ -480,7 +491,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) {
Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}")
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("storePrivateKeysInfo", realmConfiguration) { realm ->
realm.where().findFirst()?.apply {
xSignMasterPrivateKey = msk
xSignUserPrivateKey = usk
@@ -490,7 +501,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("saveBackupRecoveryKey", realmConfiguration) { realm ->
realm.where().findFirst()?.apply {
keyBackupRecoveryKey = recoveryKey
keyBackupRecoveryKeyVersion = version
@@ -516,7 +527,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun storeMSKPrivateKey(msk: String?) {
Timber.v("## CRYPTO | *** storeMSKPrivateKey ${msk != null} ")
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("storeMSKPrivateKey", realmConfiguration) { realm ->
realm.where().findFirst()?.apply {
xSignMasterPrivateKey = msk
}
@@ -525,7 +536,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun storeSSKPrivateKey(ssk: String?) {
Timber.v("## CRYPTO | *** storeSSKPrivateKey ${ssk != null} ")
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("storeSSKPrivateKey", realmConfiguration) { realm ->
realm.where().findFirst()?.apply {
xSignSelfSignedPrivateKey = ssk
}
@@ -534,7 +545,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun storeUSKPrivateKey(usk: String?) {
Timber.v("## CRYPTO | *** storeUSKPrivateKey ${usk != null} ")
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("storeUSKPrivateKey", realmConfiguration) { realm ->
realm.where().findFirst()?.apply {
xSignUserPrivateKey = usk
}
@@ -667,7 +678,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun storeRoomAlgorithm(roomId: String, algorithm: String?) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("storeRoomAlgorithm", realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).let { entity ->
entity.algorithm = algorithm
// store anyway the new algorithm, but mark the room
@@ -708,7 +719,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("setShouldEncryptForInvitedMembers", realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers
}
}
@@ -716,7 +727,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) {
Timber.tag(loggerTag.value)
.v("setShouldShareHistory for room $roomId is $shouldShareHistory")
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("setShouldShareHistory", realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).shouldShareHistory = shouldShareHistory
}
}
@@ -733,7 +744,7 @@ internal class RealmCryptoStore @Inject constructor(
if (sessionIdentifier != null) {
val key = OlmSessionEntity.createPrimaryKey(sessionIdentifier, deviceKey)
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("storeSession", realmConfiguration) {
val realmOlmSession = OlmSessionEntity().apply {
primaryKey = key
sessionId = sessionIdentifier
@@ -790,7 +801,7 @@ internal class RealmCryptoStore @Inject constructor(
return
}
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("storeInboundGroupSessions", realmConfiguration) { realm ->
sessions.forEach { wrapper ->
val sessionIdentifier = try {
@@ -914,7 +925,7 @@ internal class RealmCryptoStore @Inject constructor(
override fun removeInboundGroupSession(sessionId: String, senderKey: String) {
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("removeInboundGroupSession", realmConfiguration) {
it.where()
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
.findAll()
@@ -933,7 +944,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun setKeyBackupVersion(keyBackupVersion: String?) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("setKeyBackupVersion", realmConfiguration) {
it.where().findFirst()?.backupVersion = keyBackupVersion
}
}
@@ -945,7 +956,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("setKeysBackupData", realmConfiguration) {
if (keysBackupData == null) {
// Clear the table
it.where()
@@ -959,7 +970,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun resetBackupMarkers() {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("resetBackupMarkers", realmConfiguration) {
it.where()
.findAll()
.map { inboundGroupSession ->
@@ -973,7 +984,7 @@ internal class RealmCryptoStore @Inject constructor(
return
}
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("markBackupDoneForInboundGroupSessions", realmConfiguration) { realm ->
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
try {
val sessionIdentifier =
@@ -1032,13 +1043,13 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("setGlobalBlacklistUnverifiedDevices", realmConfiguration) {
it.where().findFirst()?.globalBlacklistUnverifiedDevices = block
}
}
override fun enableKeyGossiping(enable: Boolean) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("enableKeyGossiping", realmConfiguration) {
it.where().findFirst()?.globalEnableKeyGossiping = enable
}
}
@@ -1062,13 +1073,13 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun enableShareKeyOnInvite(enable: Boolean) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("enableShareKeyOnInvite", realmConfiguration) {
it.where().findFirst()?.enableKeyForwardingOnInvite = enable
}
}
override fun setDeviceKeysUploaded(uploaded: Boolean) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("setDeviceKeysUploaded", realmConfiguration) {
it.where().findFirst()?.deviceKeysSentToServer = uploaded
}
}
@@ -1115,7 +1126,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("blockUnverifiedDevicesInRoom", realmConfiguration) { realm ->
CryptoRoomEntity.getById(realm, roomId)
?.blacklistUnverifiedDevices = block
}
@@ -1135,7 +1146,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map) {
- doRealmTransaction(realmConfiguration) {
+ doRealmTransaction("saveDeviceTrackingStatuses", realmConfiguration) {
deviceTrackingStatuses
.map { entry ->
UserEntity.getOrCreate(it, entry.key)
@@ -1268,7 +1279,7 @@ internal class RealmCryptoStore @Inject constructor(
): OutgoingKeyRequest {
// Insert the request and return the one passed in parameter
lateinit var request: OutgoingKeyRequest
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("getOrAddOutgoingRoomKeyRequest", realmConfiguration) { realm ->
val existing = realm.where()
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId)
@@ -1306,7 +1317,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("updateOutgoingRoomKeyRequestState", realmConfiguration) { realm ->
realm.where()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
.findFirst()?.apply {
@@ -1320,7 +1331,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("updateOutgoingRoomKeyRequiredIndex", realmConfiguration) { realm ->
realm.where()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
.findFirst()?.apply {
@@ -1337,7 +1348,7 @@ internal class RealmCryptoStore @Inject constructor(
fromDevice: String?,
event: Event
) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("updateOutgoingRoomKeyReply", realmConfiguration) { realm ->
realm.where()
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId)
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId)
@@ -1353,7 +1364,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun deleteOutgoingRoomKeyRequest(requestId: String) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("deleteOutgoingRoomKeyRequest", realmConfiguration) { realm ->
realm.where()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
.findFirst()?.deleteOnCascade()
@@ -1361,7 +1372,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("deleteOutgoingRoomKeyRequestInState", realmConfiguration) { realm ->
realm.where()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, state.name)
.findAll()
@@ -1497,7 +1508,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("setMyCrossSigningInfo", realmConfiguration) { realm ->
realm.where().findFirst()?.userId?.let { userId ->
addOrUpdateCrossSigningInfo(realm, userId, info)
}
@@ -1505,7 +1516,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun setUserKeysAsTrusted(userId: String, trusted: Boolean) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("setUserKeysAsTrusted", realmConfiguration) { realm ->
val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
.findFirst()
@@ -1525,7 +1536,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("setDeviceTrust", realmConfiguration) { realm ->
realm.where(DeviceInfoEntity::class.java)
.equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId))
.findFirst()?.let { deviceInfoEntity ->
@@ -1545,7 +1556,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun clearOtherUserTrust() {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("clearOtherUserTrust", realmConfiguration) { realm ->
val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java)
.findAll()
xInfoEntities?.forEach { info ->
@@ -1560,7 +1571,7 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun updateUsersTrust(check: (String) -> Boolean) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("updateUsersTrust", realmConfiguration) { realm ->
val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java)
.findAll()
xInfoEntities?.forEach { xInfoEntity ->
@@ -1668,13 +1679,13 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("setCrossSigningInfo", realmConfiguration) { realm ->
addOrUpdateCrossSigningInfo(realm, userId, info)
}
}
override fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("markMyMasterKeyAsLocallyTrusted", realmConfiguration) { realm ->
realm.where().findFirst()?.userId?.let { myUserId ->
CrossSigningInfoEntity.get(realm, myUserId)?.getMasterKey()?.let { xInfoEntity ->
val level = xInfoEntity.trustLevelEntity
@@ -1713,7 +1724,7 @@ internal class RealmCryptoStore @Inject constructor(
val roomId = withHeldContent.roomId ?: return
val sessionId = withHeldContent.sessionId ?: return
if (withHeldContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("addWithHeldMegolmSession", realmConfiguration) { realm ->
WithHeldSessionEntity.getOrCreate(realm, roomId, sessionId)?.let {
it.code = withHeldContent.code
it.senderKey = withHeldContent.senderKey
@@ -1745,7 +1756,7 @@ internal class RealmCryptoStore @Inject constructor(
deviceIdentityKey: String,
chainIndex: Int
) {
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("markedSessionAsShared", realmConfiguration) { realm ->
SharedSessionEntity.create(
realm = realm,
roomId = roomId,
@@ -1794,7 +1805,7 @@ internal class RealmCryptoStore @Inject constructor(
*/
override fun tidyUpDataBase() {
val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000
- doRealmTransaction(realmConfiguration) { realm ->
+ doRealmTransaction("tidyUpDataBase", realmConfiguration) { realm ->
// Clean the old ones?
realm.where()
@@ -1815,4 +1826,31 @@ internal class RealmCryptoStore @Inject constructor(
// Can we do something for WithHeldSessionEntity?
}
}
+
+ override fun storeData(cryptoStoreAggregator: CryptoStoreAggregator) {
+ if (cryptoStoreAggregator.isEmpty()) {
+ return
+ }
+ doRealmTransaction("storeData - CryptoStoreAggregator", realmConfiguration) { realm ->
+ // setShouldShareHistory
+ cryptoStoreAggregator.setShouldShareHistoryData.forEach {
+ CryptoRoomEntity.getOrCreate(realm, it.key).shouldShareHistory = it.value
+ }
+ // setShouldEncryptForInvitedMembers
+ cryptoStoreAggregator.setShouldEncryptForInvitedMembersData.forEach {
+ CryptoRoomEntity.getOrCreate(realm, it.key).shouldEncryptForInvitedMembers = it.value
+ }
+ }
+ }
+
+ override fun storeData(userDataToStore: UserDataToStore) {
+ doRealmTransaction("storeData - UserDataToStore", realmConfiguration) { realm ->
+ userDataToStore.userDevices.forEach {
+ storeUserDevices(realm, it.key, it.value)
+ }
+ userDataToStore.userIdentities.forEach {
+ storeUserIdentity(realm, it.key, it.value)
+ }
+ }
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt
index 9bd197e42e..f89221b627 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt
@@ -16,7 +16,6 @@
package org.matrix.android.sdk.internal.di
-import androidx.annotation.Nullable
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.Moshi
@@ -28,7 +27,6 @@ import java.lang.reflect.Type
internal annotation class SerializeNulls {
companion object {
val JSON_ADAPTER_FACTORY: JsonAdapter.Factory = object : JsonAdapter.Factory {
- @Nullable
override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? {
val nextAnnotations = Types.nextAnnotations(annotations, SerializeNulls::class.java)
?: return null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt
index 4e0525536c..334a8c5076 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt
@@ -16,7 +16,6 @@
package org.matrix.android.sdk.internal.network.interceptors
-import androidx.annotation.NonNull
import okhttp3.logging.HttpLoggingInterceptor
import org.json.JSONArray
import org.json.JSONException
@@ -38,7 +37,7 @@ internal class FormattedJsonHttpLogger(
* @param message
*/
@Synchronized
- override fun log(@NonNull message: String) {
+ override fun log(message: String) {
Timber.v(message)
// Try to log formatted Json only if there is a chance that [message] contains Json.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt
index 8b54978279..6c28b9fcce 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt
@@ -16,7 +16,6 @@
package org.matrix.android.sdk.internal.network.parsing
-import androidx.annotation.Nullable
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
@@ -32,14 +31,12 @@ internal interface CheckNumberType {
companion object {
val JSON_ADAPTER_FACTORY = object : JsonAdapter.Factory {
- @Nullable
override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? {
if (type !== Any::class.java) {
return null
}
val delegate: JsonAdapter = moshi.nextAdapter(this, Any::class.java, emptySet())
return object : JsonAdapter() {
- @Nullable
@Throws(IOException::class)
override fun fromJson(reader: JsonReader): Any? {
return if (reader.peek() !== JsonReader.Token.NUMBER) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt
index cfc26045a0..ce34b0430e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt
@@ -42,14 +42,12 @@ internal class StreamEventsManager @Inject constructor() {
listeners.remove(listener)
}
- fun dispatchLiveEventReceived(event: Event, roomId: String, initialSync: Boolean) {
+ fun dispatchLiveEventReceived(event: Event, roomId: String) {
Timber.v("## dispatchLiveEventReceived ${event.eventId}")
coroutineScope.launch {
- if (!initialSync) {
- listeners.forEach {
- tryOrNull {
- it.onLiveEvent(roomId, event)
- }
+ listeners.forEach {
+ tryOrNull {
+ it.onLiveEvent(roomId, event)
}
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
index 793c2573be..653069b3c8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
@@ -176,7 +176,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
}
// Give info to crypto module
- cryptoService.onStateEvent(roomId, event)
+ cryptoService.onStateEvent(roomId, event, null)
}
roomMemberContentsByUser.getOrPut(event.senderId) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
index 05d50d9595..cb407bb1cb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
@@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse
import org.matrix.android.sdk.api.session.sync.model.SyncResponse
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
+import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.session.SessionListeners
@@ -92,7 +93,7 @@ internal class SyncResponseHandler @Inject constructor(
postTreatmentSyncResponse(syncResponse, isInitialSync)
- markCryptoSyncCompleted(syncResponse)
+ markCryptoSyncCompleted(syncResponse, aggregator.cryptoStoreAggregator)
handlePostSync()
@@ -218,10 +219,10 @@ internal class SyncResponseHandler @Inject constructor(
}
}
- private fun markCryptoSyncCompleted(syncResponse: SyncResponse) {
+ private fun markCryptoSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) {
relevantPlugins.measureSpan("task", "crypto_sync_handler_onSyncCompleted") {
measureTimeMillis {
- cryptoSyncHandler.onSyncCompleted(syncResponse)
+ cryptoSyncHandler.onSyncCompleted(syncResponse, cryptoStoreAggregator)
}.also {
Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms")
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt
index 2b7f936fa8..af05e08da3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt
@@ -16,6 +16,8 @@
package org.matrix.android.sdk.internal.session.sync
+import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator
+
internal class SyncResponsePostTreatmentAggregator {
// List of RoomId
val ephemeralFilesToDelete = mutableListOf()
@@ -28,4 +30,7 @@ internal class SyncResponsePostTreatmentAggregator {
// Set of users to call `crossSigningService.checkTrustAndAffectedRoomShields` once per sync
val userIdsForCheckingTrustAndAffectedRoomShields = mutableSetOf()
+
+ // For the crypto store
+ val cryptoStoreAggregator = CryptoStoreAggregator()
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
index 551db52dbd..7224b0c29c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
@@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.sync.model.SyncResponse
import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
+import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator
import org.matrix.android.sdk.internal.crypto.tasks.toDeviceTracingId
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
import org.matrix.android.sdk.internal.session.sync.ProgressReporter
@@ -85,8 +86,8 @@ internal class CryptoSyncHandler @Inject constructor(
}
}
- fun onSyncCompleted(syncResponse: SyncResponse) {
- cryptoService.onSyncCompleted(syncResponse)
+ fun onSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) {
+ cryptoService.onSyncCompleted(syncResponse, cryptoStoreAggregator)
}
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
index 4001ae2ccf..5e4886ce1e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
@@ -258,7 +258,7 @@ internal class RoomSyncHandler @Inject constructor(
root = eventEntity
}
// Give info to crypto module
- cryptoService.onStateEvent(roomId, event)
+ cryptoService.onStateEvent(roomId, event, aggregator.cryptoStoreAggregator)
roomMemberEventHandler.handle(realm, roomId, event, isInitialSync, aggregator)
}
}
@@ -376,8 +376,15 @@ internal class RoomSyncHandler @Inject constructor(
roomEntity.chunks.clearWith { it.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true) }
roomTypingUsersHandler.handle(realm, roomId, null)
roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.LEAVE)
- roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary,
- roomSync.unreadNotifications, roomSync.unreadThreadNotifications, aggregator = aggregator)
+ roomSummaryUpdater.update(
+ realm,
+ roomId,
+ membership,
+ roomSync.summary,
+ roomSync.unreadNotifications,
+ roomSync.unreadThreadNotifications,
+ aggregator = aggregator,
+ )
return roomEntity
}
@@ -423,7 +430,9 @@ internal class RoomSyncHandler @Inject constructor(
val isInitialSync = insertType == EventInsertType.INITIAL_SYNC
eventIds.add(event.eventId)
- liveEventService.get().dispatchLiveEventReceived(event, roomId, isInitialSync)
+ if (!isInitialSync) {
+ liveEventService.get().dispatchLiveEventReceived(event, roomId)
+ }
if (event.isEncrypted() && !isInitialSync) {
try {
@@ -486,7 +495,7 @@ internal class RoomSyncHandler @Inject constructor(
}
}
// Give info to crypto module
- cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync)
+ cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync, aggregator.cryptoStoreAggregator)
// Try to remove local echo
event.unsignedData?.transactionId?.also { txId ->
diff --git a/tools/lint/lint.xml b/tools/lint/lint.xml
index 3d3b073749..dbe30f2267 100644
--- a/tools/lint/lint.xml
+++ b/tools/lint/lint.xml
@@ -77,6 +77,7 @@
+
diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh
index f91e11584c..553c02101c 100755
--- a/tools/release/releaseScript.sh
+++ b/tools/release/releaseScript.sh
@@ -87,6 +87,14 @@ fi
printf "OK\n"
+printf "\n================================================================================\n"
+printf "Ensuring main and develop branches are up to date...\n"
+
+git checkout main
+git pull
+git checkout develop
+git pull
+
printf "\n================================================================================\n"
# Guessing version to propose a default version
versionMajorCandidate=`grep "ext.versionMajor" ./vector-app/build.gradle | cut -d " " -f3`
@@ -103,14 +111,6 @@ versionMinor=`echo ${version} | cut -d "." -f2`
versionPatch=`echo ${version} | cut -d "." -f3`
nextPatchVersion=$((versionPatch + 2))
-printf "\n================================================================================\n"
-printf "Ensuring main and develop branches are up to date...\n"
-
-git checkout main
-git pull
-git checkout develop
-git pull
-
printf "\n================================================================================\n"
printf "Starting the release ${version}\n"
git flow release start ${version}
@@ -190,6 +190,9 @@ yes | towncrier build --version "v${version}"
printf "\n================================================================================\n"
read -p "Check the file CHANGES.md consistency. It's possible to reorder items (most important changes first) or change their section if relevant. Also an opportunity to fix some typo, or rewrite things. Do not commit your change. Press enter when it's done."
+# Get the changes to use it to create the GitHub release
+changelogUrlEncoded=`git diff CHANGES.md | grep ^+ | tail -n +2 | cut -c2- | jq -sRr @uri | sed s/\(/%28/g | sed s/\)/%29/g`
+
printf "\n================================================================================\n"
printf "Committing...\n"
git commit -a -m "Changelog for version ${version}"
@@ -263,7 +266,7 @@ else
fi
printf "\n================================================================================\n"
-printf "Wait for the GitHub action https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Amain to build the 'main' branch.\n"
+printf "Wait for the GitHub action https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%%3Amain to build the 'main' branch.\n"
read -p "After GHA is finished, please enter the artifact URL (for 'vector-gplay-release-unsigned'): " artifactUrl
printf "\n================================================================================\n"
@@ -354,10 +357,15 @@ apkPath="${targetPath}/vector-gplay-arm64-v8a-release-signed.apk"
adb -d install ${apkPath}
read -p "Please run the APK on your phone to check that the upgrade went well (no init sync, etc.). Press enter when it's done."
-# TODO Get the block to copy from towncrier earlier (be may be edited by the release manager)?
-read -p "Create the release on gitHub from the tag https://github.com/vector-im/element-android/tags, copy paste the block from the file CHANGES.md. Press enter when it's done."
-read -p "Add the 4 signed APKs to the GitHub release. They are located at ${targetPath}. Press enter when it's done."
+printf "\n================================================================================\n"
+githubCreateReleaseLink="https://github.com/vector-im/element-android/releases/new?tag=v${version}&title=Element%%20Android%%20v${version}&body=${changelogUrlEncoded}"
+printf "Creating the release on gitHub.\n"
+printf "Open this link: ${githubCreateReleaseLink}\n"
+printf "Then\n"
+printf " - click on the 'Generate releases notes' button\n"
+printf " - Add the 4 signed APKs to the GitHub release. They are located at ${targetPath}\n"
+read -p ". Press enter when it's done. "
printf "\n================================================================================\n"
printf "Message for the Android internal room:\n\n"
diff --git a/vector/build.gradle b/vector/build.gradle
index 83af7ecc04..91d2a8c46a 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -308,7 +308,7 @@ dependencies {
// Fix issue with Jitsi. Inspired from https://github.com/android/android-test/issues/861#issuecomment-872067868
// Error was lots of `Duplicate class org.checkerframework.common.reflection.qual.MethodVal found in modules jetified-checker-3.1 (org.checkerframework:checker:3.1.1) and jetified-checker-qual-3.12.0 (org.checkerframework:checker-qual:3.12.0)
//noinspection GradleDependency Cannot use latest 3.15.0 since it required min API 26.
- implementation "org.checkerframework:checker:3.27.0"
+ implementation "org.checkerframework:checker:3.29.0"
androidTestImplementation libs.androidx.testCore
androidTestImplementation libs.androidx.testRunner
diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index d22ab51e7a..911bbfa4a3 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -84,6 +84,7 @@ import im.vector.app.features.roomprofile.banned.RoomBannedMemberListViewModel
import im.vector.app.features.roomprofile.members.RoomMemberListViewModel
import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsViewModel
import im.vector.app.features.roomprofile.permissions.RoomPermissionsViewModel
+import im.vector.app.features.roomprofile.polls.RoomPollsViewModel
import im.vector.app.features.roomprofile.settings.RoomSettingsViewModel
import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedViewModel
import im.vector.app.features.roomprofile.uploads.RoomUploadsViewModel
@@ -697,4 +698,9 @@ interface MavericksViewModelModule {
@IntoMap
@MavericksViewModelKey(SetLinkViewModel::class)
fun setLinkViewModelFactory(factory: SetLinkViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+
+ @Binds
+ @IntoMap
+ @MavericksViewModelKey(RoomPollsViewModel::class)
+ fun roomPollsViewModelFactory(factory: RoomPollsViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
}
diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
index 4e5116eda9..1e29dfff5e 100644
--- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
+++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
@@ -41,6 +41,7 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.PreferenceManager
import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.MavericksView
@@ -91,6 +92,7 @@ import im.vector.app.features.themes.ActivityOtherThemes
import im.vector.app.features.themes.ThemeUtils
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.GlobalError
@@ -123,14 +125,20 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver
protected val viewModelProvider
get() = ViewModelProvider(this, viewModelFactory)
- fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
- viewEvents
- .stream()
- .onEach {
- hideWaitingView()
- observer(it)
- }
- .launchIn(lifecycleScope)
+ fun VectorViewModel<*, *, T>.observeViewEvents(
+ observer: (T) -> Unit,
+ ) {
+ val tag = this@VectorBaseActivity::class.simpleName.toString()
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ viewEvents
+ .stream(tag)
+ .collect {
+ hideWaitingView()
+ observer(it)
+ }
+ }
+ }
}
var toolbar: ToolbarConfig? = null
diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt
index ec6f3288f8..a44fb1c9ac 100644
--- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt
+++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt
@@ -26,8 +26,10 @@ import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.annotation.CallSuper
import androidx.annotation.FloatRange
+import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.MavericksView
import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -43,6 +45,7 @@ import im.vector.app.features.analytics.plan.MobileScreen
import io.github.hyuwah.draggableviewlib.Utils
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber
@@ -199,12 +202,18 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShe
* ViewEvents
* ========================================================================================== */
- protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
- viewEvents
- .stream()
- .onEach {
- observer(it)
- }
- .launchIn(viewLifecycleOwner.lifecycleScope)
+ protected fun VectorViewModel<*, *, T>.observeViewEvents(
+ observer: (T) -> Unit,
+ ) {
+ val tag = this@VectorBaseBottomSheetDialogFragment::class.simpleName.toString()
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ viewEvents
+ .stream(tag)
+ .collect {
+ observer(it)
+ }
+ }
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt
index 5a817b989e..34e233aa7a 100644
--- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt
+++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt
@@ -23,8 +23,10 @@ import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.MavericksView
import dagger.hilt.android.EntryPointAccessors
@@ -37,6 +39,7 @@ import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.themes.ThemeUtils
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber
@@ -145,11 +148,15 @@ abstract class VectorBaseDialogFragment : DialogFragment(), Ma
* ========================================================================================== */
protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
- viewEvents
- .stream()
- .onEach {
- observer(it)
- }
- .launchIn(viewLifecycleOwner.lifecycleScope)
+ val tag = this@VectorBaseDialogFragment::class.simpleName.toString()
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ viewEvents
+ .stream(tag)
+ .collect {
+ observer(it)
+ }
+ }
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt
index 8fe2d33f6a..a82cef54e5 100644
--- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt
+++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt
@@ -34,6 +34,7 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.MavericksView
import com.bumptech.glide.util.Util.assertMainThread
@@ -53,6 +54,7 @@ import im.vector.app.features.navigation.Navigator
import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber
@@ -272,14 +274,20 @@ abstract class VectorBaseFragment : Fragment(), MavericksView
* ViewEvents
* ========================================================================================== */
- protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
- viewEvents
- .stream()
- .onEach {
- dismissLoadingDialog()
- observer(it)
- }
- .launchIn(viewLifecycleOwner.lifecycleScope)
+ protected fun VectorViewModel<*, *, T>.observeViewEvents(
+ observer: (T) -> Unit,
+ ) {
+ val tag = this@VectorBaseFragment::class.simpleName.toString()
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ viewEvents
+ .stream(tag)
+ .collect {
+ dismissLoadingDialog()
+ observer(it)
+ }
+ }
+ }
}
/* ==========================================================================================
diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt
index c9d58f9545..3dd38c455f 100644
--- a/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt
+++ b/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt
@@ -18,15 +18,16 @@ package im.vector.app.core.platform
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.MavericksViewModel
-import im.vector.app.core.utils.DataSource
-import im.vector.app.core.utils.PublishDataSource
+import im.vector.app.core.utils.EventQueue
+import im.vector.app.core.utils.SharedEvents
abstract class VectorViewModel(initialState: S) :
MavericksViewModel(initialState) {
// Used to post transient events to the View
- protected val _viewEvents = PublishDataSource()
- val viewEvents: DataSource = _viewEvents
+ protected val _viewEvents = EventQueue(capacity = 64)
+ val viewEvents: SharedEvents
+ get() = _viewEvents
abstract fun handle(action: VA)
}
diff --git a/vector/src/main/java/im/vector/app/core/resources/StringArrayProvider.kt b/vector/src/main/java/im/vector/app/core/resources/StringArrayProvider.kt
index 9ed3c02ba4..3c057e0635 100644
--- a/vector/src/main/java/im/vector/app/core/resources/StringArrayProvider.kt
+++ b/vector/src/main/java/im/vector/app/core/resources/StringArrayProvider.kt
@@ -18,7 +18,6 @@ package im.vector.app.core.resources
import android.content.res.Resources
import androidx.annotation.ArrayRes
-import androidx.annotation.NonNull
import javax.inject.Inject
class StringArrayProvider @Inject constructor(private val resources: Resources) {
@@ -31,7 +30,6 @@ class StringArrayProvider @Inject constructor(private val resources: Resources)
* @return The string array associated with the resource, stripped of styled
* text information.
*/
- @NonNull
fun getStringArray(@ArrayRes resId: Int): Array {
return resources.getStringArray(resId)
}
diff --git a/vector/src/main/java/im/vector/app/core/resources/StringProvider.kt b/vector/src/main/java/im/vector/app/core/resources/StringProvider.kt
index 7e322daaae..e5f48f8be0 100644
--- a/vector/src/main/java/im/vector/app/core/resources/StringProvider.kt
+++ b/vector/src/main/java/im/vector/app/core/resources/StringProvider.kt
@@ -17,7 +17,6 @@
package im.vector.app.core.resources
import android.content.res.Resources
-import androidx.annotation.NonNull
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import javax.inject.Inject
@@ -32,7 +31,6 @@ class StringProvider @Inject constructor(private val resources: Resources) {
* @return The string data associated with the resource, stripped of styled
* text information.
*/
- @NonNull
fun getString(@StringRes resId: Int): String {
return resources.getString(resId)
}
@@ -48,12 +46,10 @@ class StringProvider @Inject constructor(private val resources: Resources) {
* @return The string data associated with the resource, formatted and
* stripped of styled text information.
*/
- @NonNull
fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String {
return resources.getString(resId, *formatArgs)
}
- @NonNull
fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any?): String {
return resources.getQuantityString(resId, quantity, *formatArgs)
}
diff --git a/vector/src/main/java/im/vector/app/core/utils/SharedEvent.kt b/vector/src/main/java/im/vector/app/core/utils/SharedEvent.kt
new file mode 100644
index 0000000000..e712769c48
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/utils/SharedEvent.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.core.utils
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.transform
+import java.util.concurrent.CopyOnWriteArraySet
+
+interface SharedEvents {
+ fun stream(consumerId: String): Flow
+}
+
+class EventQueue(capacity: Int) : SharedEvents {
+
+ private val innerQueue = MutableSharedFlow>(replay = capacity)
+
+ fun post(event: T) {
+ innerQueue.tryEmit(OneTimeEvent(event))
+ }
+
+ override fun stream(consumerId: String): Flow = innerQueue.filterNotHandledBy(consumerId)
+}
+
+/**
+ * Event designed to be delivered only once to a concrete entity,
+ * but it can also be delivered to multiple different entities.
+ *
+ * Keeps track of who has already handled its content.
+ */
+private class OneTimeEvent(private val content: T) {
+
+ private val handlers = CopyOnWriteArraySet()
+
+ /**
+ * @param asker Used to identify, whether this "asker" has already handled this Event.
+ * @return Event content or null if it has been already handled by asker
+ */
+ fun getIfNotHandled(asker: String): T? = if (handlers.add(asker)) content else null
+}
+
+private fun Flow>.filterNotHandledBy(consumerId: String): Flow = transform { event ->
+ event.getIfNotHandled(consumerId)?.let { emit(it) }
+}
diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt
index 8ce375122e..cffb1577cf 100644
--- a/vector/src/main/java/im/vector/app/features/MainActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt
@@ -55,8 +55,6 @@ import im.vector.app.features.themes.ActivityOtherThemes
import im.vector.app.features.ui.UiStateRepository
import im.vector.lib.core.utils.compat.getParcelableExtraCompat
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
@@ -142,9 +140,9 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity
startAppViewModel.onEach {
renderState(it)
}
- startAppViewModel.viewEvents.stream()
- .onEach(::handleViewEvents)
- .launchIn(lifecycleScope)
+ startAppViewModel.observeViewEvents {
+ handleViewEvents(it)
+ }
startAppViewModel.handle(StartAppAction.StartApp)
}
diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt
index d393636a8e..aea87beea9 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt
@@ -59,7 +59,7 @@ class SharedSecureStorageActivity :
views.toolbar.visibility = View.GONE
- viewModel.observeViewEvents { observeViewEvents(it) }
+ viewModel.observeViewEvents { onViewEvents(it) }
viewModel.onEach { renderState(it) }
}
@@ -85,7 +85,7 @@ class SharedSecureStorageActivity :
showFragment(fragment)
}
- private fun observeViewEvents(it: SharedSecureStorageViewEvent?) {
+ private fun onViewEvents(it: SharedSecureStorageViewEvent) {
when (it) {
is SharedSecureStorageViewEvent.Dismiss -> {
finish()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewActions.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewActions.kt
new file mode 100644
index 0000000000..7dda460a5e
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewActions.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.home.room.threads.list.viewmodel
+
+import im.vector.app.core.platform.VectorViewModelAction
+
+sealed interface ThreadListViewActions : VectorViewModelAction {
+ object TryAgain : ThreadListViewActions
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewEvents.kt
new file mode 100644
index 0000000000..3e9af034f4
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewEvents.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.home.room.threads.list.viewmodel
+
+import im.vector.app.core.platform.VectorViewEvents
+
+sealed interface ThreadListViewEvents : VectorViewEvents {
+ data class ShowError(val throwable: Throwable) : ThreadListViewEvents
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
index 7124727bb7..f31f19849c 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
@@ -27,8 +27,6 @@ import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
-import im.vector.app.core.platform.EmptyAction
-import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.extensions.toAnalyticsInteraction
@@ -52,7 +50,7 @@ class ThreadListViewModel @AssistedInject constructor(
@Assisted val initialState: ThreadListViewState,
private val analyticsTracker: AnalyticsTracker,
private val session: Session,
-) : VectorViewModel(initialState) {
+) : VectorViewModel(initialState) {
private val room = session.getRoom(initialState.roomId)
@@ -93,7 +91,17 @@ class ThreadListViewModel @AssistedInject constructor(
fetchAndObserveThreads()
}
- override fun handle(action: EmptyAction) {}
+ override fun handle(action: ThreadListViewActions) {
+ when (action) {
+ ThreadListViewActions.TryAgain -> handleTryAgain()
+ }
+ }
+
+ private fun handleTryAgain() {
+ viewModelScope.launch {
+ fetchNextPage()
+ }
+ }
/**
* Observing thread list with respect to homeserver capabilities.
@@ -181,21 +189,23 @@ class ThreadListViewModel @AssistedInject constructor(
true -> ThreadFilter.PARTICIPATED
false -> ThreadFilter.ALL
}
- room?.threadsService()?.fetchThreadList(
- nextBatchId = nextBatchId,
- limit = defaultPagedListConfig.pageSize,
- filter = filter,
- ).let { result ->
- when (result) {
- is FetchThreadsResult.ReachedEnd -> {
- hasReachedEnd = true
- }
- is FetchThreadsResult.ShouldFetchMore -> {
- nextBatchId = result.nextBatch
- }
- else -> {
+ try {
+ room?.threadsService()?.fetchThreadList(
+ nextBatchId = nextBatchId,
+ limit = defaultPagedListConfig.pageSize,
+ filter = filter,
+ )?.let { result ->
+ when (result) {
+ is FetchThreadsResult.ReachedEnd -> {
+ hasReachedEnd = true
+ }
+ is FetchThreadsResult.ShouldFetchMore -> {
+ nextBatchId = result.nextBatch
+ }
}
}
+ } catch (throwable: Throwable) {
+ _viewEvents.post(ThreadListViewEvents.ShowError(throwable))
}
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt
index 318c250906..1e67941856 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt
@@ -26,6 +26,7 @@ import androidx.core.view.isVisible
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.extensions.cleanup
@@ -41,10 +42,14 @@ import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListController
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListPagedController
+import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewActions
+import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewEvents
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState
import im.vector.app.features.rageshake.BugReporter
import im.vector.app.features.rageshake.ReportType
+import org.matrix.android.sdk.api.failure.is400
+import org.matrix.android.sdk.api.failure.is404
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.MatrixItem
@@ -126,11 +131,45 @@ class ThreadListFragment :
views.threadListRecyclerView.configureWith(legacyThreadListController, TimelineItemAnimator(), hasFixedSize = false)
legacyThreadListController.listener = this
}
+ observeViewEvents()
+ }
+
+ private fun observeViewEvents() {
+ threadListViewModel.observeViewEvents {
+ when (it) {
+ is ThreadListViewEvents.ShowError -> handleShowError(it)
+ }
+ }
+ }
+
+ private fun handleShowError(event: ThreadListViewEvents.ShowError) {
+ val error = event.throwable
+ MaterialAlertDialogBuilder(requireActivity())
+ .setTitle(R.string.dialog_title_error)
+ .also {
+ if (error.is400() || error.is404()) {
+ // Outdated Homeserver
+ it.setMessage(R.string.thread_list_not_available)
+ it.setPositiveButton(R.string.ok) { _, _ ->
+ requireActivity().finish()
+ }
+ } else {
+ // Other error, can retry
+ // (Can happen on first request or on pagination request)
+ it.setMessage(errorFormatter.toHumanReadable(error))
+ it.setPositiveButton(R.string.ok, null)
+ it.setNegativeButton(R.string.global_retry) { _, _ ->
+ threadListViewModel.handle(ThreadListViewActions.TryAgain)
+ }
+ }
+ }
+ .show()
}
override fun onDestroyView() {
views.threadListRecyclerView.cleanup()
threadListController.listener = null
+ legacyThreadListController.listener = null
super.onDestroyView()
}
diff --git a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt
index 089fdcebd4..0d240b376b 100644
--- a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt
@@ -29,7 +29,9 @@ import androidx.core.transition.addListener
import androidx.core.view.ViewCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Transition
import com.airbnb.mvrx.viewModel
import dagger.hilt.android.AndroidEntryPoint
@@ -50,8 +52,6 @@ import im.vector.lib.attachmentviewer.AttachmentViewerActivity
import im.vector.lib.core.utils.compat.getParcelableArrayListExtraCompat
import im.vector.lib.core.utils.compat.getParcelableExtraCompat
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
@@ -239,10 +239,15 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), AttachmentInt
}
private fun observeViewEvents() {
- viewModel.viewEvents
- .stream()
- .onEach(::handleViewEvents)
- .launchIn(lifecycleScope)
+ val tag = this::class.simpleName.toString()
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ viewModel
+ .viewEvents
+ .stream(tag)
+ .collect(::handleViewEvents)
+ }
+ }
}
private fun handleViewEvents(event: VectorAttachmentViewerViewEvents) {
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt
index 526d676dee..3c37c92650 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt
@@ -36,6 +36,7 @@ import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment
import im.vector.app.features.roomprofile.members.RoomMemberListFragment
import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsFragment
import im.vector.app.features.roomprofile.permissions.RoomPermissionsFragment
+import im.vector.app.features.roomprofile.polls.RoomPollsFragment
import im.vector.app.features.roomprofile.settings.RoomSettingsFragment
import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment
import im.vector.lib.core.utils.compat.getParcelableCompat
@@ -98,6 +99,7 @@ class RoomProfileActivity :
RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings()
RoomProfileSharedAction.OpenRoomAliasesSettings -> openRoomAlias()
RoomProfileSharedAction.OpenRoomPermissionsSettings -> openRoomPermissions()
+ RoomProfileSharedAction.OpenRoomPolls -> openRoomPolls()
RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads()
RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers()
RoomProfileSharedAction.OpenRoomNotificationSettings -> openRoomNotificationSettings()
@@ -126,6 +128,10 @@ class RoomProfileActivity :
finish()
}
+ private fun openRoomPolls() {
+ addFragmentToBackstack(views.simpleFragmentContainer, RoomPollsFragment::class.java, roomProfileArgs)
+ }
+
private fun openRoomUploads() {
addFragmentToBackstack(views.simpleFragmentContainer, RoomUploadsFragment::class.java, roomProfileArgs)
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt
index eb43a345f2..30bd6c7ed3 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt
@@ -18,6 +18,7 @@
package im.vector.app.features.roomprofile
import com.airbnb.epoxy.TypedEpoxyController
+import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.epoxy.expandableTextItem
import im.vector.app.core.epoxy.profiles.buildProfileAction
@@ -56,6 +57,7 @@ class RoomProfileController @Inject constructor(
fun onMemberListClicked()
fun onBannedMemberListClicked()
fun onNotificationsClicked()
+ fun onPollHistoryClicked()
fun onUploadsClicked()
fun createShortcut()
fun onSettingsClicked()
@@ -263,6 +265,15 @@ class RoomProfileController @Inject constructor(
action = { callback?.onBannedMemberListClicked() }
)
}
+ if (BuildConfig.DEBUG) {
+ // WIP, will be in release when related screens will be finished
+ buildProfileAction(
+ id = "poll_history",
+ title = stringProvider.getString(R.string.room_profile_section_more_polls),
+ icon = R.drawable.ic_attachment_poll,
+ action = { callback?.onPollHistoryClicked() }
+ )
+ }
buildProfileAction(
id = "uploads",
title = stringProvider.getString(R.string.room_profile_section_more_uploads),
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
index f4394111ab..51885dbf39 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
@@ -269,6 +269,10 @@ class RoomProfileFragment :
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomNotificationSettings)
}
+ override fun onPollHistoryClicked() {
+ roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomPolls)
+ }
+
override fun onUploadsClicked() {
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomUploads)
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt
index 7d62bb86a1..b243ceb206 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt
@@ -25,6 +25,7 @@ sealed class RoomProfileSharedAction : VectorSharedAction {
object OpenRoomSettings : RoomProfileSharedAction()
object OpenRoomAliasesSettings : RoomProfileSharedAction()
object OpenRoomPermissionsSettings : RoomProfileSharedAction()
+ object OpenRoomPolls : RoomProfileSharedAction()
object OpenRoomUploads : RoomProfileSharedAction()
object OpenRoomMembers : RoomProfileSharedAction()
object OpenBannedRoomMembers : RoomProfileSharedAction()
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/GetPollsUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/GetPollsUseCase.kt
new file mode 100644
index 0000000000..6f2a757ed7
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/GetPollsUseCase.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls
+
+import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+
+class GetPollsUseCase @Inject constructor() {
+
+ fun execute(): Flow> {
+ // TODO unmock and add unit tests
+ return flowOf(getActivePolls() + getEndedPolls())
+ .map { it.sortedByDescending { poll -> poll.creationTimestamp } }
+ }
+
+ private fun getActivePolls(): List {
+ return listOf(
+ PollSummary.ActivePoll(
+ id = "id1",
+ // 2022/06/28 UTC+1
+ creationTimestamp = 1656367200000,
+ title = "Which charity would you like to support?"
+ ),
+ PollSummary.ActivePoll(
+ id = "id2",
+ // 2022/06/26 UTC+1
+ creationTimestamp = 1656194400000,
+ title = "Which sport should the pupils do this year?"
+ ),
+ PollSummary.ActivePoll(
+ id = "id3",
+ // 2022/06/24 UTC+1
+ creationTimestamp = 1656021600000,
+ title = "What type of food should we have at the party?"
+ ),
+ PollSummary.ActivePoll(
+ id = "id4",
+ // 2022/06/22 UTC+1
+ creationTimestamp = 1655848800000,
+ title = "What film should we show at the end of the year party?"
+ ),
+ )
+ }
+
+ private fun getEndedPolls(): List {
+ return listOf(
+ PollSummary.EndedPoll(
+ id = "id1-ended",
+ // 2022/06/28 UTC+1
+ creationTimestamp = 1656367200000,
+ title = "Which charity would you like to support?",
+ totalVotes = 22,
+ winnerOptions = listOf(
+ PollOptionViewState.PollEnded(
+ optionId = "id1",
+ optionAnswer = "Cancer research",
+ voteCount = 13,
+ votePercentage = 13 / 22.0,
+ isWinner = true,
+ )
+ ),
+ ),
+ PollSummary.EndedPoll(
+ id = "id2-ended",
+ // 2022/06/26 UTC+1
+ creationTimestamp = 1656194400000,
+ title = "Where should we do the offsite?",
+ totalVotes = 92,
+ winnerOptions = listOf(
+ PollOptionViewState.PollEnded(
+ optionId = "id1",
+ optionAnswer = "Hawaii",
+ voteCount = 43,
+ votePercentage = 43 / 92.0,
+ isWinner = true,
+ )
+ ),
+ ),
+ PollSummary.EndedPoll(
+ id = "id3-ended",
+ // 2022/06/24 UTC+1
+ creationTimestamp = 1656021600000,
+ title = "What type of food should we have at the party?",
+ totalVotes = 22,
+ winnerOptions = listOf(
+ PollOptionViewState.PollEnded(
+ optionId = "id1",
+ optionAnswer = "Brazilian",
+ voteCount = 13,
+ votePercentage = 13 / 22.0,
+ isWinner = true,
+ )
+ ),
+ ),
+ )
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/PollSummary.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/PollSummary.kt
new file mode 100644
index 0000000000..f24ac8b8a6
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/PollSummary.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls
+
+import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
+
+sealed interface PollSummary {
+ val id: String
+ val creationTimestamp: Long
+ val title: String
+
+ data class ActivePoll(
+ override val id: String,
+ override val creationTimestamp: Long,
+ override val title: String,
+ ) : PollSummary
+
+ data class EndedPoll(
+ override val id: String,
+ override val creationTimestamp: Long,
+ override val title: String,
+ val totalVotes: Int,
+ val winnerOptions: List,
+ ) : PollSummary
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsAction.kt
new file mode 100644
index 0000000000..c18142a306
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsAction.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls
+
+import im.vector.app.core.platform.VectorViewModelAction
+
+sealed interface RoomPollsAction : VectorViewModelAction
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsFragment.kt
new file mode 100644
index 0000000000..9f7e704135
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsFragment.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.airbnb.mvrx.args
+import com.airbnb.mvrx.fragmentViewModel
+import com.google.android.material.tabs.TabLayoutMediator
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.R
+import im.vector.app.core.platform.VectorBaseFragment
+import im.vector.app.databinding.FragmentRoomPollsBinding
+import im.vector.app.features.roomprofile.RoomProfileArgs
+
+@AndroidEntryPoint
+class RoomPollsFragment : VectorBaseFragment() {
+
+ private val roomProfileArgs: RoomProfileArgs by args()
+
+ private val viewModel: RoomPollsViewModel by fragmentViewModel()
+
+ private var tabLayoutMediator: TabLayoutMediator? = null
+
+ override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomPollsBinding {
+ return FragmentRoomPollsBinding.inflate(inflater, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupToolbar()
+ setupTabs()
+ }
+
+ override fun onDestroyView() {
+ views.roomPollsViewPager.adapter = null
+ tabLayoutMediator?.detach()
+ tabLayoutMediator = null
+ super.onDestroyView()
+ }
+
+ private fun setupToolbar() {
+ setupToolbar(views.roomPollsToolbar)
+ .allowBack()
+ }
+
+ private fun setupTabs() {
+ views.roomPollsViewPager.adapter = RoomPollsPagerAdapter(this)
+
+ tabLayoutMediator = TabLayoutMediator(views.roomPollsTabs, views.roomPollsViewPager) { tab, position ->
+ when (position) {
+ RoomPollsType.ACTIVE.ordinal -> tab.text = getString(R.string.room_polls_active)
+ RoomPollsType.ENDED.ordinal -> tab.text = getString(R.string.room_polls_ended)
+ }
+ }.also { it.attach() }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsPagerAdapter.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsPagerAdapter.kt
new file mode 100644
index 0000000000..c60fc5de27
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsPagerAdapter.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls
+
+import androidx.fragment.app.Fragment
+import androidx.viewpager2.adapter.FragmentStateAdapter
+import im.vector.app.features.roomprofile.polls.active.RoomActivePollsFragment
+import im.vector.app.features.roomprofile.polls.ended.RoomEndedPollsFragment
+
+class RoomPollsPagerAdapter(
+ private val fragment: Fragment
+) : FragmentStateAdapter(fragment) {
+
+ override fun getItemCount() = RoomPollsType.values().size
+
+ override fun createFragment(position: Int): Fragment {
+ return when (position) {
+ RoomPollsType.ACTIVE.ordinal -> instantiateFragment(RoomActivePollsFragment::class.java.name)
+ RoomPollsType.ENDED.ordinal -> instantiateFragment(RoomEndedPollsFragment::class.java.name)
+ else -> throw IllegalArgumentException("position should be between 0 and ${itemCount - 1}, while it was $position")
+ }
+ }
+
+ private fun instantiateFragment(fragmentName: String): Fragment {
+ return fragment.childFragmentManager.fragmentFactory.instantiate(fragment.requireContext().classLoader, fragmentName)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsType.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsType.kt
new file mode 100644
index 0000000000..134ef9a195
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsType.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls
+
+enum class RoomPollsType {
+ ACTIVE,
+ ENDED,
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewEvent.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewEvent.kt
new file mode 100644
index 0000000000..231123563a
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewEvent.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls
+
+import im.vector.app.core.platform.VectorViewEvents
+
+sealed class RoomPollsViewEvent : VectorViewEvents
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt
new file mode 100644
index 0000000000..95cb4717ca
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls
+
+import com.airbnb.mvrx.MavericksViewModelFactory
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import im.vector.app.core.di.MavericksAssistedViewModelFactory
+import im.vector.app.core.di.hiltMavericksViewModelFactory
+import im.vector.app.core.platform.VectorViewModel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+
+class RoomPollsViewModel @AssistedInject constructor(
+ @Assisted initialState: RoomPollsViewState,
+ private val getPollsUseCase: GetPollsUseCase,
+) : VectorViewModel(initialState) {
+
+ @AssistedFactory
+ interface Factory : MavericksAssistedViewModelFactory {
+ override fun create(initialState: RoomPollsViewState): RoomPollsViewModel
+ }
+
+ companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
+
+ init {
+ observePolls()
+ }
+
+ private fun observePolls() {
+ getPollsUseCase.execute()
+ .onEach { setState { copy(polls = it) } }
+ .launchIn(viewModelScope)
+ }
+
+ override fun handle(action: RoomPollsAction) {
+ // do nothing for now
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt
new file mode 100644
index 0000000000..74794c99b1
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls
+
+import com.airbnb.mvrx.MavericksState
+import im.vector.app.features.roomprofile.RoomProfileArgs
+
+data class RoomPollsViewState(
+ val roomId: String,
+ val polls: List = emptyList(),
+) : MavericksState {
+
+ constructor(roomProfileArgs: RoomProfileArgs) : this(roomId = roomProfileArgs.roomId)
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsFragment.kt
new file mode 100644
index 0000000000..1c6a03c480
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsFragment.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls.active
+
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.R
+import im.vector.app.features.roomprofile.polls.RoomPollsType
+import im.vector.app.features.roomprofile.polls.list.RoomPollsListFragment
+
+@AndroidEntryPoint
+class RoomActivePollsFragment : RoomPollsListFragment() {
+
+ override fun getEmptyListTitle(): String {
+ return getString(R.string.room_polls_active_no_item)
+ }
+
+ override fun getRoomPollsType(): RoomPollsType {
+ return RoomPollsType.ACTIVE
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/ended/RoomEndedPollsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/ended/RoomEndedPollsFragment.kt
new file mode 100644
index 0000000000..8dd0cadadf
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/ended/RoomEndedPollsFragment.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls.ended
+
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.R
+import im.vector.app.features.roomprofile.polls.RoomPollsType
+import im.vector.app.features.roomprofile.polls.list.RoomPollsListFragment
+
+@AndroidEntryPoint
+class RoomEndedPollsFragment : RoomPollsListFragment() {
+
+ override fun getEmptyListTitle(): String {
+ return getString(R.string.room_polls_ended_no_item)
+ }
+
+ override fun getRoomPollsType(): RoomPollsType {
+ return RoomPollsType.ENDED
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollItem.kt
new file mode 100644
index 0000000000..da00fedddb
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollItem.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls.list
+
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.view.isVisible
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.app.R
+import im.vector.app.core.epoxy.ClickListener
+import im.vector.app.core.epoxy.VectorEpoxyHolder
+import im.vector.app.core.epoxy.VectorEpoxyModel
+import im.vector.app.core.epoxy.onClick
+import im.vector.app.core.extensions.setTextOrHide
+import im.vector.app.features.home.room.detail.timeline.item.PollOptionView
+import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
+
+@EpoxyModelClass
+abstract class RoomPollItem : VectorEpoxyModel(R.layout.item_poll) {
+
+ @EpoxyAttribute
+ lateinit var formattedDate: String
+
+ @EpoxyAttribute
+ lateinit var title: String
+
+ @EpoxyAttribute
+ var winnerOptions: List = emptyList()
+
+ @EpoxyAttribute
+ var totalVotesStatus: String? = null
+
+ @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+ var clickListener: ClickListener? = null
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ holder.view.onClick(clickListener)
+ holder.date.text = formattedDate
+ holder.title.text = title
+ holder.winnerOptions.removeAllViews()
+ holder.winnerOptions.isVisible = winnerOptions.isNotEmpty()
+ for (winnerOption in winnerOptions) {
+ val optionView = PollOptionView(holder.view.context)
+ holder.winnerOptions.addView(optionView)
+ optionView.render(winnerOption)
+ }
+ holder.totalVotes.setTextOrHide(totalVotesStatus)
+ }
+
+ class Holder : VectorEpoxyHolder() {
+ val date by bind(R.id.pollDate)
+ val title by bind(R.id.pollTitle)
+ val winnerOptions by bind(R.id.pollWinnerOptionsContainer)
+ val totalVotes by bind(R.id.pollTotalVotes)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsController.kt
new file mode 100644
index 0000000000..f0e3b6b9a4
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsController.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls.list
+
+import com.airbnb.epoxy.TypedEpoxyController
+import im.vector.app.R
+import im.vector.app.core.date.DateFormatKind
+import im.vector.app.core.date.VectorDateFormatter
+import im.vector.app.core.resources.StringProvider
+import im.vector.app.features.roomprofile.polls.PollSummary
+import javax.inject.Inject
+
+class RoomPollsController @Inject constructor(
+ val dateFormatter: VectorDateFormatter,
+ val stringProvider: StringProvider,
+) : TypedEpoxyController>() {
+
+ interface Listener {
+ fun onPollClicked(pollId: String)
+ }
+
+ var listener: Listener? = null
+
+ override fun buildModels(data: List?) {
+ if (data.isNullOrEmpty()) {
+ return
+ }
+
+ for (poll in data) {
+ when (poll) {
+ is PollSummary.ActivePoll -> buildActivePollItem(poll)
+ is PollSummary.EndedPoll -> buildEndedPollItem(poll)
+ }
+ }
+ }
+
+ private fun buildActivePollItem(poll: PollSummary.ActivePoll) {
+ val host = this
+ roomPollItem {
+ id(poll.id)
+ formattedDate(host.dateFormatter.format(poll.creationTimestamp, DateFormatKind.TIMELINE_DAY_DIVIDER))
+ title(poll.title)
+ clickListener {
+ host.listener?.onPollClicked(poll.id)
+ }
+ }
+ }
+
+ private fun buildEndedPollItem(poll: PollSummary.EndedPoll) {
+ val host = this
+ roomPollItem {
+ id(poll.id)
+ formattedDate(host.dateFormatter.format(poll.creationTimestamp, DateFormatKind.TIMELINE_DAY_DIVIDER))
+ title(poll.title)
+ winnerOptions(poll.winnerOptions)
+ totalVotesStatus(host.stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, poll.totalVotes, poll.totalVotes))
+ clickListener {
+ host.listener?.onPollClicked(poll.id)
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsListFragment.kt
new file mode 100644
index 0000000000..0d97bd8dcb
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsListFragment.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.roomprofile.polls.list
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import com.airbnb.mvrx.parentFragmentViewModel
+import com.airbnb.mvrx.withState
+import im.vector.app.core.extensions.cleanup
+import im.vector.app.core.extensions.configureWith
+import im.vector.app.core.platform.VectorBaseFragment
+import im.vector.app.databinding.FragmentRoomPollsListBinding
+import im.vector.app.features.roomprofile.polls.PollSummary
+import im.vector.app.features.roomprofile.polls.RoomPollsType
+import im.vector.app.features.roomprofile.polls.RoomPollsViewModel
+import timber.log.Timber
+import javax.inject.Inject
+
+abstract class RoomPollsListFragment :
+ VectorBaseFragment(),
+ RoomPollsController.Listener {
+
+ @Inject
+ lateinit var roomPollsController: RoomPollsController
+
+ private val viewModel: RoomPollsViewModel by parentFragmentViewModel(RoomPollsViewModel::class)
+
+ override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomPollsListBinding {
+ return FragmentRoomPollsListBinding.inflate(inflater, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setupList()
+ }
+
+ abstract fun getEmptyListTitle(): String
+
+ abstract fun getRoomPollsType(): RoomPollsType
+
+ private fun setupList() {
+ roomPollsController.listener = this
+ views.roomPollsList.configureWith(roomPollsController)
+ views.roomPollsEmptyTitle.text = getEmptyListTitle()
+ }
+
+ override fun onDestroyView() {
+ cleanUpList()
+ super.onDestroyView()
+ }
+
+ private fun cleanUpList() {
+ views.roomPollsList.cleanup()
+ roomPollsController.listener = null
+ }
+
+ override fun invalidate() = withState(viewModel) { viewState ->
+ when (getRoomPollsType()) {
+ RoomPollsType.ACTIVE -> renderList(viewState.polls.filterIsInstance(PollSummary.ActivePoll::class.java))
+ RoomPollsType.ENDED -> renderList(viewState.polls.filterIsInstance(PollSummary.EndedPoll::class.java))
+ }
+ }
+
+ private fun renderList(polls: List) {
+ roomPollsController.setData(polls)
+ views.roomPollsEmptyTitle.isVisible = polls.isEmpty()
+ }
+
+ override fun onPollClicked(pollId: String) {
+ // TODO navigate to details
+ Timber.d("poll with id $pollId clicked")
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt
index b6fa997f41..514f2529e9 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt
@@ -25,6 +25,7 @@ import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.preference.VectorPreferenceCategory
import im.vector.app.core.preference.VectorSwitchPreference
+import im.vector.app.core.utils.copyToClipboard
import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.home.NightlyProxy
import im.vector.app.features.rageshake.RageShake
@@ -64,6 +65,14 @@ class VectorSettingsAdvancedSettingsFragment :
override fun bindPref() {
setupRageShakeSection()
setupNightlySection()
+ setupDevToolsSection()
+ }
+
+ private fun setupDevToolsSection() {
+ findPreference("SETTINGS_ACCESS_TOKEN")?.setOnPreferenceClickListener {
+ copyToClipboard(requireActivity(), session.sessionParams.credentials.accessToken)
+ true
+ }
}
private fun setupRageShakeSection() {
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt
index 38ba949a49..724807a81e 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt
@@ -20,7 +20,9 @@ import android.content.Context
import android.os.Bundle
import android.view.View
import androidx.annotation.CallSuper
+import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.PreferenceFragmentCompat
import com.airbnb.mvrx.MavericksView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -35,6 +37,7 @@ import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.MobileScreen
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber
@@ -66,13 +69,19 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick
* ViewEvents
* ========================================================================================== */
- protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
- viewEvents
- .stream()
- .onEach {
- observer(it)
- }
- .launchIn(viewLifecycleOwner.lifecycleScope)
+ protected fun VectorViewModel<*, *, T>.observeViewEvents(
+ observer: (T) -> Unit,
+ ) {
+ val tag = this@VectorSettingsBaseFragment::class.simpleName.toString()
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ viewEvents
+ .stream(tag)
+ .collect {
+ observer(it)
+ }
+ }
+ }
}
/* ==========================================================================================
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
index d56f4ad715..9cb894bb58 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
@@ -419,7 +419,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
// Next media player is already attached to this player and will start playing automatically
if (nextMediaPlayer != null) return
- val hasEnded = !isLiveListening && mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence
+ val currentSequence = playlist.currentSequence ?: 0
+ val lastChunkSequence = mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence ?: 0
+ val hasEnded = !isLiveListening && currentSequence >= lastChunkSequence
if (hasEnded) {
// We'll not receive new chunks anymore so we can stop the live listening
stop()
diff --git a/vector/src/main/res/layout/fragment_room_polls.xml b/vector/src/main/res/layout/fragment_room_polls.xml
new file mode 100644
index 0000000000..396d6fd8c5
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_room_polls.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_room_polls_list.xml b/vector/src/main/res/layout/fragment_room_polls_list.xml
new file mode 100644
index 0000000000..8eb27e5e00
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_room_polls_list.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/item_poll.xml b/vector/src/main/res/layout/item_poll.xml
new file mode 100644
index 0000000000..17f3b5abf5
--- /dev/null
+++ b/vector/src/main/res/layout/item_poll.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/xml/vector_settings_advanced_settings.xml b/vector/src/main/res/xml/vector_settings_advanced_settings.xml
index 9260b33162..6399d54cbb 100644
--- a/vector/src/main/res/xml/vector_settings_advanced_settings.xml
+++ b/vector/src/main/res/xml/vector_settings_advanced_settings.xml
@@ -93,6 +93,12 @@
android:title="@string/settings_key_requests"
app:fragment="im.vector.app.features.settings.devtools.KeyRequestsFragment" />
+
+
()
+ private val initialState = RoomPollsViewState(ROOM_ID)
+
+ private fun createViewModel(): RoomPollsViewModel {
+ return RoomPollsViewModel(
+ initialState = initialState,
+ getPollsUseCase = fakeGetPollsUseCase,
+ )
+ }
+
+ @Test
+ fun `given viewModel when created then polls list is observed and viewState is updated`() {
+ // Given
+ val polls = listOf(givenAPollSummary())
+ every { fakeGetPollsUseCase.execute() } returns flowOf(polls)
+ val expectedViewState = initialState.copy(polls = polls)
+
+ // When
+ val viewModel = createViewModel()
+ val viewModelTest = viewModel.test()
+
+ // Then
+ viewModelTest
+ .assertLatestState(expectedViewState)
+ .finish()
+ verify {
+ fakeGetPollsUseCase.execute()
+ }
+ }
+
+ private fun givenAPollSummary(): PollSummary {
+ return mockk()
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/Extensions.kt b/vector/src/test/java/im/vector/app/test/Extensions.kt
index 2fbab3b71b..0b1a22f75c 100644
--- a/vector/src/test/java/im/vector/app/test/Extensions.kt
+++ b/vector/src/test/java/im/vector/app/test/Extensions.kt
@@ -28,7 +28,7 @@ fun String.trimIndentOneLine() = trimIndent().replace("\n", "")
fun VectorViewModel.test(): ViewModelTest {
val testResultCollectingScope = CoroutineScope(Dispatchers.Unconfined)
val state = stateFlow.test(testResultCollectingScope)
- val viewEvents = viewEvents.stream().test(testResultCollectingScope)
+ val viewEvents = viewEvents.stream("test").test(testResultCollectingScope)
return ViewModelTest(state, viewEvents)
}