Merge branch 'release/25.05.2'
This commit is contained in:
commit
30a49216f7
94 changed files with 1497 additions and 1171 deletions
15
CHANGES.md
15
CHANGES.md
|
|
@ -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
|
||||
=============================
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ package io.element.android.appconfig
|
|||
|
||||
object LearnMoreConfig {
|
||||
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 IDENTITY_CHANGE_URL: String = "https://element.io/help#encryption18"
|
||||
}
|
||||
|
|
|
|||
2
fastlane/metadata/android/en-US/changelogs/202505020.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/202505020.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Main changes in this version: TODO.
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import extension.setupAnvil
|
||||
import org.gradle.kotlin.dsl.test
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import io.element.android.anvilannotations.ContributesNode
|
|||
import io.element.android.appconfig.LearnMoreConfig
|
||||
import io.element.android.features.ftue.impl.sessionverification.choosemode.ChooseSelfVerificationModeNode
|
||||
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.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -40,7 +40,7 @@ import kotlinx.parcelize.Parcelize
|
|||
class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val verifySessionEntryPoint: VerifySessionEntryPoint,
|
||||
private val outgoingVerificationEntryPoint: OutgoingVerificationEntryPoint,
|
||||
private val secureBackupEntryPoint: SecureBackupEntryPoint,
|
||||
) : BaseFlowNode<FtueSessionVerificationFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
|
|
@ -94,19 +94,19 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override fun onLearnMoreAboutEncryption() {
|
||||
learnMoreUrl.value = LearnMoreConfig.ENCRYPTION_URL
|
||||
learnMoreUrl.value = LearnMoreConfig.DEVICE_VERIFICATION_URL
|
||||
}
|
||||
}
|
||||
|
||||
createNode<ChooseSelfVerificationModeNode>(buildContext, plugins = listOf(callback))
|
||||
}
|
||||
is NavTarget.UseAnotherDevice -> {
|
||||
verifySessionEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(VerifySessionEntryPoint.Params(
|
||||
outgoingVerificationEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(OutgoingVerificationEntryPoint.Params(
|
||||
showDeviceVerifiedScreen = true,
|
||||
verificationRequest = VerificationRequest.Outgoing.CurrentSession,
|
||||
))
|
||||
.callback(object : VerifySessionEntryPoint.Callback {
|
||||
.callback(object : OutgoingVerificationEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
plugins<Callback>().forEach { it.onDone() }
|
||||
}
|
||||
|
|
@ -116,7 +116,8 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
|||
}
|
||||
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ data class LoginPasswordState(
|
|||
@Parcelize
|
||||
data class LoginFormState(
|
||||
val login: String,
|
||||
val password: String
|
||||
val password: String,
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
val Default = LoginFormState("", "")
|
||||
|
|
|
|||
|
|
@ -8,23 +8,38 @@
|
|||
package io.element.android.features.login.impl.screens.loginpassword
|
||||
|
||||
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.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
open class LoginPasswordStateProvider : PreviewParameterProvider<LoginPasswordState> {
|
||||
override val values: Sequence<LoginPasswordState>
|
||||
get() = sequenceOf(
|
||||
aLoginPasswordState(),
|
||||
// Loading
|
||||
aLoginPasswordState().copy(loginAction = AsyncData.Loading()),
|
||||
aLoginPasswordState(loginAction = AsyncData.Loading()),
|
||||
// Error
|
||||
aLoginPasswordState().copy(loginAction = AsyncData.Failure(Exception("An error occurred"))),
|
||||
aLoginPasswordState(loginAction = AsyncData.Failure(Exception("An error occurred"))),
|
||||
)
|
||||
}
|
||||
|
||||
fun aLoginPasswordState() = LoginPasswordState(
|
||||
accountProvider = anAccountProvider(),
|
||||
formState = LoginFormState.Default,
|
||||
loginAction = AsyncData.Uninitialized,
|
||||
eventSink = {}
|
||||
fun aLoginPasswordState(
|
||||
accountProvider: AccountProvider = anAccountProvider(),
|
||||
formState: LoginFormState = LoginFormState.Default,
|
||||
loginAction: AsyncData<SessionId> = AsyncData.Uninitialized,
|
||||
eventSink: (LoginPasswordEvents) -> Unit = {},
|
||||
) = LoginPasswordState(
|
||||
accountProvider = accountProvider,
|
||||
formState = formState,
|
||||
loginAction = loginAction,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
fun aLoginFormState(
|
||||
login: String = "",
|
||||
password: String = "",
|
||||
) = LoginFormState(
|
||||
login = login,
|
||||
password = password,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -201,6 +201,7 @@ private fun LoginForm(
|
|||
{
|
||||
Box(Modifier.clickable {
|
||||
loginFieldState = ""
|
||||
eventSink(LoginPasswordEvents.SetLogin(""))
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Close(),
|
||||
|
|
|
|||
|
|
@ -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_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_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>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
%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_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_notice">"Geben Sie eine Domainadresse ein."</string>
|
||||
<string name="screen_change_server_subtitle">"Wie lautet die Adresse deines Servers?"</string>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<string name="screen_change_account_provider_subtitle">"Χρησιμοποίησε διαφορετικό πάροχο λογαριασμού, όπως τον δικό σου ιδιωτικό διακομιστή ή έναν εργασιακό λογαριασμό."</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_well_known">"Το Sliding sync δεν είναι διαθέσιμο εξαιτίας ενός ζητήματος σε ένα πολύ γνωστό αρχείο:
|
||||
<string name="screen_change_server_error_invalid_well_known">"Ο διακομιστής δεν είναι διαθέσιμος λόγω προβλήματος στο αρχείο .well-known:
|
||||
%1$s"</string>
|
||||
<string name="screen_change_server_form_header">"URL οικιακού διακομιστή"</string>
|
||||
<string name="screen_change_server_subtitle">"Ποια είναι η διεύθυνση του διακομιστή σου;"</string>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
<string name="screen_change_server_error_invalid_well_known">"Server pole saadaval vea tõttu well-known failis:
|
||||
%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_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_notice">"Sisesta domeeni aadress."</string>
|
||||
<string name="screen_change_server_subtitle">"Mis on sinu koduserveri aadress?"</string>
|
||||
|
|
|
|||
|
|
@ -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_title">"Changer de fournisseur de compte"</string>
|
||||
<string name="screen_change_server_error_invalid_homeserver">"Nous n’avons pas pu atteindre ce serveur d’accueil. Vérifiez que vous avez correctement saisi l’URL du serveur d’accueil. Si l’URL est correcte, contactez l’administrateur de votre serveur d’accueil pour obtenir de l’aide."</string>
|
||||
<string name="screen_change_server_error_invalid_well_known">"Sliding sync n’est pas disponible en raison d’un problème dans le well-known file :
|
||||
<string name="screen_change_server_error_invalid_well_known">"Ce fournisseur de compte n’est pas disponible en raison d’un problème dans le fichier .well-known:
|
||||
%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 d’accueil"</string>
|
||||
|
|
|
|||
|
|
@ -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_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_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>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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_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_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>
|
||||
<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_notice">"Wprowadź adres domeny."</string>
|
||||
<string name="screen_change_server_subtitle">"Jaki jest adres Twojego serwera?"</string>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
<string name="screen_change_server_error_invalid_well_known">"Sliding sync недоступен из-за проблемы в известном файле:
|
||||
%1$s"</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_title">"Выберите свой сервер"</string>
|
||||
<string name="screen_create_account_title">"Создать учетную запись"</string>
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
%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_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_notice">"Zadajte adresu domény."</string>
|
||||
<string name="screen_change_server_subtitle">"Aká je adresa vášho servera?"</string>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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_up">"Utwórz konto"</string>
|
||||
<string name="screen_onboarding_welcome_message">"Witamy w %1$s. Szybszy i prostszy niż kiedykolwiek."</string>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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_up">"Create account"</string>
|
||||
<string name="screen_onboarding_welcome_message">"Welcome to the fastest %1$s ever. Supercharged for speed and simplicity."</string>
|
||||
|
|
|
|||
|
|
@ -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.test
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
|
|
|||
|
|
@ -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.securityandprivacy.SecurityAndPrivacyFlowNode
|
||||
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.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -71,7 +71,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
|
||||
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
|
||||
private val mediaGalleryEntryPoint: MediaGalleryEntryPoint,
|
||||
private val verifySessionEntryPoint: VerifySessionEntryPoint,
|
||||
private val outgoingVerificationEntryPoint: OutgoingVerificationEntryPoint,
|
||||
private val reportRoomEntryPoint: ReportRoomEntryPoint,
|
||||
) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
|
|
@ -328,13 +328,13 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
createNode<SecurityAndPrivacyFlowNode>(buildContext)
|
||||
}
|
||||
is NavTarget.VerifyUser -> {
|
||||
val params = VerifySessionEntryPoint.Params(
|
||||
val params = OutgoingVerificationEntryPoint.Params(
|
||||
showDeviceVerifiedScreen = true,
|
||||
verificationRequest = VerificationRequest.Outgoing.User(userId = navTarget.userId,)
|
||||
)
|
||||
verifySessionEntryPoint.nodeBuilder(this, buildContext)
|
||||
outgoingVerificationEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(params)
|
||||
.callback(object : VerifySessionEntryPoint.Callback {
|
||||
.callback(object : OutgoingVerificationEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
backstack.pop()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.impl.root.UserProfileNode
|
||||
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.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -46,7 +46,7 @@ class UserProfileFlowNode @AssistedInject constructor(
|
|||
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||
private val sessionIdHolder: CurrentSessionIdHolder,
|
||||
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
|
||||
private val verifySessionEntryPoint: VerifySessionEntryPoint,
|
||||
private val outgoingVerificationEntryPoint: OutgoingVerificationEntryPoint,
|
||||
) : BaseFlowNode<UserProfileFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
|
|
@ -110,11 +110,11 @@ class UserProfileFlowNode @AssistedInject constructor(
|
|||
.build()
|
||||
}
|
||||
is NavTarget.VerifyUser -> {
|
||||
val params = VerifySessionEntryPoint.Params(
|
||||
val params = OutgoingVerificationEntryPoint.Params(
|
||||
showDeviceVerifiedScreen = false,
|
||||
verificationRequest = VerificationRequest.Outgoing.User(userId = navTarget.userId)
|
||||
)
|
||||
verifySessionEntryPoint.nodeBuilder(this, buildContext)
|
||||
outgoingVerificationEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(params)
|
||||
.build()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
|
|||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
|
||||
interface VerifySessionEntryPoint : FeatureEntryPoint {
|
||||
interface OutgoingVerificationEntryPoint : FeatureEntryPoint {
|
||||
data class Params(
|
||||
val showDeviceVerifiedScreen: Boolean,
|
||||
val verificationRequest: VerificationRequest.Outgoing,
|
||||
|
|
@ -11,29 +11,29 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
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.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultVerifySessionEntryPoint @Inject constructor() : VerifySessionEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): VerifySessionEntryPoint.NodeBuilder {
|
||||
class DefaultOutgoingVerificationEntryPoint @Inject constructor() : OutgoingVerificationEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): OutgoingVerificationEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : VerifySessionEntryPoint.NodeBuilder {
|
||||
override fun callback(callback: VerifySessionEntryPoint.Callback): VerifySessionEntryPoint.NodeBuilder {
|
||||
return object : OutgoingVerificationEntryPoint.NodeBuilder {
|
||||
override fun callback(callback: OutgoingVerificationEntryPoint.Callback): OutgoingVerificationEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun params(params: VerifySessionEntryPoint.Params): VerifySessionEntryPoint.NodeBuilder {
|
||||
override fun params(params: OutgoingVerificationEntryPoint.Params): OutgoingVerificationEntryPoint.NodeBuilder {
|
||||
plugins += params
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<VerifySelfSessionNode>(buildContext, plugins)
|
||||
return parentNode.createNode<OutgoingVerificationNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,19 +16,19 @@ import com.bumble.appyx.core.plugin.plugins
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
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.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class VerifySelfSessionNode @AssistedInject constructor(
|
||||
class OutgoingVerificationNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: VerifySelfSessionPresenter.Factory,
|
||||
presenterFactory: OutgoingVerificationPresenter.Factory,
|
||||
) : 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(
|
||||
showDeviceVerifiedScreen = inputs.showDeviceVerifiedScreen,
|
||||
|
|
@ -38,7 +38,7 @@ class VerifySelfSessionNode @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
VerifySelfSessionView(
|
||||
OutgoingVerificationView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onLearnMoreClick = callback::onLearnMoreAboutEncryption,
|
||||
|
|
@ -31,30 +31,30 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import timber.log.Timber
|
||||
import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionStateMachine.Event as StateMachineEvent
|
||||
import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionStateMachine.State as StateMachineState
|
||||
import io.element.android.features.verifysession.impl.outgoing.OutgoingVerificationStateMachine.Event as StateMachineEvent
|
||||
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 verificationRequest: VerificationRequest.Outgoing,
|
||||
private val sessionVerificationService: SessionVerificationService,
|
||||
private val encryptionService: EncryptionService,
|
||||
) : Presenter<VerifySelfSessionState> {
|
||||
) : Presenter<OutgoingVerificationState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
verificationRequest: VerificationRequest.Outgoing,
|
||||
showDeviceVerifiedScreen: Boolean,
|
||||
): VerifySelfSessionPresenter
|
||||
): OutgoingVerificationPresenter
|
||||
}
|
||||
|
||||
private val stateMachine = VerifySelfSessionStateMachine(
|
||||
private val stateMachine = OutgoingVerificationStateMachine(
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
encryptionService = encryptionService,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun present(): VerifySelfSessionState {
|
||||
override fun present(): OutgoingVerificationState {
|
||||
val stateAndDispatch = stateMachine.rememberStateAndDispatch()
|
||||
|
||||
val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState()
|
||||
|
|
@ -63,17 +63,17 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
|
|||
when (verificationRequest) {
|
||||
is VerificationRequest.Outgoing.CurrentSession -> {
|
||||
when (sessionVerifiedStatus) {
|
||||
SessionVerifiedStatus.Unknown -> VerifySelfSessionState.Step.Loading
|
||||
SessionVerifiedStatus.Unknown -> OutgoingVerificationState.Step.Loading
|
||||
SessionVerifiedStatus.NotVerified -> {
|
||||
stateAndDispatch.state.value.toVerificationStep()
|
||||
}
|
||||
SessionVerifiedStatus.Verified -> {
|
||||
if (stateAndDispatch.state.value != StateMachineState.Initial || showDeviceVerifiedScreen) {
|
||||
// The user has verified the session, we need to show the success screen
|
||||
VerifySelfSessionState.Step.Completed
|
||||
OutgoingVerificationState.Step.Completed
|
||||
} else {
|
||||
// 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()
|
||||
}
|
||||
|
||||
fun handleEvents(event: VerifySelfSessionViewEvents) {
|
||||
fun handleEvents(event: OutgoingVerificationViewEvents) {
|
||||
Timber.d("Verification user action: ${event::class.simpleName}")
|
||||
when (event) {
|
||||
// Just relay the event to the state machine
|
||||
VerifySelfSessionViewEvents.RequestVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.RequestVerification(verificationRequest))
|
||||
VerifySelfSessionViewEvents.StartSasVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.StartSasVerification)
|
||||
VerifySelfSessionViewEvents.ConfirmVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.AcceptChallenge)
|
||||
VerifySelfSessionViewEvents.DeclineVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge)
|
||||
VerifySelfSessionViewEvents.Cancel -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel)
|
||||
VerifySelfSessionViewEvents.Reset -> stateAndDispatch.dispatchAction(StateMachineEvent.Reset)
|
||||
OutgoingVerificationViewEvents.RequestVerification -> StateMachineEvent.RequestVerification(verificationRequest)
|
||||
OutgoingVerificationViewEvents.StartSasVerification -> StateMachineEvent.StartSasVerification
|
||||
OutgoingVerificationViewEvents.ConfirmVerification -> StateMachineEvent.AcceptChallenge
|
||||
OutgoingVerificationViewEvents.DeclineVerification -> StateMachineEvent.DeclineChallenge
|
||||
OutgoingVerificationViewEvents.Cancel -> StateMachineEvent.Cancel
|
||||
OutgoingVerificationViewEvents.Reset -> StateMachineEvent.Reset
|
||||
}.let { stateMachineEvent ->
|
||||
stateAndDispatch.dispatchAction(stateMachineEvent)
|
||||
}
|
||||
}
|
||||
return VerifySelfSessionState(
|
||||
return OutgoingVerificationState(
|
||||
step = step,
|
||||
request = verificationRequest,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
||||
private fun StateMachineState?.toVerificationStep(): VerifySelfSessionState.Step =
|
||||
private fun StateMachineState?.toVerificationStep(): OutgoingVerificationState.Step =
|
||||
when (val machineState = this) {
|
||||
StateMachineState.Initial, null -> {
|
||||
VerifySelfSessionState.Step.Initial
|
||||
OutgoingVerificationState.Step.Initial
|
||||
}
|
||||
is StateMachineState.RequestingVerification,
|
||||
is StateMachineState.StartingSasVerification,
|
||||
StateMachineState.SasVerificationStarted -> {
|
||||
VerifySelfSessionState.Step.AwaitingOtherDeviceResponse
|
||||
OutgoingVerificationState.Step.AwaitingOtherDeviceResponse
|
||||
}
|
||||
|
||||
StateMachineState.VerificationRequestAccepted -> {
|
||||
VerifySelfSessionState.Step.Ready
|
||||
OutgoingVerificationState.Step.Ready
|
||||
}
|
||||
|
||||
is StateMachineState.Canceled -> {
|
||||
VerifySelfSessionState.Step.Canceled
|
||||
OutgoingVerificationState.Step.Canceled
|
||||
}
|
||||
|
||||
is StateMachineState.Verifying -> {
|
||||
|
|
@ -134,15 +136,15 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
|
|||
is StateMachineState.Verifying.Replying -> AsyncData.Loading()
|
||||
else -> AsyncData.Uninitialized
|
||||
}
|
||||
VerifySelfSessionState.Step.Verifying(machineState.data, async)
|
||||
OutgoingVerificationState.Step.Verifying(machineState.data, async)
|
||||
}
|
||||
|
||||
StateMachineState.Completed -> {
|
||||
VerifySelfSessionState.Step.Completed
|
||||
OutgoingVerificationState.Step.Completed
|
||||
}
|
||||
|
||||
StateMachineState.Exit -> {
|
||||
VerifySelfSessionState.Step.Exit
|
||||
OutgoingVerificationState.Step.Exit
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -14,10 +14,10 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationD
|
|||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
|
||||
@Immutable
|
||||
data class VerifySelfSessionState(
|
||||
data class OutgoingVerificationState(
|
||||
val step: Step,
|
||||
val request: VerificationRequest.Outgoing,
|
||||
val eventSink: (VerifySelfSessionViewEvents) -> Unit,
|
||||
val eventSink: (OutgoingVerificationViewEvents) -> Unit,
|
||||
) {
|
||||
@Stable
|
||||
sealed interface Step {
|
||||
|
|
@ -29,10 +29,10 @@ import kotlin.time.Duration.Companion.seconds
|
|||
import com.freeletics.flowredux.dsl.State as MachineState
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
class VerifySelfSessionStateMachine(
|
||||
class OutgoingVerificationStateMachine(
|
||||
private val sessionVerificationService: SessionVerificationService,
|
||||
private val encryptionService: EncryptionService,
|
||||
) : FlowReduxStateMachine<VerifySelfSessionStateMachine.State, VerifySelfSessionStateMachine.Event>(
|
||||
) : FlowReduxStateMachine<OutgoingVerificationStateMachine.State, OutgoingVerificationStateMachine.Event>(
|
||||
initialState = State.Initial,
|
||||
) {
|
||||
init {
|
||||
|
|
@ -8,64 +8,64 @@
|
|||
package io.element.android.features.verifysession.impl.outgoing
|
||||
|
||||
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.aEmojisSessionVerificationData
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
|
||||
open class VerifySelfSessionStateProvider : PreviewParameterProvider<VerifySelfSessionState> {
|
||||
override val values: Sequence<VerifySelfSessionState>
|
||||
open class OutgoingVerificationStateProvider : PreviewParameterProvider<OutgoingVerificationState> {
|
||||
override val values: Sequence<OutgoingVerificationState>
|
||||
get() = sequenceOf(
|
||||
aVerifySelfSessionState(
|
||||
anOutgoingVerificationState(
|
||||
step = Step.Initial,
|
||||
request = anOutgoingSessionVerificationRequest(),
|
||||
),
|
||||
aVerifySelfSessionState(
|
||||
anOutgoingVerificationState(
|
||||
step = Step.Initial,
|
||||
request = anOutgoingUserVerificationRequest(),
|
||||
),
|
||||
aVerifySelfSessionState(
|
||||
anOutgoingVerificationState(
|
||||
step = Step.AwaitingOtherDeviceResponse,
|
||||
request = anOutgoingSessionVerificationRequest(),
|
||||
),
|
||||
aVerifySelfSessionState(
|
||||
anOutgoingVerificationState(
|
||||
step = Step.AwaitingOtherDeviceResponse,
|
||||
request = anOutgoingUserVerificationRequest(),
|
||||
),
|
||||
aVerifySelfSessionState(
|
||||
anOutgoingVerificationState(
|
||||
step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized),
|
||||
request = anOutgoingSessionVerificationRequest(),
|
||||
),
|
||||
aVerifySelfSessionState(
|
||||
anOutgoingVerificationState(
|
||||
step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized),
|
||||
request = anOutgoingUserVerificationRequest(),
|
||||
),
|
||||
aVerifySelfSessionState(
|
||||
anOutgoingVerificationState(
|
||||
step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Loading())
|
||||
),
|
||||
aVerifySelfSessionState(
|
||||
anOutgoingVerificationState(
|
||||
step = Step.Canceled
|
||||
),
|
||||
aVerifySelfSessionState(
|
||||
anOutgoingVerificationState(
|
||||
step = Step.Ready
|
||||
),
|
||||
aVerifySelfSessionState(
|
||||
anOutgoingVerificationState(
|
||||
step = Step.Verifying(aDecimalsSessionVerificationData(), AsyncData.Uninitialized)
|
||||
),
|
||||
aVerifySelfSessionState(
|
||||
anOutgoingVerificationState(
|
||||
step = Step.Completed,
|
||||
request = anOutgoingSessionVerificationRequest(),
|
||||
),
|
||||
aVerifySelfSessionState(
|
||||
anOutgoingVerificationState(
|
||||
step = Step.Completed,
|
||||
request = anOutgoingUserVerificationRequest(),
|
||||
),
|
||||
aVerifySelfSessionState(
|
||||
anOutgoingVerificationState(
|
||||
step = Step.Loading
|
||||
),
|
||||
aVerifySelfSessionState(
|
||||
anOutgoingVerificationState(
|
||||
step = Step.Exit
|
||||
),
|
||||
// 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 anOutgoingSessionVerificationRequest() = VerificationRequest.Outgoing.CurrentSession
|
||||
|
||||
internal fun aVerifySelfSessionState(
|
||||
internal fun anOutgoingVerificationState(
|
||||
step: Step = Step.Initial,
|
||||
request: VerificationRequest.Outgoing = anOutgoingSessionVerificationRequest(),
|
||||
eventSink: (VerifySelfSessionViewEvents) -> Unit = {},
|
||||
) = VerifySelfSessionState(
|
||||
eventSink: (OutgoingVerificationViewEvents) -> Unit = {},
|
||||
) = OutgoingVerificationState(
|
||||
step = step,
|
||||
request = request,
|
||||
eventSink = eventSink,
|
||||
|
|
@ -27,7 +27,7 @@ import androidx.compose.ui.unit.dp
|
|||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
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.VerificationContentVerifying
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
|
@ -49,8 +49,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun VerifySelfSessionView(
|
||||
state: VerifySelfSessionState,
|
||||
fun OutgoingVerificationView(
|
||||
state: OutgoingVerificationState,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onFinish: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
|
|
@ -59,12 +59,12 @@ fun VerifySelfSessionView(
|
|||
val step = state.step
|
||||
fun cancelOrResetFlow() {
|
||||
when (step) {
|
||||
is Step.Canceled -> state.eventSink(VerifySelfSessionViewEvents.Reset)
|
||||
is Step.Canceled -> state.eventSink(OutgoingVerificationViewEvents.Reset)
|
||||
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 -> {
|
||||
if (!step.state.isLoading()) {
|
||||
state.eventSink(VerifySelfSessionViewEvents.DeclineVerification)
|
||||
state.eventSink(OutgoingVerificationViewEvents.DeclineVerification)
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
|
|
@ -98,10 +98,10 @@ fun VerifySelfSessionView(
|
|||
)
|
||||
},
|
||||
header = {
|
||||
VerifySelfSessionHeader(step = step, request = state.request)
|
||||
OutgoingVerificationHeader(step = step, request = state.request)
|
||||
},
|
||||
footer = {
|
||||
VerifySelfSessionBottomMenu(
|
||||
OutgoingVerificationViewBottomMenu(
|
||||
screenState = state,
|
||||
onCancelClick = ::cancelOrResetFlow,
|
||||
onContinueClick = onFinish,
|
||||
|
|
@ -109,7 +109,7 @@ fun VerifySelfSessionView(
|
|||
},
|
||||
isScrollable = true,
|
||||
) {
|
||||
VerifySelfSessionContent(
|
||||
OutgoingVerificationContent(
|
||||
flowState = step,
|
||||
request = state.request,
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
|
|
@ -119,7 +119,7 @@ fun VerifySelfSessionView(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun VerifySelfSessionHeader(step: Step, request: VerificationRequest.Outgoing) {
|
||||
private fun OutgoingVerificationHeader(step: Step, request: VerificationRequest.Outgoing) {
|
||||
val iconStyle = when (step) {
|
||||
Step.Loading -> error("Should not happen")
|
||||
Step.Initial -> when (request) {
|
||||
|
|
@ -189,7 +189,7 @@ private fun VerifySelfSessionHeader(step: Step, request: VerificationRequest.Out
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun VerifySelfSessionContent(
|
||||
private fun OutgoingVerificationContent(
|
||||
flowState: Step,
|
||||
request: VerificationRequest.Outgoing,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
|
|
@ -227,8 +227,8 @@ private fun ContentInitial(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun VerifySelfSessionBottomMenu(
|
||||
screenState: VerifySelfSessionState,
|
||||
private fun OutgoingVerificationViewBottomMenu(
|
||||
screenState: OutgoingVerificationState,
|
||||
onCancelClick: () -> Unit,
|
||||
onContinueClick: () -> Unit,
|
||||
) {
|
||||
|
|
@ -244,7 +244,7 @@ private fun VerifySelfSessionBottomMenu(
|
|||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(CommonStrings.action_start_verification),
|
||||
onClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) },
|
||||
onClick = { eventSink(OutgoingVerificationViewEvents.RequestVerification) },
|
||||
)
|
||||
InvisibleButton()
|
||||
}
|
||||
|
|
@ -264,7 +264,7 @@ private fun VerifySelfSessionBottomMenu(
|
|||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(CommonStrings.action_start),
|
||||
onClick = { eventSink(VerifySelfSessionViewEvents.StartSasVerification) },
|
||||
onClick = { eventSink(OutgoingVerificationViewEvents.StartSasVerification) },
|
||||
)
|
||||
TextButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
|
@ -287,14 +287,14 @@ private fun VerifySelfSessionBottomMenu(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.screen_session_verification_they_match),
|
||||
onClick = {
|
||||
eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
|
||||
eventSink(OutgoingVerificationViewEvents.ConfirmVerification)
|
||||
},
|
||||
)
|
||||
|
||||
TextButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
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
|
||||
@Composable
|
||||
internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionStateProvider::class) state: VerifySelfSessionState) = ElementPreview {
|
||||
VerifySelfSessionView(
|
||||
internal fun OutgoingVerificationViewPreview(@PreviewParameter(OutgoingVerificationStateProvider::class) state: OutgoingVerificationState) = ElementPreview {
|
||||
OutgoingVerificationView(
|
||||
state = state,
|
||||
onLearnMoreClick = {},
|
||||
onFinish = {},
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ package io.element.android.features.verifysession.impl.outgoing
|
|||
|
||||
import app.cash.turbine.ReceiveTurbine
|
||||
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.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||
|
|
@ -31,13 +31,13 @@ import org.junit.Rule
|
|||
import org.junit.Test
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class VerifySelfSessionPresenterTest {
|
||||
class OutgoingVerificationPresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - Initial state is received`() = runTest {
|
||||
val presenter = createVerifySelfSessionPresenter(
|
||||
val presenter = createOutgoingVerificationPresenter(
|
||||
service = unverifiedSessionService(),
|
||||
)
|
||||
presenter.test {
|
||||
|
|
@ -55,7 +55,7 @@ class VerifySelfSessionPresenterTest {
|
|||
requestSessionVerificationLambda = requestSessionVerificationRecorder,
|
||||
startVerificationLambda = startVerificationRecorder,
|
||||
)
|
||||
val presenter = createVerifySelfSessionPresenter(
|
||||
val presenter = createOutgoingVerificationPresenter(
|
||||
service = service,
|
||||
verificationRequest = anOutgoingSessionVerificationRequest(),
|
||||
)
|
||||
|
|
@ -75,7 +75,7 @@ class VerifySelfSessionPresenterTest {
|
|||
requestUserVerificationLambda = requestUserVerificationRecorder,
|
||||
startVerificationLambda = startVerificationRecorder,
|
||||
)
|
||||
val presenter = createVerifySelfSessionPresenter(
|
||||
val presenter = createOutgoingVerificationPresenter(
|
||||
service = service,
|
||||
verificationRequest = anOutgoingUserVerificationRequest(),
|
||||
)
|
||||
|
|
@ -89,14 +89,14 @@ class VerifySelfSessionPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - Cancellation on initial state moves to Exit state`() = runTest {
|
||||
val presenter = createVerifySelfSessionPresenter(
|
||||
val presenter = createOutgoingVerificationPresenter(
|
||||
service = unverifiedSessionService(),
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.step).isEqualTo(Step.Initial)
|
||||
val eventSink = initialState.eventSink
|
||||
eventSink(VerifySelfSessionViewEvents.Cancel)
|
||||
eventSink(OutgoingVerificationViewEvents.Cancel)
|
||||
|
||||
assertThat(awaitItem().step).isEqualTo(Step.Exit)
|
||||
}
|
||||
|
|
@ -109,10 +109,10 @@ class VerifySelfSessionPresenterTest {
|
|||
startVerificationLambda = { },
|
||||
approveVerificationLambda = { },
|
||||
)
|
||||
val presenter = createVerifySelfSessionPresenter(service)
|
||||
val presenter = createOutgoingVerificationPresenter(service)
|
||||
presenter.test {
|
||||
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||
state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
|
||||
state.eventSink(OutgoingVerificationViewEvents.ConfirmVerification)
|
||||
// Cancelling
|
||||
assertThat(awaitItem().step).isInstanceOf(Step.Verifying::class.java)
|
||||
service.emitVerificationFlowState(VerificationFlowState.DidFail)
|
||||
|
|
@ -126,9 +126,9 @@ class VerifySelfSessionPresenterTest {
|
|||
val service = unverifiedSessionService(
|
||||
requestSessionVerificationLambda = { },
|
||||
)
|
||||
val presenter = createVerifySelfSessionPresenter(service)
|
||||
val presenter = createOutgoingVerificationPresenter(service)
|
||||
presenter.test {
|
||||
awaitItem().eventSink(VerifySelfSessionViewEvents.RequestVerification)
|
||||
awaitItem().eventSink(OutgoingVerificationViewEvents.RequestVerification)
|
||||
service.emitVerificationFlowState(VerificationFlowState.DidFail)
|
||||
assertThat(awaitItem().step).isInstanceOf(Step.AwaitingOtherDeviceResponse::class.java)
|
||||
assertThat(awaitItem().step).isEqualTo(Step.Canceled)
|
||||
|
|
@ -142,10 +142,10 @@ class VerifySelfSessionPresenterTest {
|
|||
startVerificationLambda = { },
|
||||
cancelVerificationLambda = { },
|
||||
)
|
||||
val presenter = createVerifySelfSessionPresenter(service)
|
||||
val presenter = createOutgoingVerificationPresenter(service)
|
||||
presenter.test {
|
||||
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||
state.eventSink(VerifySelfSessionViewEvents.Cancel)
|
||||
state.eventSink(OutgoingVerificationViewEvents.Cancel)
|
||||
assertThat(awaitItem().step).isEqualTo(Step.Canceled)
|
||||
}
|
||||
}
|
||||
|
|
@ -156,7 +156,7 @@ class VerifySelfSessionPresenterTest {
|
|||
requestSessionVerificationLambda = { },
|
||||
startVerificationLambda = { },
|
||||
)
|
||||
val presenter = createVerifySelfSessionPresenter(service)
|
||||
val presenter = createOutgoingVerificationPresenter(service)
|
||||
presenter.test {
|
||||
requestVerificationAndAwaitVerifyingState(service)
|
||||
service.emitVerificationFlowState(VerificationFlowState.DidReceiveVerificationData(SessionVerificationData.Emojis(emptyList())))
|
||||
|
|
@ -170,12 +170,12 @@ class VerifySelfSessionPresenterTest {
|
|||
requestSessionVerificationLambda = { },
|
||||
startVerificationLambda = { },
|
||||
)
|
||||
val presenter = createVerifySelfSessionPresenter(service)
|
||||
val presenter = createOutgoingVerificationPresenter(service)
|
||||
presenter.test {
|
||||
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||
service.emitVerificationFlowState(VerificationFlowState.DidCancel)
|
||||
assertThat(awaitItem().step).isEqualTo(Step.Canceled)
|
||||
state.eventSink(VerifySelfSessionViewEvents.Reset)
|
||||
state.eventSink(OutgoingVerificationViewEvents.Reset)
|
||||
// Went back to initial state
|
||||
assertThat(awaitItem().step).isEqualTo(Step.Initial)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
|
|
@ -192,13 +192,13 @@ class VerifySelfSessionPresenterTest {
|
|||
startVerificationLambda = { },
|
||||
approveVerificationLambda = { },
|
||||
)
|
||||
val presenter = createVerifySelfSessionPresenter(service)
|
||||
val presenter = createOutgoingVerificationPresenter(service)
|
||||
presenter.test {
|
||||
val state = requestVerificationAndAwaitVerifyingState(
|
||||
service,
|
||||
SessionVerificationData.Emojis(emojis)
|
||||
)
|
||||
state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
|
||||
state.eventSink(OutgoingVerificationViewEvents.ConfirmVerification)
|
||||
assertThat(awaitItem().step).isEqualTo(
|
||||
Step.Verifying(
|
||||
SessionVerificationData.Emojis(emojis),
|
||||
|
|
@ -217,10 +217,10 @@ class VerifySelfSessionPresenterTest {
|
|||
startVerificationLambda = { },
|
||||
declineVerificationLambda = { },
|
||||
)
|
||||
val presenter = createVerifySelfSessionPresenter(service)
|
||||
val presenter = createOutgoingVerificationPresenter(service)
|
||||
presenter.test {
|
||||
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||
state.eventSink(VerifySelfSessionViewEvents.DeclineVerification)
|
||||
state.eventSink(OutgoingVerificationViewEvents.DeclineVerification)
|
||||
assertThat(awaitItem().step).isEqualTo(
|
||||
Step.Verifying(
|
||||
SessionVerificationData.Emojis(emptyList()),
|
||||
|
|
@ -241,7 +241,7 @@ class VerifySelfSessionPresenterTest {
|
|||
emitVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||
emitVerificationFlowState(VerificationFlowState.DidFinish)
|
||||
}
|
||||
val presenter = createVerifySelfSessionPresenter(
|
||||
val presenter = createOutgoingVerificationPresenter(
|
||||
service = service,
|
||||
showDeviceVerifiedScreen = true,
|
||||
)
|
||||
|
|
@ -259,7 +259,7 @@ class VerifySelfSessionPresenterTest {
|
|||
emitVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||
emitVerificationFlowState(VerificationFlowState.DidFinish)
|
||||
}
|
||||
val presenter = createVerifySelfSessionPresenter(
|
||||
val presenter = createOutgoingVerificationPresenter(
|
||||
service = service,
|
||||
showDeviceVerifiedScreen = false,
|
||||
)
|
||||
|
|
@ -269,13 +269,13 @@ class VerifySelfSessionPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun ReceiveTurbine<VerifySelfSessionState>.requestVerificationAndAwaitVerifyingState(
|
||||
private suspend fun ReceiveTurbine<OutgoingVerificationState>.requestVerificationAndAwaitVerifyingState(
|
||||
fakeService: FakeSessionVerificationService,
|
||||
sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()),
|
||||
): VerifySelfSessionState {
|
||||
): OutgoingVerificationState {
|
||||
var state = awaitItem()
|
||||
assertThat(state.step).isEqualTo(Step.Initial)
|
||||
state.eventSink(VerifySelfSessionViewEvents.RequestVerification)
|
||||
state.eventSink(OutgoingVerificationViewEvents.RequestVerification)
|
||||
// Await for other device response:
|
||||
fakeService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest)
|
||||
state = awaitItem()
|
||||
|
|
@ -283,7 +283,7 @@ class VerifySelfSessionPresenterTest {
|
|||
// Await for the state to be Ready
|
||||
state = awaitItem()
|
||||
assertThat(state.step).isEqualTo(Step.Ready)
|
||||
state.eventSink(VerifySelfSessionViewEvents.StartSasVerification)
|
||||
state.eventSink(OutgoingVerificationViewEvents.StartSasVerification)
|
||||
// Await for other device response (again):
|
||||
fakeService.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification)
|
||||
state = awaitItem()
|
||||
|
|
@ -321,13 +321,13 @@ class VerifySelfSessionPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun createVerifySelfSessionPresenter(
|
||||
private fun createOutgoingVerificationPresenter(
|
||||
service: SessionVerificationService,
|
||||
verificationRequest: VerificationRequest.Outgoing = anOutgoingSessionVerificationRequest(),
|
||||
encryptionService: EncryptionService = FakeEncryptionService(),
|
||||
showDeviceVerifiedScreen: Boolean = false,
|
||||
): VerifySelfSessionPresenter {
|
||||
return VerifySelfSessionPresenter(
|
||||
): OutgoingVerificationPresenter {
|
||||
return OutgoingVerificationPresenter(
|
||||
showDeviceVerifiedScreen = showDeviceVerifiedScreen,
|
||||
verificationRequest = verificationRequest,
|
||||
sessionVerificationService = service,
|
||||
|
|
@ -26,54 +26,54 @@ import org.junit.rules.TestRule
|
|||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class VerifySelfSessionViewTest {
|
||||
class OutgoingVerificationViewTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `back key pressed - when canceled resets the flow`() {
|
||||
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
||||
rule.setVerifySelfSessionView(
|
||||
aVerifySelfSessionState(
|
||||
step = VerifySelfSessionState.Step.Canceled,
|
||||
val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
|
||||
rule.setOutgoingVerificationView(
|
||||
anOutgoingVerificationState(
|
||||
step = OutgoingVerificationState.Step.Canceled,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.pressBackKey()
|
||||
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.Reset)
|
||||
eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Reset)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `back key pressed - when awaiting response cancels the verification`() {
|
||||
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
||||
rule.setVerifySelfSessionView(
|
||||
aVerifySelfSessionState(
|
||||
step = VerifySelfSessionState.Step.AwaitingOtherDeviceResponse,
|
||||
val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
|
||||
rule.setOutgoingVerificationView(
|
||||
anOutgoingVerificationState(
|
||||
step = OutgoingVerificationState.Step.AwaitingOtherDeviceResponse,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.pressBackKey()
|
||||
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.Cancel)
|
||||
eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `back key pressed - when ready to verify cancels the verification`() {
|
||||
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
||||
rule.setVerifySelfSessionView(
|
||||
aVerifySelfSessionState(
|
||||
step = VerifySelfSessionState.Step.Ready,
|
||||
val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
|
||||
rule.setOutgoingVerificationView(
|
||||
anOutgoingVerificationState(
|
||||
step = OutgoingVerificationState.Step.Ready,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.pressBackKey()
|
||||
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.Cancel)
|
||||
eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `back key pressed - when verifying and not loading declines the verification`() {
|
||||
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
||||
rule.setVerifySelfSessionView(
|
||||
aVerifySelfSessionState(
|
||||
step = VerifySelfSessionState.Step.Verifying(
|
||||
val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
|
||||
rule.setOutgoingVerificationView(
|
||||
anOutgoingVerificationState(
|
||||
step = OutgoingVerificationState.Step.Verifying(
|
||||
data = aEmojisSessionVerificationData(),
|
||||
state = AsyncData.Uninitialized,
|
||||
),
|
||||
|
|
@ -81,15 +81,15 @@ class VerifySelfSessionViewTest {
|
|||
),
|
||||
)
|
||||
rule.pressBackKey()
|
||||
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.DeclineVerification)
|
||||
eventsRecorder.assertSingle(OutgoingVerificationViewEvents.DeclineVerification)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `back key pressed - when verifying and loading does nothing`() {
|
||||
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
||||
rule.setVerifySelfSessionView(
|
||||
aVerifySelfSessionState(
|
||||
step = VerifySelfSessionState.Step.Verifying(
|
||||
val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
|
||||
rule.setOutgoingVerificationView(
|
||||
anOutgoingVerificationState(
|
||||
step = OutgoingVerificationState.Step.Verifying(
|
||||
data = aEmojisSessionVerificationData(),
|
||||
state = AsyncData.Loading(),
|
||||
),
|
||||
|
|
@ -103,10 +103,10 @@ class VerifySelfSessionViewTest {
|
|||
@Test
|
||||
fun `back key pressed - on Completed exits the flow`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setVerifySelfSessionView(
|
||||
rule.setOutgoingVerificationView(
|
||||
onBack = callback,
|
||||
state = aVerifySelfSessionState(
|
||||
step = VerifySelfSessionState.Step.Completed,
|
||||
state = anOutgoingVerificationState(
|
||||
step = OutgoingVerificationState.Step.Completed,
|
||||
),
|
||||
)
|
||||
rule.pressBackKey()
|
||||
|
|
@ -115,11 +115,11 @@ class VerifySelfSessionViewTest {
|
|||
|
||||
@Test
|
||||
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 ->
|
||||
rule.setVerifySelfSessionView(
|
||||
aVerifySelfSessionState(
|
||||
step = VerifySelfSessionState.Step.Completed,
|
||||
rule.setOutgoingVerificationView(
|
||||
anOutgoingVerificationState(
|
||||
step = OutgoingVerificationState.Step.Completed,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onFinished = callback,
|
||||
|
|
@ -130,10 +130,10 @@ class VerifySelfSessionViewTest {
|
|||
|
||||
@Test
|
||||
fun `clicking on they match emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
||||
rule.setVerifySelfSessionView(
|
||||
aVerifySelfSessionState(
|
||||
step = VerifySelfSessionState.Step.Verifying(
|
||||
val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
|
||||
rule.setOutgoingVerificationView(
|
||||
anOutgoingVerificationState(
|
||||
step = OutgoingVerificationState.Step.Verifying(
|
||||
data = aEmojisSessionVerificationData(),
|
||||
state = AsyncData.Uninitialized,
|
||||
),
|
||||
|
|
@ -141,15 +141,15 @@ class VerifySelfSessionViewTest {
|
|||
),
|
||||
)
|
||||
rule.clickOn(R.string.screen_session_verification_they_match)
|
||||
eventsRecorder.assertSingle(VerifySelfSessionViewEvents.ConfirmVerification)
|
||||
eventsRecorder.assertSingle(OutgoingVerificationViewEvents.ConfirmVerification)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on they do not match emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
||||
rule.setVerifySelfSessionView(
|
||||
aVerifySelfSessionState(
|
||||
step = VerifySelfSessionState.Step.Verifying(
|
||||
val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
|
||||
rule.setOutgoingVerificationView(
|
||||
anOutgoingVerificationState(
|
||||
step = OutgoingVerificationState.Step.Verifying(
|
||||
data = aEmojisSessionVerificationData(),
|
||||
state = AsyncData.Uninitialized,
|
||||
),
|
||||
|
|
@ -157,17 +157,17 @@ class VerifySelfSessionViewTest {
|
|||
),
|
||||
)
|
||||
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(
|
||||
state: VerifySelfSessionState,
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setOutgoingVerificationView(
|
||||
state: OutgoingVerificationState,
|
||||
onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
|
||||
onFinished: () -> Unit = EnsureNeverCalled(),
|
||||
onBack: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
VerifySelfSessionView(
|
||||
OutgoingVerificationView(
|
||||
state = state,
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onFinish = onFinished,
|
||||
|
|
@ -11,10 +11,10 @@ firebaseAppDistribution = "5.1.1"
|
|||
|
||||
# AndroidX
|
||||
core = "1.16.0"
|
||||
datastore = "1.1.4"
|
||||
datastore = "1.1.6"
|
||||
constraintlayout = "2.2.1"
|
||||
constraintlayout_compose = "1.1.1"
|
||||
lifecycle = "2.8.7"
|
||||
lifecycle = "2.9.0"
|
||||
activity = "1.10.1"
|
||||
media3 = "1.6.1"
|
||||
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-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
||||
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"
|
||||
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.11.2"
|
||||
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" }
|
||||
|
||||
# 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
|
||||
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }
|
||||
|
|
|
|||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,7 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
distributionSha256Sum=61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
|||
4
gradlew
vendored
4
gradlew
vendored
|
|
@ -114,7 +114,7 @@ case "$( uname )" in #(
|
|||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
|
|
@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
|
|
|||
4
gradlew.bat
vendored
4
gradlew.bat
vendored
|
|
@ -70,11 +70,11 @@ goto fail
|
|||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@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
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ import androidx.core.text.util.LinkifyCompat
|
|||
import timber.log.Timber
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.isNotEmpty
|
||||
import kotlin.collections.iterator
|
||||
|
||||
/**
|
||||
* Helper class to linkify text while preserving existing URL spans.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -9,16 +9,22 @@ package io.element.android.libraries.push.impl.notifications
|
|||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
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.notification.CallNotifyType
|
||||
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.timeline.item.event.EventType
|
||||
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.NotifiableRingingCallEvent
|
||||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
* @return a [NotifiableEvent] if the notification data is a call notification, null otherwise
|
||||
*/
|
||||
fun resolveEvent(
|
||||
suspend fun resolveEvent(
|
||||
sessionId: SessionId,
|
||||
notificationData: NotificationData,
|
||||
forceNotify: Boolean = false,
|
||||
|
|
@ -41,8 +47,10 @@ interface CallNotificationEventResolver {
|
|||
@ContributesBinding(AppScope::class)
|
||||
class DefaultCallNotificationEventResolver @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val appForegroundStateService: AppForegroundStateService,
|
||||
private val clientProvider: MatrixClientProvider,
|
||||
) : CallNotificationEventResolver {
|
||||
override fun resolveEvent(
|
||||
override suspend fun resolveEvent(
|
||||
sessionId: SessionId,
|
||||
notificationData: NotificationData,
|
||||
forceNotify: Boolean
|
||||
|
|
@ -50,8 +58,32 @@ class DefaultCallNotificationEventResolver @Inject constructor(
|
|||
val content = notificationData.content as? NotificationContent.MessageLike.CallNotify
|
||||
?: 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 {
|
||||
if (NotifiableRingingCallEvent.shouldRing(content.type, timestamp) && !forceNotify) {
|
||||
if (content.type == CallNotifyType.RING && isRoomCallActive && !forceNotify) {
|
||||
NotifiableRingingCallEvent(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
|
|
@ -70,9 +102,7 @@ class DefaultCallNotificationEventResolver @Inject constructor(
|
|||
senderAvatarUrl = senderAvatarUrl,
|
||||
)
|
||||
} else {
|
||||
val now = System.currentTimeMillis()
|
||||
val elapsed = now - timestamp
|
||||
Timber.d("Event $eventId is call notify but should not ring: $timestamp vs $now ($elapsed ms elapsed), notify: ${content.type}")
|
||||
Timber.d("Event $eventId is call notify but should not ring: $isRoomCallActive, notify: ${content.type}")
|
||||
// Create a simple message notification event
|
||||
buildNotifiableMessageEvent(
|
||||
sessionId = sessionId,
|
||||
|
|
|
|||
|
|
@ -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.UserId
|
||||
import io.element.android.libraries.matrix.api.notification.CallNotifyType
|
||||
import java.time.Instant
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
data class NotifiableRingingCallEvent(
|
||||
override val sessionId: SessionId,
|
||||
|
|
@ -31,13 +29,4 @@ data class NotifiableRingingCallEvent(
|
|||
val roomAvatarUrl: String? = null,
|
||||
val callNotifyType: CallNotifyType,
|
||||
val timestamp: Long,
|
||||
) : 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
|
||||
}
|
||||
}
|
||||
}
|
||||
) : NotifiableEvent
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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.InviteNotifiableEvent
|
||||
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.test.notifications.FakeCallNotificationEventResolver
|
||||
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.FakeSystemClock
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -606,80 +605,8 @@ class DefaultNotifiableEventResolverTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `resolve CallNotify - ringing`() = runTest {
|
||||
val timestamp = DefaultSystemClock().epochMillis()
|
||||
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 {
|
||||
fun `resolve CallNotify - goes through CallNotificationEventResolver`() = runTest {
|
||||
val callNotificationEventResolver = FakeCallNotificationEventResolver()
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
|
|
@ -688,7 +615,8 @@ class DefaultNotifiableEventResolverTest {
|
|||
CallNotifyType.NOTIFY
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
callNotificationEventResolver = callNotificationEventResolver,
|
||||
)
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
NotifiableMessageEvent(
|
||||
|
|
@ -710,6 +638,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
type = EventType.CALL_NOTIFY,
|
||||
)
|
||||
)
|
||||
callNotificationEventResolver.resolveEventLambda = { _, _, _ -> Result.success(expectedResult.notifiableEvent) }
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
|
@ -804,6 +733,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
notificationService: FakeNotificationService? = FakeNotificationService(),
|
||||
notificationResult: Result<NotificationData?> = Result.success(null),
|
||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||
callNotificationEventResolver: FakeCallNotificationEventResolver = FakeCallNotificationEventResolver(),
|
||||
): DefaultNotifiableEventResolver {
|
||||
val context = RuntimeEnvironment.getApplication() as Context
|
||||
notificationService?.givenGetNotificationResult(notificationResult)
|
||||
|
|
@ -824,9 +754,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
notificationMediaRepoFactory = notificationMediaRepoFactory,
|
||||
context = context,
|
||||
permalinkParser = FakePermalinkParser(),
|
||||
callNotificationEventResolver = DefaultCallNotificationEventResolver(
|
||||
stringProvider = AndroidStringProvider(context.resources)
|
||||
),
|
||||
callNotificationEventResolver = callNotificationEventResolver,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class FakeCallNotificationEventResolver(
|
|||
lambdaError()
|
||||
},
|
||||
) : 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a11y_avatar">"Аватар"</string>
|
||||
<string name="a11y_delete">"Выдаліць"</string>
|
||||
<plurals name="a11y_digits_entered">
|
||||
<item quantity="one">"Уведзеная лічба %1$d"</item>
|
||||
|
|
@ -7,6 +8,7 @@
|
|||
<item quantity="many">"Уведзена %1$d лічб"</item>
|
||||
</plurals>
|
||||
<string name="a11y_hide_password">"Схаваць пароль"</string>
|
||||
<string name="a11y_join_call">"Далучыцца да выкліку"</string>
|
||||
<string name="a11y_jump_to_bottom">"Перайсці ўніз"</string>
|
||||
<string name="a11y_notifications_mentions_only">"Толькі згадкі"</string>
|
||||
<string name="a11y_notifications_muted">"Гук адключаны"</string>
|
||||
|
|
@ -31,6 +33,7 @@
|
|||
<string name="a11y_show_password">"Паказаць пароль"</string>
|
||||
<string name="a11y_start_call">"Пазваніць"</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_stop_recording">"Спыніць запіс"</string>
|
||||
<string name="action_accept">"Прыняць"</string>
|
||||
|
|
@ -57,6 +60,7 @@
|
|||
<string name="action_delete_poll">"Выдаліць апытанне"</string>
|
||||
<string name="action_disable">"Адключыць"</string>
|
||||
<string name="action_discard">"Адмяніць"</string>
|
||||
<string name="action_dismiss">"Aдхіліць"</string>
|
||||
<string name="action_done">"Гатова"</string>
|
||||
<string name="action_edit">"Рэдагаваць"</string>
|
||||
<string name="action_edit_poll">"Рэдагаваць апытанне"</string>
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@
|
|||
<string name="action_report">"Отчет"</string>
|
||||
<string name="action_report_bug">"Сообщить об ошибке"</string>
|
||||
<string name="action_report_content">"Пожаловаться на содержание"</string>
|
||||
<string name="action_report_dm">"Пожаловаться на беседу"</string>
|
||||
<string name="action_report_room">"Комната отчетов"</string>
|
||||
<string name="action_reset">"Сбросить"</string>
|
||||
<string name="action_reset_identity">"Сбросить идентификацию"</string>
|
||||
|
|
@ -264,6 +265,7 @@
|
|||
<string name="common_shared_location">"Поделился местоположением"</string>
|
||||
<string name="common_signing_out">"Выход…"</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_sticker">"Стикер"</string>
|
||||
<string name="common_success">"Успешно"</string>
|
||||
|
|
|
|||
|
|
@ -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_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_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_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>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ private const val versionYear = 25
|
|||
private const val versionMonth = 5
|
||||
|
||||
// Note: must be in [0,99]
|
||||
private const val versionReleaseNumber = 1
|
||||
private const val versionReleaseNumber = 2
|
||||
|
||||
object Versions {
|
||||
const val VERSION_CODE = (2000 + versionYear) * 10_000 + versionMonth * 100 + versionReleaseNumber
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -27,8 +27,6 @@ import timber.log.Timber
|
|||
import javax.inject.Inject
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.iterator
|
||||
import kotlin.collections.orEmpty
|
||||
|
||||
@ContributesMultibinding(AppScope::class)
|
||||
class SentryAnalyticsProvider @Inject constructor(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue