Merge branch 'release/25.05.2'

This commit is contained in:
Jorge Martín 2025-05-12 16:43:35 +02:00
commit 00bc42feb7
94 changed files with 1497 additions and 1171 deletions

View file

@ -1,3 +1,18 @@
Changes in Element X v25.05.1
=============================
## What's Changed
### 🐛 Bugfixes
* Fix broken Element Call in 25.05.0 by @bmarty in https://github.com/element-hq/element-x-android/pull/4694
### Dependency upgrades
* fix(deps): update android.gradle.plugin to v8.10.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4687
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.5.8 by @renovate in https://github.com/element-hq/element-x-android/pull/4696
### Others
* Let EnterpriseService prevent usage of homeserver by @bmarty in https://github.com/element-hq/element-x-android/pull/4682
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.05.0...v25.05.1
Changes in Element X v25.05.0 Changes in Element X v25.05.0
============================= =============================

View file

@ -9,6 +9,7 @@ package io.element.android.appconfig
object LearnMoreConfig { object LearnMoreConfig {
const val ENCRYPTION_URL: String = "https://element.io/help#encryption" const val ENCRYPTION_URL: String = "https://element.io/help#encryption"
const val DEVICE_VERIFICATION_URL: String = "https://element.io/help#encryption-device-verification"
const val SECURE_BACKUP_URL: String = "https://element.io/help#encryption5" const val SECURE_BACKUP_URL: String = "https://element.io/help#encryption5"
const val IDENTITY_CHANGE_URL: String = "https://element.io/help#encryption18" const val IDENTITY_CHANGE_URL: String = "https://element.io/help#encryption18"
} }

View file

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

View file

@ -1,5 +1,4 @@
import extension.setupAnvil import extension.setupAnvil
import org.gradle.kotlin.dsl.test
/* /*
* Copyright 2023, 2024 New Vector Ltd. * Copyright 2023, 2024 New Vector Ltd.

View file

@ -26,7 +26,7 @@ import io.element.android.anvilannotations.ContributesNode
import io.element.android.appconfig.LearnMoreConfig import io.element.android.appconfig.LearnMoreConfig
import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModeNode import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModeNode
import io.element.android.features.securebackup.api.SecureBackupEntryPoint import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
@ -40,7 +40,7 @@ import kotlinx.parcelize.Parcelize
class FtueSessionVerificationFlowNode @AssistedInject constructor( class FtueSessionVerificationFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val verifySessionEntryPoint: VerifySessionEntryPoint, private val outgoingVerificationEntryPoint: OutgoingVerificationEntryPoint,
private val secureBackupEntryPoint: SecureBackupEntryPoint, private val secureBackupEntryPoint: SecureBackupEntryPoint,
) : BaseFlowNode<FtueSessionVerificationFlowNode.NavTarget>( ) : BaseFlowNode<FtueSessionVerificationFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
@ -94,19 +94,19 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
} }
override fun onLearnMoreAboutEncryption() { override fun onLearnMoreAboutEncryption() {
learnMoreUrl.value = LearnMoreConfig.ENCRYPTION_URL learnMoreUrl.value = LearnMoreConfig.DEVICE_VERIFICATION_URL
} }
} }
createNode<ChooseSelfVerificationModeNode>(buildContext, plugins = listOf(callback)) createNode<ChooseSelfVerificationModeNode>(buildContext, plugins = listOf(callback))
} }
is NavTarget.UseAnotherDevice -> { is NavTarget.UseAnotherDevice -> {
verifySessionEntryPoint.nodeBuilder(this, buildContext) outgoingVerificationEntryPoint.nodeBuilder(this, buildContext)
.params(VerifySessionEntryPoint.Params( .params(OutgoingVerificationEntryPoint.Params(
showDeviceVerifiedScreen = true, showDeviceVerifiedScreen = true,
verificationRequest = VerificationRequest.Outgoing.CurrentSession, verificationRequest = VerificationRequest.Outgoing.CurrentSession,
)) ))
.callback(object : VerifySessionEntryPoint.Callback { .callback(object : OutgoingVerificationEntryPoint.Callback {
override fun onDone() { override fun onDone() {
plugins<Callback>().forEach { it.onDone() } plugins<Callback>().forEach { it.onDone() }
} }
@ -116,7 +116,8 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
} }
override fun onLearnMoreAboutEncryption() { override fun onLearnMoreAboutEncryption() {
learnMoreUrl.value = LearnMoreConfig.ENCRYPTION_URL // Note that this callback is never called. The "Learn more" link is not displayed
// for the self session interactive verification.
} }
}) })
.build() .build()

View file

@ -28,7 +28,7 @@ data class LoginPasswordState(
@Parcelize @Parcelize
data class LoginFormState( data class LoginFormState(
val login: String, val login: String,
val password: String val password: String,
) : Parcelable { ) : Parcelable {
companion object { companion object {
val Default = LoginFormState("", "") val Default = LoginFormState("", "")

View file

@ -8,23 +8,38 @@
package io.element.android.features.login.impl.screens.loginpassword package io.element.android.features.login.impl.screens.loginpassword
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.SessionId
open class LoginPasswordStateProvider : PreviewParameterProvider<LoginPasswordState> { open class LoginPasswordStateProvider : PreviewParameterProvider<LoginPasswordState> {
override val values: Sequence<LoginPasswordState> override val values: Sequence<LoginPasswordState>
get() = sequenceOf( get() = sequenceOf(
aLoginPasswordState(), aLoginPasswordState(),
// Loading // Loading
aLoginPasswordState().copy(loginAction = AsyncData.Loading()), aLoginPasswordState(loginAction = AsyncData.Loading()),
// Error // Error
aLoginPasswordState().copy(loginAction = AsyncData.Failure(Exception("An error occurred"))), aLoginPasswordState(loginAction = AsyncData.Failure(Exception("An error occurred"))),
) )
} }
fun aLoginPasswordState() = LoginPasswordState( fun aLoginPasswordState(
accountProvider = anAccountProvider(), accountProvider: AccountProvider = anAccountProvider(),
formState = LoginFormState.Default, formState: LoginFormState = LoginFormState.Default,
loginAction = AsyncData.Uninitialized, loginAction: AsyncData<SessionId> = AsyncData.Uninitialized,
eventSink = {} eventSink: (LoginPasswordEvents) -> Unit = {},
) = LoginPasswordState(
accountProvider = accountProvider,
formState = formState,
loginAction = loginAction,
eventSink = eventSink,
)
fun aLoginFormState(
login: String = "",
password: String = "",
) = LoginFormState(
login = login,
password = password,
) )

View file

@ -201,6 +201,7 @@ private fun LoginForm(
{ {
Box(Modifier.clickable { Box(Modifier.clickable {
loginFieldState = "" loginFieldState = ""
eventSink(LoginPasswordEvents.SetLogin(""))
}) { }) {
Icon( Icon(
imageVector = CompoundIcons.Close(), imageVector = CompoundIcons.Close(),

View file

@ -14,7 +14,7 @@
<string name="screen_change_account_provider_subtitle">"Použijte jiného poskytovatele účtu, například vlastní soukromý server nebo pracovní účet."</string> <string name="screen_change_account_provider_subtitle">"Použijte jiného poskytovatele účtu, například vlastní soukromý server nebo pracovní účet."</string>
<string name="screen_change_account_provider_title">"Změnit poskytovatele účtu"</string> <string name="screen_change_account_provider_title">"Změnit poskytovatele účtu"</string>
<string name="screen_change_server_error_invalid_homeserver">"Nepodařilo se nám připojit k tomuto domovskému serveru. Zkontrolujte prosím, zda jste správně zadali adresu URL domovského serveru. Pokud je adresa URL správná, obraťte se na správce domovského serveru, který vám poskytne další pomoc."</string> <string name="screen_change_server_error_invalid_homeserver">"Nepodařilo se nám připojit k tomuto domovskému serveru. Zkontrolujte prosím, zda jste správně zadali adresu URL domovského serveru. Pokud je adresa URL správná, obraťte se na správce domovského serveru, který vám poskytne další pomoc."</string>
<string name="screen_change_server_error_invalid_well_known">"Klouzavá synchronizace není k dispozici kvůli problému se souborem well-known: <string name="screen_change_server_error_invalid_well_known">"Server není k dispozici kvůli problému se souborem well-known:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Vybraný poskytovatel účtu nepodporuje klouzavou synchronizaci. Pro použití %1$s je nutná aktualizace serveru."</string> <string name="screen_change_server_error_no_sliding_sync_message">"Vybraný poskytovatel účtu nepodporuje klouzavou synchronizaci. Pro použití %1$s je nutná aktualizace serveru."</string>
<string name="screen_change_server_form_header">"Adresa URL domovského serveru"</string> <string name="screen_change_server_form_header">"Adresa URL domovského serveru"</string>

View file

@ -17,6 +17,7 @@
<string name="screen_change_server_error_invalid_well_known">"Der Server ist aufgrund eines Problems im \"well-known file\" nicht verfügbar: <string name="screen_change_server_error_invalid_well_known">"Der Server ist aufgrund eines Problems im \"well-known file\" nicht verfügbar:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Der gewählte Kontoanbieter unterstützt Sliding Sync nicht. Für die Verwendung von %1$s ist ein Upgrade des Servers erforderlich."</string> <string name="screen_change_server_error_no_sliding_sync_message">"Der gewählte Kontoanbieter unterstützt Sliding Sync nicht. Für die Verwendung von %1$s ist ein Upgrade des Servers erforderlich."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$sdarf keine Verbindung herstellen zu%2$s."</string>
<string name="screen_change_server_form_header">"Homeserver-URL"</string> <string name="screen_change_server_form_header">"Homeserver-URL"</string>
<string name="screen_change_server_form_notice">"Geben Sie eine Domainadresse ein."</string> <string name="screen_change_server_form_notice">"Geben Sie eine Domainadresse ein."</string>
<string name="screen_change_server_subtitle">"Wie lautet die Adresse deines Servers?"</string> <string name="screen_change_server_subtitle">"Wie lautet die Adresse deines Servers?"</string>

View file

@ -14,7 +14,7 @@
<string name="screen_change_account_provider_subtitle">"Χρησιμοποίησε διαφορετικό πάροχο λογαριασμού, όπως τον δικό σου ιδιωτικό διακομιστή ή έναν εργασιακό λογαριασμό."</string> <string name="screen_change_account_provider_subtitle">"Χρησιμοποίησε διαφορετικό πάροχο λογαριασμού, όπως τον δικό σου ιδιωτικό διακομιστή ή έναν εργασιακό λογαριασμό."</string>
<string name="screen_change_account_provider_title">"Αλλαγή παρόχου λογαριασμού"</string> <string name="screen_change_account_provider_title">"Αλλαγή παρόχου λογαριασμού"</string>
<string name="screen_change_server_error_invalid_homeserver">"Δεν μπορούσαμε να επικοινωνήσουμε με αυτόν τον οικιακό διακομιστή. Βεβαιώσου ότι έχεις εισαγάγει σωστά τη διεύθυνση URL του αρχικού διακομιστή. Εάν η διεύθυνση URL είναι σωστή, επικοινώνησε με τον διαχειριστή του κεντρικού διακομιστή για περαιτέρω βοήθεια."</string> <string name="screen_change_server_error_invalid_homeserver">"Δεν μπορούσαμε να επικοινωνήσουμε με αυτόν τον οικιακό διακομιστή. Βεβαιώσου ότι έχεις εισαγάγει σωστά τη διεύθυνση URL του αρχικού διακομιστή. Εάν η διεύθυνση URL είναι σωστή, επικοινώνησε με τον διαχειριστή του κεντρικού διακομιστή για περαιτέρω βοήθεια."</string>
<string name="screen_change_server_error_invalid_well_known">"Το Sliding sync δεν είναι διαθέσιμο εξαιτίας ενός ζητήματος σε ένα πολύ γνωστό αρχείο: <string name="screen_change_server_error_invalid_well_known">"Ο διακομιστής δεν είναι διαθέσιμος λόγω προβλήματος στο αρχείο .well-known:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_form_header">"URL οικιακού διακομιστή"</string> <string name="screen_change_server_form_header">"URL οικιακού διακομιστή"</string>
<string name="screen_change_server_subtitle">"Ποια είναι η διεύθυνση του διακομιστή σου;"</string> <string name="screen_change_server_subtitle">"Ποια είναι η διεύθυνση του διακομιστή σου;"</string>

View file

@ -17,6 +17,7 @@
<string name="screen_change_server_error_invalid_well_known">"Server pole saadaval vea tõttu well-known failis: <string name="screen_change_server_error_invalid_well_known">"Server pole saadaval vea tõttu well-known failis:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Valitud teenusepakkuja ei toeta „sliding sync“ režiimi. Rakenduse %1$s kasutamiseks on vaja serverit uuendada."</string> <string name="screen_change_server_error_no_sliding_sync_message">"Valitud teenusepakkuja ei toeta „sliding sync“ režiimi. Rakenduse %1$s kasutamiseks on vaja serverit uuendada."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s ei saa kasutada %2$s koduserverit."</string>
<string name="screen_change_server_form_header">"Koduserveri url"</string> <string name="screen_change_server_form_header">"Koduserveri url"</string>
<string name="screen_change_server_form_notice">"Sisesta domeeni aadress."</string> <string name="screen_change_server_form_notice">"Sisesta domeeni aadress."</string>
<string name="screen_change_server_subtitle">"Mis on sinu koduserveri aadress?"</string> <string name="screen_change_server_subtitle">"Mis on sinu koduserveri aadress?"</string>

View file

@ -14,7 +14,7 @@
<string name="screen_change_account_provider_subtitle">"Utilisez un autre fournisseur de compte, tel que votre propre serveur privé ou un serveur professionnel."</string> <string name="screen_change_account_provider_subtitle">"Utilisez un autre fournisseur de compte, tel que votre propre serveur privé ou un serveur professionnel."</string>
<string name="screen_change_account_provider_title">"Changer de fournisseur de compte"</string> <string name="screen_change_account_provider_title">"Changer de fournisseur de compte"</string>
<string name="screen_change_server_error_invalid_homeserver">"Nous navons pas pu atteindre ce serveur daccueil. Vérifiez que vous avez correctement saisi lURL du serveur daccueil. Si lURL est correcte, contactez ladministrateur de votre serveur daccueil pour obtenir de laide."</string> <string name="screen_change_server_error_invalid_homeserver">"Nous navons pas pu atteindre ce serveur daccueil. Vérifiez que vous avez correctement saisi lURL du serveur daccueil. Si lURL est correcte, contactez ladministrateur de votre serveur daccueil pour obtenir de laide."</string>
<string name="screen_change_server_error_invalid_well_known">"Sliding sync nest pas disponible en raison dun problème dans le well-known file : <string name="screen_change_server_error_invalid_well_known">"Ce fournisseur de compte nest pas disponible en raison dun problème dans le fichier .well-known:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Le fournisseur de compte sélectionné ne prend pas en charge le sliding sync. Une mise à jour du serveur est nécessaire pour pouvoir utiliser %1$s."</string> <string name="screen_change_server_error_no_sliding_sync_message">"Le fournisseur de compte sélectionné ne prend pas en charge le sliding sync. Une mise à jour du serveur est nécessaire pour pouvoir utiliser %1$s."</string>
<string name="screen_change_server_form_header">"URL du serveur daccueil"</string> <string name="screen_change_server_form_header">"URL du serveur daccueil"</string>

View file

@ -14,7 +14,7 @@
<string name="screen_change_account_provider_subtitle">"Másik fiókszolgáltató, például a saját privát kiszolgáló vagy egy munkahelyi fiók használata."</string> <string name="screen_change_account_provider_subtitle">"Másik fiókszolgáltató, például a saját privát kiszolgáló vagy egy munkahelyi fiók használata."</string>
<string name="screen_change_account_provider_title">"Fiókszolgáltató módosítása"</string> <string name="screen_change_account_provider_title">"Fiókszolgáltató módosítása"</string>
<string name="screen_change_server_error_invalid_homeserver">"Nem sikerült elérni ezt a Matrix-kiszolgálót. Ellenőrizze, hogy helyesen adta-e meg a Matrix-kiszolgáló webcímét. Ha a webcím helyes, akkor további segítségért lépjen kapcsolatba a Matrix-kiszolgáló adminisztrátorával."</string> <string name="screen_change_server_error_invalid_homeserver">"Nem sikerült elérni ezt a Matrix-kiszolgálót. Ellenőrizze, hogy helyesen adta-e meg a Matrix-kiszolgáló webcímét. Ha a webcím helyes, akkor további segítségért lépjen kapcsolatba a Matrix-kiszolgáló adminisztrátorával."</string>
<string name="screen_change_server_error_invalid_well_known">"A Sliding sync protokoll a well-known fájl problémája miatt nem érhető el: <string name="screen_change_server_error_invalid_well_known">"A kiszolgáló a well-known fájl problémája miatt nem érhető el:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"A kiválasztott fiókszolgáltató nem támogatja a csúszóablakos szinkronizálást. Az %1$s használatához kiszolgálófrissítés szükséges."</string> <string name="screen_change_server_error_no_sliding_sync_message">"A kiválasztott fiókszolgáltató nem támogatja a csúszóablakos szinkronizálást. Az %1$s használatához kiszolgálófrissítés szükséges."</string>
<string name="screen_change_server_form_header">"Matrix-kiszolgáló webcíme"</string> <string name="screen_change_server_form_header">"Matrix-kiszolgáló webcíme"</string>

View file

@ -14,9 +14,10 @@
<string name="screen_change_account_provider_subtitle">"Użyj innego dostawcy konta, takiego jak własny serwer lub konta służbowego."</string> <string name="screen_change_account_provider_subtitle">"Użyj innego dostawcy konta, takiego jak własny serwer lub konta służbowego."</string>
<string name="screen_change_account_provider_title">"Zmień dostawcę konta"</string> <string name="screen_change_account_provider_title">"Zmień dostawcę konta"</string>
<string name="screen_change_server_error_invalid_homeserver">"Nie mogliśmy połączyć się z tym serwerem domowym. Sprawdź, czy adres URL serwera został wprowadzony poprawnie. Jeśli adres URL jest poprawny, skontaktuj się z administratorem serwera w celu uzyskania dalszej pomocy."</string> <string name="screen_change_server_error_invalid_homeserver">"Nie mogliśmy połączyć się z tym serwerem domowym. Sprawdź, czy adres URL serwera został wprowadzony poprawnie. Jeśli adres URL jest poprawny, skontaktuj się z administratorem serwera w celu uzyskania dalszej pomocy."</string>
<string name="screen_change_server_error_invalid_well_known">"Sliding sync nie jest dostępny z powodu problemu w znanym pliku: <string name="screen_change_server_error_invalid_well_known">"Serwer nie jest dostępny z powodu problemu pliku .well-known:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Wybrany dostawca konta nie wspiera synchronizacji przesuwnej. Wymagana jest aktualizacja serwera %1$s."</string> <string name="screen_change_server_error_no_sliding_sync_message">"Wybrany dostawca konta nie wspiera synchronizacji przesuwnej. Wymagana jest aktualizacja serwera %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s nie posiada zezwolenia na dołączenie do %2$s."</string>
<string name="screen_change_server_form_header">"URL serwera domowego"</string> <string name="screen_change_server_form_header">"URL serwera domowego"</string>
<string name="screen_change_server_form_notice">"Wprowadź adres domeny."</string> <string name="screen_change_server_form_notice">"Wprowadź adres domeny."</string>
<string name="screen_change_server_subtitle">"Jaki jest adres Twojego serwera?"</string> <string name="screen_change_server_subtitle">"Jaki jest adres Twojego serwera?"</string>

View file

@ -17,6 +17,7 @@
<string name="screen_change_server_error_invalid_well_known">"Sliding sync недоступен из-за проблемы в известном файле: <string name="screen_change_server_error_invalid_well_known">"Sliding sync недоступен из-за проблемы в известном файле:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_form_header">"URL-адрес домашнего сервера"</string> <string name="screen_change_server_form_header">"URL-адрес домашнего сервера"</string>
<string name="screen_change_server_form_notice">"Введите адрес домена."</string>
<string name="screen_change_server_subtitle">"Какой адрес у вашего сервера?"</string> <string name="screen_change_server_subtitle">"Какой адрес у вашего сервера?"</string>
<string name="screen_change_server_title">"Выберите свой сервер"</string> <string name="screen_change_server_title">"Выберите свой сервер"</string>
<string name="screen_create_account_title">"Создать учетную запись"</string> <string name="screen_create_account_title">"Создать учетную запись"</string>

View file

@ -17,6 +17,7 @@
<string name="screen_change_server_error_invalid_well_known">"Server nie je k dispozícii kvôli problému v známom súbore: <string name="screen_change_server_error_invalid_well_known">"Server nie je k dispozícii kvôli problému v známom súbore:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Vybraný poskytovateľ účtu nepodporuje kĺzavú synchronizáciu. Na používanie aplikácie %1$s je potrebná aktualizácia servera,"</string> <string name="screen_change_server_error_no_sliding_sync_message">"Vybraný poskytovateľ účtu nepodporuje kĺzavú synchronizáciu. Na používanie aplikácie %1$s je potrebná aktualizácia servera,"</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s nemá dovolené pripojiť sa k %2$s."</string>
<string name="screen_change_server_form_header">"Adresa URL domovského servera"</string> <string name="screen_change_server_form_header">"Adresa URL domovského servera"</string>
<string name="screen_change_server_form_notice">"Zadajte adresu domény."</string> <string name="screen_change_server_form_notice">"Zadajte adresu domény."</string>
<string name="screen_change_server_subtitle">"Aká je adresa vášho servera?"</string> <string name="screen_change_server_subtitle">"Aká je adresa vášho servera?"</string>

View file

@ -0,0 +1,189 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.screens.loginpassword
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.matrix.test.A_PASSWORD
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
class LoginPasswordViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on back invoke back callback`() {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>(expectEvents = false)
ensureCalledOnce { callback ->
rule.setLoginPasswordView(
aLoginPasswordState(
eventSink = eventsRecorder
),
onBackClick = callback,
)
rule.pressBack()
}
}
@Test
fun `changing login invokes the expected event`() {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>()
rule.setLoginPasswordView(
aLoginPasswordState(
eventSink = eventsRecorder,
),
)
val userNameHint = rule.activity.getString(CommonStrings.common_username)
rule.onNodeWithText(userNameHint).performTextInput(A_USER_NAME)
eventsRecorder.assertSingle(
LoginPasswordEvents.SetLogin(A_USER_NAME)
)
}
@Test
fun `changing login removes new lines the expected event`() {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>()
rule.setLoginPasswordView(
aLoginPasswordState(
eventSink = eventsRecorder,
),
)
val userNameHint = rule.activity.getString(CommonStrings.common_username)
rule.onNodeWithText(userNameHint).performTextInput("a\nb")
eventsRecorder.assertSingle(
LoginPasswordEvents.SetLogin("ab")
)
}
@Test
fun `clearing login invokes the expected event`() {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>()
rule.setLoginPasswordView(
aLoginPasswordState(
formState = aLoginFormState(A_USER_NAME),
eventSink = eventsRecorder,
),
)
val a11yClear = rule.activity.getString(CommonStrings.action_clear)
rule.onNodeWithContentDescription(a11yClear).performClick()
eventsRecorder.assertSingle(
LoginPasswordEvents.SetLogin("")
)
}
@Test
fun `changing password invokes the expected event`() {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>()
rule.setLoginPasswordView(
aLoginPasswordState(
eventSink = eventsRecorder,
),
)
val userNameHint = rule.activity.getString(CommonStrings.common_password)
rule.onNodeWithText(userNameHint).performTextInput(A_PASSWORD)
eventsRecorder.assertSingle(
LoginPasswordEvents.SetPassword(A_PASSWORD)
)
}
@Test
fun `reveal password makes the password visible`() {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>(expectEvents = false)
rule.setLoginPasswordView(
aLoginPasswordState(
formState = aLoginFormState(password = A_PASSWORD),
eventSink = eventsRecorder,
),
)
rule.onNodeWithText(A_PASSWORD).assertDoesNotExist()
// Show password
val a11yShowPassword = rule.activity.getString(CommonStrings.a11y_show_password)
rule.onNodeWithContentDescription(a11yShowPassword).performClick()
rule.onNodeWithText(A_PASSWORD).assertExists()
// Hide password
val a11yHidePassword = rule.activity.getString(CommonStrings.a11y_hide_password)
rule.onNodeWithContentDescription(a11yHidePassword).performClick()
rule.onNodeWithText(A_PASSWORD).assertDoesNotExist()
}
@Test
fun `when login is empty, continue button is not enabled`() {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>(expectEvents = false)
rule.setLoginPasswordView(
aLoginPasswordState(
formState = aLoginFormState(password = A_PASSWORD),
eventSink = eventsRecorder,
),
)
val continueStr = rule.activity.getString(CommonStrings.action_continue)
rule.onNodeWithText(continueStr).assertIsNotEnabled()
}
@Test
fun `when password is empty, continue button is not enabled`() {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>(expectEvents = false)
rule.setLoginPasswordView(
aLoginPasswordState(
formState = aLoginFormState(login = A_USER_NAME),
eventSink = eventsRecorder,
),
)
val continueStr = rule.activity.getString(CommonStrings.action_continue)
rule.onNodeWithText(continueStr).assertIsNotEnabled()
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on Continue sends expected event`() {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>()
rule.setLoginPasswordView(
aLoginPasswordState(
formState = aLoginFormState(login = A_USER_NAME, password = A_PASSWORD),
eventSink = eventsRecorder,
),
)
val continueStr = rule.activity.getString(CommonStrings.action_continue)
rule.onNodeWithText(continueStr).assertIsEnabled()
rule.clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(
LoginPasswordEvents.Submit
)
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setLoginPasswordView(
state: LoginPasswordState,
onBackClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
LoginPasswordView(
state = state,
onBackClick = onBackClick,
)
}
}

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_onboarding_sign_in_manually">"Zaloguj się ręcznie"</string> <string name="screen_onboarding_sign_in_manually">"Zaloguj się ręcznie"</string>
<string name="screen_onboarding_sign_in_to">"Zaloguj się do %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Zaloguj się za pomocą kodu QR"</string> <string name="screen_onboarding_sign_in_with_qr_code">"Zaloguj się za pomocą kodu QR"</string>
<string name="screen_onboarding_sign_up">"Utwórz konto"</string> <string name="screen_onboarding_sign_up">"Utwórz konto"</string>
<string name="screen_onboarding_welcome_message">"Witamy w %1$s. Szybszy i prostszy niż kiedykolwiek."</string> <string name="screen_onboarding_welcome_message">"Witamy w %1$s. Szybszy i prostszy niż kiedykolwiek."</string>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_onboarding_sign_in_manually">"Sign in manually"</string> <string name="screen_onboarding_sign_in_manually">"Sign in manually"</string>
<string name="screen_onboarding_sign_in_to">"Sign in to %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Sign in with QR code"</string> <string name="screen_onboarding_sign_in_with_qr_code">"Sign in with QR code"</string>
<string name="screen_onboarding_sign_up">"Create account"</string> <string name="screen_onboarding_sign_up">"Create account"</string>
<string name="screen_onboarding_welcome_message">"Welcome to the fastest %1$s ever. Supercharged for speed and simplicity."</string> <string name="screen_onboarding_welcome_message">"Welcome to the fastest %1$s ever. Supercharged for speed and simplicity."</string>

View file

@ -25,7 +25,6 @@ import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test

View file

@ -38,7 +38,7 @@ import io.element.android.features.roomdetails.impl.notificationsettings.RoomNot
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode
import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyFlowNode import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyFlowNode
import io.element.android.features.userprofile.shared.UserProfileNodeHelper import io.element.android.features.userprofile.shared.UserProfileNodeHelper
import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint
import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BackstackWithOverlayBox
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
@ -71,7 +71,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint, private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
private val mediaViewerEntryPoint: MediaViewerEntryPoint, private val mediaViewerEntryPoint: MediaViewerEntryPoint,
private val mediaGalleryEntryPoint: MediaGalleryEntryPoint, private val mediaGalleryEntryPoint: MediaGalleryEntryPoint,
private val verifySessionEntryPoint: VerifySessionEntryPoint, private val outgoingVerificationEntryPoint: OutgoingVerificationEntryPoint,
private val reportRoomEntryPoint: ReportRoomEntryPoint, private val reportRoomEntryPoint: ReportRoomEntryPoint,
) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>( ) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
@ -328,13 +328,13 @@ class RoomDetailsFlowNode @AssistedInject constructor(
createNode<SecurityAndPrivacyFlowNode>(buildContext) createNode<SecurityAndPrivacyFlowNode>(buildContext)
} }
is NavTarget.VerifyUser -> { is NavTarget.VerifyUser -> {
val params = VerifySessionEntryPoint.Params( val params = OutgoingVerificationEntryPoint.Params(
showDeviceVerifiedScreen = true, showDeviceVerifiedScreen = true,
verificationRequest = VerificationRequest.Outgoing.User(userId = navTarget.userId,) verificationRequest = VerificationRequest.Outgoing.User(userId = navTarget.userId,)
) )
verifySessionEntryPoint.nodeBuilder(this, buildContext) outgoingVerificationEntryPoint.nodeBuilder(this, buildContext)
.params(params) .params(params)
.callback(object : VerifySessionEntryPoint.Callback { .callback(object : OutgoingVerificationEntryPoint.Callback {
override fun onDone() { override fun onDone() {
backstack.pop() backstack.pop()
} }

View file

@ -25,7 +25,7 @@ import io.element.android.features.call.api.ElementCallEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.userprofile.impl.root.UserProfileNode import io.element.android.features.userprofile.impl.root.UserProfileNode
import io.element.android.features.userprofile.shared.UserProfileNodeHelper import io.element.android.features.userprofile.shared.UserProfileNodeHelper
import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
@ -46,7 +46,7 @@ class UserProfileFlowNode @AssistedInject constructor(
private val elementCallEntryPoint: ElementCallEntryPoint, private val elementCallEntryPoint: ElementCallEntryPoint,
private val sessionIdHolder: CurrentSessionIdHolder, private val sessionIdHolder: CurrentSessionIdHolder,
private val mediaViewerEntryPoint: MediaViewerEntryPoint, private val mediaViewerEntryPoint: MediaViewerEntryPoint,
private val verifySessionEntryPoint: VerifySessionEntryPoint, private val outgoingVerificationEntryPoint: OutgoingVerificationEntryPoint,
) : BaseFlowNode<UserProfileFlowNode.NavTarget>( ) : BaseFlowNode<UserProfileFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.Root, initialElement = NavTarget.Root,
@ -110,11 +110,11 @@ class UserProfileFlowNode @AssistedInject constructor(
.build() .build()
} }
is NavTarget.VerifyUser -> { is NavTarget.VerifyUser -> {
val params = VerifySessionEntryPoint.Params( val params = OutgoingVerificationEntryPoint.Params(
showDeviceVerifiedScreen = false, showDeviceVerifiedScreen = false,
verificationRequest = VerificationRequest.Outgoing.User(userId = navTarget.userId) verificationRequest = VerificationRequest.Outgoing.User(userId = navTarget.userId)
) )
verifySessionEntryPoint.nodeBuilder(this, buildContext) outgoingVerificationEntryPoint.nodeBuilder(this, buildContext)
.params(params) .params(params)
.build() .build()
} }

View file

@ -14,7 +14,7 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.matrix.api.verification.VerificationRequest
interface VerifySessionEntryPoint : FeatureEntryPoint { interface OutgoingVerificationEntryPoint : FeatureEntryPoint {
data class Params( data class Params(
val showDeviceVerifiedScreen: Boolean, val showDeviceVerifiedScreen: Boolean,
val verificationRequest: VerificationRequest.Outgoing, val verificationRequest: VerificationRequest.Outgoing,

View file

@ -11,29 +11,29 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import javax.inject.Inject import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultVerifySessionEntryPoint @Inject constructor() : VerifySessionEntryPoint { class DefaultOutgoingVerificationEntryPoint @Inject constructor() : OutgoingVerificationEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): VerifySessionEntryPoint.NodeBuilder { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): OutgoingVerificationEntryPoint.NodeBuilder {
val plugins = ArrayList<Plugin>() val plugins = ArrayList<Plugin>()
return object : VerifySessionEntryPoint.NodeBuilder { return object : OutgoingVerificationEntryPoint.NodeBuilder {
override fun callback(callback: VerifySessionEntryPoint.Callback): VerifySessionEntryPoint.NodeBuilder { override fun callback(callback: OutgoingVerificationEntryPoint.Callback): OutgoingVerificationEntryPoint.NodeBuilder {
plugins += callback plugins += callback
return this return this
} }
override fun params(params: VerifySessionEntryPoint.Params): VerifySessionEntryPoint.NodeBuilder { override fun params(params: OutgoingVerificationEntryPoint.Params): OutgoingVerificationEntryPoint.NodeBuilder {
plugins += params plugins += params
return this return this
} }
override fun build(): Node { override fun build(): Node {
return parentNode.createNode<VerifySelfSessionNode>(buildContext, plugins) return parentNode.createNode<OutgoingVerificationNode>(buildContext, plugins)
} }
} }
} }

View file

@ -16,19 +16,19 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
class VerifySelfSessionNode @AssistedInject constructor( class OutgoingVerificationNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
presenterFactory: VerifySelfSessionPresenter.Factory, presenterFactory: OutgoingVerificationPresenter.Factory,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
private val callback = plugins<VerifySessionEntryPoint.Callback>().first() private val callback = plugins<OutgoingVerificationEntryPoint.Callback>().first()
private val inputs = inputs<VerifySessionEntryPoint.Params>() private val inputs = inputs<OutgoingVerificationEntryPoint.Params>()
private val presenter = presenterFactory.create( private val presenter = presenterFactory.create(
showDeviceVerifiedScreen = inputs.showDeviceVerifiedScreen, showDeviceVerifiedScreen = inputs.showDeviceVerifiedScreen,
@ -38,7 +38,7 @@ class VerifySelfSessionNode @AssistedInject constructor(
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
VerifySelfSessionView( OutgoingVerificationView(
state = state, state = state,
modifier = modifier, modifier = modifier,
onLearnMoreClick = callback::onLearnMoreAboutEncryption, onLearnMoreClick = callback::onLearnMoreAboutEncryption,

View file

@ -31,30 +31,30 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionStateMachine.Event as StateMachineEvent import io.element.android.features.verifysession.impl.outgoing.OutgoingVerificationStateMachine.Event as StateMachineEvent
import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionStateMachine.State as StateMachineState import io.element.android.features.verifysession.impl.outgoing.OutgoingVerificationStateMachine.State as StateMachineState
class VerifySelfSessionPresenter @AssistedInject constructor( class OutgoingVerificationPresenter @AssistedInject constructor(
@Assisted private val showDeviceVerifiedScreen: Boolean, @Assisted private val showDeviceVerifiedScreen: Boolean,
@Assisted private val verificationRequest: VerificationRequest.Outgoing, @Assisted private val verificationRequest: VerificationRequest.Outgoing,
private val sessionVerificationService: SessionVerificationService, private val sessionVerificationService: SessionVerificationService,
private val encryptionService: EncryptionService, private val encryptionService: EncryptionService,
) : Presenter<VerifySelfSessionState> { ) : Presenter<OutgoingVerificationState> {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create( fun create(
verificationRequest: VerificationRequest.Outgoing, verificationRequest: VerificationRequest.Outgoing,
showDeviceVerifiedScreen: Boolean, showDeviceVerifiedScreen: Boolean,
): VerifySelfSessionPresenter ): OutgoingVerificationPresenter
} }
private val stateMachine = VerifySelfSessionStateMachine( private val stateMachine = OutgoingVerificationStateMachine(
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,
encryptionService = encryptionService, encryptionService = encryptionService,
) )
@Composable @Composable
override fun present(): VerifySelfSessionState { override fun present(): OutgoingVerificationState {
val stateAndDispatch = stateMachine.rememberStateAndDispatch() val stateAndDispatch = stateMachine.rememberStateAndDispatch()
val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState() val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState()
@ -63,17 +63,17 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
when (verificationRequest) { when (verificationRequest) {
is VerificationRequest.Outgoing.CurrentSession -> { is VerificationRequest.Outgoing.CurrentSession -> {
when (sessionVerifiedStatus) { when (sessionVerifiedStatus) {
SessionVerifiedStatus.Unknown -> VerifySelfSessionState.Step.Loading SessionVerifiedStatus.Unknown -> OutgoingVerificationState.Step.Loading
SessionVerifiedStatus.NotVerified -> { SessionVerifiedStatus.NotVerified -> {
stateAndDispatch.state.value.toVerificationStep() stateAndDispatch.state.value.toVerificationStep()
} }
SessionVerifiedStatus.Verified -> { SessionVerifiedStatus.Verified -> {
if (stateAndDispatch.state.value != StateMachineState.Initial || showDeviceVerifiedScreen) { if (stateAndDispatch.state.value != StateMachineState.Initial || showDeviceVerifiedScreen) {
// The user has verified the session, we need to show the success screen // The user has verified the session, we need to show the success screen
VerifySelfSessionState.Step.Completed OutgoingVerificationState.Step.Completed
} else { } else {
// Automatic verification, which can happen on freshly created account, in this case, skip the screen // Automatic verification, which can happen on freshly created account, in this case, skip the screen
VerifySelfSessionState.Step.Exit OutgoingVerificationState.Step.Exit
} }
} }
} }
@ -91,42 +91,44 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
observeVerificationService() observeVerificationService()
} }
fun handleEvents(event: VerifySelfSessionViewEvents) { fun handleEvents(event: OutgoingVerificationViewEvents) {
Timber.d("Verification user action: ${event::class.simpleName}") Timber.d("Verification user action: ${event::class.simpleName}")
when (event) { when (event) {
// Just relay the event to the state machine // Just relay the event to the state machine
VerifySelfSessionViewEvents.RequestVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.RequestVerification(verificationRequest)) OutgoingVerificationViewEvents.RequestVerification -> StateMachineEvent.RequestVerification(verificationRequest)
VerifySelfSessionViewEvents.StartSasVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.StartSasVerification) OutgoingVerificationViewEvents.StartSasVerification -> StateMachineEvent.StartSasVerification
VerifySelfSessionViewEvents.ConfirmVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.AcceptChallenge) OutgoingVerificationViewEvents.ConfirmVerification -> StateMachineEvent.AcceptChallenge
VerifySelfSessionViewEvents.DeclineVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge) OutgoingVerificationViewEvents.DeclineVerification -> StateMachineEvent.DeclineChallenge
VerifySelfSessionViewEvents.Cancel -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel) OutgoingVerificationViewEvents.Cancel -> StateMachineEvent.Cancel
VerifySelfSessionViewEvents.Reset -> stateAndDispatch.dispatchAction(StateMachineEvent.Reset) OutgoingVerificationViewEvents.Reset -> StateMachineEvent.Reset
}.let { stateMachineEvent ->
stateAndDispatch.dispatchAction(stateMachineEvent)
} }
} }
return VerifySelfSessionState( return OutgoingVerificationState(
step = step, step = step,
request = verificationRequest, request = verificationRequest,
eventSink = ::handleEvents, eventSink = ::handleEvents,
) )
} }
private fun StateMachineState?.toVerificationStep(): VerifySelfSessionState.Step = private fun StateMachineState?.toVerificationStep(): OutgoingVerificationState.Step =
when (val machineState = this) { when (val machineState = this) {
StateMachineState.Initial, null -> { StateMachineState.Initial, null -> {
VerifySelfSessionState.Step.Initial OutgoingVerificationState.Step.Initial
} }
is StateMachineState.RequestingVerification, is StateMachineState.RequestingVerification,
is StateMachineState.StartingSasVerification, is StateMachineState.StartingSasVerification,
StateMachineState.SasVerificationStarted -> { StateMachineState.SasVerificationStarted -> {
VerifySelfSessionState.Step.AwaitingOtherDeviceResponse OutgoingVerificationState.Step.AwaitingOtherDeviceResponse
} }
StateMachineState.VerificationRequestAccepted -> { StateMachineState.VerificationRequestAccepted -> {
VerifySelfSessionState.Step.Ready OutgoingVerificationState.Step.Ready
} }
is StateMachineState.Canceled -> { is StateMachineState.Canceled -> {
VerifySelfSessionState.Step.Canceled OutgoingVerificationState.Step.Canceled
} }
is StateMachineState.Verifying -> { is StateMachineState.Verifying -> {
@ -134,15 +136,15 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
is StateMachineState.Verifying.Replying -> AsyncData.Loading() is StateMachineState.Verifying.Replying -> AsyncData.Loading()
else -> AsyncData.Uninitialized else -> AsyncData.Uninitialized
} }
VerifySelfSessionState.Step.Verifying(machineState.data, async) OutgoingVerificationState.Step.Verifying(machineState.data, async)
} }
StateMachineState.Completed -> { StateMachineState.Completed -> {
VerifySelfSessionState.Step.Completed OutgoingVerificationState.Step.Completed
} }
StateMachineState.Exit -> { StateMachineState.Exit -> {
VerifySelfSessionState.Step.Exit OutgoingVerificationState.Step.Exit
} }
} }

View file

@ -14,10 +14,10 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationD
import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.matrix.api.verification.VerificationRequest
@Immutable @Immutable
data class VerifySelfSessionState( data class OutgoingVerificationState(
val step: Step, val step: Step,
val request: VerificationRequest.Outgoing, val request: VerificationRequest.Outgoing,
val eventSink: (VerifySelfSessionViewEvents) -> Unit, val eventSink: (OutgoingVerificationViewEvents) -> Unit,
) { ) {
@Stable @Stable
sealed interface Step { sealed interface Step {

View file

@ -29,10 +29,10 @@ import kotlin.time.Duration.Companion.seconds
import com.freeletics.flowredux.dsl.State as MachineState import com.freeletics.flowredux.dsl.State as MachineState
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
class VerifySelfSessionStateMachine( class OutgoingVerificationStateMachine(
private val sessionVerificationService: SessionVerificationService, private val sessionVerificationService: SessionVerificationService,
private val encryptionService: EncryptionService, private val encryptionService: EncryptionService,
) : FlowReduxStateMachine<VerifySelfSessionStateMachine.State, VerifySelfSessionStateMachine.Event>( ) : FlowReduxStateMachine<OutgoingVerificationStateMachine.State, OutgoingVerificationStateMachine.Event>(
initialState = State.Initial, initialState = State.Initial,
) { ) {
init { init {

View file

@ -8,64 +8,64 @@
package io.element.android.features.verifysession.impl.outgoing package io.element.android.features.verifysession.impl.outgoing
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionState.Step import io.element.android.features.verifysession.impl.outgoing.OutgoingVerificationState.Step
import io.element.android.features.verifysession.impl.ui.aDecimalsSessionVerificationData import io.element.android.features.verifysession.impl.ui.aDecimalsSessionVerificationData
import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.matrix.api.verification.VerificationRequest
open class VerifySelfSessionStateProvider : PreviewParameterProvider<VerifySelfSessionState> { open class OutgoingVerificationStateProvider : PreviewParameterProvider<OutgoingVerificationState> {
override val values: Sequence<VerifySelfSessionState> override val values: Sequence<OutgoingVerificationState>
get() = sequenceOf( get() = sequenceOf(
aVerifySelfSessionState( anOutgoingVerificationState(
step = Step.Initial, step = Step.Initial,
request = anOutgoingSessionVerificationRequest(), request = anOutgoingSessionVerificationRequest(),
), ),
aVerifySelfSessionState( anOutgoingVerificationState(
step = Step.Initial, step = Step.Initial,
request = anOutgoingUserVerificationRequest(), request = anOutgoingUserVerificationRequest(),
), ),
aVerifySelfSessionState( anOutgoingVerificationState(
step = Step.AwaitingOtherDeviceResponse, step = Step.AwaitingOtherDeviceResponse,
request = anOutgoingSessionVerificationRequest(), request = anOutgoingSessionVerificationRequest(),
), ),
aVerifySelfSessionState( anOutgoingVerificationState(
step = Step.AwaitingOtherDeviceResponse, step = Step.AwaitingOtherDeviceResponse,
request = anOutgoingUserVerificationRequest(), request = anOutgoingUserVerificationRequest(),
), ),
aVerifySelfSessionState( anOutgoingVerificationState(
step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized), step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized),
request = anOutgoingSessionVerificationRequest(), request = anOutgoingSessionVerificationRequest(),
), ),
aVerifySelfSessionState( anOutgoingVerificationState(
step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized), step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized),
request = anOutgoingUserVerificationRequest(), request = anOutgoingUserVerificationRequest(),
), ),
aVerifySelfSessionState( anOutgoingVerificationState(
step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Loading()) step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Loading())
), ),
aVerifySelfSessionState( anOutgoingVerificationState(
step = Step.Canceled step = Step.Canceled
), ),
aVerifySelfSessionState( anOutgoingVerificationState(
step = Step.Ready step = Step.Ready
), ),
aVerifySelfSessionState( anOutgoingVerificationState(
step = Step.Verifying(aDecimalsSessionVerificationData(), AsyncData.Uninitialized) step = Step.Verifying(aDecimalsSessionVerificationData(), AsyncData.Uninitialized)
), ),
aVerifySelfSessionState( anOutgoingVerificationState(
step = Step.Completed, step = Step.Completed,
request = anOutgoingSessionVerificationRequest(), request = anOutgoingSessionVerificationRequest(),
), ),
aVerifySelfSessionState( anOutgoingVerificationState(
step = Step.Completed, step = Step.Completed,
request = anOutgoingUserVerificationRequest(), request = anOutgoingUserVerificationRequest(),
), ),
aVerifySelfSessionState( anOutgoingVerificationState(
step = Step.Loading step = Step.Loading
), ),
aVerifySelfSessionState( anOutgoingVerificationState(
step = Step.Exit step = Step.Exit
), ),
// Add other state here // Add other state here
@ -75,11 +75,11 @@ open class VerifySelfSessionStateProvider : PreviewParameterProvider<VerifySelfS
internal fun anOutgoingUserVerificationRequest() = VerificationRequest.Outgoing.User(userId = UserId("@alice:example.com")) internal fun anOutgoingUserVerificationRequest() = VerificationRequest.Outgoing.User(userId = UserId("@alice:example.com"))
internal fun anOutgoingSessionVerificationRequest() = VerificationRequest.Outgoing.CurrentSession internal fun anOutgoingSessionVerificationRequest() = VerificationRequest.Outgoing.CurrentSession
internal fun aVerifySelfSessionState( internal fun anOutgoingVerificationState(
step: Step = Step.Initial, step: Step = Step.Initial,
request: VerificationRequest.Outgoing = anOutgoingSessionVerificationRequest(), request: VerificationRequest.Outgoing = anOutgoingSessionVerificationRequest(),
eventSink: (VerifySelfSessionViewEvents) -> Unit = {}, eventSink: (OutgoingVerificationViewEvents) -> Unit = {},
) = VerifySelfSessionState( ) = OutgoingVerificationState(
step = step, step = step,
request = request, request = request,
eventSink = eventSink, eventSink = eventSink,

View file

@ -27,7 +27,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.verifysession.impl.R import io.element.android.features.verifysession.impl.R
import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionState.Step import io.element.android.features.verifysession.impl.outgoing.OutgoingVerificationState.Step
import io.element.android.features.verifysession.impl.ui.VerificationBottomMenu import io.element.android.features.verifysession.impl.ui.VerificationBottomMenu
import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -49,8 +49,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun VerifySelfSessionView( fun OutgoingVerificationView(
state: VerifySelfSessionState, state: OutgoingVerificationState,
onLearnMoreClick: () -> Unit, onLearnMoreClick: () -> Unit,
onFinish: () -> Unit, onFinish: () -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
@ -59,12 +59,12 @@ fun VerifySelfSessionView(
val step = state.step val step = state.step
fun cancelOrResetFlow() { fun cancelOrResetFlow() {
when (step) { when (step) {
is Step.Canceled -> state.eventSink(VerifySelfSessionViewEvents.Reset) is Step.Canceled -> state.eventSink(OutgoingVerificationViewEvents.Reset)
Step.Initial, Step.Completed -> onBack() Step.Initial, Step.Completed -> onBack()
Step.Ready, is Step.AwaitingOtherDeviceResponse -> state.eventSink(VerifySelfSessionViewEvents.Cancel) Step.Ready, is Step.AwaitingOtherDeviceResponse -> state.eventSink(OutgoingVerificationViewEvents.Cancel)
is Step.Verifying -> { is Step.Verifying -> {
if (!step.state.isLoading()) { if (!step.state.isLoading()) {
state.eventSink(VerifySelfSessionViewEvents.DeclineVerification) state.eventSink(OutgoingVerificationViewEvents.DeclineVerification)
} }
} }
else -> Unit else -> Unit
@ -98,10 +98,10 @@ fun VerifySelfSessionView(
) )
}, },
header = { header = {
VerifySelfSessionHeader(step = step, request = state.request) OutgoingVerificationHeader(step = step, request = state.request)
}, },
footer = { footer = {
VerifySelfSessionBottomMenu( OutgoingVerificationViewBottomMenu(
screenState = state, screenState = state,
onCancelClick = ::cancelOrResetFlow, onCancelClick = ::cancelOrResetFlow,
onContinueClick = onFinish, onContinueClick = onFinish,
@ -109,7 +109,7 @@ fun VerifySelfSessionView(
}, },
isScrollable = true, isScrollable = true,
) { ) {
VerifySelfSessionContent( OutgoingVerificationContent(
flowState = step, flowState = step,
request = state.request, request = state.request,
onLearnMoreClick = onLearnMoreClick, onLearnMoreClick = onLearnMoreClick,
@ -119,7 +119,7 @@ fun VerifySelfSessionView(
} }
@Composable @Composable
private fun VerifySelfSessionHeader(step: Step, request: VerificationRequest.Outgoing) { private fun OutgoingVerificationHeader(step: Step, request: VerificationRequest.Outgoing) {
val iconStyle = when (step) { val iconStyle = when (step) {
Step.Loading -> error("Should not happen") Step.Loading -> error("Should not happen")
Step.Initial -> when (request) { Step.Initial -> when (request) {
@ -189,7 +189,7 @@ private fun VerifySelfSessionHeader(step: Step, request: VerificationRequest.Out
} }
@Composable @Composable
private fun VerifySelfSessionContent( private fun OutgoingVerificationContent(
flowState: Step, flowState: Step,
request: VerificationRequest.Outgoing, request: VerificationRequest.Outgoing,
onLearnMoreClick: () -> Unit, onLearnMoreClick: () -> Unit,
@ -227,8 +227,8 @@ private fun ContentInitial(
} }
@Composable @Composable
private fun VerifySelfSessionBottomMenu( private fun OutgoingVerificationViewBottomMenu(
screenState: VerifySelfSessionState, screenState: OutgoingVerificationState,
onCancelClick: () -> Unit, onCancelClick: () -> Unit,
onContinueClick: () -> Unit, onContinueClick: () -> Unit,
) { ) {
@ -244,7 +244,7 @@ private fun VerifySelfSessionBottomMenu(
Button( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_start_verification), text = stringResource(CommonStrings.action_start_verification),
onClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) }, onClick = { eventSink(OutgoingVerificationViewEvents.RequestVerification) },
) )
InvisibleButton() InvisibleButton()
} }
@ -264,7 +264,7 @@ private fun VerifySelfSessionBottomMenu(
Button( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_start), text = stringResource(CommonStrings.action_start),
onClick = { eventSink(VerifySelfSessionViewEvents.StartSasVerification) }, onClick = { eventSink(OutgoingVerificationViewEvents.StartSasVerification) },
) )
TextButton( TextButton(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -287,14 +287,14 @@ private fun VerifySelfSessionBottomMenu(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_session_verification_they_match), text = stringResource(R.string.screen_session_verification_they_match),
onClick = { onClick = {
eventSink(VerifySelfSessionViewEvents.ConfirmVerification) eventSink(OutgoingVerificationViewEvents.ConfirmVerification)
}, },
) )
TextButton( TextButton(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_session_verification_they_dont_match), text = stringResource(R.string.screen_session_verification_they_dont_match),
onClick = { eventSink(VerifySelfSessionViewEvents.DeclineVerification) }, onClick = { eventSink(OutgoingVerificationViewEvents.DeclineVerification) },
) )
} }
} }
@ -315,8 +315,8 @@ private fun VerifySelfSessionBottomMenu(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionStateProvider::class) state: VerifySelfSessionState) = ElementPreview { internal fun OutgoingVerificationViewPreview(@PreviewParameter(OutgoingVerificationStateProvider::class) state: OutgoingVerificationState) = ElementPreview {
VerifySelfSessionView( OutgoingVerificationView(
state = state, state = state,
onLearnMoreClick = {}, onLearnMoreClick = {},
onFinish = {}, onFinish = {},

View file

@ -0,0 +1,17 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.verifysession.impl.outgoing
sealed interface OutgoingVerificationViewEvents {
data object RequestVerification : OutgoingVerificationViewEvents
data object StartSasVerification : OutgoingVerificationViewEvents
data object ConfirmVerification : OutgoingVerificationViewEvents
data object DeclineVerification : OutgoingVerificationViewEvents
data object Cancel : OutgoingVerificationViewEvents
data object Reset : OutgoingVerificationViewEvents
}

View file

@ -1,17 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.verifysession.impl.outgoing
sealed interface VerifySelfSessionViewEvents {
data object RequestVerification : VerifySelfSessionViewEvents
data object StartSasVerification : VerifySelfSessionViewEvents
data object ConfirmVerification : VerifySelfSessionViewEvents
data object DeclineVerification : VerifySelfSessionViewEvents
data object Cancel : VerifySelfSessionViewEvents
data object Reset : VerifySelfSessionViewEvents
}

View file

@ -9,7 +9,7 @@ package io.element.android.features.verifysession.impl.outgoing
import app.cash.turbine.ReceiveTurbine import app.cash.turbine.ReceiveTurbine
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionState.Step import io.element.android.features.verifysession.impl.outgoing.OutgoingVerificationState.Step
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.EncryptionService
@ -31,13 +31,13 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
class VerifySelfSessionPresenterTest { class OutgoingVerificationPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
@Test @Test
fun `present - Initial state is received`() = runTest { fun `present - Initial state is received`() = runTest {
val presenter = createVerifySelfSessionPresenter( val presenter = createOutgoingVerificationPresenter(
service = unverifiedSessionService(), service = unverifiedSessionService(),
) )
presenter.test { presenter.test {
@ -55,7 +55,7 @@ class VerifySelfSessionPresenterTest {
requestSessionVerificationLambda = requestSessionVerificationRecorder, requestSessionVerificationLambda = requestSessionVerificationRecorder,
startVerificationLambda = startVerificationRecorder, startVerificationLambda = startVerificationRecorder,
) )
val presenter = createVerifySelfSessionPresenter( val presenter = createOutgoingVerificationPresenter(
service = service, service = service,
verificationRequest = anOutgoingSessionVerificationRequest(), verificationRequest = anOutgoingSessionVerificationRequest(),
) )
@ -75,7 +75,7 @@ class VerifySelfSessionPresenterTest {
requestUserVerificationLambda = requestUserVerificationRecorder, requestUserVerificationLambda = requestUserVerificationRecorder,
startVerificationLambda = startVerificationRecorder, startVerificationLambda = startVerificationRecorder,
) )
val presenter = createVerifySelfSessionPresenter( val presenter = createOutgoingVerificationPresenter(
service = service, service = service,
verificationRequest = anOutgoingUserVerificationRequest(), verificationRequest = anOutgoingUserVerificationRequest(),
) )
@ -89,14 +89,14 @@ class VerifySelfSessionPresenterTest {
@Test @Test
fun `present - Cancellation on initial state moves to Exit state`() = runTest { fun `present - Cancellation on initial state moves to Exit state`() = runTest {
val presenter = createVerifySelfSessionPresenter( val presenter = createOutgoingVerificationPresenter(
service = unverifiedSessionService(), service = unverifiedSessionService(),
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.step).isEqualTo(Step.Initial) assertThat(initialState.step).isEqualTo(Step.Initial)
val eventSink = initialState.eventSink val eventSink = initialState.eventSink
eventSink(VerifySelfSessionViewEvents.Cancel) eventSink(OutgoingVerificationViewEvents.Cancel)
assertThat(awaitItem().step).isEqualTo(Step.Exit) assertThat(awaitItem().step).isEqualTo(Step.Exit)
} }
@ -109,10 +109,10 @@ class VerifySelfSessionPresenterTest {
startVerificationLambda = { }, startVerificationLambda = { },
approveVerificationLambda = { }, approveVerificationLambda = { },
) )
val presenter = createVerifySelfSessionPresenter(service) val presenter = createOutgoingVerificationPresenter(service)
presenter.test { presenter.test {
val state = requestVerificationAndAwaitVerifyingState(service) val state = requestVerificationAndAwaitVerifyingState(service)
state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification) state.eventSink(OutgoingVerificationViewEvents.ConfirmVerification)
// Cancelling // Cancelling
assertThat(awaitItem().step).isInstanceOf(Step.Verifying::class.java) assertThat(awaitItem().step).isInstanceOf(Step.Verifying::class.java)
service.emitVerificationFlowState(VerificationFlowState.DidFail) service.emitVerificationFlowState(VerificationFlowState.DidFail)
@ -126,9 +126,9 @@ class VerifySelfSessionPresenterTest {
val service = unverifiedSessionService( val service = unverifiedSessionService(
requestSessionVerificationLambda = { }, requestSessionVerificationLambda = { },
) )
val presenter = createVerifySelfSessionPresenter(service) val presenter = createOutgoingVerificationPresenter(service)
presenter.test { presenter.test {
awaitItem().eventSink(VerifySelfSessionViewEvents.RequestVerification) awaitItem().eventSink(OutgoingVerificationViewEvents.RequestVerification)
service.emitVerificationFlowState(VerificationFlowState.DidFail) service.emitVerificationFlowState(VerificationFlowState.DidFail)
assertThat(awaitItem().step).isInstanceOf(Step.AwaitingOtherDeviceResponse::class.java) assertThat(awaitItem().step).isInstanceOf(Step.AwaitingOtherDeviceResponse::class.java)
assertThat(awaitItem().step).isEqualTo(Step.Canceled) assertThat(awaitItem().step).isEqualTo(Step.Canceled)
@ -142,10 +142,10 @@ class VerifySelfSessionPresenterTest {
startVerificationLambda = { }, startVerificationLambda = { },
cancelVerificationLambda = { }, cancelVerificationLambda = { },
) )
val presenter = createVerifySelfSessionPresenter(service) val presenter = createOutgoingVerificationPresenter(service)
presenter.test { presenter.test {
val state = requestVerificationAndAwaitVerifyingState(service) val state = requestVerificationAndAwaitVerifyingState(service)
state.eventSink(VerifySelfSessionViewEvents.Cancel) state.eventSink(OutgoingVerificationViewEvents.Cancel)
assertThat(awaitItem().step).isEqualTo(Step.Canceled) assertThat(awaitItem().step).isEqualTo(Step.Canceled)
} }
} }
@ -156,7 +156,7 @@ class VerifySelfSessionPresenterTest {
requestSessionVerificationLambda = { }, requestSessionVerificationLambda = { },
startVerificationLambda = { }, startVerificationLambda = { },
) )
val presenter = createVerifySelfSessionPresenter(service) val presenter = createOutgoingVerificationPresenter(service)
presenter.test { presenter.test {
requestVerificationAndAwaitVerifyingState(service) requestVerificationAndAwaitVerifyingState(service)
service.emitVerificationFlowState(VerificationFlowState.DidReceiveVerificationData(SessionVerificationData.Emojis(emptyList()))) service.emitVerificationFlowState(VerificationFlowState.DidReceiveVerificationData(SessionVerificationData.Emojis(emptyList())))
@ -170,12 +170,12 @@ class VerifySelfSessionPresenterTest {
requestSessionVerificationLambda = { }, requestSessionVerificationLambda = { },
startVerificationLambda = { }, startVerificationLambda = { },
) )
val presenter = createVerifySelfSessionPresenter(service) val presenter = createOutgoingVerificationPresenter(service)
presenter.test { presenter.test {
val state = requestVerificationAndAwaitVerifyingState(service) val state = requestVerificationAndAwaitVerifyingState(service)
service.emitVerificationFlowState(VerificationFlowState.DidCancel) service.emitVerificationFlowState(VerificationFlowState.DidCancel)
assertThat(awaitItem().step).isEqualTo(Step.Canceled) assertThat(awaitItem().step).isEqualTo(Step.Canceled)
state.eventSink(VerifySelfSessionViewEvents.Reset) state.eventSink(OutgoingVerificationViewEvents.Reset)
// Went back to initial state // Went back to initial state
assertThat(awaitItem().step).isEqualTo(Step.Initial) assertThat(awaitItem().step).isEqualTo(Step.Initial)
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
@ -192,13 +192,13 @@ class VerifySelfSessionPresenterTest {
startVerificationLambda = { }, startVerificationLambda = { },
approveVerificationLambda = { }, approveVerificationLambda = { },
) )
val presenter = createVerifySelfSessionPresenter(service) val presenter = createOutgoingVerificationPresenter(service)
presenter.test { presenter.test {
val state = requestVerificationAndAwaitVerifyingState( val state = requestVerificationAndAwaitVerifyingState(
service, service,
SessionVerificationData.Emojis(emojis) SessionVerificationData.Emojis(emojis)
) )
state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification) state.eventSink(OutgoingVerificationViewEvents.ConfirmVerification)
assertThat(awaitItem().step).isEqualTo( assertThat(awaitItem().step).isEqualTo(
Step.Verifying( Step.Verifying(
SessionVerificationData.Emojis(emojis), SessionVerificationData.Emojis(emojis),
@ -217,10 +217,10 @@ class VerifySelfSessionPresenterTest {
startVerificationLambda = { }, startVerificationLambda = { },
declineVerificationLambda = { }, declineVerificationLambda = { },
) )
val presenter = createVerifySelfSessionPresenter(service) val presenter = createOutgoingVerificationPresenter(service)
presenter.test { presenter.test {
val state = requestVerificationAndAwaitVerifyingState(service) val state = requestVerificationAndAwaitVerifyingState(service)
state.eventSink(VerifySelfSessionViewEvents.DeclineVerification) state.eventSink(OutgoingVerificationViewEvents.DeclineVerification)
assertThat(awaitItem().step).isEqualTo( assertThat(awaitItem().step).isEqualTo(
Step.Verifying( Step.Verifying(
SessionVerificationData.Emojis(emptyList()), SessionVerificationData.Emojis(emptyList()),
@ -241,7 +241,7 @@ class VerifySelfSessionPresenterTest {
emitVerifiedStatus(SessionVerifiedStatus.Verified) emitVerifiedStatus(SessionVerifiedStatus.Verified)
emitVerificationFlowState(VerificationFlowState.DidFinish) emitVerificationFlowState(VerificationFlowState.DidFinish)
} }
val presenter = createVerifySelfSessionPresenter( val presenter = createOutgoingVerificationPresenter(
service = service, service = service,
showDeviceVerifiedScreen = true, showDeviceVerifiedScreen = true,
) )
@ -259,7 +259,7 @@ class VerifySelfSessionPresenterTest {
emitVerifiedStatus(SessionVerifiedStatus.Verified) emitVerifiedStatus(SessionVerifiedStatus.Verified)
emitVerificationFlowState(VerificationFlowState.DidFinish) emitVerificationFlowState(VerificationFlowState.DidFinish)
} }
val presenter = createVerifySelfSessionPresenter( val presenter = createOutgoingVerificationPresenter(
service = service, service = service,
showDeviceVerifiedScreen = false, showDeviceVerifiedScreen = false,
) )
@ -269,13 +269,13 @@ class VerifySelfSessionPresenterTest {
} }
} }
private suspend fun ReceiveTurbine<VerifySelfSessionState>.requestVerificationAndAwaitVerifyingState( private suspend fun ReceiveTurbine<OutgoingVerificationState>.requestVerificationAndAwaitVerifyingState(
fakeService: FakeSessionVerificationService, fakeService: FakeSessionVerificationService,
sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()), sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()),
): VerifySelfSessionState { ): OutgoingVerificationState {
var state = awaitItem() var state = awaitItem()
assertThat(state.step).isEqualTo(Step.Initial) assertThat(state.step).isEqualTo(Step.Initial)
state.eventSink(VerifySelfSessionViewEvents.RequestVerification) state.eventSink(OutgoingVerificationViewEvents.RequestVerification)
// Await for other device response: // Await for other device response:
fakeService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest) fakeService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest)
state = awaitItem() state = awaitItem()
@ -283,7 +283,7 @@ class VerifySelfSessionPresenterTest {
// Await for the state to be Ready // Await for the state to be Ready
state = awaitItem() state = awaitItem()
assertThat(state.step).isEqualTo(Step.Ready) assertThat(state.step).isEqualTo(Step.Ready)
state.eventSink(VerifySelfSessionViewEvents.StartSasVerification) state.eventSink(OutgoingVerificationViewEvents.StartSasVerification)
// Await for other device response (again): // Await for other device response (again):
fakeService.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification) fakeService.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification)
state = awaitItem() state = awaitItem()
@ -321,13 +321,13 @@ class VerifySelfSessionPresenterTest {
} }
} }
private fun createVerifySelfSessionPresenter( private fun createOutgoingVerificationPresenter(
service: SessionVerificationService, service: SessionVerificationService,
verificationRequest: VerificationRequest.Outgoing = anOutgoingSessionVerificationRequest(), verificationRequest: VerificationRequest.Outgoing = anOutgoingSessionVerificationRequest(),
encryptionService: EncryptionService = FakeEncryptionService(), encryptionService: EncryptionService = FakeEncryptionService(),
showDeviceVerifiedScreen: Boolean = false, showDeviceVerifiedScreen: Boolean = false,
): VerifySelfSessionPresenter { ): OutgoingVerificationPresenter {
return VerifySelfSessionPresenter( return OutgoingVerificationPresenter(
showDeviceVerifiedScreen = showDeviceVerifiedScreen, showDeviceVerifiedScreen = showDeviceVerifiedScreen,
verificationRequest = verificationRequest, verificationRequest = verificationRequest,
sessionVerificationService = service, sessionVerificationService = service,

View file

@ -26,54 +26,54 @@ import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class VerifySelfSessionViewTest { class OutgoingVerificationViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>() @get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `back key pressed - when canceled resets the flow`() { fun `back key pressed - when canceled resets the flow`() {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>() val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
rule.setVerifySelfSessionView( rule.setOutgoingVerificationView(
aVerifySelfSessionState( anOutgoingVerificationState(
step = VerifySelfSessionState.Step.Canceled, step = OutgoingVerificationState.Step.Canceled,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressBackKey() rule.pressBackKey()
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.Reset) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Reset)
} }
@Test @Test
fun `back key pressed - when awaiting response cancels the verification`() { fun `back key pressed - when awaiting response cancels the verification`() {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>() val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
rule.setVerifySelfSessionView( rule.setOutgoingVerificationView(
aVerifySelfSessionState( anOutgoingVerificationState(
step = VerifySelfSessionState.Step.AwaitingOtherDeviceResponse, step = OutgoingVerificationState.Step.AwaitingOtherDeviceResponse,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressBackKey() rule.pressBackKey()
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.Cancel) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel)
} }
@Test @Test
fun `back key pressed - when ready to verify cancels the verification`() { fun `back key pressed - when ready to verify cancels the verification`() {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>() val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
rule.setVerifySelfSessionView( rule.setOutgoingVerificationView(
aVerifySelfSessionState( anOutgoingVerificationState(
step = VerifySelfSessionState.Step.Ready, step = OutgoingVerificationState.Step.Ready,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressBackKey() rule.pressBackKey()
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.Cancel) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel)
} }
@Test @Test
fun `back key pressed - when verifying and not loading declines the verification`() { fun `back key pressed - when verifying and not loading declines the verification`() {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>() val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
rule.setVerifySelfSessionView( rule.setOutgoingVerificationView(
aVerifySelfSessionState( anOutgoingVerificationState(
step = VerifySelfSessionState.Step.Verifying( step = OutgoingVerificationState.Step.Verifying(
data = aEmojisSessionVerificationData(), data = aEmojisSessionVerificationData(),
state = AsyncData.Uninitialized, state = AsyncData.Uninitialized,
), ),
@ -81,15 +81,15 @@ class VerifySelfSessionViewTest {
), ),
) )
rule.pressBackKey() rule.pressBackKey()
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.DeclineVerification) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.DeclineVerification)
} }
@Test @Test
fun `back key pressed - when verifying and loading does nothing`() { fun `back key pressed - when verifying and loading does nothing`() {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>() val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
rule.setVerifySelfSessionView( rule.setOutgoingVerificationView(
aVerifySelfSessionState( anOutgoingVerificationState(
step = VerifySelfSessionState.Step.Verifying( step = OutgoingVerificationState.Step.Verifying(
data = aEmojisSessionVerificationData(), data = aEmojisSessionVerificationData(),
state = AsyncData.Loading(), state = AsyncData.Loading(),
), ),
@ -103,10 +103,10 @@ class VerifySelfSessionViewTest {
@Test @Test
fun `back key pressed - on Completed exits the flow`() { fun `back key pressed - on Completed exits the flow`() {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setVerifySelfSessionView( rule.setOutgoingVerificationView(
onBack = callback, onBack = callback,
state = aVerifySelfSessionState( state = anOutgoingVerificationState(
step = VerifySelfSessionState.Step.Completed, step = OutgoingVerificationState.Step.Completed,
), ),
) )
rule.pressBackKey() rule.pressBackKey()
@ -115,11 +115,11 @@ class VerifySelfSessionViewTest {
@Test @Test
fun `when flow is completed and the user clicks on the continue button, the expected callback is invoked`() { fun `when flow is completed and the user clicks on the continue button, the expected callback is invoked`() {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setVerifySelfSessionView( rule.setOutgoingVerificationView(
aVerifySelfSessionState( anOutgoingVerificationState(
step = VerifySelfSessionState.Step.Completed, step = OutgoingVerificationState.Step.Completed,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onFinished = callback, onFinished = callback,
@ -130,10 +130,10 @@ class VerifySelfSessionViewTest {
@Test @Test
fun `clicking on they match emits the expected event`() { fun `clicking on they match emits the expected event`() {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>() val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
rule.setVerifySelfSessionView( rule.setOutgoingVerificationView(
aVerifySelfSessionState( anOutgoingVerificationState(
step = VerifySelfSessionState.Step.Verifying( step = OutgoingVerificationState.Step.Verifying(
data = aEmojisSessionVerificationData(), data = aEmojisSessionVerificationData(),
state = AsyncData.Uninitialized, state = AsyncData.Uninitialized,
), ),
@ -141,15 +141,15 @@ class VerifySelfSessionViewTest {
), ),
) )
rule.clickOn(R.string.screen_session_verification_they_match) rule.clickOn(R.string.screen_session_verification_they_match)
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.ConfirmVerification) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.ConfirmVerification)
} }
@Test @Test
fun `clicking on they do not match emits the expected event`() { fun `clicking on they do not match emits the expected event`() {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>() val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
rule.setVerifySelfSessionView( rule.setOutgoingVerificationView(
aVerifySelfSessionState( anOutgoingVerificationState(
step = VerifySelfSessionState.Step.Verifying( step = OutgoingVerificationState.Step.Verifying(
data = aEmojisSessionVerificationData(), data = aEmojisSessionVerificationData(),
state = AsyncData.Uninitialized, state = AsyncData.Uninitialized,
), ),
@ -157,17 +157,17 @@ class VerifySelfSessionViewTest {
), ),
) )
rule.clickOn(R.string.screen_session_verification_they_dont_match) rule.clickOn(R.string.screen_session_verification_they_dont_match)
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.DeclineVerification) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.DeclineVerification)
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setVerifySelfSessionView( private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setOutgoingVerificationView(
state: VerifySelfSessionState, state: OutgoingVerificationState,
onLearnMoreClick: () -> Unit = EnsureNeverCalled(), onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
onFinished: () -> Unit = EnsureNeverCalled(), onFinished: () -> Unit = EnsureNeverCalled(),
onBack: () -> Unit = EnsureNeverCalled(), onBack: () -> Unit = EnsureNeverCalled(),
) { ) {
setContent { setContent {
VerifySelfSessionView( OutgoingVerificationView(
state = state, state = state,
onLearnMoreClick = onLearnMoreClick, onLearnMoreClick = onLearnMoreClick,
onFinish = onFinished, onFinish = onFinished,

View file

@ -11,10 +11,10 @@ firebaseAppDistribution = "5.1.1"
# AndroidX # AndroidX
core = "1.16.0" core = "1.16.0"
datastore = "1.1.4" datastore = "1.1.6"
constraintlayout = "2.2.1" constraintlayout = "2.2.1"
constraintlayout_compose = "1.1.1" constraintlayout_compose = "1.1.1"
lifecycle = "2.8.7" lifecycle = "2.9.0"
activity = "1.10.1" activity = "1.10.1"
media3 = "1.6.1" media3 = "1.6.1"
camera = "1.4.2" camera = "1.4.2"
@ -176,7 +176,7 @@ sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", ver
sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" }
sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4" sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4"
sqlite = "androidx.sqlite:sqlite-ktx:2.5.0" sqlite = "androidx.sqlite:sqlite-ktx:2.5.1"
unifiedpush = "com.github.UnifiedPush:android-connector:2.4.0" unifiedpush = "com.github.UnifiedPush:android-connector:2.4.0"
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.11.2" otaliastudios_transcoder = "com.otaliastudios:transcoder:0.11.2"
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0" vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
@ -207,7 +207,7 @@ anvil_compiler_api = { module = "dev.zacsweers.anvil:compiler-api", version.ref
anvil_compiler_utils = { module = "dev.zacsweers.anvil:compiler-utils", version.ref = "anvil" } anvil_compiler_utils = { module = "dev.zacsweers.anvil:compiler-utils", version.ref = "anvil" }
# Element Call # Element Call
element_call_embedded = "io.element.android:element-call-embedded:0.10.0" element_call_embedded = "io.element.android:element-call-embedded:0.9.0"
# Auto services # Auto services
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" } google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }

Binary file not shown.

View file

@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78 distributionSha256Sum=61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

4
gradlew vendored
View file

@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \ -classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.

4
gradlew.bat vendored
View file

@ -70,11 +70,11 @@ goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

View file

@ -16,8 +16,6 @@ import androidx.core.text.util.LinkifyCompat
import timber.log.Timber import timber.log.Timber
import kotlin.collections.component1 import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
import kotlin.collections.isNotEmpty
import kotlin.collections.iterator
/** /**
* Helper class to linkify text while preserving existing URL spans. * Helper class to linkify text while preserving existing URL spans.

View file

@ -1,34 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.designsystem.components.keyboard
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.isImeVisible
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.rememberUpdatedState
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
/**
* Inspired from https://stackoverflow.com/questions/68847559/how-can-i-detect-keyboard-opening-and-closing-in-jetpack-compose
*/
enum class Keyboard {
Opened,
Closed
}
// Note: it does not work as expected...
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun keyboardAsState(): State<Keyboard> {
val lifecycle = LocalLifecycleOwner.current.lifecycle
val isResumed = lifecycle.currentState == Lifecycle.State.RESUMED
return rememberUpdatedState(if (WindowInsets.isImeVisible && isResumed) Keyboard.Opened else Keyboard.Closed)
}

View file

@ -9,16 +9,22 @@ package io.element.android.libraries.push.impl.notifications
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.notification.CallNotifyType
import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
import io.element.android.services.appnavstate.api.AppForegroundStateService
import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.strings.StringProvider
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
/** /**
* Helper to resolve a valid [NotifiableEvent] from a [NotificationData]. * Helper to resolve a valid [NotifiableEvent] from a [NotificationData].
@ -31,7 +37,7 @@ interface CallNotificationEventResolver {
* @param forceNotify `true` to force the notification to be non-ringing, `false` to use the default behaviour. Default is `false`. * @param forceNotify `true` to force the notification to be non-ringing, `false` to use the default behaviour. Default is `false`.
* @return a [NotifiableEvent] if the notification data is a call notification, null otherwise * @return a [NotifiableEvent] if the notification data is a call notification, null otherwise
*/ */
fun resolveEvent( suspend fun resolveEvent(
sessionId: SessionId, sessionId: SessionId,
notificationData: NotificationData, notificationData: NotificationData,
forceNotify: Boolean = false, forceNotify: Boolean = false,
@ -41,8 +47,10 @@ interface CallNotificationEventResolver {
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultCallNotificationEventResolver @Inject constructor( class DefaultCallNotificationEventResolver @Inject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val appForegroundStateService: AppForegroundStateService,
private val clientProvider: MatrixClientProvider,
) : CallNotificationEventResolver { ) : CallNotificationEventResolver {
override fun resolveEvent( override suspend fun resolveEvent(
sessionId: SessionId, sessionId: SessionId,
notificationData: NotificationData, notificationData: NotificationData,
forceNotify: Boolean forceNotify: Boolean
@ -50,8 +58,32 @@ class DefaultCallNotificationEventResolver @Inject constructor(
val content = notificationData.content as? NotificationContent.MessageLike.CallNotify val content = notificationData.content as? NotificationContent.MessageLike.CallNotify
?: throw ResolvingException("content is not a call notify") ?: throw ResolvingException("content is not a call notify")
val previousRingingCallStatus = appForegroundStateService.hasRingingCall.value
// We need the sync service working to get the updated room info
val isRoomCallActive = runCatching {
if (content.type == CallNotifyType.RING) {
appForegroundStateService.updateHasRingingCall(true)
val client = clientProvider.getOrRestore(sessionId).getOrNull() ?: throw ResolvingException("Session $sessionId not found")
val room = client.getRoom(notificationData.roomId) ?: throw ResolvingException("Room ${notificationData.roomId} not found")
// Give a few seconds for the room info flow to catch up with the sync, if needed - this is usually instant
val isActive = withTimeoutOrNull(3.seconds) { room.roomInfoFlow.firstOrNull { it.hasRoomCall } }?.hasRoomCall ?: false
// We no longer need the sync service to be active because of a call notification.
appForegroundStateService.updateHasRingingCall(previousRingingCallStatus)
isActive
} else {
// If the call notification is not of ringing type, we don't need to check if the call is active
false
}
}.onFailure {
// Make sure to reset the hasRingingCall state in case of failure
appForegroundStateService.updateHasRingingCall(previousRingingCallStatus)
}.getOrDefault(false)
notificationData.run { notificationData.run {
if (NotifiableRingingCallEvent.shouldRing(content.type, timestamp) && !forceNotify) { if (content.type == CallNotifyType.RING && isRoomCallActive && !forceNotify) {
NotifiableRingingCallEvent( NotifiableRingingCallEvent(
sessionId = sessionId, sessionId = sessionId,
roomId = roomId, roomId = roomId,
@ -70,9 +102,7 @@ class DefaultCallNotificationEventResolver @Inject constructor(
senderAvatarUrl = senderAvatarUrl, senderAvatarUrl = senderAvatarUrl,
) )
} else { } else {
val now = System.currentTimeMillis() Timber.d("Event $eventId is call notify but should not ring: $isRoomCallActive, notify: ${content.type}")
val elapsed = now - timestamp
Timber.d("Event $eventId is call notify but should not ring: $timestamp vs $now ($elapsed ms elapsed), notify: ${content.type}")
// Create a simple message notification event // Create a simple message notification event
buildNotifiableMessageEvent( buildNotifiableMessageEvent(
sessionId = sessionId, sessionId = sessionId,

View file

@ -12,8 +12,6 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallNotifyType import io.element.android.libraries.matrix.api.notification.CallNotifyType
import java.time.Instant
import kotlin.time.Duration.Companion.seconds
data class NotifiableRingingCallEvent( data class NotifiableRingingCallEvent(
override val sessionId: SessionId, override val sessionId: SessionId,
@ -31,13 +29,4 @@ data class NotifiableRingingCallEvent(
val roomAvatarUrl: String? = null, val roomAvatarUrl: String? = null,
val callNotifyType: CallNotifyType, val callNotifyType: CallNotifyType,
val timestamp: Long, val timestamp: Long,
) : NotifiableEvent { ) : NotifiableEvent
companion object {
fun shouldRing(callNotifyType: CallNotifyType, timestamp: Long): Boolean {
val timeout = 10.seconds.inWholeMilliseconds
val elapsed = Instant.now().toEpochMilli() - timestamp
// Only ring if the type is RING and the elapsed time is less than the timeout
return callNotifyType == CallNotifyType.RING && elapsed < timeout
}
}
}

View file

@ -0,0 +1,171 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.impl.notifications
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.notification.CallNotifyType
import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.A_USER_NAME_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.notification.aNotificationData
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultCallNotificationEventResolverTest {
@Test
fun `resolve CallNotify - RING when call is still ongoing`() = runTest {
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
// The call is still ongoing
initialRoomInfo = aRoomInfo(hasRoomCall = true),
)
)
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
val resolver = createDefaultNotifiableEventResolver(
clientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }),
)
val expectedResult = NotifiableRingingCallEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
senderId = A_USER_ID_2,
roomName = A_ROOM_NAME,
editedEventId = null,
description = "📹 Incoming call",
timestamp = 567L,
canBeReplaced = true,
isRedacted = false,
isUpdated = false,
senderDisambiguatedDisplayName = A_USER_NAME_2,
senderAvatarUrl = null,
callNotifyType = CallNotifyType.RING,
)
val notificationData = aNotificationData(
content = NotificationContent.MessageLike.CallNotify(A_USER_ID_2, CallNotifyType.RING)
)
val result = resolver.resolveEvent(A_SESSION_ID, notificationData)
assertThat(result.getOrNull()).isEqualTo(expectedResult)
}
@Test
fun `resolve CallNotify - NOTIFY`() = runTest {
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
// The call already ended
initialRoomInfo = aRoomInfo(hasRoomCall = true),
)
)
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
val resolver = createDefaultNotifiableEventResolver(
clientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }),
)
val expectedResult = NotifiableMessageEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
senderId = A_USER_ID_2,
roomName = A_ROOM_NAME,
editedEventId = null,
body = "📹 Incoming call",
timestamp = 567L,
canBeReplaced = false,
isRedacted = false,
isUpdated = false,
senderDisambiguatedDisplayName = A_USER_NAME_2,
noisy = true,
imageUriString = null,
imageMimeType = null,
threadId = null,
type = "m.call.notify",
)
val notificationData = aNotificationData(
content = NotificationContent.MessageLike.CallNotify(A_USER_ID_2, CallNotifyType.NOTIFY)
)
val result = resolver.resolveEvent(A_SESSION_ID, notificationData)
assertThat(result.getOrNull()).isEqualTo(expectedResult)
}
@Test
fun `resolve CallNotify - RING but timed out displays the same as NOTIFY`() = runTest {
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
// The call already ended
initialRoomInfo = aRoomInfo(hasRoomCall = false),
)
)
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
val resolver = createDefaultNotifiableEventResolver(
clientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }),
)
val expectedResult = NotifiableMessageEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
senderId = A_USER_ID_2,
roomName = A_ROOM_NAME,
editedEventId = null,
body = "📹 Incoming call",
timestamp = 567L,
canBeReplaced = false,
isRedacted = false,
isUpdated = false,
senderDisambiguatedDisplayName = A_USER_NAME_2,
noisy = true,
imageUriString = null,
imageMimeType = null,
threadId = null,
type = "m.call.notify",
)
val notificationData = aNotificationData(
content = NotificationContent.MessageLike.CallNotify(A_USER_ID_2, CallNotifyType.RING)
)
val result = resolver.resolveEvent(A_SESSION_ID, notificationData)
assertThat(result.getOrNull()).isEqualTo(expectedResult)
}
private fun createDefaultNotifiableEventResolver(
stringProvider: FakeStringProvider = FakeStringProvider(defaultResult = "\uD83D\uDCF9 Incoming call"),
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
clientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
) = DefaultCallNotificationEventResolver(
stringProvider = stringProvider,
appForegroundStateService = appForegroundStateService,
clientProvider = clientProvider,
)
}

View file

@ -49,10 +49,9 @@ import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiable
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
import io.element.android.services.toolbox.impl.systemclock.DefaultSystemClock
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -606,80 +605,8 @@ class DefaultNotifiableEventResolverTest {
} }
@Test @Test
fun `resolve CallNotify - ringing`() = runTest { fun `resolve CallNotify - goes through CallNotificationEventResolver`() = runTest {
val timestamp = DefaultSystemClock().epochMillis() val callNotificationEventResolver = FakeCallNotificationEventResolver()
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
aNotificationData(
content = NotificationContent.MessageLike.CallNotify(
A_USER_ID_2,
CallNotifyType.RING
),
timestamp = timestamp,
)
)
)
val expectedResult = ResolvedPushEvent.Event(
NotifiableRingingCallEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
senderId = A_USER_ID_2,
roomName = A_ROOM_NAME,
editedEventId = null,
description = "📹 Incoming call",
timestamp = timestamp,
canBeReplaced = true,
isRedacted = false,
isUpdated = false,
senderDisambiguatedDisplayName = A_USER_NAME_2,
senderAvatarUrl = null,
callNotifyType = CallNotifyType.RING,
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
assertThat(result.getOrNull()).isEqualTo(expectedResult)
}
@Test
fun `resolve CallNotify - ring but timed out displays the same as notify`() = runTest {
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
aNotificationData(
content = NotificationContent.MessageLike.CallNotify(
A_USER_ID_2,
CallNotifyType.RING
),
timestamp = 0L,
)
)
)
val expectedResult = ResolvedPushEvent.Event(
NotifiableMessageEvent(
sessionId = A_SESSION_ID,
eventId = AN_EVENT_ID,
editedEventId = null,
noisy = true,
timestamp = 0L,
senderDisambiguatedDisplayName = A_USER_NAME_2,
senderId = A_USER_ID_2,
body = "📹 Incoming call",
roomId = A_ROOM_ID,
threadId = null,
roomName = A_ROOM_NAME,
canBeReplaced = false,
isRedacted = false,
imageUriString = null,
imageMimeType = null,
type = EventType.CALL_NOTIFY,
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
assertThat(result.getOrNull()).isEqualTo(expectedResult)
}
@Test
fun `resolve CallNotify - notify`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( aNotificationData(
@ -688,7 +615,8 @@ class DefaultNotifiableEventResolverTest {
CallNotifyType.NOTIFY CallNotifyType.NOTIFY
), ),
) )
) ),
callNotificationEventResolver = callNotificationEventResolver,
) )
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
NotifiableMessageEvent( NotifiableMessageEvent(
@ -710,6 +638,7 @@ class DefaultNotifiableEventResolverTest {
type = EventType.CALL_NOTIFY, type = EventType.CALL_NOTIFY,
) )
) )
callNotificationEventResolver.resolveEventLambda = { _, _, _ -> Result.success(expectedResult.notifiableEvent) }
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getOrNull()).isEqualTo(expectedResult)
} }
@ -804,6 +733,7 @@ class DefaultNotifiableEventResolverTest {
notificationService: FakeNotificationService? = FakeNotificationService(), notificationService: FakeNotificationService? = FakeNotificationService(),
notificationResult: Result<NotificationData?> = Result.success(null), notificationResult: Result<NotificationData?> = Result.success(null),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
callNotificationEventResolver: FakeCallNotificationEventResolver = FakeCallNotificationEventResolver(),
): DefaultNotifiableEventResolver { ): DefaultNotifiableEventResolver {
val context = RuntimeEnvironment.getApplication() as Context val context = RuntimeEnvironment.getApplication() as Context
notificationService?.givenGetNotificationResult(notificationResult) notificationService?.givenGetNotificationResult(notificationResult)
@ -824,9 +754,7 @@ class DefaultNotifiableEventResolverTest {
notificationMediaRepoFactory = notificationMediaRepoFactory, notificationMediaRepoFactory = notificationMediaRepoFactory,
context = context, context = context,
permalinkParser = FakePermalinkParser(), permalinkParser = FakePermalinkParser(),
callNotificationEventResolver = DefaultCallNotificationEventResolver( callNotificationEventResolver = callNotificationEventResolver,
stringProvider = AndroidStringProvider(context.resources)
),
appPreferencesStore = appPreferencesStore, appPreferencesStore = appPreferencesStore,
) )
} }

View file

@ -18,7 +18,7 @@ class FakeCallNotificationEventResolver(
lambdaError() lambdaError()
}, },
) : CallNotificationEventResolver { ) : CallNotificationEventResolver {
override fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean): Result<NotifiableEvent> { override suspend fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean): Result<NotifiableEvent> {
return resolveEventLambda(sessionId, notificationData, forceNotify) return resolveEventLambda(sessionId, notificationData, forceNotify)
} }
} }

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="a11y_avatar">"Аватар"</string>
<string name="a11y_delete">"Выдаліць"</string> <string name="a11y_delete">"Выдаліць"</string>
<plurals name="a11y_digits_entered"> <plurals name="a11y_digits_entered">
<item quantity="one">"Уведзеная лічба %1$d"</item> <item quantity="one">"Уведзеная лічба %1$d"</item>
@ -7,6 +8,7 @@
<item quantity="many">"Уведзена %1$d лічб"</item> <item quantity="many">"Уведзена %1$d лічб"</item>
</plurals> </plurals>
<string name="a11y_hide_password">"Схаваць пароль"</string> <string name="a11y_hide_password">"Схаваць пароль"</string>
<string name="a11y_join_call">"Далучыцца да выкліку"</string>
<string name="a11y_jump_to_bottom">"Перайсці ўніз"</string> <string name="a11y_jump_to_bottom">"Перайсці ўніз"</string>
<string name="a11y_notifications_mentions_only">"Толькі згадкі"</string> <string name="a11y_notifications_mentions_only">"Толькі згадкі"</string>
<string name="a11y_notifications_muted">"Гук адключаны"</string> <string name="a11y_notifications_muted">"Гук адключаны"</string>
@ -31,6 +33,7 @@
<string name="a11y_show_password">"Паказаць пароль"</string> <string name="a11y_show_password">"Паказаць пароль"</string>
<string name="a11y_start_call">"Пазваніць"</string> <string name="a11y_start_call">"Пазваніць"</string>
<string name="a11y_user_menu">"Меню карыстальніка"</string> <string name="a11y_user_menu">"Меню карыстальніка"</string>
<string name="a11y_view_details">"Паглядзець падрабязнасці"</string>
<string name="a11y_voice_message_record">"Запісаць галасавое паведамленне."</string> <string name="a11y_voice_message_record">"Запісаць галасавое паведамленне."</string>
<string name="a11y_voice_message_stop_recording">"Спыніць запіс"</string> <string name="a11y_voice_message_stop_recording">"Спыніць запіс"</string>
<string name="action_accept">"Прыняць"</string> <string name="action_accept">"Прыняць"</string>
@ -57,6 +60,7 @@
<string name="action_delete_poll">"Выдаліць апытанне"</string> <string name="action_delete_poll">"Выдаліць апытанне"</string>
<string name="action_disable">"Адключыць"</string> <string name="action_disable">"Адключыць"</string>
<string name="action_discard">"Адмяніць"</string> <string name="action_discard">"Адмяніць"</string>
<string name="action_dismiss">"Aдхіліць"</string>
<string name="action_done">"Гатова"</string> <string name="action_done">"Гатова"</string>
<string name="action_edit">"Рэдагаваць"</string> <string name="action_edit">"Рэдагаваць"</string>
<string name="action_edit_poll">"Рэдагаваць апытанне"</string> <string name="action_edit_poll">"Рэдагаваць апытанне"</string>

View file

@ -111,6 +111,7 @@
<string name="action_report">"Отчет"</string> <string name="action_report">"Отчет"</string>
<string name="action_report_bug">"Сообщить об ошибке"</string> <string name="action_report_bug">"Сообщить об ошибке"</string>
<string name="action_report_content">"Пожаловаться на содержание"</string> <string name="action_report_content">"Пожаловаться на содержание"</string>
<string name="action_report_dm">"Пожаловаться на беседу"</string>
<string name="action_report_room">"Комната отчетов"</string> <string name="action_report_room">"Комната отчетов"</string>
<string name="action_reset">"Сбросить"</string> <string name="action_reset">"Сбросить"</string>
<string name="action_reset_identity">"Сбросить идентификацию"</string> <string name="action_reset_identity">"Сбросить идентификацию"</string>
@ -264,6 +265,7 @@
<string name="common_shared_location">"Поделился местоположением"</string> <string name="common_shared_location">"Поделился местоположением"</string>
<string name="common_signing_out">"Выход…"</string> <string name="common_signing_out">"Выход…"</string>
<string name="common_something_went_wrong">"Что-то пошло не так"</string> <string name="common_something_went_wrong">"Что-то пошло не так"</string>
<string name="common_something_went_wrong_message">"Мы столкнулись с проблемой. Пожалуйста, попробуйте еще раз."</string>
<string name="common_starting_chat">"Чат запускается…"</string> <string name="common_starting_chat">"Чат запускается…"</string>
<string name="common_sticker">"Стикер"</string> <string name="common_sticker">"Стикер"</string>
<string name="common_success">"Успешно"</string> <string name="common_success">"Успешно"</string>

View file

@ -351,6 +351,11 @@ Are you sure you want to continue?"</string>
<string name="screen_bottom_sheet_manage_room_member_remove">"Remove from room"</string> <string name="screen_bottom_sheet_manage_room_member_remove">"Remove from room"</string>
<string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Remove member and ban from joining in the future?"</string> <string name="screen_bottom_sheet_manage_room_member_remove_confirmation_title">"Remove member and ban from joining in the future?"</string>
<string name="screen_bottom_sheet_manage_room_member_removing_user">"Removing %1$s…"</string> <string name="screen_bottom_sheet_manage_room_member_removing_user">"Removing %1$s…"</string>
<string name="screen_bottom_sheet_manage_room_member_unban">"Unban from room"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_action">"Unban"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_description">"They would be able to join the room again if invited"</string>
<string name="screen_bottom_sheet_manage_room_member_unban_member_confirmation_title">"Are you sure you want to unban this member?"</string>
<string name="screen_bottom_sheet_manage_room_member_unbanning_user">"Unbanning $1%s"</string>
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string> <string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
<string name="screen_media_upload_preview_caption_warning">"Captions might not be visible to people using older apps."</string> <string name="screen_media_upload_preview_caption_warning">"Captions might not be visible to people using older apps."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string> <string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>

View file

@ -32,7 +32,7 @@ private const val versionYear = 25
private const val versionMonth = 5 private const val versionMonth = 5
// Note: must be in [0,99] // Note: must be in [0,99]
private const val versionReleaseNumber = 1 private const val versionReleaseNumber = 2
object Versions { object Versions {
const val VERSION_CODE = (2000 + versionYear) * 10_000 + versionMonth * 100 + versionReleaseNumber const val VERSION_CODE = (2000 + versionYear) * 10_000 + versionMonth * 100 + versionReleaseNumber

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ac3a523da98d3cf64cd908b83824d75e060b14cb581b4015a75e943524547048
size 16399

File diff suppressed because it is too large Load diff

View file

@ -27,8 +27,6 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.component1 import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
import kotlin.collections.iterator
import kotlin.collections.orEmpty
@ContributesMultibinding(AppScope::class) @ContributesMultibinding(AppScope::class)
class SentryAnalyticsProvider @Inject constructor( class SentryAnalyticsProvider @Inject constructor(