diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/OnBoardingConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/OnBoardingConfig.kt index d70f831eaa..7c08b28ce3 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/OnBoardingConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/OnBoardingConfig.kt @@ -12,5 +12,5 @@ object OnBoardingConfig { const val CAN_LOGIN_WITH_QR_CODE = true /** Whether the user can create an account using the app. */ - const val CAN_CREATE_ACCOUNT = false + const val CAN_CREATE_ACCOUNT = true } diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index fae6fc9c15..fd389645a9 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -59,6 +59,7 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.oidc.impl) + testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.features.networkmonitor.test) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt index cd156bce02..4f60e543bb 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt @@ -9,4 +9,6 @@ package io.element.android.appnav.loggedin sealed interface LoggedInEvents { data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents + data object CheckSlidingSyncProxyAvailability : LoggedInEvents + data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvents } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index e6779c2c99..c619629152 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import im.vector.app.features.analytics.plan.CryptoSessionStateChange import im.vector.app.features.analytics.plan.UserProperties import io.element.android.features.networkmonitor.api.NetworkMonitor @@ -29,6 +30,7 @@ import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus +import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase import io.element.android.libraries.push.api.PushService import io.element.android.libraries.pushproviders.api.RegistrationFailure import io.element.android.services.analytics.api.AnalyticsService @@ -48,6 +50,7 @@ class LoggedInPresenter @Inject constructor( private val sessionVerificationService: SessionVerificationService, private val analyticsService: AnalyticsService, private val encryptionService: EncryptionService, + private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase, ) : Presenter { @Composable override fun present(): LoggedInState { @@ -78,6 +81,7 @@ class LoggedInPresenter @Inject constructor( networkStatus == NetworkStatus.Online && syncIndicator == RoomListService.SyncIndicator.Show } } + var forceNativeSlidingSyncMigration by remember { mutableStateOf(false) } LaunchedEffect(Unit) { combine( sessionVerificationService.sessionVerifiedStatus, @@ -97,6 +101,18 @@ class LoggedInPresenter @Inject constructor( } } } + LoggedInEvents.CheckSlidingSyncProxyAvailability -> coroutineScope.launch { + // Force the user to log out if they were using the proxy sliding sync and it's no longer available, but native sliding sync is. + forceNativeSlidingSyncMigration = !matrixClient.isUsingNativeSlidingSync() && + matrixClient.isNativeSlidingSyncSupported() && + !matrixClient.isSlidingSyncProxySupported() + } + LoggedInEvents.LogoutAndMigrateToNativeSlidingSync -> coroutineScope.launch { + // Enable native sliding sync if it wasn't already the case + enableNativeSlidingSyncUseCase() + // Then force the logout + matrixClient.logout(userInitiated = true, ignoreSdkError = true) + } } } @@ -104,6 +120,7 @@ class LoggedInPresenter @Inject constructor( showSyncSpinner = showSyncSpinner, pusherRegistrationState = pusherRegistrationState.value, ignoreRegistrationError = ignoreRegistrationError, + forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration, eventSink = ::handleEvent ) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt index 7ef0e6f549..2735b02706 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt @@ -13,5 +13,6 @@ data class LoggedInState( val showSyncSpinner: Boolean, val pusherRegistrationState: AsyncData, val ignoreRegistrationError: Boolean, + val forceNativeSlidingSyncMigration: Boolean, val eventSink: (LoggedInEvents) -> Unit, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt index bcc88b1c3a..13d100820b 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt @@ -16,15 +16,18 @@ open class LoggedInStateProvider : PreviewParameterProvider { aLoggedInState(), aLoggedInState(showSyncSpinner = true), aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())), + aLoggedInState(forceNativeSlidingSyncMigration = true), ) } fun aLoggedInState( showSyncSpinner: Boolean = false, pusherRegistrationState: AsyncData = AsyncData.Uninitialized, + forceNativeSlidingSyncMigration: Boolean = false, ) = LoggedInState( showSyncSpinner = showSyncSpinner, pusherRegistrationState = pusherRegistrationState, ignoreRegistrationError = false, + forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration, eventSink = {}, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt index 0af150e0b7..3134270965 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt @@ -15,10 +15,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.Lifecycle +import io.element.android.appnav.R import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogWithDoNotShowAgain import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.matrix.api.exception.isNetworkError import io.element.android.libraries.ui.strings.CommonStrings @@ -28,6 +32,11 @@ fun LoggedInView( navigateToNotificationTroubleshoot: () -> Unit, modifier: Modifier = Modifier ) { + OnLifecycleEvent { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + state.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability) + } + } Box( modifier = modifier .fillMaxSize() @@ -61,6 +70,13 @@ fun LoggedInView( } } } + + // Set the force migration dialog here so it's always displayed over every screen + if (state.forceNativeSlidingSyncMigration) { + ForceNativeSlidingSyncMigrationDialog(onSubmit = { + state.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync) + }) + } } private fun Throwable.getReason(): String? { @@ -80,6 +96,19 @@ private fun Throwable.getReason(): String? { } } +@Composable +private fun ForceNativeSlidingSyncMigrationDialog( + onSubmit: () -> Unit, +) { + ErrorDialog( + title = null, + content = stringResource(R.string.banner_migrate_to_native_sliding_sync_force_logout_title), + submitText = stringResource(R.string.banner_migrate_to_native_sliding_sync_action), + onSubmit = onSubmit, + canDismiss = false, + ) +} + @PreviewsDayNight @Composable internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) = ElementPreview { diff --git a/appnav/src/main/res/values-cs/translations.xml b/appnav/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..e749887bc4 --- /dev/null +++ b/appnav/src/main/res/values-cs/translations.xml @@ -0,0 +1,5 @@ + + + "Odhlásit se a upgradovat" + "Váš domovský server již nepodporuje starý protokol. Chcete-li pokračovat v používání aplikace, odhlaste se a znovu se přihlaste." + diff --git a/appnav/src/main/res/values-de/translations.xml b/appnav/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..2b33dacaca --- /dev/null +++ b/appnav/src/main/res/values-de/translations.xml @@ -0,0 +1,5 @@ + + + "Abmelden und aktualisieren" + "Dein Homeserver unterstützt das alte Protokoll nicht mehr. Bitte logge dich aus und melde dich wieder an, um die App weiter zu nutzen." + diff --git a/appnav/src/main/res/values-el/translations.xml b/appnav/src/main/res/values-el/translations.xml new file mode 100644 index 0000000000..42925c0b6f --- /dev/null +++ b/appnav/src/main/res/values-el/translations.xml @@ -0,0 +1,5 @@ + + + "Αποσύνδεση & Αναβάθμιση" + "Ο οικιακός διακομιστής σου δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για να συνεχίσεις να χρησιμοποιείς την εφαρμογή." + diff --git a/appnav/src/main/res/values-et/translations.xml b/appnav/src/main/res/values-et/translations.xml new file mode 100644 index 0000000000..1631134d60 --- /dev/null +++ b/appnav/src/main/res/values-et/translations.xml @@ -0,0 +1,5 @@ + + + "Logi välja ja uuenda" + "Sinu koduserver enam ei toeta vana protokolli. Jätkamaks rakenduse kasutamist palun logi välja ning seejärel tagasi." + diff --git a/appnav/src/main/res/values-fr/translations.xml b/appnav/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..6295bc88df --- /dev/null +++ b/appnav/src/main/res/values-fr/translations.xml @@ -0,0 +1,5 @@ + + + "Déconnecter et mettre à niveau" + "Votre serveur d’accueil ne prend plus en charge l’ancien protocole. Veuillez vous déconnecter puis vous reconnecter pour continuer à utiliser l’application." + diff --git a/appnav/src/main/res/values-hu/translations.xml b/appnav/src/main/res/values-hu/translations.xml new file mode 100644 index 0000000000..e4b24f811b --- /dev/null +++ b/appnav/src/main/res/values-hu/translations.xml @@ -0,0 +1,5 @@ + + + "Kijelentkezés és frissítés" + "A Matrix-kiszolgáló már nem támogatja a régi protokollt. Az alkalmazás további használatához jelentkezzen ki és be." + diff --git a/appnav/src/main/res/values-ru/translations.xml b/appnav/src/main/res/values-ru/translations.xml new file mode 100644 index 0000000000..df600ea045 --- /dev/null +++ b/appnav/src/main/res/values-ru/translations.xml @@ -0,0 +1,5 @@ + + + "Выйти и обновить" + "Ваш homeserver больше не поддерживает старый протокол. Пожалуйста, выйдите из системы и войдите снова, чтобы продолжить использование приложения." + diff --git a/appnav/src/main/res/values-sk/translations.xml b/appnav/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..d1ac766f8c --- /dev/null +++ b/appnav/src/main/res/values-sk/translations.xml @@ -0,0 +1,5 @@ + + + "Odhlásiť sa a aktualizovať" + "Váš domovský server už nepodporuje starý protokol. Ak chcete pokračovať v používaní aplikácie, odhláste sa a znova sa prihláste." + diff --git a/appnav/src/main/res/values/localazy.xml b/appnav/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..bb6df44051 --- /dev/null +++ b/appnav/src/main/res/values/localazy.xml @@ -0,0 +1,5 @@ + + + "Log Out & Upgrade" + "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app." + diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt index ee060f3444..519cceddfe 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt @@ -29,6 +29,8 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.test.FakePushService import io.element.android.libraries.pushproviders.api.Distributor @@ -42,6 +44,10 @@ import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -91,7 +97,8 @@ class LoggedInPresenterTest { pushService = FakePushService(), sessionVerificationService = verificationService, analyticsService = analyticsService, - encryptionService = encryptionService + encryptionService = encryptionService, + enableNativeSlidingSyncUseCase = EnableNativeSlidingSyncUseCase(InMemoryAppPreferencesStore(), this), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -487,26 +494,103 @@ class LoggedInPresenterTest { ) } + @Test + fun `present - CheckSlidingSyncProxyAvailability forces the sliding sync migration under the right circumstances`() = runTest { + // The migration will be forced if: + // - The user is not using the native sliding sync + // - The sliding sync proxy is no longer supported + // - The native sliding sync is supported + val matrixClient = FakeMatrixClient( + isUsingNativeSlidingSyncLambda = { false }, + isSlidingSyncProxySupportedLambda = { false }, + isNativeSlidingSyncSupportedLambda = { true }, + ) + val presenter = createLoggedInPresenter(matrixClient = matrixClient) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.forceNativeSlidingSyncMigration).isFalse() + + initialState.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability) + + assertThat(awaitItem().forceNativeSlidingSyncMigration).isTrue() + } + } + + @Test + fun `present - CheckSlidingSyncProxyAvailability will not force the migration if native sliding sync is not supported too`() = runTest { + val matrixClient = FakeMatrixClient( + isUsingNativeSlidingSyncLambda = { false }, + isSlidingSyncProxySupportedLambda = { false }, + isNativeSlidingSyncSupportedLambda = { false }, + ) + val presenter = createLoggedInPresenter(matrixClient = matrixClient) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.forceNativeSlidingSyncMigration).isFalse() + + initialState.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability) + + expectNoEvents() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - LogoutAndMigrateToNativeSlidingSync enables native sliding sync and logs out the user`() = runTest { + val logoutLambda = lambdaRecorder { userInitiated, ignoreSdkError -> + assertThat(userInitiated).isTrue() + assertThat(ignoreSdkError).isTrue() + null + } + val matrixClient = FakeMatrixClient().apply { + this.logoutLambda = logoutLambda + } + val appPreferencesStore = InMemoryAppPreferencesStore() + val enableNativeSlidingSyncUseCase = EnableNativeSlidingSyncUseCase(appPreferencesStore, this) + val presenter = createLoggedInPresenter(matrixClient = matrixClient, enableNativeSlidingSyncUseCase = enableNativeSlidingSyncUseCase) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + + assertThat(appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse() + + initialState.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync) + + advanceUntilIdle() + + assertThat(appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue() + assertThat(logoutLambda.assertions().isCalledOnce()) + } + } + private suspend fun ReceiveTurbine.awaitFirstItem(): T { skipItems(1) return awaitItem() } - private fun createLoggedInPresenter( + private fun TestScope.createLoggedInPresenter( roomListService: RoomListService = FakeRoomListService(), networkStatus: NetworkStatus = NetworkStatus.Offline, analyticsService: AnalyticsService = FakeAnalyticsService(), sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(), encryptionService: EncryptionService = FakeEncryptionService(), pushService: PushService = FakePushService(), + enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase = EnableNativeSlidingSyncUseCase(InMemoryAppPreferencesStore(), this), + matrixClient: MatrixClient = FakeMatrixClient(roomListService = roomListService), ): LoggedInPresenter { return LoggedInPresenter( - matrixClient = FakeMatrixClient(roomListService = roomListService), + matrixClient = matrixClient, networkMonitor = FakeNetworkMonitor(networkStatus), pushService = pushService, sessionVerificationService = sessionVerificationService, analyticsService = analyticsService, - encryptionService = encryptionService + encryptionService = encryptionService, + enableNativeSlidingSyncUseCase = enableNativeSlidingSyncUseCase, ) } } diff --git a/features/analytics/api/src/main/res/values-nl/translations.xml b/features/analytics/api/src/main/res/values-nl/translations.xml index fcd05769c5..996fe846d5 100644 --- a/features/analytics/api/src/main/res/values-nl/translations.xml +++ b/features/analytics/api/src/main/res/values-nl/translations.xml @@ -3,4 +3,5 @@ "Deel anonieme gebruiksgegevens om ons te helpen problemen te identificeren." "Je kunt al onze voorwaarden %1$s lezen." "hier" + "Gebruiksgegevens delen" diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt index 7d0f1f6676..4641e4e7bc 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt @@ -111,7 +111,7 @@ internal fun CallScreenView( is AsyncData.Failure -> ErrorDialog( content = state.urlState.error.message.orEmpty(), - onDismiss = { state.eventSink(CallScreenEvents.Hangup) }, + onSubmit = { state.eventSink(CallScreenEvents.Hangup) }, ) is AsyncData.Success -> Unit } diff --git a/features/deactivation/api/build.gradle.kts b/features/deactivation/api/build.gradle.kts new file mode 100644 index 0000000000..25d59790e5 --- /dev/null +++ b/features/deactivation/api/build.gradle.kts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.features.deactivation.api" +} + +dependencies { + implementation(projects.libraries.architecture) +} diff --git a/features/deactivation/api/src/main/kotlin/io/element/android/features/deactivation/api/AccountDeactivationEntryPoint.kt b/features/deactivation/api/src/main/kotlin/io/element/android/features/deactivation/api/AccountDeactivationEntryPoint.kt new file mode 100644 index 0000000000..5865f1a2b8 --- /dev/null +++ b/features/deactivation/api/src/main/kotlin/io/element/android/features/deactivation/api/AccountDeactivationEntryPoint.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.deactivation.api + +import io.element.android.libraries.architecture.SimpleFeatureEntryPoint + +interface AccountDeactivationEntryPoint : SimpleFeatureEntryPoint diff --git a/features/deactivation/impl/build.gradle.kts b/features/deactivation/impl/build.gradle.kts new file mode 100644 index 0000000000..25a71098ac --- /dev/null +++ b/features/deactivation/impl/build.gradle.kts @@ -0,0 +1,50 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.deactivation.impl" + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) + implementation(projects.libraries.androidutils) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.testtags) + implementation(projects.libraries.uiStrings) + api(projects.features.deactivation.api) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(libs.test.robolectric) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) +} diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationEvents.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationEvents.kt new file mode 100644 index 0000000000..35be3f4bf2 --- /dev/null +++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationEvents.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.logout.impl + +sealed interface AccountDeactivationEvents { + data class SetEraseData(val eraseData: Boolean) : AccountDeactivationEvents + data class SetPassword(val password: String) : AccountDeactivationEvents + data class DeactivateAccount(val isRetry: Boolean) : AccountDeactivationEvents + data object CloseDialogs : AccountDeactivationEvents +} diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationNode.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationNode.kt new file mode 100644 index 0000000000..0faad3ad20 --- /dev/null +++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationNode.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.logout.impl + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class AccountDeactivationNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: AccountDeactivationPresenter, +) : Node(buildContext, plugins = plugins) { + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + AccountDeactivationView( + state = state, + onBackClick = ::navigateUp, + modifier = modifier, + ) + } +} diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenter.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenter.kt new file mode 100644 index 0000000000..8f0e895524 --- /dev/null +++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenter.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.logout.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.matrix.api.MatrixClient +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +class AccountDeactivationPresenter @Inject constructor( + private val matrixClient: MatrixClient, +) : Presenter { + @Composable + override fun present(): AccountDeactivationState { + val localCoroutineScope = rememberCoroutineScope() + val action: MutableState> = remember { + mutableStateOf(AsyncAction.Uninitialized) + } + + val formState = remember { mutableStateOf(DeactivateFormState.Default) } + + fun handleEvents(event: AccountDeactivationEvents) { + when (event) { + is AccountDeactivationEvents.SetEraseData -> { + updateFormState(formState) { + copy(eraseData = event.eraseData) + } + } + is AccountDeactivationEvents.SetPassword -> { + updateFormState(formState) { + copy(password = event.password) + } + } + is AccountDeactivationEvents.DeactivateAccount -> + if (action.value.isConfirming() || event.isRetry) { + localCoroutineScope.deactivateAccount( + formState = formState.value, + action + ) + } else { + action.value = AsyncAction.Confirming + } + AccountDeactivationEvents.CloseDialogs -> { + action.value = AsyncAction.Uninitialized + } + } + } + + return AccountDeactivationState( + deactivateFormState = formState.value, + accountDeactivationAction = action.value, + eventSink = ::handleEvents + ) + } + + private fun updateFormState(formState: MutableState, updateLambda: DeactivateFormState.() -> DeactivateFormState) { + formState.value = updateLambda(formState.value) + } + + private fun CoroutineScope.deactivateAccount( + formState: DeactivateFormState, + action: MutableState>, + ) = launch { + suspend { + matrixClient.deactivateAccount( + password = formState.password, + eraseData = formState.eraseData, + ).getOrThrow() + }.runCatchingUpdatingState(action) + } +} diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationState.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationState.kt new file mode 100644 index 0000000000..5504c3b0bf --- /dev/null +++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationState.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.logout.impl + +import android.os.Parcelable +import io.element.android.libraries.architecture.AsyncAction +import kotlinx.parcelize.Parcelize + +data class AccountDeactivationState( + val deactivateFormState: DeactivateFormState, + val accountDeactivationAction: AsyncAction, + val eventSink: (AccountDeactivationEvents) -> Unit, +) { + val submitEnabled: Boolean + get() = accountDeactivationAction is AsyncAction.Uninitialized && + deactivateFormState.password.isNotEmpty() +} + +@Parcelize +data class DeactivateFormState( + val eraseData: Boolean, + val password: String +) : Parcelable { + companion object { + val Default = DeactivateFormState(false, "") + } +} diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationStateProvider.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationStateProvider.kt new file mode 100644 index 0000000000..07ef6590bb --- /dev/null +++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationStateProvider.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.logout.impl + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction + +open class AccountDeactivationStateProvider : PreviewParameterProvider { + private val filledForm = aDeactivateFormState(eraseData = true, password = "password") + override val values: Sequence + get() = sequenceOf( + anAccountDeactivationState(), + anAccountDeactivationState( + deactivateFormState = filledForm + ), + anAccountDeactivationState( + deactivateFormState = filledForm, + accountDeactivationAction = AsyncAction.Confirming, + ), + anAccountDeactivationState( + deactivateFormState = filledForm, + accountDeactivationAction = AsyncAction.Loading + ), + anAccountDeactivationState( + deactivateFormState = filledForm, + accountDeactivationAction = AsyncAction.Failure(Exception("Failed to deactivate account")) + ), + ) +} + +internal fun aDeactivateFormState( + eraseData: Boolean = false, + password: String = "", +) = DeactivateFormState( + eraseData = eraseData, + password = password, +) + +internal fun anAccountDeactivationState( + deactivateFormState: DeactivateFormState = aDeactivateFormState(), + accountDeactivationAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (AccountDeactivationEvents) -> Unit = {}, +) = AccountDeactivationState( + deactivateFormState = deactivateFormState, + accountDeactivationAction = accountDeactivationAction, + eventSink = eventSink, +) diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt new file mode 100644 index 0000000000..7df20acb96 --- /dev/null +++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt @@ -0,0 +1,339 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +@file:OptIn(ExperimentalComposeUiApi::class) + +package io.element.android.features.logout.impl + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.autofill.AutofillType +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.PreviewParameter +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.deactivation.impl.R +import io.element.android.features.logout.impl.ui.AccountDeactivationActionDialog +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem +import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.form.textFieldState +import io.element.android.libraries.designsystem.components.list.SwitchListItem +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart +import io.element.android.libraries.designsystem.theme.aliasScreenTitle +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.OutlinedTextField +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.designsystem.theme.components.autofill +import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.persistentListOf + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AccountDeactivationView( + state: AccountDeactivationState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val eventSink = state.eventSink + + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + navigationIcon = { + BackButton(onClick = onBackClick) + }, + title = { + Text( + text = stringResource(R.string.screen_deactivate_account_title), + style = ElementTheme.typography.aliasScreenTitle, + ) + }, + ) + }, + ) { padding -> + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .imePadding() + .padding(padding) + .consumeWindowInsets(padding) + .verticalScroll(state = scrollState) + .padding(vertical = 16.dp, horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Content( + state = state, + onSubmitClick = { + eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false)) + } + ) + Spacer(modifier = Modifier.height(32.dp)) + Buttons( + state = state, + onSubmitClick = { + eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false)) + } + ) + } + } + AccountDeactivationActionDialog( + state.accountDeactivationAction, + onConfirmClick = { + eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false)) + }, + onRetryClick = { + eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = true)) + }, + onDismissDialog = { + eventSink(AccountDeactivationEvents.CloseDialogs) + }, + ) +} + +@Composable +private fun ColumnScope.Buttons( + state: AccountDeactivationState, + onSubmitClick: () -> Unit, +) { + val logoutAction = state.accountDeactivationAction + Button( + text = stringResource(CommonStrings.action_deactivate), + showProgress = logoutAction is AsyncAction.Loading, + destructive = true, + enabled = state.submitEnabled, + modifier = Modifier.fillMaxWidth(), + onClick = onSubmitClick, + ) +} + +@Composable +private fun Content( + state: AccountDeactivationState, + onSubmitClick: () -> Unit, +) { + val isLoading by remember(state.deactivateFormState) { + derivedStateOf { + state.accountDeactivationAction is AsyncAction.Loading + } + } + val eraseData = state.deactivateFormState.eraseData + var passwordFieldState by textFieldState(stateValue = state.deactivateFormState.password) + + val focusManager = LocalFocusManager.current + val eventSink = state.eventSink + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = buildAnnotatedStringWithStyledPart( + R.string.screen_deactivate_account_description, + R.string.screen_deactivate_account_description_bold_part, + color = ElementTheme.colors.textSecondary, + bold = true, + underline = false, + ), + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + ) + InfoListOrganism( + items = persistentListOf( + InfoListItem( + message = buildAnnotatedStringWithStyledPart( + R.string.screen_deactivate_account_list_item_1, + R.string.screen_deactivate_account_list_item_1_bold_part, + color = ElementTheme.colors.textSecondary, + bold = true, + underline = false, + ), + iconComposable = { + Icon( + modifier = Modifier.size(20.dp), + imageVector = CompoundIcons.Close(), + contentDescription = null, + tint = ElementTheme.colors.iconCriticalPrimary, + ) + }, + ), + InfoListItem( + message = stringResource(R.string.screen_deactivate_account_list_item_2), + iconComposable = { + Icon( + modifier = Modifier.size(20.dp), + imageVector = CompoundIcons.Close(), + contentDescription = null, + tint = ElementTheme.colors.iconCriticalPrimary, + ) + }, + ), + InfoListItem( + message = stringResource(R.string.screen_deactivate_account_list_item_3), + iconComposable = { + Icon( + modifier = Modifier.size(20.dp), + imageVector = CompoundIcons.Close(), + contentDescription = null, + tint = ElementTheme.colors.iconCriticalPrimary, + ) + }, + ), + InfoListItem( + message = stringResource(R.string.screen_deactivate_account_list_item_4), + iconComposable = { + Icon( + modifier = Modifier.size(20.dp), + imageVector = CompoundIcons.Check(), + contentDescription = null, + tint = ElementTheme.colors.iconSuccessPrimary, + ) + }, + ), + ), + textStyle = ElementTheme.typography.fontBodyMdRegular, + textColor = ElementTheme.colors.textSecondary, + iconTint = ElementTheme.colors.iconSuccessPrimary, + backgroundColor = Color.Transparent, + ) + + Column { + SwitchListItem( + headline = stringResource(R.string.screen_deactivate_account_delete_all_messages), + value = eraseData, + onChange = { + eventSink(AccountDeactivationEvents.SetEraseData(it)) + }, + enabled = !isLoading, + ) + Text( + modifier = Modifier.padding(start = 16.dp), + text = stringResource(R.string.screen_deactivate_account_delete_all_messages_notice), + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + ) { + Text( + text = stringResource(CommonStrings.action_confirm_password), + style = ElementTheme.typography.fontBodySmMedium, + color = ElementTheme.colors.textSecondary, + ) + var passwordVisible by remember { mutableStateOf(false) } + if (isLoading) { + // Ensure password is hidden when user submits the form + passwordVisible = false + } + OutlinedTextField( + value = passwordFieldState, + readOnly = isLoading, + modifier = Modifier + .padding(top = 8.dp) + .fillMaxWidth() + .onTabOrEnterKeyFocusNext(focusManager) + .testTag(TestTags.loginPassword) + .autofill( + autofillTypes = listOf(AutofillType.Password), + onFill = { + val sanitized = it.sanitize() + passwordFieldState = sanitized + eventSink(AccountDeactivationEvents.SetPassword(sanitized)) + } + ), + onValueChange = { + val sanitized = it.sanitize() + passwordFieldState = sanitized + eventSink(AccountDeactivationEvents.SetPassword(sanitized)) + }, + placeholder = { + Text(text = stringResource(CommonStrings.common_password)) + }, + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + val image = + if (passwordVisible) CompoundIcons.VisibilityOn() else CompoundIcons.VisibilityOff() + val description = + if (passwordVisible) stringResource(CommonStrings.a11y_hide_password) else stringResource(CommonStrings.a11y_show_password) + + IconButton(onClick = { passwordVisible = !passwordVisible }) { + Icon(imageVector = image, description) + } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions( + onDone = { onSubmitClick() } + ), + singleLine = true, + ) + } + } +} + +/** + * Ensure that the string does not contain any new line characters, which can happen when pasting values. + */ +private fun String.sanitize(): String { + return replace("\n", "") +} + +@PreviewsDayNight +@Composable +internal fun AccountDeactivationViewPreview( + @PreviewParameter(AccountDeactivationStateProvider::class) state: AccountDeactivationState, +) = ElementPreview { + AccountDeactivationView( + state, + onBackClick = {}, + ) +} diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultAccountDeactivationEntryPoint.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultAccountDeactivationEntryPoint.kt new file mode 100644 index 0000000000..dd9197684c --- /dev/null +++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultAccountDeactivationEntryPoint.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.logout.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.deactivation.api.AccountDeactivationEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultAccountDeactivationEntryPoint @Inject constructor() : AccountDeactivationEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext): Node { + return parentNode.createNode(buildContext) + } +} diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationActionDialog.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationActionDialog.kt new file mode 100644 index 0000000000..8fcd9557cf --- /dev/null +++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationActionDialog.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.logout.impl.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.dialogs.RetryDialog +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun AccountDeactivationActionDialog( + state: AsyncAction, + onConfirmClick: () -> Unit, + onRetryClick: () -> Unit, + onDismissDialog: () -> Unit, +) { + when (state) { + AsyncAction.Uninitialized -> + Unit + AsyncAction.Confirming -> + AccountDeactivationConfirmationDialog( + onSubmitClick = onConfirmClick, + onDismiss = onDismissDialog + ) + is AsyncAction.Loading -> + ProgressDialog(text = stringResource(CommonStrings.common_please_wait)) + is AsyncAction.Failure -> + RetryDialog( + title = stringResource(id = CommonStrings.dialog_title_error), + content = stringResource(id = CommonStrings.error_unknown), + onRetry = onRetryClick, + onDismiss = onDismissDialog, + ) + is AsyncAction.Success -> Unit + } +} diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationConfirmationDialog.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationConfirmationDialog.kt new file mode 100644 index 0000000000..18a79c8ed5 --- /dev/null +++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationConfirmationDialog.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.logout.impl.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import io.element.android.features.deactivation.impl.R +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun AccountDeactivationConfirmationDialog( + onSubmitClick: () -> Unit, + onDismiss: () -> Unit, +) { + ConfirmationDialog( + title = stringResource(id = R.string.screen_deactivate_account_title), + content = stringResource(R.string.screen_deactivate_account_confirmation_dialog_content), + submitText = stringResource(id = CommonStrings.action_deactivate), + onSubmitClick = onSubmitClick, + onDismiss = onDismiss, + destructiveSubmit = true, + ) +} diff --git a/features/deactivation/impl/src/main/res/values/localazy.xml b/features/deactivation/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..0380cf1c94 --- /dev/null +++ b/features/deactivation/impl/src/main/res/values/localazy.xml @@ -0,0 +1,14 @@ + + + "Please confirm that you want to deactivate your account. This action cannot be undone." + "Delete all my messages" + "Warning: Future users may see incomplete conversations." + "Deactivating your account is %1$s, it will:" + "irreversible" + "%1$s your account (you can\'t log back in, and your ID can\'t be reused)." + "Permanently disable" + "Remove you from all chat rooms." + "Delete your account information from our identity server." + "Your messages will still be visible to registered users but won’t be available to new or unregistered users if you choose to delete them." + "Deactivate account" + diff --git a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenterTest.kt b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenterTest.kt new file mode 100644 index 0000000000..b34ac2f391 --- /dev/null +++ b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenterTest.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.logout.impl + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class AccountDeactivationPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.accountDeactivationAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(initialState.deactivateFormState).isEqualTo(DeactivateFormState.Default) + } + } + + @Test + fun `present - form update`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.deactivateFormState).isEqualTo(DeactivateFormState.Default) + initialState.eventSink(AccountDeactivationEvents.SetEraseData(true)) + val updatedState = awaitItem() + assertThat(updatedState.deactivateFormState).isEqualTo(DeactivateFormState.Default.copy(eraseData = true)) + assertThat(updatedState.submitEnabled).isFalse() + updatedState.eventSink(AccountDeactivationEvents.SetPassword("password")) + val updatedState2 = awaitItem() + assertThat(updatedState2.deactivateFormState).isEqualTo(DeactivateFormState(password = "password", eraseData = true)) + assertThat(updatedState2.submitEnabled).isTrue() + } + } + + @Test + fun `present - submit`() = runTest { + val recorder = lambdaRecorder> { _, _ -> + Result.success(Unit) + } + val matrixClient = FakeMatrixClient( + deactivateAccountResult = recorder + ) + val presenter = createPresenter(matrixClient) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(AccountDeactivationEvents.SetPassword("password")) + skipItems(1) + initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false)) + val updatedState = awaitItem() + assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.Confirming) + updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false)) + val updatedState2 = awaitItem() + assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading) + val finalState = awaitItem() + assertThat(finalState.accountDeactivationAction).isEqualTo(AsyncAction.Success(Unit)) + recorder.assertions().isCalledOnce().with(value("password"), value(false)) + } + } + + @Test + fun `present - submit with error and retry`() = runTest { + val recorder = lambdaRecorder> { _, _ -> + Result.failure(AN_EXCEPTION) + } + val matrixClient = FakeMatrixClient( + deactivateAccountResult = recorder + ) + val presenter = createPresenter(matrixClient) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(AccountDeactivationEvents.SetPassword("password")) + initialState.eventSink(AccountDeactivationEvents.SetEraseData(true)) + skipItems(2) + initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false)) + val updatedState = awaitItem() + assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.Confirming) + updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false)) + val updatedState2 = awaitItem() + assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading) + val finalState = awaitItem() + assertThat(finalState.accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) + recorder.assertions().isCalledOnce().with(value("password"), value(true)) + // Retry + finalState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = true)) + val finalState2 = awaitItem() + assertThat(finalState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading) + assertThat(awaitItem().accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) + } + } + + @Test + fun `present - submit with error and cancel`() = runTest { + val recorder = lambdaRecorder> { _, _ -> + Result.failure(AN_EXCEPTION) + } + val matrixClient = FakeMatrixClient( + deactivateAccountResult = recorder + ) + val presenter = createPresenter(matrixClient) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(AccountDeactivationEvents.SetPassword("password")) + initialState.eventSink(AccountDeactivationEvents.SetEraseData(true)) + skipItems(2) + initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false)) + val updatedState = awaitItem() + assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.Confirming) + updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false)) + val updatedState2 = awaitItem() + assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading) + val finalState = awaitItem() + assertThat(finalState.accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) + recorder.assertions().isCalledOnce().with(value("password"), value(true)) + // Cancel + finalState.eventSink(AccountDeactivationEvents.CloseDialogs) + val finalState2 = awaitItem() + assertThat(finalState2.accountDeactivationAction).isEqualTo(AsyncAction.Uninitialized) + } + } + + private fun createPresenter( + matrixClient: MatrixClient = FakeMatrixClient(), + ) = AccountDeactivationPresenter( + matrixClient = matrixClient, + ) +} diff --git a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt new file mode 100644 index 0000000000..06d27e463b --- /dev/null +++ b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.logout.impl + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performTextInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.deactivation.impl.R +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_PASSWORD +import io.element.android.libraries.testtags.TestTags +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 io.element.android.tests.testutils.pressTag +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 AccountDeactivationViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on back invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setAccountDeactivationView( + state = anAccountDeactivationState(eventSink = eventsRecorder), + onBackClick = it, + ) + rule.pressBack() + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on Deactivate emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setAccountDeactivationView( + state = anAccountDeactivationState( + deactivateFormState = aDeactivateFormState( + password = A_PASSWORD, + ), + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_deactivate) + eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false)) + } + + @Test + fun `clicking on Deactivate on the confirmation dialog emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setAccountDeactivationView( + state = anAccountDeactivationState( + deactivateFormState = aDeactivateFormState( + password = A_PASSWORD, + ), + accountDeactivationAction = AsyncAction.Confirming, + eventSink = eventsRecorder, + ), + ) + rule.pressTag(TestTags.dialogPositive.value) + eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false)) + } + + @Test + fun `clicking on retry on the confirmation dialog emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setAccountDeactivationView( + state = anAccountDeactivationState( + deactivateFormState = aDeactivateFormState( + password = A_PASSWORD, + ), + accountDeactivationAction = AsyncAction.Failure(AN_EXCEPTION), + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_retry) + eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(true)) + } + + @Test + fun `switching on the erase all switch emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setAccountDeactivationView( + state = anAccountDeactivationState( + eventSink = eventsRecorder, + ), + ) + rule.clickOn(R.string.screen_deactivate_account_delete_all_messages) + eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(true)) + } + + @Test + fun `switching off the erase all switch emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setAccountDeactivationView( + state = anAccountDeactivationState( + deactivateFormState = aDeactivateFormState( + eraseData = true, + ), + eventSink = eventsRecorder, + ), + ) + rule.clickOn(R.string.screen_deactivate_account_delete_all_messages) + eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(false)) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `typing text in the password field emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setAccountDeactivationView( + state = anAccountDeactivationState( + deactivateFormState = aDeactivateFormState( + password = A_PASSWORD, + ), + eventSink = eventsRecorder, + ), + ) + rule.onNodeWithTag(TestTags.loginPassword.value).performTextInput("A") + eventsRecorder.assertSingle(AccountDeactivationEvents.SetPassword("A$A_PASSWORD")) + } +} + +private fun AndroidComposeTestRule.setAccountDeactivationView( + state: AccountDeactivationState, + onBackClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + AccountDeactivationView( + state = state, + onBackClick = onBackClick, + ) + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt index 7d117dba5e..c44a6cfb4f 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt @@ -112,8 +112,8 @@ class AcceptDeclineInvitePresenter @Inject constructor( private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState>) = launch { suspend { - client.getRoom(roomId)?.use { - it.leave().getOrThrow() + client.getInvitedRoom(roomId)?.use { + it.declineInvite().getOrThrow() notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId) } roomId diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt index f4dcb4f278..45fb2efc86 100644 --- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt @@ -21,7 +21,7 @@ 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.FakeMatrixClient -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeInvitedRoom import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom import io.element.android.libraries.push.api.notifications.NotificationCleaner import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner @@ -83,12 +83,7 @@ class AcceptDeclineInvitePresenterTest { Result.failure(RuntimeException("Failed to leave room")) } val client = FakeMatrixClient().apply { - givenGetRoomResult( - roomId = A_ROOM_ID, - result = FakeMatrixRoom( - leaveRoomLambda = declineInviteFailure - ) - ) + getInvitedRoomResults[A_ROOM_ID] = FakeInvitedRoom(declineInviteResult = declineInviteFailure) } val presenter = createAcceptDeclineInvitePresenter(client = client) presenter.test { @@ -133,12 +128,7 @@ class AcceptDeclineInvitePresenterTest { Result.success(Unit) } val client = FakeMatrixClient().apply { - givenGetRoomResult( - roomId = A_ROOM_ID, - result = FakeMatrixRoom( - leaveRoomLambda = declineInviteSuccess - ) - ) + getInvitedRoomResults[A_ROOM_ID] = FakeInvitedRoom(declineInviteResult = declineInviteSuccess) } val presenter = createAcceptDeclineInvitePresenter( client = client, diff --git a/features/joinroom/impl/src/main/res/values-nl/translations.xml b/features/joinroom/impl/src/main/res/values-nl/translations.xml new file mode 100644 index 0000000000..d778ef8640 --- /dev/null +++ b/features/joinroom/impl/src/main/res/values-nl/translations.xml @@ -0,0 +1,11 @@ + + + "Toetreden tot de kamer" + "Klop om deel te nemen" + "%1$s ondersteunt nog geen spaces. Je kunt spaces benaderen via de webbrowser." + "Spaces worden nog niet ondersteund" + "Klik op de knop hieronder en een kamerbeheerder wordt op de hoogte gebracht. Na goedkeuring kun je deelnemen aan het gesprek." + "Je moet lid zijn van deze kamer om de berichtgeschiedenis te bekijken." + "Wil je tot deze kamer toetreden?" + "Voorbeeld is niet beschikbaar" + diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt index 6ee2990c5b..a98bfbff2f 100644 --- a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomView.kt @@ -105,7 +105,7 @@ private fun LeaveRoomErrorDialog( is LeaveRoomState.Error.Hidden -> {} is LeaveRoomState.Error.Shown -> ErrorDialog( content = stringResource(CommonStrings.error_unknown), - onDismiss = { state.eventSink(LeaveRoomEvent.HideError) } + onSubmit = { state.eventSink(LeaveRoomEvent.HideError) } ) } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt index b794f2eed4..b3cd532487 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt @@ -116,7 +116,7 @@ private fun SetupPinContent( ErrorDialog( title = state.setupPinFailure.title(), content = state.setupPinFailure.content(), - onDismiss = { + onSubmit = { state.eventSink(SetupPinEvents.ClearFailure) } ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 702e253f03..8667b3806a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -104,7 +104,7 @@ fun PinUnlockView( if (state.showBiometricUnlockError) { ErrorDialog( content = state.biometricUnlockErrorMessage ?: "", - onDismiss = { state.eventSink(PinUnlockEvents.ClearBiometricError) } + onSubmit = { state.eventSink(PinUnlockEvents.ClearBiometricError) } ) } } @@ -206,7 +206,7 @@ private fun SignOutPrompt( ErrorDialog( title = stringResource(id = R.string.screen_app_lock_signout_alert_title), content = stringResource(id = R.string.screen_app_lock_signout_alert_message), - onDismiss = onSignOut, + onSubmit = onSignOut, ) } } diff --git a/features/lockscreen/impl/src/main/res/values-de/translations.xml b/features/lockscreen/impl/src/main/res/values-de/translations.xml index bf89b3f5e4..4042df3d1b 100644 --- a/features/lockscreen/impl/src/main/res/values-de/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-de/translations.xml @@ -28,8 +28,8 @@ Wähle etwas Einprägsames. Bei falscher Eingabe wirst du aus der App ausgeloggt "Du hast %1$d Versuche zum Entsperren" - "Falsche PIN. Du hast %1$d weitere Chance" - "Falsche PIN. Du hast %1$d weitere Chancen" + "Falsche PIN. Du hast %1$d weiteren Versuch" + "Falsche PIN. Du hast %1$d weitere Versuche" "Biometrie verwenden" "PIN verwenden" diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index 6ea1eb7936..fc783e8733 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation(projects.libraries.oidc.api) implementation(libs.androidx.browser) implementation(platform(libs.network.retrofit.bom)) + implementation(libs.androidx.webkit) implementation(libs.network.retrofit) implementation(libs.serialization.json) api(projects.features.login.api) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt index a118920ebd..c9e97e8995 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt @@ -30,6 +30,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderDat import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode +import io.element.android.features.login.impl.screens.createaccount.CreateAccountNode import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode import io.element.android.libraries.architecture.BackstackView @@ -109,6 +110,9 @@ class LoginFlowNode @AssistedInject constructor( @Parcelize data object LoginPassword : NavTarget + @Parcelize + data class CreateAccount(val url: String) : NavTarget + @Parcelize data class OidcView(val oidcDetails: OidcDetails) : NavTarget } @@ -140,6 +144,10 @@ class LoginFlowNode @AssistedInject constructor( } } + override fun onCreateAccountContinue(url: String) { + backstack.push(NavTarget.CreateAccount(url)) + } + override fun onLoginPasswordNeeded() { backstack.push(NavTarget.LoginPassword) } @@ -180,6 +188,12 @@ class LoginFlowNode @AssistedInject constructor( is NavTarget.OidcView -> { oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.oidcDetails.url) } + is NavTarget.CreateAccount -> { + val inputs = CreateAccountNode.Inputs( + url = navTarget.url, + ) + createNode(buildContext, listOf(inputs)) + } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt index a2c3b48251..c6139ae492 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt @@ -36,7 +36,7 @@ fun ChangeServerView( ErrorDialog( modifier = modifier, content = error.message(), - onDismiss = { + onSubmit = { eventSink.invoke(ChangeServerEvents.ClearError) } ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/ElementWellKnown.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/ElementWellKnown.kt new file mode 100644 index 0000000000..d149957a55 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/ElementWellKnown.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2023, 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.login.impl.resolver.network + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Example: + *
+ * {
+ *     "registration_helper_url": "https://element.io"
+ * }
+ * 
+ * . + */ +@Serializable +data class ElementWellKnown( + @SerialName("registration_helper_url") + val registrationHelperUrl: String? = null, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownAPI.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownAPI.kt index 05658e9e7c..11f1621b03 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownAPI.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownAPI.kt @@ -12,4 +12,7 @@ import retrofit2.http.GET internal interface WellknownAPI { @GET(".well-known/matrix/client") suspend fun getWellKnown(): WellKnown + + @GET(".well-known/element/element.json") + suspend fun getElementWellKnown(): ElementWellKnown } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt index 8e865d9e39..454da3f795 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt @@ -43,6 +43,7 @@ class ConfirmAccountProviderNode @AssistedInject constructor( interface Callback : Plugin { fun onLoginPasswordNeeded() fun onOidcDetails(oidcDetails: OidcDetails) + fun onCreateAccountContinue(url: String) fun onChangeAccountProvider() } @@ -54,6 +55,10 @@ class ConfirmAccountProviderNode @AssistedInject constructor( plugins().forEach { it.onLoginPasswordNeeded() } } + private fun onCreateAccountContinue(url: String) { + plugins().forEach { it.onCreateAccountContinue(url) } + } + private fun onChangeAccountProvider() { plugins().forEach { it.onChangeAccountProvider() } } @@ -67,6 +72,7 @@ class ConfirmAccountProviderNode @AssistedInject constructor( modifier = modifier, onOidcDetails = ::onOidcDetails, onNeedLoginPassword = ::onLoginPasswordNeeded, + onCreateAccountContinue = ::onCreateAccountContinue, onChange = ::onChangeAccountProvider, onLearnMoreClick = { openLearnMorePage(context) }, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt index a51573f602..c15c84744f 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt @@ -21,6 +21,8 @@ import dagger.assisted.AssistedInject import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.error.ChangeServerError +import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported +import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState @@ -36,6 +38,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor( private val authenticationService: MatrixAuthenticationService, private val oidcActionFlow: OidcActionFlow, private val defaultLoginUserStory: DefaultLoginUserStory, + private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever, ) : Presenter { data class Params( val isAccountCreation: Boolean, @@ -90,13 +93,24 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor( if (matrixHomeServerDetails.supportsOidcLogin) { // Retrieve the details right now LoginFlow.OidcFlow(authenticationService.getOidcUrl().getOrThrow()) + } else if (params.isAccountCreation) { + val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl) + LoginFlow.AccountCreationFlow(url) } else if (matrixHomeServerDetails.supportsPasswordLogin) { LoginFlow.PasswordLogin } else { error("Unsupported login flow") } }.getOrThrow() - }.runCatchingUpdatingState(loginFlowAction, errorTransform = ChangeServerError::from) + }.runCatchingUpdatingState( + state = loginFlowAction, + errorTransform = { + when (it) { + is AccountCreationNotSupported -> it + else -> ChangeServerError.from(it) + } + } + ) } private suspend fun onOidcAction( diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt index b73acab4ec..3c80d76d08 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt @@ -24,4 +24,5 @@ data class ConfirmAccountProviderState( sealed interface LoginFlow { data object PasswordLogin : LoginFlow data class OidcFlow(val oidcDetails: OidcDetails) : LoginFlow + data class AccountCreationFlow(val url: String) : LoginFlow } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt index d643f0001a..5c95823ded 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt @@ -8,20 +8,33 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider +import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported import io.element.android.libraries.architecture.AsyncData open class ConfirmAccountProviderStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aConfirmAccountProviderState(), - // Add other state here + aConfirmAccountProviderState( + isAccountCreation = true, + ), + aConfirmAccountProviderState( + isAccountCreation = true, + loginFlow = AsyncData.Failure(AccountCreationNotSupported()) + ), ) } -fun aConfirmAccountProviderState() = ConfirmAccountProviderState( - accountProvider = anAccountProvider(), - isAccountCreation = false, - loginFlow = AsyncData.Uninitialized, - eventSink = {} +private fun aConfirmAccountProviderState( + accountProvider: AccountProvider = anAccountProvider(), + isAccountCreation: Boolean = false, + loginFlow: AsyncData = AsyncData.Uninitialized, + eventSink: (ConfirmAccountProviderEvents) -> Unit = {}, +) = ConfirmAccountProviderState( + accountProvider = accountProvider, + isAccountCreation = isAccountCreation, + loginFlow = loginFlow, + eventSink = eventSink ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt index a516c1b4a6..407ea9d88b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp import io.element.android.features.login.impl.R import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog import io.element.android.features.login.impl.error.ChangeServerError +import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule @@ -42,6 +43,7 @@ fun ConfirmAccountProviderView( onOidcDetails: (OidcDetails) -> Unit, onNeedLoginPassword: () -> Unit, onLearnMoreClick: () -> Unit, + onCreateAccountContinue: (url: String) -> Unit, onChange: () -> Unit, modifier: Modifier = Modifier, ) { @@ -103,18 +105,29 @@ fun ConfirmAccountProviderView( is ChangeServerError.Error -> { ErrorDialog( content = error.message(), - onDismiss = { + onSubmit = { eventSink.invoke(ConfirmAccountProviderEvents.ClearError) } ) } is ChangeServerError.SlidingSyncAlert -> { - SlidingSyncNotSupportedDialog(onLearnMoreClick = { - onLearnMoreClick() - eventSink(ConfirmAccountProviderEvents.ClearError) - }, onDismiss = { - eventSink(ConfirmAccountProviderEvents.ClearError) - }) + SlidingSyncNotSupportedDialog( + onLearnMoreClick = { + onLearnMoreClick() + eventSink(ConfirmAccountProviderEvents.ClearError) + }, + onDismiss = { + eventSink(ConfirmAccountProviderEvents.ClearError) + } + ) + } + is AccountCreationNotSupported -> { + ErrorDialog( + content = stringResource(CommonStrings.error_account_creation_not_possible), + onSubmit = { + eventSink.invoke(ConfirmAccountProviderEvents.ClearError) + } + ) } } } @@ -123,6 +136,7 @@ fun ConfirmAccountProviderView( when (val loginFlowState = state.loginFlow.data) { is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails) LoginFlow.PasswordLogin -> onNeedLoginPassword() + is LoginFlow.AccountCreationFlow -> onCreateAccountContinue(loginFlowState.url) } } AsyncData.Uninitialized -> Unit @@ -139,6 +153,7 @@ internal fun ConfirmAccountProviderViewPreview( state = state, onOidcDetails = {}, onNeedLoginPassword = {}, + onCreateAccountContinue = {}, onLearnMoreClick = {}, onChange = {}, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/AccountCreationNotSupported.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/AccountCreationNotSupported.kt new file mode 100644 index 0000000000..8a299a6d54 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/AccountCreationNotSupported.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.createaccount + +class AccountCreationNotSupported : Exception() diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountEvents.kt new file mode 100644 index 0000000000..8647d71c43 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountEvents.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.createaccount + +sealed interface CreateAccountEvents { + data class SetPageProgress(val progress: Int) : CreateAccountEvents + data class OnMessageReceived(val message: String) : CreateAccountEvents +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountNode.kt new file mode 100644 index 0000000000..b17699e6f8 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountNode.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.createaccount + +import android.app.Activity +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.AppScope + +@ContributesNode(AppScope::class) +class CreateAccountNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: CreateAccountPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + data class Inputs( + val url: String, + ) : NodeInputs + + private val presenter = presenterFactory.create(inputs().url) + + private fun onOpenExternalUrl(activity: Activity, darkTheme: Boolean, url: String) { + activity.openUrlInChromeCustomTab(null, darkTheme, url) + } + + @Composable + override fun View(modifier: Modifier) { + val activity = LocalContext.current as Activity + val isDark = ElementTheme.isLightTheme.not() + val state = presenter.present() + CreateAccountView( + state = state, + modifier = modifier, + onBackClick = ::navigateUp, + onOpenExternalUrl = { + onOpenExternalUrl(activity, isDark, it) + }, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenter.kt new file mode 100644 index 0000000000..69ffb10883 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenter.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2023, 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.createaccount + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.features.login.impl.DefaultLoginUserStory +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.extensions.flatMap +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.core.SessionId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class CreateAccountPresenter @AssistedInject constructor( + @Assisted private val url: String, + private val authenticationService: MatrixAuthenticationService, + private val defaultLoginUserStory: DefaultLoginUserStory, + private val messageParser: MessageParser, + private val buildMeta: BuildMeta, +) : Presenter { + @AssistedFactory + interface Factory { + fun create(url: String): CreateAccountPresenter + } + + @Composable + override fun present(): CreateAccountState { + val coroutineScope = rememberCoroutineScope() + val pageProgress: MutableState = remember { mutableIntStateOf(0) } + val createAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } + + fun handleEvents(event: CreateAccountEvents) { + when (event) { + is CreateAccountEvents.SetPageProgress -> { + pageProgress.value = event.progress + } + is CreateAccountEvents.OnMessageReceived -> { + // Ignore unexpected message + if (event.message.contains("isTrusted")) return + coroutineScope.importSession(event.message, createAction) + } + } + } + + return CreateAccountState( + url = url, + pageProgress = pageProgress.value, + isDebugBuild = buildMeta.isDebuggable, + createAction = createAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.importSession(message: String, loggedInState: MutableState>) = launch { + loggedInState.value = AsyncAction.Loading + runCatching { + messageParser.parse(message) + }.flatMap { externalSession -> + authenticationService.importCreatedSession(externalSession) + }.onSuccess { sessionId -> + // We will not navigate to the WaitList screen, so the login user story is done + defaultLoginUserStory.setLoginFlowIsDone(true) + loggedInState.value = AsyncAction.Success(sessionId) + }.onFailure { failure -> + loggedInState.value = AsyncAction.Failure(failure) + } + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountState.kt new file mode 100644 index 0000000000..de05825d69 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountState.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.createaccount + +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.SessionId + +data class CreateAccountState( + val url: String, + val pageProgress: Int, + val createAction: AsyncAction, + val isDebugBuild: Boolean, + val eventSink: (CreateAccountEvents) -> Unit +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountStateProvider.kt new file mode 100644 index 0000000000..45b3b4d004 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountStateProvider.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2023, 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.createaccount + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.SessionId + +open class CreateAccountStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aCreateAccountState(), + aCreateAccountState(pageProgress = 33), + aCreateAccountState(createAction = AsyncAction.Loading), + aCreateAccountState(createAction = AsyncAction.Failure(Throwable("Failed to create account"))), + ) +} + +private fun aCreateAccountState( + pageProgress: Int = 100, + createAction: AsyncAction = AsyncAction.Uninitialized, +) = CreateAccountState( + url = "https://example.com", + isDebugBuild = true, + pageProgress = pageProgress, + createAction = createAction, + eventSink = {} +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountView.kt new file mode 100644 index 0000000000..aaa67a80f9 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountView.kt @@ -0,0 +1,181 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.createaccount + +import android.annotation.SuppressLint +import android.view.ViewGroup +import android.webkit.JsResult +import android.webkit.WebChromeClient +import android.webkit.WebView +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import io.element.android.compound.theme.ElementTheme +import io.element.android.features.login.impl.R +import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.aliasScreenTitle +import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor +import timber.log.Timber + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreateAccountView( + state: CreateAccountState, + onBackClick: () -> Unit, + onOpenExternalUrl: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { + Text( + stringResource(R.string.screen_create_account_title), + style = ElementTheme.typography.aliasScreenTitle, + ) + }, + navigationIcon = { + BackButton(onClick = onBackClick) + }, + ) + } + ) { contentPadding -> + Box( + modifier = Modifier + .padding(contentPadding) + .consumeWindowInsets(contentPadding) + .fillMaxSize() + ) { + CreateAccountWebView( + modifier = Modifier + .fillMaxSize(), + state = state, + onWebViewCreate = { webView -> + WebViewMessageInterceptor( + webView, + state.isDebugBuild, + onOpenExternalUrl = onOpenExternalUrl, + onMessage = { + state.eventSink(CreateAccountEvents.OnMessageReceived(it)) + }, + ) + } + ) + AnimatedVisibility( + visible = state.pageProgress != 100, + // Disable enter animation + enter = fadeIn(initialAlpha = 1f), + exit = fadeOut(), + ) { + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(2.dp), + progress = { state.pageProgress / 100f }, + trackColor = ElementTheme.colors.progressIndicatorTrackColor, + ) + } + } + } + + AsyncActionView( + async = state.createAction, + onSuccess = {}, + onErrorDismiss = onBackClick, + onRetry = null + ) +} + +@Composable +private fun CreateAccountWebView( + state: CreateAccountState, + onWebViewCreate: (WebView) -> Unit, + modifier: Modifier = Modifier, +) { + if (LocalInspectionMode.current) { + Box(modifier = modifier, contentAlignment = Alignment.Center) { + Text("WebView - can't be previewed") + } + } else { + AndroidView( + modifier = modifier, + factory = { context -> + WebView(context).apply { + onWebViewCreate(this) + setup(state) + } + }, + update = { webView -> + if (webView.url != state.url) { + webView.loadUrl(state.url) + } + }, + onRelease = { webView -> + webView.destroy() + } + ) + } +} + +@SuppressLint("SetJavaScriptEnabled") +private fun WebView.setup(state: CreateAccountState) { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + with(settings) { + javaScriptEnabled = true + domStorageEnabled = true + } + + webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView?, newProgress: Int) { + super.onProgressChanged(view, newProgress) + state.eventSink(CreateAccountEvents.SetPageProgress(newProgress)) + } + + override fun onJsBeforeUnload(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean { + Timber.w("onJsBeforeUnload, cancelling the dialog, we will open external links in a Custom Chrome Tab") + result?.confirm() + return true + } + } +} + +@PreviewsDayNight +@Composable +internal fun CreateAccountViewPreview(@PreviewParameter(CreateAccountStateProvider::class) state: CreateAccountState) = ElementPreview { + CreateAccountView( + state = state, + onBackClick = {}, + onOpenExternalUrl = {}, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/MessageParser.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/MessageParser.kt new file mode 100644 index 0000000000..a47bd8a83c --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/MessageParser.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.createaccount + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.auth.external.ExternalSession +import kotlinx.serialization.json.Json +import javax.inject.Inject + +interface MessageParser { + /** + * Parse the message and return the ExternalSession object, or + * throw an exception if the message is invalid. + */ + fun parse(message: String): ExternalSession +} + +@ContributesBinding(AppScope::class) +class DefaultMessageParser @Inject constructor( + private val accountProviderDataSource: AccountProviderDataSource, +) : MessageParser { + override fun parse(message: String): ExternalSession { + val parser = Json { ignoreUnknownKeys = true } + val response = parser.decodeFromString(MobileRegistrationResponse.serializer(), message) + val userId = response.userId ?: error("No user ID in response") + val homeServer = response.homeServer ?: accountProviderDataSource.flow().value.url + val accessToken = response.accessToken ?: error("No access token in response") + val deviceId = response.deviceId ?: error("No device ID in response") + return ExternalSession( + userId = userId, + homeserverUrl = homeServer, + accessToken = accessToken, + deviceId = deviceId, + refreshToken = null, + slidingSyncProxy = null + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/MobileRegistrationResponse.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/MobileRegistrationResponse.kt new file mode 100644 index 0000000000..5db50e1804 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/MobileRegistrationResponse.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.createaccount + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * For ref: + * https://github.com/element-hq/matrix-react-sdk/pull/42/files#diff-2bbba5a742004fd4e924a639ded444279f66f7ad890cb669fbc91ac6b8638c64R56 + */ +@Serializable +data class MobileRegistrationResponse( + @SerialName("user_id") + val userId: String? = null, + @SerialName("home_server") + val homeServer: String? = null, + @SerialName("access_token") + val accessToken: String? = null, + @SerialName("device_id") + val deviceId: String? = null, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/WebViewMessageInterceptor.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/WebViewMessageInterceptor.kt new file mode 100644 index 0000000000..6dfe903b09 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/createaccount/WebViewMessageInterceptor.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2023, 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.createaccount + +import android.graphics.Bitmap +import android.webkit.JavascriptInterface +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.webkit.WebViewCompat +import androidx.webkit.WebViewFeature + +class WebViewMessageInterceptor( + webView: WebView, + private val debugLog: Boolean, + private val onOpenExternalUrl: (String) -> Unit, + private val onMessage: (String) -> Unit, +) { + companion object { + // We call both the WebMessageListener and the JavascriptInterface objects in JS with this + // 'listenerName' so they can both receive the data from the WebView when + // `${LISTENER_NAME}.postMessage(...)` is called + const val LISTENER_NAME = "elementX" + } + + init { + webView.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + + // We inject this JS code when the page starts loading to attach a message listener to the window. + view?.evaluateJavascript( + """ + window.addEventListener( + "mobileregistrationresponse", + (event) => { + let json = JSON.stringify(event.detail) + ${"console.log('message sent: ' + json);".takeIf { debugLog }} + $LISTENER_NAME.postMessage(json); + }, + false, + ); + """.trimIndent(), + null + ) + } + + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + request ?: return super.shouldOverrideUrlLoading(view, request) + // Load the URL in a Chrome Custom Tab, and return true to cancel the load + onOpenExternalUrl(request.url.toString()) + return true + } + } + + // Use WebMessageListener if supported, otherwise use JavascriptInterface + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + // Create a WebMessageListener, which will receive messages from the WebView and reply to them + val webMessageListener = WebViewCompat.WebMessageListener { _, message, _, _, _ -> + onMessageReceived(message.data) + } + WebViewCompat.addWebMessageListener( + webView, + LISTENER_NAME, + setOf("*"), + webMessageListener + ) + } else { + webView.addJavascriptInterface( + object { + @JavascriptInterface + fun postMessage(json: String?) { + onMessageReceived(json) + } + }, + LISTENER_NAME, + ) + } + } + + private fun onMessageReceived(json: String?) { + // Here is where we would handle the messages from the WebView, passing them to the listener + json?.let { onMessage(it) } + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt index bd6437ddba..48fe71e0fc 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt @@ -283,7 +283,7 @@ private fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) { ErrorDialog( title = stringResource(id = CommonStrings.dialog_title_error), content = stringResource(loginError(error)), - onDismiss = onDismiss + onSubmit = onDismiss ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/web/WebClientUrlForAuthenticationRetriever.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/web/WebClientUrlForAuthenticationRetriever.kt new file mode 100644 index 0000000000..b7a30ee843 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/web/WebClientUrlForAuthenticationRetriever.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.login.impl.web + +import android.net.Uri +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.appconfig.AuthenticationConfig +import io.element.android.features.login.impl.resolver.network.WellknownAPI +import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.network.RetrofitFactory +import timber.log.Timber +import java.net.HttpURLConnection +import javax.inject.Inject + +interface WebClientUrlForAuthenticationRetriever { + suspend fun retrieve(homeServerUrl: String): String +} + +@ContributesBinding(AppScope::class) +class DefaultWebClientUrlForAuthenticationRetriever @Inject constructor( + private val retrofitFactory: RetrofitFactory, +) : WebClientUrlForAuthenticationRetriever { + override suspend fun retrieve(homeServerUrl: String): String { + if (homeServerUrl != AuthenticationConfig.MATRIX_ORG_URL) { + Timber.w("Temporary account creation flow is only supported on matrix.org") + throw AccountCreationNotSupported() + } + val wellknownApi = retrofitFactory.create(homeServerUrl) + .create(WellknownAPI::class.java) + val result = try { + wellknownApi.getElementWellKnown() + } catch (e: retrofit2.HttpException) { + throw when { + e.code() == HttpURLConnection.HTTP_NOT_FOUND -> AccountCreationNotSupported() + else -> e + } + } + val registrationHelperUrl = result.registrationHelperUrl + return if (registrationHelperUrl != null) { + Uri.parse(registrationHelperUrl) + .buildUpon() + .appendQueryParameter("hs_url", homeServerUrl) + .build() + .toString() + } else { + throw AccountCreationNotSupported() + } + } +} diff --git a/features/login/impl/src/main/res/values-be/translations.xml b/features/login/impl/src/main/res/values-be/translations.xml index f8e675854e..777acc423d 100644 --- a/features/login/impl/src/main/res/values-be/translations.xml +++ b/features/login/impl/src/main/res/values-be/translations.xml @@ -78,10 +78,4 @@ "Тут будуць захоўвацца вашыя размовы - сапраўды гэтак жа, як вы выкарыстоўваеце паштовага правайдара для захоўвання сваіх лістоў." "Вы збіраецеся ўвайсці ў %1$s" "Вы збіраецеся стварыць уліковы запіс на %1$s" - "Зараз існуе высокі попыт на %1$s на %2$s. Калі ласка, вярніцеся ў праграму праз некалькі дзён і паспрабуйце зноў. - -Дзякуй за цярпенне!" - "Вітаем у %1$s!" - "Амаль гатова." - "Вы зарэгістраваны." diff --git a/features/login/impl/src/main/res/values-bg/translations.xml b/features/login/impl/src/main/res/values-bg/translations.xml index cfc51c3f8c..eced3ef828 100644 --- a/features/login/impl/src/main/res/values-bg/translations.xml +++ b/features/login/impl/src/main/res/values-bg/translations.xml @@ -24,5 +24,4 @@ "Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли." "На път сте да влезете в %1$s" "На път сте да създадете акаунт в %1$s" - "Добре дошли в %1$s!" diff --git a/features/login/impl/src/main/res/values-cs/translations.xml b/features/login/impl/src/main/res/values-cs/translations.xml index c0f00a87af..d65a5571e4 100644 --- a/features/login/impl/src/main/res/values-cs/translations.xml +++ b/features/login/impl/src/main/res/values-cs/translations.xml @@ -78,10 +78,4 @@ Zkuste se přihlásit ručně nebo naskenujte QR kód pomocí jiného zařízen "Zde budou uloženy vaše konverzace - podobně jako u poskytovatele e-mailových služeb uchováváte své e-maily." "Chystáte se přihlásit do služby %1$s" "Chystáte se vytvořit účet na %1$s" - "Na %2$s je momentálně vysoká poptávka po %1$s. Vraťte se do aplikace za pár dní a zkuste to znovu. - -Díky za trpělivost!" - "Vítá vás %1$s!" - "Jste v pořadníku!" - "Jdete do toho!" diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml index 2399ec86ba..0ead5a8147 100644 --- a/features/login/impl/src/main/res/values-de/translations.xml +++ b/features/login/impl/src/main/res/values-de/translations.xml @@ -21,6 +21,7 @@ "Du kannst nur eine Verbindung zu einem vorhandenen Server herstellen, der Sliding Sync unterstützt. Dein Homeserver-Administrator muss das konfigurieren. %1$s" "Wie lautet die Adresse deines Servers?" "Wähle deinen Server aus" + "Konto erstellen" "Dieses Konto wurde deaktiviert." "Falscher Benutzername und/oder Passwort" "Dies ist keine gültige Benutzerkennung. Erwartetes Format: \'@user:homeserver.org\'" @@ -78,10 +79,4 @@ Versuche, dich manuell anzumelden, oder scanne den QR-Code mit einem anderen Ger "Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würden." "Du bist dabei, dich bei %1$s anzumelden" "Du bist dabei, ein Konto auf %1$s zu erstellen" - "Derzeit besteht eine hohe Nachfrage nach %1$s auf %2$s. Kehre in ein paar Tagen zur App zurück und versuche es erneut. - -Danke für deine Geduld!" - "Willkommen bei %1$s!" - "Du bist fast am Ziel." - "Du bist dabei." diff --git a/features/login/impl/src/main/res/values-el/translations.xml b/features/login/impl/src/main/res/values-el/translations.xml index 8ff0227168..b0ad469732 100644 --- a/features/login/impl/src/main/res/values-el/translations.xml +++ b/features/login/impl/src/main/res/values-el/translations.xml @@ -21,6 +21,7 @@ "Μπορείτε να συνδεθείς μόνο σε υπάρχοντα διακομιστή που υποστηρίζει Sliding sync. Ο διαχειριστής του οικιακού διακομιστή σου θα πρέπει να το ρυθμίσει. %1$s" "Ποια είναι η διεύθυνση του διακομιστή σου;" "Επέλεξε το διακομιστή σου" + "Δημιουργία λογαριασμού" "Αυτός ο λογαριασμός έχει απενεργοποιηθεί." "Λανθασμένο όνομα χρήστη ή κωδικός πρόσβασης" "Αυτό δεν είναι έγκυρο αναγνωριστικό χρήστη. Αναμενόμενη μορφή: \'@χρήστης:homeserver.org\'" @@ -78,10 +79,4 @@ "Εδώ θα ζουν οι συνομιλίες σου - όπως θα χρησιμοποιούσες έναν πάροχο email για να διατηρήσεις τα email σου." "Πρόκειται να συνδεθείς στο %1$s" "Πρόκειται να δημιουργήσεις έναν λογαριασμό στο %1$s" - "Υπάρχει μεγάλη ζήτηση για το %1$s στον %2$s αυτή τη στιγμή. Επέστρεψε στην εφαρμογή σε λίγες μέρες και δοκίμασε ξανά. - -Ευχαριστώ για την υπομονή σου!" - "Καλώς ήρθες στο %1$s!" - "Σχεδόν τα κατάφερες." - "Είσαι μέσα." diff --git a/features/login/impl/src/main/res/values-es/translations.xml b/features/login/impl/src/main/res/values-es/translations.xml index c7537c9d7e..60324ae89d 100644 --- a/features/login/impl/src/main/res/values-es/translations.xml +++ b/features/login/impl/src/main/res/values-es/translations.xml @@ -37,10 +37,4 @@ "Aquí es donde se alojarán tus conversaciones — justo como utilizarías un proveedor de correo electrónico para guardar tus correos electrónicos." "Estás a punto de iniciar sesión en %1$s" "Estás a punto de crear una cuenta en %1$s" - "Hay una gran demanda para %1$s en %2$s en este momento. Vuelve a la aplicación en unos días e inténtalo de nuevo. - -¡Gracias por tu paciencia!" - "¡Bienvenido a %1$s!" - "Ya casi has terminado." - "Estás dentro." diff --git a/features/login/impl/src/main/res/values-et/translations.xml b/features/login/impl/src/main/res/values-et/translations.xml index 0558b349b4..36ce791bb2 100644 --- a/features/login/impl/src/main/res/values-et/translations.xml +++ b/features/login/impl/src/main/res/values-et/translations.xml @@ -21,6 +21,7 @@ "Sa saad luua ühendust vaid olemasoleva serveriga, mis toetab Sliding sync režiimi. Sinu koduserveri haldur peaks selle seadistama. %1$s" "Mis on sinu koduserveri aadress?" "Vali oma server" + "Loo kasutajakonto" "Konto on kasutusest eemaldatud." "Vigane kasutajanimi ja/või salasõna" "See ei ole korrektne kasutajanimi. Õige vorming on: „@kasutaja:koduserver.ee“" @@ -78,10 +79,4 @@ Proovi käsitsi sisselogimist või skaneeri QR-koodi mõne muu seadmega.""See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postitenuse pakkujat." "Sa oled sisselogimas koduserverisse %1$s" "Sa oled loomas kasutajakontot koduserveris %1$s" - "%1$s kasutamiseks %2$s koduserveris on hetkel palju huvilisi. Proovi seda samast rakendusest mõne päeva pärast. - -Täname kannatlikkuse eest!" - "Tere tulemast rakendusse %1$s!" - "Peaaegu olemas." - "Oled nüüd jututoas." diff --git a/features/login/impl/src/main/res/values-fr/translations.xml b/features/login/impl/src/main/res/values-fr/translations.xml index 49e513c2a8..b075da9fda 100644 --- a/features/login/impl/src/main/res/values-fr/translations.xml +++ b/features/login/impl/src/main/res/values-fr/translations.xml @@ -21,6 +21,7 @@ "Vous ne pouvez vous connecter qu’à un serveur existant qui prend en charge le sliding sync. L’administrateur de votre serveur d’accueil devra le configurer. %1$s" "Quelle est l’adresse de votre serveur ?" "Choisissez votre serveur" + "Créer un compte" "Ce compte a été désactivé." "Nom d’utilisateur et/ou mot de passe incorrects" "Il ne s’agit pas d’un identifiant utilisateur valide. Format attendu : « @user:homeserver.org »" @@ -76,10 +77,4 @@ "C’est ici que vos conversations seront enregistrées, comme vous le feriez avec un fournisseur de messagerie pour conserver vos e-mails." "Vous êtes sur le point de vous connecter à %1$s" "Vous êtes sur le point de créer un compte sur %1$s" - "Il y a une forte demande pour %1$s sur %2$s à l’heure actuelle. Revenez sur l’application dans quelques jours et réessayez. - -Merci pour votre patience !" - "Bienvenue dans %1$s !" - "Vous y êtes presque." - "Vous y êtes." diff --git a/features/login/impl/src/main/res/values-hu/translations.xml b/features/login/impl/src/main/res/values-hu/translations.xml index 8d8bf40f3f..1fb7f12d0c 100644 --- a/features/login/impl/src/main/res/values-hu/translations.xml +++ b/features/login/impl/src/main/res/values-hu/translations.xml @@ -21,6 +21,7 @@ "Csak olyan meglévő kiszolgálóhoz csatlakozhat, amely támogatja a Sliding sync protokollt. Ezt a Matrix-kiszolgáló adminisztrátorának kell beállítania. %1$s" "Mi a kiszolgálója címe?" "Válassza ki a kiszolgálóját" + "Fiók létrehozása" "Ez a fiók deaktiválva lett." "Helytelen felhasználónév vagy jelszó" "Ez nem érvényes felhasználóazonosító. A várt formátum: „@user:homeserver.org”" @@ -78,10 +79,4 @@ Próbáljon meg kézileg bejelentkezni, vagy olvassa be a QR-kódot egy másik e "Itt lesznek a beszélgetései – ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez." "Hamarosan bejelentkezik ebbe: %1$s" "Hamarosan létrehoz egy fiókot ezen: %1$s" - "Jelenleg nagy a kereslet a(z) %2$s oldalon futó %1$s iránt. Térjen vissza néhány nap múlva az alkalmazáshoz, és próbálja újra. - -Köszönjük a türelmét!" - "Üdvözli az %1$s!" - "Már majdnem kész van." - "Bent van." diff --git a/features/login/impl/src/main/res/values-in/translations.xml b/features/login/impl/src/main/res/values-in/translations.xml index 97d84ff1f4..749406da04 100644 --- a/features/login/impl/src/main/res/values-in/translations.xml +++ b/features/login/impl/src/main/res/values-in/translations.xml @@ -78,10 +78,4 @@ Coba masuk secara manual, atau pindai kode QR dengan perangkat lain." "Di sinilah percakapan Anda akan berlangsung — sama seperti Anda menggunakan penyedia surel untuk menyimpan surel Anda." "Anda akan masuk ke %1$s" "Anda akan membuat akun di %1$s" - "Ada permintaan tinggi untuk %1$s di %2$s saat ini. Kembalilah ke aplikasi dalam beberapa hari dan coba lagi. - -Terima kasih atas kesabaran Anda!" - "Selamat datang di %1$s!" - "Anda hampir selesai." - "Anda sudah masuk." diff --git a/features/login/impl/src/main/res/values-it/translations.xml b/features/login/impl/src/main/res/values-it/translations.xml index dcd9a7c2ae..eaa5313b03 100644 --- a/features/login/impl/src/main/res/values-it/translations.xml +++ b/features/login/impl/src/main/res/values-it/translations.xml @@ -78,10 +78,4 @@ Prova ad accedere manualmente o scansiona il codice QR con un altro dispositivo. "Qui è dove vivranno le tue conversazioni — proprio come useresti un fornitore di posta elettronica per conservare le tue email." "Stai per accedere a %1$s" "Stai per creare un account su %1$s" - "Al momento c\'è una grande richiesta per %1$s su %2$s. Torna a visitare l\'app tra qualche giorno e riprova. - -Grazie per la pazienza!" - "Benvenuti in %1$s!" - "Ci sei quasi." - "Sei dentro." diff --git a/features/login/impl/src/main/res/values-ka/translations.xml b/features/login/impl/src/main/res/values-ka/translations.xml index 84e97e0a34..6fc49e7b41 100644 --- a/features/login/impl/src/main/res/values-ka/translations.xml +++ b/features/login/impl/src/main/res/values-ka/translations.xml @@ -34,10 +34,4 @@ "აქ იქნება თქვენი საუბრები - ისევე, როგორც თქვენ ელ. ფოსტაში ინახება თქვენი ელ.წერილები." "თქვენ აპირებთ შესვლას %1$s-ში" "თქვენ აპირებთ ანგარიშის შექმნას %1$s-ში" - "ახლა დიდი მოთხოვნაა %1$s-ზე %2$s-ში. დაბრუნდით რამდენიმე დღეში და სცადეთ ერთხელაც. - -მადლობა მოთმენისათვის!" - "კეთილი იყოს თქვენი მობრძანება %1$s-ში!" - "თითქმის მზადაა." - "თქვენ შეხვედით." diff --git a/features/login/impl/src/main/res/values-nl/translations.xml b/features/login/impl/src/main/res/values-nl/translations.xml index 80fbe57afb..627f5b873e 100644 --- a/features/login/impl/src/main/res/values-nl/translations.xml +++ b/features/login/impl/src/main/res/values-nl/translations.xml @@ -14,6 +14,8 @@ "Gebruik een andere accountprovider, zoals je eigen privéserver of een zakelijke account." "Wijzig accountprovider" "We konden deze homeserver niet bereiken. Controleer of je de homeserver-URL juist hebt ingevoerd. Als de URL juist is, neem dan contact op met de beheerder van je homeserver voor verdere hulp." + "Sliding sync is niet beschikbaar vanwege een probleem in het well-known bestand: +%1$s" "Deze server ondersteunt op dit moment geen sliding sync." "Homeserver-URL" "Je kunt alleen verbinding maken met een bestaande server die sliding sync ondersteunt. De beheerder van de homeserver moet dit configureren. %1$s" @@ -28,17 +30,45 @@ "Matrix is een open netwerk voor veilige, gedecentraliseerde communicatie." "Welkom terug!" "Inloggen bij %1$s" + "Een beveiligde verbinding tot stand brengen" + "Er kon geen beveiligde verbinding worden gemaakt met het nieuwe apparaat. Je bestaande apparaten zijn nog steeds veilig en je hoeft je daarover geen zorgen te maken." + "Wat nu?" + "Probeer opnieuw in te loggen met een QR-code voor het geval dit een netwerkprobleem was" + "Als je hetzelfde probleem ondervindt, probeer dan een ander wifi-netwerk of gebruik je mobiele data in plaats van wifi." + "Als dat niet werkt, log dan handmatig in" + "Verbinding niet veilig" + "Daar word je gevraagd om de twee cijfers in te voeren die op dit apparaat worden weergegeven." + "Voer het onderstaande nummer in op je andere apparaat" + "De aanmelding is geannuleerd op het andere apparaat." + "Login verzoek geannuleerd" + "De aanmelding is geweigerd op het andere apparaat." + "Aanmelden geweigerd" + "Aanmelden is verlopen. Probeer het opnieuw." + "De aanmelding was niet op tijd voltooid" + "QR-code wordt niet ondersteund" + "Klaar om te scannen" + "Open %1$s op een desktopapparaat" + "Klik op je afbeelding" + "Selecteer %1$s" + "“Nieuw apparaat koppelen”" + "Scan de QR-code met dit apparaat" + "Open %1$s op een ander apparaat om de QR-code te krijgen" + "Gebruik de QR-code die op het andere apparaat wordt weergegeven." "Probeer het opnieuw" + "Verkeerde QR-code" + "Ga naar camera-instellingen" + "Je moet %1$s toestemming geven om de camera van je apparaat te gebruiken om verder te gaan." + "Cameratoegang toestaan om de QR-code te scannen" + "Scan de QR-code" + "Opnieuw beginnen" + "Er is een onverwachte fout opgetreden. Probeer het opnieuw." + "Aan het wachten op je andere apparaat" + "Je accountprovider kan om de volgende code vragen om de aanmelding te verifiëren." + "Je verificatiecode" "Accountprovider wijzigen" "Een privéserver voor medewerkers van Element." "Matrix is een open netwerk voor veilige, gedecentraliseerde communicatie." "Dit is waar je gesprekken zullen worden bewaard — net zoals je een e-mailprovider zou gebruiken om je e-mails te bewaren." "Je staat op het punt je aan te melden bij %1$s" "Je staat op het punt een account aan te maken op %1$s" - "Er is momenteel veel vraag naar %1$s op %2$s. Kom over een paar dagen terug naar de app en probeer het opnieuw. - -Bedankt voor je geduld!" - "Welkom bij %1$s!" - "Je bent er bijna." - "Je bent binnen." diff --git a/features/login/impl/src/main/res/values-pl/translations.xml b/features/login/impl/src/main/res/values-pl/translations.xml index 4d23e15fe8..6990121a43 100644 --- a/features/login/impl/src/main/res/values-pl/translations.xml +++ b/features/login/impl/src/main/res/values-pl/translations.xml @@ -78,10 +78,4 @@ Spróbuj zalogować się ręcznie lub zeskanuj kod QR na innym urządzeniu.""Tutaj będą przechowywane Twoje konwersacje - w podobnej formie jak wiadomości widnieją na skrzynce e-mail." "Zamierzasz się zalogować do %1$s" "Zamierzasz utworzyć konto na %1$s" - "Obecnie istnieje duże zapotrzebowanie na %1$s na %2$s. Wróć do aplikacji za kilka dni i spróbuj ponownie. - -Dziękujemy za Twoją cierpliwość!" - "Witamy w %1$s!" - "Już prawie gotowe!" - "Witamy!" diff --git a/features/login/impl/src/main/res/values-pt-rBR/translations.xml b/features/login/impl/src/main/res/values-pt-rBR/translations.xml index 4cb9b4f7b0..7b753ffb9f 100644 --- a/features/login/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/login/impl/src/main/res/values-pt-rBR/translations.xml @@ -34,10 +34,4 @@ "Aqui é onde suas conversas vão ficar — assim como você usa um provedor de e-mails para manter seus e-mails." "Você está prestes a fazer login em %1$s" "Você está prestes a criar uma conta em %1$s" - "Há uma grande demanda por %1$s sobre %2$s no momento. Volte ao aplicativo em alguns dias e tente novamente. - -Obrigado pela sua paciência!" - "Bem-vindo ao %1$s!" - "Você está quase lá." - "Você está dentro." diff --git a/features/login/impl/src/main/res/values-pt/translations.xml b/features/login/impl/src/main/res/values-pt/translations.xml index f5feb0cbdd..f03b48c5dd 100644 --- a/features/login/impl/src/main/res/values-pt/translations.xml +++ b/features/login/impl/src/main/res/values-pt/translations.xml @@ -78,10 +78,4 @@ Tenta iniciar a sessão manualmente ou digitaliza o código QR com outro disposi "É aqui que as tuas conversas vão ficar — tal como num serviço de e-mail." "Irás iniciar sessão em %1$s" "Irás criar uma conta em %1$s" - "Há uma grande procura pela %1$s no %2$s, de momento. Volta à aplicação daqui a uns dias e tenta novamente. - -Obrigado!" - "Bem-vindo à %1$s!" - "Estás quase lá." - "Estás dentro." diff --git a/features/login/impl/src/main/res/values-ro/translations.xml b/features/login/impl/src/main/res/values-ro/translations.xml index 1ff3fe2436..261f16d9f6 100644 --- a/features/login/impl/src/main/res/values-ro/translations.xml +++ b/features/login/impl/src/main/res/values-ro/translations.xml @@ -78,10 +78,4 @@ "Aici vor trăi conversațiile dvs. - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile." "Sunteți pe cale să vă conectați la %1$s" "Sunteți pe cale să creați un cont pe %1$s" - "Există o cerere mare pentru %1$s pentru %2$s în acest moment. Reveniți la aplicație în câteva zile și încercați din nou. - -Vă mulțumim pentru răbdare!" - "Bun venit la%1$s!" - "Sunteți pe lista de așteptare" - "Sunteți conectat!" diff --git a/features/login/impl/src/main/res/values-ru/translations.xml b/features/login/impl/src/main/res/values-ru/translations.xml index c64d683a12..40f3f9b5df 100644 --- a/features/login/impl/src/main/res/values-ru/translations.xml +++ b/features/login/impl/src/main/res/values-ru/translations.xml @@ -21,6 +21,7 @@ "Вы можете подключиться только к существующему серверу, поддерживающему sliding sync. Администратору домашнего сервера потребуется настроить его. %1$s" "Какой адрес у вашего сервера?" "Выберите свой сервер" + "Создать учетную запись" "Данная учетная запись была деактивирована." "Неверное имя пользователя и/или пароль" "Это не корректный идентификатор пользователя. Ожидаемый формат: \'@user:homeserver.org\'" @@ -78,10 +79,4 @@ "Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем." "Вы собираетесь войти в %1$s" "Вы собираетесь создать учетную запись на %1$s" - "В настоящее время существует высокий спрос на %1$s на %2$s. Вернитесь в приложение через несколько дней и попробуйте снова. - -Спасибо за терпение!" - "Добро пожаловать в %1$s!" - "Почти готово." - "Вы зарегистрированы." diff --git a/features/login/impl/src/main/res/values-sk/translations.xml b/features/login/impl/src/main/res/values-sk/translations.xml index fea7879d01..7cadc5ecb8 100644 --- a/features/login/impl/src/main/res/values-sk/translations.xml +++ b/features/login/impl/src/main/res/values-sk/translations.xml @@ -21,6 +21,7 @@ "Môžete sa pripojiť iba k existujúcemu serveru, ktorý podporuje kĺzavú synchronizáciu. Správca domovského servera ju bude musieť nakonfigurovať. %1$s" "Aká je adresa vášho servera?" "Vyberte svoj server" + "Vytvoriť účet" "Tento účet bol deaktivovaný." "Nesprávne používateľské meno a/alebo heslo" "Toto nie je platný identifikátor používateľa. Očakávaný formát: \'@pouzivatel:homeserver.sk\'" @@ -78,10 +79,4 @@ Skúste sa prihlásiť manuálne alebo naskenujte QR kód pomocou iného zariade "Tu budú žiť vaše konverzácie - podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov." "Chystáte sa prihlásiť do %1$s" "Chystáte sa vytvoriť účet na %1$s" - "Momentálne je veľký dopyt po %1$s na %2$s. Vráťte sa do aplikácie za pár dní a skúste to znova. - -Ďakujeme za trpezlivosť!" - "Vitajte v %1$s!" - "Ste na čakanej listine!" - "Ste dnu!" diff --git a/features/login/impl/src/main/res/values-sv/translations.xml b/features/login/impl/src/main/res/values-sv/translations.xml index d50610adcc..de290469ca 100644 --- a/features/login/impl/src/main/res/values-sv/translations.xml +++ b/features/login/impl/src/main/res/values-sv/translations.xml @@ -78,10 +78,4 @@ Prova att logga in manuellt eller skanna QR-koden med en annan enhet." "Det är här dina konversationer kommer att sparas - precis som du skulle använda en e-postleverantör för att spara dina e-brev." "Du är på väg att logga in på %1$s" "Du är på väg att skapa ett konto på %1$s" - "Det finns en stor efterfrågan på %1$s på %2$s just nu. Kom tillbaka till appen om några dagar och försök igen. - -Tack för ditt tålamod!" - "Välkommen till %1$s!" - "Du är nästan framme." - "Du är inne." diff --git a/features/login/impl/src/main/res/values-uk/translations.xml b/features/login/impl/src/main/res/values-uk/translations.xml index 9cb6eb1f5f..df436a9a69 100644 --- a/features/login/impl/src/main/res/values-uk/translations.xml +++ b/features/login/impl/src/main/res/values-uk/translations.xml @@ -78,10 +78,4 @@ "Тут будуть зберігатися Ваші розмови - так само, як Ви використовуєте поштову скриньку для зберігання своїх електронних листів." "Ви збираєтесь увійти в %1$s" "Ви збираєтеся створити обліковий запис на %1$s" - "На цей момент існує високий попит на %1$s в %2$s. Поверніться до застосунку через кілька днів і спробуйте ще раз. - -Дякуємо за терпіння!" - "Ласкаво просимо до %1$s!" - "Майже готово." - "Готово." diff --git a/features/login/impl/src/main/res/values-uz/translations.xml b/features/login/impl/src/main/res/values-uz/translations.xml index 67dc5129d7..db16e17b09 100644 --- a/features/login/impl/src/main/res/values-uz/translations.xml +++ b/features/login/impl/src/main/res/values-uz/translations.xml @@ -33,10 +33,4 @@ "Bu sizning suhbatlaringiz yashaydigan joy - xuddi siz elektron pochta xabarlaringizni saqlash uchun elektron pochta provayderidan foydalanganingiz kabi." "Siz tizimga kirmoqchisiz%1$s" "Hisob yaratmoqchisiz%1$s" - "Hozirgi paytda %2$sga %1$sda talab yuqori. Bir necha kundan keyin ilovaga qayting va qaytadan urining. - -Sabr-toqatingiz uchun rahmat!" - "%1$sga Xush kelibsiz!" - "Siz deyarli keldingiz." - "Siz kirdingiz." diff --git a/features/login/impl/src/main/res/values-zh-rTW/translations.xml b/features/login/impl/src/main/res/values-zh-rTW/translations.xml index d345095c4b..b66cfca31f 100644 --- a/features/login/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/login/impl/src/main/res/values-zh-rTW/translations.xml @@ -29,5 +29,4 @@ "您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。" "您即將登入 %1$s" "您即將在 %1$s 建立帳號" - "歡迎使用 %1$s!" diff --git a/features/login/impl/src/main/res/values-zh/translations.xml b/features/login/impl/src/main/res/values-zh/translations.xml index 0afd6dad93..2f24aacf69 100644 --- a/features/login/impl/src/main/res/values-zh/translations.xml +++ b/features/login/impl/src/main/res/values-zh/translations.xml @@ -78,10 +78,4 @@ "这是您的对话将进行的地方,就像您使用电子邮件提供商来保存电子邮件一样。" "即将登录 %1$s" "即将在 %1$s 上创建一个账户" - "目前 %1$s 上 %2$s 的负载很大。过几天再回来试试吧。 - -感谢您的耐心!" - "欢迎使用 %1$s" - "马上就好。" - "您已加入。" diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index cade81c4f2..a46b083d63 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -21,6 +21,7 @@ "You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$s" "What is the address of your server?" "Select your server" + "Create account" "This account has been deactivated." "Incorrect username and/or password" "This is not a valid user identifier. Expected format: ‘@user:homeserver.org’" @@ -78,10 +79,4 @@ Try signing in manually, or scan the QR code with another device." "This is where your conversations will live — just like you would use an email provider to keep your emails." "You’re about to sign in to %1$s" "You’re about to create an account on %1$s" - "There\'s a high demand for %1$s on %2$s at the moment. Come back to the app in a few days and try again. - -Thanks for your patience!" - "Welcome to %1$s!" - "You’re almost there." - "You\'re in." diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt index 1609ffef3c..1d70580e4d 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt @@ -13,7 +13,10 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported import io.element.android.features.login.impl.util.defaultAccountProvider +import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever +import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.test.A_HOMESERVER @@ -255,17 +258,109 @@ class ConfirmAccountProviderPresenterTest { } } + @Test + fun `present - confirm account creation without oidc and without url generates an error`() = runTest { + val authenticationService = FakeMatrixAuthenticationService() + authenticationService.givenHomeserver(A_HOMESERVER) + val presenter = createConfirmAccountProviderPresenter( + params = ConfirmAccountProviderPresenter.Params(isAccountCreation = true), + matrixAuthenticationService = authenticationService, + webClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever { + throw AccountCreationNotSupported() + }, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvents.Continue) + skipItems(1) // Loading + // Check an error was returned + val submittedState = awaitItem() + assertThat(submittedState.loginFlow.errorOrNull()).isInstanceOf(AccountCreationNotSupported::class.java) + // Assert the error is then cleared + submittedState.eventSink(ConfirmAccountProviderEvents.ClearError) + val clearedState = awaitItem() + assertThat(clearedState.loginFlow).isEqualTo(AsyncData.Uninitialized) + } + } + + @Test + fun `present - confirm account creation with oidc is successful`() = runTest { + val authenticationService = FakeMatrixAuthenticationService() + authenticationService.givenHomeserver(A_HOMESERVER_OIDC) + val presenter = createConfirmAccountProviderPresenter( + params = ConfirmAccountProviderPresenter.Params(isAccountCreation = true), + matrixAuthenticationService = authenticationService, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvents.Continue) + skipItems(1) // Loading + val submittedState = awaitItem() + assertThat(submittedState.loginFlow).isInstanceOf(AsyncData.Success::class.java) + assertThat(submittedState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) + } + } + + @Test + fun `present - confirm account creation with oidc and url continues with oidc`() = runTest { + val aUrl = "aUrl" + val authenticationService = FakeMatrixAuthenticationService() + authenticationService.givenHomeserver(A_HOMESERVER_OIDC) + val presenter = createConfirmAccountProviderPresenter( + params = ConfirmAccountProviderPresenter.Params(isAccountCreation = true), + matrixAuthenticationService = authenticationService, + webClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever { aUrl }, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvents.Continue) + skipItems(1) // Loading + val submittedState = awaitItem() + assertThat(submittedState.loginFlow).isInstanceOf(AsyncData.Success::class.java) + assertThat(submittedState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) + } + } + + @Test + fun `present - confirm account creation without oidc and with url continuing with url`() = runTest { + val aUrl = "aUrl" + val authenticationService = FakeMatrixAuthenticationService() + authenticationService.givenHomeserver(A_HOMESERVER) + val presenter = createConfirmAccountProviderPresenter( + params = ConfirmAccountProviderPresenter.Params(isAccountCreation = true), + matrixAuthenticationService = authenticationService, + webClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever { aUrl }, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvents.Continue) + skipItems(1) // Loading + val submittedState = awaitItem() + assertThat(submittedState.loginFlow.dataOrNull()).isEqualTo(LoginFlow.AccountCreationFlow(aUrl)) + } + } + private fun createConfirmAccountProviderPresenter( params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(), matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), defaultOidcActionFlow: DefaultOidcActionFlow = DefaultOidcActionFlow(), defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(), + webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), ) = ConfirmAccountProviderPresenter( params = params, accountProviderDataSource = accountProviderDataSource, authenticationService = matrixAuthenticationService, oidcActionFlow = defaultOidcActionFlow, defaultLoginUserStory = defaultLoginUserStory, + webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever ) } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenterTest.kt new file mode 100644 index 0000000000..eb89e80638 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/CreateAccountPresenterTest.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.createaccount + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.login.impl.DefaultLoginUserStory +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.auth.external.ExternalSession +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class CreateAccountPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.url).isEqualTo("aUrl") + assertThat(initialState.pageProgress).isEqualTo(0) + assertThat(initialState.createAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(initialState.isDebugBuild).isTrue() + } + } + + @Test + fun `present - set up progress update the state`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(CreateAccountEvents.SetPageProgress(33)) + assertThat(awaitItem().pageProgress).isEqualTo(33) + } + } + + @Test + fun `present - receiving a message not able to be parsed change the state to error`() = runTest { + val presenter = createPresenter( + messageParser = FakeMessageParser { error("An error") } + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(CreateAccountEvents.OnMessageReceived("")) + assertThat(awaitItem().createAction).isInstanceOf(AsyncAction.Failure::class.java) + } + } + + @Test + fun `present - receiving a message containing isTrusted is ignored`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(CreateAccountEvents.OnMessageReceived("isTrusted")) + } + } + + @Test + fun `present - receiving a message able to be parsed change the state to success`() = runTest { + val defaultLoginUserStory = DefaultLoginUserStory() + defaultLoginUserStory.setLoginFlowIsDone(false) + assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse() + val lambda = lambdaRecorder { _ -> anExternalSession() } + val presenter = createPresenter( + authenticationService = FakeMatrixAuthenticationService( + importCreatedSessionLambda = { Result.success(A_SESSION_ID) } + ), + messageParser = FakeMessageParser(lambda), + defaultLoginUserStory = defaultLoginUserStory, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(CreateAccountEvents.OnMessageReceived("aMessage")) + assertThat(awaitItem().createAction.isLoading()).isTrue() + assertThat(awaitItem().createAction.dataOrNull()).isEqualTo(A_SESSION_ID) + } + lambda.assertions().isCalledOnce().with(value("aMessage")) + assertThat(defaultLoginUserStory.loginFlowIsDone.value).isTrue() + } + + @Test + fun `present - receiving a message able to be parsed but error in importing change the state to error`() = runTest { + val defaultLoginUserStory = DefaultLoginUserStory() + defaultLoginUserStory.setLoginFlowIsDone(false) + assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse() + val presenter = createPresenter( + authenticationService = FakeMatrixAuthenticationService( + importCreatedSessionLambda = { Result.failure(AN_EXCEPTION) } + ), + messageParser = FakeMessageParser { anExternalSession() } + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(CreateAccountEvents.OnMessageReceived("")) + assertThat(awaitItem().createAction.isLoading()).isTrue() + assertThat(awaitItem().createAction.errorOrNull()).isNotNull() + } + assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse() + } + + private fun createPresenter( + url: String = "aUrl", + authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), + defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(), + messageParser: MessageParser = FakeMessageParser(), + buildMeta: BuildMeta = aBuildMeta(), + ) = CreateAccountPresenter( + url = url, + authenticationService = authenticationService, + defaultLoginUserStory = defaultLoginUserStory, + messageParser = messageParser, + buildMeta = buildMeta, + ) +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/DefaultMessageParserTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/DefaultMessageParserTest.kt new file mode 100644 index 0000000000..44ccb5f0fb --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/DefaultMessageParserTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.createaccount + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.util.defaultAccountProvider +import io.element.android.libraries.matrix.api.auth.external.ExternalSession +import kotlinx.serialization.SerializationException +import org.junit.Assert.assertThrows +import org.junit.Test + +class DefaultMessageParserTest { + private val validMessage = """ + { + "user_id": "user_id", + "home_server": "home_server", + "access_token": "access_token", + "device_id": "device_id" + } + """.trimIndent() + + @Test + fun `DefaultMessageParser is able to parse correct message`() { + val sut = DefaultMessageParser( + AccountProviderDataSource() + ) + assertThat(sut.parse(validMessage)).isEqualTo( + anExternalSession( + homeserverUrl = "home_server", + ) + ) + } + + @Test + fun `DefaultMessageParser should throw Exception in case of error`() { + val sut = DefaultMessageParser( + AccountProviderDataSource() + ) + // kotlinx.serialization.json.internal.JsonDecodingException + assertThrows(SerializationException::class.java) { sut.parse("invalid json") } + // missing userId + assertThrows(IllegalStateException::class.java) { sut.parse(validMessage.replace(""""user_id": "user_id",""", "")) } + // missing accessToken + assertThrows(IllegalStateException::class.java) { sut.parse(validMessage.replace(""""access_token": "access_token",""", "")) } + // missing deviceId + assertThrows(IllegalStateException::class.java) { + sut.parse( + validMessage + .replace(""""access_token": "access_token",""", """"access_token": "access_token"""") + .replace(""""device_id": "device_id"""", "") + ) + } + } + + @Test + fun `DefaultMessageParser should be successful even is homeserver url is missing`() { + val sut = DefaultMessageParser( + AccountProviderDataSource() + ) + // missing homeServer + assertThat(sut.parse(validMessage.replace(""""home_server": "home_server",""", ""))).isEqualTo( + anExternalSession( + homeserverUrl = defaultAccountProvider.url, + ) + ) + } +} + +internal fun anExternalSession( + homeserverUrl: String = "home_server", +) = ExternalSession( + userId = "user_id", + homeserverUrl = homeserverUrl, + accessToken = "access_token", + deviceId = "device_id", + refreshToken = null, + slidingSyncProxy = null +) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/FakeMessageParser.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/FakeMessageParser.kt new file mode 100644 index 0000000000..4207d6b102 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/createaccount/FakeMessageParser.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.createaccount + +import io.element.android.libraries.matrix.api.auth.external.ExternalSession +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeMessageParser( + private val parseResult: (String) -> ExternalSession = { lambdaError() } +) : MessageParser { + override fun parse(message: String): ExternalSession { + return parseResult(message) + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/web/FakeWebClientUrlForAuthenticationRetriever.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/web/FakeWebClientUrlForAuthenticationRetriever.kt new file mode 100644 index 0000000000..c94d114be5 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/web/FakeWebClientUrlForAuthenticationRetriever.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.login.impl.web + +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeWebClientUrlForAuthenticationRetriever( + private val retrieveLambda: suspend (homeServerUrl: String) -> String = { lambdaError() } +) : WebClientUrlForAuthenticationRetriever { + override suspend fun retrieve(homeServerUrl: String): String { + return retrieveLambda(homeServerUrl) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index bad7c2eda8..361dc5ffe1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -246,6 +246,9 @@ fun MessagesView( state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event)) }, onEmojiReactionClick = ::onEmojiReactionClick, + onVerifiedUserSendFailureClick = { event -> + state.timelineState.eventSink(TimelineEvents.ComputeVerifiedUserSendFailure(event)) + }, ) CustomReactionBottomSheet( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index f7e50904e1..ea57345198 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -22,6 +22,8 @@ import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEn import io.element.android.features.messages.impl.UserEventPermissions import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor +import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure +import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory import io.element.android.features.messages.impl.emojis.RecentEmojiDataSource import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent @@ -58,6 +60,7 @@ class DefaultActionListPresenter @AssistedInject constructor( private val recentEmojiDataSource: RecentEmojiDataSource, private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled, private val room: MatrixRoom, + private val userSendFailureFactory: VerifiedUserSendFailureFactory, ) : ActionListPresenter { @AssistedFactory @ContributesBinding(RoomScope::class) @@ -117,13 +120,15 @@ class DefaultActionListPresenter @AssistedInject constructor( isEventPinned = pinnedEventIds.contains(timelineItem.eventId), ) - val displayEmojiReactions = usersEventPermissions.canSendReaction && - timelineItem.content.canReact() - if (actions.isNotEmpty() || displayEmojiReactions) { + val verifiedUserSendFailure = userSendFailureFactory.create(timelineItem.localSendState) + val displayEmojiReactions = usersEventPermissions.canSendReaction && timelineItem.content.canReact() + + if (actions.isNotEmpty() || displayEmojiReactions || verifiedUserSendFailure != VerifiedUserSendFailure.None) { target.value = ActionListState.Target.Success( event = timelineItem, displayEmojiReactions = displayEmojiReactions, recentEmojis = recentEmojiDataSource.getRecentEmojisSorted(), + verifiedUserSendFailure = verifiedUserSendFailure, actions = actions.toImmutableList() ) } else { @@ -193,9 +198,9 @@ private fun List.postFilter(content: TimelineItemEventConten when (content) { is TimelineItemCallNotifyContent, is TimelineItemLegacyCallInviteContent, - is TimelineItemStateContent, + is TimelineItemStateContent -> action == TimelineItemAction.ViewSource is TimelineItemRedactedContent -> { - action == TimelineItemAction.ViewSource + action == TimelineItemAction.ViewSource || action == TimelineItemAction.Unpin } else -> true } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt index 8758b422ff..2c919c7173 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt @@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.actionlist import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure import io.element.android.features.messages.impl.timeline.model.TimelineItem import kotlinx.collections.immutable.ImmutableList @@ -17,6 +18,7 @@ data class ActionListState( val target: Target, val eventSink: (ActionListEvents) -> Unit, ) { + @Immutable sealed interface Target { data object None : Target data class Loading(val event: TimelineItem.Event) : Target @@ -24,6 +26,7 @@ data class ActionListState( val event: TimelineItem.Event, val displayEmojiReactions: Boolean, val recentEmojis: List = emptyList(), + val verifiedUserSendFailure: VerifiedUserSendFailure, val actions: ImmutableList, ) : Target } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt index 722d8af11e..4f92bc72ba 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt @@ -9,6 +9,8 @@ package io.element.android.features.messages.impl.actionlist import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure +import io.element.android.features.messages.impl.crypto.sendfailure.resolve.anUnsignedDeviceSendFailure import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemAudioContent @@ -35,6 +37,7 @@ open class ActionListStateProvider : PreviewParameterProvider { reactionsState = reactionsState ), displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), ) ), @@ -47,6 +50,7 @@ open class ActionListStateProvider : PreviewParameterProvider { reactionsState = reactionsState, ), displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), ) ), @@ -56,6 +60,7 @@ open class ActionListStateProvider : PreviewParameterProvider { reactionsState = reactionsState ), displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), ) ), @@ -65,6 +70,7 @@ open class ActionListStateProvider : PreviewParameterProvider { reactionsState = reactionsState ), displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), ) ), @@ -74,6 +80,7 @@ open class ActionListStateProvider : PreviewParameterProvider { reactionsState = reactionsState ), displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), ) ), @@ -83,6 +90,7 @@ open class ActionListStateProvider : PreviewParameterProvider { reactionsState = reactionsState ), displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), ) ), @@ -92,6 +100,7 @@ open class ActionListStateProvider : PreviewParameterProvider { reactionsState = reactionsState ), displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), ) ), @@ -101,6 +110,7 @@ open class ActionListStateProvider : PreviewParameterProvider { reactionsState = reactionsState ), displayEmojiReactions = false, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), ), ), @@ -110,6 +120,7 @@ open class ActionListStateProvider : PreviewParameterProvider { reactionsState = reactionsState ), displayEmojiReactions = false, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemPollActionList(), ), ), @@ -120,6 +131,15 @@ open class ActionListStateProvider : PreviewParameterProvider { messageShield = MessageShield.UnknownDevice(isCritical = true) ), displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, + actions = aTimelineItemActionList(), + ) + ), + anActionListState().copy( + target = ActionListState.Target.Success( + event = aTimelineItemEvent(), + displayEmojiReactions = true, + verifiedUserSendFailure = anUnsignedDeviceSendFailure(), actions = aTimelineItemActionList(), ) ), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index 276a79581f..86f5574a68 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -46,6 +47,10 @@ 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.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure +import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure.ChangedIdentity +import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure.None +import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure.UnsignedDevice import io.element.android.features.messages.impl.timeline.components.MessageShieldView import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent @@ -90,6 +95,7 @@ fun ActionListView( onSelectAction: (action: TimelineItemAction, TimelineItem.Event) -> Unit, onEmojiReactionClick: (String, TimelineItem.Event) -> Unit, onCustomReactionClick: (TimelineItem.Event) -> Unit, + onVerifiedUserSendFailureClick: (TimelineItem.Event) -> Unit, modifier: Modifier = Modifier, ) { val sheetState = rememberModalBottomSheetState() @@ -126,6 +132,14 @@ fun ActionListView( state.eventSink(ActionListEvents.Clear) } + fun onVerifiedUserSendFailureClick() { + if (targetItem == null) return + sheetState.hide(coroutineScope) { + state.eventSink(ActionListEvents.Clear) + onVerifiedUserSendFailureClick(targetItem) + } + } + if (targetItem != null) { ModalBottomSheet( sheetState = sheetState, @@ -137,6 +151,7 @@ fun ActionListView( onActionClick = ::onItemActionClick, onEmojiReactionClick = ::onEmojiReactionClick, onCustomReactionClick = ::onCustomReactionClick, + onVerifiedUserSendFailureClick = ::onVerifiedUserSendFailureClick, modifier = Modifier .navigationBarsPadding() .imePadding() @@ -151,6 +166,7 @@ private fun SheetContent( onActionClick: (TimelineItemAction) -> Unit, onEmojiReactionClick: (String) -> Unit, onCustomReactionClick: () -> Unit, + onVerifiedUserSendFailureClick: () -> Unit, modifier: Modifier = Modifier, ) { when (val target = state.target) { @@ -184,6 +200,16 @@ private fun SheetContent( HorizontalDivider() } } + if (target.verifiedUserSendFailure != None) { + item { + VerifiedUserSendFailureView( + sendFailure = target.verifiedUserSendFailure, + modifier = Modifier.fillMaxWidth(), + onClick = onVerifiedUserSendFailureClick + ) + HorizontalDivider() + } + } if (target.displayEmojiReactions) { item { EmojiReactionsRow( @@ -340,6 +366,43 @@ private fun EmojiReactionsRow( } } +@Composable +private fun VerifiedUserSendFailureView( + sendFailure: VerifiedUserSendFailure, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + @Composable + fun VerifiedUserSendFailure.headline(): String { + return when (this) { + is None -> "" + is UnsignedDevice.FromOther -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_unsigned_device, userDisplayName) + is UnsignedDevice.FromYou -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_you_unsigned_device) + is ChangedIdentity -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, userDisplayName) + } + } + + ListItem( + modifier = modifier + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 8.dp), + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Error())), + trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChevronRight())), + headlineContent = { + Text( + text = sendFailure.headline(), + style = ElementTheme.typography.fontBodySmMedium, + ) + }, + colors = ListItemDefaults.colors( + containerColor = Color.Transparent, + leadingIconColor = ElementTheme.colors.iconCriticalPrimary, + trailingIconColor = ElementTheme.colors.iconPrimary, + headlineColor = ElementTheme.colors.textCriticalPrimary, + ), + ) +} + @Composable private fun EmojiButton( emoji: String, @@ -389,5 +452,6 @@ internal fun SheetContentPreview( onActionClick = {}, onEmojiReactionClick = {}, onCustomReactionClick = {}, + onVerifiedUserSendFailureClick = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/VerifiedUserSendFailure.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/VerifiedUserSendFailure.kt new file mode 100644 index 0000000000..13a26a8bdb --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/VerifiedUserSendFailure.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.crypto.sendfailure + +import androidx.compose.runtime.Immutable + +@Immutable +sealed interface VerifiedUserSendFailure { + data object None : VerifiedUserSendFailure + + sealed interface UnsignedDevice : VerifiedUserSendFailure { + data object FromYou : UnsignedDevice + data class FromOther(val userDisplayName: String) : UnsignedDevice + } + + data class ChangedIdentity( + val userDisplayName: String, + ) : VerifiedUserSendFailure +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/VerifiedUserSendFailureFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/VerifiedUserSendFailureFactory.kt new file mode 100644 index 0000000000..b96baed27d --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/VerifiedUserSendFailureFactory.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.crypto.sendfailure + +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState +import javax.inject.Inject + +class VerifiedUserSendFailureFactory @Inject constructor( + private val room: MatrixRoom, +) { + suspend fun create( + sendState: LocalEventSendState?, + ): VerifiedUserSendFailure { + return when (sendState) { + is LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice -> { + val userId = sendState.devices.keys.firstOrNull() + if (userId == null) { + VerifiedUserSendFailure.None + } else { + if (userId == room.sessionId) { + VerifiedUserSendFailure.UnsignedDevice.FromYou + } else { + val displayName = room.userDisplayName(userId).getOrNull() ?: userId.value + VerifiedUserSendFailure.UnsignedDevice.FromOther(displayName) + } + } + } + is LocalEventSendState.Failed.VerifiedUserChangedIdentity -> { + val userId = sendState.users.firstOrNull() + if (userId == null) { + VerifiedUserSendFailure.None + } else { + val displayName = room.userDisplayName(userId).getOrNull() ?: userId.value + VerifiedUserSendFailure.ChangedIdentity(displayName) + } + } + else -> VerifiedUserSendFailure.None + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureEvents.kt new file mode 100644 index 0000000000..7743ef9dcd --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureEvents.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.crypto.sendfailure.resolve + +import io.element.android.features.messages.impl.timeline.model.TimelineItem + +sealed interface ResolveVerifiedUserSendFailureEvents { + data class ComputeForMessage( + val messageEvent: TimelineItem.Event, + ) : ResolveVerifiedUserSendFailureEvents + + data object ResolveAndResend : ResolveVerifiedUserSendFailureEvents + data object Retry : ResolveVerifiedUserSendFailureEvents + data object Dismiss : ResolveVerifiedUserSendFailureEvents +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailurePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailurePresenter.kt new file mode 100644 index 0000000000..c96e695375 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailurePresenter.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.crypto.sendfailure.resolve + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure +import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runUpdatingState +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState +import kotlinx.coroutines.launch +import javax.inject.Inject + +class ResolveVerifiedUserSendFailurePresenter @Inject constructor( + private val room: MatrixRoom, + private val verifiedUserSendFailureFactory: VerifiedUserSendFailureFactory, +) : Presenter { + @Composable + override fun present(): ResolveVerifiedUserSendFailureState { + var resolver by remember { + mutableStateOf(null) + } + val verifiedUserSendFailure by produceState(VerifiedUserSendFailure.None, resolver?.currentSendFailure?.value) { + val currentSendFailure = resolver?.currentSendFailure?.value + value = verifiedUserSendFailureFactory.create(currentSendFailure) + } + + val resolveAction = remember { + mutableStateOf>(AsyncAction.Uninitialized) + } + val retryAction = remember { + mutableStateOf>(AsyncAction.Uninitialized) + } + val coroutineScope = rememberCoroutineScope() + + fun handleEvents(event: ResolveVerifiedUserSendFailureEvents) { + when (event) { + is ResolveVerifiedUserSendFailureEvents.ComputeForMessage -> { + val sendState = event.messageEvent.localSendState as? LocalEventSendState.Failed.VerifiedUser + val transactionId = event.messageEvent.transactionId + resolver = if (sendState != null && transactionId != null) { + VerifiedUserSendFailureResolver( + room = room, + transactionId = transactionId, + iterator = VerifiedUserSendFailureIterator.from(sendState) + ) + } else { + null + } + } + ResolveVerifiedUserSendFailureEvents.Dismiss -> { + resolver = null + } + ResolveVerifiedUserSendFailureEvents.Retry -> { + coroutineScope.launch { + resolver?.run { + runUpdatingState(retryAction) { + resend() + } + } + } + } + ResolveVerifiedUserSendFailureEvents.ResolveAndResend -> { + coroutineScope.launch { + resolver?.run { + runUpdatingState(resolveAction) { + resolveAndResend() + } + } + } + } + } + } + + return ResolveVerifiedUserSendFailureState( + verifiedUserSendFailure = verifiedUserSendFailure, + resolveAction = resolveAction.value, + retryAction = retryAction.value, + eventSink = ::handleEvents + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureState.kt new file mode 100644 index 0000000000..44ec6640b0 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureState.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.crypto.sendfailure.resolve + +import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure +import io.element.android.libraries.architecture.AsyncAction + +data class ResolveVerifiedUserSendFailureState( + val verifiedUserSendFailure: VerifiedUserSendFailure, + val resolveAction: AsyncAction, + val retryAction: AsyncAction, + val eventSink: (ResolveVerifiedUserSendFailureEvents) -> Unit +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureStateProvider.kt new file mode 100644 index 0000000000..cdbdf6fcb0 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureStateProvider.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.crypto.sendfailure.resolve + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure +import io.element.android.libraries.architecture.AsyncAction + +open class ResolveVerifiedUserSendFailureStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aResolveVerifiedUserSendFailureState(), + aResolveVerifiedUserSendFailureState( + verifiedUserSendFailure = anUnsignedDeviceSendFailure() + ), + aResolveVerifiedUserSendFailureState( + verifiedUserSendFailure = aChangedIdentitySendFailure() + ) + ) +} + +fun aResolveVerifiedUserSendFailureState( + verifiedUserSendFailure: VerifiedUserSendFailure = VerifiedUserSendFailure.None, + resolveAction: AsyncAction = AsyncAction.Uninitialized, + retryAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (ResolveVerifiedUserSendFailureEvents) -> Unit = {} +) = ResolveVerifiedUserSendFailureState( + verifiedUserSendFailure = verifiedUserSendFailure, + resolveAction = resolveAction, + retryAction = retryAction, + eventSink = eventSink +) + +fun anUnsignedDeviceSendFailure(userDisplayName: String = "Alice") = VerifiedUserSendFailure.UnsignedDevice.FromOther( + userDisplayName = userDisplayName, +) + +fun aChangedIdentitySendFailure(userDisplayName: String = "Alice") = VerifiedUserSendFailure.ChangedIdentity( + userDisplayName = userDisplayName, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureView.kt new file mode 100644 index 0000000000..fb602fa943 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureView.kt @@ -0,0 +1,161 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.crypto.sendfailure.resolve + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +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.messages.impl.crypto.sendfailure.VerifiedUserSendFailure +import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.OutlinedButton +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ResolveVerifiedUserSendFailureView( + state: ResolveVerifiedUserSendFailureState, + modifier: Modifier = Modifier, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + var showSheet by remember { mutableStateOf(false) } + + fun dismiss() { + state.eventSink(ResolveVerifiedUserSendFailureEvents.Dismiss) + } + + fun onRetryClick() { + state.eventSink(ResolveVerifiedUserSendFailureEvents.Retry) + } + + fun onResolveAndResendClick() { + state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend) + } + + LaunchedEffect(state.verifiedUserSendFailure) { + if (state.verifiedUserSendFailure is VerifiedUserSendFailure.None) { + sheetState.hide() + showSheet = false + } else { + showSheet = true + } + } + + Box(modifier = modifier) { + if (showSheet) { + ModalBottomSheet( + modifier = Modifier + .systemBarsPadding() + .navigationBarsPadding(), + sheetState = sheetState, + onDismissRequest = ::dismiss, + ) { + IconTitleSubtitleMolecule( + modifier = Modifier.padding(24.dp), + title = state.verifiedUserSendFailure.title(), + subTitle = state.verifiedUserSendFailure.subtitle(), + iconImageVector = CompoundIcons.Error(), + iconTint = ElementTheme.colors.iconCriticalPrimary, + iconBackgroundTint = ElementTheme.colors.bgCriticalSubtle, + ) + ButtonColumnMolecule( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp), + ) { + Button( + modifier = Modifier.fillMaxWidth(), + text = state.verifiedUserSendFailure.resolveAction(), + showProgress = state.resolveAction.isLoading(), + onClick = ::onResolveAndResendClick + ) + OutlinedButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = CommonStrings.action_retry), + showProgress = state.retryAction.isLoading(), + onClick = ::onRetryClick + ) + TextButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = CommonStrings.action_cancel_for_now), + onClick = ::dismiss, + ) + } + } + } + } +} + +@Composable +private fun VerifiedUserSendFailure.title(): String { + return when (this) { + is VerifiedUserSendFailure.UnsignedDevice.FromOther -> stringResource( + id = CommonStrings.screen_resolve_send_failure_unsigned_device_title, + userDisplayName + ) + VerifiedUserSendFailure.UnsignedDevice.FromYou -> stringResource(id = CommonStrings.screen_resolve_send_failure_you_unsigned_device_title) + is VerifiedUserSendFailure.ChangedIdentity -> stringResource( + id = CommonStrings.screen_resolve_send_failure_changed_identity_title, + userDisplayName + ) + VerifiedUserSendFailure.None -> "" + } +} + +@Composable +private fun VerifiedUserSendFailure.subtitle(): String { + return when (this) { + is VerifiedUserSendFailure.UnsignedDevice.FromOther -> stringResource( + id = CommonStrings.screen_resolve_send_failure_unsigned_device_subtitle, + userDisplayName, + userDisplayName, + ) + VerifiedUserSendFailure.UnsignedDevice.FromYou -> stringResource(id = CommonStrings.screen_resolve_send_failure_you_unsigned_device_subtitle) + is VerifiedUserSendFailure.ChangedIdentity -> stringResource( + id = CommonStrings.screen_resolve_send_failure_changed_identity_subtitle, + userDisplayName + ) + VerifiedUserSendFailure.None -> "" + } +} + +@Composable +private fun VerifiedUserSendFailure.resolveAction(): String { + return when (this) { + is VerifiedUserSendFailure.UnsignedDevice -> stringResource(id = CommonStrings.screen_resolve_send_failure_unsigned_device_primary_button_title) + is VerifiedUserSendFailure.ChangedIdentity -> stringResource(id = CommonStrings.screen_resolve_send_failure_changed_identity_primary_button_title) + VerifiedUserSendFailure.None -> "" + } +} + +@PreviewsDayNight +@Composable +internal fun ResolveVerifiedUserSendFailureViewPreview( + @PreviewParameter(ResolveVerifiedUserSendFailureStateProvider::class) state: ResolveVerifiedUserSendFailureState +) = ElementPreview { + ResolveVerifiedUserSendFailureView(state) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/VerifiedUserSendFailureIterator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/VerifiedUserSendFailureIterator.kt new file mode 100644 index 0000000000..8b438808ff --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/VerifiedUserSendFailureIterator.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.crypto.sendfailure.resolve + +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState +import timber.log.Timber + +/** + * Iterator for [LocalEventSendState.Failed.VerifiedUser] + * Allow to iterate through the internal state of the failure. + * This is useful to allow solving the failure step by step (e.g. for each user). + */ +interface VerifiedUserSendFailureIterator : Iterator { + companion object { + fun from(failure: LocalEventSendState.Failed.VerifiedUser): VerifiedUserSendFailureIterator { + return when (failure) { + is LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice -> UnsignedDeviceSendFailureIterator(failure) + is LocalEventSendState.Failed.VerifiedUserChangedIdentity -> ChangedIdentitySendFailureIterator(failure) + } + } + } +} + +class UnsignedDeviceSendFailureIterator( + failure: LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice +) : VerifiedUserSendFailureIterator { + private val iterator = failure.devices.iterator() + + init { + if (!hasNext()) { + Timber.w("Got $failure without any devices, shouldn't happen.") + } + } + + override fun hasNext(): Boolean { + return iterator.hasNext() + } + + override fun next(): LocalEventSendState.Failed.VerifiedUser { + val (userId, deviceIds) = iterator.next() + return LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice( + mapOf(userId to deviceIds) + ) + } +} + +class ChangedIdentitySendFailureIterator( + failure: LocalEventSendState.Failed.VerifiedUserChangedIdentity +) : VerifiedUserSendFailureIterator { + private val iterator = failure.users.iterator() + + init { + if (!hasNext()) { + Timber.w("Got $failure without any users, shouldn't happen.") + } + } + + override fun hasNext(): Boolean { + return iterator.hasNext() + } + + override fun next(): LocalEventSendState.Failed.VerifiedUser { + val userId = iterator.next() + return LocalEventSendState.Failed.VerifiedUserChangedIdentity( + listOf(userId) + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/VerifiedUserSendFailureResolver.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/VerifiedUserSendFailureResolver.kt new file mode 100644 index 0000000000..be775ed122 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/VerifiedUserSendFailureResolver.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.crypto.sendfailure.resolve + +import androidx.compose.runtime.mutableStateOf +import io.element.android.libraries.matrix.api.core.TransactionId +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState +import timber.log.Timber + +/** + * This class is responsible for resolving and resending a failed message sent to a verified user. + * It also allow to resend the message without resolving the failure, for example if the user has in the meantime verified their device again. + * It's using the [VerifiedUserSendFailureIterator] to iterate over the different failures (ie. the different users concerned by the failure). + * This way, the user can resolve and resend the message for each user concerned, one by one. + */ +class VerifiedUserSendFailureResolver( + private val room: MatrixRoom, + private val transactionId: TransactionId, + private val iterator: VerifiedUserSendFailureIterator, +) { + val currentSendFailure = mutableStateOf(null) + + init { + if (iterator.hasNext()) { + currentSendFailure.value = iterator.next() + } + } + + suspend fun resend(): Result { + return room.retrySendMessage(transactionId) + .onSuccess { + Timber.d("Succeed to resend message with transactionId: $transactionId") + currentSendFailure.value = null + } + .onFailure { + Timber.e(it, "Failed to resend message with transactionId: $transactionId") + } + } + + suspend fun resolveAndResend(): Result { + return when (val failure = currentSendFailure.value) { + is LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice -> { + room.ignoreDeviceTrustAndResend(failure.devices, transactionId) + } + is LocalEventSendState.Failed.VerifiedUserChangedIdentity -> { + room.withdrawVerificationAndResend(failure.users, transactionId) + } + else -> { + Result.failure(IllegalStateException("Unknown send failure type")) + } + }.onSuccess { + Timber.d("Succeed to resolve and resend message with transactionId: $transactionId") + if (iterator.hasNext()) { + val failure = iterator.next() + currentSendFailure.value = failure + } else { + currentSendFailure.value = null + Timber.d("No more failure to resolve for transactionId: $transactionId") + } + }.onFailure { + Timber.e(it, "Failed to resolve and resend message with transactionId: $transactionId") + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt index e6cb916aef..8c488d61da 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt @@ -10,6 +10,8 @@ package io.element.android.features.messages.impl.di import com.squareup.anvil.annotations.ContributesTo import dagger.Binds import dagger.Module +import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailurePresenter +import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState import io.element.android.libraries.architecture.Presenter @@ -20,4 +22,7 @@ import io.element.android.libraries.di.RoomScope interface MessagesModule { @Binds fun bindPinnedMessagesBannerPresenter(presenter: PinnedMessagesBannerPresenter): Presenter + + @Binds + fun bindResolveVerifiedUserSendFailurePresenter(presenter: ResolveVerifiedUserSendFailurePresenter): Presenter } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt index 81405c8f38..457e96c5b8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt @@ -25,14 +25,12 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import javax.inject.Inject -import kotlin.time.Duration.Companion.milliseconds class PinnedMessagesBannerPresenter @Inject constructor( private val room: MatrixRoom, @@ -123,7 +121,6 @@ class PinnedMessagesBannerPresenter @Inject constructor( is AsyncData.Loading -> flowOf(AsyncData.Loading()) is AsyncData.Success -> { asyncTimeline.data.timelineItems - .debounce(300.milliseconds) .map { timelineItems -> val pinnedItems = timelineItems.mapNotNull { timelineItem -> itemFactory.create(timelineItem) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index b31e38d99f..1525a8a5e1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -43,14 +43,12 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber -import kotlin.time.Duration.Companion.milliseconds class PinnedMessagesListPresenter @AssistedInject constructor( @Assisted private val navigator: PinnedMessagesListNavigator, @@ -119,7 +117,6 @@ class PinnedMessagesListPresenter @AssistedInject constructor( targetEvent: TimelineItem.Event, ) = launch { when (action) { - TimelineItemAction.Redact -> handleActionRedact(targetEvent) TimelineItemAction.ViewSource -> { navigator.onShowEventDebugInfoClick(targetEvent.eventId, targetEvent.debugInfo) } @@ -149,13 +146,6 @@ class PinnedMessagesListPresenter @AssistedInject constructor( } } - private suspend fun handleActionRedact(event: TimelineItem.Event) { - timelineProvider.invokeOnTimeline { - redactEvent(eventId = event.eventId, transactionId = event.transactionId, reason = null) - .onFailure { Timber.e(it) } - } - } - @Composable private fun userEventPermissions(updateKey: Long): State { return produceState(UserEventPermissions.DEFAULT, key1 = updateKey) { @@ -182,7 +172,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor( is AsyncData.Failure -> flowOf(AsyncData.Failure(asyncTimeline.error)) is AsyncData.Loading -> flowOf(AsyncData.Loading()) is AsyncData.Success -> { - val timelineItemsFlow = asyncTimeline.data.timelineItems.debounce(300.milliseconds) + val timelineItemsFlow = asyncTimeline.data.timelineItems combine(timelineItemsFlow, room.membersStateFlow) { items, membersState -> timelineItemsFactory.replaceWith( timelineItems = items, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt index 3e88a9d716..48fdb83d79 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt @@ -17,7 +17,6 @@ class PinnedMessagesListTimelineActionPostProcessor : TimelineItemActionPostProc actions.firstOrNull { it is TimelineItemAction.Unpin }?.let(::add) actions.firstOrNull { it is TimelineItemAction.Forward }?.let(::add) actions.firstOrNull { it is TimelineItemAction.ViewSource }?.let(::add) - actions.firstOrNull { it is TimelineItemAction.Redact }?.let(::add) } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index 4a08d999f5..ff05ba34a1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -108,7 +108,7 @@ private fun PinnedMessagesListContent( ErrorDialog( title = stringResource(id = CommonStrings.error_unknown), content = stringResource(id = CommonStrings.error_failed_loading_messages), - onDismiss = onErrorDismiss + onSubmit = onErrorDismiss ) } PinnedMessagesListState.Empty -> PinnedMessagesListEmpty() @@ -181,6 +181,7 @@ private fun PinnedMessagesListLoaded( onSelectAction = ::onActionSelected, onCustomReactionClick = {}, onEmojiReactionClick = { _, _ -> }, + onVerifiedUserSendFailureClick = {} ) LazyColumn( modifier = modifier.fillMaxSize(), @@ -199,19 +200,18 @@ private fun PinnedMessagesListLoaded( renderReadReceipts = false, isLastOutgoingMessage = false, focusedEventId = null, - onClick = onEventClick, - onLongClick = ::onMessageLongClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, + onClick = onEventClick, + onLongClick = ::onMessageLongClick, inReplyToClick = {}, onReactionClick = { _, _ -> }, onReactionLongClick = { _, _ -> }, onMoreReactionsClick = {}, onReadReceiptClick = {}, - eventSink = {}, onSwipeToReply = {}, onJoinCallClick = {}, - onShieldClick = {}, + eventSink = {}, eventContentView = { event, contentModifier, onContentLayoutChange -> TimelineItemEventContentViewWrapper( event = event, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt index 77c212f08c..d3c62239cc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt @@ -7,6 +7,7 @@ package io.element.android.features.messages.impl.timeline +import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield @@ -23,7 +24,6 @@ sealed interface TimelineEvents { data object OnFocusEventRender : TimelineEvents data object JumpToLive : TimelineEvents - data class ShowShieldDialog(val messageShield: MessageShield) : TimelineEvents data object HideShieldDialog : TimelineEvents /** @@ -31,6 +31,8 @@ sealed interface TimelineEvents { */ sealed interface EventFromTimelineItem : TimelineEvents + data class ComputeVerifiedUserSendFailure(val event: TimelineItem.Event) : EventFromTimelineItem + data class ShowShieldDialog(val messageShield: MessageShield) : EventFromTimelineItem data class LoadMore(val direction: Timeline.PaginationDirection) : EventFromTimelineItem /** diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 8bf9db47a3..0674638e66 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import chat.schildi.lib.preferences.ScPrefs import chat.schildi.lib.preferences.state @@ -24,6 +25,8 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.features.messages.impl.MessagesNavigator +import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureEvents +import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig import io.element.android.features.messages.impl.timeline.model.NewEventState @@ -69,6 +72,7 @@ class TimelinePresenter @AssistedInject constructor( private val endPollAction: EndPollAction, private val sessionPreferencesStore: SessionPreferencesStore, private val timelineController: TimelineController, + private val resolveVerifiedUserSendFailurePresenter: Presenter, ) : Presenter { @AssistedFactory interface Factory { @@ -81,6 +85,7 @@ class TimelinePresenter @AssistedInject constructor( computeReactions = true, ) ) + private var timelineItems by mutableStateOf>(persistentListOf()) @Composable override fun present(): TimelineState { @@ -89,9 +94,12 @@ class TimelinePresenter @AssistedInject constructor( mutableStateOf(FocusRequestState.None) } + LaunchedEffect(Unit) { + timelineItemsFactory.timelineItems.collect { timelineItems = it } + } + val lastReadReceiptId = rememberSaveable { mutableStateOf(null) } - val timelineItems by timelineItemsFactory.timelineItems.collectAsState(initial = persistentListOf()) val roomInfo by room.roomInfoFlow.collectAsState(initial = null) val syncUpdateFlow = room.syncUpdateFlow.collectAsState() @@ -104,6 +112,7 @@ class TimelinePresenter @AssistedInject constructor( val newEventState = remember { mutableStateOf(NewEventState.None) } val messageShield: MutableState = remember { mutableStateOf(null) } + val resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailurePresenter.present() val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true) val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true) val isLive by timelineController.isLive().collectAsState(initial = true) @@ -177,6 +186,9 @@ class TimelinePresenter @AssistedInject constructor( } TimelineEvents.HideShieldDialog -> messageShield.value = null is TimelineEvents.ShowShieldDialog -> messageShield.value = event.messageShield + is TimelineEvents.ComputeVerifiedUserSendFailure -> { + resolveVerifiedUserSendFailureState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(event.event)) + } } } @@ -254,6 +266,7 @@ class TimelinePresenter @AssistedInject constructor( isLive = isLive, focusRequestState = focusRequestState.value, messageShield = messageShield.value, + resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState, eventSink = { handleEvents(it) } ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index da983d748c..ff0d79d2e7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.timeline import androidx.compose.runtime.Immutable +import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.matrix.api.core.EventId @@ -26,6 +27,7 @@ data class TimelineState( val focusRequestState: FocusRequestState, // If not null, info will be rendered in a dialog val messageShield: MessageShield?, + val resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState, val eventSink: (TimelineEvents) -> Unit, ) { val hasAnyEvent = timelineItems.any { it is TimelineItem.Event } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index 516559cc37..3317c2976c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -7,6 +7,8 @@ package io.element.android.features.messages.impl.timeline +import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState +import io.element.android.features.messages.impl.crypto.sendfailure.resolve.aResolveVerifiedUserSendFailureState import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.ReadReceiptData @@ -44,6 +46,7 @@ fun aTimelineState( focusedEventIndex: Int = -1, isLive: Boolean = true, messageShield: MessageShield? = null, + resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState = aResolveVerifiedUserSendFailureState(), eventSink: (TimelineEvents) -> Unit = {}, ): TimelineState { val focusedEventId = timelineItems.filterIsInstance().getOrNull(focusedEventIndex)?.eventId @@ -60,6 +63,7 @@ fun aTimelineState( isLive = isLive, focusRequestState = focusRequestState, messageShield = messageShield, + resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState, eventSink = eventSink, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index ce2d6c57f9..ab7aed7244 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -52,6 +52,7 @@ import chat.schildi.timeline.FloatingDateHeader import com.google.common.primitives.Ints.min import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureView import io.element.android.features.messages.impl.timeline.components.TimelineItemRow import io.element.android.features.messages.impl.timeline.components.toText import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories @@ -138,8 +139,8 @@ fun TimelineView( Box(modifier) { LazyColumn( modifier = Modifier - .fillMaxSize() - .nestedScroll(nestedScrollConnection), + .fillMaxSize() + .nestedScroll(nestedScrollConnection), state = lazyListState, reverseLayout = useReverseLayout, contentPadding = PaddingValues(vertical = 8.dp), @@ -161,19 +162,18 @@ fun TimelineView( isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true && state.timelineItems.first().identifier() == timelineItem.identifier(), focusedEventId = state.focusedEventId, - onClick = onMessageClick, - onLongClick = onMessageLongClick, - onShieldClick = ::onShieldClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, + onClick = onMessageClick, + onLongClick = onMessageLongClick, inReplyToClick = ::inReplyToClick, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, onMoreReactionsClick = onMoreReactionsClick, onReadReceiptClick = onReadReceiptClick, - eventSink = state.eventSink, onSwipeToReply = onSwipeToReply, onJoinCallClick = onJoinCallClick, + eventSink = state.eventSink, ) } } @@ -200,6 +200,8 @@ fun TimelineView( } } + ResolveVerifiedUserSendFailureView(state = state.resolveVerifiedUserSendFailureState) + MessageShieldDialog(state) } @@ -282,8 +284,8 @@ private fun BoxScope.TimelineScrollHelper( // Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive, modifier = Modifier - .align(Alignment.BottomEnd) - .padding(end = 24.dp, bottom = 12.dp), + .align(Alignment.BottomEnd) + .padding(end = 24.dp, bottom = 12.dp), onClick = { jumpToBottom() }, ) } @@ -310,8 +312,8 @@ private fun JumpToBottomButton( ) { Icon( modifier = Modifier - .size(24.dp) - .rotate(90f), + .size(24.dp) + .rotate(90f), imageVector = CompoundIcons.ArrowRight(), contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom) ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt index a73dbef79c..0db7a8ac52 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt @@ -28,9 +28,8 @@ internal fun ATimelineItemEventRow( isHighlighted = isHighlighted, onClick = {}, onLongClick = {}, - onShieldClick = {}, - onUserDataClick = {}, onLinkClick = {}, + onUserDataClick = {}, inReplyToClick = {}, onReactionClick = { _, _ -> }, onReactionLongClick = { _, _ -> }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt index 60d2f0b9a5..b77f8bc3bc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter 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.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.isEdited import io.element.android.libraries.core.bool.orFalse @@ -31,14 +32,13 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState -import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import io.element.android.libraries.matrix.api.timeline.item.event.isCritical import io.element.android.libraries.ui.strings.CommonStrings @Composable fun TimelineEventTimestampView( event: TimelineItem.Event, - onShieldClick: (MessageShield) -> Unit, + eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, modifier: Modifier = Modifier, ) { val formattedTime = event.sentTime @@ -48,8 +48,8 @@ fun TimelineEventTimestampView( val tint = if (hasError || hasEncryptionCritical) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary Row( modifier = Modifier - .padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing)) - .then(modifier), + .padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing)) + .then(modifier), verticalAlignment = Alignment.CenterVertically, ) { if (isMessageEdited) { @@ -66,12 +66,17 @@ fun TimelineEventTimestampView( color = tint, ) if (hasError) { + val isVerifiedUserSendFailure = event.localSendState is LocalEventSendState.Failed.VerifiedUser Spacer(modifier = Modifier.width(2.dp)) Icon( imageVector = CompoundIcons.Error(), contentDescription = stringResource(id = CommonStrings.common_sending_failed), tint = tint, - modifier = Modifier.size(15.dp, 18.dp), + modifier = Modifier + .size(15.dp, 18.dp) + .clickable(isVerifiedUserSendFailure) { + eventSink(TimelineEvents.ComputeVerifiedUserSendFailure(event)) + }, ) } else { ScTimestampIcon(event.localSendState) @@ -82,8 +87,10 @@ fun TimelineEventTimestampView( imageVector = shield.toIcon(), contentDescription = shield.toText(), modifier = Modifier - .size(15.dp) - .clickable { onShieldClick(shield) }, + .size(15.dp) + .clickable { + eventSink(TimelineEvents.ShowShieldDialog(shield)) + }, tint = shield.toIconColor(), ) Spacer(modifier = Modifier.width(4.dp)) @@ -96,7 +103,7 @@ fun TimelineEventTimestampView( internal fun TimelineEventTimestampViewPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) = ElementPreview { TimelineEventTimestampView( event = event, - onShieldClick = {}, + eventSink = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index fde338b4ad..2a1437b046 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -86,7 +86,6 @@ import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.matrix.ui.messages.reply.InReplyToView @@ -117,7 +116,6 @@ fun TimelineItemEventRow( isHighlighted: Boolean, onClick: () -> Unit, onLongClick: () -> Unit, - onShieldClick: (MessageShield) -> Unit, onLinkClick: (String) -> Unit, onUserDataClick: (UserId) -> Unit, inReplyToClick: (EventId) -> Unit, @@ -170,6 +168,17 @@ fun TimelineItemEventRow( ReplySwipeIndicator({ offset / 120 }) } TimelineItemEventRowContent( + event = event, + isHighlighted = isHighlighted, + timelineRoomInfo = timelineRoomInfo, + interactionSource = interactionSource, + onClick = onClick, + onLongClick = onLongClick, + inReplyToClick = ::inReplyToClick, + onUserDataClick = ::onUserDataClick, + onReactionClick = { emoji -> onReactionClick(emoji, event) }, + onReactionLongClick = { emoji -> onReactionLongClick(emoji, event) }, + onMoreReactionsClick = { onMoreReactionsClick(event) }, modifier = Modifier .absoluteOffset { IntOffset(x = offset.roundToInt(), y = 0) } .draggable( @@ -185,18 +194,7 @@ fun TimelineItemEventRow( }, state = state.draggableState, ), - event = event, - isHighlighted = isHighlighted, - timelineRoomInfo = timelineRoomInfo, - interactionSource = interactionSource, - onClick = onClick, - onLongClick = onLongClick, - onShieldClick = onShieldClick, - inReplyToClick = ::inReplyToClick, - onUserDataClick = ::onUserDataClick, - onReactionClick = { emoji -> onReactionClick(emoji, event) }, - onReactionLongClick = { emoji -> onReactionLongClick(emoji, event) }, - onMoreReactionsClick = { onMoreReactionsClick(event) }, + eventSink = eventSink, eventContentView = eventContentView, ) } @@ -209,12 +207,12 @@ fun TimelineItemEventRow( interactionSource = interactionSource, onClick = onClick, onLongClick = onLongClick, - onShieldClick = onShieldClick, inReplyToClick = ::inReplyToClick, onUserDataClick = ::onUserDataClick, onReactionClick = { emoji -> onReactionClick(emoji, event) }, onReactionLongClick = { emoji -> onReactionLongClick(emoji, event) }, onMoreReactionsClick = { onMoreReactionsClick(event) }, + eventSink = eventSink, eventContentView = eventContentView, ) } @@ -264,12 +262,12 @@ private fun TimelineItemEventRowContent( interactionSource: MutableInteractionSource, onClick: () -> Unit, onLongClick: () -> Unit, - onShieldClick: (MessageShield) -> Unit, inReplyToClick: () -> Unit, onUserDataClick: () -> Unit, onReactionClick: (emoji: String) -> Unit, onReactionLongClick: (emoji: String) -> Unit, onMoreReactionsClick: (event: TimelineItem.Event) -> Unit, + eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, modifier: Modifier = Modifier, eventContentView: @Composable (Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit, ) { @@ -332,9 +330,9 @@ private fun TimelineItemEventRowContent( ) { MessageEventBubbleContent( event = event, - onShieldClick = onShieldClick, onMessageLongClick = onLongClick, inReplyToClick = inReplyToClick, + eventSink = eventSink, eventContentView = eventContentView, ) } @@ -392,9 +390,9 @@ private fun MessageSenderInformation( @Composable private fun MessageEventBubbleContent( event: TimelineItem.Event, - onShieldClick: (MessageShield) -> Unit, onMessageLongClick: () -> Unit, inReplyToClick: () -> Unit, + eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, @SuppressLint("ModifierParameter") // need to rename this modifier to prevent linter false positives @Suppress("ModifierNaming") @@ -432,7 +430,7 @@ private fun MessageEventBubbleContent( @Composable fun WithTimestampLayout( timestampPosition: TimestampPosition, - onShieldClick: (MessageShield) -> Unit, + eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, modifier: Modifier = Modifier, canShrinkContent: Boolean = false, content: @Composable (onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit) -> Unit, @@ -443,7 +441,7 @@ private fun MessageEventBubbleContent( content {} TimelineEventTimestampView( event = event, - onShieldClick = onShieldClick, + eventSink = eventSink, modifier = Modifier .scOrElse( forSc = Modifier @@ -470,7 +468,7 @@ private fun MessageEventBubbleContent( overlay = { TimelineEventTimestampView( event = event, - onShieldClick = onShieldClick, + eventSink = eventSink, modifier = Modifier .padding(horizontal = 8.dp, vertical = 4.dp) ) @@ -481,7 +479,7 @@ private fun MessageEventBubbleContent( content {} TimelineEventTimestampView( event = event, - onShieldClick = onShieldClick, + eventSink = eventSink, modifier = Modifier .align(Alignment.End) .padding(horizontal = 8.dp, vertical = 4.dp) @@ -529,7 +527,7 @@ private fun MessageEventBubbleContent( val contentWithTimestamp = @Composable { WithTimestampLayout( timestampPosition = timestampPosition, - onShieldClick = onShieldClick, + eventSink = eventSink, canShrinkContent = canShrinkContent, modifier = timestampLayoutModifier, content = { onContentLayoutChange -> diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt index bd4e60f954..f688591ee8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt @@ -29,7 +29,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield @Composable fun TimelineItemGroupedEventsRow( @@ -40,7 +39,6 @@ fun TimelineItemGroupedEventsRow( focusedEventId: EventId?, onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, - onShieldClick: (MessageShield) -> Unit, inReplyToClick: (EventId) -> Unit, onUserDataClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, @@ -78,7 +76,6 @@ fun TimelineItemGroupedEventsRow( isLastOutgoingMessage = isLastOutgoingMessage, onClick = onClick, onLongClick = onLongClick, - onShieldClick = onShieldClick, inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, @@ -103,7 +100,6 @@ private fun TimelineItemGroupedEventsRowContent( isLastOutgoingMessage: Boolean, onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, - onShieldClick: (MessageShield) -> Unit, inReplyToClick: (EventId) -> Unit, onUserDataClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, @@ -145,19 +141,18 @@ private fun TimelineItemGroupedEventsRowContent( renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, focusedEventId = focusedEventId, + onUserDataClick = onUserDataClick, + onLinkClick = onLinkClick, onClick = onClick, onLongClick = onLongClick, - onShieldClick = onShieldClick, inReplyToClick = inReplyToClick, - onUserDataClick = onUserDataClick, - onLinkClick = onLinkClick, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, onMoreReactionsClick = onMoreReactionsClick, onReadReceiptClick = onReadReceiptClick, - eventSink = eventSink, onSwipeToReply = {}, onJoinCallClick = {}, + eventSink = eventSink, eventContentView = eventContentView, ) } @@ -190,7 +185,6 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi isLastOutgoingMessage = false, onClick = {}, onLongClick = {}, - onShieldClick = {}, inReplyToClick = {}, onUserDataClick = {}, onLinkClick = {}, @@ -215,7 +209,6 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi isLastOutgoingMessage = false, onClick = {}, onLongClick = {}, - onShieldClick = {}, inReplyToClick = {}, onUserDataClick = {}, onLinkClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index ed94b62aa5..58d8def69c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -31,7 +31,6 @@ import io.element.android.libraries.designsystem.text.toPx import io.element.android.libraries.designsystem.theme.highlightedMessageBackgroundColor import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield @Composable internal fun TimelineItemRow( @@ -44,7 +43,6 @@ internal fun TimelineItemRow( onLinkClick: (String) -> Unit, onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, - onShieldClick: (MessageShield) -> Unit, inReplyToClick: (EventId) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, @@ -117,9 +115,8 @@ internal fun TimelineItemRow( isHighlighted = timelineItem.isEvent(focusedEventId), onClick = { onClick(timelineItem) }, onLongClick = { onLongClick(timelineItem) }, - onShieldClick = onShieldClick, - onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, + onUserDataClick = onUserDataClick, inReplyToClick = inReplyToClick, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, @@ -143,7 +140,6 @@ internal fun TimelineItemRow( focusedEventId = focusedEventId, onClick = onClick, onLongClick = onLongClick, - onShieldClick = onShieldClick, inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateView.kt index 7bd5301208..75d4d5ebac 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateView.kt @@ -36,7 +36,7 @@ fun FocusRequestStateView( } ErrorDialog( content = errorMessage, - onDismiss = onClearFocusRequestState, + onSubmit = onClearFocusRequestState, modifier = modifier, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageSendingFailedDialog.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageSendingFailedDialog.kt index f2cc385553..ffed5f31ac 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageSendingFailedDialog.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageSendingFailedDialog.kt @@ -19,7 +19,7 @@ internal fun VoiceMessageSendingFailedDialog( ErrorDialog( title = stringResource(CommonStrings.common_error), content = stringResource(CommonStrings.error_failed_uploading_voice_message), - onDismiss = onDismiss, + onSubmit = onDismiss, submitText = stringResource(CommonStrings.action_ok), ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index df873d2328..3d6af28f3d 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -16,6 +16,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.crypto.sendfailure.resolve.aResolveVerifiedUserSendFailureState import io.element.android.features.messages.impl.draft.FakeComposerDraftService import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator @@ -1035,6 +1036,7 @@ class MessagesPresenterTest { sessionPreferencesStore = sessionPreferencesStore, timelineItemIndexer = TimelineItemIndexer(), timelineController = TimelineController(matrixRoom), + resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() }, ) val timelinePresenterFactory = object : TimelinePresenter.Factory { override fun create(navigator: MessagesNavigator): TimelinePresenter { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index 52387f821a..210ffb4fe6 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -34,6 +34,8 @@ import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure +import io.element.android.features.messages.impl.crypto.sendfailure.resolve.aChangedIdentitySendFailure import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerItem import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState @@ -329,6 +331,7 @@ class MessagesViewTest { event = timelineItem, displayEmojiReactions = true, actions = persistentListOf(TimelineItemAction.Edit), + verifiedUserSendFailure = VerifiedUserSendFailure.None, ) ), ) @@ -399,6 +402,7 @@ class MessagesViewTest { target = ActionListState.Target.Success( event = timelineItem, displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf(TimelineItemAction.Edit), ), ), @@ -416,6 +420,32 @@ class MessagesViewTest { eventsRecorder.assertSingle(CustomReactionEvents.ShowCustomReactionSheet(timelineItem)) } + @Test + fun `clicking on verified user send failure from action list emits the expected Event`() { + val eventsRecorder = EventsRecorder() + val state = aMessagesState() + val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event + val stateWithActionListState = state.copy( + actionListState = anActionListState( + target = ActionListState.Target.Success( + event = timelineItem, + displayEmojiReactions = true, + verifiedUserSendFailure = aChangedIdentitySendFailure(), + actions = persistentListOf(), + ), + ), + timelineState = aTimelineState(eventSink = eventsRecorder) + ) + rule.setMessagesView( + state = stateWithActionListState, + ) + val verifiedUserSendFailure = rule.activity.getString(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, "Alice") + rule.onNodeWithText(verifiedUserSendFailure).performClick() + // Give time for the close animation to complete + rule.mainClock.advanceTimeBy(milliseconds = 1_000) + eventsRecorder.assertSingle(TimelineEvents.ComputeVerifiedUserSendFailure(timelineItem)) + } + @Test fun `clicking on a custom emoji emits the expected Events`() { val aUnicode = "🙈" diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt index bd580b5f98..078fb9e676 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt @@ -14,6 +14,8 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.aUserEventPermissions import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor +import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure +import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent @@ -25,8 +27,10 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_MESSAGE +import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore @@ -79,6 +83,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = false, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.ViewSource, ) @@ -120,6 +125,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = false, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.ViewSource, ) @@ -161,6 +167,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Forward, @@ -208,6 +215,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.Forward, TimelineItemAction.Pin, @@ -252,6 +260,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Forward, @@ -298,6 +307,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = false, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Forward, @@ -345,6 +355,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Forward, @@ -392,6 +403,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Forward, @@ -439,6 +451,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Forward, @@ -484,6 +497,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = stateEvent, displayEmojiReactions = false, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.ViewSource, ) @@ -553,6 +567,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Forward, @@ -599,6 +614,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Forward, @@ -652,6 +668,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Forward, @@ -748,6 +765,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.Edit, TimelineItemAction.Copy, @@ -787,6 +805,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Edit, @@ -829,6 +848,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.EndPoll, @@ -870,6 +890,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Pin, @@ -910,6 +931,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = true, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Forward, @@ -949,6 +971,7 @@ class ActionListPresenterTest { ActionListState.Target.Success( event = messageEvent, displayEmojiReactions = false, + verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.ViewSource ) @@ -956,6 +979,32 @@ class ActionListPresenterTest { ) } } + + @Test + fun `present - compute for verified user send failure`() = runTest { + val room = FakeMatrixRoom( + userDisplayNameResult = { Result.success("Alice") } + ) + val presenter = createActionListPresenter(isDeveloperModeEnabled = false, isPinFeatureEnabled = false, room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + sendState = LocalEventSendState.Failed.VerifiedUserChangedIdentity(users = listOf(A_USER_ID)), + ) + initialState.eventSink.invoke( + ActionListEvents.ComputeForMessage( + event = messageEvent, + userEventPermissions = aUserEventPermissions(), + ) + ) + skipItems(1) + val successState = awaitItem() + val target = successState.target as ActionListState.Target.Success + assertThat(target.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.ChangedIdentity(userDisplayName = "Alice")) + } + } } private fun createActionListPresenter( @@ -968,6 +1017,7 @@ private fun createActionListPresenter( postProcessor = TimelineItemActionPostProcessor.Default, appPreferencesStore = preferencesStore, isPinnedMessagesFeatureEnabled = { isPinFeatureEnabled }, - room = room + room = room, + userSendFailureFactory = VerifiedUserSendFailureFactory(room) ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailurePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailurePresenterTest.kt new file mode 100644 index 0000000000..e74ae89b4f --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailurePresenterTest.kt @@ -0,0 +1,353 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.crypto.sendfailure.resolve + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure +import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory +import io.element.android.features.messages.impl.fixtures.aMessageEvent +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_TRANSACTION_ID +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class ResolveVerifiedUserSendFailurePresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createResolveVerifiedUserSendFailurePresenter() + presenter.test { + val initialState = awaitItem() + assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None) + } + } + + @Test + fun `present - remote message scenario`() = runTest { + val presenter = createResolveVerifiedUserSendFailurePresenter() + presenter.test { + val sentMessage = aMessageEvent() + val initialState = awaitItem() + assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None) + initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(sentMessage)) + ensureAllEventsConsumed() + } + } + + @Test + fun `present - sent message scenario`() = runTest { + val presenter = createResolveVerifiedUserSendFailurePresenter() + presenter.test { + val sentMessage = aMessageEvent( + sendState = LocalEventSendState.Sent(AN_EVENT_ID) + ) + val initialState = awaitItem() + assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None) + initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(sentMessage)) + ensureAllEventsConsumed() + } + } + + @Test + fun `present - unknown failed message scenario`() = runTest { + val presenter = createResolveVerifiedUserSendFailurePresenter() + presenter.test { + val failedMessage = aMessageEvent( + sendState = LocalEventSendState.Failed.Unknown("") + ) + val initialState = awaitItem() + assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None) + initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage)) + ensureAllEventsConsumed() + } + } + + @Test + fun `present - verified user unsigned device failure dismiss scenario`() = runTest { + val room = FakeMatrixRoom( + userDisplayNameResult = { userId -> + Result.success(userId.value) + }, + ) + val presenter = createResolveVerifiedUserSendFailurePresenter(room) + presenter.test { + val failedMessage = aVerifiedUserHasUnsignedDeviceFailedMessage() + val initialState = awaitItem() + assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None) + initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage)) + skipItems(1) + awaitItem().also { state -> + assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromYou) + state.eventSink(ResolveVerifiedUserSendFailureEvents.Dismiss) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None) + } + ensureAllEventsConsumed() + } + } + + @Test + fun `present - verified user unsigned device failure retry scenario`() = runTest { + val room = FakeMatrixRoom( + userDisplayNameResult = { userId -> + Result.success(userId.value) + }, + retrySendMessageResult = { + Result.success(Unit) + }, + ) + val presenter = createResolveVerifiedUserSendFailurePresenter(room) + presenter.test { + val failedMessage = aVerifiedUserHasUnsignedDeviceFailedMessage() + val initialState = awaitItem() + assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None) + initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage)) + + skipItems(1) + awaitItem().also { state -> + assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromYou) + state.eventSink(ResolveVerifiedUserSendFailureEvents.Retry) + } + awaitItem().also { state -> + assertThat(state.retryAction).isEqualTo(AsyncAction.Loading) + } + skipItems(2) + awaitItem().also { state -> + assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None) + assertThat(state.retryAction).isEqualTo(AsyncAction.Success(Unit)) + } + ensureAllEventsConsumed() + } + } + + @Test + fun `present - verified user unsigned device failure resolve and resend scenario`() = runTest { + val room = FakeMatrixRoom( + userDisplayNameResult = { userId -> + Result.success(userId.value) + }, + ignoreDeviceTrustAndResendResult = { _, _ -> + Result.success(Unit) + }, + ) + val presenter = createResolveVerifiedUserSendFailurePresenter(room) + presenter.test { + val failedMessage = aVerifiedUserHasUnsignedDeviceFailedMessage() + val initialState = awaitItem() + assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None) + initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage)) + + skipItems(1) + awaitItem().also { state -> + assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromYou) + state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend) + } + awaitItem().also { state -> + assertThat(state.resolveAction).isEqualTo(AsyncAction.Loading) + } + // This should move to the next user + skipItems(2) + awaitItem().also { state -> + assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromOther(A_USER_ID_2.value)) + assertThat(state.resolveAction).isEqualTo(AsyncAction.Success(Unit)) + state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend) + } + + skipItems(3) + awaitItem().also { state -> + assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None) + } + ensureAllEventsConsumed() + } + } + + @Test + fun `present - verified user unsigned device failure resolve and resend scenario with error`() = runTest { + val room = FakeMatrixRoom( + userDisplayNameResult = { userId -> + Result.success(userId.value) + }, + ignoreDeviceTrustAndResendResult = { _, _ -> + Result.failure(Exception()) + }, + ) + val presenter = createResolveVerifiedUserSendFailurePresenter(room) + presenter.test { + val failedMessage = aVerifiedUserHasUnsignedDeviceFailedMessage() + val initialState = awaitItem() + assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None) + initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage)) + + skipItems(1) + awaitItem().also { state -> + assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromYou) + state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend) + } + awaitItem().also { state -> + assertThat(state.resolveAction).isEqualTo(AsyncAction.Loading) + } + awaitItem().also { state -> + assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromYou) + assertThat(state.resolveAction).isInstanceOf(AsyncAction.Failure::class.java) + } + ensureAllEventsConsumed() + } + } + + @Test + fun `present - verified user changed identity failure retry scenario`() = runTest { + val room = FakeMatrixRoom( + userDisplayNameResult = { userId -> + Result.success(userId.value) + }, + retrySendMessageResult = { + Result.success(Unit) + }, + ) + val presenter = createResolveVerifiedUserSendFailurePresenter(room) + presenter.test { + val failedMessage = aVerifiedUserChangedIdentityMessage() + val initialState = awaitItem() + assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None) + initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage)) + + skipItems(1) + awaitItem().also { state -> + assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.ChangedIdentity(A_USER_ID.value)) + state.eventSink(ResolveVerifiedUserSendFailureEvents.Retry) + } + awaitItem().also { state -> + assertThat(state.retryAction).isEqualTo(AsyncAction.Loading) + } + skipItems(2) + awaitItem().also { state -> + assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None) + assertThat(state.retryAction).isEqualTo(AsyncAction.Success(Unit)) + } + ensureAllEventsConsumed() + } + } + + @Test + fun `present - verified user changed identity failure resolve and resend scenario`() = runTest { + val room = FakeMatrixRoom( + userDisplayNameResult = { userId -> + Result.success(userId.value) + }, + withdrawVerificationAndResendResult = { _, _ -> + Result.success(Unit) + }, + ) + val presenter = createResolveVerifiedUserSendFailurePresenter(room) + presenter.test { + val failedMessage = aVerifiedUserChangedIdentityMessage() + val initialState = awaitItem() + assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None) + initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage)) + + skipItems(1) + awaitItem().also { state -> + assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.ChangedIdentity(A_USER_ID.value)) + state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend) + } + awaitItem().also { state -> + assertThat(state.resolveAction).isEqualTo(AsyncAction.Loading) + } + // This should move to the next user + skipItems(2) + awaitItem().also { state -> + assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.ChangedIdentity(A_USER_ID_2.value)) + assertThat(state.resolveAction).isEqualTo(AsyncAction.Success(Unit)) + state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend) + } + + skipItems(3) + awaitItem().also { state -> + assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None) + } + ensureAllEventsConsumed() + } + } + + @Test + fun `present - verified user changed identity failure resolve and resend scenario with error`() = runTest { + val room = FakeMatrixRoom( + userDisplayNameResult = { userId -> + Result.success(userId.value) + }, + withdrawVerificationAndResendResult = { _, _ -> + Result.failure(Exception()) + }, + ) + val presenter = createResolveVerifiedUserSendFailurePresenter(room) + presenter.test { + val failedMessage = aVerifiedUserChangedIdentityMessage() + val initialState = awaitItem() + assertThat(initialState.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.None) + initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage)) + + skipItems(1) + awaitItem().also { state -> + assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.ChangedIdentity(A_USER_ID.value)) + state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend) + } + awaitItem().also { state -> + assertThat(state.resolveAction).isEqualTo(AsyncAction.Loading) + } + awaitItem().also { state -> + assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.ChangedIdentity(A_USER_ID.value)) + assertThat(state.resolveAction).isInstanceOf(AsyncAction.Failure::class.java) + } + ensureAllEventsConsumed() + } + } + + private fun aVerifiedUserHasUnsignedDeviceFailedMessage(): TimelineItem.Event { + return aMessageEvent( + transactionId = A_TRANSACTION_ID, + sendState = LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice( + mapOf( + A_USER_ID to emptyList(), + A_USER_ID_2 to emptyList() + ) + ) + ) + } + + private fun aVerifiedUserChangedIdentityMessage(): TimelineItem.Event { + return aMessageEvent( + transactionId = A_TRANSACTION_ID, + sendState = LocalEventSendState.Failed.VerifiedUserChangedIdentity( + listOf(A_USER_ID, A_USER_ID_2) + ) + ) + } + + private fun createResolveVerifiedUserSendFailurePresenter( + room: MatrixRoom = FakeMatrixRoom(), + ): ResolveVerifiedUserSendFailurePresenter { + return ResolveVerifiedUserSendFailurePresenter( + room = room, + verifiedUserSendFailureFactory = VerifiedUserSendFailureFactory(room), + ) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt new file mode 100644 index 0000000000..e5d67848dd --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.crypto.sendfailure.resolve + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ResolveVerifiedUserSendFailureViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on resolve and resend emit the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setResolveVerifiedUserSendFailureView( + state = aResolveVerifiedUserSendFailureState( + verifiedUserSendFailure = aChangedIdentitySendFailure(), + eventSink = eventsRecorder, + ), + ) + + rule.clickOn(res = CommonStrings.screen_resolve_send_failure_changed_identity_primary_button_title) + eventsRecorder.assertSingle(ResolveVerifiedUserSendFailureEvents.ResolveAndResend) + } + + @Test + fun `clicking on retry emit the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setResolveVerifiedUserSendFailureView( + state = aResolveVerifiedUserSendFailureState( + verifiedUserSendFailure = aChangedIdentitySendFailure(), + eventSink = eventsRecorder, + ), + ) + + rule.clickOn(res = CommonStrings.action_retry) + eventsRecorder.assertSingle(ResolveVerifiedUserSendFailureEvents.Retry) + } + + private fun AndroidComposeTestRule.setResolveVerifiedUserSendFailureView( + state: ResolveVerifiedUserSendFailureState, + ) { + setContent { + ResolveVerifiedUserSendFailureView(state = state) + } + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt index 5c84fec8ac..efbe67a337 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt @@ -19,7 +19,6 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo @@ -138,34 +137,6 @@ class PinnedMessagesListPresenterTest { } } - @Test - fun `present - redact event`() = runTest { - val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(true) } - val pinnedEventsTimeline = createPinnedMessagesTimeline().apply { - this.redactEventLambda = redactEventLambda - } - val room = FakeMatrixRoom( - pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }, - canRedactOwnResult = { Result.success(true) }, - canRedactOtherResult = { Result.success(true) }, - canUserPinUnpinResult = { Result.success(true) }, - ).apply { - givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) - } - val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true) - presenter.test { - skipItems(3) - val filledState = awaitItem() as PinnedMessagesListState.Filled - val eventItem = filledState.timelineItems.first() as TimelineItem.Event - filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Redact, eventItem)) - advanceUntilIdle() - cancelAndIgnoreRemainingEvents() - assert(redactEventLambda) - .isCalledOnce() - .with(value(AN_EVENT_ID), value(null), value(null)) - } - } - @Test fun `present - unpin event`() = runTest { val successUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.success(true) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index 9df9dfc9f8..a0712907cc 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -13,6 +13,7 @@ import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.FakeMessagesNavigator +import io.element.android.features.messages.impl.crypto.sendfailure.resolve.aResolveVerifiedUserSendFailureState import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator import io.element.android.features.messages.impl.timeline.components.aCriticalShield @@ -680,6 +681,7 @@ import kotlin.time.Duration.Companion.seconds sessionPreferencesStore = sessionPreferencesStore, timelineItemIndexer = timelineItemIndexer, timelineController = TimelineController(room), + resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() }, ) } } diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05Test.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05Test.kt index 394c4a34b3..850674cde7 100644 --- a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05Test.kt +++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05Test.kt @@ -21,7 +21,7 @@ class AppMigration05Test { val sessionStore = InMemorySessionStore().apply { updateData( aSessionData( - sessionId = A_SESSION_ID, + sessionId = A_SESSION_ID.value, sessionPath = "", ) ) @@ -37,7 +37,7 @@ class AppMigration05Test { val sessionStore = InMemorySessionStore().apply { updateData( aSessionData( - sessionId = A_SESSION_ID, + sessionId = A_SESSION_ID.value, sessionPath = "/a/path/existing", ) ) diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration06Test.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration06Test.kt index 6e1b9a237d..f60a3af5ad 100644 --- a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration06Test.kt +++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration06Test.kt @@ -21,7 +21,7 @@ class AppMigration06Test { val sessionStore = InMemorySessionStore().apply { updateData( aSessionData( - sessionId = A_SESSION_ID, + sessionId = A_SESSION_ID.value, sessionPath = "/a/path/to/a/session/AN_ID", cachePath = "", ) @@ -38,7 +38,7 @@ class AppMigration06Test { val sessionStore = InMemorySessionStore().apply { updateData( aSessionData( - sessionId = A_SESSION_ID, + sessionId = A_SESSION_ID.value, cachePath = "/a/path/existing", ) ) diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt index 8ba1679a5e..b63ca0a12f 100644 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt @@ -39,8 +39,8 @@ import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconSource -import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings @@ -171,10 +171,9 @@ private fun OnBoardingButtons( .testTag(TestTags.onBoardingSignIn) ) if (state.canCreateAccount) { - OutlinedButton( + TextButton( text = stringResource(id = R.string.screen_onboarding_sign_up), onClick = onCreateAccount, - enabled = true, modifier = Modifier .fillMaxWidth() ) diff --git a/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt b/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt index fff5763221..8791992021 100644 --- a/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt +++ b/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt @@ -11,6 +11,7 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.appconfig.OnBoardingConfig import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService @@ -46,7 +47,7 @@ class OnBoardingPresenterTest { assertThat(initialState.isDebugBuild).isTrue() assertThat(initialState.canLoginWithQrCode).isFalse() assertThat(initialState.productionApplicationName).isEqualTo("B") - assertThat(initialState.canCreateAccount).isFalse() + assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT) assertThat(awaitItem().canLoginWithQrCode).isTrue() } diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index 27aceff0a1..39a8b95287 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { implementation(projects.features.ftue.api) implementation(projects.features.licenses.api) implementation(projects.features.logout.api) + implementation(projects.features.deactivation.api) implementation(projects.features.roomlist.api) implementation(projects.services.analytics.api) implementation(projects.services.toolbox.api) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt index f5229d4838..c1025a07af 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt @@ -22,6 +22,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.deactivation.api.AccountDeactivationEntryPoint import io.element.android.features.licenses.api.OpenSourceLicensesEntryPoint import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.logout.api.LogoutEntryPoint @@ -54,6 +55,7 @@ class PreferencesFlowNode @AssistedInject constructor( private val notificationTroubleShootEntryPoint: NotificationTroubleShootEntryPoint, private val logoutEntryPoint: LogoutEntryPoint, private val openSourceLicensesEntryPoint: OpenSourceLicensesEntryPoint, + private val accountDeactivationEntryPoint: AccountDeactivationEntryPoint, ) : BaseFlowNode( backstack = BackStack( initialElement = plugins.filterIsInstance().first().initialElement.toNavTarget(), @@ -105,6 +107,9 @@ class PreferencesFlowNode @AssistedInject constructor( @Parcelize data object SignOut : NavTarget + @Parcelize + data object AccountDeactivation : NavTarget + @Parcelize data object OssLicenses : NavTarget } @@ -160,6 +165,10 @@ class PreferencesFlowNode @AssistedInject constructor( override fun onSignOutClick() { backstack.push(NavTarget.SignOut) } + + override fun onOpenAccountDeactivation() { + backstack.push(NavTarget.AccountDeactivation) + } } createNode(buildContext, plugins = listOf(callback)) } @@ -254,6 +263,9 @@ class PreferencesFlowNode @AssistedInject constructor( is NavTarget.OssLicenses -> { openSourceLicensesEntryPoint.getNode(this, buildContext) } + NavTarget.AccountDeactivation -> { + accountDeactivationEntryPoint.createNode(this, buildContext) + } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt index 25cc166103..22884bc6ed 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt @@ -259,7 +259,7 @@ private fun InvalidNotificationSettingsView( ErrorDialog( title = stringResource(id = CommonStrings.dialog_title_error), content = stringResource(id = R.string.screen_notification_settings_failed_fixing_configuration), - onDismiss = onDismissError + onSubmit = onDismissError ) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt index 295e2c2df5..e400a99e05 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt @@ -47,6 +47,7 @@ class PreferencesRootNode @AssistedInject constructor( fun onOpenUserProfile(matrixUser: MatrixUser) fun onOpenBlockedUsers() fun onSignOutClick() + fun onOpenAccountDeactivation() } private fun onOpenBugReport() { @@ -111,6 +112,10 @@ class PreferencesRootNode @AssistedInject constructor( plugins().forEach { it.onSignOutClick() } } + private fun onOpenAccountDeactivation() { + plugins().forEach { it.onOpenAccountDeactivation() } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -139,6 +144,7 @@ class PreferencesRootNode @AssistedInject constructor( onSignOutClick() } }, + onDeactivateClick = this::onOpenAccountDeactivation ) directLogoutView.Render( diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt index 500570412d..f1507c45f4 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import io.element.android.features.logout.api.direct.DirectLogoutPresenter import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider import io.element.android.libraries.architecture.Presenter @@ -75,6 +76,12 @@ class PreferencesRootPresenter @Inject constructor( val devicesManagementUrl: MutableState = remember { mutableStateOf(null) } + var canDeactivateAccount by remember { + mutableStateOf(false) + } + LaunchedEffect(Unit) { + canDeactivateAccount = matrixClient.canDeactivateAccount() + } val showBlockedUsersItem by produceState(initialValue = false) { matrixClient.ignoredUsersFlow @@ -108,6 +115,7 @@ class PreferencesRootPresenter @Inject constructor( devicesManagementUrl = devicesManagementUrl.value, showAnalyticsSettings = hasAnalyticsProviders, showDeveloperSettings = showDeveloperSettings, + canDeactivateAccount = canDeactivateAccount, showNotificationSettings = showNotificationSettings.value, showLockScreenSettings = showLockScreenSettings.value, showBlockedUsersItem = showBlockedUsersItem, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt index f6db1cbec1..4ece77a55c 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt @@ -22,6 +22,7 @@ data class PreferencesRootState( val devicesManagementUrl: String?, val showAnalyticsSettings: Boolean, val showDeveloperSettings: Boolean, + val canDeactivateAccount: Boolean, val showLockScreenSettings: Boolean, val showNotificationSettings: Boolean, val showBlockedUsersItem: Boolean, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt index 464288fd9b..c91a7e1adc 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt @@ -29,6 +29,7 @@ fun aPreferencesRootState( showNotificationSettings = true, showLockScreenSettings = true, showBlockedUsersItem = true, + canDeactivateAccount = true, snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete), directLogoutState = aDirectLogoutState(), eventSink = eventSink, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt index 45bf7aaf07..f4e3e8be13 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt @@ -63,6 +63,7 @@ fun PreferencesRootView( onOpenUserProfile: (MatrixUser) -> Unit, onOpenBlockedUsers: () -> Unit, onSignOutClick: () -> Unit, + onDeactivateClick: () -> Unit, modifier: Modifier = Modifier, ) { val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) @@ -112,6 +113,7 @@ fun PreferencesRootView( onOpenAdvancedSettings = onOpenAdvancedSettings, onOpenDeveloperSettings = onOpenDeveloperSettings, onSignOutClick = onSignOutClick, + onDeactivateClick = onDeactivateClick, ) Footer( @@ -206,6 +208,7 @@ private fun ColumnScope.GeneralSection( onOpenAdvancedSettings: () -> Unit, onOpenDeveloperSettings: () -> Unit, onSignOutClick: () -> Unit, + onDeactivateClick: () -> Unit, ) { ListItem( headlineContent = { Text(stringResource(id = CommonStrings.common_about)) }, @@ -238,6 +241,14 @@ private fun ColumnScope.GeneralSection( style = ListItemStyle.Destructive, onClick = onSignOutClick, ) + if (state.canDeactivateAccount) { + ListItem( + headlineContent = { Text(stringResource(id = CommonStrings.action_deactivate_account)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Warning())), + style = ListItemStyle.Destructive, + onClick = onDeactivateClick, + ) + } } @Composable @@ -306,5 +317,6 @@ private fun ContentToPreview(matrixUser: MatrixUser) { onOpenUserProfile = {}, onOpenBlockedUsers = {}, onSignOutClick = {}, + onDeactivateClick = {}, ) } diff --git a/features/preferences/impl/src/main/res/values-nl/translations.xml b/features/preferences/impl/src/main/res/values-nl/translations.xml index 6296b409b2..06f4c6cd26 100644 --- a/features/preferences/impl/src/main/res/values-nl/translations.xml +++ b/features/preferences/impl/src/main/res/values-nl/translations.xml @@ -6,12 +6,14 @@ "Aangepaste basis-URL voor Element Call" "Stel een aangepaste basis-URL in voor Element Call." "Ongeldige URL, zorg ervoor dat je het protocol (http/https) en het juiste adres invult." + "Push-meldingen provider" "Schakel de uitgebreide tekstverwerker uit om Markdown handmatig te typen." "Leesbevestigingen" "Indien uitgeschakeld worden er geen leesbevestigingen verstuurd. Je ontvangt nog steeds leesbevestigingen van andere gebruikers." "Aanwezigheid delen" "Indien uitgeschakeld kun je geen leesbevestigingen en typmeldingen verzenden of ontvangen." "Schakel optie in om de berichtbron in de tijdlijn te bekijken." + "Je hebt geen geblokkeerde gebruikers." "Deblokkeren" "Je zult alle berichten van hen weer kunnen zien." "Gebruiker deblokkeren" @@ -49,4 +51,6 @@ Als je doorgaat, kunnen sommige van je instellingen veranderen." "systeeminstellingen" "Systeemmeldingen uitgeschakeld" "Meldingen" + "Problemen oplossen" + "Problemen met meldingen oplossen" diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index 4e276701f4..774d52076a 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -10,6 +10,7 @@ package io.element.android.features.preferences.impl.root import androidx.compose.runtime.Composable import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow +import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.logout.api.direct.DirectLogoutPresenter @@ -45,7 +46,7 @@ class PreferencesRootPresenterTest { @Test fun `present - initial state`() = runTest { - val matrixClient = FakeMatrixClient() + val matrixClient = FakeMatrixClient(canDeactivateAccountResult = { true }) val presenter = createPresenter(matrixClient = matrixClient) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -76,11 +77,27 @@ class PreferencesRootPresenterTest { assertThat(loadedState.showDeveloperSettings).isTrue() assertThat(loadedState.showLockScreenSettings).isTrue() assertThat(loadedState.showNotificationSettings).isTrue() + assertThat(loadedState.canDeactivateAccount).isTrue() assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState) assertThat(loadedState.snackbarMessage).isNull() } } + @Test + fun `present - can deactivate account is false if the Matrix client say so`() = runTest { + val presenter = createPresenter( + matrixClient = FakeMatrixClient( + canDeactivateAccountResult = { false } + ) + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val loadedState = awaitFirstItem() + assertThat(loadedState.canDeactivateAccount).isFalse() + } + } + @Test fun `present - developer settings is hidden by default in release builds`() = runTest { val presenter = createPresenter( @@ -89,8 +106,7 @@ class PreferencesRootPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) - val loadedState = awaitItem() + val loadedState = awaitFirstItem() assertThat(loadedState.showDeveloperSettings).isFalse() } } @@ -103,20 +119,22 @@ class PreferencesRootPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) - val loadedState = awaitItem() - + val loadedState = awaitFirstItem() repeat(times = ShowDeveloperSettingsProvider.DEVELOPER_SETTINGS_COUNTER) { assertThat(loadedState.showDeveloperSettings).isFalse() loadedState.eventSink(PreferencesRootEvents.OnVersionInfoClick) } - assertThat(awaitItem().showDeveloperSettings).isTrue() } } + private suspend fun ReceiveTurbine.awaitFirstItem(): T { + skipItems(1) + return awaitItem() + } + private fun createPresenter( - matrixClient: FakeMatrixClient = FakeMatrixClient(), + matrixClient: FakeMatrixClient = FakeMatrixClient(canDeactivateAccountResult = { true }), sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), showDeveloperSettingsProvider: ShowDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.DEBUG)), ) = PreferencesRootPresenter( diff --git a/features/rageshake/impl/build.gradle.kts b/features/rageshake/impl/build.gradle.kts index 83b66551cd..f7286672f0 100644 --- a/features/rageshake/impl/build.gradle.kts +++ b/features/rageshake/impl/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { testImplementation(libs.test.mockk) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.sessionStorage.implMemory) + testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.features.rageshake.test) testImplementation(projects.tests.testutils) testImplementation(projects.services.toolbox.test) diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt index 45aaf6eeda..a30112034e 100755 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt @@ -17,9 +17,8 @@ import io.element.android.libraries.matrix.test.FakeSdkMetadata import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.network.useragent.DefaultUserAgentProvider -import io.element.android.libraries.sessionstorage.api.LoginType -import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -94,7 +93,7 @@ class DefaultBugReporterTest { server.start() val mockSessionStore = InMemorySessionStore().apply { - storeData(mockSessionData("@foo:eample.com", "ABCDEFGH")) + storeData(aSessionData(sessionId = "@foo:example.com", deviceId = "ABCDEFGH")) } val buildMeta = aBuildMeta() @@ -143,7 +142,7 @@ class DefaultBugReporterTest { assertThat(foundValues["can_contact"]).isEqualTo("true") assertThat(foundValues["device_id"]).isEqualTo("ABCDEFGH") assertThat(foundValues["sdk_sha"]).isEqualTo("123456789") - assertThat(foundValues["user_id"]).isEqualTo("@foo:eample.com") + assertThat(foundValues["user_id"]).isEqualTo("@foo:example.com") assertThat(foundValues["text"]).isEqualTo("a bug occurred") assertThat(foundValues["device_keys"]).isEqualTo("curve25519:CURVECURVECURVE, ed25519:EDKEYEDKEYEDKY") @@ -163,7 +162,7 @@ class DefaultBugReporterTest { server.start() val mockSessionStore = InMemorySessionStore().apply { - storeData(mockSessionData("@foo:eample.com", "ABCDEFGH")) + storeData(aSessionData("@foo:example.com", "ABCDEFGH")) } val buildMeta = aBuildMeta() @@ -267,21 +266,6 @@ class DefaultBugReporterTest { return foundValues } - private fun mockSessionData(userId: String, deviceId: String) = SessionData( - userId = userId, - deviceId = deviceId, - homeserverUrl = "example.com", - accessToken = "AA", - isTokenValid = true, - loginType = LoginType.DIRECT, - loginTimestamp = null, - oidcData = null, - refreshToken = null, - slidingSyncProxy = null, - passphrase = null, - sessionPath = "session", - cachePath = "cache", - ) @Test fun `test sendBugReport error`() = runTest { val server = MockWebServer() diff --git a/features/roomaliasresolver/impl/src/main/res/values-nl/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-nl/translations.xml new file mode 100644 index 0000000000..9bf0f7156b --- /dev/null +++ b/features/roomaliasresolver/impl/src/main/res/values-nl/translations.xml @@ -0,0 +1,4 @@ + + + "Kan het kameradres niet vinden." + diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsView.kt index bfc19026a9..a3f39895df 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsView.kt @@ -124,7 +124,7 @@ fun RolesAndPermissionsView( is AsyncAction.Failure -> { ErrorDialog( content = stringResource(CommonStrings.error_unknown), - onDismiss = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) } + onSubmit = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) } ) } else -> Unit diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt index 2f7cfb16a0..ce362373f2 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt @@ -210,7 +210,7 @@ fun ChangeRolesView( is AsyncAction.Failure -> { ErrorDialog( content = stringResource(CommonStrings.error_unknown), - onDismiss = { state.eventSink(ChangeRolesEvent.ClearError) } + onSubmit = { state.eventSink(ChangeRolesEvent.ClearError) } ) } is AsyncAction.Success -> { diff --git a/features/roomdetails/impl/src/main/res/values-nl/translations.xml b/features/roomdetails/impl/src/main/res/values-nl/translations.xml index 117ef56b9f..a3a4dc965a 100644 --- a/features/roomdetails/impl/src/main/res/values-nl/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-nl/translations.xml @@ -3,14 +3,41 @@ "Er is een fout opgetreden bij het bijwerken van de meldingsinstelling." "Je homeserver ondersteunt deze optie niet in versleutelde kamers; in sommige kamers krijg je mogelijk geen meldingen." "Peilingen" + "Alleen beheerders" + "Personen verbannen" + "Berichten verwijderen" "Iedereen" + "Personen uitnodigen" + "Moderatie van leden" + "Berichten en inhoud" + "Beheerders en moderators" + "Personen verwijderen" + "Kamerafbeelding wijzigen" + "Kamergegevens" + "Kamernaam wijzigen" + "Kameronderwerp wijzigen" + "Berichten verzenden" + "Beheerders bewerken" "Je kunt deze actie niet ongedaan maken. Je bevordert deze gebruiker tot hetzelfde machtsniveau als jij." "Beheerder toevoegen?" + "Degraderen" + "Je kunt deze wijziging niet ongedaan maken omdat je jezelf degradeert. Als je de laatste gebruiker met bevoegdheden in de kamer bent, is het onmogelijk om deze bevoegdheden terug te krijgen." "Jezelf degraderen?" + "%1$s (In behandeling)" + "(In afwachting)" + "Beheerders hebben automatisch moderatorrechten" + "Moderators bewerken" + "Beheerders" + "Moderators" "Leden" + "Je hebt niet-opgeslagen wijzigingen" + "Wijzigingen opslaan?" "Onderwerp toevoegen" "Reeds lid" "Reeds uitgenodigd" + "Versleuteld" + "Niet versleuteld" + "Openbare kamer" "Kamer bewerken" "Er is een onbekende fout opgetreden en de informatie kon niet worden gewijzigd." "Kan kamer niet bijwerken" @@ -25,20 +52,30 @@ "Aangepast" "Standaard" "Meldingen" + "Rollen en rechten" "Naam van de kamer" "Beveiliging" "Kamer delen" + "Kamer info" "Onderwerp" "Kamer bijwerken…" + "Verbannen" + "Ze kunnen niet meer toetreden tot deze kamer als ze worden uitgenodigd." + "Weet je zeker dat je dit lid wilt verbannen?" + "Er zijn geen verbannen gebruikers in deze kamer." + "%1$s verbannen" "%1$d persoon" "%1$d personen" + "Lid verwijderen en verbannen" "Verwijderen uit kamer" "Lid verwijderen en verbannen" "Alleen lid verwijderen" "Lid verwijderen en toekomstige deelname verbieden?" "Ontbannen" + "Ze kunnen opnieuw tot de kamer toetreden als ze worden uitgenodigd." + "Ontban gebruiker" "Profiel bekijken" "Verbannen" "Leden" @@ -47,6 +84,7 @@ "Beheerder" "Moderator" "Kamerleden" + "%1$s ontbannen" "Aanpassen toestaan" "Als je dit inschakelt, wordt je standaardinstelling overschreven" "Stuur me een melding in deze chat voor" @@ -61,4 +99,18 @@ "Alle berichten" "Alleen vermeldingen en trefwoorden" "In deze kamer, stuur me een melding voor" + "Beheerders" + "Mijn rol wijzigen" + "Degraderen tot lid" + "Degraderen tot moderator" + "Moderatie van leden" + "Berichten en inhoud" + "Moderators" + "Rechten" + "Rechten opnieuw instellen" + "Als je de rechten opnieuw instelt, raak je de huidige instellingen kwijt." + "Rechten opnieuw instellen?" + "Rollen" + "Kamergegevens" + "Rollen en rechten" diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/MatrixRoomFixture.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/MatrixRoomFixture.kt index 0bcb881af6..8ff03a69b9 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/MatrixRoomFixture.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/MatrixRoomFixture.kt @@ -31,7 +31,7 @@ fun aMatrixRoom( emitRoomInfo: Boolean = false, canInviteResult: (UserId) -> Result = { lambdaError() }, canSendStateResult: (UserId, StateEventType) -> Result = { _, _ -> lambdaError() }, - userDisplayNameResult: () -> Result = { lambdaError() }, + userDisplayNameResult: (UserId) -> Result = { lambdaError() }, userAvatarUrlResult: () -> Result = { lambdaError() }, setNameResult: (String) -> Result = { lambdaError() }, setTopicResult: (String) -> Result = { lambdaError() }, diff --git a/features/roomdirectory/impl/src/main/res/values-nl/translations.xml b/features/roomdirectory/impl/src/main/res/values-nl/translations.xml new file mode 100644 index 0000000000..a2c6da3d09 --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-nl/translations.xml @@ -0,0 +1,5 @@ + + + "Laden mislukt" + "Kamergids" + diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index b1b5e0ffd5..c4ec507064 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -73,7 +73,7 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first @@ -85,6 +85,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject private const val EXTENDED_RANGE_SIZE = 40 +private const val SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS = 300L class RoomListPresenter @Inject constructor( private val client: MatrixClient, @@ -338,7 +339,10 @@ class RoomListPresenter @Inject constructor( private var currentUpdateVisibleRangeJob: Job? = null private fun CoroutineScope.updateVisibleRange(range: IntRange, spaceNavEnabled: Boolean) { currentUpdateVisibleRangeJob?.cancel() - currentUpdateVisibleRangeJob = launch(SupervisorJob()) { + currentUpdateVisibleRangeJob = launch { + // Debounce the subscription to avoid subscribing to too many rooms + delay(SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS) + if (range.isEmpty()) return@launch val currentRoomList = if (spaceNavEnabled) spaceAwareRoomListDataSource.spaceRooms.first() else roomListDataSource.allRooms.first() // Use extended range to 'prefetch' the next rooms info diff --git a/features/roomlist/impl/src/main/res/values-cs/translations.xml b/features/roomlist/impl/src/main/res/values-cs/translations.xml index 69779542a9..52a18f8e9c 100644 --- a/features/roomlist/impl/src/main/res/values-cs/translations.xml +++ b/features/roomlist/impl/src/main/res/values-cs/translations.xml @@ -1,5 +1,9 @@ + "Odhlásit se a upgradovat" + "Váš server nyní podporuje nový, rychlejší protokol. Chcete-li upgradovat, odhlaste se a znovu se přihlaste. Pokud to uděláte nyní, pomůže vám vyhnout se nucenému odhlášení, když bude starý protokol později odstraněn." + "Váš domovský server již nepodporuje starý protokol. Chcete-li pokračovat v používání aplikace, odhlaste se a znovu se přihlaste." + "Upgrade k dispozici" "Vygenerujte nový klíč pro obnovení, který lze použít k obnovení historie šifrovaných zpráv v případě, že ztratíte přístup ke svým zařízením." "Nastavení obnovy" "Vaše záloha chatu není aktuálně synchronizována. Abyste si zachovali přístup k záloze chatu, musíte potvrdit klíč pro obnovení." diff --git a/features/roomlist/impl/src/main/res/values-de/translations.xml b/features/roomlist/impl/src/main/res/values-de/translations.xml index 6215b3d4e4..372e918e09 100644 --- a/features/roomlist/impl/src/main/res/values-de/translations.xml +++ b/features/roomlist/impl/src/main/res/values-de/translations.xml @@ -1,5 +1,10 @@ + "Abmelden und aktualisieren" + "Dein Server unterstützt jetzt ein neues, schnelleres Protokoll. Melde dich ab und melde dich wieder an, um zu aktualisieren. Wenn du das jetzt tust, vermeidest du eine erzwungene Abmeldung, wenn das alte Protokoll später entfernt wird." + "Dein Homeserver unterstützt das alte Protokoll nicht mehr. Bitte logge dich aus und melde dich wieder an, um die App weiter zu nutzen." + "Aktualisierung verfügbar" + "Wiederherstellung einrichten" "Dein Chat-Backup ist derzeit nicht synchronisiert. Du musst deinen Wiederherstellungsschlüssel bestätigen, um Zugriff auf dein Chat-Backup zu erhalten." "Wiederherstellungsschlüssel bestätigen." "Damit du keinen wichtigen Anruf verpasst, ändere bitte deine Einstellungen so, dass du bei gesperrtem Telefon Benachrichtigungen im Vollbildmodus erhältst." diff --git a/features/roomlist/impl/src/main/res/values-el/translations.xml b/features/roomlist/impl/src/main/res/values-el/translations.xml index 71aca711ad..a7e2a0f79f 100644 --- a/features/roomlist/impl/src/main/res/values-el/translations.xml +++ b/features/roomlist/impl/src/main/res/values-el/translations.xml @@ -1,5 +1,11 @@ + "Αποσύνδεση & Αναβάθμιση" + "Ο διακομιστής σου υποστηρίζει τώρα ένα νέο, ταχύτερο πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για αναβάθμιση τώρα. Κάνοντας αυτό τώρα θα σε βοηθήσει να αποφύγεις μια αναγκαστική αποσύνδεση όταν το παλιό πρωτόκολλο καταργηθεί αργότερα." + "Ο οικιακός διακομιστής σου δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για να συνεχίσεις να χρησιμοποιείς την εφαρμογή." + "Διαθέσιμη αναβάθμιση" + "Δημιούργησε ένα νέο κλειδί ανάκτησης που μπορεί να χρησιμοποιηθεί για την επαναφορά του ιστορικού των κρυπτογραφημένων μηνυμάτων σου σε περίπτωση που χάσεις την πρόσβαση στις συσκευές σου." + "Ρύθμιση ανάκτησης" "Το αντίγραφο ασφαλείας της συνομιλίας σου δεν είναι συγχρονισμένο αυτήν τη στιγμή. Πρέπει να εισαγάγεις το κλειδί ανάκτησης για να διατηρήσεις την πρόσβαση στο αντίγραφο ασφαλείας της συνομιλίας σου." "Εισήγαγε το κλειδί ανάκτησης" "Για να διασφαλίσεις ότι δεν θα χάσεις ποτέ μια σημαντική κλήση, άλλαξε τις ρυθμίσεις σου για να επιτρέψεις τις ειδοποιήσεις πλήρους οθόνης όταν το τηλέφωνό σου είναι κλειδωμένο." diff --git a/features/roomlist/impl/src/main/res/values-et/translations.xml b/features/roomlist/impl/src/main/res/values-et/translations.xml index e64e0fc6e0..a8f61ea37a 100644 --- a/features/roomlist/impl/src/main/res/values-et/translations.xml +++ b/features/roomlist/impl/src/main/res/values-et/translations.xml @@ -1,5 +1,9 @@ + "Logi välja ja uuenda" + "Sinu koduserver toetab uut ja kiiremat protokolli. Uuendamiseks logi korraks rakendusest välja ja siis tagasi. Mingil hetkel tulevikus vana protokoll eemaldatakse kasutusest ja tehes uuenduse nüüd väldid hilisemat sundkorras uuendust." + "Sinu koduserver enam ei toeta vana protokolli. Jätkamaks rakenduse kasutamist palun logi välja ning seejärel tagasi." + "Saadaval on uuendus" "Loo uus taastevõti, mida saad kasutada oma krüptitud sõnumite ajaloo taastamisel olukorras, kus kaotad ligipääsu oma seadmetele." "Seadista taastamine" "Sinu vestluste varukoopia pole hetkel sünkroonis. Säilitamaks ligipääsu vestluse varukoopiale palun sisesta oma taastevõti." diff --git a/features/roomlist/impl/src/main/res/values-fr/translations.xml b/features/roomlist/impl/src/main/res/values-fr/translations.xml index e2f3b22fe1..fb2188a8a4 100644 --- a/features/roomlist/impl/src/main/res/values-fr/translations.xml +++ b/features/roomlist/impl/src/main/res/values-fr/translations.xml @@ -1,5 +1,11 @@ + "Déconnecter et mettre à niveau" + "Votre serveur prend désormais en charge un nouveau protocole plus rapide. Déconnectez-vous, puis reconnectez-vous pour effectuer la mise à niveau dès maintenant. En le faisant tout de suite, vous éviterez une déconnexion forcée lorsque l’ancien protocole sera supprimé." + "Votre serveur d’accueil ne prend plus en charge l’ancien protocole. Veuillez vous déconnecter puis vous reconnecter pour continuer à utiliser l’application." + "Mise à niveau disponible" + "Générez une nouvelle clé de récupération qui peut être utilisée pour restaurer l’historique de vos messages chiffrés au cas où vous perdriez l’accès à vos appareils." + "Configurer la récupération" "La sauvegarde des conversations est désynchronisée. Vous devez confirmer la clé de récupération pour accéder à votre historique." "Confirmer votre clé de récupération" "Afin de ne jamais manquer un appel important, veuillez modifier vos paramètres pour autoriser les notifications en plein écran lorsque votre appareil est verrouillé." diff --git a/features/roomlist/impl/src/main/res/values-hu/translations.xml b/features/roomlist/impl/src/main/res/values-hu/translations.xml index 2325b7ae21..20006fd466 100644 --- a/features/roomlist/impl/src/main/res/values-hu/translations.xml +++ b/features/roomlist/impl/src/main/res/values-hu/translations.xml @@ -1,5 +1,9 @@ + "Kijelentkezés és frissítés" + "A kiszolgálója mostantól egy új, gyorsabb protokollt támogat. A frissítéshez jelentkezzen ki, majd jelentkezzen be újra. Ha ezt most megteszi, elkerülheti a kényszerített kijelentkeztetést a régi protokollt eltávolításakor." + "A Matrix-kiszolgáló már nem támogatja a régi protokollt. Az alkalmazás további használatához jelentkezzen ki és be." + "Frissítés érhető el" "Hozzon létre egy új helyreállítási kulcsot, amellyel visszaállíthatja a titkosított üzenetek előzményeit, ha elveszíti az eszközökhöz való hozzáférést." "Helyreállítás beállítása" "A csevegés biztonsági mentése nincs szinkronban. Meg kell erősítenie a helyreállítási kulcsát, hogy továbbra is hozzáférjen a csevegés biztonsági mentéséhez." diff --git a/features/roomlist/impl/src/main/res/values-nl/translations.xml b/features/roomlist/impl/src/main/res/values-nl/translations.xml index a20e0a695c..b20bad6cdd 100644 --- a/features/roomlist/impl/src/main/res/values-nl/translations.xml +++ b/features/roomlist/impl/src/main/res/values-nl/translations.xml @@ -14,13 +14,25 @@ "Ga aan de slag door iemand een bericht te sturen." "Nog geen chats." "Favorieten" + "Je kunt een chat toevoegen aan je favorieten in de chatinstellingen. +Voor nu kun je filters deselecteren om je andere chats te zien" + "Je hebt nog geen favoriete chats" + "Uitnodigingen" + "Je hebt geen openstaande uitnodigingen." "Lage prioriteit" + "Je kunt filters deselecteren om je andere chats te zien" + "Je hebt geen chats voor deze selectie" "Personen" + "Je hebt nog geen directe chats" "Kamers" + "Je zit nog niet in een kamer" "Ongelezen" + "Gefeliciteerd! +Je hebt geen ongelezen berichten!" "Chats" "Markeren als gelezen" "Markeren als ongelezen" + "Blader door alle kamers" "Het lijkt erop dat je een nieuw apparaat gebruikt. Verifieer met een ander apparaat om toegang te krijgen tot je versleutelde berichten." "Verifieer dat jij het bent" diff --git a/features/roomlist/impl/src/main/res/values-ru/translations.xml b/features/roomlist/impl/src/main/res/values-ru/translations.xml index c9ccc1f44f..a057f9284d 100644 --- a/features/roomlist/impl/src/main/res/values-ru/translations.xml +++ b/features/roomlist/impl/src/main/res/values-ru/translations.xml @@ -1,5 +1,11 @@ + "Выйти и обновить" + "Теперь ваш сервер поддерживает новый, более быстрый протокол. Выйдите из системы и снова войдите в систему для обновления прямо сейчас. Сделав это сейчас, вы сможете избежать принудительного выхода из системы при последующем удалении старого протокола." + "Ваш homeserver больше не поддерживает старый протокол. Пожалуйста, выйдите из системы и войдите снова, чтобы продолжить использование приложения." + "Доступно обновление" + "Создайте новый ключ восстановления, который можно использовать для восстановления зашифрованной истории сообщений в случае потери доступа к своим устройствам." + "Настроить восстановление" "В настоящее время резервная копия вашего чата не синхронизирована. Требуется подтвердить вашим ключом восстановления, чтобы сохранить доступ к резервной копии чата." "Введите " diff --git a/features/roomlist/impl/src/main/res/values-sk/translations.xml b/features/roomlist/impl/src/main/res/values-sk/translations.xml index 3d6e215cc6..f1d15ebbf1 100644 --- a/features/roomlist/impl/src/main/res/values-sk/translations.xml +++ b/features/roomlist/impl/src/main/res/values-sk/translations.xml @@ -1,5 +1,9 @@ + "Odhlásiť sa a aktualizovať" + "Váš server teraz podporuje nový, rýchlejší protokol. Odhláste sa a prihláste sa znova, aby ste mohli aktualizovať. Ak to urobíte teraz, pomôže vám vyhnúť sa nútenému odhláseniu, keď sa starý protokol neskôr odstráni." + "Váš domovský server už nepodporuje starý protokol. Ak chcete pokračovať v používaní aplikácie, odhláste sa a znova sa prihláste." + "Aktualizácia je k dispozícii" "Vytvorte nový kľúč na obnovenie, ktorý môžete použiť na obnovenie vašej histórie šifrovaných správ v prípade straty prístupu k vašim zariadeniam." "Nastaviť obnovenie" "Vaša záloha konverzácie nie je momentálne synchronizovaná. Na zachovanie prístupu k zálohe konverzácie musíte potvrdiť svoj kľúč na obnovu." diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml index ff6c626c63..36dc4b8d65 100644 --- a/features/roomlist/impl/src/main/res/values/localazy.xml +++ b/features/roomlist/impl/src/main/res/values/localazy.xml @@ -2,6 +2,7 @@ "Log Out & Upgrade" "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later." + "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app." "Upgrade available" "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices." "Set up recovery" diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt index 8b7294b165..c77f59a0a8 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt @@ -85,13 +85,16 @@ import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +import kotlin.time.Duration.Companion.seconds class RoomListPresenterTest { @get:Rule @@ -599,6 +602,38 @@ class RoomListPresenterTest { } } + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - UpdateVisibleRange will cancel the previous subscription if called too soon`() = runTest { + val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List -> } + val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda) + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val matrixClient = FakeMatrixClient( + roomListService = roomListService, + ) + val roomSummary = aRoomSummary( + currentUserMembership = CurrentUserMembership.INVITED + ) + roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) + roomListService.postAllRooms(listOf(roomSummary)) + val presenter = createRoomListPresenter( + coroutineScope = scope, + client = matrixClient, + ) + presenter.test { + val state = consumeItemsUntilPredicate { + it.contentState is RoomListContentState.Rooms + }.last() + + state.eventSink(RoomListEvents.UpdateVisibleRange(IntRange(0, 10))) + // If called again, it will cancel the current one, which should not result in a test failure + state.eventSink(RoomListEvents.UpdateVisibleRange(IntRange(0, 11))) + advanceTimeBy(1.seconds) + subscribeToVisibleRoomsLambda.assertions().isCalledOnce() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) @Test fun `present - UpdateVisibleRange subscribes to rooms in visible range`() = runTest { val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List -> } @@ -622,10 +657,12 @@ class RoomListPresenterTest { }.last() state.eventSink(RoomListEvents.UpdateVisibleRange(IntRange(0, 10))) + advanceTimeBy(1.seconds) subscribeToVisibleRoomsLambda.assertions().isCalledOnce() - // If called again, it will cancel the current one, which should not result in a test failure + // If called again, it will subscribe to the next items state.eventSink(RoomListEvents.UpdateVisibleRange(IntRange(0, 11))) + advanceTimeBy(1.seconds) subscribeToVisibleRoomsLambda.assertions().isCalledExactly(2) } } diff --git a/features/securebackup/impl/src/main/res/values-be/translations.xml b/features/securebackup/impl/src/main/res/values-be/translations.xml index 75e4c2ccb9..910350f41e 100644 --- a/features/securebackup/impl/src/main/res/values-be/translations.xml +++ b/features/securebackup/impl/src/main/res/values-be/translations.xml @@ -38,7 +38,6 @@ "Паўтарыце спробу, каб пацвердзіць доступ да рэзервовай копіі чата." "Няправільны ключ аднаўлення" "Калі ў вас ёсць ключ аднаўлення або парольная фраза, гэта таксама будзе працаваць." - "Ключ аднаўлення або код доступу" "Увесці…" "Страцілі ключ аднаўлення?" "Ключ аднаўлення пацверджаны" diff --git a/features/securebackup/impl/src/main/res/values-cs/translations.xml b/features/securebackup/impl/src/main/res/values-cs/translations.xml index 6425f52b66..ba113f9c89 100644 --- a/features/securebackup/impl/src/main/res/values-cs/translations.xml +++ b/features/securebackup/impl/src/main/res/values-cs/translations.xml @@ -39,7 +39,6 @@ "Zkuste prosím znovu potvrdit přístup k záloze chatu." "Nesprávný klíč pro obnovení" "Pokud máte bezpečnostní klíč nebo bezpečnostní frázi, bude to fungovat také." - "Klíč pro obnovení nebo přístupový kód" "Zadejte…" "Ztratili jste klíč pro obnovení?" "Klíč pro obnovení potvrzen" diff --git a/features/securebackup/impl/src/main/res/values-de/translations.xml b/features/securebackup/impl/src/main/res/values-de/translations.xml index c95e51db7e..3b95814763 100644 --- a/features/securebackup/impl/src/main/res/values-de/translations.xml +++ b/features/securebackup/impl/src/main/res/values-de/translations.xml @@ -29,13 +29,13 @@ "Wiederherstellungsschlüssel" " mit einem anderen Gerät" + "Zurücksetzen fortsetzen" "Ausschalten" "Du verlierst deine verschlüsselten Nachrichten, wenn du auf allen Geräten abgemeldet bist." "Bist du sicher, dass du das Backup deaktivieren willst?" - "Wenn du das Backup deaktivierst, wird dein aktuelles Backup des Verschlüsselungsschlüssels entfernt und andere Sicherheitsfunktionen werden deaktiviert. -Das bedeutet:" - "Keine Historie für verschlüsselte Nachrichten auf neuen Geräten ." - "Du verlierst den Zugriff auf deine verschlüsselten Nachrichten, wenn du dich überall von %1$s abmeldest" + "Wenn du das Backup deaktivierst, wird dein aktuelles Schlüssel Backup für die Verschlüsselung entfernt und andere Sicherheitsfunktionen werden deaktiviert. Das bedeutet:" + "Keinen Nachrichtenverlauf für verschlüsselte Nachrichten auf neuen Geräten ." + "Verlust des Zugriffs auf Deine verschlüsselten Nachrichten, wenn Du Dich überall von %1$s abmeldest" "Bist du sicher, dass du das Backup deaktivieren willst?" "Hier kannst Du einen neuen Wiederherstellungsschlüssel erstellen. Nachdem Du einen neuen Wiederherstellungsschlüssel erstellt hast, funktioniert dein alter Schlüssel nicht mehr." "Wiederherstellungsschlüssel erstellen" @@ -51,10 +51,6 @@ Das bedeutet:" "Bitte versuche es noch einmal, um den Zugriff auf dein Chat-Backup zu bestätigen." "Falscher Wiederherstellungsschlüssel" "Dies funktioniert auch mit einem Sicherheitsschlüssel oder Sicherheitsphrase." - - "Wiederherstellungsschlüssel" - " oder Passcode" - "Eingeben…" "Hast du deinen Wiederherstellungschlüssel vergessen?" "Wiederherstellungsschlüssel bestätigt" @@ -72,4 +68,6 @@ Das bedeutet:" "Stelle sicher, dass du deinen Wiederherstellungsschlüssel an einem sicheren Ort aufbewahren kannst" "Einrichtung der Wiederherstellung erfolgreich" "Wiederherstellung einrichten" + "Es ist ein unbekannter Fehler aufgetreten. Bitte überprüfe das Passwort deines Kontos und versuche es erneut." + "Eingeben…" diff --git a/features/securebackup/impl/src/main/res/values-el/translations.xml b/features/securebackup/impl/src/main/res/values-el/translations.xml index e62f465743..f4574b523d 100644 --- a/features/securebackup/impl/src/main/res/values-el/translations.xml +++ b/features/securebackup/impl/src/main/res/values-el/translations.xml @@ -16,6 +16,12 @@ "Ακολούθησε τις οδηγίες για να δημιουργήσεις ένα νέο κλειδί ανάκτησης" "Αποθήκευσε το νέο κλειδί ανάκτησης σε έναν διαχειριστή κωδικών πρόσβασης ή σε κρυπτογραφημένη σημείωση" "Επανάφερε την κρυπτογράφηση για το λογαριασμό σου χρησιμοποιώντας άλλη συσκευή" + "Συνέχιση επαναφοράς" + "Τα στοιχεία του λογαριασμού σου, οι επαφές, οι προτιμήσεις και η λίστα συνομιλιών θα διατηρηθούν" + "Θα χάσεις το υπάρχον ιστορικό μηνυμάτων σου" + "Θα χρειαστεί να επαληθεύσεις ξανά όλες τις υπάρχουσες συσκευές και επαφές σου" + "Επανάφερε την ταυτότητά σου μόνο εάν δεν έχεις πρόσβαση σε άλλη συνδεδεμένη συσκευή και έχεις χάσει το κλειδί ανάκτησης." + "Δεν μπορείς να επιβεβαιώσεις; Θα χρειαστεί να επαναφέρεις την ταυτότητά σου." "Απενεργοποίηση" "Θα χάσεις τα κρυπτογραφημένα μηνύματά σου εάν αποσυνδεθείς από όλες τις συσκευές." "Σίγουρα θες να απενεργοποιήσεις τα αντίγραφα ασφαλείας;" @@ -33,7 +39,6 @@ "Προσπάθησε ξανά για να επιβεβαιώσεις την πρόσβαση στο αντίγραφο ασφαλείας της συνομιλίας σου." "Λανθασμένο κλειδί ανάκτησης" "Εάν έχεις ένα κλειδί ασφαλείας ή μια φράση ασφαλείας, θα λειτουργήσει επίσης." - "Κλειδί ανάκτησης ή κωδικός πρόσβασης" "Εισαγωγή…" "Έχασες το κλειδί ανάκτησης;" "Επιβεβαιώθηκε το κλειδί ανάκτησης" @@ -51,4 +56,11 @@ "Βεβαιώσου ότι μπορείς να αποθηκεύσεις το κλειδί ανάκτησης κάπου ασφαλές" "Επιτυχής ρύθμιση ανάκτησης" "Ρύθμιση ανάκτησης" + "Ναι, επαναφορά τώρα" + "Η διαδικασία είναι μη αναστρέψιμη." + "Σίγουρα θες να επαναφέρεις την ταυτότητά σου;" + "Συνέβη ένα άγνωστο σφάλμα. Έλεγξε ότι ο κωδικός πρόσβασης του λογαριασμού σου είναι σωστός και δοκίμασε ξανά." + "Εισαγωγή…" + "Επιβεβαίωσε ότι θες να επαναφέρεις την ταυτότητά σου." + "Εισήγαγε τον κωδικό πρόσβασης του λογαριασμού σου για να συνεχίσεις" diff --git a/features/securebackup/impl/src/main/res/values-et/translations.xml b/features/securebackup/impl/src/main/res/values-et/translations.xml index 2c3aa2d12b..b618432839 100644 --- a/features/securebackup/impl/src/main/res/values-et/translations.xml +++ b/features/securebackup/impl/src/main/res/values-et/translations.xml @@ -39,7 +39,6 @@ "Kinnitamaks ligipääsu sinu vestluse varukoopiale, palun proovi uuesti" "Vigane taastevõti" "Kui sul on turvavõti või turvafraas, siis need toimivad ka." - "Taastevõti või turvafraas" "Sisesta…" "Kas sa oled taastevõtme kaotanud?" "Taastevõti on kinnitatud" diff --git a/features/securebackup/impl/src/main/res/values-fr/translations.xml b/features/securebackup/impl/src/main/res/values-fr/translations.xml index 6656e0a8e3..346ad49329 100644 --- a/features/securebackup/impl/src/main/res/values-fr/translations.xml +++ b/features/securebackup/impl/src/main/res/values-fr/translations.xml @@ -16,6 +16,7 @@ "Suivez les instructions pour créer une nouvelle clé de récupération" "Enregistrez votre nouvelle clé dans un gestionnaire de mots de passe ou dans une note chiffrée" "Réinitialisez le chiffrement de votre compte en utilisant un autre appareil" + "Continuer la réinitialisation" "Les détails de votre compte, vos contacts, vos préférences et votre liste de discussions seront conservés" "Vous perdrez l’historique de vos messages" "Vous devrez vérifier à nouveau tous vos appareils et tous vos contacts" @@ -37,7 +38,6 @@ "Veuillez réessayer afin de pouvoir accéder à vos anciens messages." "Clé de récupération incorrecte" "Si vous avez une clé de sécurité ou une phrase de sécurité, cela fonctionnera également." - "Clé de récupération" "Saisissez la clé ici…" "Clé de récupération perdue?" "Clé de récupération confirmée" diff --git a/features/securebackup/impl/src/main/res/values-hu/translations.xml b/features/securebackup/impl/src/main/res/values-hu/translations.xml index 20575540b7..48fe08a31b 100644 --- a/features/securebackup/impl/src/main/res/values-hu/translations.xml +++ b/features/securebackup/impl/src/main/res/values-hu/translations.xml @@ -39,7 +39,6 @@ "Próbálja meg újra megerősíteni a csevegés biztonsági mentéséhez való hozzáférését." "Helytelen helyreállítási kulcs" "Ha van biztonsági kulcsa vagy biztonsági jelmondata, akkor ez is fog működni." - "Helyreállítási kulcs vagy jelkód" "Megadás…" "Elvesztette a helyreállítási kulcsát?" "Helyreállítási kulcs megerősítve" diff --git a/features/securebackup/impl/src/main/res/values-in/translations.xml b/features/securebackup/impl/src/main/res/values-in/translations.xml index 83fa1f8092..62968d3471 100644 --- a/features/securebackup/impl/src/main/res/values-in/translations.xml +++ b/features/securebackup/impl/src/main/res/values-in/translations.xml @@ -33,7 +33,6 @@ "Silakan coba lagi untuk mengonfirmasi akses ke cadangan percakapan Anda." "Kunci pemulihan salah" "Jika Anda memiliki kunci keamanan atau frasa keamanan, ini juga bisa digunakan." - "Kunci pemulihan atau kode sandi" "Masukkan…" "Kehilangan kunci pemulihan Anda?" "Kunci pemulihan dikonfirmasi" diff --git a/features/securebackup/impl/src/main/res/values-it/translations.xml b/features/securebackup/impl/src/main/res/values-it/translations.xml index c1d67299ba..0e069b0ab2 100644 --- a/features/securebackup/impl/src/main/res/values-it/translations.xml +++ b/features/securebackup/impl/src/main/res/values-it/translations.xml @@ -38,7 +38,6 @@ "Riprova per confermare l\'accesso al backup della chat." "Chiave di recupero errata" "Se hai una chiave di sicurezza o una password, andrà bene anche questo." - "Chiave di recupero o codice di accesso" "Inserisci…" "Hai perso la chiave di recupero?" "Chiave di recupero confermata" diff --git a/features/securebackup/impl/src/main/res/values-nl/translations.xml b/features/securebackup/impl/src/main/res/values-nl/translations.xml index b4dbb23562..388230dd7d 100644 --- a/features/securebackup/impl/src/main/res/values-nl/translations.xml +++ b/features/securebackup/impl/src/main/res/values-nl/translations.xml @@ -9,6 +9,13 @@ "Je chatback-up is momenteel niet gesynchroniseerd." "Herstelmogelijkheid instellen" "Krijg toegang tot je versleutelde berichten als je al je apparaten kwijtraakt of overal uit %1$s bent uitgelogd." + "Open %1$s op een desktopapparaat" + "Log opnieuw in op je account" + "Wanneer je wordt gevraagd om je apparaat te verifiëren, selecteer %1$s" + "“Alles opnieuw instellen”" + "Volg de instructies om een nieuwe herstelsleutel te maken" + "Sla je nieuwe herstelsleutel op in een wachtwoordmanager of versleutelde notitie" + "Stel de versleuteling voor je account opnieuw in met een ander apparaat" "Uitschakelen" "Je verliest je versleutelde berichten als je bent uitgelogd op alle apparaten." "Weet je zeker dat je de back-up wilt uitschakelen?" @@ -21,11 +28,13 @@ "Zorg ervoor dat je je herstelsleutel op een veilige plek kunt bewaren" "Herstelsleutel gewijzigd" "Herstelsleutel wijzigen?" + "Maak een nieuwe herstelsleutel" "Zorg ervoor dat niemand dit scherm kan zien!" "Probeer het opnieuw om toegang tot je chatback-up te bevestigen." "Onjuiste herstelsleutel" "Als je een beveiligingssleutel of beveiligingszin hebt, werkt dit ook." "Voer in…" + "Herstelsleutel kwijt?" "Herstelsleutel bevestigd" "Voer je herstelsleutel in" "Herstelsleutel gekopieerd" diff --git a/features/securebackup/impl/src/main/res/values-pl/translations.xml b/features/securebackup/impl/src/main/res/values-pl/translations.xml index eea550627f..d708e18bab 100644 --- a/features/securebackup/impl/src/main/res/values-pl/translations.xml +++ b/features/securebackup/impl/src/main/res/values-pl/translations.xml @@ -38,7 +38,6 @@ "Spróbuj ponownie, aby potwierdzić dostęp do backupu czatu." "Nieprawidłowy klucz przywracania" "To też zadziała, jeśli posiadasz klucz lub frazę bezpieczeństwa." - "Klucz przywracania lub hasło" "Wprowadź…" "Zgubiłeś swój kod przywracania?" "Potwierdzono klucz przywracania" diff --git a/features/securebackup/impl/src/main/res/values-pt/translations.xml b/features/securebackup/impl/src/main/res/values-pt/translations.xml index f93e5dedf4..0e409ea22c 100644 --- a/features/securebackup/impl/src/main/res/values-pt/translations.xml +++ b/features/securebackup/impl/src/main/res/values-pt/translations.xml @@ -38,7 +38,6 @@ "Por favor, tenta novamente para confirmar o acesso à tua cópia de segurança das conversas." "Chave de recuperação incorreta" "Também funciona se tiveres uma chave ou frase de segurança." - "Chave ou código de recuperação" "Inserir…" "Perdeste a tua chave?" "Chave de recuperação confirmada" diff --git a/features/securebackup/impl/src/main/res/values-ro/translations.xml b/features/securebackup/impl/src/main/res/values-ro/translations.xml index 6b2f2e31dc..7266533d78 100644 --- a/features/securebackup/impl/src/main/res/values-ro/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ro/translations.xml @@ -33,7 +33,6 @@ "Vă rugăm să încercați din nou să confirmați accesul la backup." "Cheie de recuperare incorectă" "Dacă aveți o cheie de securitate sau o frază de securitate, aceasta va funcționa și ea." - "Cheie de recuperare sau cod de acces" "Introduceți…" "Ați pierdut cheia de recuperare?" "Cheia de recuperare confirmată" diff --git a/features/securebackup/impl/src/main/res/values-ru/translations.xml b/features/securebackup/impl/src/main/res/values-ru/translations.xml index 8866dc9613..52d239069d 100644 --- a/features/securebackup/impl/src/main/res/values-ru/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ru/translations.xml @@ -26,6 +26,7 @@ " в менеджере паролей или зашифрованной заметке" "Сбросьте шифрование вашей учетной записи с помощью другого устройства." + "Продолжить сброс" "Данные вашей учетной записи, контакты, настройки и список чатов будут сохранены" "Вы потеряете существующую историю сообщений" "Вам нужно будет заново подтвердить все существующие устройства и контакты." @@ -60,10 +61,6 @@ "ключ восстановления" "Если у вас есть пароль для восстановления или секретный пароль/ключ, это тоже сработает." - - "Ключ восстановления" - " или пароль" - "Вход…" "Потеряли ключ восстановления?" diff --git a/features/securebackup/impl/src/main/res/values-sk/translations.xml b/features/securebackup/impl/src/main/res/values-sk/translations.xml index 8a088f4245..24d80c6b00 100644 --- a/features/securebackup/impl/src/main/res/values-sk/translations.xml +++ b/features/securebackup/impl/src/main/res/values-sk/translations.xml @@ -39,7 +39,6 @@ "Skúste prosím znova potvrdiť prístup k vašej zálohe konverzácie." "Nesprávny kľúč na obnovenie" "Ak máte bezpečnostný kľúč alebo bezpečnostnú frázu, bude to fungovať tiež." - "Kľúč na obnovenie alebo prístupový kód" "Zadať…" "Stratili ste kľúč na obnovenie?" "Kľúč na obnovu potvrdený" diff --git a/features/securebackup/impl/src/main/res/values-sv/translations.xml b/features/securebackup/impl/src/main/res/values-sv/translations.xml index 3599320cd8..7b2364f450 100644 --- a/features/securebackup/impl/src/main/res/values-sv/translations.xml +++ b/features/securebackup/impl/src/main/res/values-sv/translations.xml @@ -39,7 +39,6 @@ "Vänligen pröva igen för att bekräfta åtkomsten till din chattsäkerhetskopia." "Felaktig återställningsnyckel" "Om du har en säkerhetsnyckel eller säkerhetsfras så funkar den också." - "Återställningsnyckel eller lösenkod" "Ange …" "Blivit av med din återställningsnyckel?" "Återställningsnyckel bekräftad" diff --git a/features/securebackup/impl/src/main/res/values-uk/translations.xml b/features/securebackup/impl/src/main/res/values-uk/translations.xml index 14e47f8bad..7d88980f8b 100644 --- a/features/securebackup/impl/src/main/res/values-uk/translations.xml +++ b/features/securebackup/impl/src/main/res/values-uk/translations.xml @@ -38,7 +38,6 @@ "Будь ласка, спробуйте ще раз, щоб підтвердити доступ до резервної копії чату." "Неправильний ключ відновлення" "Якщо у вас є ключ безпеки або фраза безпеки, це теж спрацює." - "Ключ відновлення або код допуску" "Ввести…" "Загубили ключ відновлення?" "Ключ відновлення підтверджено" diff --git a/features/securebackup/impl/src/main/res/values-zh/translations.xml b/features/securebackup/impl/src/main/res/values-zh/translations.xml index 0b4f12b17a..c8f2b7d9ed 100644 --- a/features/securebackup/impl/src/main/res/values-zh/translations.xml +++ b/features/securebackup/impl/src/main/res/values-zh/translations.xml @@ -38,7 +38,6 @@ "请重试以访问您的聊天备份。" "恢复密钥不正确" "如果您有安全密钥或安全短语,也可以用。" - "恢复密钥或密码" "输入……" "丢失了恢复密钥?" "恢复密钥已确认" diff --git a/features/securebackup/impl/src/main/res/values/localazy.xml b/features/securebackup/impl/src/main/res/values/localazy.xml index f4795c23da..55c6c547f5 100644 --- a/features/securebackup/impl/src/main/res/values/localazy.xml +++ b/features/securebackup/impl/src/main/res/values/localazy.xml @@ -39,7 +39,6 @@ "Please try again to confirm access to your chat backup." "Incorrect recovery key" "If you have a security key or security phrase, this will work too." - "Recovery key or passcode" "Enter…" "Lost your recovery key?" "Recovery key confirmed" diff --git a/features/signedout/impl/build.gradle.kts b/features/signedout/impl/build.gradle.kts index 35ad8ecb16..8d8b3f518c 100644 --- a/features/signedout/impl/build.gradle.kts +++ b/features/signedout/impl/build.gradle.kts @@ -37,5 +37,6 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.sessionStorage.implMemory) + testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.tests.testutils) } diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt index 9221fb2e33..b6cdaaaa0e 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt @@ -8,7 +8,6 @@ package io.element.android.features.signedout.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.sessionstorage.api.LoginType import io.element.android.libraries.sessionstorage.api.SessionData @@ -20,18 +19,18 @@ open class SignedOutStateProvider : PreviewParameterProvider { ) } -fun aSignedOutState() = SignedOutState( +private fun aSignedOutState() = SignedOutState( appName = "AppName", signedOutSession = aSessionData(), eventSink = {}, ) -fun aSessionData( - sessionId: SessionId = SessionId("@alice:server.org"), +private fun aSessionData( + sessionId: String = "@alice:server.org", isTokenValid: Boolean = false, ): SessionData { return SessionData( - userId = sessionId.value, + userId = sessionId, deviceId = "aDeviceId", accessToken = "anAccessToken", refreshToken = "aRefreshToken", diff --git a/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt index 8945020da3..a5390c153e 100644 --- a/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt +++ b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest import org.junit.Rule diff --git a/features/verifysession/impl/src/main/res/values-de/translations.xml b/features/verifysession/impl/src/main/res/values-de/translations.xml index 5f0d9504f4..1ddda0126b 100644 --- a/features/verifysession/impl/src/main/res/values-de/translations.xml +++ b/features/verifysession/impl/src/main/res/values-de/translations.xml @@ -4,6 +4,7 @@ "Verifiziere dieses Gerät, um sicheres Messaging einzurichten." "Bestätige, dass du es bist" "Ein anderes Gerät verwenden" + "Wiederherstellungsschlüssel verwenden" "Du kannst nun verschlüsselte Nachrichten lesen oder versenden." "Gerät verifiziert" "Ein anderes Gerät verwenden" diff --git a/features/verifysession/impl/src/main/res/values-el/translations.xml b/features/verifysession/impl/src/main/res/values-el/translations.xml index cc99091441..e53bd54e00 100644 --- a/features/verifysession/impl/src/main/res/values-el/translations.xml +++ b/features/verifysession/impl/src/main/res/values-el/translations.xml @@ -1,9 +1,11 @@ + "Δεν μπορείς να επιβεβαιώσεις;" "Δημιουργία νέου κλειδιού ανάκτησης" "Επαλήθευσε αυτήν τη συσκευή για να ρυθμίσεις την ασφαλή επικοινωνία." "Επιβεβαίωσε ότι είσαι εσύ" "Χρήση άλλης συσκευής" + "Χρήση κλειδιού ανάκτησης" "Τώρα μπορείς να διαβάζεις ή να στέλνεις μηνύματα με ασφάλεια και επίσης μπορεί να εμπιστευτεί αυτήν τη συσκευή οποιοσδήποτε με τον οποίο συνομιλείς." "Επαληθευμένη συσκευή" "Χρήση άλλης συσκευής" diff --git a/features/verifysession/impl/src/main/res/values-nl/translations.xml b/features/verifysession/impl/src/main/res/values-nl/translations.xml index 6330ad2fc6..bebac551e2 100644 --- a/features/verifysession/impl/src/main/res/values-nl/translations.xml +++ b/features/verifysession/impl/src/main/res/values-nl/translations.xml @@ -1,12 +1,18 @@ + "Maak een nieuwe herstelsleutel" + "Verifieer dit apparaat om beveiligde berichten in te stellen." + "Bevestig dat jij het bent" + "Nu kun je veilig berichten lezen of verzenden, en iedereen met wie je chat kan dit apparaat ook vertrouwen." + "Apparaat geverifieerd" + "Wachten op ander apparaat…" "Er lijkt iets niet goed te gaan. Of er is een time-out opgetreden of het verzoek is geweigerd." "Bevestig dat de emoji\'s hieronder overeenkomen met de emoji\'s in je andere sessie." "Vergelijk emoji\'s" "Bevestig dat de onderstaande cijfers overeenkomen met de cijfers die worden weergegeven in je andere sessie." "Vergelijk getallen" "Je nieuwe sessie is nu geverifieerd. Het heeft toegang tot je versleutelde berichten en andere gebruikers zullen het als vertrouwd beschouwen." - "Voer recovery key in" + "Voer herstelsleutel in" "Bewijs dat jij het bent om toegang te krijgen tot je versleutelde berichtgeschiedenis." "Open een bestaande sessie" "Verificatie opnieuw proberen" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c0cad7783a..83f8043bbb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -162,7 +162,7 @@ jsoup = "org.jsoup:jsoup:1.18.1" appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0" timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "chat.schildi.rustcomponents:sdk-android:0.2.29" +matrix_sdk = "chat.schildi.rustcomponents:sdk-android:0.2.30" matrix_richtexteditor = { module = "chat.schildi:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "chat.schildi:wysiwyg-compose", version.ref = "wysiwyg" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoundedIconAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoundedIconAtom.kt index 9dc739da96..c21e1a5f38 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoundedIconAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoundedIconAtom.kt @@ -37,6 +37,7 @@ import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial * @param resourceId the resource id of the icon to display, exclusive with [imageVector] * @param imageVector the image vector of the icon to display, exclusive with [resourceId] * @param tint the tint to apply to the icon + * @param backgroundTint the tint to apply to the icon background */ @Composable fun RoundedIconAtom( @@ -44,13 +45,14 @@ fun RoundedIconAtom( size: RoundedIconAtomSize = RoundedIconAtomSize.Large, resourceId: Int? = null, imageVector: ImageVector? = null, - tint: Color = MaterialTheme.colorScheme.secondary + tint: Color = MaterialTheme.colorScheme.secondary, + backgroundTint: Color = ElementTheme.colors.temporaryColorBgSpecial, ) { Box( modifier = modifier .size(size.toContainerSize()) .background( - color = ElementTheme.colors.temporaryColorBgSpecial, + color = backgroundTint, shape = RoundedCornerShape(size.toCornerSize()) ) ) { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt index f4c88ab59b..4812eaddee 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.designsystem.icons.CompoundDrawables import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial /** * IconTitleSubtitleMolecule is a molecule which displays an icon, a title and a subtitle. @@ -37,6 +38,7 @@ import io.element.android.libraries.designsystem.theme.components.Text * @param iconResourceId the resource id of the icon to display, exclusive with [iconImageVector] * @param iconImageVector the image vector of the icon to display, exclusive with [iconResourceId] * @param iconTint the tint to apply to the icon + * @param iconBackgroundTint the tint to apply to the icon background */ @Composable fun IconTitleSubtitleMolecule( @@ -46,6 +48,7 @@ fun IconTitleSubtitleMolecule( iconResourceId: Int? = null, iconImageVector: ImageVector? = null, iconTint: Color = MaterialTheme.colorScheme.primary, + iconBackgroundTint: Color = ElementTheme.colors.temporaryColorBgSpecial, ) { Column(modifier) { RoundedIconAtom( @@ -55,6 +58,7 @@ fun IconTitleSubtitleMolecule( resourceId = iconResourceId, imageVector = iconImageVector, tint = iconTint, + backgroundTint = iconBackgroundTint, ) Spacer(modifier = Modifier.height(16.dp)) Text( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/InfoListOrganism.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/InfoListOrganism.kt index fec66c6f96..f3ff74909c 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/InfoListOrganism.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/InfoListOrganism.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -38,6 +39,7 @@ fun InfoListOrganism( iconTint: Color = LocalContentColor.current, iconSize: Dp = 20.dp, textStyle: TextStyle = LocalTextStyle.current, + textColor: Color = ElementTheme.colors.textPrimary, verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(4.dp), ) { Column( @@ -53,11 +55,19 @@ fun InfoListOrganism( } InfoListItemMolecule( message = { - Text( - text = item.message, - style = textStyle, - color = ElementTheme.colors.textPrimary, - ) + if (item.message is AnnotatedString) { + Text( + text = item.message, + style = textStyle, + color = textColor, + ) + } else { + Text( + text = item.message.toString(), + style = textStyle, + color = textColor, + ) + } }, icon = { if (item.iconId != null) { @@ -86,7 +96,7 @@ fun InfoListOrganism( } data class InfoListItem( - val message: String, + val message: CharSequence, @DrawableRes val iconId: Int? = null, val iconVector: ImageVector? = null, val iconComposable: @Composable () -> Unit = {}, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncActionView.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncActionView.kt index 08c2f352e6..6e42909f0b 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncActionView.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncActionView.kt @@ -48,7 +48,7 @@ fun AsyncActionView( ErrorDialog( title = errorTitle(async.error), content = errorMessage(async.error), - onDismiss = onErrorDismiss + onSubmit = onErrorDismiss ) } else { RetryDialog( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt index acef5bf9f9..add7bf2904 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.window.DialogProperties import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup @@ -25,17 +26,23 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun ErrorDialog( content: String, - onDismiss: () -> Unit, + onSubmit: () -> Unit, modifier: Modifier = Modifier, - title: String = ErrorDialogDefaults.title, + title: String? = ErrorDialogDefaults.title, submitText: String = ErrorDialogDefaults.submitText, + onDismiss: () -> Unit = onSubmit, + canDismiss: Boolean = true, ) { - BasicAlertDialog(modifier = modifier, onDismissRequest = onDismiss) { + BasicAlertDialog( + modifier = modifier, + onDismissRequest = onDismiss, + properties = DialogProperties(dismissOnClickOutside = canDismiss, dismissOnBackPress = canDismiss) + ) { ErrorDialogContent( title = title, content = content, submitText = submitText, - onSubmitClick = onDismiss, + onSubmitClick = onSubmit, ) } } @@ -44,7 +51,7 @@ fun ErrorDialog( private fun ErrorDialogContent( content: String, onSubmitClick: () -> Unit, - title: String = ErrorDialogDefaults.title, + title: String? = ErrorDialogDefaults.title, submitText: String = ErrorDialogDefaults.submitText, ) { SimpleAlertDialogContent( @@ -78,6 +85,6 @@ internal fun ErrorDialogContentPreview() { internal fun ErrorDialogPreview() = ElementPreview { ErrorDialog( content = "Content", - onDismiss = {}, + onSubmit = {}, ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt index 25f5009299..f09ccad5d9 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt @@ -65,7 +65,41 @@ fun ListItem( disabledLeadingIconColor = ListItemDefaultColors.iconDisabled, disabledTrailingIconColor = ListItemDefaultColors.iconDisabled, ) + ListItem( + headlineContent = headlineContent, + modifier = modifier, + supportingContent = supportingContent, + leadingContent = leadingContent, + trailingContent = trailingContent, + colors = colors, + enabled = enabled, + onClick = onClick, + ) +} +/** + * A List Item component to be used in lists and menus with simple layouts, matching the Material 3 guidelines. + * @param headlineContent The main content of the list item, usually a text. + * @param colors The colors to use for the list item. You can use [ListItemDefaults.colors] to create this. + * @param modifier The modifier to be applied to the list item. + * @param supportingContent The content to be displayed below the headline content. + * @param leadingContent The content to be displayed before the headline content. + * @param trailingContent The content to be displayed after the headline content. + * @param enabled Whether the list item is enabled. When disabled, will change the color of the headline content and the leading content to use disabled tokens. + * @param onClick The callback to be called when the list item is clicked. + */ +@Suppress("LongParameterList") +@Composable +fun ListItem( + headlineContent: @Composable () -> Unit, + colors: ListItemColors, + modifier: Modifier = Modifier, + supportingContent: @Composable (() -> Unit)? = null, + leadingContent: ListItemContent? = null, + trailingContent: ListItemContent? = null, + enabled: Boolean = true, + onClick: (() -> Unit)? = null, +) { // We cannot just pass the disabled colors, they must be set manually: https://issuetracker.google.com/issues/280480132 val headlineColor = if (enabled) colors.headlineColor else colors.disabledHeadlineColor val leadingContentColor = if (enabled) colors.leadingIconColor else colors.disabledLeadingIconColor @@ -378,8 +412,8 @@ private object PreviewItems { ) { ElementThemedPreview { ListItem( - headlineContent = PreviewItems.headline(), - supportingContent = PreviewItems.text(), + headlineContent = headline(), + supportingContent = text(), leadingContent = leadingContent, trailingContent = trailingContent, style = style, @@ -397,8 +431,8 @@ private object PreviewItems { ) { ElementThemedPreview { ListItem( - headlineContent = PreviewItems.headline(), - supportingContent = PreviewItems.textSingleLine(), + headlineContent = headline(), + supportingContent = textSingleLine(), leadingContent = leadingContent, trailingContent = trailingContent, style = style, @@ -417,7 +451,7 @@ private object PreviewItems { ) { ElementThemedPreview { ListItem( - headlineContent = PreviewItems.headline(), + headlineContent = headline(), leadingContent = leadingContent, trailingContent = trailingContent, enabled = enabled, diff --git a/libraries/eventformatter/impl/src/main/res/values-de/translations.xml b/libraries/eventformatter/impl/src/main/res/values-de/translations.xml index 597632a1f6..1b99fd2e5d 100644 --- a/libraries/eventformatter/impl/src/main/res/values-de/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-de/translations.xml @@ -45,6 +45,12 @@ "Du hast den Raum-Namen entfernt" "%1$shat keine Änderungen vorgenommen" "Du hast keine Änderungen vorgenommen" + "%1$s hat die fixierten Nachrichten geändert" + "Du hast die fixierten Nachrichten geändert" + "%1$s fixierte eine Nachricht" + "Du hast eine Nachricht fixiert" + "%1$s löste eine Nachricht" + "Du hast eine Nachricht gelöst" "%1$s hat die Einladung abgelehnt" "Du hast die Einladung abgelehnt" "%1$s hat %2$s entfernt" diff --git a/libraries/eventformatter/impl/src/main/res/values-el/translations.xml b/libraries/eventformatter/impl/src/main/res/values-el/translations.xml index 71adc9ed56..4e476d3c4b 100644 --- a/libraries/eventformatter/impl/src/main/res/values-el/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-el/translations.xml @@ -45,6 +45,12 @@ "Αφαίρεσες το όνομα του δωματίου" "Ο χρήστης %1$s δεν έκανε καμία αλλαγή" "Δεν έκανες καμία αλλαγή" + "Ο χρήστης %1$s άλλαξε τα καρφιτσωμένα μηνύματα" + "Άλλαξες τα καρφιτσωμένα μηνύματα" + "Ο χρήστης %1$s καρφίτσωσε ένα μήνυμα" + "Καρφίτσωσες ένα μήνυμα" + "Ο χρήστης %1$s ξεκαρφίτσωσε ένα μήνυμα" + "Ξεκαρφίτσωσες ένα μήνυμα" "Ο χρήστης %1$s απέρριψε την πρόσκληση" "Απέρριψες την πρόσκληση" "Ο χρήστης %1$s αφαίρεσε τον χρήστη %2$s" diff --git a/libraries/eventformatter/impl/src/main/res/values-nl/translations.xml b/libraries/eventformatter/impl/src/main/res/values-nl/translations.xml index b10c806572..dba242a458 100644 --- a/libraries/eventformatter/impl/src/main/res/values-nl/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-nl/translations.xml @@ -3,12 +3,16 @@ "(afbeelding is ook gewijzigd)" "%1$s wijzigde van afbeelding" "Je hebt je afbeelding gewijzigd" + "%1$s werd gedegradeerd tot lid" + "%1$s werd gedegradeerd tot moderator" "%1$s heeft de weergavenaam aangepast van %2$s naar %3$s" "Je hebt je weergavenaam aangepast van %1$s naar %2$s" "%1$s heeft de weergavenaam verwijderd (dit was %2$s)" "Je hebt je weergavenaam verwijderd (dit was %1$s)" "%1$s heeft de weergavenaam %2$s aangenomen" "Je hebt de weergavenaam %1$s aangenomen" + "%1$s werd bevorderd tot beheerder" + "%1$s werd bevorderd tot moderator" "%1$s heeft de kamerafbeelding gewijzigd" "Je hebt de kamerafbeelding gewijzigd" "%1$s heeft de kamerafbeelding verwijderd" diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 124d3a3318..d71c0de360 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -13,8 +13,6 @@ import io.element.android.libraries.core.meta.BuildType /** * To enable or disable a FeatureFlags, change the `defaultValue` value. - * Warning: to enable a flag for the release app, you MUST update the file - * [io.element.android.libraries.featureflag.impl.StaticFeatureFlagProvider] */ enum class FeatureFlags( override val key: String, @@ -125,4 +123,13 @@ enum class FeatureFlags( defaultValue = { true }, isFinished = false, ), + InvisibleCrypto( + key = "feature.invisibleCrypto", + title = "Invisible Crypto", + description = "This setting controls how end-to-end encryption (E2E) keys are shared." + + " Enabling it will prevent the inclusion of devices that have not been explicitly verified by their owners." + + " You'll have to stop and re-open the app manually for that setting to take effect.", + defaultValue = { false }, + isFinished = false, + ), } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index eb6a334f97..c618020752 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.pusher.PushersService +import io.element.android.libraries.matrix.api.room.InvitedRoom import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @@ -49,9 +50,10 @@ interface MatrixClient : Closeable { val sessionCoroutineScope: CoroutineScope val ignoredUsersFlow: StateFlow> suspend fun getRoom(roomId: RoomId): MatrixRoom? - suspend fun getAccountData(eventType: String): String? - suspend fun getRoomAccountData(roomId: RoomId, eventType: String): String? - suspend fun setAccountData(eventType: String, content: String) + suspend fun getAccountData(eventType: String): String? // SC + suspend fun getRoomAccountData(roomId: RoomId, eventType: String): String? // SC + suspend fun setAccountData(eventType: String, content: String) // SC + suspend fun getInvitedRoom(roomId: RoomId): InvitedRoom? suspend fun findDM(userId: UserId): RoomId? suspend fun ignoreUser(userId: UserId): Result suspend fun unignoreUser(userId: UserId): Result @@ -134,6 +136,12 @@ interface MatrixClient : Closeable { /** Returns `true` if the home server supports native sliding sync. */ suspend fun isNativeSlidingSyncSupported(): Boolean - /** Returns `true` if the current session is using native sliding sync. */ + /** Returns `true` if the home server supports sliding sync using a proxy. */ + suspend fun isSlidingSyncProxySupported(): Boolean + + /** Returns `true` if the current session is using native sliding sync, `false` if it's using a proxy. */ fun isUsingNativeSlidingSync(): Boolean + + fun canDeactivateAccount(): Boolean + suspend fun deactivateAccount(password: String, eraseData: Boolean): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt index bf0955098b..bb3d396202 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.api.auth import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.auth.external.ExternalSession import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep import io.element.android.libraries.matrix.api.core.SessionId @@ -30,6 +31,11 @@ interface MatrixAuthenticationService { suspend fun setHomeserver(homeserver: String): Result suspend fun login(username: String, password: String): Result + /** + * Import a session that was created using another client, for instance Element Web. + */ + suspend fun importCreatedSession(externalSession: ExternalSession): Result + /* * OIDC part. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/external/ExternalSession.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/external/ExternalSession.kt new file mode 100644 index 0000000000..d85135b26e --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/external/ExternalSession.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.auth.external + +/*** + * Represents a session data of a session created by another client. + */ +data class ExternalSession( + val userId: String, + val deviceId: String, + val accessToken: String, + val refreshToken: String?, + val homeserverUrl: String, + val slidingSyncProxy: String? +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt index 851dd44e3e..454dfba462 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt @@ -149,7 +149,12 @@ object MatrixPatterns { add(MatrixPatternResult(MatrixPatternType.USER_ID, permalink.userId.toString(), match.range.first, match.range.last + 1)) } is PermalinkData.RoomLink -> { - add(MatrixPatternResult(MatrixPatternType.ROOM_ALIAS, permalink.roomIdOrAlias.identifier, match.range.first, match.range.last + 1)) + when (permalink.roomIdOrAlias) { + is RoomIdOrAlias.Alias -> MatrixPatternType.ROOM_ALIAS + is RoomIdOrAlias.Id -> if (permalink.eventId == null) MatrixPatternType.ROOM_ID else null + }?.let { type -> + add(MatrixPatternResult(type, permalink.roomIdOrAlias.identifier, match.range.first, match.range.last + 1)) + } } else -> Unit } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/InvitedRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/InvitedRoom.kt new file mode 100644 index 0000000000..7e1dd5d10d --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/InvitedRoom.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.room + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId + +/** A reference to a room the current user has been invited to, with the ability to decline the invite. */ +interface InvitedRoom : AutoCloseable { + val sessionId: SessionId + val roomId: RoomId + + /** Decline the invite to this room. */ + suspend fun declineInvite(): Result +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 2d8c807ce0..d266b45d35 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.api.room +import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.RoomAlias @@ -349,5 +350,24 @@ interface MatrixRoom : Closeable { */ suspend fun clearComposerDraft(): Result + /** + * Ignore the local trust for the given devices and resend messages that failed to send because said devices are unverified. + * + * @param devices The map of users identifiers to device identifiers received in the error + * @param transactionId The send queue transaction identifier of the local echo the send error applies to. + * + */ + suspend fun ignoreDeviceTrustAndResend(devices: Map>, transactionId: TransactionId): Result + + /** + * Remove verification requirements for the given users and + * resend messages that failed to send because their identities were no longer verified. + * + * @param userIds The list of users identifiers received in the error. + * @param transactionId The send queue transaction identifier of the local echo the send error applies to. + * + */ + suspend fun withdrawVerificationAndResend(userIds: List, transactionId: TransactionId): Result + override fun close() = destroy() } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt index 32eddf7b00..b83c2c98d6 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt @@ -31,7 +31,7 @@ data class MatrixRoomInfo( val isTombstoned: Boolean, val isFavorite: Boolean, val canonicalAlias: RoomAlias?, - val alternativeAliases: ImmutableList, + val alternativeAliases: ImmutableList, val currentUserMembership: CurrentUserMembership, val inviter: RoomMember?, val activeMembersCount: Long, @@ -42,7 +42,8 @@ data class MatrixRoomInfo( val notificationCount: Long, val userDefinedNotificationMode: RoomNotificationMode?, val hasRoomCall: Boolean, - val activeRoomCallParticipants: ImmutableList, + val activeRoomCallParticipants: ImmutableList, val heroes: ImmutableList, - val pinnedEventIds: ImmutableList + val pinnedEventIds: ImmutableList, + val creator: UserId?, ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt index 92242faf18..452073bb25 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.api.timeline.item.event import androidx.compose.runtime.Immutable +import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId @@ -18,21 +19,24 @@ sealed interface LocalEventSendState { data class Unknown(val error: String) : Failed data object CrossSigningNotSetup : Failed data object SendingFromUnverifiedDevice : Failed + + sealed interface VerifiedUser : Failed data class VerifiedUserHasUnsignedDevice( /** * The unsigned devices belonging to verified users. A map from user ID * to a list of device IDs. */ - val devices: Map> - ) : Failed + val devices: Map> + ) : VerifiedUser data class VerifiedUserChangedIdentity( /** * The users that were previously verified but are no longer. */ val users: List - ) : Failed + ) : VerifiedUser } + data class Sent( val eventId: EventId ) : LocalEventSendState diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index c36beba389..aa6eba9f98 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -48,7 +48,10 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.robolectric) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.sessionStorage.implMemory) + testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.services.analytics.test) + testImplementation(projects.services.toolbox.test) testImplementation(projects.tests.testutils) testImplementation(libs.coroutines.test) testImplementation(libs.test.turbine) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegate.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegate.kt index 03a59203e5..dcdb719511 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegate.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegate.kt @@ -50,7 +50,6 @@ class RustClientSessionDelegate( */ fun bindClient(client: RustMatrixClient) { this.client = client - client.setDelegate(this) } override fun saveSessionInKeychain(session: Session) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 19b92a82fa..9f51e1fdae 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl import io.element.android.libraries.androidutils.file.getSizeOfFiles import io.element.android.libraries.androidutils.file.safeDelete +import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.childScope import io.element.android.libraries.matrix.api.MatrixClient @@ -29,6 +30,7 @@ import io.element.android.libraries.matrix.api.notificationsettings.Notification import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.InvitedRoom import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @@ -88,6 +90,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout +import org.matrix.rustcomponents.sdk.AuthData +import org.matrix.rustcomponents.sdk.AuthDataPasswordDetails import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientException import org.matrix.rustcomponents.sdk.IgnoredUsersListener @@ -118,7 +122,7 @@ class RustMatrixClient( private val baseDirectory: File, baseCacheDirectory: File, private val clock: SystemClock, - sessionDelegate: RustClientSessionDelegate, + private val sessionDelegate: RustClientSessionDelegate, ) : MatrixClient { override val sessionId: UserId = UserId(client.userId()) override val deviceId: DeviceId = DeviceId(client.deviceId()) @@ -191,7 +195,7 @@ class RustMatrixClient( private val roomMembershipObserver = RoomMembershipObserver() - private val clientDelegateTaskHandle: TaskHandle? = client.setDelegate(sessionDelegate) + private var clientDelegateTaskHandle: TaskHandle? = client.setDelegate(sessionDelegate) private val _userProfile: MutableStateFlow = MutableStateFlow( MatrixUser( @@ -245,6 +249,10 @@ class RustMatrixClient( return roomFactory.create(roomId) } + override suspend fun getInvitedRoom(roomId: RoomId): InvitedRoom? { + return roomFactory.createInvitedRoom(roomId) + } + /** * Wait for the room to be available in the room list, with a membership for the current user of [CurrentUserMembership.JOINED]. * @param roomIdOrAlias the room id or alias to wait for @@ -267,6 +275,8 @@ class RustMatrixClient( .filter(predicate) .first() .first() + // Ensure that the room is ready + .also { client.awaitRoomRemoteEcho(it.roomId.value) } } } @@ -457,12 +467,12 @@ class RustMatrixClient( override fun close() { appCoroutineScope.launch { roomFactory.destroy() + rustSyncService.destroy() } sessionCoroutineScope.cancel() clientDelegateTaskHandle?.cancelAndDestroy() notificationSettingsService.destroy() verificationService.destroy() - syncService.destroy() innerRoomListService.destroy() notificationClient.destroy() notificationProcessSetup.destroy() @@ -481,7 +491,9 @@ class RustMatrixClient( override suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean): String? { var result: String? = null - syncService.stop() + // Remove current delegate so we don't receive an auth error + clientDelegateTaskHandle?.cancelAndDestroy() + clientDelegateTaskHandle = null withContext(sessionDispatcher) { if (userInitiated) { try { @@ -490,12 +502,15 @@ class RustMatrixClient( if (ignoreSdkError) { Timber.e(failure, "Fail to call logout on HS. Still delete local files.") } else { + // If the logout failed we need to restore the delegate + clientDelegateTaskHandle = client.setDelegate(sessionDelegate) Timber.e(failure, "Fail to call logout on HS.") throw failure } } } close() + deleteSessionDirectory(deleteCryptoDb = true) if (userInitiated) { sessionStore.removeSession(sessionId.value) @@ -504,6 +519,55 @@ class RustMatrixClient( return result } + override fun canDeactivateAccount(): Boolean { + return runCatching { + client.canDeactivateAccount() + } + .getOrNull() + .orFalse() + } + + override suspend fun deactivateAccount(password: String, eraseData: Boolean): Result = withContext(sessionDispatcher) { + Timber.w("Deactivating account") + // Remove current delegate so we don't receive an auth error + clientDelegateTaskHandle?.cancelAndDestroy() + clientDelegateTaskHandle = null + runCatching { + // First call without AuthData, should fail + val firstAttempt = runCatching { + client.deactivateAccount( + authData = null, + eraseData = eraseData, + ) + } + if (firstAttempt.isFailure) { + Timber.w(firstAttempt.exceptionOrNull(), "Expected failure, try again") + // This is expected, try again with the password + runCatching { + client.deactivateAccount( + authData = AuthData.Password( + passwordDetails = AuthDataPasswordDetails( + identifier = sessionId.value, + password = password, + ), + ), + eraseData = eraseData, + ) + }.onFailure { + Timber.e(it, "Failed to deactivate account") + // If the deactivation failed we need to restore the delegate + clientDelegateTaskHandle = client.setDelegate(sessionDelegate) + throw it + } + } + close() + deleteSessionDirectory(deleteCryptoDb = true) + sessionStore.removeSession(sessionId.value) + }.onFailure { + Timber.e(it, "Failed to deactivate account") + } + } + override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result = withContext(sessionDispatcher) { val rustAction = action?.toRustAction() runCatching { @@ -552,12 +616,12 @@ class RustMatrixClient( return client.availableSlidingSyncVersions().contains(SlidingSyncVersion.Native) } - override fun isUsingNativeSlidingSync(): Boolean { - return client.session().slidingSyncVersion == SlidingSyncVersion.Native + override suspend fun isSlidingSyncProxySupported(): Boolean { + return client.availableSlidingSyncVersions().any { it is SlidingSyncVersion.Proxy } } - internal fun setDelegate(delegate: RustClientSessionDelegate) { - client.setDelegate(delegate) + override fun isUsingNativeSlidingSync(): Boolean { + return client.session().slidingSyncVersion == SlidingSyncVersion.Native } private suspend fun File.getCacheSize( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt index 2fe4284f98..512b6e8ea2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt @@ -7,9 +7,10 @@ package io.element.android.libraries.matrix.impl -import io.element.android.appconfig.AuthenticationConfig import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.CacheDirectory +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.impl.analytics.UtdTracker import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider import io.element.android.libraries.matrix.impl.paths.SessionPaths @@ -17,12 +18,10 @@ import io.element.android.libraries.matrix.impl.paths.getSessionPaths import io.element.android.libraries.matrix.impl.proxy.ProxyProvider import io.element.android.libraries.matrix.impl.util.anonymizedTokens import io.element.android.libraries.network.useragent.UserAgentProvider -import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.ClientBuilder import org.matrix.rustcomponents.sdk.Session @@ -30,6 +29,7 @@ import org.matrix.rustcomponents.sdk.SlidingSyncVersion import org.matrix.rustcomponents.sdk.SlidingSyncVersionBuilder import org.matrix.rustcomponents.sdk.use import timber.log.Timber +import uniffi.matrix_sdk_crypto.CollectStrategy import java.io.File import javax.inject.Inject @@ -44,14 +44,14 @@ class RustMatrixClientFactory @Inject constructor( private val proxyProvider: ProxyProvider, private val clock: SystemClock, private val utdTracker: UtdTracker, - private val appPreferencesStore: AppPreferencesStore, + private val featureFlagService: FeatureFlagService, ) { suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) { val sessionDelegate = RustClientSessionDelegate(sessionStore, appCoroutineScope, coroutineDispatchers) val client = getBaseClientBuilder( sessionPaths = sessionData.getSessionPaths(), passphrase = sessionData.passphrase, - restore = true, + slidingSyncType = ClientBuilderSlidingSync.Restored, ) .homeserverUrl(sessionData.homeserverUrl) .username(sessionData.userId) @@ -84,16 +84,8 @@ class RustMatrixClientFactory @Inject constructor( internal suspend fun getBaseClientBuilder( sessionPaths: SessionPaths, passphrase: String?, - restore: Boolean, + slidingSyncType: ClientBuilderSlidingSync, ): ClientBuilder { - val slidingSync = when { - // Always check restore first, since otherwise other values could accidentally override the already persisted config - restore -> ClientBuilderSlidingSync.Restored - AuthenticationConfig.SLIDING_SYNC_PROXY_URL != null -> ClientBuilderSlidingSync.CustomProxy(AuthenticationConfig.SLIDING_SYNC_PROXY_URL!!) - appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first() -> ClientBuilderSlidingSync.Simplified - else -> ClientBuilderSlidingSync.Discovered - } - return ClientBuilder() .sessionPaths( dataPath = sessionPaths.fileDirectory.absolutePath, @@ -104,11 +96,18 @@ class RustMatrixClientFactory @Inject constructor( .addRootCertificates(userCertificatesProvider.provides()) .autoEnableBackups(true) .autoEnableCrossSigning(true) + .roomKeyRecipientStrategy( + strategy = if (featureFlagService.isFeatureEnabled(FeatureFlags.InvisibleCrypto)) { + CollectStrategy.IdentityBasedStrategy + } else { + CollectStrategy.DeviceBasedStrategy(onlyAllowTrustedDevices = false, errorOnVerifiedUserProblem = true) + } + ) .run { // Apply sliding sync version settings - when (slidingSync) { + when (slidingSyncType) { ClientBuilderSlidingSync.Restored -> this - is ClientBuilderSlidingSync.CustomProxy -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.Proxy(slidingSync.url)) + is ClientBuilderSlidingSync.CustomProxy -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.Proxy(slidingSyncType.url)) ClientBuilderSlidingSync.Discovered -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.DiscoverProxy) ClientBuilderSlidingSync.Simplified -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.DiscoverNative) ClientBuilderSlidingSync.ForcedSimplified -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.Native) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt index a454c1b58f..81ef1672e3 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt @@ -10,9 +10,10 @@ package io.element.android.libraries.matrix.impl.analytics import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.libraries.matrix.api.room.MatrixRoom -private fun Long?.toAnalyticsRoomSize(): JoinedRoom.RoomSize { +private fun Long.toAnalyticsRoomSize(): JoinedRoom.RoomSize { return when (this) { - null, + 0L, + 1L -> JoinedRoom.RoomSize.One 2L -> JoinedRoom.RoomSize.Two in 3..10 -> JoinedRoom.RoomSize.ThreeToTen in 11..100 -> JoinedRoom.RoomSize.ElevenToOneHundred diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt index 278b814fe2..cf48d68c70 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt @@ -8,14 +8,21 @@ package io.element.android.libraries.matrix.impl.auth import io.element.android.libraries.matrix.api.auth.AuthenticationException -import org.matrix.rustcomponents.sdk.ClientBuildException as RustAuthenticationException +import org.matrix.rustcomponents.sdk.ClientBuildException fun Throwable.mapAuthenticationException(): AuthenticationException { val message = this.message ?: "Unknown error" return when (this) { - is RustAuthenticationException.Generic -> AuthenticationException.Generic(message) - is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(message) - is RustAuthenticationException.SlidingSyncVersion -> AuthenticationException.SlidingSyncVersion(message) + is ClientBuildException -> when (this) { + is ClientBuildException.Generic -> AuthenticationException.Generic(message) + is ClientBuildException.InvalidServerName -> AuthenticationException.InvalidServerName(message) + is ClientBuildException.SlidingSyncVersion -> AuthenticationException.SlidingSyncVersion(message) + is ClientBuildException.Sdk -> AuthenticationException.Generic(message) + is ClientBuildException.ServerUnreachable -> AuthenticationException.Generic(message) + is ClientBuildException.SlidingSync -> AuthenticationException.Generic(message) + is ClientBuildException.WellKnownDeserializationException -> AuthenticationException.Generic(message) + is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message) + } else -> AuthenticationException.Generic(message) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index 6e97ddec6d..4766c51a6e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl.auth import android.os.Build import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.appconfig.AuthenticationConfig import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.meta.BuildMeta @@ -18,9 +19,11 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.matrix.api.auth.external.ExternalSession import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.impl.ClientBuilderSlidingSync import io.element.android.libraries.matrix.impl.RustMatrixClientFactory import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData @@ -30,6 +33,7 @@ import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator import io.element.android.libraries.matrix.impl.mapper.toSessionData import io.element.android.libraries.matrix.impl.paths.SessionPaths import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory +import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.api.LoginType import io.element.android.libraries.sessionstorage.api.SessionStore @@ -37,9 +41,14 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client +import org.matrix.rustcomponents.sdk.ClientBuildException +import org.matrix.rustcomponents.sdk.ClientBuilder import org.matrix.rustcomponents.sdk.HumanQrLoginException +import org.matrix.rustcomponents.sdk.OidcConfiguration +import org.matrix.rustcomponents.sdk.QrCodeData import org.matrix.rustcomponents.sdk.QrCodeDecodeException import org.matrix.rustcomponents.sdk.QrLoginProgress import org.matrix.rustcomponents.sdk.QrLoginProgressListener @@ -57,7 +66,8 @@ class RustMatrixAuthenticationService @Inject constructor( private val rustMatrixClientFactory: RustMatrixClientFactory, private val passphraseGenerator: PassphraseGenerator, private val oidcConfigurationProvider: OidcConfigurationProvider, - private val buildMeta: BuildMeta, + private val buildMeta: BuildMeta, // SC + private val appPreferencesStore: AppPreferencesStore, ) : MatrixAuthenticationService { // Passphrase which will be used for new sessions. Existing sessions will use the passphrase // stored in the SessionData. @@ -120,9 +130,10 @@ class RustMatrixAuthenticationService @Inject constructor( withContext(coroutineDispatchers.io) { val emptySessionPath = rotateSessionPath() runCatching { - val client = getBaseClientBuilder(emptySessionPath) - .serverNameOrHomeserverUrl(homeserver) - .build() + val client = makeClient(sessionPaths = emptySessionPath) { + serverNameOrHomeserverUrl(homeserver) + } + currentClient = client val homeServerDetails = client.homeserverLoginDetails().map() currentHomeserver.value = homeServerDetails.copy(url = homeserver) @@ -154,6 +165,23 @@ class RustMatrixAuthenticationService @Inject constructor( } } + override suspend fun importCreatedSession(externalSession: ExternalSession): Result = + withContext(coroutineDispatchers.io) { + runCatching { + currentClient ?: error("You need to call `setHomeserver()` first") + val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first") + val sessionData = externalSession.toSessionData( + isTokenValid = true, + loginType = LoginType.PASSWORD, + passphrase = pendingPassphrase, + sessionPaths = currentSessionPaths, + ) + clear() + sessionStore.storeData(sessionData) + SessionId(sessionData.userId) + } + } + private var pendingOidcAuthorizationData: OidcAuthorizationData? = null override suspend fun getOidcUrl(): Result { @@ -210,34 +238,34 @@ class RustMatrixAuthenticationService @Inject constructor( override suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) = withContext(coroutineDispatchers.io) { + val sdkQrCodeLoginData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData val emptySessionPaths = rotateSessionPath() + val oidcConfiguration = oidcConfigurationProvider.get() + val progressListener = object : QrLoginProgressListener { + override fun onUpdate(state: QrLoginProgress) { + Timber.d("QR Code login progress: $state") + progress(state.toStep()) + } + } runCatching { - val client = rustMatrixClientFactory.getBaseClientBuilder( + val client = makeQrCodeLoginClient( sessionPaths = emptySessionPaths, passphrase = pendingPassphrase, - restore = false, + qrCodeData = sdkQrCodeLoginData, + oidcConfiguration = oidcConfiguration, + progressListener = progressListener, ) - .buildWithQrCode( - qrCodeData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData, - oidcConfiguration = oidcConfigurationProvider.get(), - progressListener = object : QrLoginProgressListener { - override fun onUpdate(state: QrLoginProgress) { - Timber.d("QR Code login progress: $state") - progress(state.toStep()) - } - } - ) - client.use { rustClient -> - val sessionData = rustClient.session() + val sessionData = client.use { rustClient -> + rustClient.session() .toSessionData( isTokenValid = true, loginType = LoginType.QR, passphrase = pendingPassphrase, sessionPaths = emptySessionPaths, ) - sessionStore.storeData(sessionData) - SessionId(sessionData.userId) } + sessionStore.storeData(sessionData) + SessionId(sessionData.userId) }.mapFailure { when (it) { is QrCodeDecodeException -> QrErrorMapper.map(it) @@ -252,14 +280,80 @@ class RustMatrixAuthenticationService @Inject constructor( } } - private suspend fun getBaseClientBuilder( + private suspend fun makeClient( + sessionPaths: SessionPaths, + config: suspend ClientBuilder.() -> ClientBuilder, + ): Client { + val slidingSyncType = getSlidingSyncType() + if (slidingSyncType is ClientBuilderSlidingSync.Simplified) { + Timber.d("Creating client with simplified sliding sync") + try { + return rustMatrixClientFactory + .getBaseClientBuilder( + sessionPaths = sessionPaths, + passphrase = pendingPassphrase, + slidingSyncType = slidingSyncType, + ) + .config() + .build() + } catch (e: ClientBuildException.SlidingSyncVersion) { + Timber.e(e, "Failed to create client with simplified sliding sync, trying with Proxy now") + } + } + Timber.d("Creating client with Proxy sliding sync") + return rustMatrixClientFactory + .getBaseClientBuilder( + sessionPaths = sessionPaths, + passphrase = pendingPassphrase, + slidingSyncType = getSlidingSyncProxy(), + ) + .config() + .build() + } + + private suspend fun makeQrCodeLoginClient( sessionPaths: SessionPaths, - ) = rustMatrixClientFactory - .getBaseClientBuilder( - sessionPaths = sessionPaths, - passphrase = pendingPassphrase, - restore = false, - ) + passphrase: String?, + qrCodeData: QrCodeData, + oidcConfiguration: OidcConfiguration, + progressListener: QrLoginProgressListener, + ): Client { + val slidingSyncType = getSlidingSyncType() + if (slidingSyncType is ClientBuilderSlidingSync.Simplified) { + Timber.d("Creating client for QR Code login with simplified sliding sync") + try { + return rustMatrixClientFactory + .getBaseClientBuilder( + sessionPaths = sessionPaths, + passphrase = pendingPassphrase, + slidingSyncType = slidingSyncType, + ) + .passphrase(passphrase) + .buildWithQrCode(qrCodeData, oidcConfiguration, progressListener) + } catch (e: HumanQrLoginException.SlidingSyncNotAvailable) { + Timber.e(e, "Failed to create client with simplified sliding sync, trying with Proxy now") + } + } + Timber.d("Creating client for QR Code login with Proxy sliding sync") + return rustMatrixClientFactory + .getBaseClientBuilder( + sessionPaths = sessionPaths, + passphrase = pendingPassphrase, + slidingSyncType = getSlidingSyncProxy(), + ) + .passphrase(passphrase) + .buildWithQrCode(qrCodeData, oidcConfiguration, progressListener) + } + + private suspend fun getSlidingSyncType(nativeSlidingSyncFailed: Boolean = false) = when { + appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first() && !nativeSlidingSyncFailed -> ClientBuilderSlidingSync.Simplified + else -> getSlidingSyncProxy() + } + + private fun getSlidingSyncProxy() = when { + AuthenticationConfig.SLIDING_SYNC_PROXY_URL != null -> ClientBuilderSlidingSync.CustomProxy(AuthenticationConfig.SLIDING_SYNC_PROXY_URL!!) + else -> ClientBuilderSlidingSync.Discovered + } private fun clear() { currentClient?.close() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryExceptionMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryExceptionMapper.kt index e200ca5434..b33d59375b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryExceptionMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryExceptionMapper.kt @@ -14,13 +14,17 @@ import org.matrix.rustcomponents.sdk.RecoveryException as RustRecoveryException fun Throwable.mapRecoveryException(): RecoveryException { return when (this) { - is RustRecoveryException.SecretStorage -> RecoveryException.SecretStorage( - message = errorMessage - ) - is RustRecoveryException.BackupExistsOnServer -> RecoveryException.BackupExistsOnServer - is RustRecoveryException.Client -> RecoveryException.Client( - source.mapClientException() - ) + is RustRecoveryException -> { + when (this) { + is RustRecoveryException.SecretStorage -> RecoveryException.SecretStorage( + message = errorMessage + ) + is RustRecoveryException.BackupExistsOnServer -> RecoveryException.BackupExistsOnServer + is RustRecoveryException.Client -> RecoveryException.Client( + source.mapClientException() + ) + } + } else -> RecoveryException.Client( ClientException.Other("Unknown error") ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt index c15b873d73..87364930ce 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt @@ -12,7 +12,11 @@ import org.matrix.rustcomponents.sdk.ClientException as RustClientException fun Throwable.mapClientException(): ClientException { return when (this) { - is RustClientException.Generic -> ClientException.Generic(msg) + is RustClientException -> { + when (this) { + is RustClientException.Generic -> ClientException.Generic(msg) + } + } else -> ClientException.Other(message ?: "Unknown error") } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt index 95bf823872..0f9bd5c87d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.impl.mapper +import io.element.android.libraries.matrix.api.auth.external.ExternalSession import io.element.android.libraries.matrix.impl.paths.SessionPaths import io.element.android.libraries.sessionstorage.api.LoginType import io.element.android.libraries.sessionstorage.api.SessionData @@ -35,3 +36,24 @@ internal fun Session.toSessionData( sessionPath = sessionPaths.fileDirectory.absolutePath, cachePath = sessionPaths.cacheDirectory.absolutePath, ) + +internal fun ExternalSession.toSessionData( + isTokenValid: Boolean, + loginType: LoginType, + passphrase: String?, + sessionPaths: SessionPaths, +) = SessionData( + userId = userId, + deviceId = deviceId, + accessToken = accessToken, + refreshToken = refreshToken, + homeserverUrl = homeserverUrl, + oidcData = null, + slidingSyncProxy = slidingSyncProxy, + loginTimestamp = Date(), + isTokenValid = isTokenValid, + loginType = loginType, + passphrase = passphrase, + sessionPath = sessionPaths.fileDirectory.absolutePath, + cachePath = sessionPaths.cacheDirectory.absolutePath, +) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt index d5a0b86083..c7ed6c1205 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt @@ -28,6 +28,7 @@ class MatrixRoomInfoMapper { fun map(rustRoomInfo: RustRoomInfo): MatrixRoomInfo = rustRoomInfo.let { return MatrixRoomInfo( id = RoomId(it.id), + creator = it.creator?.let(::UserId), name = it.displayName, rawName = it.rawName, topic = it.topic, @@ -38,7 +39,7 @@ class MatrixRoomInfoMapper { isTombstoned = it.isTombstoned, isFavorite = it.isFavourite, canonicalAlias = it.canonicalAlias?.let(::RoomAlias), - alternativeAliases = it.alternativeAliases.toImmutableList(), + alternativeAliases = it.alternativeAliases.map(::RoomAlias).toImmutableList(), currentUserMembership = it.membership.map(), inviter = it.inviter?.let(RoomMemberMapper::map), activeMembersCount = it.activeMembersCount.toLong(), @@ -49,7 +50,7 @@ class MatrixRoomInfoMapper { notificationCount = it.notificationCount.toLong(), userDefinedNotificationMode = it.cachedUserDefinedNotificationMode?.map(), hasRoomCall = it.hasRoomCall, - activeRoomCallParticipants = it.activeRoomCallParticipants.toImmutableList(), + activeRoomCallParticipants = it.activeRoomCallParticipants.map(::UserId).toImmutableList(), heroes = it.elementHeroes().toImmutableList(), pinnedEventIds = it.pinnedEventIds.map(::EventId).toImmutableList(), ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustInvitedRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustInvitedRoom.kt new file mode 100644 index 0000000000..67e9e5e7a5 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustInvitedRoom.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.room + +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.room.InvitedRoom +import org.matrix.rustcomponents.sdk.Room + +class RustInvitedRoom( + override val sessionId: SessionId, + private val invitedRoom: Room, +) : InvitedRoom { + override val roomId = RoomId(invitedRoom.id()) + + override suspend fun declineInvite(): Result = runCatching { + invitedRoom.leave() + } + + override fun close() { + invitedRoom.destroy() + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index aad3777071..b871062f70 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -459,8 +459,8 @@ class RustMatrixRoom( return liveTimeline.forwardEvent(eventId, roomIds) } - override suspend fun retrySendMessage(transactionId: TransactionId): Result { - return Result.failure(UnsupportedOperationException("Not supported")) + override suspend fun retrySendMessage(transactionId: TransactionId): Result = runCatching { + innerRoom.tryResend(transactionId.value) } override suspend fun cancelSend(transactionId: TransactionId): Result { @@ -645,6 +645,22 @@ class RustMatrixRoom( innerRoom.clearComposerDraft() } + override suspend fun ignoreDeviceTrustAndResend(devices: Map>, transactionId: TransactionId) = runCatching { + innerRoom.ignoreDeviceTrustAndResend( + devices = devices.entries.associate { entry -> + entry.key.value to entry.value.map { it.value } + }, + transactionId = transactionId.value + ) + } + + override suspend fun withdrawVerificationAndResend(userIds: List, transactionId: TransactionId) = runCatching { + innerRoom.withdrawVerificationAndResend( + userIds = userIds.map { it.value }, + transactionId = transactionId.value + ) + } + private fun createTimeline( timeline: InnerTimeline, mode: Timeline.Mode, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt index 04210d6959..a440578513 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.core.DeviceId 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.notificationsettings.NotificationSettingsService +import io.element.android.libraries.matrix.api.room.InvitedRoom import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.awaitLoaded @@ -27,6 +28,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.FilterTimelineEventType +import org.matrix.rustcomponents.sdk.Membership import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.RoomListException import org.matrix.rustcomponents.sdk.RoomListItem @@ -123,6 +125,33 @@ class RustRoomFactory( } } + suspend fun createInvitedRoom(roomId: RoomId): InvitedRoom? = withContext(dispatcher) { + if (isDestroyed) { + Timber.d("Room factory is destroyed, returning null for $roomId") + return@withContext null + } + val roomListItem = innerRoomListService.roomOrNull(roomId.value) + if (roomListItem == null) { + Timber.d("Room not found for $roomId") + return@withContext null + } + if (roomListItem.membership() != Membership.INVITED) { + Timber.d("Room $roomId is not in invited state") + return@withContext null + } + val invitedRoom = try { + roomListItem.invitedRoom() + } catch (e: RoomListException) { + Timber.e(e, "Failed to get invited room for $roomId") + return@withContext null + } + + RustInvitedRoom( + sessionId = sessionId, + invitedRoom = invitedRoom, + ) + } + private suspend fun getRoomReferences(roomId: RoomId): RustRoomReferences? { cache[roomId]?.let { Timber.d("Room found in cache for $roomId") diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt index e91242f165..267de74ba7 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt @@ -21,13 +21,17 @@ class RoomDescriptionMapper { topic = roomDescription.topic, avatarUrl = roomDescription.avatarUrl, alias = roomDescription.alias?.let(::RoomAlias), - joinRule = when (roomDescription.joinRule) { - PublicRoomJoinRule.PUBLIC -> RoomDescription.JoinRule.PUBLIC - PublicRoomJoinRule.KNOCK -> RoomDescription.JoinRule.KNOCK - null -> RoomDescription.JoinRule.UNKNOWN - }, + joinRule = roomDescription.joinRule.map(), isWorldReadable = roomDescription.isWorldReadable, numberOfMembers = roomDescription.joinedMembers.toLong(), ) } } + +internal fun PublicRoomJoinRule?.map(): RoomDescription.JoinRule { + return when (this) { + PublicRoomJoinRule.PUBLIC -> RoomDescription.JoinRule.PUBLIC + PublicRoomJoinRule.KNOCK -> RoomDescription.JoinRule.KNOCK + null -> RoomDescription.JoinRule.UNKNOWN + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListEntriesUpdateExt.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListEntriesUpdateExt.kt index d0ed9bb777..e03c8529c7 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListEntriesUpdateExt.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListEntriesUpdateExt.kt @@ -7,8 +7,11 @@ package io.element.android.libraries.matrix.impl.roomlist +import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate +@Suppress("unused") +@ExcludeFromCoverage internal fun RoomListEntriesUpdate.describe(): String { return when (this) { is RoomListEntriesUpdate.Set -> { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt index b4a2d79432..b61f9eaae2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt @@ -91,11 +91,7 @@ internal class RustRoomListService( } override suspend fun subscribeToVisibleRooms(roomIds: List) { - val toSubscribe = roomIds.filterNot { roomSyncSubscriber.isSubscribedTo(it) } - if (toSubscribe.isNotEmpty()) { - Timber.d("Subscribe to ${toSubscribe.size} rooms: $toSubscribe") - roomSyncSubscriber.batchSubscribe(toSubscribe) - } + roomSyncSubscriber.batchSubscribe(roomIds) } override val allRooms: DynamicRoomList = roomListFactory.createRoomList( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt index cf52609fa0..0a859ee146 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt @@ -16,15 +16,22 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn -import org.matrix.rustcomponents.sdk.SyncServiceInterface import org.matrix.rustcomponents.sdk.SyncServiceState import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean +import org.matrix.rustcomponents.sdk.SyncService as InnerSyncService class RustSyncService( - private val innerSyncService: SyncServiceInterface, + private val innerSyncService: InnerSyncService, sessionCoroutineScope: CoroutineScope ) : SyncService { + private val isServiceReady = AtomicBoolean(true) + override suspend fun startSync() = runCatching { + if (!isServiceReady.get()) { + Timber.d("Can't start sync: service is not ready") + return@runCatching + } Timber.i("Start sync") innerSyncService.start() }.onFailure { @@ -32,12 +39,24 @@ class RustSyncService( } override suspend fun stopSync() = runCatching { + if (!isServiceReady.get()) { + Timber.d("Can't stop sync: service is not ready") + return@runCatching + } Timber.i("Stop sync") innerSyncService.stop() }.onFailure { Timber.d("Stop sync failed: $it") } + suspend fun destroy() { + // If the service was still running, stop it + stopSync() + Timber.d("Destroying sync service") + isServiceReady.set(false) + innerSyncService.destroy() + } + override val syncState: StateFlow = innerSyncService.stateFlow() .map(SyncServiceState::toSyncState) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt index 47be0fd07a..a48817efa5 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt @@ -100,7 +100,8 @@ internal class MatrixTimelineDiffProcessor( clear() } TimelineChange.TRUNCATE -> { - // Not supported + val index = diff.truncate() ?: return + subList(index.toInt(), size).clear() } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 2cd3d9600c..4b58ff0109 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -86,7 +86,7 @@ class RustTimeline( onNewSyncedEvent: () -> Unit, ) : Timeline { private val initLatch = CompletableDeferred() - private val isInit = MutableStateFlow(false) + private val isTimelineInitialized = MutableStateFlow(false) private val _timelineItems: MutableStateFlow> = MutableStateFlow(emptyList()) @@ -110,7 +110,7 @@ class RustTimeline( timelineCoroutineScope = coroutineScope, timelineDiffProcessor = timelineDiffProcessor, initLatch = initLatch, - isInit = isInit, + isTimelineInitialized = isTimelineInitialized, dispatcher = dispatcher, onNewSyncedEvent = onNewSyncedEvent, ) @@ -189,7 +189,7 @@ class RustTimeline( } private fun canPaginate(direction: Timeline.PaginationDirection): Boolean { - if (!isInit.value) return false + if (!isTimelineInitialized.value) return false return when (direction) { Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus.value.canPaginate Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.value.canPaginate @@ -207,23 +207,37 @@ class RustTimeline( _timelineItems, backPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged(), forwardPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged(), - isInit, - ) { timelineItems, hasMoreToLoadBackward, hasMoreToLoadForward, isInit -> + matrixRoom.roomInfoFlow.map { it.creator }, + isTimelineInitialized, + ) { timelineItems, + hasMoreToLoadBackward, + hasMoreToLoadForward, + roomCreator, + isTimelineInitialized -> withContext(dispatcher) { timelineItems - .process { items -> + .let { items -> roomBeginningPostProcessor.process( items = items, isDm = matrixRoom.isDm, - hasMoreToLoadBackwards = hasMoreToLoadBackward + roomCreator = roomCreator, + hasMoreToLoadBackwards = hasMoreToLoadBackward, ) } - .process(predicate = isInit) { items -> - loadingIndicatorsPostProcessor.process(items, hasMoreToLoadBackward, hasMoreToLoadForward) + .let { items -> + loadingIndicatorsPostProcessor.process( + items = items, + isTimelineInitialized = isTimelineInitialized, + hasMoreToLoadBackward = hasMoreToLoadBackward, + hasMoreToLoadForward = hasMoreToLoadForward + ) } // Keep lastForwardIndicatorsPostProcessor last - .process(predicate = isInit) { items -> - lastForwardIndicatorsPostProcessor.process(items) + .let { items -> + lastForwardIndicatorsPostProcessor.process( + items = items, + isTimelineInitialized = isTimelineInitialized, + ) } } }.onStart { @@ -559,14 +573,3 @@ class RustTimeline( } } } - -private suspend fun List.process( - predicate: Boolean = true, - processor: suspend (List) -> List -): List { - return if (predicate) { - processor(this) - } else { - this - } -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriber.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriber.kt index 0269c48bae..5e9c6ac37a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriber.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriber.kt @@ -38,7 +38,7 @@ internal class TimelineItemsSubscriber( private val timeline: Timeline, private val timelineDiffProcessor: MatrixTimelineDiffProcessor, private val initLatch: CompletableDeferred, - private val isInit: MutableStateFlow, + private val isTimelineInitialized: MutableStateFlow, private val onNewSyncedEvent: () -> Unit, ) { private var subscriptionCount = 0 @@ -85,13 +85,13 @@ internal class TimelineItemsSubscriber( ensureActive() timelineDiffProcessor.postItems(it) } - isInit.value = true + isTimelineInitialized.value = true initLatch.complete(Unit) } private suspend fun postDiffs(diffs: List) { val diffsToProcess = diffs.toMutableList() - if (!isInit.value) { + if (!isTimelineInitialized.value) { val resetDiff = diffsToProcess.firstOrNull { it.change() == TimelineChange.RESET } if (resetDiff != null) { // Keep using the postItems logic so we can post the timelineItems asap. diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt index 5f170c9387..f83d6d0672 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.impl.timeline.item.event +import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UserId @@ -87,7 +88,9 @@ fun RustEventSendState?.map(): LocalEventSendState? { } is RustEventSendState.VerifiedUserHasUnsignedDevice -> { LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice( - devices = devices.mapKeys { UserId(it.key) } + devices = devices.entries.associate { entry -> + UserId(entry.key) to entry.value.map { DeviceId(it) } + } ) } EventSendState.CrossSigningNotSetup -> LocalEventSendState.Failed.CrossSigningNotSetup diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt index 193682f81b..7f89c40a0e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessor.kt @@ -22,7 +22,9 @@ class LastForwardIndicatorsPostProcessor( fun process( items: List, + isTimelineInitialized: Boolean, ): List { + if (!isTimelineInitialized) return items // We don't need to add the last forward indicator if we are not in the FOCUSED_ON_EVENT mode if (mode != Timeline.Mode.FOCUSED_ON_EVENT) { return items diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessor.kt index a4aa6bf05a..216ccd6f89 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessor.kt @@ -16,9 +16,11 @@ import io.element.android.services.toolbox.api.systemclock.SystemClock class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) { fun process( items: List, + isTimelineInitialized: Boolean, hasMoreToLoadBackward: Boolean, hasMoreToLoadForward: Boolean, ): List { + if (!isTimelineInitialized) return items val shouldAddForwardLoadingIndicator = hasMoreToLoadForward && items.isNotEmpty() val currentTimestamp = systemClock.epochMillis() return buildList { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt index 2fb41a745c..7ed9de2fe3 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor import androidx.annotation.VisibleForTesting import io.element.android.libraries.matrix.api.core.UniqueId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange @@ -25,12 +26,14 @@ class RoomBeginningPostProcessor(private val mode: Timeline.Mode) { fun process( items: List, isDm: Boolean, - hasMoreToLoadBackwards: Boolean + roomCreator: UserId?, + hasMoreToLoadBackwards: Boolean, ): List { return when { + items.isEmpty() -> items mode == Timeline.Mode.PINNED_EVENTS -> items + isDm -> processForDM(items, roomCreator) hasMoreToLoadBackwards -> items - isDm -> processForDM(items) else -> processForRoom(items) } } @@ -40,15 +43,18 @@ class RoomBeginningPostProcessor(private val mode: Timeline.Mode) { return listOf(roomBeginningItem) + items } - private fun processForDM(items: List): List { - // Find room creation event. This is usually index 0 + private fun processForDM(items: List, roomCreator: UserId?): List { + // Find room creation event. + // This is usually the first MatrixTimelineItem.Event (so index 1, index 0 is a date) val roomCreationEventIndex = items.indexOfFirst { val stateEventContent = (it as? MatrixTimelineItem.Event)?.event?.content as? StateContent stateEventContent?.content is OtherState.RoomCreate } - // Find self-join event for room creator. This is usually index 1 - val roomCreatorUserId = (items.getOrNull(roomCreationEventIndex) as? MatrixTimelineItem.Event)?.event?.sender + // If the parameter roomCreator is null, the creator is the sender of the RoomCreate Event. + val roomCreatorUserId = roomCreator ?: (items.getOrNull(roomCreationEventIndex) as? MatrixTimelineItem.Event)?.event?.sender + // Find self-join event for the room creator. + // This is usually the second MatrixTimelineItem.Event (so index 2) val selfUserJoinedEventIndex = roomCreatorUserId?.let { creatorUserId -> items.indexOfFirst { val stateEventContent = (it as? MatrixTimelineItem.Event)?.event?.content as? RoomMembershipContent @@ -56,6 +62,9 @@ class RoomBeginningPostProcessor(private val mode: Timeline.Mode) { } } ?: -1 + if (roomCreationEventIndex == -1 && selfUserJoinedEventIndex == -1) { + return items + } // Remove items at the indices we found val newItems = items.toMutableList() if (selfUserJoinedEventIndex in newItems.indices) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt index 9d55d3402c..71004a74d7 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt @@ -206,6 +206,7 @@ class RustSessionVerificationService( } } } + private suspend fun updateVerificationStatus() { if (verificationFlowState.value == VerificationFlowState.Finished) { // Calling `encryptionService.verificationState()` performs a network call and it will deadlock if there is no network diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExtKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExtKtTest.kt new file mode 100644 index 0000000000..6140f32b2e --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExtKtTest.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.analytics + +import com.google.common.truth.Truth.assertThat +import im.vector.app.features.analytics.plan.JoinedRoom +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import org.junit.Test + +class JoinedRoomExtKtTest { + @Test + fun `test room size mapping`() { + mapOf( + listOf(0L, 1L) to JoinedRoom.RoomSize.One, + listOf(2L, 2L) to JoinedRoom.RoomSize.Two, + listOf(3L, 10L) to JoinedRoom.RoomSize.ThreeToTen, + listOf(11L, 100L) to JoinedRoom.RoomSize.ElevenToOneHundred, + listOf(101L, 1000L) to JoinedRoom.RoomSize.OneHundredAndOneToAThousand, + listOf(1001L, 2000L) to JoinedRoom.RoomSize.MoreThanAThousand + ).forEach { (joinedMemberCounts, expectedRoomSize) -> + joinedMemberCounts.forEach { joinedMemberCount -> + assertThat(aMatrixRoom(joinedMemberCount = joinedMemberCount).toAnalyticsJoinedRoom(null)) + .isEqualTo( + JoinedRoom( + isDM = false, + isSpace = false, + roomSize = expectedRoomSize, + trigger = null + ) + ) + } + } + } + + @Test + fun `test isDirect parameter mapping`() { + assertThat(aMatrixRoom(isDirect = true).toAnalyticsJoinedRoom(null)) + .isEqualTo( + JoinedRoom( + isDM = true, + isSpace = false, + roomSize = JoinedRoom.RoomSize.One, + trigger = null + ) + ) + } + + @Test + fun `test isSpace parameter mapping`() { + assertThat(aMatrixRoom(isSpace = true).toAnalyticsJoinedRoom(null)) + .isEqualTo( + JoinedRoom( + isDM = false, + isSpace = true, + roomSize = JoinedRoom.RoomSize.One, + trigger = null + ) + ) + } + + @Test + fun `test trigger parameter mapping`() { + assertThat(aMatrixRoom().toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite)) + .isEqualTo( + JoinedRoom( + isDM = false, + isSpace = false, + roomSize = JoinedRoom.RoomSize.One, + trigger = JoinedRoom.Trigger.Invite + ) + ) + } + + private fun aMatrixRoom( + isDirect: Boolean = false, + isSpace: Boolean = false, + joinedMemberCount: Long = 0 + ): MatrixRoom { + return FakeMatrixRoom( + isDirect = isDirect, + isSpace = isSpace, + joinedMemberCount = joinedMemberCount + ) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTest.kt index fd007151c8..81a1667f4e 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTest.kt @@ -36,13 +36,24 @@ class AuthenticationExceptionMappingTest { assertThat(ClientBuildException.InvalidServerName("Invalid server name").mapAuthenticationException()) .isException("Invalid server name") - assertThat(ClientBuildException.Sdk("SDK issue").mapAuthenticationException()) - .isException("SDK issue") - assertThat(ClientBuildException.SlidingSyncVersion("Sliding sync not available").mapAuthenticationException()) .isException("Sliding sync not available") } + @Test + fun `mapping other exceptions map to the Generic Kotlin`() { + assertThat(ClientBuildException.Sdk("SDK issue").mapAuthenticationException()) + .isException("SDK issue") + assertThat(ClientBuildException.ServerUnreachable("Server unreachable").mapAuthenticationException()) + .isException("Server unreachable") + assertThat(ClientBuildException.SlidingSync("Sliding Sync").mapAuthenticationException()) + .isException("Sliding Sync") + assertThat(ClientBuildException.WellKnownDeserializationException("WellKnown Deserialization").mapAuthenticationException()) + .isException("WellKnown Deserialization") + assertThat(ClientBuildException.WellKnownLookupFailed("WellKnown Lookup Failed").mapAuthenticationException()) + .isException("WellKnown Lookup Failed") + } + private inline fun ThrowableSubject.isException(message: String) { isInstanceOf(T::class.java) hasMessageThat().isEqualTo(message) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrErrorMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrErrorMapperTest.kt new file mode 100644 index 0000000000..3bbb98fb34 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrErrorMapperTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.auth.qrlogin + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeDecodeException +import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException +import org.junit.Test +import org.matrix.rustcomponents.sdk.HumanQrLoginException as RustHumanQrLoginException +import org.matrix.rustcomponents.sdk.QrCodeDecodeException as RustQrCodeDecodeException + +class QrErrorMapperTest { + @Test + fun `test map QrCodeDecodeException`() { + val result = QrErrorMapper.map(RustQrCodeDecodeException.Crypto("test")) + assertThat(result).isInstanceOf(QrCodeDecodeException.Crypto::class.java) + assertThat(result.message).isEqualTo("test") + } + + @Test + fun `test map HumanQrLoginException`() { + assertThat(QrErrorMapper.map(RustHumanQrLoginException.Cancelled())).isEqualTo(QrLoginException.Cancelled) + assertThat(QrErrorMapper.map(RustHumanQrLoginException.ConnectionInsecure())).isEqualTo(QrLoginException.ConnectionInsecure) + assertThat(QrErrorMapper.map(RustHumanQrLoginException.Declined())).isEqualTo(QrLoginException.Declined) + assertThat(QrErrorMapper.map(RustHumanQrLoginException.Expired())).isEqualTo(QrLoginException.Expired) + assertThat(QrErrorMapper.map(RustHumanQrLoginException.OtherDeviceNotSignedIn())).isEqualTo(QrLoginException.OtherDeviceNotSignedIn) + assertThat(QrErrorMapper.map(RustHumanQrLoginException.LinkingNotSupported())).isEqualTo(QrLoginException.LinkingNotSupported) + assertThat(QrErrorMapper.map(RustHumanQrLoginException.Unknown())).isEqualTo(QrLoginException.Unknown) + assertThat(QrErrorMapper.map(RustHumanQrLoginException.OidcMetadataInvalid())).isEqualTo(QrLoginException.OidcMetadataInvalid) + assertThat(QrErrorMapper.map(RustHumanQrLoginException.SlidingSyncNotAvailable())).isEqualTo(QrLoginException.SlidingSyncNotAvailable) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrLoginProgressExtensionsKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrLoginProgressExtensionsKtTest.kt new file mode 100644 index 0000000000..172b415e98 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrLoginProgressExtensionsKtTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.auth.qrlogin + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep +import org.junit.Test +import org.matrix.rustcomponents.sdk.QrLoginProgress + +class QrLoginProgressExtensionsKtTest { + @Test + fun `mapping QrLoginProgress should return expected result`() { + assertThat(QrLoginProgress.Starting.toStep()) + .isEqualTo(QrCodeLoginStep.Starting) + assertThat(QrLoginProgress.EstablishingSecureChannel(1u, "01").toStep()) + .isEqualTo(QrCodeLoginStep.EstablishingSecureChannel("01")) + assertThat(QrLoginProgress.WaitingForToken("userCode").toStep()) + .isEqualTo(QrCodeLoginStep.WaitingForToken("userCode")) + assertThat(QrLoginProgress.Done.toStep()) + .isEqualTo(QrCodeLoginStep.Finished) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProviderTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProviderTest.kt index 03174d5359..a5333d3c1f 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProviderTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProviderTest.kt @@ -91,8 +91,9 @@ class DefaultElementCallBaseUrlProviderTest { private fun createElementWellKnown(widgetUrl: String): ElementWellKnown { return ElementWellKnown( call = ElementCallWellKnown( - widgetUrl = widgetUrl - ) + widgetUrl = widgetUrl, + ), + registrationHelperUrl = null, ) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/core/ProgressWatcherWrapperKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/core/ProgressWatcherWrapperKtTest.kt new file mode 100644 index 0000000000..316c69ebb7 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/core/ProgressWatcherWrapperKtTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.core + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.ProgressCallback +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.rustcomponents.sdk.TransmissionProgress +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class ProgressWatcherWrapperKtTest { + @Test + fun testToProgressWatcher() = runTest { + suspendCoroutine { continuation -> + val callback = object : ProgressCallback { + override fun onProgress(current: Long, total: Long) { + assertThat(current).isEqualTo(1) + assertThat(total).isEqualTo(2) + continuation.resume(Unit) + } + } + val result = callback.toProgressWatcher() + result.transmissionProgress(TransmissionProgress(1.toULong(), 2.toULong())) + } + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/di/TracingMatrixModuleTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/di/TracingMatrixModuleTest.kt new file mode 100644 index 0000000000..687ca3d56c --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/di/TracingMatrixModuleTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.di + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.core.meta.BuildType +import io.element.android.libraries.matrix.api.tracing.TracingFilterConfigurations +import io.element.android.libraries.matrix.test.core.aBuildMeta +import org.junit.Test + +class TracingMatrixModuleTest { + @Test + fun `providesTracingFilterConfiguration returns debug config for debug build`() { + assertThat(TracingMatrixModule.providesTracingFilterConfiguration(aBuildMeta(BuildType.DEBUG))) + .isEqualTo(TracingFilterConfigurations.debug) + } + + @Test + fun `providesTracingFilterConfiguration returns nightly config for nightly build`() { + assertThat(TracingMatrixModule.providesTracingFilterConfiguration(aBuildMeta(BuildType.NIGHTLY))) + .isEqualTo(TracingFilterConfigurations.nightly) + } + + @Test + fun `providesTracingFilterConfiguration returns release config for release build`() { + assertThat(TracingMatrixModule.providesTracingFilterConfiguration(aBuildMeta(BuildType.RELEASE))) + .isEqualTo(TracingFilterConfigurations.release) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupStateMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupStateMapperTest.kt new file mode 100644 index 0000000000..f529aacbcb --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupStateMapperTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.encryption + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.encryption.BackupState +import org.junit.Test +import org.matrix.rustcomponents.sdk.BackupState as RustBackupState + +class BackupStateMapperTest { + @Test + fun `Ensure that mapping is correct`() { + val sut = BackupStateMapper() + assertThat(sut.map(RustBackupState.UNKNOWN)).isEqualTo(BackupState.UNKNOWN) + assertThat(sut.map(RustBackupState.CREATING)).isEqualTo(BackupState.CREATING) + assertThat(sut.map(RustBackupState.ENABLING)).isEqualTo(BackupState.ENABLING) + assertThat(sut.map(RustBackupState.RESUMING)).isEqualTo(BackupState.RESUMING) + assertThat(sut.map(RustBackupState.ENABLED)).isEqualTo(BackupState.ENABLED) + assertThat(sut.map(RustBackupState.DOWNLOADING)).isEqualTo(BackupState.DOWNLOADING) + assertThat(sut.map(RustBackupState.DISABLING)).isEqualTo(BackupState.DISABLING) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapperTest.kt new file mode 100644 index 0000000000..6148cd6971 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapperTest.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.encryption + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.encryption.BackupUploadState +import org.junit.Test +import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState + +class BackupUploadStateMapperTest { + @Test + fun `Ensure that mapping is correct`() { + val sut = BackupUploadStateMapper() + assertThat(sut.map(RustBackupUploadState.Waiting)) + .isEqualTo(BackupUploadState.Waiting) + assertThat(sut.map(RustBackupUploadState.Error)) + .isEqualTo(BackupUploadState.Error) + assertThat(sut.map(RustBackupUploadState.Done)) + .isEqualTo(BackupUploadState.Done) + assertThat(sut.map(RustBackupUploadState.Uploading(1.toUInt(), 2.toUInt()))) + .isEqualTo(BackupUploadState.Uploading(1, 2)) + } + + @Test + fun `Ensure that full uploading is mapper to Done`() { + val sut = BackupUploadStateMapper() + assertThat(sut.map(RustBackupUploadState.Uploading(2.toUInt(), 2.toUInt()))) + .isEqualTo(BackupUploadState.Done) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/encryption/EnableRecoveryProgressMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/encryption/EnableRecoveryProgressMapperTest.kt new file mode 100644 index 0000000000..fb39ff2d38 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/encryption/EnableRecoveryProgressMapperTest.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.encryption + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress +import org.junit.Test +import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress + +class EnableRecoveryProgressMapperTest { + @Test + fun `Ensure that mapping is correct`() { + val sut = EnableRecoveryProgressMapper() + assertThat(sut.map(RustEnableRecoveryProgress.CreatingRecoveryKey)) + .isEqualTo(EnableRecoveryProgress.CreatingRecoveryKey) + assertThat(sut.map(RustEnableRecoveryProgress.CreatingBackup)) + .isEqualTo(EnableRecoveryProgress.CreatingBackup) + assertThat(sut.map(RustEnableRecoveryProgress.Starting)) + .isEqualTo(EnableRecoveryProgress.Starting) + assertThat(sut.map(RustEnableRecoveryProgress.BackingUp(1.toUInt(), 2.toUInt()))) + .isEqualTo(EnableRecoveryProgress.BackingUp(1, 2)) + assertThat(sut.map(RustEnableRecoveryProgress.RoomKeyUploadError)) + .isEqualTo(EnableRecoveryProgress.RoomKeyUploadError) + assertThat(sut.map(RustEnableRecoveryProgress.Done("recoveryKey"))) + .isEqualTo(EnableRecoveryProgress.Done("recoveryKey")) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryStateMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryStateMapperTest.kt new file mode 100644 index 0000000000..b3c93af7ef --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryStateMapperTest.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.encryption + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.encryption.RecoveryState +import org.junit.Test +import org.matrix.rustcomponents.sdk.RecoveryState as RustRecoveryState + +class RecoveryStateMapperTest { + @Test + fun `Ensure that mapping is correct`() { + val sut = RecoveryStateMapper() + assertThat(sut.map(RustRecoveryState.UNKNOWN)).isEqualTo(RecoveryState.UNKNOWN) + assertThat(sut.map(RustRecoveryState.ENABLED)).isEqualTo(RecoveryState.ENABLED) + assertThat(sut.map(RustRecoveryState.DISABLED)).isEqualTo(RecoveryState.DISABLED) + assertThat(sut.map(RustRecoveryState.INCOMPLETE)).isEqualTo(RecoveryState.INCOMPLETE) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomDescription.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomDescription.kt new file mode 100644 index 0000000000..d80b3d3723 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomDescription.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.factories + +import io.element.android.libraries.matrix.test.A_ROOM_ALIAS +import io.element.android.libraries.matrix.test.A_ROOM_ID +import org.matrix.rustcomponents.sdk.PublicRoomJoinRule +import org.matrix.rustcomponents.sdk.RoomDescription + +internal fun aRustRoomDescription(): RoomDescription { + return RoomDescription( + roomId = A_ROOM_ID.value, + name = "name", + topic = "topic", + alias = A_ROOM_ALIAS.value, + avatarUrl = "avatarUrl", + joinRule = PublicRoomJoinRule.PUBLIC, + isWorldReadable = true, + joinedMembers = 2u + ) +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomHero.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomHero.kt new file mode 100644 index 0000000000..0eac725c2e --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomHero.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.factories + +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.A_USER_ID +import org.matrix.rustcomponents.sdk.RoomHero + +internal fun aRustRoomHero( + userId: UserId = A_USER_ID, +): RoomHero { + return RoomHero( + userId = userId.value, + displayName = "displayName", + avatarUrl = "avatarUrl", + ) +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomInfo.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomInfo.kt new file mode 100644 index 0000000000..e7e3ff219f --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomInfo.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.factories + +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_NAME +import org.matrix.rustcomponents.sdk.Membership +import org.matrix.rustcomponents.sdk.RoomHero +import org.matrix.rustcomponents.sdk.RoomInfo +import org.matrix.rustcomponents.sdk.RoomMember +import org.matrix.rustcomponents.sdk.RoomNotificationMode + +fun aRustRoomInfo( + id: String = A_ROOM_ID.value, + displayName: String? = A_ROOM_NAME, + rawName: String? = A_ROOM_NAME, + topic: String? = null, + avatarUrl: String? = null, + isDirect: Boolean = false, + isPublic: Boolean = false, + isSpace: Boolean = false, + isTombstoned: Boolean = false, + isFavourite: Boolean = false, + canonicalAlias: String? = null, + alternativeAliases: List = listOf(), + membership: Membership = Membership.JOINED, + inviter: RoomMember? = null, + heroes: List = listOf(), + activeMembersCount: ULong = 0uL, + invitedMembersCount: ULong = 0uL, + joinedMembersCount: ULong = 0uL, + userPowerLevels: Map = mapOf(), + highlightCount: ULong = 0uL, + notificationCount: ULong = 0uL, + userDefinedNotificationMode: RoomNotificationMode? = null, + hasRoomCall: Boolean = false, + activeRoomCallParticipants: List = listOf(), + isMarkedUnread: Boolean = false, + numUnreadMessages: ULong = 0uL, + numUnreadNotifications: ULong = 0uL, + numUnreadMentions: ULong = 0uL, + pinnedEventIds: List = listOf(), + roomCreator: UserId? = null, +) = RoomInfo( + id = id, + displayName = displayName, + rawName = rawName, + topic = topic, + avatarUrl = avatarUrl, + isDirect = isDirect, + isPublic = isPublic, + isSpace = isSpace, + isTombstoned = isTombstoned, + isFavourite = isFavourite, + canonicalAlias = canonicalAlias, + alternativeAliases = alternativeAliases, + membership = membership, + inviter = inviter, + heroes = heroes, + activeMembersCount = activeMembersCount, + invitedMembersCount = invitedMembersCount, + joinedMembersCount = joinedMembersCount, + userPowerLevels = userPowerLevels, + highlightCount = highlightCount, + notificationCount = notificationCount, + cachedUserDefinedNotificationMode = userDefinedNotificationMode, + hasRoomCall = hasRoomCall, + activeRoomCallParticipants = activeRoomCallParticipants, + isMarkedUnread = isMarkedUnread, + numUnreadMessages = numUnreadMessages, + numUnreadNotifications = numUnreadNotifications, + numUnreadMentions = numUnreadMentions, + pinnedEventIds = pinnedEventIds, + creator = roomCreator?.value, +) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomMember.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomMember.kt new file mode 100644 index 0000000000..7aebed99ea --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomMember.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.factories + +import io.element.android.libraries.matrix.api.core.UserId +import org.matrix.rustcomponents.sdk.MembershipState +import org.matrix.rustcomponents.sdk.RoomMember +import uniffi.matrix_sdk.RoomMemberRole + +fun aRustRoomMember( + userId: UserId, + displayName: String? = null, + avatarUrl: String? = null, + membership: MembershipState = MembershipState.JOIN, + isNameAmbiguous: Boolean = false, + powerLevel: Long = 0L, + isIgnored: Boolean = false, + role: RoomMemberRole = RoomMemberRole.USER, +) = RoomMember( + userId = userId.value, + displayName = displayName, + avatarUrl = avatarUrl, + membership = membership, + isNameAmbiguous = isNameAmbiguous, + powerLevel = powerLevel, + normalizedPowerLevel = powerLevel, + isIgnored = isIgnored, + suggestedRoleForPowerLevel = role, +) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPowerLevels.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPowerLevels.kt new file mode 100644 index 0000000000..adfb65c19a --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPowerLevels.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.factories + +import org.matrix.rustcomponents.sdk.RoomPowerLevels + +internal fun aRustRoomPowerLevels( + ban: Long, + invite: Long, + kick: Long, + redact: Long, + eventsDefault: Long, + stateDefault: Long, + usersDefault: Long, + roomName: Long, + roomAvatar: Long, + roomTopic: Long, +) = RoomPowerLevels( + ban = ban, + invite = invite, + kick = kick, + redact = redact, + eventsDefault = eventsDefault, + stateDefault = stateDefault, + usersDefault = usersDefault, + roomName = roomName, + roomAvatar = roomAvatar, + roomTopic = roomTopic, +) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPreview.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPreview.kt new file mode 100644 index 0000000000..54abeb90b8 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPreview.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.factories + +import io.element.android.libraries.matrix.test.A_ROOM_ALIAS +import io.element.android.libraries.matrix.test.A_ROOM_ID +import org.matrix.rustcomponents.sdk.RoomPreview + +internal fun aRustRoomPreview( + canonicalAlias: String? = A_ROOM_ALIAS.value, + isJoined: Boolean = true, + isInvited: Boolean = true, + isPublic: Boolean = true, + canKnock: Boolean = true, +): RoomPreview { + return RoomPreview( + roomId = A_ROOM_ID.value, + canonicalAlias = canonicalAlias, + name = "name", + topic = "topic", + avatarUrl = "avatarUrl", + numJoinedMembers = 1u, + roomType = null, + isHistoryWorldReadable = true, + isJoined = isJoined, + isInvited = isInvited, + isPublic = isPublic, + canKnock = canKnock, + ) +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/SearchUsersResults.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/SearchUsersResults.kt new file mode 100644 index 0000000000..f8397d066d --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/SearchUsersResults.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.factories + +import org.matrix.rustcomponents.sdk.SearchUsersResults +import org.matrix.rustcomponents.sdk.UserProfile + +internal fun aRustSearchUsersResults( + results: List, + limited: Boolean, +) = SearchUsersResults( + results = results, + limited = limited, +) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/Session.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/Session.kt new file mode 100644 index 0000000000..64d1667157 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/Session.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.factories + +import io.element.android.libraries.matrix.test.A_DEVICE_ID +import io.element.android.libraries.matrix.test.A_HOMESERVER_URL +import io.element.android.libraries.matrix.test.A_USER_ID +import org.matrix.rustcomponents.sdk.Session +import org.matrix.rustcomponents.sdk.SlidingSyncVersion + +internal fun aRustSession( + proxy: SlidingSyncVersion = SlidingSyncVersion.None +): Session { + return Session( + accessToken = "accessToken", + refreshToken = "refreshToken", + userId = A_USER_ID.value, + deviceId = A_DEVICE_ID.value, + homeserverUrl = A_HOMESERVER_URL, + oidcData = null, + slidingSyncVersion = proxy, + ) +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/UserProfile.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/UserProfile.kt new file mode 100644 index 0000000000..f6444d1376 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/UserProfile.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.factories + +import io.element.android.libraries.matrix.test.A_USER_ID +import org.matrix.rustcomponents.sdk.UserProfile + +fun aRustUserProfile( + userId: String = A_USER_ID.value, + displayName: String = "displayName", + avatarUrl: String = "avatarUrl", +) = UserProfile( + userId = userId, + displayName = displayName, + avatarUrl = avatarUrl, +) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoom.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoom.kt new file mode 100644 index 0000000000..f066f37e9b --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoom.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.tests.testutils.lambda.lambdaError +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.RoomMembersIterator + +class FakeRustRoom( + private val roomId: RoomId = A_ROOM_ID, + private val getMembers: () -> RoomMembersIterator = { lambdaError() }, + private val getMembersNoSync: () -> RoomMembersIterator = { lambdaError() }, +) : Room(NoPointer) { + override fun id(): String { + return roomId.value + } + + override suspend fun members(): RoomMembersIterator { + return getMembers() + } + + override suspend fun membersNoSync(): RoomMembersIterator { + return getMembersNoSync() + } + + override fun close() { + // No-op + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomListItem.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomListItem.kt new file mode 100644 index 0000000000..6d20221d88 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomListItem.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo +import org.matrix.rustcomponents.sdk.EventTimelineItem +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.RoomInfo +import org.matrix.rustcomponents.sdk.RoomListItem + +class FakeRustRoomListItem( + private val roomId: RoomId, + private val roomInfo: RoomInfo = aRustRoomInfo(id = roomId.value), + private val latestEvent: EventTimelineItem? = null, +) : RoomListItem(NoPointer) { + override fun id(): String { + return roomId.value + } + + override suspend fun roomInfo(): RoomInfo { + return roomInfo + } + + override suspend fun latestEvent(): EventTimelineItem? { + return latestEvent + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomMembersIterator.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomMembersIterator.kt new file mode 100644 index 0000000000..6d44ccbc75 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomMembersIterator.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.RoomMember +import org.matrix.rustcomponents.sdk.RoomMembersIterator + +class FakeRustRoomMembersIterator( + private var members: List? = null +) : RoomMembersIterator(NoPointer) { + override fun len(): UInt { + return members?.size?.toUInt() ?: 0u + } + + override fun nextChunk(chunkSize: UInt): List? { + if (members?.isEmpty() == true) { + return null + } + return members?.let { + val result = it.take(chunkSize.toInt()) + members = it.subList(result.size, it.size) + result + } + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineDiff.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineDiff.kt new file mode 100644 index 0000000000..f4fe5e15d9 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineDiff.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.InsertData +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.SetData +import org.matrix.rustcomponents.sdk.TimelineChange +import org.matrix.rustcomponents.sdk.TimelineDiff +import org.matrix.rustcomponents.sdk.TimelineItem + +class FakeRustTimelineDiff( + private val change: TimelineChange, + private val item: TimelineItem? = FakeRustTimelineItem() +) : TimelineDiff(NoPointer) { + override fun change() = change + override fun append(): List? = item?.let { listOf(it) } + override fun insert(): InsertData? = item?.let { InsertData(1u, it) } + override fun pushBack(): TimelineItem? = item + override fun pushFront(): TimelineItem? = item + override fun remove(): UInt? = 1u + override fun reset(): List? = item?.let { listOf(it) } + override fun set(): SetData? = item?.let { SetData(1u, it) } + override fun truncate(): UInt? = 1u +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineItem.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineItem.kt new file mode 100644 index 0000000000..c20698fe62 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineItem.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.EventTimelineItem +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.TimelineItem +import org.matrix.rustcomponents.sdk.VirtualTimelineItem + +class FakeRustTimelineItem : TimelineItem(NoPointer) { + override fun asEvent(): EventTimelineItem? = null + override fun asVirtual(): VirtualTimelineItem? = null + override fun fmtDebug(): String = "fmtDebug" + override fun uniqueId(): String = "uniqueId" +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultPassphraseGeneratorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultPassphraseGeneratorTest.kt new file mode 100644 index 0000000000..e4ac3b591e --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultPassphraseGeneratorTest.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.keys + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DefaultPassphraseGeneratorTest { + @Test + fun `check that generated passphrase has the expected length`() { + val passphraseGenerator = DefaultPassphraseGenerator() + val passphrase = passphraseGenerator.generatePassphrase() + assertThat(passphrase!!.length).isEqualTo(342) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/mapper/SessionKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/mapper/SessionKtTest.kt new file mode 100644 index 0000000000..9e6d87957c --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/mapper/SessionKtTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.mapper + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession +import io.element.android.libraries.matrix.impl.paths.SessionPaths +import io.element.android.libraries.matrix.test.A_DEVICE_ID +import io.element.android.libraries.matrix.test.A_HOMESERVER_URL +import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2 +import io.element.android.libraries.matrix.test.A_SECRET +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.sessionstorage.api.LoginType +import org.junit.Test +import org.matrix.rustcomponents.sdk.SlidingSyncVersion +import java.io.File + +class SessionKtTest { + @Test + fun `toSessionData compute the expected result`() { + val result = aRustSession().toSessionData( + isTokenValid = true, + loginType = LoginType.PASSWORD, + passphrase = A_SECRET, + sessionPaths = SessionPaths(File("/a/file"), File("/a/cache")), + ) + assertThat(result.userId).isEqualTo(A_USER_ID.value) + assertThat(result.deviceId).isEqualTo(A_DEVICE_ID.value) + assertThat(result.accessToken).isEqualTo("accessToken") + assertThat(result.refreshToken).isEqualTo("refreshToken") + assertThat(result.homeserverUrl).isEqualTo(A_HOMESERVER_URL) + assertThat(result.isTokenValid).isTrue() + assertThat(result.oidcData).isNull() + assertThat(result.slidingSyncProxy).isNull() + assertThat(result.loginType).isEqualTo(LoginType.PASSWORD) + assertThat(result.loginTimestamp).isNotNull() + assertThat(result.passphrase).isEqualTo(A_SECRET) + assertThat(result.sessionPath).isEqualTo("/a/file") + assertThat(result.cachePath).isEqualTo("/a/cache") + } + + @Test + fun `toSessionData can change the validity of the token`() { + val result = aRustSession().toSessionData( + isTokenValid = false, + loginType = LoginType.PASSWORD, + passphrase = A_SECRET, + sessionPaths = SessionPaths(File("/a/file"), File("/a/cache")), + homeserverUrl = null, + ) + assertThat(result.isTokenValid).isFalse() + } + + @Test + fun `toSessionData can override the value of the homeserver url`() { + val result = aRustSession().toSessionData( + isTokenValid = true, + loginType = LoginType.PASSWORD, + passphrase = A_SECRET, + sessionPaths = SessionPaths(File("/a/file"), File("/a/cache")), + homeserverUrl = A_HOMESERVER_URL_2, + ) + assertThat(result.homeserverUrl).isEqualTo(A_HOMESERVER_URL_2) + } + + @Test + fun `toSessionData copy the sliding sync url if present`() { + val result = aRustSession( + proxy = SlidingSyncVersion.Proxy("proxyUrl") + ).toSessionData( + isTokenValid = true, + loginType = LoginType.PASSWORD, + passphrase = A_SECRET, + sessionPaths = SessionPaths(File("/a/file"), File("/a/cache")), + homeserverUrl = A_HOMESERVER_URL_2, + ) + assertThat(result.slidingSyncProxy).isEqualTo("proxyUrl") + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/poll/PollKindKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/poll/PollKindKtTest.kt new file mode 100644 index 0000000000..4be515530c --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/poll/PollKindKtTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2023, 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.poll + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.poll.PollKind +import org.junit.Test +import org.matrix.rustcomponents.sdk.PollKind as RustPollKind + +class PollKindKtTest { + @Test + fun `map should return Disclosed when RustPollKind is Disclosed`() { + val pollKind = RustPollKind.DISCLOSED.map() + assertThat(pollKind).isEqualTo(PollKind.Disclosed) + } + + @Test + fun `map should return Undisclosed when RustPollKind is Undisclosed`() { + val pollKind = RustPollKind.UNDISCLOSED.map() + assertThat(pollKind).isEqualTo(PollKind.Undisclosed) + } + + @Test + fun `toInner should return DISCLOSED when PollKind is Disclosed`() { + val rustPollKind = PollKind.Disclosed.toInner() + assertThat(rustPollKind).isEqualTo(RustPollKind.DISCLOSED) + } + + @Test + fun `toInner should return UNDISCLOSED when PollKind is Undisclosed`() { + val rustPollKind = PollKind.Undisclosed.toInner() + assertThat(rustPollKind).isEqualTo(RustPollKind.UNDISCLOSED) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt new file mode 100644 index 0000000000..c9e80f493d --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt @@ -0,0 +1,194 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.room + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.MatrixRoomInfo +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomHero +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomMember +import io.element.android.libraries.matrix.test.AN_AVATAR_URL +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_ROOM_ALIAS +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_3 +import io.element.android.libraries.matrix.test.A_USER_ID_6 +import io.element.android.libraries.matrix.test.room.aRoomMember +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableMap +import kotlinx.collections.immutable.toPersistentList +import org.junit.Test +import org.matrix.rustcomponents.sdk.Membership +import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode + +class MatrixRoomInfoMapperTest { + @Test + fun `mapping of RustRoomInfo should map all the fields`() { + assertThat( + MatrixRoomInfoMapper().map( + aRustRoomInfo( + id = A_ROOM_ID.value, + displayName = "displayName", + rawName = "rawName", + topic = "topic", + avatarUrl = AN_AVATAR_URL, + isDirect = true, + isPublic = false, + isSpace = false, + isTombstoned = false, + isFavourite = false, + canonicalAlias = A_ROOM_ALIAS.value, + alternativeAliases = listOf(A_ROOM_ALIAS.value), + membership = Membership.JOINED, + inviter = aRustRoomMember(A_USER_ID), + heroes = listOf(aRustRoomHero()), + activeMembersCount = 2uL, + invitedMembersCount = 3uL, + joinedMembersCount = 4uL, + userPowerLevels = mapOf(A_USER_ID_6.value to 50L), + highlightCount = 10uL, + notificationCount = 11uL, + userDefinedNotificationMode = RustRoomNotificationMode.MUTE, + hasRoomCall = true, + activeRoomCallParticipants = listOf(A_USER_ID_3.value), + isMarkedUnread = false, + numUnreadMessages = 12uL, + numUnreadNotifications = 13uL, + numUnreadMentions = 14uL, + pinnedEventIds = listOf(AN_EVENT_ID.value), + roomCreator = A_USER_ID, + ) + ) + ).isEqualTo( + MatrixRoomInfo( + id = A_ROOM_ID, + name = "displayName", + rawName = "rawName", + topic = "topic", + avatarUrl = AN_AVATAR_URL, + isDirect = true, + isPublic = false, + isSpace = false, + isTombstoned = false, + isFavorite = false, + canonicalAlias = A_ROOM_ALIAS, + alternativeAliases = listOf(A_ROOM_ALIAS).toImmutableList(), + currentUserMembership = CurrentUserMembership.JOINED, + inviter = aRoomMember(A_USER_ID), + activeMembersCount = 2L, + invitedMembersCount = 3L, + joinedMembersCount = 4L, + userPowerLevels = mapOf(A_USER_ID_6 to 50L).toImmutableMap(), + highlightCount = 10L, + notificationCount = 11L, + userDefinedNotificationMode = RoomNotificationMode.MUTE, + hasRoomCall = true, + activeRoomCallParticipants = listOf(A_USER_ID_3).toImmutableList(), + heroes = listOf( + MatrixUser( + userId = A_USER_ID, + displayName = "displayName", + avatarUrl = "avatarUrl", + ) + ).toImmutableList(), + pinnedEventIds = listOf(AN_EVENT_ID).toPersistentList(), + creator = A_USER_ID, + ) + ) + } + + @Test + fun `mapping of RustRoomInfo with null members should map all the fields`() { + assertThat( + MatrixRoomInfoMapper().map( + aRustRoomInfo( + id = A_ROOM_ID.value, + displayName = null, + rawName = null, + topic = null, + avatarUrl = null, + isDirect = false, + isPublic = true, + isSpace = false, + isTombstoned = false, + isFavourite = true, + canonicalAlias = null, + alternativeAliases = emptyList(), + membership = Membership.INVITED, + inviter = null, + heroes = listOf(aRustRoomHero()), + activeMembersCount = 2uL, + invitedMembersCount = 3uL, + joinedMembersCount = 4uL, + userPowerLevels = emptyMap(), + highlightCount = 10uL, + notificationCount = 11uL, + userDefinedNotificationMode = null, + hasRoomCall = false, + activeRoomCallParticipants = emptyList(), + isMarkedUnread = true, + numUnreadMessages = 12uL, + numUnreadNotifications = 13uL, + numUnreadMentions = 14uL, + pinnedEventIds = emptyList(), + roomCreator = null, + ) + ) + ).isEqualTo( + MatrixRoomInfo( + id = A_ROOM_ID, + name = null, + rawName = null, + topic = null, + avatarUrl = null, + isDirect = false, + isPublic = true, + isSpace = false, + isTombstoned = false, + isFavorite = true, + canonicalAlias = null, + alternativeAliases = emptyList().toPersistentList(), + currentUserMembership = CurrentUserMembership.INVITED, + inviter = null, + activeMembersCount = 2L, + invitedMembersCount = 3L, + joinedMembersCount = 4L, + userPowerLevels = emptyMap().toImmutableMap(), + highlightCount = 10L, + notificationCount = 11L, + userDefinedNotificationMode = null, + hasRoomCall = false, + activeRoomCallParticipants = emptyList().toImmutableList(), + heroes = emptyList().toImmutableList(), + pinnedEventIds = emptyList().toPersistentList(), + creator = null, + ) + ) + } + + @Test + fun `mapping Membership`() { + assertThat(Membership.INVITED.map()).isEqualTo(CurrentUserMembership.INVITED) + assertThat(Membership.JOINED.map()).isEqualTo(CurrentUserMembership.JOINED) + assertThat(Membership.LEFT.map()).isEqualTo(CurrentUserMembership.LEFT) + } + + @Test + fun `mapping RoomNotificationMode`() { + assertThat(RustRoomNotificationMode.ALL_MESSAGES.map()).isEqualTo(RoomNotificationMode.ALL_MESSAGES) + assertThat(RustRoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY.map()).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + assertThat(RustRoomNotificationMode.MUTE.map()).isEqualTo(RoomNotificationMode.MUTE) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventTypeKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventTypeKtTest.kt new file mode 100644 index 0000000000..3c439d28b0 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventTypeKtTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.room + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.room.MessageEventType +import org.junit.Test +import org.matrix.rustcomponents.sdk.MessageLikeEventType + +class MessageEventTypeKtTest { + @Test + fun `map Rust type should result to correct Kotlin type`() { + assertThat(MessageLikeEventType.CALL_ANSWER.map()).isEqualTo(MessageEventType.CALL_ANSWER) + assertThat(MessageLikeEventType.CALL_INVITE.map()).isEqualTo(MessageEventType.CALL_INVITE) + assertThat(MessageLikeEventType.CALL_HANGUP.map()).isEqualTo(MessageEventType.CALL_HANGUP) + assertThat(MessageLikeEventType.CALL_CANDIDATES.map()).isEqualTo(MessageEventType.CALL_CANDIDATES) + assertThat(MessageLikeEventType.CALL_NOTIFY.map()).isEqualTo(MessageEventType.CALL_NOTIFY) + assertThat(MessageLikeEventType.KEY_VERIFICATION_READY.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_READY) + assertThat(MessageLikeEventType.KEY_VERIFICATION_START.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_START) + assertThat(MessageLikeEventType.KEY_VERIFICATION_CANCEL.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_CANCEL) + assertThat(MessageLikeEventType.KEY_VERIFICATION_ACCEPT.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_ACCEPT) + assertThat(MessageLikeEventType.KEY_VERIFICATION_KEY.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_KEY) + assertThat(MessageLikeEventType.KEY_VERIFICATION_MAC.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_MAC) + assertThat(MessageLikeEventType.KEY_VERIFICATION_DONE.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_DONE) + assertThat(MessageLikeEventType.REACTION.map()).isEqualTo(MessageEventType.REACTION) + assertThat(MessageLikeEventType.ROOM_ENCRYPTED.map()).isEqualTo(MessageEventType.ROOM_ENCRYPTED) + assertThat(MessageLikeEventType.ROOM_MESSAGE.map()).isEqualTo(MessageEventType.ROOM_MESSAGE) + assertThat(MessageLikeEventType.ROOM_REDACTION.map()).isEqualTo(MessageEventType.ROOM_REDACTION) + assertThat(MessageLikeEventType.STICKER.map()).isEqualTo(MessageEventType.STICKER) + assertThat(MessageLikeEventType.POLL_END.map()).isEqualTo(MessageEventType.POLL_END) + assertThat(MessageLikeEventType.POLL_RESPONSE.map()).isEqualTo(MessageEventType.POLL_RESPONSE) + assertThat(MessageLikeEventType.POLL_START.map()).isEqualTo(MessageEventType.POLL_START) + assertThat(MessageLikeEventType.UNSTABLE_POLL_END.map()).isEqualTo(MessageEventType.UNSTABLE_POLL_END) + assertThat(MessageLikeEventType.UNSTABLE_POLL_RESPONSE.map()).isEqualTo(MessageEventType.UNSTABLE_POLL_RESPONSE) + assertThat(MessageLikeEventType.UNSTABLE_POLL_START.map()).isEqualTo(MessageEventType.UNSTABLE_POLL_START) + } + + @Test + fun `map Kotlin type should result to correct Rust type`() { + assertThat(MessageEventType.CALL_ANSWER.map()).isEqualTo(MessageLikeEventType.CALL_ANSWER) + assertThat(MessageEventType.CALL_INVITE.map()).isEqualTo(MessageLikeEventType.CALL_INVITE) + assertThat(MessageEventType.CALL_HANGUP.map()).isEqualTo(MessageLikeEventType.CALL_HANGUP) + assertThat(MessageEventType.CALL_CANDIDATES.map()).isEqualTo(MessageLikeEventType.CALL_CANDIDATES) + assertThat(MessageEventType.CALL_NOTIFY.map()).isEqualTo(MessageLikeEventType.CALL_NOTIFY) + assertThat(MessageEventType.KEY_VERIFICATION_READY.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_READY) + assertThat(MessageEventType.KEY_VERIFICATION_START.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_START) + assertThat(MessageEventType.KEY_VERIFICATION_CANCEL.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_CANCEL) + assertThat(MessageEventType.KEY_VERIFICATION_ACCEPT.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_ACCEPT) + assertThat(MessageEventType.KEY_VERIFICATION_KEY.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_KEY) + assertThat(MessageEventType.KEY_VERIFICATION_MAC.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_MAC) + assertThat(MessageEventType.KEY_VERIFICATION_DONE.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_DONE) + assertThat(MessageEventType.REACTION.map()).isEqualTo(MessageLikeEventType.REACTION) + assertThat(MessageEventType.ROOM_ENCRYPTED.map()).isEqualTo(MessageLikeEventType.ROOM_ENCRYPTED) + assertThat(MessageEventType.ROOM_MESSAGE.map()).isEqualTo(MessageLikeEventType.ROOM_MESSAGE) + assertThat(MessageEventType.ROOM_REDACTION.map()).isEqualTo(MessageLikeEventType.ROOM_REDACTION) + assertThat(MessageEventType.STICKER.map()).isEqualTo(MessageLikeEventType.STICKER) + assertThat(MessageEventType.POLL_END.map()).isEqualTo(MessageLikeEventType.POLL_END) + assertThat(MessageEventType.POLL_RESPONSE.map()).isEqualTo(MessageLikeEventType.POLL_RESPONSE) + assertThat(MessageEventType.POLL_START.map()).isEqualTo(MessageLikeEventType.POLL_START) + assertThat(MessageEventType.UNSTABLE_POLL_END.map()).isEqualTo(MessageLikeEventType.UNSTABLE_POLL_END) + assertThat(MessageEventType.UNSTABLE_POLL_RESPONSE.map()).isEqualTo(MessageLikeEventType.UNSTABLE_POLL_RESPONSE) + assertThat(MessageEventType.UNSTABLE_POLL_START.map()).isEqualTo(MessageLikeEventType.UNSTABLE_POLL_START) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoExtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoExtTest.kt new file mode 100644 index 0000000000..8cf3134825 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoExtTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.room + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomHero +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo +import io.element.android.libraries.matrix.test.A_USER_ID +import org.junit.Test + +class RoomInfoExtTest { + @Test + fun `get non empty element Heroes`() { + val result = aRustRoomInfo( + isDirect = true, + activeMembersCount = 2uL, + heroes = listOf(aRustRoomHero()) + ).elementHeroes() + assertThat(result).isEqualTo( + listOf( + MatrixUser( + userId = UserId(A_USER_ID.value), + displayName = "displayName", + avatarUrl = "avatarUrl", + ) + ) + ) + } + + @Test + fun `too many heroes and element Heroes is empty`() { + val result = aRustRoomInfo( + isDirect = true, + activeMembersCount = 2uL, + heroes = listOf(aRustRoomHero(), aRustRoomHero()) + ).elementHeroes() + assertThat(result).isEmpty() + } + + @Test + fun `not direct and element Heroes is empty`() { + val result = aRustRoomInfo( + isDirect = false, + activeMembersCount = 2uL, + heroes = listOf(aRustRoomHero()) + ).elementHeroes() + assertThat(result).isEmpty() + } + + @Test + fun `too many members and element Heroes is empty`() { + val result = aRustRoomInfo( + isDirect = true, + activeMembersCount = 3uL, + heroes = listOf(aRustRoomHero()) + ).elementHeroes() + assertThat(result).isEmpty() + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RoomTypeKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RoomTypeKtTest.kt new file mode 100644 index 0000000000..541598781e --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RoomTypeKtTest.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ +package io.element.android.libraries.matrix.impl.room + +import io.element.android.libraries.matrix.api.room.RoomType +import org.junit.Test + +class RoomTypeKtTest { + @Test + fun toRoomType() { + assert(null.toRoomType() == RoomType.Room) + assert("m.space".toRoomType() == RoomType.Space) + assert("m.other".toRoomType() == RoomType.Other("m.other")) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/StateEventTypeTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/StateEventTypeTest.kt new file mode 100644 index 0000000000..1900575bbf --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/StateEventTypeTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.room + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.room.StateEventType +import org.junit.Test +import org.matrix.rustcomponents.sdk.StateEventType as RustStateEventType + +class StateEventTypeTest { + @Test + fun `mapping Rust type should work`() { + assertThat(RustStateEventType.CALL_MEMBER.map()).isEqualTo(StateEventType.CALL_MEMBER) + assertThat(RustStateEventType.POLICY_RULE_ROOM.map()).isEqualTo(StateEventType.POLICY_RULE_ROOM) + assertThat(RustStateEventType.POLICY_RULE_SERVER.map()).isEqualTo(StateEventType.POLICY_RULE_SERVER) + assertThat(RustStateEventType.POLICY_RULE_USER.map()).isEqualTo(StateEventType.POLICY_RULE_USER) + assertThat(RustStateEventType.ROOM_ALIASES.map()).isEqualTo(StateEventType.ROOM_ALIASES) + assertThat(RustStateEventType.ROOM_AVATAR.map()).isEqualTo(StateEventType.ROOM_AVATAR) + assertThat(RustStateEventType.ROOM_CANONICAL_ALIAS.map()).isEqualTo(StateEventType.ROOM_CANONICAL_ALIAS) + assertThat(RustStateEventType.ROOM_CREATE.map()).isEqualTo(StateEventType.ROOM_CREATE) + assertThat(RustStateEventType.ROOM_ENCRYPTION.map()).isEqualTo(StateEventType.ROOM_ENCRYPTION) + assertThat(RustStateEventType.ROOM_GUEST_ACCESS.map()).isEqualTo(StateEventType.ROOM_GUEST_ACCESS) + assertThat(RustStateEventType.ROOM_HISTORY_VISIBILITY.map()).isEqualTo(StateEventType.ROOM_HISTORY_VISIBILITY) + assertThat(RustStateEventType.ROOM_JOIN_RULES.map()).isEqualTo(StateEventType.ROOM_JOIN_RULES) + assertThat(RustStateEventType.ROOM_MEMBER_EVENT.map()).isEqualTo(StateEventType.ROOM_MEMBER_EVENT) + assertThat(RustStateEventType.ROOM_NAME.map()).isEqualTo(StateEventType.ROOM_NAME) + assertThat(RustStateEventType.ROOM_PINNED_EVENTS.map()).isEqualTo(StateEventType.ROOM_PINNED_EVENTS) + assertThat(RustStateEventType.ROOM_POWER_LEVELS.map()).isEqualTo(StateEventType.ROOM_POWER_LEVELS) + assertThat(RustStateEventType.ROOM_SERVER_ACL.map()).isEqualTo(StateEventType.ROOM_SERVER_ACL) + assertThat(RustStateEventType.ROOM_THIRD_PARTY_INVITE.map()).isEqualTo(StateEventType.ROOM_THIRD_PARTY_INVITE) + assertThat(RustStateEventType.ROOM_TOMBSTONE.map()).isEqualTo(StateEventType.ROOM_TOMBSTONE) + assertThat(RustStateEventType.ROOM_TOPIC.map()).isEqualTo(StateEventType.ROOM_TOPIC) + assertThat(RustStateEventType.SPACE_CHILD.map()).isEqualTo(StateEventType.SPACE_CHILD) + assertThat(RustStateEventType.SPACE_PARENT.map()).isEqualTo(StateEventType.SPACE_PARENT) + } + + @Test + fun `mapping Kotlin type should work`() { + assertThat(StateEventType.CALL_MEMBER.map()).isEqualTo(RustStateEventType.CALL_MEMBER) + assertThat(StateEventType.POLICY_RULE_ROOM.map()).isEqualTo(RustStateEventType.POLICY_RULE_ROOM) + assertThat(StateEventType.POLICY_RULE_SERVER.map()).isEqualTo(RustStateEventType.POLICY_RULE_SERVER) + assertThat(StateEventType.POLICY_RULE_USER.map()).isEqualTo(RustStateEventType.POLICY_RULE_USER) + assertThat(StateEventType.ROOM_ALIASES.map()).isEqualTo(RustStateEventType.ROOM_ALIASES) + assertThat(StateEventType.ROOM_AVATAR.map()).isEqualTo(RustStateEventType.ROOM_AVATAR) + assertThat(StateEventType.ROOM_CANONICAL_ALIAS.map()).isEqualTo(RustStateEventType.ROOM_CANONICAL_ALIAS) + assertThat(StateEventType.ROOM_CREATE.map()).isEqualTo(RustStateEventType.ROOM_CREATE) + assertThat(StateEventType.ROOM_ENCRYPTION.map()).isEqualTo(RustStateEventType.ROOM_ENCRYPTION) + assertThat(StateEventType.ROOM_GUEST_ACCESS.map()).isEqualTo(RustStateEventType.ROOM_GUEST_ACCESS) + assertThat(StateEventType.ROOM_HISTORY_VISIBILITY.map()).isEqualTo(RustStateEventType.ROOM_HISTORY_VISIBILITY) + assertThat(StateEventType.ROOM_JOIN_RULES.map()).isEqualTo(RustStateEventType.ROOM_JOIN_RULES) + assertThat(StateEventType.ROOM_MEMBER_EVENT.map()).isEqualTo(RustStateEventType.ROOM_MEMBER_EVENT) + assertThat(StateEventType.ROOM_NAME.map()).isEqualTo(RustStateEventType.ROOM_NAME) + assertThat(StateEventType.ROOM_PINNED_EVENTS.map()).isEqualTo(RustStateEventType.ROOM_PINNED_EVENTS) + assertThat(StateEventType.ROOM_POWER_LEVELS.map()).isEqualTo(RustStateEventType.ROOM_POWER_LEVELS) + assertThat(StateEventType.ROOM_SERVER_ACL.map()).isEqualTo(RustStateEventType.ROOM_SERVER_ACL) + assertThat(StateEventType.ROOM_THIRD_PARTY_INVITE.map()).isEqualTo(RustStateEventType.ROOM_THIRD_PARTY_INVITE) + assertThat(StateEventType.ROOM_TOMBSTONE.map()).isEqualTo(RustStateEventType.ROOM_TOMBSTONE) + assertThat(StateEventType.ROOM_TOPIC.map()).isEqualTo(RustStateEventType.ROOM_TOPIC) + assertThat(StateEventType.SPACE_CHILD.map()).isEqualTo(RustStateEventType.SPACE_CHILD) + assertThat(StateEventType.SPACE_PARENT.map()).isEqualTo(RustStateEventType.SPACE_PARENT) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/location/AssetTypeKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/location/AssetTypeKtTest.kt new file mode 100644 index 0000000000..78807bc324 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/location/AssetTypeKtTest.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.room.location + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.room.location.AssetType +import org.junit.Test + +class AssetTypeKtTest { + @Test + fun toInner() { + assertThat(AssetType.SENDER.toInner()).isEqualTo(org.matrix.rustcomponents.sdk.AssetType.SENDER) + assertThat(AssetType.PIN.toInner()).isEqualTo(org.matrix.rustcomponents.sdk.AssetType.PIN) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcherTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcherTest.kt index 2411b67c61..a65995fc86 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcherTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcherTest.kt @@ -9,13 +9,14 @@ package io.element.android.libraries.matrix.impl.room.member import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.roomMembers +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomMember +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoom +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomMembersIterator import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher.Source.CACHE import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher.Source.CACHE_AND_SERVER import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher.Source.SERVER -import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID_3 @@ -23,22 +24,16 @@ import io.element.android.libraries.matrix.test.A_USER_ID_4 import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runTest import org.junit.Test -import org.matrix.rustcomponents.sdk.MembershipState -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.Room -import org.matrix.rustcomponents.sdk.RoomMember -import org.matrix.rustcomponents.sdk.RoomMembersIterator -import uniffi.matrix_sdk.RoomMemberRole class RoomMemberListFetcherTest { @Test fun `fetchRoomMembers with CACHE source - emits cached members, if any`() = runTest { val room = FakeRustRoom(getMembersNoSync = { - FakeRoomMembersIterator( + FakeRustRoomMembersIterator( listOf( - fakeRustRoomMember(A_USER_ID), - fakeRustRoomMember(A_USER_ID_2), - fakeRustRoomMember(A_USER_ID_3), + aRustRoomMember(A_USER_ID), + aRustRoomMember(A_USER_ID_2), + aRustRoomMember(A_USER_ID_3), ) ) }) @@ -55,17 +50,13 @@ class RoomMemberListFetcherTest { val cachedItemsState = awaitItem() assertThat(cachedItemsState).isInstanceOf(MatrixRoomMembersState.Ready::class.java) assertThat((cachedItemsState as? MatrixRoomMembersState.Ready)?.roomMembers).hasSize(3) - - // Assert only the 'no sync' method was called, so no new member sync happened - assertThat(room.membersNoSyncCallCount).isEqualTo(1) - assertThat(room.membersCallCount).isEqualTo(0) } } @Test fun `fetchRoomMembers with CACHE source - emits empty list, if no members exist`() = runTest { val room = FakeRustRoom(getMembersNoSync = { - FakeRoomMembersIterator(emptyList()) + FakeRustRoomMembersIterator(emptyList()) }) val fetcher = RoomMemberListFetcher(room, Dispatchers.Default) @@ -95,11 +86,11 @@ class RoomMemberListFetcherTest { @Test fun `fetchRoomMembers with CACHE source - emits all items at once`() = runTest { val room = FakeRustRoom(getMembersNoSync = { - FakeRoomMembersIterator( + FakeRustRoomMembersIterator( listOf( - fakeRustRoomMember(A_USER_ID), - fakeRustRoomMember(A_USER_ID_2), - fakeRustRoomMember(A_USER_ID_3), + aRustRoomMember(A_USER_ID), + aRustRoomMember(A_USER_ID_2), + aRustRoomMember(A_USER_ID_3), ) ) }) @@ -122,11 +113,11 @@ class RoomMemberListFetcherTest { @Test fun `fetchRoomMembers with SERVER source - emits only new members, if any`() = runTest { val room = FakeRustRoom(getMembers = { - FakeRoomMembersIterator( + FakeRustRoomMembersIterator( listOf( - fakeRustRoomMember(A_USER_ID), - fakeRustRoomMember(A_USER_ID_2), - fakeRustRoomMember(A_USER_ID_3), + aRustRoomMember(A_USER_ID), + aRustRoomMember(A_USER_ID_2), + aRustRoomMember(A_USER_ID_3), ) ) }) @@ -138,10 +129,6 @@ class RoomMemberListFetcherTest { assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java) assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java) assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(3) - - // Assert only the 'sync' method was called, so a new member sync happened - assertThat(room.membersNoSyncCallCount).isEqualTo(0) - assertThat(room.membersCallCount).isEqualTo(1) } } @@ -163,14 +150,14 @@ class RoomMemberListFetcherTest { fun `fetchRoomMembers with CACHE_AND_SERVER source - returns cached items first, then new ones`() = runTest { val room = FakeRustRoom( getMembersNoSync = { - FakeRoomMembersIterator(listOf(fakeRustRoomMember(A_USER_ID_4))) + FakeRustRoomMembersIterator(listOf(aRustRoomMember(A_USER_ID_4))) }, getMembers = { - FakeRoomMembersIterator( + FakeRustRoomMembersIterator( listOf( - fakeRustRoomMember(A_USER_ID), - fakeRustRoomMember(A_USER_ID_2), - fakeRustRoomMember(A_USER_ID_3), + aRustRoomMember(A_USER_ID), + aRustRoomMember(A_USER_ID_2), + aRustRoomMember(A_USER_ID_3), ) ) } @@ -196,76 +183,6 @@ class RoomMemberListFetcherTest { assertThat(ready).isInstanceOf(MatrixRoomMembersState.Ready::class.java) assertThat(ready.roomMembers()).hasSize(3) } - - // Assert both member methods were called, so both the cache was hit and a new member sync happened - assertThat(room.membersNoSyncCallCount).isEqualTo(1) - assertThat(room.membersCallCount).isEqualTo(1) } } } - -class FakeRustRoom( - private val getMembers: () -> RoomMembersIterator = { FakeRoomMembersIterator() }, - private val getMembersNoSync: () -> RoomMembersIterator = { FakeRoomMembersIterator() }, -) : Room(NoPointer) { - var membersCallCount = 0 - var membersNoSyncCallCount = 0 - - override fun id(): String { - return A_ROOM_ID.value - } - - override suspend fun members(): RoomMembersIterator { - membersCallCount++ - return getMembers() - } - - override suspend fun membersNoSync(): RoomMembersIterator { - membersNoSyncCallCount++ - return getMembersNoSync() - } - - override fun close() { - // No-op - } -} - -class FakeRoomMembersIterator( - private var members: List? = null -) : RoomMembersIterator(NoPointer) { - override fun len(): UInt { - return members?.size?.toUInt() ?: 0u - } - - override fun nextChunk(chunkSize: UInt): List? { - if (members?.isEmpty() == true) { - return null - } - return members?.let { - val result = it.take(chunkSize.toInt()) - members = it.subList(result.size, it.size) - result - } - } -} - -private fun fakeRustRoomMember( - userId: UserId, - displayName: String? = null, - avatarUrl: String? = null, - membership: MembershipState = MembershipState.JOIN, - isNameAmbiguous: Boolean = false, - powerLevel: Long = 0L, - isIgnored: Boolean = false, - role: RoomMemberRole = RoomMemberRole.USER, -) = RoomMember( - userId = userId.value, - displayName = displayName, - avatarUrl = avatarUrl, - membership = membership, - isNameAmbiguous = isNameAmbiguous, - powerLevel = powerLevel, - normalizedPowerLevel = powerLevel, - isIgnored = isIgnored, - suggestedRoleForPowerLevel = role, -) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberMapperTest.kt new file mode 100644 index 0000000000..dcf3ed5ee1 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberMapperTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.room.member + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import org.junit.Test +import uniffi.matrix_sdk.RoomMemberRole +import org.matrix.rustcomponents.sdk.MembershipState as RustMembershipState + +class RoomMemberMapperTest { + @Test + fun mapRole() { + assertThat(RoomMemberMapper.mapRole(RoomMemberRole.USER)).isEqualTo(RoomMember.Role.USER) + assertThat(RoomMemberMapper.mapRole(RoomMemberRole.MODERATOR)).isEqualTo(RoomMember.Role.MODERATOR) + assertThat(RoomMemberMapper.mapRole(RoomMemberRole.ADMINISTRATOR)).isEqualTo(RoomMember.Role.ADMIN) + } + + @Test + fun mapMembership() { + assertThat(RoomMemberMapper.mapMembership(RustMembershipState.BAN)).isEqualTo(RoomMembershipState.BAN) + assertThat(RoomMemberMapper.mapMembership(RustMembershipState.INVITE)).isEqualTo(RoomMembershipState.INVITE) + assertThat(RoomMemberMapper.mapMembership(RustMembershipState.JOIN)).isEqualTo(RoomMembershipState.JOIN) + assertThat(RoomMemberMapper.mapMembership(RustMembershipState.KNOCK)).isEqualTo(RoomMembershipState.KNOCK) + assertThat(RoomMemberMapper.mapMembership(RustMembershipState.LEAVE)).isEqualTo(RoomMembershipState.LEAVE) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsMapperTest.kt new file mode 100644 index 0000000000..20cbdc5deb --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsMapperTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.room.powerlevels + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomPowerLevels +import org.junit.Test + +class RoomPowerLevelsMapperTest { + @Test + fun `test that mapping of RoomPowerLevels is correct`() { + assertThat( + RoomPowerLevelsMapper.map( + aRustRoomPowerLevels( + ban = 1, + invite = 2, + kick = 3, + redact = 4, + eventsDefault = 5, + stateDefault = 6, + usersDefault = 7, + roomName = 8, + roomAvatar = 9, + roomTopic = 10, + ) + ) + ).isEqualTo( + MatrixRoomPowerLevels( + ban = 1, + invite = 2, + kick = 3, + sendEvents = 5, + redactEvents = 4, + roomName = 8, + roomAvatar = 9, + roomTopic = 10, + ) + ) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewMapperTest.kt new file mode 100644 index 0000000000..7d7890198e --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewMapperTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.room.preview + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.room.RoomType +import io.element.android.libraries.matrix.api.room.preview.RoomPreview +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomPreview +import io.element.android.libraries.matrix.test.A_ROOM_ALIAS +import io.element.android.libraries.matrix.test.A_ROOM_ID +import org.junit.Test + +class RoomPreviewMapperTest { + @Test + fun `map should map values 1`() { + assertThat( + RoomPreviewMapper.map( + aRustRoomPreview( + isJoined = false, + isInvited = false, + ) + ) + ).isEqualTo( + RoomPreview( + roomId = A_ROOM_ID, + canonicalAlias = A_ROOM_ALIAS, + name = "name", + topic = "topic", + avatarUrl = "avatarUrl", + numberOfJoinedMembers = 1L, + roomType = RoomType.Room, + isHistoryWorldReadable = true, + isJoined = false, + isInvited = false, + isPublic = true, + canKnock = true, + ) + ) + } + + @Test + fun `map should map values 2`() { + assertThat( + RoomPreviewMapper.map( + aRustRoomPreview( + canonicalAlias = null, + isPublic = false, + canKnock = false, + ) + ) + ).isEqualTo( + RoomPreview( + roomId = A_ROOM_ID, + canonicalAlias = null, + name = "name", + topic = "topic", + avatarUrl = "avatarUrl", + numberOfJoinedMembers = 1L, + roomType = RoomType.Room, + isHistoryWorldReadable = true, + isJoined = true, + isInvited = true, + isPublic = false, + canKnock = false, + ) + ) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapperTest.kt new file mode 100644 index 0000000000..808b15dd1f --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapperTest.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.roomdirectory + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomDescription +import io.element.android.libraries.matrix.test.A_ROOM_ALIAS +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.roomdirectory.aRoomDescription +import org.junit.Test +import org.matrix.rustcomponents.sdk.PublicRoomJoinRule + +class RoomDescriptionMapperTest { + @Test + fun map() { + assertThat(RoomDescriptionMapper().map(aRustRoomDescription())).isEqualTo( + aRoomDescription( + roomId = A_ROOM_ID, + name = "name", + topic = "topic", + alias = A_ROOM_ALIAS, + avatarUrl = "avatarUrl", + joinRule = RoomDescription.JoinRule.PUBLIC, + isWorldReadable = true, + joinedMembers = 2L + ) + ) + } + + @Test + fun mapWithNullAlias() { + assertThat(RoomDescriptionMapper().map(aRustRoomDescription().copy(alias = null)).alias).isNull() + } + + @Test + fun `map join rule`() { + assertThat(PublicRoomJoinRule.PUBLIC.map()).isEqualTo(RoomDescription.JoinRule.PUBLIC) + assertThat(PublicRoomJoinRule.KNOCK.map()).isEqualTo(RoomDescription.JoinRule.KNOCK) + assertThat(null.map()).isEqualTo(RoomDescription.JoinRule.UNKNOWN) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTest.kt index f4b7f8a1d0..d6ed69706c 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTest.kt @@ -9,11 +9,11 @@ package io.element.android.libraries.matrix.impl.roomlist import com.google.common.truth.Truth.assertThat import com.sun.jna.Pointer -import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.roomlist.RoomSummary +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListItem import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID_2 -import io.element.android.libraries.matrix.test.A_ROOM_NAME +import io.element.android.libraries.matrix.test.A_ROOM_ID_3 import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled import kotlinx.coroutines.flow.MutableStateFlow @@ -21,19 +21,12 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test -import org.matrix.rustcomponents.sdk.EventTimelineItem -import org.matrix.rustcomponents.sdk.Membership -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.RoomHero -import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomList import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListServiceInterface import org.matrix.rustcomponents.sdk.RoomListServiceStateListener import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicatorListener -import org.matrix.rustcomponents.sdk.RoomMember -import org.matrix.rustcomponents.sdk.RoomNotificationMode import org.matrix.rustcomponents.sdk.RoomSubscription import org.matrix.rustcomponents.sdk.TaskHandle @@ -47,7 +40,7 @@ class RoomSummaryListProcessorTest { summaries.value = listOf(aRoomSummary()) val processor = createProcessor() - val newEntry = FakeRoomListItem(A_ROOM_ID_2) + val newEntry = FakeRustRoomListItem(A_ROOM_ID_2) processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(newEntry, newEntry, newEntry)))) assertThat(summaries.value.count()).isEqualTo(4) @@ -58,7 +51,7 @@ class RoomSummaryListProcessorTest { fun `PushBack adds a new entry at the end of the list`() = runTest { summaries.value = listOf(aRoomSummaryFilled()) val processor = createProcessor() - processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(FakeRoomListItem(A_ROOM_ID_2)))) + processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(FakeRustRoomListItem(A_ROOM_ID_2)))) assertThat(summaries.value.count()).isEqualTo(2) assertThat(summaries.value.last().roomId).isEqualTo(A_ROOM_ID_2) @@ -68,7 +61,7 @@ class RoomSummaryListProcessorTest { fun `PushFront inserts a new entry at the start of the list`() = runTest { summaries.value = listOf(aRoomSummaryFilled()) val processor = createProcessor() - processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(FakeRoomListItem(A_ROOM_ID_2)))) + processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(FakeRustRoomListItem(A_ROOM_ID_2)))) assertThat(summaries.value.count()).isEqualTo(2) assertThat(summaries.value.first().roomId).isEqualTo(A_ROOM_ID_2) @@ -80,7 +73,7 @@ class RoomSummaryListProcessorTest { val processor = createProcessor() val index = 0 - processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), FakeRoomListItem(A_ROOM_ID_2)))) + processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), FakeRustRoomListItem(A_ROOM_ID_2)))) assertThat(summaries.value.count()).isEqualTo(1) assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_2) @@ -92,7 +85,7 @@ class RoomSummaryListProcessorTest { val processor = createProcessor() val index = 0 - processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), FakeRoomListItem(A_ROOM_ID_2)))) + processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), FakeRustRoomListItem(A_ROOM_ID_2)))) assertThat(summaries.value.count()).isEqualTo(2) assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_2) @@ -156,6 +149,18 @@ class RoomSummaryListProcessorTest { assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID) } + @Test + fun `Reset removes all entries and add the provided ones`() = runTest { + summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) + val processor = createProcessor() + val index = 0 + + processor.postUpdate(listOf(RoomListEntriesUpdate.Reset(listOf(FakeRustRoomListItem(A_ROOM_ID_3))))) + + assertThat(summaries.value.count()).isEqualTo(1) + assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_3) + } + private fun TestScope.createProcessor() = RoomSummaryListProcessor( summaries, fakeRoomListService, @@ -184,83 +189,3 @@ class RoomSummaryListProcessorTest { override fun subscribeToRooms(roomIds: List, settings: RoomSubscription?) = Unit } } - -private fun aRustRoomInfo( - id: String = A_ROOM_ID.value, - displayName: String = A_ROOM_NAME, - rawName: String = A_ROOM_NAME, - topic: String? = null, - avatarUrl: String? = null, - isDirect: Boolean = false, - isPublic: Boolean = false, - isSpace: Boolean = false, - isTombstoned: Boolean = false, - isFavourite: Boolean = false, - canonicalAlias: String? = null, - alternativeAliases: List = listOf(), - membership: Membership = Membership.JOINED, - inviter: RoomMember? = null, - heroes: List = listOf(), - activeMembersCount: ULong = 0uL, - invitedMembersCount: ULong = 0uL, - joinedMembersCount: ULong = 0uL, - userPowerLevels: Map = mapOf(), - highlightCount: ULong = 0uL, - notificationCount: ULong = 0uL, - userDefinedNotificationMode: RoomNotificationMode? = null, - hasRoomCall: Boolean = false, - activeRoomCallParticipants: List = listOf(), - isMarkedUnread: Boolean = false, - numUnreadMessages: ULong = 0uL, - numUnreadNotifications: ULong = 0uL, - numUnreadMentions: ULong = 0uL, - pinnedEventIds: List = listOf(), -) = RoomInfo( - id = id, - displayName = displayName, - rawName = rawName, - topic = topic, - avatarUrl = avatarUrl, - isDirect = isDirect, - isPublic = isPublic, - isSpace = isSpace, - isTombstoned = isTombstoned, - isFavourite = isFavourite, - canonicalAlias = canonicalAlias, - alternativeAliases = alternativeAliases, - membership = membership, - inviter = inviter, - heroes = heroes, - activeMembersCount = activeMembersCount, - invitedMembersCount = invitedMembersCount, - joinedMembersCount = joinedMembersCount, - userPowerLevels = userPowerLevels, - highlightCount = highlightCount, - notificationCount = notificationCount, - cachedUserDefinedNotificationMode = userDefinedNotificationMode, - hasRoomCall = hasRoomCall, - activeRoomCallParticipants = activeRoomCallParticipants, - isMarkedUnread = isMarkedUnread, - numUnreadMessages = numUnreadMessages, - numUnreadNotifications = numUnreadNotifications, - numUnreadMentions = numUnreadMentions, - pinnedEventIds = pinnedEventIds, -) - -class FakeRoomListItem( - private val roomId: RoomId, - private val roomInfo: RoomInfo = aRustRoomInfo(id = roomId.value), - private val latestEvent: EventTimelineItem? = null, -) : RoomListItem(NoPointer) { - override fun id(): String { - return roomId.value - } - - override suspend fun roomInfo(): RoomInfo { - return roomInfo - } - - override suspend fun latestEvent(): EventTimelineItem? { - return latestEvent - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/server/DefaultUserServerResolverTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/server/DefaultUserServerResolverTest.kt new file mode 100644 index 0000000000..b9778190ef --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/server/DefaultUserServerResolverTest.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.server + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.FakeMatrixClient +import org.junit.Test + +class DefaultUserServerResolverTest { + @Test + fun resolve() { + // Given + val userServerResolver = DefaultUserServerResolver(FakeMatrixClient( + userIdServerNameLambda = { "dummy.org" } + )) + + // When + val result = userServerResolver.resolve() + + // Then + assertThat(result).isEqualTo("dummy.org") + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/sync/AppStateMapperKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/sync/AppStateMapperKtTest.kt new file mode 100644 index 0000000000..54fc06f8f1 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/sync/AppStateMapperKtTest.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.sync + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.sync.SyncState +import org.junit.Test +import org.matrix.rustcomponents.sdk.SyncServiceState + +class AppStateMapperKtTest { + @Test + fun toSyncState() { + assertThat(SyncServiceState.IDLE.toSyncState()).isEqualTo(SyncState.Idle) + assertThat(SyncServiceState.RUNNING.toSyncState()).isEqualTo(SyncState.Running) + assertThat(SyncServiceState.TERMINATED.toSyncState()).isEqualTo(SyncState.Terminated) + assertThat(SyncServiceState.ERROR.toSyncState()).isEqualTo(SyncState.Error) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt new file mode 100644 index 0000000000..d442d004d6 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt @@ -0,0 +1,171 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.timeline + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineDiff +import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper +import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper +import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper +import io.element.android.libraries.matrix.test.A_UNIQUE_ID +import io.element.android.libraries.matrix.test.A_UNIQUE_ID_2 +import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.rustcomponents.sdk.TimelineChange + +class MatrixTimelineDiffProcessorTest { + private val timelineItems = MutableStateFlow>(emptyList()) + + private val anEvent = MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem()) + private val anEvent2 = MatrixTimelineItem.Event(A_UNIQUE_ID_2, anEventTimelineItem()) + + @Test + fun `Append adds new entries at the end of the list`() = runTest { + timelineItems.value = listOf(anEvent) + val processor = createProcessor() + processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.APPEND))) + assertThat(timelineItems.value.count()).isEqualTo(2) + assertThat(timelineItems.value).containsExactly( + anEvent, + MatrixTimelineItem.Other, + ) + } + + @Test + fun `PushBack adds a new entry at the end of the list`() = runTest { + timelineItems.value = listOf(anEvent) + val processor = createProcessor() + processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.PUSH_BACK))) + assertThat(timelineItems.value.count()).isEqualTo(2) + assertThat(timelineItems.value).containsExactly( + anEvent, + MatrixTimelineItem.Other, + ) + } + + @Test + fun `PushFront inserts a new entry at the start of the list`() = runTest { + timelineItems.value = listOf(anEvent) + val processor = createProcessor() + processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.PUSH_FRONT))) + assertThat(timelineItems.value.count()).isEqualTo(2) + assertThat(timelineItems.value).containsExactly( + MatrixTimelineItem.Other, + anEvent, + ) + } + + @Test + fun `Set replaces an entry at some index`() = runTest { + timelineItems.value = listOf(anEvent, anEvent2) + val processor = createProcessor() + processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.SET))) + assertThat(timelineItems.value.count()).isEqualTo(2) + assertThat(timelineItems.value).containsExactly( + anEvent, + MatrixTimelineItem.Other + ) + } + + @Test + fun `Insert inserts a new entry at the provided index`() = runTest { + timelineItems.value = listOf(anEvent, anEvent2) + val processor = createProcessor() + processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.INSERT))) + assertThat(timelineItems.value.count()).isEqualTo(3) + assertThat(timelineItems.value).containsExactly( + anEvent, + MatrixTimelineItem.Other, + anEvent2, + ) + } + + @Test + fun `Remove removes an entry at some index`() = runTest { + timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2) + val processor = createProcessor() + processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.REMOVE))) + assertThat(timelineItems.value.count()).isEqualTo(2) + assertThat(timelineItems.value).containsExactly( + anEvent, + anEvent2, + ) + } + + @Test + fun `PopBack removes an entry at the end of the list`() = runTest { + timelineItems.value = listOf(anEvent, anEvent2) + val processor = createProcessor() + processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.POP_BACK))) + assertThat(timelineItems.value.count()).isEqualTo(1) + assertThat(timelineItems.value).containsExactly( + anEvent, + ) + } + + @Test + fun `PopFront removes an entry at the start of the list`() = runTest { + timelineItems.value = listOf(anEvent, anEvent2) + val processor = createProcessor() + processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.POP_FRONT))) + assertThat(timelineItems.value.count()).isEqualTo(1) + assertThat(timelineItems.value).containsExactly( + anEvent2, + ) + } + + @Test + fun `Clear removes all the entries`() = runTest { + timelineItems.value = listOf(anEvent, anEvent2) + val processor = createProcessor() + processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.CLEAR))) + assertThat(timelineItems.value).isEmpty() + } + + @Test + fun `Truncate removes all entries after the provided length`() = runTest { + timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2) + val processor = createProcessor() + processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.TRUNCATE))) + assertThat(timelineItems.value.count()).isEqualTo(1) + assertThat(timelineItems.value).containsExactly( + anEvent, + ) + } + + @Test + fun `Reset removes all entries and add the provided ones`() = runTest { + timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2) + val processor = createProcessor() + processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.RESET))) + assertThat(timelineItems.value.count()).isEqualTo(1) + assertThat(timelineItems.value).containsExactly( + MatrixTimelineItem.Other, + ) + } + + private fun TestScope.createProcessor(): MatrixTimelineDiffProcessor { + val timelineEventContentMapper = TimelineEventContentMapper() + val timelineItemMapper = MatrixTimelineItemMapper( + fetchDetailsForEvent = { _ -> Result.success(Unit) }, + coroutineScope = this, + virtualTimelineItemMapper = VirtualTimelineItemMapper(), + eventTimelineItemMapper = EventTimelineItemMapper( + contentMapper = timelineEventContentMapper + ) + ) + return MatrixTimelineDiffProcessor( + timelineItems, + timelineItemFactory = timelineItemMapper, + ) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/ReceiptTypeMapperKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/ReceiptTypeMapperKtTest.kt new file mode 100644 index 0000000000..5ba93d26ce --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/ReceiptTypeMapperKtTest.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ +package io.element.android.libraries.matrix.impl.timeline + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.timeline.ReceiptType +import org.junit.Test +import org.matrix.rustcomponents.sdk.ReceiptType as RustReceiptType + +class ReceiptTypeMapperKtTest { + @Test + fun toRustReceiptType() { + assertThat(ReceiptType.READ.toRustReceiptType()).isEqualTo(RustReceiptType.READ) + assertThat(ReceiptType.READ_PRIVATE.toRustReceiptType()).isEqualTo(RustReceiptType.READ_PRIVATE) + assertThat(ReceiptType.FULLY_READ.toRustReceiptType()).isEqualTo(RustReceiptType.FULLY_READ) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/Fixtures.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/Fixtures.kt new file mode 100644 index 0000000000..cc295d2517 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/Fixtures.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.timeline.postprocessor + +import io.element.android.libraries.matrix.api.core.UniqueId +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange +import io.element.android.libraries.matrix.api.timeline.item.event.OtherState +import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent +import io.element.android.libraries.matrix.api.timeline.item.event.StateContent +import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.timeline.aMessageContent +import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem + +internal val roomCreateEvent = MatrixTimelineItem.Event( + uniqueId = UniqueId("m.room.create"), + event = anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate)) +) +internal val roomCreatorJoinEvent = MatrixTimelineItem.Event( + uniqueId = UniqueId("m.room.member"), + event = anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED)) +) +internal val otherMemberJoinEvent = MatrixTimelineItem.Event( + uniqueId = UniqueId("m.room.member_other"), + event = anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED)) +) +internal val messageEvent = MatrixTimelineItem.Event( + uniqueId = UniqueId("m.room.message"), + event = anEventTimelineItem(content = aMessageContent("hi")) +) +internal val messageEvent2 = MatrixTimelineItem.Event( + uniqueId = UniqueId("m.room.message2"), + event = anEventTimelineItem(content = aMessageContent("hello")) +) +internal val dayEvent = MatrixTimelineItem.Virtual( + uniqueId = UniqueId("day"), + virtual = VirtualTimelineItem.DayDivider(0), +) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessorTest.kt new file mode 100644 index 0000000000..398b2f5883 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LastForwardIndicatorsPostProcessorTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.timeline.postprocessor + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.UniqueId +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem +import org.junit.Test + +class LastForwardIndicatorsPostProcessorTest { + @Test + fun `LastForwardIndicatorsPostProcessor does not alter the items with mode not FOCUSED_ON_EVENT`() { + val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.LIVE) + val result = sut.process(listOf(messageEvent), true) + assertThat(result).containsExactly(messageEvent) + } + + @Test + fun `LastForwardIndicatorsPostProcessor does not alter the items with mode FOCUSED_ON_EVENT but timeline not initialized`() { + val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT) + val result = sut.process(listOf(messageEvent), false) + assertThat(result).containsExactly(messageEvent) + } + + @Test + fun `LastForwardIndicatorsPostProcessor add virtual items`() { + val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT) + val result = sut.process(listOf(messageEvent), true) + assertThat(result).containsExactly( + messageEvent, + MatrixTimelineItem.Virtual( + uniqueId = UniqueId("last_forward_indicator_${messageEvent.uniqueId}"), + virtual = VirtualTimelineItem.LastForwardIndicator + ) + ) + } + + @Test + fun `LastForwardIndicatorsPostProcessor add virtual items on empty list`() { + val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT) + val result = sut.process(listOf(), true) + assertThat(result).containsExactly( + MatrixTimelineItem.Virtual( + uniqueId = UniqueId("last_forward_indicator_fake_id"), + virtual = VirtualTimelineItem.LastForwardIndicator + ) + ) + } + + @Test + fun `LastForwardIndicatorsPostProcessor add virtual items but does not alter the list if called a second time`() { + val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT) + // Process a first time + sut.process(listOf(messageEvent), true) + // Process a second time with the same Event + val result = sut.process(listOf(messageEvent), true) + assertThat(result).containsExactly( + messageEvent, + MatrixTimelineItem.Virtual( + uniqueId = UniqueId("last_forward_indicator_${messageEvent.uniqueId}"), + virtual = VirtualTimelineItem.LastForwardIndicator + ) + ) + } + + @Test + fun `LastForwardIndicatorsPostProcessor add virtual items each time it is called with new Events`() { + val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT) + // Process a first time + sut.process(listOf(dayEvent, messageEvent), true) + // Process a second time with the same Event + val result = sut.process(listOf(dayEvent, messageEvent, messageEvent2), true) + assertThat(result).containsExactly( + dayEvent, + messageEvent, + MatrixTimelineItem.Virtual( + uniqueId = UniqueId("last_forward_indicator_${messageEvent.uniqueId}"), + virtual = VirtualTimelineItem.LastForwardIndicator + ), + messageEvent2, + MatrixTimelineItem.Virtual( + uniqueId = UniqueId("last_forward_indicator_${messageEvent2.uniqueId}"), + virtual = VirtualTimelineItem.LastForwardIndicator + ) + ) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessorTest.kt new file mode 100644 index 0000000000..4859355917 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessorTest.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.timeline.postprocessor + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.UniqueId +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem +import io.element.android.services.toolbox.test.systemclock.FakeSystemClock +import org.junit.Test + +class LoadingIndicatorsPostProcessorTest { + @Test + fun `LoadingIndicatorsPostProcessor does not alter the items is the timeline is not initialized`() { + val sut = LoadingIndicatorsPostProcessor(FakeSystemClock()) + val result = sut.process( + items = listOf(messageEvent, messageEvent2), + isTimelineInitialized = false, + hasMoreToLoadBackward = true, + hasMoreToLoadForward = true, + ) + assertThat(result).containsExactly(messageEvent, messageEvent2) + } + + @Test + fun `LoadingIndicatorsPostProcessor adds Loading indicator at the top of the list if hasMoreToLoadBackward is true`() { + val clock = FakeSystemClock() + val sut = LoadingIndicatorsPostProcessor(clock) + val result = sut.process( + items = listOf(messageEvent, messageEvent2), + isTimelineInitialized = true, + hasMoreToLoadBackward = true, + hasMoreToLoadForward = false, + ) + assertThat(result).containsExactly( + MatrixTimelineItem.Virtual( + uniqueId = UniqueId("BackwardLoadingIndicator"), + virtual = VirtualTimelineItem.LoadingIndicator( + direction = Timeline.PaginationDirection.BACKWARDS, + timestamp = clock.epochMillis() + ) + ), + messageEvent, + messageEvent2, + ) + } + + @Test + fun `LoadingIndicatorsPostProcessor adds Loading indicator at the bottom of the list if hasMoreToLoadForward is true`() { + val clock = FakeSystemClock() + val sut = LoadingIndicatorsPostProcessor(clock) + val result = sut.process( + items = listOf(messageEvent, messageEvent2), + isTimelineInitialized = true, + hasMoreToLoadBackward = false, + hasMoreToLoadForward = true, + ) + assertThat(result).containsExactly( + messageEvent, + messageEvent2, + MatrixTimelineItem.Virtual( + uniqueId = UniqueId("ForwardLoadingIndicator"), + virtual = VirtualTimelineItem.LoadingIndicator( + direction = Timeline.PaginationDirection.FORWARDS, + timestamp = clock.epochMillis() + ) + ), + ) + } + + @Test + fun `LoadingIndicatorsPostProcessor adds Loading indicator at the bottom and at the top of the list`() { + val clock = FakeSystemClock() + val sut = LoadingIndicatorsPostProcessor(clock) + val result = sut.process( + items = listOf(messageEvent, messageEvent2), + isTimelineInitialized = true, + hasMoreToLoadBackward = true, + hasMoreToLoadForward = true, + ) + assertThat(result).containsExactly( + MatrixTimelineItem.Virtual( + uniqueId = UniqueId("BackwardLoadingIndicator"), + virtual = VirtualTimelineItem.LoadingIndicator( + direction = Timeline.PaginationDirection.BACKWARDS, + timestamp = clock.epochMillis() + ) + ), + messageEvent, + messageEvent2, + MatrixTimelineItem.Virtual( + uniqueId = UniqueId("ForwardLoadingIndicator"), + virtual = VirtualTimelineItem.LoadingIndicator( + direction = Timeline.PaginationDirection.FORWARDS, + timestamp = clock.epochMillis() + ) + ), + ) + } + + @Test + fun `LoadingIndicatorsPostProcessor only adds 1 Loading indicator if there is no items in the list`() { + val clock = FakeSystemClock() + val sut = LoadingIndicatorsPostProcessor(clock) + val result = sut.process( + items = listOf(), + isTimelineInitialized = true, + hasMoreToLoadBackward = true, + hasMoreToLoadForward = true, + ) + assertThat(result).containsExactly( + MatrixTimelineItem.Virtual( + uniqueId = UniqueId("BackwardLoadingIndicator"), + virtual = VirtualTimelineItem.LoadingIndicator( + direction = Timeline.PaginationDirection.BACKWARDS, + timestamp = clock.epochMillis() + ) + ), + ) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt index db93b86142..f7eadd0d47 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt @@ -8,99 +8,162 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.core.UniqueId -import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.Timeline -import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange -import io.element.android.libraries.matrix.api.timeline.item.event.OtherState -import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent -import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.matrix.test.A_USER_ID_2 -import io.element.android.libraries.matrix.test.timeline.aMessageContent -import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import org.junit.Test class RoomBeginningPostProcessorTest { + @Test + fun `processor returns empty list when empty list is provided`() { + val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) + val processedItems = processor.process( + items = emptyList(), + isDm = true, + roomCreator = A_USER_ID, + hasMoreToLoadBackwards = false, + ) + assertThat(processedItems).isEmpty() + } + + @Test + fun `processor returns the provided list when it only contains a message`() { + val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) + val processedItems = processor.process( + items = listOf(messageEvent), + isDm = true, + roomCreator = A_USER_ID, + hasMoreToLoadBackwards = false, + ) + assertThat(processedItems).isEqualTo(listOf(messageEvent)) + } + + @Test + fun `processor returns the provided list when it only contains a message and the roomCreator is not provided`() { + val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) + val processedItems = processor.process( + items = listOf(messageEvent), + isDm = true, + roomCreator = null, + hasMoreToLoadBackwards = false, + ) + assertThat(processedItems).isEqualTo(listOf(messageEvent)) + } + @Test fun `processor removes room creation event and self-join event from DM timeline`() { val timelineItems = listOf( - MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), - MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))), + roomCreateEvent, + roomCreatorJoinEvent, ) val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) - val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false) + val processedItems = processor.process( + items = timelineItems, + isDm = true, + roomCreator = A_USER_ID, + hasMoreToLoadBackwards = false, + ) assertThat(processedItems).isEmpty() } + @Test + fun `processor does not remove anything with PINNED_EVENTS mode`() { + val timelineItems = listOf( + roomCreateEvent, + roomCreatorJoinEvent, + ) + val processor = RoomBeginningPostProcessor(Timeline.Mode.PINNED_EVENTS) + val processedItems = processor.process( + items = timelineItems, + isDm = true, + roomCreator = A_USER_ID, + hasMoreToLoadBackwards = false, + ) + assertThat(processedItems).isEqualTo(timelineItems) + } + @Test fun `processor removes room creation event and self-join event from DM timeline even if they're not the first items`() { val timelineItems = listOf( - MatrixTimelineItem.Event( - UniqueId("m.room.member_other"), - anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED)) - ), - MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), - MatrixTimelineItem.Event(UniqueId("m.room.message"), anEventTimelineItem(content = aMessageContent("hi"))), - MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))), + otherMemberJoinEvent, + roomCreateEvent, + messageEvent, + roomCreatorJoinEvent, ) val expected = listOf( - MatrixTimelineItem.Event( - UniqueId("m.room.member_other"), - anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED)) - ), - MatrixTimelineItem.Event(UniqueId("m.room.message"), anEventTimelineItem(content = aMessageContent("hi"))), + otherMemberJoinEvent, + messageEvent, ) val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) - val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false) + val processedItems = processor.process(timelineItems, isDm = true, roomCreator = A_USER_ID, hasMoreToLoadBackwards = false) assertThat(processedItems).isEqualTo(expected) } @Test fun `processor will add beginning of room item if it's not a DM`() { val timelineItems = listOf( - MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), - MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))), + roomCreateEvent, + roomCreatorJoinEvent, ) val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) - val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false) + val processedItems = processor.process(timelineItems, isDm = false, roomCreator = A_USER_ID, hasMoreToLoadBackwards = false) assertThat(processedItems).isEqualTo( listOf(processor.createRoomBeginningItem()) + timelineItems ) } @Test - fun `processor won't remove items if it's not at the start of the timeline`() { + fun `processor will not add beginning of room item if it's not a DM but the room has more to load`() { val timelineItems = listOf( - MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), - MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))), + roomCreateEvent, + roomCreatorJoinEvent, ) val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) - val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) + val processedItems = processor.process(timelineItems, isDm = false, roomCreator = A_USER_ID, hasMoreToLoadBackwards = true) assertThat(processedItems).isEqualTo(timelineItems) } @Test - fun `processor won't remove the first member join event if it can't find the room creation event`() { + fun `processor will add beginning of room item if it's not a DM, when the parameter roomCreator is null`() { val timelineItems = listOf( - MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))), + roomCreateEvent, + roomCreatorJoinEvent, ) val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) - val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) - assertThat(processedItems).isEqualTo(timelineItems) + val processedItems = processor.process(timelineItems, isDm = false, roomCreator = null, hasMoreToLoadBackwards = false) + assertThat(processedItems).isEqualTo( + listOf(processor.createRoomBeginningItem()) + timelineItems + ) + } + + @Test + fun `processor removes items event it's not at the start of the timeline`() { + val timelineItems = listOf( + roomCreateEvent, + roomCreatorJoinEvent, + ) + val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) + val processedItems = processor.process(timelineItems, isDm = true, roomCreator = A_USER_ID, hasMoreToLoadBackwards = true) + assertThat(processedItems).isEmpty() + } + + @Test + fun `processor removes the first member join event if it matches the roomCreator parameter`() { + val timelineItems = listOf( + roomCreatorJoinEvent, + ) + val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) + val processedItems = processor.process(timelineItems, isDm = true, roomCreator = A_USER_ID, hasMoreToLoadBackwards = true) + assertThat(processedItems).isEmpty() } @Test fun `processor won't remove the first member join event if it's not from the room creator`() { val timelineItems = listOf( - MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), - MatrixTimelineItem.Event( - UniqueId("m.room.member"), - anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED)) - ), + roomCreateEvent, + otherMemberJoinEvent, ) val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) - val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) - assertThat(processedItems).isEqualTo(timelineItems) + val processedItems = processor.process(timelineItems, isDm = true, roomCreator = A_USER_ID, hasMoreToLoadBackwards = true) + assertThat(processedItems).isEqualTo(listOf(otherMemberJoinEvent)) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserProfileMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserProfileMapperTest.kt new file mode 100644 index 0000000000..f8bb7a0e0e --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserProfileMapperTest.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.usersearch + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustUserProfile +import io.element.android.libraries.matrix.test.A_USER_ID +import org.junit.Test + +class UserProfileMapperTest { + @Test + fun map() { + assertThat(UserProfileMapper.map(aRustUserProfile(A_USER_ID.value, "displayName", "avatarUrl"))) + .isEqualTo(MatrixUser(A_USER_ID, "displayName", "avatarUrl")) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserSearchResultMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserSearchResultMapperTest.kt new file mode 100644 index 0000000000..068515e255 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserSearchResultMapperTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.usersearch + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSearchUsersResults +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustUserProfile +import io.element.android.libraries.matrix.test.A_USER_ID +import kotlinx.collections.immutable.toImmutableList +import org.junit.Test + +class UserSearchResultMapperTest { + @Test + fun `map limited list`() { + assertThat( + UserSearchResultMapper.map( + aRustSearchUsersResults( + results = listOf(aRustUserProfile(A_USER_ID.value, "displayName", "avatarUrl")), + limited = true, + ) + ) + ) + .isEqualTo( + MatrixSearchUserResults( + results = listOf(MatrixUser(A_USER_ID, "displayName", "avatarUrl")).toImmutableList(), + limited = true, + ) + ) + } + + @Test + fun `map not limited list`() { + assertThat( + UserSearchResultMapper.map( + aRustSearchUsersResults( + results = listOf(aRustUserProfile(A_USER_ID.value, "displayName", "avatarUrl")), + limited = false, + ) + ) + ) + .isEqualTo( + MatrixSearchUserResults( + results = listOf(MatrixUser(A_USER_ID, "displayName", "avatarUrl")).toImmutableList(), + limited = false, + ) + ) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/util/SessionPathsProviderTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/util/SessionPathsProviderTest.kt new file mode 100644 index 0000000000..f530fc04cc --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/util/SessionPathsProviderTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.util + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.aSessionData +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class SessionPathsProviderTest { + @Test + fun `if session is not found, provides returns null`() = runTest { + val sut = SessionPathsProvider(InMemorySessionStore()) + val result = sut.provides(A_SESSION_ID) + assertThat(result).isNull() + } + + @Test + fun `if session is found, provides returns the data`() = runTest { + val store = InMemorySessionStore() + val sut = SessionPathsProvider(store) + store.storeData( + aSessionData( + sessionPath = "/a/path/to/a/session", + cachePath = "/a/path/to/a/cache", + ) + ) + val result = sut.provides(A_SESSION_ID)!! + assertThat(result.fileDirectory.absolutePath).isEqualTo("/a/path/to/a/session") + assertThat(result.cacheDirectory.absolutePath).isEqualTo("/a/path/to/a/cache") + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index a6cdcbe23e..b88637775e 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.pusher.PushersService +import io.element.android.libraries.matrix.api.room.InvitedRoom import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @@ -78,7 +79,10 @@ class FakeMatrixClient( private val clearCacheLambda: () -> Unit = { lambdaError() }, private val userIdServerNameLambda: () -> String = { lambdaError() }, private val getUrlLambda: (String) -> Result = { lambdaError() }, + private val canDeactivateAccountResult: () -> Boolean = { lambdaError() }, + private val deactivateAccountResult: (String, Boolean) -> Result = { _, _ -> lambdaError() }, var isNativeSlidingSyncSupportedLambda: suspend () -> Boolean = { true }, + var isSlidingSyncProxySupportedLambda: suspend () -> Boolean = { true }, var isUsingNativeSlidingSyncLambda: () -> Boolean = { true }, ) : MatrixClient { var setDisplayNameCalled: Boolean = false @@ -98,6 +102,7 @@ class FakeMatrixClient( private var createDmResult: Result = Result.success(A_ROOM_ID) private var findDmResult: RoomId? = A_ROOM_ID private val getRoomResults = mutableMapOf() + val getInvitedRoomResults = mutableMapOf() private val searchUserResults = mutableMapOf>() private val getProfileResults = mutableMapOf>() private var uploadMediaResult: Result = Result.success(AN_AVATAR_URL) @@ -130,6 +135,10 @@ class FakeMatrixClient( override suspend fun setAccountData(eventType: String, content: String) {} // SC additions end + override suspend fun getInvitedRoom(roomId: RoomId): InvitedRoom? { + return getInvitedRoomResults[roomId] + } + override suspend fun findDM(userId: UserId): RoomId? { return findDmResult } @@ -174,6 +183,12 @@ class FakeMatrixClient( return logoutLambda(ignoreSdkError, userInitiated) } + override fun canDeactivateAccount() = canDeactivateAccountResult() + + override suspend fun deactivateAccount(password: String, eraseData: Boolean): Result = simulateLongTask { + deactivateAccountResult(password, eraseData) + } + override fun close() = Unit override suspend fun getUserProfile(): Result = simulateLongTask { @@ -330,6 +345,10 @@ class FakeMatrixClient( return isNativeSlidingSyncSupportedLambda() } + override suspend fun isSlidingSyncProxySupported(): Boolean { + return isSlidingSyncProxySupportedLambda() + } + override fun isUsingNativeSlidingSync(): Boolean { return isUsingNativeSlidingSyncLambda() } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt index 6f5ffa3928..8c18629817 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt @@ -11,12 +11,14 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.matrix.api.auth.external.ExternalSession import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.sessionstorage.api.LoggedInState +import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.flow.Flow @@ -30,6 +32,7 @@ class FakeMatrixAuthenticationService( var matrixClientResult: ((SessionId) -> Result)? = null, var loginWithQrCodeResult: (qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) -> Result = lambdaRecorder Unit, Result> { _, _ -> Result.success(A_SESSION_ID) }, + private val importCreatedSessionLambda: (ExternalSession) -> Result = { lambdaError() } ) : MatrixAuthenticationService { private val homeserver = MutableStateFlow(null) private var oidcError: Throwable? = null @@ -73,6 +76,10 @@ class FakeMatrixAuthenticationService( loginError?.let { Result.failure(it) } ?: Result.success(A_USER_ID) } + override suspend fun importCreatedSession(externalSession: ExternalSession): Result = simulateLongTask { + return importCreatedSessionLambda(externalSession) + } + override suspend fun getOidcUrl(): Result = simulateLongTask { oidcError?.let { Result.failure(it) } ?: Result.success(A_OIDC_DATA) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeInvitedRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeInvitedRoom.kt new file mode 100644 index 0000000000..3224d1fd2f --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeInvitedRoom.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.test.room + +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.room.InvitedRoom +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeInvitedRoom( + override val sessionId: SessionId = A_SESSION_ID, + override val roomId: RoomId = A_ROOM_ID, + private val declineInviteResult: () -> Result = { lambdaError() } +) : InvitedRoom { + override suspend fun declineInvite(): Result { + return declineInviteResult() + } + + override fun close() = Unit +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 8418d0c047..076baf5f2b 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.test.room +import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.RoomAlias @@ -79,7 +80,7 @@ class FakeMatrixRoom( private var roomPermalinkResult: () -> Result = { lambdaError() }, private var eventPermalinkResult: (EventId) -> Result = { lambdaError() }, private val sendCallNotificationIfNeededResult: () -> Result = { lambdaError() }, - private val userDisplayNameResult: () -> Result = { lambdaError() }, + private val userDisplayNameResult: (UserId) -> Result = { lambdaError() }, private val userAvatarUrlResult: () -> Result = { lambdaError() }, private val userRoleResult: () -> Result = { lambdaError() }, private val getUpdatedMemberResult: (UserId) -> Result = { lambdaError() }, @@ -134,7 +135,9 @@ class FakeMatrixRoom( private val loadComposerDraftLambda: () -> Result = { Result.success(null) }, private val clearComposerDraftLambda: () -> Result = { Result.success(Unit) }, private val subscribeToSyncLambda: () -> Unit = { lambdaError() }, -) : MatrixRoom { + private val ignoreDeviceTrustAndResendResult: (Map>, TransactionId) -> Result = { _, _ -> lambdaError() }, + private val withdrawVerificationAndResendResult: (List, TransactionId) -> Result = { _, _ -> lambdaError() }, + ) : MatrixRoom { private val _roomInfoFlow: MutableSharedFlow = MutableSharedFlow(replay = 1) override val roomInfoFlow: Flow = _roomInfoFlow @@ -199,7 +202,7 @@ class FakeMatrixRoom( override fun destroy() = Unit override suspend fun userDisplayName(userId: UserId): Result = simulateLongTask { - userDisplayNameResult() + userDisplayNameResult(userId) } override suspend fun userAvatarUrl(userId: UserId): Result = simulateLongTask { @@ -226,7 +229,7 @@ class FakeMatrixRoom( return toggleReactionResult(emoji, uniqueId) } - override suspend fun retrySendMessage(transactionId: TransactionId): Result { + override suspend fun retrySendMessage(transactionId: TransactionId): Result = simulateLongTask { return retrySendMessageResult(transactionId) } @@ -492,6 +495,14 @@ class FakeMatrixRoom( return getWidgetDriverResult(widgetSettings) } + override suspend fun ignoreDeviceTrustAndResend(devices: Map>, transactionId: TransactionId): Result = simulateLongTask { + return ignoreDeviceTrustAndResendResult(devices, transactionId) + } + + override suspend fun withdrawVerificationAndResend(userIds: List, transactionId: TransactionId): Result = simulateLongTask { + return withdrawVerificationAndResendResult(userIds, transactionId) + } + fun givenRoomMembersState(state: MatrixRoomMembersState) { membersStateFlow.value = state } @@ -509,7 +520,7 @@ fun aRoomInfo( isTombstoned: Boolean = false, isFavorite: Boolean = false, canonicalAlias: RoomAlias? = null, - alternativeAliases: List = emptyList(), + alternativeAliases: List = emptyList(), currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED, inviter: RoomMember? = null, activeMembersCount: Long = 1, @@ -520,9 +531,10 @@ fun aRoomInfo( userDefinedNotificationMode: RoomNotificationMode? = null, hasRoomCall: Boolean = false, userPowerLevels: ImmutableMap = persistentMapOf(), - activeRoomCallParticipants: List = emptyList(), + activeRoomCallParticipants: List = emptyList(), heroes: List = emptyList(), pinnedEventIds: List = emptyList(), + roomCreator: UserId? = null, ) = MatrixRoomInfo( id = id, name = name, @@ -549,6 +561,7 @@ fun aRoomInfo( activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(), heroes = heroes.toImmutableList(), pinnedEventIds = pinnedEventIds.toImmutableList(), + creator = roomCreator, ) fun defaultRoomPowerLevels() = MatrixRoomPowerLevels( diff --git a/libraries/permissions/impl/src/main/res/values-nl/translations.xml b/libraries/permissions/impl/src/main/res/values-nl/translations.xml new file mode 100644 index 0000000000..b23c075bae --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-nl/translations.xml @@ -0,0 +1,5 @@ + + + "Controleren of de applicatie meldingen kan weergeven." + "Controleer machtigingen" + diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt index 8e09547fb4..ab985e798d 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt @@ -87,7 +87,7 @@ class DefaultAppPreferencesStore @Inject constructor( override fun isSimplifiedSlidingSyncEnabledFlow(): Flow { return store.data.map { prefs -> - prefs[simplifiedSlidingSyncKey] ?: false + prefs[simplifiedSlidingSyncKey] ?: true } } diff --git a/libraries/push/impl/src/main/res/values-de/translations.xml b/libraries/push/impl/src/main/res/values-de/translations.xml index 35924e6247..fc5edbf1d9 100644 --- a/libraries/push/impl/src/main/res/values-de/translations.xml +++ b/libraries/push/impl/src/main/res/values-de/translations.xml @@ -34,6 +34,7 @@ "Schnelle Antwort" "Du wurdest eingeladen, den Raum zu betreten" "Ich" + "%1$s hat Dich erwähnt oder geantwortet" "Du siehst dir die Benachrichtigung an! Klicke hier!" "%1$s: %2$s" "%1$s: %2$s %3$s" diff --git a/libraries/push/impl/src/main/res/values-el/translations.xml b/libraries/push/impl/src/main/res/values-el/translations.xml index 081adb46b8..b008447c80 100644 --- a/libraries/push/impl/src/main/res/values-el/translations.xml +++ b/libraries/push/impl/src/main/res/values-el/translations.xml @@ -34,6 +34,7 @@ "Γρήγορη απάντηση" "Σέ προσκάλεσε να συμμετάσχεις στο δωμάτιο" "Εγώ" + "Ο χρήστης %1$s αναφέρθηκε ή απάντησε" "Βλέπεις την ειδοποίηση! Κάνε μου κλικ!" "%1$s: %2$s" "%1$s: %2$s %3$s" diff --git a/libraries/push/impl/src/main/res/values-nl/translations.xml b/libraries/push/impl/src/main/res/values-nl/translations.xml index d9ed0161ed..425973551a 100644 --- a/libraries/push/impl/src/main/res/values-nl/translations.xml +++ b/libraries/push/impl/src/main/res/values-nl/translations.xml @@ -49,4 +49,28 @@ "Achtergrondsynchronisatie" "Google-services" "Geen geldige Google Play-services gevonden. Meldingen werken mogelijk niet goed." + "Naam van de huidige provider aan het ophalen." + "Er zijn geen push-providers geselecteerd." + "Huidige push-provider: %1$s." + "Huidige push-provider" + "Zorg ervoor dat de applicatie minimaal één push-provider heeft." + "Geen push-providers gevonden." + + "%1$d push-provider gevonden: %2$s" + "%1$d push-providers gevonden: %2$s" + + "Push-providers detecteren" + "Controleer of de applicatie een melding kan weergeven." + "Er is niet op de melding geklikt." + "Kan de melding niet weergeven." + "Er is op de melding geklikt!" + "Melding weergeven" + "Klik op de melding om verder te gaan met de test." + "Ervoor zorgen dat de applicatie pushberichten ontvangt." + "Fout: pusher heeft het verzoek afgewezen." + "Fout: %1$s." + "Fout, kan push niet testen." + "Fout, time-out tijdens het wachten op push." + "Push terugkoppeling duurde %1$d ms." + "Test Push terugkoppeling" diff --git a/libraries/pushproviders/firebase/src/main/res/values-nl/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-nl/translations.xml new file mode 100644 index 0000000000..d847c39901 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-nl/translations.xml @@ -0,0 +1,11 @@ + + + "Zorg ervoor dat Firebase beschikbaar is." + "Firebase is niet beschikbaar." + "Firebase is beschikbaar." + "Firebase controleren" + "Zorg ervoor dat de Firebase-token beschikbaar is." + "Firebase-token is niet bekend." + "Firebase-token: %1$s." + "Firebase-token controleren" + diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/DefaultFirebaseNewTokenHandlerTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/DefaultFirebaseNewTokenHandlerTest.kt index 2da1e8554d..668eeebf98 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/DefaultFirebaseNewTokenHandlerTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/DefaultFirebaseNewTokenHandlerTest.kt @@ -51,9 +51,9 @@ class DefaultFirebaseNewTokenHandlerTest { val pusherSubscriber = FakePusherSubscriber(registerPusherResult = registerPusherResult) val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler( sessionStore = InMemoryMultiSessionsStore().apply { - storeData(aSessionData(A_USER_ID)) - storeData(aSessionData(A_USER_ID_2)) - storeData(aSessionData(A_USER_ID_3)) + storeData(aSessionData(A_USER_ID.value)) + storeData(aSessionData(A_USER_ID_2.value)) + storeData(aSessionData(A_USER_ID_3.value)) }, matrixClientProvider = FakeMatrixClientProvider { sessionId -> when (sessionId) { @@ -90,7 +90,7 @@ class DefaultFirebaseNewTokenHandlerTest { val pusherSubscriber = FakePusherSubscriber(registerPusherResult = registerPusherResult) val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler( sessionStore = InMemoryMultiSessionsStore().apply { - storeData(aSessionData(A_USER_ID)) + storeData(aSessionData(A_USER_ID.value)) }, matrixClientProvider = FakeMatrixClientProvider { Result.failure(IllegalStateException()) @@ -114,7 +114,7 @@ class DefaultFirebaseNewTokenHandlerTest { val pusherSubscriber = FakePusherSubscriber(registerPusherResult = registerPusherResult) val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler( sessionStore = InMemoryMultiSessionsStore().apply { - storeData(aSessionData(A_USER_ID)) + storeData(aSessionData(A_USER_ID.value)) }, matrixClientProvider = FakeMatrixClientProvider { Result.success(aMatrixClient1) diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-nl/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-nl/translations.xml new file mode 100644 index 0000000000..afe222b147 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-nl/translations.xml @@ -0,0 +1,10 @@ + + + "Ervoor zorgen dat UnifiedPush verdelers beschikbaar zijn." + "Geen push-verdelers gevonden." + + "%1$d verdeler gevonden: %2$s." + "%1$d verdelers gevonden: %2$s." + + "UnifiedPush controleren" + diff --git a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt index 8b3a0e5b26..ad62337ae6 100644 --- a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt +++ b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt @@ -7,19 +7,19 @@ package io.element.android.libraries.sessionstorage.test -import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.sessionstorage.api.LoginType import io.element.android.libraries.sessionstorage.api.SessionData fun aSessionData( - sessionId: SessionId = SessionId("@alice:server.org"), + sessionId: String = "@alice:server.org", + deviceId: String = "aDeviceId", isTokenValid: Boolean = false, sessionPath: String = "/a/path/to/a/session", cachePath: String = "/a/path/to/a/cache", ): SessionData { return SessionData( - userId = sessionId.value, - deviceId = "aDeviceId", + userId = sessionId, + deviceId = deviceId, accessToken = "anAccessToken", refreshToken = "aRefreshToken", homeserverUrl = "aHomeserverUrl", diff --git a/libraries/troubleshoot/impl/src/main/res/values-nl/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-nl/translations.xml new file mode 100644 index 0000000000..5ce2bd89ad --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-nl/translations.xml @@ -0,0 +1,11 @@ + + + "Tests uitvoeren" + "Tests opnieuw uitvoeren" + "Sommige tests zijn mislukt. Controleer de details." + "Voer de tests uit om problemen in je configuratie op te sporen waardoor meldingen mogelijk niet naar verwachting werken." + "Probeer het op te lossen" + "Alle tests zijn geslaagd." + "Problemen met meldingen oplossen" + "Sommige tests vereisen je aandacht. Controleer de details." + diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml index 6fcecc495b..164aed50f3 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -38,6 +38,7 @@ "Zpět" "Hovor" "Zrušit" + "Prozatím zrušit" "Vybrat fotku" "Vymazat" "Zavřít" @@ -288,6 +289,12 @@ Důvod: %1$s." "Připnuté zprávy" "Chystáte se přejít na svůj %1$s účet a obnovit svou identitu. Poté budete přesměrováni zpět do aplikace." "Nemůžete to potvrdit? Přejděte na svůj účet a resetujte svou identitu." + "Zrušit ověření a odeslat" + "Ověření můžete zrušit a přesto odeslat tuto zprávu, nebo můžete prozatím zrušit a zkusit to znovu později po opětovném ověření %1$s." + "Vaše zpráva nebyla odeslána, protože ověřená identita uživatele %1$s se změnila" + "Přesto odeslat zprávu" + "%1$s používá jedno nebo více neověřených zařízení. Zprávu můžete přesto odeslat, nebo můžete prozatím zrušit a zkusit to znovu později poté, co %2$s ověří všechna svá zařízení." + "Vaše zpráva nebyla odeslána, protože%1$s neověřil jedno nebo více zařízení" "Připnuté zprávy" "Nahrání média se nezdařilo, zkuste to prosím znovu." "Nepodařilo se načíst údaje o uživateli" @@ -309,6 +316,8 @@ Důvod: %1$s." "Otevřít v Mapách Google" "Otevřít v OpenStreetMap" "Sdílet tuto polohu" + "Zpráva nebyla odeslána, protože ověřená identita uživatele %1$s se změnila." + "Zpráva nebyla odeslána, protože%1$s neověřil jedno nebo více zařízení." "Poloha" "Verze: %1$s (%2$s)" "en" diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index dcd41ca7a6..13bd0b7d93 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -36,6 +36,7 @@ "Zurück" "Anruf" "Abbrechen" + "Jetzt abbrechen" "Foto auswählen" "Löschen" "Schließen" @@ -80,6 +81,7 @@ "Ok" "Einstellungen öffnen" "Öffnen mit" + "Fixieren" "Schnelle Antwort" "Zitat" "Reagieren" @@ -109,7 +111,8 @@ "Foto aufnehmen" "Für Optionen tippen" "Erneut versuchen" - "Entpinnen" + "Lösen" + "Im Chatverlauf anzeigen" "Quellcode anzeigen" "Ja" "Über" @@ -175,6 +178,7 @@ Grund: %1$s." "Personen" "Permalink" "Erlaubnis" + "Fixiert" "Bitte warten …" "Bist du sicher, dass du diese Umfrage beenden möchtest?" "Umfrage: %1$s" @@ -246,6 +250,7 @@ Grund: %1$s." "Warnung" "Deine Änderungen wurden nicht gespeichert. Bist du sicher, dass du zurückgehen willst?" "Änderungen speichern?" + "Dein Homeserver muss aktualisiert werden, um den Matrix Authentication Services und die Erstellung von Konten zu unterstützen." "Fehler beim Erstellen des Permalinks" "%1$s konnte die Karte nicht laden. Bitte versuche es später erneut." "Fehler beim Laden der Nachrichten" @@ -258,6 +263,8 @@ Grund: %1$s." "Einige Nachrichten wurden nicht gesendet" "Entschuldigung, es ist ein Fehler aufgetreten" "Die Authentizität dieser verschlüsselten Nachricht kann auf diesem Gerät nicht garantiert werden." + "Verschlüsselt von einem zuvor verifizierten Nutzer." + "Unverschlüsselt." "Verschlüsselt von einem unbekannten oder gelöschten Gerät." "Verschlüsselt durch ein Gerät, das nicht von seinem Besitzer verifiziert wurde." "Verschlüsselt durch einen nicht verifizierten Benutzer." @@ -268,6 +275,22 @@ Grund: %1$s." "Medienauswahl fehlgeschlagen, bitte versuche es erneut." "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." "Das Hochladen der Medien ist fehlgeschlagen. Bitte versuche es erneut." + "Drücke auf eine Nachricht und wähle “%1$s”, um sie hier einzufügen." + "Fixiere wichtige Nachrichten, so dass sie leicht gefunden werden können" + + "%1$d fixierte Nachricht" + "%1$d fixierte Nachrichten" + + "Fixierte Nachrichten" + "Du wirst jetzt zu deinem %1$s Konto geleitet, um deine Identität zurückzusetzen. Danach wirst du zur App zurückgebracht." + "Kannst du das nicht bestätigen? Gehe zu deinem Konto, um deine Identität zurückzusetzen." + "Verifizierung zurückziehen und senden" + "Du kannst deine Verifizierung zurückziehen und diese Nachricht trotzdem senden, oder du kannst sie vorerst abbrechen und es später noch einmal versuchen, nachdem du %1$s erneut verifiziert hast." + "Deine Nachricht wurde nicht gesendet, da sich die verifizierte Identität von %1$s geändert hat" + "Nachricht trotzdem senden" + "%1$s verwendet wenigstens ein nicht verifiziertes Gerät. Du kannst die Nachricht trotzdem verschicken, oder vorerst abbrechen und später erneut versuchen, nachdem %2$s alle Geräte verifiziert hat." + "Deine Nachricht wurde nicht gesendet, weil %1$s nicht alle Geräte verifiziert hat" + "Fixierte Nachrichten" "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." "Benutzerdetails konnten nicht abgerufen werden" "Blockieren" @@ -279,6 +302,7 @@ Grund: %1$s." "Blockierung aufheben" "%1$s von %2$s" "%1$s Angepinnte Nachrichten" + "Nachricht wird geladen…" "Alle anzeigen" "Chat" "Standort teilen" @@ -287,6 +311,8 @@ Grund: %1$s." "In Google Maps öffnen" "In OpenStreetMap öffnen" "Diesen Standort teilen" + "Nachricht nicht gesendet, weil sich die verifizierte Identität von %1$s geändert hat." + "Nachricht wurde nicht gesendet, weil %1$s nicht alle Geräte verifiziert hat" "Standort" "Version: %1$s (%2$s)" "en" diff --git a/libraries/ui-strings/src/main/res/values-el/translations.xml b/libraries/ui-strings/src/main/res/values-el/translations.xml index b38211a60d..d0fd3cbdab 100644 --- a/libraries/ui-strings/src/main/res/values-el/translations.xml +++ b/libraries/ui-strings/src/main/res/values-el/translations.xml @@ -36,6 +36,7 @@ "Πίσω" "Κάλεσε" "Άκυρο" + "Ακύρωση προς το παρόν" "Επιλογή φωτογραφίας" "Εκκαθάριση" "Κλείσιμο" @@ -91,6 +92,7 @@ "Αναφορά σφάλματος" "Αναφορά περιεχομένου" "Επαναφορά" + "Επαναφορά ταυτότητας" "Επανάληψη" "Επανάληψη αποκρυπτογράφησης" "Αποθήκευση" @@ -111,6 +113,7 @@ "Πάτα για επιλογές" "Προσπάθησε ξανά" "Ξεκαρφίτσωμα" + "Προβολή στο χρονοδιάγραμμα" "Προβολή πηγής" "Ναι" "Σχετικά" @@ -176,6 +179,7 @@ "Άτομα" "Μόνιμος σύνδεσμος" "Αδεια" + "Καρφιτσωμένο" "Παρακαλώ περίμενε…" "Θες σίγουρα να τερματίσεις αυτή τη δημοσκόπηση;" "Δημοσκόπηση: %1$s" @@ -247,6 +251,7 @@ "Προειδοποίηση" "Οι αλλαγές σου δεν έχουν αποθηκευτεί. Σίγουρα θες να πας πίσω;" "Αποθήκευση αλλαγών;" + "Ο οικιακός διακομιστής σου πρέπει να αναβαθμιστεί για να υποστηρίζει το Matrix Authentication Server και τη δημιουργία λογαριασμού." "Αποτυχία δημιουργίας του μόνιμου συνδέσμου" "%1$s δεν ήταν δυνατή η φόρτωση του χάρτη. Παρακαλώ δοκίμασε ξανά αργότερα." "Αποτυχία φόρτωσης μηνυμάτων" @@ -259,6 +264,8 @@ "Ορισμένα μηνύματα δεν έχουν σταλεί" "Λυπούμαστε, παρουσιάστηκε σφάλμα" "Η αυθεντικότητα αυτού του κρυπτογραφημένου μηνύματος δεν είναι εγγυημένη σε αυτήν τη συσκευή." + "Κρυπτογραφημένο από έναν προηγουμένως επαληθευμένο χρήστη." + "Μη κρυπτογραφημένο." "Κρυπτογραφημένο από άγνωστη ή διαγεγραμμένη συσκευή." "Κρυπτογραφημένο από μια συσκευή που δεν έχει επαληθευτεί από τον ιδιοκτήτη της." "Κρυπτογραφημένο από μη επαληθευμένο χρήστη." @@ -269,6 +276,22 @@ "Αποτυχία επιλογής πολυμέσου, δοκίμασε ξανά." "Αποτυχία μεταφόρτωσης μέσου, δοκίμασε ξανά." "Αποτυχία μεταφόρτωσης πολυμέσων, δοκίμασε ξανά." + "Πάτα σε ένα μήνυμα και επέλεξε «%1$s» για να συμπεριληφθεί εδώ." + "Καρφίτσωσε σημαντικά μηνύματα, ώστε να μπορούν να εντοπιστούν εύκολα" + + "%1$d Καρφιτσωμένο μήνυμα" + "%1$d Καρφιτσωμένα μηνύματα" + + "Καρφιτσωμένα μηνύματα" + "Πρόκειται να μεταβείς στον λογαριασμό σου %1$s για να επαναφέρεις την ταυτότητά σου. Στη συνέχεια, θα επιστρέψεις στην εφαρμογή." + "Δεν μπορείς να επιβεβαιώσεις; Πήγαινε στον λογαριασμό σου για να επαναφέρεις την ταυτότητά σου." + "Ανάκληση επαλήθευσης και αποστολή" + "Μπορείτε να ανακαλέσεις την επαλήθευσή σου και να στείλεις αυτό το μήνυμα όπως και να \'χει ή μπορείς να το ακυρώσεις προς το παρόν και να προσπαθήσεις ξανά αργότερα μετά την επαλήθευση του χρήστη %1$s." + "Το μήνυμά σου δεν στάλθηκε επειδή η επαληθευμένη ταυτότητα του χρήστη %1$s έχει αλλάξει" + "Αποστολή μηνύματος ούτως ή άλλως" + "Ο χρήστης %1$s χρησιμοποιεί τουλάχιστον μία μη επαληθευμένη συσκευή. Μπορείς να στείλεις το μήνυμα όπως και να \'χει ή μπορείς να το ακυρώσεις προς το παρόν και να δοκιμάσεις ξανά αργότερα αφού ο χρήστης %2$s επαληθεύσει όλες τις συσκευές του." + "Το μήνυμά σου δεν στάλθηκε επειδή ο χρήστης %1$s δεν έχει επαληθεύσει τουλάχιστον μία συσκευή" + "Καρφιτσωμένα μηνύματα" "Αποτυχία μεταφόρτωσης μέσου, δοκίμασε ξανά." "Δεν ήταν δυνατή η ανάκτηση στοιχείων χρήστη" "Αποκλεισμός" @@ -280,6 +303,7 @@ "Κατάργηση αποκλεισμού χρήστη" "%1$s από %2$s" "%1$s Καρφιτσωμένα μηνύματα" + "Φόρτωση μηνύματος…" "Προβολή Όλων" "Συνομιλία" "Κοινή χρήση τοποθεσίας" @@ -288,6 +312,8 @@ "Άνοιγμα στο Google Maps" "Άνοιγμα στο OpenStreetMap" "Κοινή χρήση αυτής της τοποθεσίας" + "Το μήνυμα δεν στάλθηκε επειδή η επαληθευμένη ταυτότητα του χρήστη %1$s έχει αλλάξει." + "Το μήνυμα δεν στάλθηκε επειδή ο χρήστης %1$s δεν έχει επαληθεύσει τουλάχιστον μία συσκευή." "Τοποθεσία" "Έκδοση: %1$s (%2$s)" "el" diff --git a/libraries/ui-strings/src/main/res/values-et/translations.xml b/libraries/ui-strings/src/main/res/values-et/translations.xml index 8f6c8740e6..1de14e5f1f 100644 --- a/libraries/ui-strings/src/main/res/values-et/translations.xml +++ b/libraries/ui-strings/src/main/res/values-et/translations.xml @@ -36,6 +36,7 @@ "Tagasi" "Helista kasutajale" "Loobu" + "Hetkel jäta tegemata" "Vali foto" "Selge" "Sulge" @@ -250,6 +251,7 @@ Põhjus: %1$s." "Hoiatus" "Sinu tehtud muudatused pole veel salvestatud. Kas sa oled kindel, et soovid minna tagasi?" "Kas salvestame muudatused?" + "Selleks et koos kasutajakonto loomisega toimiks Matrix Authentication Service\'i tugi, vajab sinu koduserver uuendamist." "Püsilingi loomine ei õnnestumud" "%1$s kaardi laadimine ei õnnestunud. Palun proovi hiljem uuesti." "Sõnumite laadimine ei õnnestunud" @@ -283,6 +285,12 @@ Põhjus: %1$s." "Esiletõstetud sõnumid" "Oma võrguidentiteedi lähtestamiseks suuname sind %1$s kasutajakonto halduse lehele. Hiljem suunatakse sind tagasi sama rakenduse juurde." "Sa ei saa seda kinnitada? Ava oma kasutajakonto haldus ja lähtesta oma võrguidentiteet." + "Unusta verifitseerimine ja saada ikkagi" + "Sa võid jätta verifitseerimisvea tähelepanuta ja sõnumi ikkagi saata või katkestad saatmise ja peale kasutaja %1$s verifitseerimist proovid seda uuesti." + "Sinu sõnum on saatmata, kuna kasutaja %1$s verifitseeritud identiteet on muutunud." + "Saada sõnum ikkagi" + "%1$s kasutab ühte või enamat verifitseerimata seadet. Sa võid sõnumi ikkagi saata või katkestad selle ning ootad kuni %2$s on kõik oma seadmed verifitseerinud ning proovid seejärel uuesti." + "Sinu sõnum on saatmata, kuna %1$s pole verifitseerinud ühte või enamat oma seadet" "Esiletõstetud sõnumid" "Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti." "Kasutaja andmete laadimine ei õnnestunud" @@ -304,6 +312,8 @@ Põhjus: %1$s." "Ava Google Mapsis" "Ava OpenStreetMapis" "Jaga seda asukohta" + "Sõnum on saatmata, kuna kasutaja %1$s verifitseeritud identiteet on muutunud." + "Sõnum on saatmata, kuna %1$s pole verifitseerinud ühte või enamat oma seadet." "Asukoht" "Versioon: %1$s (%2$s)" "et" diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 258f386137..8c41675598 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -36,6 +36,7 @@ "Retour" "Appel" "Annuler" + "Annuler pour l’instant" "Choisir une photo" "Effacer" "Fermer" @@ -249,6 +250,7 @@ Raison: %1$s." "Attention" "Vos modifications n’ont pas été enregistrées. Êtes-vous certain de vouloir quitter?" "Enregistrer les changements?" + "Votre serveur d’accueil doit être mis à jour pour prendre en charge le protocole MAS (Matrix Authentication Service) et la création de compte." "Échec de la création du permalien" "%1$s n’a pas pu charger la carte. Veuillez réessayer ultérieurement." "Échec du chargement des messages" @@ -280,6 +282,14 @@ Raison: %1$s." "%1$d messages épinglés" "Messages épinglés" + "Vous êtes sur le point d’accéder à votre compte %1$s pour réinitialiser votre identité. Vous serez ensuite redirigé vers l’application." + "Vous ne pouvez pas confirmer ? Accédez à votre compte pour réinitialiser votre identité." + "Révoquer la verification et envoyer" + "Vous pouvez révoquer la verification et envoyer ce message, ou vous pouvez annuler pour l’instant et réessayer plus tard après avoir vérifié à nouveau %1$s." + "Votre message n’a pas été envoyé car l’identité vérifiée de %1$s a changé" + "Envoyer le message quand même" + "%1$s utilise un ou plusieurs appareils non vérifiés. Vous pouvez quand même envoyer le message, ou vous pouvez annuler pour l’instant et réessayer plus tard après que %2$s vérifie tous ses appareils." + "Votre message n’a pas été envoyé car %1$s n’a pas vérifié tous ses appareils" "Messages épinglés" "Échec du traitement des médias à télécharger, veuillez réessayer." "Impossible de récupérer les détails de l’utilisateur" @@ -301,6 +311,8 @@ Raison: %1$s." "Ouvrir dans Google Maps" "Ouvrir dans OpenStreetMap" "Partager cette position" + "Le message n’a pas été envoyé car l’identité vérifiée de %1$s a changé." + "Le message n’a pas été envoyé car %1$s n’a pas vérifié tous ses appareils." "Position" "Version : %1$s ( %2$s )" "fr" diff --git a/libraries/ui-strings/src/main/res/values-hu/translations.xml b/libraries/ui-strings/src/main/res/values-hu/translations.xml index fff7fcbe20..977c6c8928 100644 --- a/libraries/ui-strings/src/main/res/values-hu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hu/translations.xml @@ -36,6 +36,7 @@ "Vissza" "Hívás" "Mégse" + "Egyelőre nem" "Fénykép kiválasztása" "Törlés" "Bezárás" @@ -250,6 +251,7 @@ Ok: %1$s." "Figyelmeztetés" "A módosítások nem lettek mentve. Biztos, hogy visszalép?" "Menti a módosításokat?" + "A Matrix-kiszolgálót frissíteni kell a Matrix Authentication Service és a fióklétrehozás támogatásához." "Nem sikerült létrehozni az állandó hivatkozást" "Az %1$s nem tudta betölteni a térképet. Próbálja meg újra később." "Nem sikerült betölteni az üzeneteket" @@ -283,6 +285,12 @@ Ok: %1$s." "Kitűzött üzenetek" "Arra készül, hogy belépjen a(z) %1$s fiókjába, hogy visszaállítsa a személyazonosságát. Ezután vissza fog térni az alkalmazásba." "Nem tudja megerősíteni? Ugorjon a fiókjához, és állítsa vissza a személyazonosságát." + "Ellenőrzés visszavonása és elküldés" + "Visszavonhatja az ellenőrzést, és ennek ellenére elküldheti ezt az üzenetet, vagy egyelőre törölheti, és %1$s újbóli ellenőrzése után újra megpróbálhatja." + "Az üzenete nem lett elküldve, mert %1$s ellenőrzött személyazonossága megváltozott." + "Üzenet elküldése mindenképp" + "%1$s egy vagy több ellenőrizetlen eszközt használ. Így is elküldheti az üzenetet, vagy megszakíthatja most, és megpróbálhatja újra, miután %2$s ellenőrizte az összes eszközét." + "Az üzenet nem lett elküldve, mert %1$s nem ellenőrizte az összes eszközét" "Kitűzött üzenetek" "Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra." "Nem sikerült letölteni a felhasználói adatokat" @@ -304,6 +312,8 @@ Ok: %1$s." "Megnyitás a Google Mapsben" "Megnyitás az OpenStreetMapen" "E hely megosztása" + "Az üzenet nem lett elküldve, mert %1$s ellenőrzött személyazonossága megváltozott." + "Az üzenet nem lett elküldve, mert %1$s nem ellenőrizte az összes eszközét." "Hely" "Verzió: %1$s (%2$s)" "hu" diff --git a/libraries/ui-strings/src/main/res/values-nl/translations.xml b/libraries/ui-strings/src/main/res/values-nl/translations.xml index 7b9034291f..a1d8c194a4 100644 --- a/libraries/ui-strings/src/main/res/values-nl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-nl/translations.xml @@ -34,6 +34,7 @@ "Accepteren" "Toevoegen aan tijdlijn" "Terug" + "Bellen" "Annuleren" "Kies foto" "Wissen" @@ -49,6 +50,7 @@ "Weigeren" "Peiling verwijderen" "Uitschakelen" + "Verwerpen" "Gereed" "Bewerken" "Peiling wijzigen" @@ -57,6 +59,7 @@ "Voer pincode in" "Wachtwoord vergeten?" "Doorsturen" + "Terug" "Uitnodigen" "Mensen uitnodigen" "Nodig mensen uit voor %1$s" @@ -70,6 +73,7 @@ "Meer laden" "Account beheren" "Apparaten beheren" + "Bericht" "Volgende" "Nee" "Niet nu" @@ -84,6 +88,7 @@ "Antwoord in subchat" "Probleem melden" "Inhoud melden" + "Opnieuw instellen" "Opnieuw proberen" "Decryptie opnieuw proberen" "Opslaan" @@ -108,10 +113,13 @@ "Over" "Beleid inzake redelijk gebruik" "Geavanceerde instellingen" + "Gebruiksgegevens" "Weergave" "Geluid" "Geblokkeerde gebruikers" "Bubbels" + "Gesprek bezig (niet ondersteund)" + "Oproep gestart" "Chat back-up" "Copyright" "Kamer maken…" @@ -153,12 +161,14 @@ "Modern" "Dempen" "Geen resultaten" + "Geen kamernaam" "Offline" "of" "Wachtwoord" "Personen" "Permalink" "Toestemming" + "Even geduld…" "Weet je zeker dat je deze peiling wilt beëindigen?" "Peiling: %1$s" "Totaal aantal stemmen: %1$s" @@ -180,6 +190,8 @@ "Kamer" "Naam van de kamer" "bijv. de naam van je project" + "Wijzigingen opgeslagen" + "Opslaan" "Schermvergrendeling" "Iemand zoeken" "Zoekresultaten" @@ -193,6 +205,7 @@ "Instellingen" "Gedeelde locatie" "Uitloggen" + "Er is iets misgegaan" "Chat starten…" "Sticker" "Geslaagd" @@ -205,6 +218,7 @@ "Onderwerp" "Waar gaat deze kamer over?" "Kan niet ontsleutelen" + "Je hebt geen toegang tot dit bericht" "Uitnodigingen konden niet naar een of meerdere gebruikers worden verzonden." "Kan uitnodiging(en) niet verzenden" "Ontgrendelen" @@ -222,11 +236,14 @@ "Fout" "Geslaagd" "Waarschuwing" + "Je wijzigingen zijn niet opgeslagen. Weet je zeker dat je terug wilt gaan?" + "Wijzigingen opslaan?" "Het aanmaken van de permanente link is mislukt" "%1$s kon de kaart niet laden. Probeer het later opnieuw." "Het laden van berichten is mislukt" "%1$s had geen toegang tot je locatie. Probeer het later opnieuw." "Het uploaden van je spraakbericht is mislukt." + "Bericht niet gevonden" "%1$s heeft geen toegang tot je locatie. Je kunt dit inschakelen bij Instellingen." "%1$s heeft geen toegang tot je locatie. Schakel toegang hieronder in." "%1$s heeft geen toegang tot je microfoon. Schakel toegang in om een spraakbericht op te nemen." @@ -244,9 +261,11 @@ "Blokkeren" "Geblokkeerde gebruikers kunnen je geen berichten sturen en al hun berichten worden verborgen. Je kunt ze op elk moment deblokkeren." "Gebruiker blokkeren" + "Profiel" "Deblokkeren" "Je zult alle berichten van hen weer kunnen zien." "Gebruiker deblokkeren" + "Chat" "Locatie delen" "Deel mijn locatie" "Openen in Apple Maps" diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml index 6dd12d85df..407ce90c96 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -38,6 +38,7 @@ "Назад" "Позвонить" "Отмена" + "Отмените сейчас" "Выбрать фото" "Очистить" "Закрыть" @@ -256,6 +257,7 @@ "Предупреждение" "Изменения не сохранены. Вы действительно хотите вернуться?" "Сохранить изменения?" + "Ваш homeserver необходимо обновить, чтобы он поддерживал Matrix Authentication Service и создание учетной записи." "Не удалось создать постоянную ссылку" "Не удалось загрузить карту %1$s. Пожалуйста, повторите попытку позже." "Не удалось загрузить сообщения" @@ -290,6 +292,12 @@ "Закрепленные сообщения" "Вы собираетесь перейти в свою учетную запись %1$s, чтобы сбросить идентификацию. После этого вы вернетесь в приложение." "Не можете подтвердить? Перейдите в свою учетную запись, чтобы сбросить свою идентификацию." + "Отозвать верификацию и отправить" + "Вы можете отозвать свою верификацию и отправить это сообщение в любом случае или вы можете отменить ее сейчас и повторить попытку позже после повторной верификации %1$s." + "Ваше сообщение не было отправлено, потому что изменилась подтвержденная личность %1$s" + "Отправь сообщение в любом случае" + "%1$s использует одно или несколько непроверенных устройств. Вы все равно можете отправить сообщение или отменить его пока и повторить попытку позже %2$s, проверив все устройства пользователя." + "Ваше сообщение не было отправлено, потому что %1$s не проверил одно или несколько устройств" "Закрепленные сообщения" "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." "Не удалось получить данные о пользователе" @@ -311,6 +319,8 @@ "Открыть в Google Maps" "Открыть в OpenStreetMap" "Поделиться этим местоположением" + "Сообщение не отправлено, потому что верифицированная личность %1$s изменилась." + "Сообщение не отправлено, потому что %1$s не проверил одно или несколько устройств." "Местоположение" "Версия: %1$s (%2$s)" "ru" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index 5fbe3c55e7..d184c35e0c 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -38,6 +38,7 @@ "Späť" "Zavolať" "Zrušiť" + "Zatiaľ zrušiť" "Vybrať fotku" "Vyčistiť" "Zavrieť" @@ -254,6 +255,7 @@ Dôvod: %1$s." "Upozornenie" "Vaše zmeny neboli uložené. Naozaj sa chcete vrátiť?" "Uložiť zmeny?" + "Váš domovský server musí byť aktualizovaný tak, aby podporoval Matrix Authentication Server a vytvorenie účtu." "Nepodarilo sa vytvoriť trvalý odkaz" "%1$s nedokázal načítať mapu. Skúste to prosím neskôr." "Načítanie správ zlyhalo" @@ -288,6 +290,12 @@ Dôvod: %1$s." "Pripnuté správy" "Chystáte sa prejsť na svoj %1$s účet, aby ste obnovili svoju identitu. Potom budete vrátení späť do aplikácie." "Neviete potvrdiť? Prejdite do svojho účtu a obnovte svoju identitu." + "Odvolať overenie a odoslať" + "Svoje overenie môžete odvolať a odoslať túto správu aj tak, alebo ju môžete zatiaľ zrušiť a po opätovnom overení to skúsiť znova %1$s ." + "Vaša správa nebola odoslaná, pretože sa zmenila overená totožnosť používateľa %1$s." + "Odoslať správu aj tak" + "%1$s používa jedno alebo viac neoverených zariadení. Správu môžete odoslať aj tak, alebo ju môžete zatiaľ zrušiť a skúsiť to znova neskôr po %2$s overení všetkých zariadení." + "Vaša správa nebola odoslaná, pretože %1$s neoveril/a jedno alebo viac zariadení" "Pripnuté správy" "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." "Nepodarilo sa získať údaje o používateľovi" @@ -309,6 +317,8 @@ Dôvod: %1$s." "Otvoriť v Mapách Google" "Otvoriť v OpenStreetMap" "Zdieľajte túto polohu" + "Správa nebola odoslaná, pretože sa zmenila overená totožnosť používateľa %1$s." + "Správa nebola odoslaná, pretože %1$s neoveril/a jedno alebo viac zariadení." "Poloha" "Verzia: %1$s (%2$s)" "sk" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index f7c3213ba4..4ef598a33b 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -36,17 +36,21 @@ "Back" "Call" "Cancel" + "Cancel for now" "Choose photo" "Clear" "Close" "Complete verification" "Confirm" + "Confirm password" "Continue" "Copy" "Copy link" "Copy link to message" "Create" "Create a room" + "Deactivate" + "Deactivate account" "Decline" "Delete Poll" "Disable" @@ -250,6 +254,7 @@ Reason: %1$s." "Warning" "Your changes have not been saved. Are you sure you want to go back?" "Save changes?" + "Your homeserver needs to be upgraded to support Matrix Authentication Service and account creation." "Failed creating the permalink" "%1$s could not load the map. Please try again later." "Failed loading messages" @@ -283,6 +288,14 @@ Reason: %1$s." "Pinned messages" "You\'re about to go to your %1$s account to reset your identity. Afterwards you\'ll be taken back to the app." "Can\'t confirm? Go to your account to reset your identity." + "Withdraw verification and send" + "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$s." + "Your message was not sent because %1$s’s verified identity has changed" + "Send message anyway" + "%1$s is using one or more unverified devices. You can send the message anyway, or you can cancel for now and try again later after %2$s has verified all their devices." + "Your message was not sent because %1$s has not verified all devices" + "One or more of your devices are unverified. You can send the message anyway, or you can cancel for now and try again later after you have verified all of your devices." + "Your message was not sent because you have not verified one or more of your devices" "Pinned messages" "Failed processing media to upload, please try again." "Could not retrieve user details" @@ -304,6 +317,9 @@ Reason: %1$s." "Open in Google Maps" "Open in OpenStreetMap" "Share this location" + "Message not sent because %1$s’s verified identity has changed." + "Message not sent because %1$s has not verified all devices." + "Message not sent because you have not verified one or more of your devices." "Location" "Version: %1$s (%2$s)" "en" diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index 59048156b3..54afab0525 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -47,7 +47,7 @@ private const val versionMinor = 6 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -private const val versionPatch = 0 +private const val versionPatch = 3 object Versions { val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/AlwaysEnabledFeatureFlagService.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/AlwaysEnabledFeatureFlagService.kt new file mode 100644 index 0000000000..67151d5aa4 --- /dev/null +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/AlwaysEnabledFeatureFlagService.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.samples.minimal + +import io.element.android.libraries.featureflag.api.Feature +import io.element.android.libraries.featureflag.api.FeatureFlagService +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +class AlwaysEnabledFeatureFlagService : FeatureFlagService { + override fun isFeatureEnabledFlow(feature: Feature): Flow { + return flowOf(true) + } + + override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean { + return true + } +} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt index e1654a8fb7..19e1390f47 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt @@ -55,10 +55,11 @@ class MainActivity : ComponentActivity() { proxyProvider = proxyProvider, clock = DefaultSystemClock(), utdTracker = UtdTracker(NoopAnalyticsService()), - appPreferencesStore = InMemoryAppPreferencesStore(), + featureFlagService = AlwaysEnabledFeatureFlagService(), ), passphraseGenerator = NullPassphraseGenerator(), oidcConfigurationProvider = OidcConfigurationProvider(baseDirectory), + appPreferencesStore = InMemoryAppPreferencesStore(), ) } diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index 70f2a1f8a4..c6a2d49c89 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -25,8 +25,6 @@ import io.element.android.features.roomlist.impl.filters.selection.DefaultFilter import io.element.android.features.roomlist.impl.search.RoomListSearchDataSource import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.dateformatter.impl.DateFormatters import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider @@ -35,8 +33,6 @@ import io.element.android.libraries.eventformatter.impl.DefaultRoomLastMessageFo import io.element.android.libraries.eventformatter.impl.ProfileChangeContentFormatter import io.element.android.libraries.eventformatter.impl.RoomMembershipContentFormatter import io.element.android.libraries.eventformatter.impl.StateContentFormatter -import io.element.android.libraries.featureflag.impl.DefaultFeatureFlagService -import io.element.android.libraries.featureflag.impl.PreferencesFeatureFlagProvider import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsPresenter import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.indicator.impl.DefaultIndicatorService @@ -70,13 +66,7 @@ class RoomListScreen( private val sessionVerificationService = matrixClient.sessionVerificationService() private val encryptionService = matrixClient.encryptionService() private val stringProvider = AndroidStringProvider(context.resources) - private val buildMeta = getBuildMeta(context) - private val featureFlagService = DefaultFeatureFlagService( - providers = setOf( - PreferencesFeatureFlagProvider(context = context, buildMeta = buildMeta) - ), - buildMeta = buildMeta, - ) + private val featureFlagService = AlwaysEnabledFeatureFlagService() private val roomListRoomSummaryFactory = RoomListRoomSummaryFactory( lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter( localDateTimeProvider = dateTimeProvider, @@ -191,25 +181,4 @@ class RoomListScreen( } } } - - private fun getBuildMeta(context: Context): BuildMeta { - val buildType = BuildType.valueOf(BuildConfig.BUILD_TYPE.uppercase()) - val name = context.getString(R.string.app_name) - return BuildMeta( - isDebuggable = BuildConfig.DEBUG, - buildType = buildType, - applicationName = name, - productionApplicationName = name, - desktopApplicationName = name, - applicationId = BuildConfig.APPLICATION_ID, - lowPrivacyLoggingEnabled = false, - versionName = BuildConfig.VERSION_NAME, - versionCode = BuildConfig.VERSION_CODE.toLong(), - gitRevision = "", - gitBranchName = "", - flavorDescription = "", - flavorShortDescription = "", - isEnterpriseBuild = false, - ) - } } diff --git a/screenshots/html/data.js b/screenshots/html/data.js index 4e00986bda..6ac9ca9bfe 100644 --- a/screenshots/html/data.js +++ b/screenshots/html/data.js @@ -1,41 +1,41 @@ // Generated file, do not edit export const screenshots = [ ["en","en-dark","de",], -["features.preferences.impl.about_AboutView_Day_0_en","features.preferences.impl.about_AboutView_Night_0_en",19972,], +["features.preferences.impl.about_AboutView_Day_0_en","features.preferences.impl.about_AboutView_Night_0_en",19983,], ["features.invite.impl.response_AcceptDeclineInviteView_Day_0_en","features.invite.impl.response_AcceptDeclineInviteView_Night_0_en",0,], -["features.invite.impl.response_AcceptDeclineInviteView_Day_1_en","features.invite.impl.response_AcceptDeclineInviteView_Night_1_en",19972,], -["features.invite.impl.response_AcceptDeclineInviteView_Day_2_en","features.invite.impl.response_AcceptDeclineInviteView_Night_2_en",19972,], -["features.invite.impl.response_AcceptDeclineInviteView_Day_3_en","features.invite.impl.response_AcceptDeclineInviteView_Night_3_en",19972,], -["features.invite.impl.response_AcceptDeclineInviteView_Day_4_en","features.invite.impl.response_AcceptDeclineInviteView_Night_4_en",19972,], +["features.invite.impl.response_AcceptDeclineInviteView_Day_1_en","features.invite.impl.response_AcceptDeclineInviteView_Night_1_en",19983,], +["features.invite.impl.response_AcceptDeclineInviteView_Day_2_en","features.invite.impl.response_AcceptDeclineInviteView_Night_2_en",19983,], +["features.invite.impl.response_AcceptDeclineInviteView_Day_3_en","features.invite.impl.response_AcceptDeclineInviteView_Night_3_en",19983,], +["features.invite.impl.response_AcceptDeclineInviteView_Day_4_en","features.invite.impl.response_AcceptDeclineInviteView_Night_4_en",19983,], ["features.login.impl.accountprovider_AccountProviderView_Day_0_en","features.login.impl.accountprovider_AccountProviderView_Night_0_en",0,], ["features.login.impl.accountprovider_AccountProviderView_Day_1_en","features.login.impl.accountprovider_AccountProviderView_Night_1_en",0,], ["features.login.impl.accountprovider_AccountProviderView_Day_2_en","features.login.impl.accountprovider_AccountProviderView_Night_2_en",0,], ["features.login.impl.accountprovider_AccountProviderView_Day_3_en","features.login.impl.accountprovider_AccountProviderView_Night_3_en",0,], ["features.login.impl.accountprovider_AccountProviderView_Day_4_en","features.login.impl.accountprovider_AccountProviderView_Night_4_en",0,], -["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",19972,], -["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",19972,], -["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",19972,], -["features.createroom.impl.addpeople_AddPeopleView_Day_3_en","features.createroom.impl.addpeople_AddPeopleView_Night_3_en",19972,], -["features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en",19972,], -["features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en",19972,], -["features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en",19972,], -["features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en",19972,], -["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",19972,], -["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",19972,], -["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",19972,], -["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",19972,], -["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",19972,], -["services.apperror.impl_AppErrorView_Day_0_en","services.apperror.impl_AppErrorView_Night_0_en",19972,], +["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",19983,], +["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",19983,], +["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",19983,], +["features.createroom.impl.addpeople_AddPeopleView_Day_3_en","features.createroom.impl.addpeople_AddPeopleView_Night_3_en",19983,], +["features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en",19983,], +["features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en",19983,], +["features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en",19983,], +["features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en",19983,], +["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",19983,], +["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",19983,], +["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",19983,], +["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",19983,], +["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",19983,], +["services.apperror.impl_AppErrorView_Day_0_en","services.apperror.impl_AppErrorView_Night_0_en",19983,], ["libraries.designsystem.components.async_AsyncActionView_Day_0_en","libraries.designsystem.components.async_AsyncActionView_Night_0_en",0,], -["libraries.designsystem.components.async_AsyncActionView_Day_1_en","libraries.designsystem.components.async_AsyncActionView_Night_1_en",19972,], +["libraries.designsystem.components.async_AsyncActionView_Day_1_en","libraries.designsystem.components.async_AsyncActionView_Night_1_en",19983,], ["libraries.designsystem.components.async_AsyncActionView_Day_2_en","libraries.designsystem.components.async_AsyncActionView_Night_2_en",0,], -["libraries.designsystem.components.async_AsyncActionView_Day_3_en","libraries.designsystem.components.async_AsyncActionView_Night_3_en",19972,], +["libraries.designsystem.components.async_AsyncActionView_Day_3_en","libraries.designsystem.components.async_AsyncActionView_Night_3_en",19983,], ["libraries.designsystem.components.async_AsyncActionView_Day_4_en","libraries.designsystem.components.async_AsyncActionView_Night_4_en",0,], -["libraries.designsystem.components.async_AsyncFailure_Day_0_en","libraries.designsystem.components.async_AsyncFailure_Night_0_en",19972,], +["libraries.designsystem.components.async_AsyncFailure_Day_0_en","libraries.designsystem.components.async_AsyncFailure_Night_0_en",19983,], ["libraries.designsystem.components.async_AsyncIndicatorFailure_Day_0_en","libraries.designsystem.components.async_AsyncIndicatorFailure_Night_0_en",0,], ["libraries.designsystem.components.async_AsyncIndicatorLoading_Day_0_en","libraries.designsystem.components.async_AsyncIndicatorLoading_Night_0_en",0,], ["libraries.designsystem.components.async_AsyncLoading_Day_0_en","libraries.designsystem.components.async_AsyncLoading_Night_0_en",0,], -["features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en","features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en",19972,], +["features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en","features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en",19983,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_0_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_0_en",0,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_1_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_1_en",0,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_2_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_2_en",0,], @@ -45,11 +45,11 @@ export const screenshots = [ ["libraries.matrix.ui.components_AttachmentThumbnail_Day_6_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_6_en",0,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_7_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_7_en",0,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_8_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_8_en",0,], -["features.messages.impl.attachments.preview_AttachmentsView_0_en","",19972,], -["features.messages.impl.attachments.preview_AttachmentsView_1_en","",19972,], -["features.messages.impl.attachments.preview_AttachmentsView_2_en","",19972,], -["features.messages.impl.attachments.preview_AttachmentsView_3_en","",19972,], -["libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en","libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en",19972,], +["features.messages.impl.attachments.preview_AttachmentsView_0_en","",19983,], +["features.messages.impl.attachments.preview_AttachmentsView_1_en","",19983,], +["features.messages.impl.attachments.preview_AttachmentsView_2_en","",19983,], +["features.messages.impl.attachments.preview_AttachmentsView_3_en","",19983,], +["libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en","libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en",19983,], ["libraries.designsystem.components.avatar_Avatar_Avatars_0_en","",0,], ["libraries.designsystem.components.avatar_Avatar_Avatars_10_en","",0,], ["libraries.designsystem.components.avatar_Avatar_Avatars_11_en","",0,], @@ -129,13 +129,13 @@ export const screenshots = [ ["libraries.designsystem.components_Badge_Day_0_en","libraries.designsystem.components_Badge_Night_0_en",0,], ["libraries.designsystem.components_BigCheckmark_Day_0_en","libraries.designsystem.components_BigCheckmark_Night_0_en",0,], ["libraries.designsystem.components_BigIcon_Day_0_en","libraries.designsystem.components_BigIcon_Night_0_en",0,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_0_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_0_en",19972,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",19972,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",19972,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",19972,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",19972,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",19972,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",19972,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_0_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_0_en",19983,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",19983,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",19983,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",19983,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",19983,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",19983,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",19983,], ["libraries.designsystem.components_BloomInitials_Day_0_en","libraries.designsystem.components_BloomInitials_Night_0_en",0,], ["libraries.designsystem.components_BloomInitials_Day_1_en","libraries.designsystem.components_BloomInitials_Night_1_en",0,], ["libraries.designsystem.components_BloomInitials_Day_2_en","libraries.designsystem.components_BloomInitials_Night_2_en",0,], @@ -146,87 +146,93 @@ export const screenshots = [ ["libraries.designsystem.components_BloomInitials_Day_7_en","libraries.designsystem.components_BloomInitials_Night_7_en",0,], ["libraries.designsystem.components_Bloom_Day_0_en","libraries.designsystem.components_Bloom_Night_0_en",0,], ["libraries.designsystem.theme.components_BottomSheetDragHandle_Day_0_en","libraries.designsystem.theme.components_BottomSheetDragHandle_Night_0_en",0,], -["features.rageshake.impl.bugreport_BugReportView_Day_0_en","features.rageshake.impl.bugreport_BugReportView_Night_0_en",19972,], -["features.rageshake.impl.bugreport_BugReportView_Day_1_en","features.rageshake.impl.bugreport_BugReportView_Night_1_en",19972,], -["features.rageshake.impl.bugreport_BugReportView_Day_2_en","features.rageshake.impl.bugreport_BugReportView_Night_2_en",19972,], -["features.rageshake.impl.bugreport_BugReportView_Day_3_en","features.rageshake.impl.bugreport_BugReportView_Night_3_en",19972,], -["features.rageshake.impl.bugreport_BugReportView_Day_4_en","features.rageshake.impl.bugreport_BugReportView_Night_4_en",19972,], +["features.rageshake.impl.bugreport_BugReportView_Day_0_en","features.rageshake.impl.bugreport_BugReportView_Night_0_en",19983,], +["features.rageshake.impl.bugreport_BugReportView_Day_1_en","features.rageshake.impl.bugreport_BugReportView_Night_1_en",19983,], +["features.rageshake.impl.bugreport_BugReportView_Day_2_en","features.rageshake.impl.bugreport_BugReportView_Night_2_en",19983,], +["features.rageshake.impl.bugreport_BugReportView_Day_3_en","features.rageshake.impl.bugreport_BugReportView_Night_3_en",19983,], +["features.rageshake.impl.bugreport_BugReportView_Day_4_en","features.rageshake.impl.bugreport_BugReportView_Night_4_en",19983,], ["libraries.designsystem.atomic.molecules_ButtonColumnMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ButtonColumnMolecule_Night_0_en",0,], ["libraries.designsystem.atomic.molecules_ButtonRowMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ButtonRowMolecule_Night_0_en",0,], ["features.call.impl.ui_CallScreenPipView_Day_0_en","features.call.impl.ui_CallScreenPipView_Night_0_en",0,], ["features.call.impl.ui_CallScreenPipView_Day_1_en","features.call.impl.ui_CallScreenPipView_Night_1_en",0,], ["features.call.impl.ui_CallScreenView_Day_0_en","features.call.impl.ui_CallScreenView_Night_0_en",0,], -["features.call.impl.ui_CallScreenView_Day_1_en","features.call.impl.ui_CallScreenView_Night_1_en",19972,], -["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",19972,], -["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",19972,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_0_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_0_en",19972,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_10_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_10_en",19972,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_1_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_1_en",19972,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_2_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_2_en",19972,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_3_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_3_en",19972,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_4_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_4_en",19972,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_5_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_5_en",19972,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_6_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_6_en",19972,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_7_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_7_en",19972,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_8_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_8_en",19972,], -["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_9_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_9_en",19972,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_0_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_0_en",19972,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_1_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_1_en",19972,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_2_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_2_en",19972,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_3_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_3_en",19972,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_4_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_4_en",19972,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_5_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_5_en",19972,], -["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_6_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_6_en",19972,], +["features.call.impl.ui_CallScreenView_Day_1_en","features.call.impl.ui_CallScreenView_Night_1_en",19983,], +["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",19983,], +["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",19983,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_0_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_0_en",19983,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_10_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_10_en",19983,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_1_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_1_en",19983,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_2_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_2_en",19983,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_3_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_3_en",19983,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_4_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_4_en",19983,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_5_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_5_en",19983,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_6_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_6_en",19983,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_7_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_7_en",19983,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_8_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_8_en",19983,], +["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_9_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_9_en",19983,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_0_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_0_en",19983,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_1_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_1_en",19983,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_2_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_2_en",19983,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_3_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_3_en",19983,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_4_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_4_en",19983,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_5_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_5_en",19983,], +["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_6_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_6_en",19983,], ["features.login.impl.changeserver_ChangeServerView_Day_0_en","features.login.impl.changeserver_ChangeServerView_Night_0_en",0,], -["features.login.impl.changeserver_ChangeServerView_Day_1_en","features.login.impl.changeserver_ChangeServerView_Night_1_en",19972,], -["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",19972,], +["features.login.impl.changeserver_ChangeServerView_Day_1_en","features.login.impl.changeserver_ChangeServerView_Night_1_en",19983,], +["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",19983,], ["libraries.matrix.ui.components_CheckableResolvedUserRow_en","",0,], -["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",19972,], +["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",19983,], ["libraries.designsystem.theme.components_Checkboxes_Toggles_en","",0,], ["libraries.designsystem.theme.components_CircularProgressIndicator_Progress Indicators_en","",0,], ["libraries.designsystem.components_ClickableLinkText_Text_en","",0,], ["libraries.designsystem.theme_ColorAliases_Day_0_en","libraries.designsystem.theme_ColorAliases_Night_0_en",0,], ["libraries.textcomposer.components_ComposerOptionsButton_Day_0_en","libraries.textcomposer.components_ComposerOptionsButton_Night_0_en",0,], ["libraries.designsystem.components.avatar_CompositeAvatar_Avatars_en","",0,], -["features.createroom.impl.configureroom_ConfigureRoomView_Day_0_en","features.createroom.impl.configureroom_ConfigureRoomView_Night_0_en",19972,], -["features.createroom.impl.configureroom_ConfigureRoomView_Day_1_en","features.createroom.impl.configureroom_ConfigureRoomView_Night_1_en",19972,], +["features.createroom.impl.configureroom_ConfigureRoomView_Day_0_en","features.createroom.impl.configureroom_ConfigureRoomView_Night_0_en",19983,], +["features.createroom.impl.configureroom_ConfigureRoomView_Day_1_en","features.createroom.impl.configureroom_ConfigureRoomView_Night_1_en",19983,], ["features.preferences.impl.developer.tracing_ConfigureTracingView_Day_0_en","features.preferences.impl.developer.tracing_ConfigureTracingView_Night_0_en",0,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",19972,], -["features.roomlist.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.roomlist.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",19972,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",19983,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en",19983,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en",19983,], +["features.roomlist.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.roomlist.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",19983,], ["libraries.designsystem.components.dialogs_ConfirmationDialogContent_Dialogs_en","",0,], ["libraries.designsystem.components.dialogs_ConfirmationDialog_Day_0_en","libraries.designsystem.components.dialogs_ConfirmationDialog_Night_0_en",0,], ["features.networkmonitor.api.ui_ConnectivityIndicatorView_Day_0_en","features.networkmonitor.api.ui_ConnectivityIndicatorView_Night_0_en",0,], -["features.rageshake.api.crash_CrashDetectionView_Day_0_en","features.rageshake.api.crash_CrashDetectionView_Night_0_en",19972,], -["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",19972,], -["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",19972,], -["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",19972,], -["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",19972,], -["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",19972,], -["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",19972,], -["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",19972,], -["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",19972,], -["features.createroom.impl.root_CreateRoomRootView_Day_0_en","features.createroom.impl.root_CreateRoomRootView_Night_0_en",19972,], -["features.createroom.impl.root_CreateRoomRootView_Day_1_en","features.createroom.impl.root_CreateRoomRootView_Night_1_en",19972,], -["features.createroom.impl.root_CreateRoomRootView_Day_2_en","features.createroom.impl.root_CreateRoomRootView_Night_2_en",19972,], -["features.createroom.impl.root_CreateRoomRootView_Day_3_en","features.createroom.impl.root_CreateRoomRootView_Night_3_en",19972,], -["libraries.designsystem.theme.components.previews_DatePickerDark_DateTime pickers_en","",19972,], -["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime pickers_en","",19972,], +["features.rageshake.api.crash_CrashDetectionView_Day_0_en","features.rageshake.api.crash_CrashDetectionView_Night_0_en",19983,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_0_en","features.login.impl.screens.createaccount_CreateAccountView_Night_0_en",19983,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_1_en","features.login.impl.screens.createaccount_CreateAccountView_Night_1_en",19983,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_2_en","features.login.impl.screens.createaccount_CreateAccountView_Night_2_en",19983,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_3_en","features.login.impl.screens.createaccount_CreateAccountView_Night_3_en",19983,], +["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",19983,], +["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",19983,], +["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",19983,], +["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",19983,], +["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",19983,], +["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",19983,], +["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",19983,], +["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",19983,], +["features.createroom.impl.root_CreateRoomRootView_Day_0_en","features.createroom.impl.root_CreateRoomRootView_Night_0_en",19983,], +["features.createroom.impl.root_CreateRoomRootView_Day_1_en","features.createroom.impl.root_CreateRoomRootView_Night_1_en",19983,], +["features.createroom.impl.root_CreateRoomRootView_Day_2_en","features.createroom.impl.root_CreateRoomRootView_Night_2_en",19983,], +["features.createroom.impl.root_CreateRoomRootView_Day_3_en","features.createroom.impl.root_CreateRoomRootView_Night_3_en",19983,], +["libraries.designsystem.theme.components.previews_DatePickerDark_DateTime pickers_en","",19983,], +["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime pickers_en","",19983,], ["features.logout.impl.direct_DefaultDirectLogoutView_Day_0_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_0_en",0,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_1_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_1_en",19972,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",19972,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",19972,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_1_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_1_en",19983,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",19983,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",19983,], ["features.logout.impl.direct_DefaultDirectLogoutView_Day_4_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_4_en",0,], -["features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en","features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en",19972,], -["features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_en","features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Night_0_en",19972,], -["features.roomlist.impl.components_DefaultRoomListTopBar_Day_0_en","features.roomlist.impl.components_DefaultRoomListTopBar_Night_0_en",19972,], +["features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en","features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en",19983,], +["features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_en","features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Night_0_en",19983,], +["features.roomlist.impl.components_DefaultRoomListTopBar_Day_0_en","features.roomlist.impl.components_DefaultRoomListTopBar_Night_0_en",19983,], ["features.licenses.impl.details_DependenciesDetailsView_Day_0_en","features.licenses.impl.details_DependenciesDetailsView_Night_0_en",0,], -["features.licenses.impl.list_DependencyLicensesListView_Day_0_en","features.licenses.impl.list_DependencyLicensesListView_Night_0_en",19975,], -["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",19975,], -["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",19975,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",19972,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",19972,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",19972,], -["libraries.designsystem.atomic.molecules_DialogLikeBannerMolecule_Day_0_en","libraries.designsystem.atomic.molecules_DialogLikeBannerMolecule_Night_0_en",19972,], +["features.licenses.impl.list_DependencyLicensesListView_Day_0_en","features.licenses.impl.list_DependencyLicensesListView_Night_0_en",19983,], +["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",19983,], +["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",19983,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",19983,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",19983,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",19983,], +["libraries.designsystem.atomic.molecules_DialogLikeBannerMolecule_Day_0_en","libraries.designsystem.atomic.molecules_DialogLikeBannerMolecule_Night_0_en",19983,], ["libraries.designsystem.theme.components_DialogWithDestructiveButton_Dialog with destructive button_Dialogs_en","",0,], ["libraries.designsystem.theme.components_DialogWithOnlyMessageAndOkButton_Dialog with only message and ok button_Dialogs_en","",0,], ["libraries.designsystem.theme.components_DialogWithThirdButton_Dialog with third button_Dialogs_en","",0,], @@ -238,12 +244,12 @@ export const screenshots = [ ["libraries.designsystem.text_DpScale_1_0f__en","",0,], ["libraries.designsystem.text_DpScale_1_5f__en","",0,], ["libraries.designsystem.theme.components_DropdownMenuItem_Menus_en","",0,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en",19972,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",19972,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",19972,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",19972,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",19972,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",19972,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en",19983,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",19983,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",19983,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",19983,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",19983,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",19983,], ["libraries.matrix.ui.components_EditableAvatarView_Day_0_en","libraries.matrix.ui.components_EditableAvatarView_Night_0_en",0,], ["libraries.matrix.ui.components_EditableAvatarView_Day_1_en","libraries.matrix.ui.components_EditableAvatarView_Night_1_en",0,], ["libraries.matrix.ui.components_EditableAvatarView_Day_2_en","libraries.matrix.ui.components_EditableAvatarView_Night_2_en",0,], @@ -253,10 +259,9 @@ export const screenshots = [ ["libraries.designsystem.atomic.atoms_ElementLogoAtomMedium_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomMedium_Night_0_en",0,], ["features.messages.impl.timeline.components.customreaction_EmojiItem_Day_0_en","features.messages.impl.timeline.components.customreaction_EmojiItem_Night_0_en",0,], ["features.messages.impl.timeline.components.customreaction_EmojiPicker_Day_0_en","features.messages.impl.timeline.components.customreaction_EmojiPicker_Night_0_en",0,], -["features.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_Day_0_en","features.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_Night_0_en",19972,], -["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",19972,], -["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",19972,], -["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",19972,], +["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",19983,], +["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",19983,], +["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",19983,], ["features.messages.impl.timeline.debug_EventDebugInfoView_Day_0_en","features.messages.impl.timeline.debug_EventDebugInfoView_Night_0_en",0,], ["libraries.featureflag.ui_FeatureListView_Day_0_en","libraries.featureflag.ui_FeatureListView_Night_0_en",0,], ["libraries.designsystem.theme.components_FilledButtonLargeLowPadding_Buttons_en","",0,], @@ -267,15 +272,15 @@ export const screenshots = [ ["libraries.designsystem.theme.components_FloatingActionButton_Floating Action Buttons_en","",0,], ["libraries.designsystem.atomic.pages_FlowStepPage_Day_0_en","libraries.designsystem.atomic.pages_FlowStepPage_Night_0_en",0,], ["features.messages.impl.timeline.focus_FocusRequestStateView_Day_0_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_0_en",0,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_1_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_1_en",19972,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",19972,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",19972,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_1_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_1_en",19983,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",19983,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",19983,], ["libraries.textcomposer.components_FormattingOption_Day_0_en","libraries.textcomposer.components_FormattingOption_Night_0_en",0,], ["features.messages.impl.forward_ForwardMessagesView_Day_0_en","features.messages.impl.forward_ForwardMessagesView_Night_0_en",0,], ["features.messages.impl.forward_ForwardMessagesView_Day_1_en","features.messages.impl.forward_ForwardMessagesView_Night_1_en",0,], ["features.messages.impl.forward_ForwardMessagesView_Day_2_en","features.messages.impl.forward_ForwardMessagesView_Night_2_en",0,], -["features.messages.impl.forward_ForwardMessagesView_Day_3_en","features.messages.impl.forward_ForwardMessagesView_Night_3_en",19972,], -["features.roomlist.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.roomlist.impl.components_FullScreenIntentPermissionBanner_Night_0_en",19972,], +["features.messages.impl.forward_ForwardMessagesView_Day_3_en","features.messages.impl.forward_ForwardMessagesView_Night_3_en",19983,], +["features.roomlist.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.roomlist.impl.components_FullScreenIntentPermissionBanner_Night_0_en",19983,], ["libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Day_0_en","libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Night_0_en",0,], ["libraries.designsystem.components.button_GradientFloatingActionButton_Day_0_en","libraries.designsystem.components.button_GradientFloatingActionButton_Night_0_en",0,], ["features.messages.impl.timeline.components.group_GroupHeaderView_Day_0_en","features.messages.impl.timeline.components.group_GroupHeaderView_Night_0_en",0,], @@ -302,37 +307,37 @@ export const screenshots = [ ["libraries.matrix.ui.messages.reply_InReplyToView_Day_1_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_1_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_2_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_2_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_3_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_3_en",0,], -["libraries.matrix.ui.messages.reply_InReplyToView_Day_4_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_4_en",19972,], +["libraries.matrix.ui.messages.reply_InReplyToView_Day_4_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_4_en",19983,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_5_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_5_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_6_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_6_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_7_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_7_en",0,], -["libraries.matrix.ui.messages.reply_InReplyToView_Day_8_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_8_en",19972,], +["libraries.matrix.ui.messages.reply_InReplyToView_Day_8_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_8_en",19983,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_9_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_9_en",0,], -["features.call.impl.ui_IncomingCallScreen_Day_0_en","features.call.impl.ui_IncomingCallScreen_Night_0_en",19972,], +["features.call.impl.ui_IncomingCallScreen_Day_0_en","features.call.impl.ui_IncomingCallScreen_Night_0_en",19983,], ["libraries.designsystem.atomic.molecules_InfoListItemMolecule_Day_0_en","libraries.designsystem.atomic.molecules_InfoListItemMolecule_Night_0_en",0,], ["libraries.designsystem.atomic.organisms_InfoListOrganism_Day_0_en","libraries.designsystem.atomic.organisms_InfoListOrganism_Night_0_en",0,], -["libraries.matrix.ui.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",19972,], +["libraries.matrix.ui.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",19983,], ["features.joinroom.impl_JoinRoomView_Day_0_en","features.joinroom.impl_JoinRoomView_Night_0_en",0,], ["features.joinroom.impl_JoinRoomView_Day_10_en","features.joinroom.impl_JoinRoomView_Night_10_en",0,], -["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",19972,], -["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",19972,], -["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",19972,], -["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",19972,], -["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",19972,], -["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",19972,], -["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",19972,], -["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",19972,], -["features.joinroom.impl_JoinRoomView_Day_9_en","features.joinroom.impl_JoinRoomView_Night_9_en",19972,], +["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",19983,], +["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",19983,], +["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",19983,], +["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",19983,], +["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",19983,], +["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",19983,], +["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",19983,], +["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",19983,], +["features.joinroom.impl_JoinRoomView_Day_9_en","features.joinroom.impl_JoinRoomView_Night_9_en",19983,], ["libraries.designsystem.components_LabelledCheckbox_Toggles_en","",0,], ["libraries.designsystem.components_LabelledOutlinedTextField_Day_0_en","libraries.designsystem.components_LabelledOutlinedTextField_Night_0_en",0,], ["libraries.designsystem.components_LabelledTextField_Day_0_en","libraries.designsystem.components_LabelledTextField_Night_0_en",0,], ["features.leaveroom.api_LeaveRoomView_Day_0_en","features.leaveroom.api_LeaveRoomView_Night_0_en",0,], -["features.leaveroom.api_LeaveRoomView_Day_1_en","features.leaveroom.api_LeaveRoomView_Night_1_en",19972,], -["features.leaveroom.api_LeaveRoomView_Day_2_en","features.leaveroom.api_LeaveRoomView_Night_2_en",19972,], -["features.leaveroom.api_LeaveRoomView_Day_3_en","features.leaveroom.api_LeaveRoomView_Night_3_en",19972,], -["features.leaveroom.api_LeaveRoomView_Day_4_en","features.leaveroom.api_LeaveRoomView_Night_4_en",19972,], -["features.leaveroom.api_LeaveRoomView_Day_5_en","features.leaveroom.api_LeaveRoomView_Night_5_en",19972,], -["features.leaveroom.api_LeaveRoomView_Day_6_en","features.leaveroom.api_LeaveRoomView_Night_6_en",19972,], +["features.leaveroom.api_LeaveRoomView_Day_1_en","features.leaveroom.api_LeaveRoomView_Night_1_en",19983,], +["features.leaveroom.api_LeaveRoomView_Day_2_en","features.leaveroom.api_LeaveRoomView_Night_2_en",19983,], +["features.leaveroom.api_LeaveRoomView_Day_3_en","features.leaveroom.api_LeaveRoomView_Night_3_en",19983,], +["features.leaveroom.api_LeaveRoomView_Day_4_en","features.leaveroom.api_LeaveRoomView_Night_4_en",19983,], +["features.leaveroom.api_LeaveRoomView_Day_5_en","features.leaveroom.api_LeaveRoomView_Night_5_en",19983,], +["features.leaveroom.api_LeaveRoomView_Day_6_en","features.leaveroom.api_LeaveRoomView_Night_6_en",19983,], ["libraries.designsystem.background_LightGradientBackground_Day_0_en","libraries.designsystem.background_LightGradientBackground_Night_0_en",0,], ["libraries.designsystem.theme.components_LinearProgressIndicator_Progress Indicators_en","",0,], ["libraries.designsystem.components.dialogs_ListDialogContent_Dialogs_en","",0,], @@ -383,28 +388,29 @@ export const screenshots = [ ["libraries.designsystem.theme.components_ListSupportingTextSmallPadding_List supporting text - small padding_List sections_en","",0,], ["libraries.textcomposer.components_LiveWaveformView_Day_0_en","libraries.textcomposer.components_LiveWaveformView_Night_0_en",0,], ["appnav.room.joined_LoadingRoomNodeView_Day_0_en","appnav.room.joined_LoadingRoomNodeView_Night_0_en",0,], -["appnav.room.joined_LoadingRoomNodeView_Day_1_en","appnav.room.joined_LoadingRoomNodeView_Night_1_en",19972,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en",19972,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",19972,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",19972,], +["appnav.room.joined_LoadingRoomNodeView_Day_1_en","appnav.room.joined_LoadingRoomNodeView_Night_1_en",19983,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en",19983,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",19983,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",19983,], ["appnav.loggedin_LoggedInView_Day_0_en","appnav.loggedin_LoggedInView_Night_0_en",0,], -["appnav.loggedin_LoggedInView_Day_1_en","appnav.loggedin_LoggedInView_Night_1_en",19972,], -["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",19972,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",19972,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",19972,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",19972,], -["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",19972,], -["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",19972,], -["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",19972,], -["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",19972,], -["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",19972,], -["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",19972,], -["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",19972,], -["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",19972,], -["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",19972,], -["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",19972,], +["appnav.loggedin_LoggedInView_Day_1_en","appnav.loggedin_LoggedInView_Night_1_en",19983,], +["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",19983,], +["appnav.loggedin_LoggedInView_Day_3_en","appnav.loggedin_LoggedInView_Night_3_en",19983,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",19983,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",19983,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",19983,], +["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",19983,], +["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",19983,], +["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",19983,], +["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",19983,], +["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",19983,], +["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",19983,], +["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",19983,], +["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",19983,], +["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",19983,], +["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",19983,], ["libraries.designsystem.components.button_MainActionButton_Buttons_en","",0,], -["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",19972,], +["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",19983,], ["libraries.textcomposer.components.markdown_MarkdownTextInput_Day_0_en","libraries.textcomposer.components.markdown_MarkdownTextInput_Night_0_en",0,], ["libraries.matrix.ui.components_MatrixUserHeaderPlaceholder_Day_0_en","libraries.matrix.ui.components_MatrixUserHeaderPlaceholder_Night_0_en",0,], ["libraries.matrix.ui.components_MatrixUserHeader_Day_0_en","libraries.matrix.ui.components_MatrixUserHeader_Night_0_en",0,], @@ -414,7 +420,7 @@ export const screenshots = [ ["libraries.mediaviewer.api.viewer_MediaViewerView_0_en","",0,], ["libraries.mediaviewer.api.viewer_MediaViewerView_10_en","",0,], ["libraries.mediaviewer.api.viewer_MediaViewerView_1_en","",0,], -["libraries.mediaviewer.api.viewer_MediaViewerView_2_en","",19972,], +["libraries.mediaviewer.api.viewer_MediaViewerView_2_en","",19983,], ["libraries.mediaviewer.api.viewer_MediaViewerView_3_en","",0,], ["libraries.mediaviewer.api.viewer_MediaViewerView_4_en","",0,], ["libraries.mediaviewer.api.viewer_MediaViewerView_5_en","",0,], @@ -426,7 +432,7 @@ export const screenshots = [ ["libraries.textcomposer.mentions_MentionSpanTheme_Day_0_en","libraries.textcomposer.mentions_MentionSpanTheme_Night_0_en",0,], ["libraries.designsystem.theme.components.previews_Menu_Menus_en","",0,], ["features.messages.impl.messagecomposer_MessageComposerViewVoice_Day_0_en","features.messages.impl.messagecomposer_MessageComposerViewVoice_Night_0_en",0,], -["features.messages.impl.messagecomposer_MessageComposerView_Day_0_en","features.messages.impl.messagecomposer_MessageComposerView_Night_0_en",19972,], +["features.messages.impl.messagecomposer_MessageComposerView_Day_0_en","features.messages.impl.messagecomposer_MessageComposerView_Night_0_en",19983,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_0_en","features.messages.impl.timeline.components_MessageEventBubble_Night_0_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_10_en","features.messages.impl.timeline.components_MessageEventBubble_Night_10_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_11_en","features.messages.impl.timeline.components_MessageEventBubble_Night_11_en",0,], @@ -443,7 +449,7 @@ export const screenshots = [ ["features.messages.impl.timeline.components_MessageEventBubble_Day_7_en","features.messages.impl.timeline.components_MessageEventBubble_Night_7_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_8_en","features.messages.impl.timeline.components_MessageEventBubble_Night_8_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_9_en","features.messages.impl.timeline.components_MessageEventBubble_Night_9_en",0,], -["features.messages.impl.timeline.components_MessageShieldView_Day_0_en","features.messages.impl.timeline.components_MessageShieldView_Night_0_en",19972,], +["features.messages.impl.timeline.components_MessageShieldView_Day_0_en","features.messages.impl.timeline.components_MessageShieldView_Night_0_en",19983,], ["features.messages.impl.timeline.components_MessageStateEventContainer_Day_0_en","features.messages.impl.timeline.components_MessageStateEventContainer_Night_0_en",0,], ["features.messages.impl.timeline.components_MessagesReactionButtonAdd_Day_0_en","features.messages.impl.timeline.components_MessagesReactionButtonAdd_Night_0_en",0,], ["features.messages.impl.timeline.components_MessagesReactionButtonExtra_Day_0_en","features.messages.impl.timeline.components_MessagesReactionButtonExtra_Night_0_en",0,], @@ -451,25 +457,25 @@ export const screenshots = [ ["features.messages.impl.timeline.components_MessagesReactionButton_Day_1_en","features.messages.impl.timeline.components_MessagesReactionButton_Night_1_en",0,], ["features.messages.impl.timeline.components_MessagesReactionButton_Day_2_en","features.messages.impl.timeline.components_MessagesReactionButton_Night_2_en",0,], ["features.messages.impl.timeline.components_MessagesReactionButton_Day_3_en","features.messages.impl.timeline.components_MessagesReactionButton_Night_3_en",0,], -["features.messages.impl.typing_MessagesViewWithTyping_Day_0_en","features.messages.impl.typing_MessagesViewWithTyping_Night_0_en",19972,], -["features.messages.impl.typing_MessagesViewWithTyping_Day_1_en","features.messages.impl.typing_MessagesViewWithTyping_Night_1_en",19972,], -["features.messages.impl.typing_MessagesViewWithTyping_Day_2_en","features.messages.impl.typing_MessagesViewWithTyping_Night_2_en",19972,], -["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",19972,], -["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",19972,], -["features.messages.impl_MessagesView_Day_11_en","features.messages.impl_MessagesView_Night_11_en",19972,], -["features.messages.impl_MessagesView_Day_12_en","features.messages.impl_MessagesView_Night_12_en",19972,], -["features.messages.impl_MessagesView_Day_13_en","features.messages.impl_MessagesView_Night_13_en",19972,], -["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",19972,], -["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",19972,], -["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",19972,], -["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",19972,], -["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",19972,], -["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",19972,], -["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",19972,], -["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",19972,], -["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",19972,], +["features.messages.impl.typing_MessagesViewWithTyping_Day_0_en","features.messages.impl.typing_MessagesViewWithTyping_Night_0_en",19983,], +["features.messages.impl.typing_MessagesViewWithTyping_Day_1_en","features.messages.impl.typing_MessagesViewWithTyping_Night_1_en",19983,], +["features.messages.impl.typing_MessagesViewWithTyping_Day_2_en","features.messages.impl.typing_MessagesViewWithTyping_Night_2_en",19983,], +["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",19983,], +["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",19983,], +["features.messages.impl_MessagesView_Day_11_en","features.messages.impl_MessagesView_Night_11_en",19983,], +["features.messages.impl_MessagesView_Day_12_en","features.messages.impl_MessagesView_Night_12_en",19983,], +["features.messages.impl_MessagesView_Day_13_en","features.messages.impl_MessagesView_Night_13_en",19983,], +["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",19983,], +["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",19983,], +["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",19983,], +["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",19983,], +["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",19983,], +["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",19983,], +["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",19983,], +["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",19983,], +["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",19983,], ["features.migration.impl_MigrationView_Day_0_en","features.migration.impl_MigrationView_Night_0_en",0,], -["features.migration.impl_MigrationView_Day_1_en","features.migration.impl_MigrationView_Night_1_en",19972,], +["features.migration.impl_MigrationView_Day_1_en","features.migration.impl_MigrationView_Night_1_en",19983,], ["libraries.designsystem.theme.components_ModalBottomSheetDark_Bottom Sheets_en","",0,], ["libraries.designsystem.theme.components_ModalBottomSheetLight_Bottom Sheets_en","",0,], ["appicon.element_MonochromeIcon_en","",0,], @@ -478,28 +484,29 @@ export const screenshots = [ ["libraries.designsystem.components.list_MutipleSelectionListItemSelectedTrailingContent_Multiple selection List item - selection in trailing content_List items_en","",0,], ["libraries.designsystem.components.list_MutipleSelectionListItemSelected_Multiple selection List item - selection in supporting text_List items_en","",0,], ["libraries.designsystem.components.list_MutipleSelectionListItem_Multiple selection List item - no selection_List items_en","",0,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",19972,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",19972,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",19972,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",19972,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",19972,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",19972,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",19972,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",19972,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",19972,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",19972,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",19972,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",19972,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",19972,], -["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",19972,], +["features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en","features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en",19983,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",19983,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",19983,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",19983,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",19983,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",19983,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",19983,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",19983,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",19983,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",19983,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",19983,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",19983,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",19983,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",19983,], +["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",19983,], ["libraries.oidc.impl.webview_OidcView_Day_0_en","libraries.oidc.impl.webview_OidcView_Night_0_en",0,], ["libraries.oidc.impl.webview_OidcView_Day_1_en","libraries.oidc.impl.webview_OidcView_Night_1_en",0,], ["libraries.designsystem.atomic.pages_OnBoardingPage_Day_0_en","libraries.designsystem.atomic.pages_OnBoardingPage_Night_0_en",0,], -["features.onboarding.impl_OnBoardingView_Day_0_en","features.onboarding.impl_OnBoardingView_Night_0_en",19972,], -["features.onboarding.impl_OnBoardingView_Day_1_en","features.onboarding.impl_OnBoardingView_Night_1_en",19972,], -["features.onboarding.impl_OnBoardingView_Day_2_en","features.onboarding.impl_OnBoardingView_Night_2_en",19972,], -["features.onboarding.impl_OnBoardingView_Day_3_en","features.onboarding.impl_OnBoardingView_Night_3_en",19972,], -["features.onboarding.impl_OnBoardingView_Day_4_en","features.onboarding.impl_OnBoardingView_Night_4_en",19972,], +["features.onboarding.impl_OnBoardingView_Day_0_en","features.onboarding.impl_OnBoardingView_Night_0_en",19983,], +["features.onboarding.impl_OnBoardingView_Day_1_en","features.onboarding.impl_OnBoardingView_Night_1_en",19983,], +["features.onboarding.impl_OnBoardingView_Day_2_en","features.onboarding.impl_OnBoardingView_Night_2_en",19983,], +["features.onboarding.impl_OnBoardingView_Day_3_en","features.onboarding.impl_OnBoardingView_Night_3_en",19983,], +["features.onboarding.impl_OnBoardingView_Day_4_en","features.onboarding.impl_OnBoardingView_Night_4_en",19983,], ["libraries.designsystem.background_OnboardingBackground_Day_0_en","libraries.designsystem.background_OnboardingBackground_Night_0_en",0,], ["libraries.designsystem.theme.components_OutlinedButtonLargeLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonLarge_Buttons_en","",0,], @@ -514,62 +521,62 @@ export const screenshots = [ ["libraries.designsystem.components_PageTitleWithIconFull_Day_3_en","libraries.designsystem.components_PageTitleWithIconFull_Night_3_en",0,], ["libraries.designsystem.components_PageTitleWithIconFull_Day_4_en","libraries.designsystem.components_PageTitleWithIconFull_Night_4_en",0,], ["libraries.designsystem.components_PageTitleWithIconMinimal_Day_0_en","libraries.designsystem.components_PageTitleWithIconMinimal_Night_0_en",0,], -["features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_en","features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Night_0_en",19972,], -["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",19972,], -["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",19972,], -["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",19972,], -["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",19972,], +["features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_en","features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Night_0_en",19983,], +["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",19983,], +["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",19983,], +["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",19983,], +["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",19983,], ["features.lockscreen.impl.components_PinEntryTextField_Day_0_en","features.lockscreen.impl.components_PinEntryTextField_Night_0_en",0,], ["libraries.designsystem.components_PinIcon_Day_0_en","libraries.designsystem.components_PinIcon_Night_0_en",0,], ["features.lockscreen.impl.unlock.keypad_PinKeypad_Day_0_en","features.lockscreen.impl.unlock.keypad_PinKeypad_Night_0_en",0,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en",19972,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",19972,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",19972,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",19972,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",19972,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",19972,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",19972,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",19972,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",19972,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",19972,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",19972,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",19972,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",19972,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",19972,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en",19983,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",19983,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",19983,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",19983,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",19983,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",19983,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",19983,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",19983,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",19983,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",19983,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",19983,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",19983,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",19983,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",19983,], ["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_0_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_0_en",0,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_10_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_10_en",19975,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",0,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",19972,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",19972,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",19972,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",19972,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",19972,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",19972,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",19972,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",19972,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",19975,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",0,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",0,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",0,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_10_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_10_en",19983,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",19983,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",19983,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",19983,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",19983,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",19983,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",19983,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",19983,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",19983,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",19983,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",19983,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",19983,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",19983,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",19983,], ["libraries.designsystem.atomic.atoms_PlaceholderAtom_Day_0_en","libraries.designsystem.atomic.atoms_PlaceholderAtom_Night_0_en",0,], -["features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en",19972,], -["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",19972,], -["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",19972,], -["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",19972,], -["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",19972,], +["features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en",19983,], +["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",19983,], +["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",19983,], +["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",19983,], +["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",19983,], ["features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Night_0_en",0,], ["features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Night_0_en",0,], -["features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en",19972,], -["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",19972,], -["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",19972,], -["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",19972,], -["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",19972,], -["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",19972,], -["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",19972,], -["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",19972,], -["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",19972,], -["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",19972,], -["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",19972,], +["features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en",19983,], +["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",19983,], +["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",19983,], +["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",19983,], +["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",19983,], +["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",19983,], +["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",19983,], +["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",19983,], +["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",19983,], +["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",19983,], +["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",19983,], ["features.poll.api.pollcontent_PollTitleView_Day_0_en","features.poll.api.pollcontent_PollTitleView_Night_0_en",0,], ["libraries.designsystem.components.preferences_PreferenceCategory_Preferences_en","",0,], ["libraries.designsystem.components.preferences_PreferenceCheckbox_Preferences_en","",0,], @@ -586,191 +593,195 @@ export const screenshots = [ ["libraries.designsystem.components.preferences_PreferenceTextLight_Preferences_en","",0,], ["libraries.designsystem.components.preferences_PreferenceTextWithEndBadgeDark_Preferences_en","",0,], ["libraries.designsystem.components.preferences_PreferenceTextWithEndBadgeLight_Preferences_en","",0,], -["features.preferences.impl.root_PreferencesRootViewDark_0_en","",19972,], -["features.preferences.impl.root_PreferencesRootViewDark_1_en","",19972,], -["features.preferences.impl.root_PreferencesRootViewLight_0_en","",19972,], -["features.preferences.impl.root_PreferencesRootViewLight_1_en","",19972,], +["features.preferences.impl.root_PreferencesRootViewDark_0_en","",19983,], +["features.preferences.impl.root_PreferencesRootViewDark_1_en","",19983,], +["features.preferences.impl.root_PreferencesRootViewLight_0_en","",19983,], +["features.preferences.impl.root_PreferencesRootViewLight_1_en","",19983,], ["features.messages.impl.timeline.components.event_ProgressButton_Day_0_en","features.messages.impl.timeline.components.event_ProgressButton_Night_0_en",0,], -["libraries.designsystem.components_ProgressDialogContent_Dialogs_en","",19972,], -["libraries.designsystem.components_ProgressDialog_Day_0_en","libraries.designsystem.components_ProgressDialog_Night_0_en",19972,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",19972,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",19972,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",19972,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",19972,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",19972,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",19972,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",19972,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",19972,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",19972,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",19972,], -["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",19972,], -["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",19972,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",19972,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",19972,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",19972,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",19972,], +["libraries.designsystem.components_ProgressDialogContent_Dialogs_en","",19983,], +["libraries.designsystem.components_ProgressDialog_Day_0_en","libraries.designsystem.components_ProgressDialog_Night_0_en",19983,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",19983,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",19983,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",19983,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",19983,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",19983,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",19983,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",19983,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",19983,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",19983,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",19983,], +["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",19983,], +["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",19983,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",19983,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",19983,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",19983,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",19983,], ["libraries.designsystem.theme.components_RadioButton_Toggles_en","",0,], -["features.rageshake.api.detection_RageshakeDialogContent_Day_0_en","features.rageshake.api.detection_RageshakeDialogContent_Night_0_en",19972,], -["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",19972,], +["features.rageshake.api.detection_RageshakeDialogContent_Day_0_en","features.rageshake.api.detection_RageshakeDialogContent_Night_0_en",19983,], +["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",19983,], ["features.rageshake.api.preferences_RageshakePreferencesView_Day_1_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_1_en",0,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en",19972,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",19972,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",19972,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",19972,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",19972,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",19972,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",19972,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",19972,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",19972,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",19975,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",19975,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",19972,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",19972,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",19972,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",19972,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",19972,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",19972,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",19972,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",19972,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",19972,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en",19983,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",19983,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",19983,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",19983,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",19983,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",19983,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",19983,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",19983,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",19983,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",19983,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",19983,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",19983,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",19983,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",19983,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",19983,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",19983,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",19983,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",19983,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",19983,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",19983,], ["libraries.designsystem.atomic.atoms_RedIndicatorAtom_Day_0_en","libraries.designsystem.atomic.atoms_RedIndicatorAtom_Night_0_en",0,], ["features.messages.impl.timeline.components_ReplySwipeIndicator_Day_0_en","features.messages.impl.timeline.components_ReplySwipeIndicator_Night_0_en",0,], -["features.messages.impl.report_ReportMessageView_Day_0_en","features.messages.impl.report_ReportMessageView_Night_0_en",19972,], -["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",19972,], -["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",19972,], -["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",19972,], -["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",19972,], -["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",19972,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",19972,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",19972,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",19972,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",19972,], +["features.messages.impl.report_ReportMessageView_Day_0_en","features.messages.impl.report_ReportMessageView_Night_0_en",19983,], +["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",19983,], +["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",19983,], +["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",19983,], +["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",19983,], +["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",19983,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",19983,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",19983,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",19983,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",19983,], ["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en",0,], -["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",19972,], -["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",19972,], -["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",19972,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_0_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_0_en",19972,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_1_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_1_en",19972,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_2_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_2_en",19972,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_3_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_3_en",19972,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_4_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_4_en",19972,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_5_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_5_en",19972,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_6_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_6_en",19972,], -["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_7_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_7_en",19972,], +["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",19983,], +["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_0_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_0_en",0,], +["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en",19983,], +["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en",19983,], +["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",19983,], +["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",19983,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_0_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_0_en",19983,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_1_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_1_en",19983,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_2_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_2_en",19983,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_3_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_3_en",19983,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_4_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_4_en",19983,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_5_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_5_en",19983,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_6_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_6_en",19983,], +["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_7_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_7_en",19983,], ["features.roomaliasresolver.impl_RoomAliasResolverView_Day_0_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_0_en",0,], ["features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_1_en",0,], -["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",19972,], +["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",19983,], ["features.roomdetails.impl.components_RoomBadgeNegative_Day_0_en","features.roomdetails.impl.components_RoomBadgeNegative_Night_0_en",0,], ["features.roomdetails.impl.components_RoomBadgeNeutral_Day_0_en","features.roomdetails.impl.components_RoomBadgeNeutral_Night_0_en",0,], ["features.roomdetails.impl.components_RoomBadgePositive_Day_0_en","features.roomdetails.impl.components_RoomBadgePositive_Night_0_en",0,], -["features.roomdetails.impl_RoomDetailsDark_0_en","",19972,], -["features.roomdetails.impl_RoomDetailsDark_10_en","",19972,], -["features.roomdetails.impl_RoomDetailsDark_11_en","",19972,], -["features.roomdetails.impl_RoomDetailsDark_12_en","",19972,], -["features.roomdetails.impl_RoomDetailsDark_13_en","",19975,], -["features.roomdetails.impl_RoomDetailsDark_1_en","",19972,], -["features.roomdetails.impl_RoomDetailsDark_2_en","",19972,], -["features.roomdetails.impl_RoomDetailsDark_3_en","",19972,], -["features.roomdetails.impl_RoomDetailsDark_4_en","",19972,], -["features.roomdetails.impl_RoomDetailsDark_5_en","",19972,], -["features.roomdetails.impl_RoomDetailsDark_6_en","",19972,], -["features.roomdetails.impl_RoomDetailsDark_7_en","",19972,], -["features.roomdetails.impl_RoomDetailsDark_8_en","",19972,], -["features.roomdetails.impl_RoomDetailsDark_9_en","",19972,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_0_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_0_en",19972,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_1_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_1_en",19972,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_2_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_2_en",19972,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_3_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_3_en",19972,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_4_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_4_en",19972,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_5_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_5_en",19972,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_6_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_6_en",19972,], -["features.roomdetails.impl.edit_RoomDetailsEditView_Day_7_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_7_en",19972,], -["features.roomdetails.impl_RoomDetails_0_en","",19972,], -["features.roomdetails.impl_RoomDetails_10_en","",19972,], -["features.roomdetails.impl_RoomDetails_11_en","",19972,], -["features.roomdetails.impl_RoomDetails_12_en","",19972,], -["features.roomdetails.impl_RoomDetails_13_en","",19975,], -["features.roomdetails.impl_RoomDetails_1_en","",19972,], -["features.roomdetails.impl_RoomDetails_2_en","",19972,], -["features.roomdetails.impl_RoomDetails_3_en","",19972,], -["features.roomdetails.impl_RoomDetails_4_en","",19972,], -["features.roomdetails.impl_RoomDetails_5_en","",19972,], -["features.roomdetails.impl_RoomDetails_6_en","",19972,], -["features.roomdetails.impl_RoomDetails_7_en","",19972,], -["features.roomdetails.impl_RoomDetails_8_en","",19972,], -["features.roomdetails.impl_RoomDetails_9_en","",19972,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",19972,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",19972,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",19972,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",19972,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",19972,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",19972,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en",19972,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_4_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_4_en",19972,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_5_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_5_en",19972,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_6_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_6_en",19972,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_7_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_7_en",19972,], -["features.roomlist.impl.components_RoomListContentView_Day_0_en","features.roomlist.impl.components_RoomListContentView_Night_0_en",19972,], -["features.roomlist.impl.components_RoomListContentView_Day_1_en","features.roomlist.impl.components_RoomListContentView_Night_1_en",19972,], +["features.roomdetails.impl_RoomDetailsDark_0_en","",19983,], +["features.roomdetails.impl_RoomDetailsDark_10_en","",19983,], +["features.roomdetails.impl_RoomDetailsDark_11_en","",19983,], +["features.roomdetails.impl_RoomDetailsDark_12_en","",19983,], +["features.roomdetails.impl_RoomDetailsDark_13_en","",19983,], +["features.roomdetails.impl_RoomDetailsDark_1_en","",19983,], +["features.roomdetails.impl_RoomDetailsDark_2_en","",19983,], +["features.roomdetails.impl_RoomDetailsDark_3_en","",19983,], +["features.roomdetails.impl_RoomDetailsDark_4_en","",19983,], +["features.roomdetails.impl_RoomDetailsDark_5_en","",19983,], +["features.roomdetails.impl_RoomDetailsDark_6_en","",19983,], +["features.roomdetails.impl_RoomDetailsDark_7_en","",19983,], +["features.roomdetails.impl_RoomDetailsDark_8_en","",19983,], +["features.roomdetails.impl_RoomDetailsDark_9_en","",19983,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_0_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_0_en",19983,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_1_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_1_en",19983,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_2_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_2_en",19983,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_3_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_3_en",19983,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_4_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_4_en",19983,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_5_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_5_en",19983,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_6_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_6_en",19983,], +["features.roomdetails.impl.edit_RoomDetailsEditView_Day_7_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_7_en",19983,], +["features.roomdetails.impl_RoomDetails_0_en","",19983,], +["features.roomdetails.impl_RoomDetails_10_en","",19983,], +["features.roomdetails.impl_RoomDetails_11_en","",19983,], +["features.roomdetails.impl_RoomDetails_12_en","",19983,], +["features.roomdetails.impl_RoomDetails_13_en","",19983,], +["features.roomdetails.impl_RoomDetails_1_en","",19983,], +["features.roomdetails.impl_RoomDetails_2_en","",19983,], +["features.roomdetails.impl_RoomDetails_3_en","",19983,], +["features.roomdetails.impl_RoomDetails_4_en","",19983,], +["features.roomdetails.impl_RoomDetails_5_en","",19983,], +["features.roomdetails.impl_RoomDetails_6_en","",19983,], +["features.roomdetails.impl_RoomDetails_7_en","",19983,], +["features.roomdetails.impl_RoomDetails_8_en","",19983,], +["features.roomdetails.impl_RoomDetails_9_en","",19983,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",19983,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",19983,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",19983,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",19983,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",19983,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",19983,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en",19983,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_4_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_4_en",19983,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_5_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_5_en",19983,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_6_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_6_en",19983,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_7_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_7_en",19983,], +["features.roomlist.impl.components_RoomListContentView_Day_0_en","features.roomlist.impl.components_RoomListContentView_Night_0_en",19983,], +["features.roomlist.impl.components_RoomListContentView_Day_1_en","features.roomlist.impl.components_RoomListContentView_Night_1_en",19983,], ["features.roomlist.impl.components_RoomListContentView_Day_2_en","features.roomlist.impl.components_RoomListContentView_Night_2_en",0,], -["features.roomlist.impl.components_RoomListContentView_Day_3_en","features.roomlist.impl.components_RoomListContentView_Night_3_en",19972,], -["features.roomlist.impl.filters_RoomListFiltersView_Day_0_en","features.roomlist.impl.filters_RoomListFiltersView_Night_0_en",19972,], -["features.roomlist.impl.filters_RoomListFiltersView_Day_1_en","features.roomlist.impl.filters_RoomListFiltersView_Night_1_en",19972,], -["features.roomlist.impl_RoomListModalBottomSheetContent_Day_0_en","features.roomlist.impl_RoomListModalBottomSheetContent_Night_0_en",19972,], -["features.roomlist.impl_RoomListModalBottomSheetContent_Day_1_en","features.roomlist.impl_RoomListModalBottomSheetContent_Night_1_en",19972,], -["features.roomlist.impl_RoomListModalBottomSheetContent_Day_2_en","features.roomlist.impl_RoomListModalBottomSheetContent_Night_2_en",19972,], +["features.roomlist.impl.components_RoomListContentView_Day_3_en","features.roomlist.impl.components_RoomListContentView_Night_3_en",19983,], +["features.roomlist.impl.components_RoomListContentView_Day_4_en","features.roomlist.impl.components_RoomListContentView_Night_4_en",19983,], +["features.roomlist.impl.filters_RoomListFiltersView_Day_0_en","features.roomlist.impl.filters_RoomListFiltersView_Night_0_en",19983,], +["features.roomlist.impl.filters_RoomListFiltersView_Day_1_en","features.roomlist.impl.filters_RoomListFiltersView_Night_1_en",19983,], +["features.roomlist.impl_RoomListModalBottomSheetContent_Day_0_en","features.roomlist.impl_RoomListModalBottomSheetContent_Night_0_en",19983,], +["features.roomlist.impl_RoomListModalBottomSheetContent_Day_1_en","features.roomlist.impl_RoomListModalBottomSheetContent_Night_1_en",19983,], +["features.roomlist.impl_RoomListModalBottomSheetContent_Day_2_en","features.roomlist.impl_RoomListModalBottomSheetContent_Night_2_en",19983,], ["features.roomlist.impl.search_RoomListSearchContent_Day_0_en","features.roomlist.impl.search_RoomListSearchContent_Night_0_en",0,], -["features.roomlist.impl.search_RoomListSearchContent_Day_1_en","features.roomlist.impl.search_RoomListSearchContent_Night_1_en",19972,], -["features.roomlist.impl.search_RoomListSearchContent_Day_2_en","features.roomlist.impl.search_RoomListSearchContent_Night_2_en",19972,], -["features.roomlist.impl_RoomListView_Day_0_en","features.roomlist.impl_RoomListView_Night_0_en",19972,], -["features.roomlist.impl_RoomListView_Day_10_en","features.roomlist.impl_RoomListView_Night_10_en",19975,], -["features.roomlist.impl_RoomListView_Day_1_en","features.roomlist.impl_RoomListView_Night_1_en",19972,], -["features.roomlist.impl_RoomListView_Day_2_en","features.roomlist.impl_RoomListView_Night_2_en",19972,], -["features.roomlist.impl_RoomListView_Day_3_en","features.roomlist.impl_RoomListView_Night_3_en",19972,], -["features.roomlist.impl_RoomListView_Day_4_en","features.roomlist.impl_RoomListView_Night_4_en",19972,], -["features.roomlist.impl_RoomListView_Day_5_en","features.roomlist.impl_RoomListView_Night_5_en",19972,], -["features.roomlist.impl_RoomListView_Day_6_en","features.roomlist.impl_RoomListView_Night_6_en",19972,], -["features.roomlist.impl_RoomListView_Day_7_en","features.roomlist.impl_RoomListView_Night_7_en",19972,], +["features.roomlist.impl.search_RoomListSearchContent_Day_1_en","features.roomlist.impl.search_RoomListSearchContent_Night_1_en",19983,], +["features.roomlist.impl.search_RoomListSearchContent_Day_2_en","features.roomlist.impl.search_RoomListSearchContent_Night_2_en",19983,], +["features.roomlist.impl_RoomListView_Day_0_en","features.roomlist.impl_RoomListView_Night_0_en",19983,], +["features.roomlist.impl_RoomListView_Day_10_en","features.roomlist.impl_RoomListView_Night_10_en",19983,], +["features.roomlist.impl_RoomListView_Day_1_en","features.roomlist.impl_RoomListView_Night_1_en",19983,], +["features.roomlist.impl_RoomListView_Day_2_en","features.roomlist.impl_RoomListView_Night_2_en",19983,], +["features.roomlist.impl_RoomListView_Day_3_en","features.roomlist.impl_RoomListView_Night_3_en",19983,], +["features.roomlist.impl_RoomListView_Day_4_en","features.roomlist.impl_RoomListView_Night_4_en",19983,], +["features.roomlist.impl_RoomListView_Day_5_en","features.roomlist.impl_RoomListView_Night_5_en",19983,], +["features.roomlist.impl_RoomListView_Day_6_en","features.roomlist.impl_RoomListView_Night_6_en",19983,], +["features.roomlist.impl_RoomListView_Day_7_en","features.roomlist.impl_RoomListView_Night_7_en",19983,], ["features.roomlist.impl_RoomListView_Day_8_en","features.roomlist.impl_RoomListView_Night_8_en",0,], ["features.roomlist.impl_RoomListView_Day_9_en","features.roomlist.impl_RoomListView_Night_9_en",0,], -["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_0_en",19972,], -["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_1_en",19972,], -["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_2_en",19972,], -["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",19972,], -["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",19972,], -["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",19972,], -["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",19972,], -["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",19972,], +["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_0_en",19983,], +["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_1_en",19983,], +["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_2_en",19983,], +["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",19983,], +["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",19983,], +["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",19983,], +["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",19983,], +["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",19983,], ["features.roomdetails.impl.members_RoomMemberListView_Day_5_en","features.roomdetails.impl.members_RoomMemberListView_Night_5_en",0,], -["features.roomdetails.impl.members_RoomMemberListView_Day_6_en","features.roomdetails.impl.members_RoomMemberListView_Night_6_en",19972,], -["features.roomdetails.impl.members_RoomMemberListView_Day_7_en","features.roomdetails.impl.members_RoomMemberListView_Night_7_en",19972,], -["features.roomdetails.impl.members_RoomMemberListView_Day_8_en","features.roomdetails.impl.members_RoomMemberListView_Night_8_en",19972,], +["features.roomdetails.impl.members_RoomMemberListView_Day_6_en","features.roomdetails.impl.members_RoomMemberListView_Night_6_en",19983,], +["features.roomdetails.impl.members_RoomMemberListView_Day_7_en","features.roomdetails.impl.members_RoomMemberListView_Night_7_en",19983,], +["features.roomdetails.impl.members_RoomMemberListView_Day_8_en","features.roomdetails.impl.members_RoomMemberListView_Night_8_en",19983,], ["libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Day_0_en","libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Night_0_en",0,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_0_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_0_en",19972,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_1_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_1_en",19972,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_2_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_2_en",19972,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_3_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_3_en",19972,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_4_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_4_en",19972,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_5_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_5_en",19972,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_6_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_6_en",19972,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_7_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_7_en",19972,], -["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_8_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_8_en",19972,], +["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_0_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_0_en",19983,], +["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_1_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_1_en",19983,], +["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_2_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_2_en",19983,], +["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_3_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_3_en",19983,], +["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_4_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_4_en",19983,], +["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_5_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_5_en",19983,], +["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_6_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_6_en",19983,], +["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_7_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_7_en",19983,], +["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_8_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_8_en",19983,], ["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_9_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_9_en",0,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en",19972,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",19972,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",19972,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",19972,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",19972,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",19972,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",19972,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",19972,], -["features.createroom.impl.components_RoomPrivacyOption_Day_0_en","features.createroom.impl.components_RoomPrivacyOption_Night_0_en",19972,], -["libraries.roomselect.impl_RoomSelectView_Day_0_en","libraries.roomselect.impl_RoomSelectView_Night_0_en",19972,], -["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",19972,], -["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",19972,], -["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",19972,], -["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",19972,], -["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",19972,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en",19983,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",19983,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",19983,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",19983,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",19983,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",19983,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",19983,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",19983,], +["features.createroom.impl.components_RoomPrivacyOption_Day_0_en","features.createroom.impl.components_RoomPrivacyOption_Night_0_en",19983,], +["libraries.roomselect.impl_RoomSelectView_Day_0_en","libraries.roomselect.impl_RoomSelectView_Night_0_en",19983,], +["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",19983,], +["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",19983,], +["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",19983,], +["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",19983,], +["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",19983,], ["features.roomlist.impl.components_RoomSummaryPlaceholderRow_Day_0_en","features.roomlist.impl.components_RoomSummaryPlaceholderRow_Night_0_en",0,], ["features.roomlist.impl.components_RoomSummaryRow_Day_0_en","features.roomlist.impl.components_RoomSummaryRow_Night_0_en",0,], ["features.roomlist.impl.components_RoomSummaryRow_Day_10_en","features.roomlist.impl.components_RoomSummaryRow_Night_10_en",0,], @@ -793,10 +804,10 @@ export const screenshots = [ ["features.roomlist.impl.components_RoomSummaryRow_Day_26_en","features.roomlist.impl.components_RoomSummaryRow_Night_26_en",0,], ["features.roomlist.impl.components_RoomSummaryRow_Day_27_en","features.roomlist.impl.components_RoomSummaryRow_Night_27_en",0,], ["features.roomlist.impl.components_RoomSummaryRow_Day_28_en","features.roomlist.impl.components_RoomSummaryRow_Night_28_en",0,], -["features.roomlist.impl.components_RoomSummaryRow_Day_29_en","features.roomlist.impl.components_RoomSummaryRow_Night_29_en",19972,], -["features.roomlist.impl.components_RoomSummaryRow_Day_2_en","features.roomlist.impl.components_RoomSummaryRow_Night_2_en",19972,], -["features.roomlist.impl.components_RoomSummaryRow_Day_30_en","features.roomlist.impl.components_RoomSummaryRow_Night_30_en",19972,], -["features.roomlist.impl.components_RoomSummaryRow_Day_31_en","features.roomlist.impl.components_RoomSummaryRow_Night_31_en",19972,], +["features.roomlist.impl.components_RoomSummaryRow_Day_29_en","features.roomlist.impl.components_RoomSummaryRow_Night_29_en",19983,], +["features.roomlist.impl.components_RoomSummaryRow_Day_2_en","features.roomlist.impl.components_RoomSummaryRow_Night_2_en",19983,], +["features.roomlist.impl.components_RoomSummaryRow_Day_30_en","features.roomlist.impl.components_RoomSummaryRow_Night_30_en",19983,], +["features.roomlist.impl.components_RoomSummaryRow_Day_31_en","features.roomlist.impl.components_RoomSummaryRow_Night_31_en",19983,], ["features.roomlist.impl.components_RoomSummaryRow_Day_3_en","features.roomlist.impl.components_RoomSummaryRow_Night_3_en",0,], ["features.roomlist.impl.components_RoomSummaryRow_Day_4_en","features.roomlist.impl.components_RoomSummaryRow_Night_4_en",0,], ["features.roomlist.impl.components_RoomSummaryRow_Day_5_en","features.roomlist.impl.components_RoomSummaryRow_Night_5_en",0,], @@ -804,64 +815,64 @@ export const screenshots = [ ["features.roomlist.impl.components_RoomSummaryRow_Day_7_en","features.roomlist.impl.components_RoomSummaryRow_Night_7_en",0,], ["features.roomlist.impl.components_RoomSummaryRow_Day_8_en","features.roomlist.impl.components_RoomSummaryRow_Night_8_en",0,], ["features.roomlist.impl.components_RoomSummaryRow_Day_9_en","features.roomlist.impl.components_RoomSummaryRow_Night_9_en",0,], -["appnav.root_RootView_Day_0_en","appnav.root_RootView_Night_0_en",19972,], -["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",19972,], -["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",19972,], +["appnav.root_RootView_Day_0_en","appnav.root_RootView_Night_0_en",19983,], +["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",19983,], +["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",19983,], ["appicon.element_RoundIcon_en","",0,], ["appicon.enterprise_RoundIcon_en","",0,], ["libraries.designsystem.atomic.atoms_RoundedIconAtom_Day_0_en","libraries.designsystem.atomic.atoms_RoundedIconAtom_Night_0_en",0,], -["features.verifysession.impl.emoji_SasEmojis_Day_0_en","features.verifysession.impl.emoji_SasEmojis_Night_0_en",19972,], -["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",19972,], -["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",19972,], +["features.verifysession.impl.emoji_SasEmojis_Day_0_en","features.verifysession.impl.emoji_SasEmojis_Night_0_en",19983,], +["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",19983,], +["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",19983,], ["libraries.designsystem.theme.components_SearchBarActiveNoneQuery_Search views_en","",0,], ["libraries.designsystem.theme.components_SearchBarActiveWithContent_Search views_en","",0,], -["libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search views_en","",19972,], +["libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search views_en","",19983,], ["libraries.designsystem.theme.components_SearchBarActiveWithQueryNoBackButton_Search views_en","",0,], ["libraries.designsystem.theme.components_SearchBarActiveWithQuery_Search views_en","",0,], ["libraries.designsystem.theme.components_SearchBarInactive_Search views_en","",0,], -["features.createroom.impl.components_SearchMultipleUsersResultItem_en","",19972,], -["features.createroom.impl.components_SearchSingleUserResultItem_en","",19972,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",19972,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",19972,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",19972,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",19972,], -["features.securebackup.impl.enable_SecureBackupEnableView_Day_0_en","features.securebackup.impl.enable_SecureBackupEnableView_Night_0_en",19972,], -["features.securebackup.impl.enable_SecureBackupEnableView_Day_1_en","features.securebackup.impl.enable_SecureBackupEnableView_Night_1_en",19972,], -["features.securebackup.impl.enable_SecureBackupEnableView_Day_2_en","features.securebackup.impl.enable_SecureBackupEnableView_Night_2_en",19972,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",19972,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",19972,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",19972,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",19972,], -["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",19972,], -["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",19972,], -["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",19972,], -["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",19972,], -["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",19972,], -["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",19972,], -["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",19972,], -["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",19972,], -["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",19972,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",19972,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",19972,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",19972,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",19972,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",19972,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",19972,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",19972,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",19972,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",19972,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",19972,], +["features.createroom.impl.components_SearchMultipleUsersResultItem_en","",19983,], +["features.createroom.impl.components_SearchSingleUserResultItem_en","",19983,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",19983,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",19983,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",19983,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",19983,], +["features.securebackup.impl.enable_SecureBackupEnableView_Day_0_en","features.securebackup.impl.enable_SecureBackupEnableView_Night_0_en",19983,], +["features.securebackup.impl.enable_SecureBackupEnableView_Day_1_en","features.securebackup.impl.enable_SecureBackupEnableView_Night_1_en",19983,], +["features.securebackup.impl.enable_SecureBackupEnableView_Day_2_en","features.securebackup.impl.enable_SecureBackupEnableView_Night_2_en",19983,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",19983,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",19983,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",19983,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",19983,], +["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",19983,], +["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",19983,], +["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",19983,], +["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",19983,], +["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",19983,], +["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",19983,], +["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",19983,], +["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",19983,], +["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",19983,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",19983,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",19983,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",19983,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",19983,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",19983,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",19983,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",19983,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",19983,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",19983,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",19983,], ["libraries.matrix.ui.components_SelectedRoom_Day_0_en","libraries.matrix.ui.components_SelectedRoom_Night_0_en",0,], ["libraries.matrix.ui.components_SelectedRoom_Day_1_en","libraries.matrix.ui.components_SelectedRoom_Night_1_en",0,], ["libraries.matrix.ui.components_SelectedUserCannotRemove_Day_0_en","libraries.matrix.ui.components_SelectedUserCannotRemove_Night_0_en",0,], ["libraries.matrix.ui.components_SelectedUser_Day_0_en","libraries.matrix.ui.components_SelectedUser_Night_0_en",0,], ["libraries.matrix.ui.components_SelectedUsersRowList_Day_0_en","libraries.matrix.ui.components_SelectedUsersRowList_Night_0_en",0,], ["libraries.textcomposer.components_SendButton_Day_0_en","libraries.textcomposer.components_SendButton_Night_0_en",0,], -["features.location.impl.send_SendLocationView_Day_0_en","features.location.impl.send_SendLocationView_Night_0_en",19972,], -["features.location.impl.send_SendLocationView_Day_1_en","features.location.impl.send_SendLocationView_Night_1_en",19972,], -["features.location.impl.send_SendLocationView_Day_2_en","features.location.impl.send_SendLocationView_Night_2_en",19972,], -["features.location.impl.send_SendLocationView_Day_3_en","features.location.impl.send_SendLocationView_Night_3_en",19972,], -["features.location.impl.send_SendLocationView_Day_4_en","features.location.impl.send_SendLocationView_Night_4_en",19972,], +["features.location.impl.send_SendLocationView_Day_0_en","features.location.impl.send_SendLocationView_Night_0_en",19983,], +["features.location.impl.send_SendLocationView_Day_1_en","features.location.impl.send_SendLocationView_Night_1_en",19983,], +["features.location.impl.send_SendLocationView_Day_2_en","features.location.impl.send_SendLocationView_Night_2_en",19983,], +["features.location.impl.send_SendLocationView_Day_3_en","features.location.impl.send_SendLocationView_Night_3_en",19983,], +["features.location.impl.send_SendLocationView_Day_4_en","features.location.impl.send_SendLocationView_Night_4_en",19983,], ["libraries.matrix.ui.messages.sender_SenderName_Day_0_en","libraries.matrix.ui.messages.sender_SenderName_Night_0_en",0,], ["libraries.matrix.ui.messages.sender_SenderName_Day_1_en","libraries.matrix.ui.messages.sender_SenderName_Night_1_en",0,], ["libraries.matrix.ui.messages.sender_SenderName_Day_2_en","libraries.matrix.ui.messages.sender_SenderName_Night_2_en",0,], @@ -871,39 +882,40 @@ export const screenshots = [ ["libraries.matrix.ui.messages.sender_SenderName_Day_6_en","libraries.matrix.ui.messages.sender_SenderName_Night_6_en",0,], ["libraries.matrix.ui.messages.sender_SenderName_Day_7_en","libraries.matrix.ui.messages.sender_SenderName_Night_7_en",0,], ["libraries.matrix.ui.messages.sender_SenderName_Day_8_en","libraries.matrix.ui.messages.sender_SenderName_Night_8_en",0,], -["features.roomlist.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.roomlist.impl.components_SetUpRecoveryKeyBanner_Night_0_en",19975,], -["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",19972,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",19972,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",19972,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",19972,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",19972,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",19972,], +["features.roomlist.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.roomlist.impl.components_SetUpRecoveryKeyBanner_Night_0_en",19983,], +["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",19983,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",19983,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",19983,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",19983,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",19983,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",19983,], ["features.share.impl_ShareView_Day_0_en","features.share.impl_ShareView_Night_0_en",0,], ["features.share.impl_ShareView_Day_1_en","features.share.impl_ShareView_Night_1_en",0,], ["features.share.impl_ShareView_Day_2_en","features.share.impl_ShareView_Night_2_en",0,], -["features.share.impl_ShareView_Day_3_en","features.share.impl_ShareView_Night_3_en",19972,], +["features.share.impl_ShareView_Day_3_en","features.share.impl_ShareView_Night_3_en",19983,], ["features.messages.impl.actionlist_SheetContent_Day_0_en","features.messages.impl.actionlist_SheetContent_Night_0_en",0,], ["features.messages.impl.timeline.components.reactionsummary_SheetContent_Day_0_en","features.messages.impl.timeline.components.reactionsummary_SheetContent_Night_0_en",0,], -["features.messages.impl.actionlist_SheetContent_Day_10_en","features.messages.impl.actionlist_SheetContent_Night_10_en",19972,], -["features.messages.impl.actionlist_SheetContent_Day_11_en","features.messages.impl.actionlist_SheetContent_Night_11_en",19972,], +["features.messages.impl.actionlist_SheetContent_Day_10_en","features.messages.impl.actionlist_SheetContent_Night_10_en",19983,], +["features.messages.impl.actionlist_SheetContent_Day_11_en","features.messages.impl.actionlist_SheetContent_Night_11_en",19983,], +["features.messages.impl.actionlist_SheetContent_Day_12_en","features.messages.impl.actionlist_SheetContent_Night_12_en",19983,], ["features.messages.impl.actionlist_SheetContent_Day_1_en","features.messages.impl.actionlist_SheetContent_Night_1_en",0,], -["features.messages.impl.actionlist_SheetContent_Day_2_en","features.messages.impl.actionlist_SheetContent_Night_2_en",19972,], -["features.messages.impl.actionlist_SheetContent_Day_3_en","features.messages.impl.actionlist_SheetContent_Night_3_en",19972,], -["features.messages.impl.actionlist_SheetContent_Day_4_en","features.messages.impl.actionlist_SheetContent_Night_4_en",19972,], -["features.messages.impl.actionlist_SheetContent_Day_5_en","features.messages.impl.actionlist_SheetContent_Night_5_en",19972,], -["features.messages.impl.actionlist_SheetContent_Day_6_en","features.messages.impl.actionlist_SheetContent_Night_6_en",19972,], -["features.messages.impl.actionlist_SheetContent_Day_7_en","features.messages.impl.actionlist_SheetContent_Night_7_en",19972,], -["features.messages.impl.actionlist_SheetContent_Day_8_en","features.messages.impl.actionlist_SheetContent_Night_8_en",19972,], -["features.messages.impl.actionlist_SheetContent_Day_9_en","features.messages.impl.actionlist_SheetContent_Night_9_en",19972,], -["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",19972,], -["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",19972,], -["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",19972,], -["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",19972,], -["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",19972,], -["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",19972,], -["features.location.impl.show_ShowLocationView_Day_6_en","features.location.impl.show_ShowLocationView_Night_6_en",19972,], -["features.location.impl.show_ShowLocationView_Day_7_en","features.location.impl.show_ShowLocationView_Night_7_en",19972,], -["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",19972,], +["features.messages.impl.actionlist_SheetContent_Day_2_en","features.messages.impl.actionlist_SheetContent_Night_2_en",19983,], +["features.messages.impl.actionlist_SheetContent_Day_3_en","features.messages.impl.actionlist_SheetContent_Night_3_en",19983,], +["features.messages.impl.actionlist_SheetContent_Day_4_en","features.messages.impl.actionlist_SheetContent_Night_4_en",19983,], +["features.messages.impl.actionlist_SheetContent_Day_5_en","features.messages.impl.actionlist_SheetContent_Night_5_en",19983,], +["features.messages.impl.actionlist_SheetContent_Day_6_en","features.messages.impl.actionlist_SheetContent_Night_6_en",19983,], +["features.messages.impl.actionlist_SheetContent_Day_7_en","features.messages.impl.actionlist_SheetContent_Night_7_en",19983,], +["features.messages.impl.actionlist_SheetContent_Day_8_en","features.messages.impl.actionlist_SheetContent_Night_8_en",19983,], +["features.messages.impl.actionlist_SheetContent_Day_9_en","features.messages.impl.actionlist_SheetContent_Night_9_en",19983,], +["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",19983,], +["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",19983,], +["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",19983,], +["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",19983,], +["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",19983,], +["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",19983,], +["features.location.impl.show_ShowLocationView_Day_6_en","features.location.impl.show_ShowLocationView_Night_6_en",19983,], +["features.location.impl.show_ShowLocationView_Day_7_en","features.location.impl.show_ShowLocationView_Night_7_en",19983,], +["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",19983,], ["libraries.designsystem.components.dialogs_SingleSelectionDialogContent_Dialogs_en","",0,], ["libraries.designsystem.components.dialogs_SingleSelectionDialog_Day_0_en","libraries.designsystem.components.dialogs_SingleSelectionDialog_Night_0_en",0,], ["libraries.designsystem.components.list_SingleSelectionListItemCustomFormattert_Single selection List item - custom formatter_List items_en","",0,], @@ -912,7 +924,7 @@ export const screenshots = [ ["libraries.designsystem.components.list_SingleSelectionListItemUnselectedWithSupportingText_Single selection List item - no selection, supporting text_List items_en","",0,], ["libraries.designsystem.components.list_SingleSelectionListItem_Single selection List item - no selection_List items_en","",0,], ["libraries.designsystem.theme.components_Sliders_Sliders_en","",0,], -["features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en","features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en",19972,], +["features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en","features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en",19983,], ["libraries.designsystem.theme.components_SnackbarWithActionAndCloseButton_Snackbar with action and close button_Snackbars_en","",0,], ["libraries.designsystem.theme.components_SnackbarWithActionOnNewLineAndCloseButton_Snackbar with action and close button on new line_Snackbars_en","",0,], ["libraries.designsystem.theme.components_SnackbarWithActionOnNewLine_Snackbar with action on new line_Snackbars_en","",0,], @@ -922,37 +934,37 @@ export const screenshots = [ ["libraries.designsystem.modifiers_SquareSizeModifierLargeHeight_en","",0,], ["libraries.designsystem.modifiers_SquareSizeModifierLargeWidth_en","",0,], ["features.location.api.internal_StaticMapPlaceholder_Day_0_en","features.location.api.internal_StaticMapPlaceholder_Night_0_en",0,], -["features.location.api.internal_StaticMapPlaceholder_Day_1_en","features.location.api.internal_StaticMapPlaceholder_Night_1_en",19972,], +["features.location.api.internal_StaticMapPlaceholder_Day_1_en","features.location.api.internal_StaticMapPlaceholder_Night_1_en",19983,], ["features.location.api_StaticMapView_Day_0_en","features.location.api_StaticMapView_Night_0_en",0,], -["features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en","features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en",19972,], +["features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en","features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en",19983,], ["libraries.designsystem.atomic.pages_SunsetPage_Day_0_en","libraries.designsystem.atomic.pages_SunsetPage_Night_0_en",0,], ["libraries.designsystem.components.button_SuperButton_Day_0_en","libraries.designsystem.components.button_SuperButton_Night_0_en",0,], ["libraries.designsystem.theme.components_Surface_en","",0,], ["libraries.designsystem.theme.components_Switch_Toggles_en","",0,], -["appnav.loggedin_SyncStateView_Day_0_en","appnav.loggedin_SyncStateView_Night_0_en",19972,], +["appnav.loggedin_SyncStateView_Day_0_en","appnav.loggedin_SyncStateView_Night_0_en",19983,], ["libraries.designsystem.theme.components_TextButtonLargeLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_TextButtonLarge_Buttons_en","",0,], ["libraries.designsystem.theme.components_TextButtonMediumLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_TextButtonMedium_Buttons_en","",0,], ["libraries.designsystem.theme.components_TextButtonSmall_Buttons_en","",0,], -["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",19972,], -["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",19972,], -["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",19972,], -["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",19972,], -["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",19972,], -["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",19972,], -["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",19972,], -["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",19972,], -["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",19972,], -["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",19972,], -["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",19972,], -["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",19972,], -["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",19972,], -["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",19972,], -["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",19972,], -["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",19972,], -["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",19972,], -["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",19972,], +["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",19983,], +["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",19983,], +["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",19983,], +["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",19983,], +["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",19983,], +["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",19983,], +["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",19983,], +["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",19983,], +["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",19983,], +["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",19983,], +["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",19983,], +["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",19983,], +["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",19983,], +["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",19983,], +["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",19983,], +["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",19983,], +["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",19983,], +["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",19983,], ["libraries.textcomposer_TextComposerVoice_Day_0_en","libraries.textcomposer_TextComposerVoice_Night_0_en",0,], ["libraries.designsystem.theme.components_TextDark_Text_en","",0,], ["libraries.designsystem.theme.components_TextFieldDark_TextFields_en","",0,], @@ -964,26 +976,26 @@ export const screenshots = [ ["libraries.designsystem.theme.components_TextFieldValueTextFieldDark_TextFields_en","",0,], ["libraries.textcomposer.components_TextFormatting_Day_0_en","libraries.textcomposer.components_TextFormatting_Night_0_en",0,], ["libraries.designsystem.theme.components_TextLight_Text_en","",0,], -["libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime pickers_en","",19972,], -["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime pickers_en","",19972,], -["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime pickers_en","",19972,], +["libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime pickers_en","",19983,], +["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime pickers_en","",19983,], +["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime pickers_en","",19983,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_0_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_1_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_2_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_2_en",0,], -["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en",19972,], -["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",19972,], +["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en",19983,], +["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",19983,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_5_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_5_en",0,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_6_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_6_en",0,], ["features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en","features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_1_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_2_en",0,], -["features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en","features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en",19972,], +["features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en","features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en",19983,], ["features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Night_0_en",0,], ["features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Day_1_en","features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Night_1_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_0_en",19972,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",19972,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",19972,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_0_en",19983,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",19983,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",19983,], ["features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowLongSenderName_en","",0,], @@ -991,16 +1003,16 @@ export const screenshots = [ ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_2_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en",19972,], -["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",19972,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en",19983,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",19983,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_6_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_6_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",19972,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",19983,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_2_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en",19972,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",19972,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en",19983,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",19983,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en",0,], @@ -1009,36 +1021,36 @@ export const screenshots = [ ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en",19972,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en",19983,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en",19972,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en",19983,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRow_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRow_Night_0_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en","",19972,], +["features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en","",19983,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_1_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_2_en",0,], -["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en",19972,], -["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",19972,], +["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en",19983,], +["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",19983,], ["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_1_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_2_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemInformativeView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemInformativeView_Night_0_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en",19972,], +["features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en",19983,], ["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_1_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en",19972,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",19972,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",19972,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",19972,], -["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",19972,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en",19983,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",19983,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",19983,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",19983,], +["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",19983,], ["features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Night_0_en",0,], -["features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en",19972,], -["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",19972,], +["features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en",19983,], +["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",19983,], ["features.messages.impl.timeline.components_TimelineItemReactionsView_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsView_Night_0_en",0,], -["features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en",19972,], +["features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en",19983,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_0_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_0_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_1_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_1_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_2_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_2_en",0,], @@ -1047,8 +1059,8 @@ export const screenshots = [ ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_5_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_5_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_6_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_6_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_7_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_7_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en",19972,], -["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",19972,], +["features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en",19983,], +["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",19983,], ["features.messages.impl.timeline.components_TimelineItemStateEventRow_Day_0_en","features.messages.impl.timeline.components_TimelineItemStateEventRow_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemStateView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemStateView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_0_en",0,], @@ -1060,7 +1072,7 @@ export const screenshots = [ ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_3_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_4_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_5_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en",19972,], +["features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en",19983,], ["features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_1_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_2_en",0,], @@ -1082,84 +1094,84 @@ export const screenshots = [ ["features.messages.impl.timeline.components.event_TimelineItemVoiceView_Day_9_en","features.messages.impl.timeline.components.event_TimelineItemVoiceView_Night_9_en",0,], ["features.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en","features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en",0,], -["features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en","features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en",19972,], -["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",19972,], +["features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en","features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en",19983,], +["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",19983,], ["features.messages.impl.timeline_TimelineView_Day_10_en","features.messages.impl.timeline_TimelineView_Night_10_en",0,], -["features.messages.impl.timeline_TimelineView_Day_11_en","features.messages.impl.timeline_TimelineView_Night_11_en",19972,], -["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",19972,], -["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",19972,], -["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",19972,], -["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",19972,], -["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",19972,], -["features.messages.impl.timeline_TimelineView_Day_17_en","features.messages.impl.timeline_TimelineView_Night_17_en",19975,], -["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",19972,], +["features.messages.impl.timeline_TimelineView_Day_11_en","features.messages.impl.timeline_TimelineView_Night_11_en",19983,], +["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",19983,], +["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",19983,], +["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",19983,], +["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",19983,], +["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",19983,], +["features.messages.impl.timeline_TimelineView_Day_17_en","features.messages.impl.timeline_TimelineView_Night_17_en",19983,], +["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",19983,], ["features.messages.impl.timeline_TimelineView_Day_2_en","features.messages.impl.timeline_TimelineView_Night_2_en",0,], ["features.messages.impl.timeline_TimelineView_Day_3_en","features.messages.impl.timeline_TimelineView_Night_3_en",0,], -["features.messages.impl.timeline_TimelineView_Day_4_en","features.messages.impl.timeline_TimelineView_Night_4_en",19972,], +["features.messages.impl.timeline_TimelineView_Day_4_en","features.messages.impl.timeline_TimelineView_Night_4_en",19983,], ["features.messages.impl.timeline_TimelineView_Day_5_en","features.messages.impl.timeline_TimelineView_Night_5_en",0,], -["features.messages.impl.timeline_TimelineView_Day_6_en","features.messages.impl.timeline_TimelineView_Night_6_en",19972,], +["features.messages.impl.timeline_TimelineView_Day_6_en","features.messages.impl.timeline_TimelineView_Night_6_en",19983,], ["features.messages.impl.timeline_TimelineView_Day_7_en","features.messages.impl.timeline_TimelineView_Night_7_en",0,], -["features.messages.impl.timeline_TimelineView_Day_8_en","features.messages.impl.timeline_TimelineView_Night_8_en",19972,], +["features.messages.impl.timeline_TimelineView_Day_8_en","features.messages.impl.timeline_TimelineView_Night_8_en",19983,], ["features.messages.impl.timeline_TimelineView_Day_9_en","features.messages.impl.timeline_TimelineView_Night_9_en",0,], ["libraries.designsystem.theme.components_TopAppBar_App Bars_en","",0,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_0_en",19972,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",19972,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",19972,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",19972,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",19972,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",19972,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",19972,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",19972,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_0_en",19983,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",19983,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",19983,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",19983,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",19983,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",19983,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",19983,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",19983,], ["features.messages.impl.typing_TypingNotificationView_Day_0_en","features.messages.impl.typing_TypingNotificationView_Night_0_en",0,], -["features.messages.impl.typing_TypingNotificationView_Day_1_en","features.messages.impl.typing_TypingNotificationView_Night_1_en",19972,], -["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",19972,], -["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",19972,], -["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",19972,], -["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",19972,], -["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",19972,], +["features.messages.impl.typing_TypingNotificationView_Day_1_en","features.messages.impl.typing_TypingNotificationView_Night_1_en",19983,], +["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",19983,], +["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",19983,], +["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",19983,], +["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",19983,], +["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",19983,], ["features.messages.impl.typing_TypingNotificationView_Day_7_en","features.messages.impl.typing_TypingNotificationView_Night_7_en",0,], ["features.messages.impl.typing_TypingNotificationView_Day_8_en","features.messages.impl.typing_TypingNotificationView_Night_8_en",0,], ["libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Day_0_en","libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Night_0_en",0,], -["libraries.matrix.ui.components_UnresolvedUserRow_en","",19972,], +["libraries.matrix.ui.components_UnresolvedUserRow_en","",19983,], ["libraries.matrix.ui.components_UnsavedAvatar_Day_0_en","libraries.matrix.ui.components_UnsavedAvatar_Night_0_en",0,], ["libraries.designsystem.components.avatar_UserAvatarColors_Day_0_en","libraries.designsystem.components.avatar_UserAvatarColors_Night_0_en",0,], -["features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en",19972,], -["features.createroom.impl.components_UserListView_Day_0_en","features.createroom.impl.components_UserListView_Night_0_en",19972,], -["features.createroom.impl.components_UserListView_Day_1_en","features.createroom.impl.components_UserListView_Night_1_en",19972,], -["features.createroom.impl.components_UserListView_Day_2_en","features.createroom.impl.components_UserListView_Night_2_en",19972,], +["features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en",19983,], +["features.createroom.impl.components_UserListView_Day_0_en","features.createroom.impl.components_UserListView_Night_0_en",19983,], +["features.createroom.impl.components_UserListView_Day_1_en","features.createroom.impl.components_UserListView_Night_1_en",19983,], +["features.createroom.impl.components_UserListView_Day_2_en","features.createroom.impl.components_UserListView_Night_2_en",19983,], ["features.createroom.impl.components_UserListView_Day_3_en","features.createroom.impl.components_UserListView_Night_3_en",0,], ["features.createroom.impl.components_UserListView_Day_4_en","features.createroom.impl.components_UserListView_Night_4_en",0,], ["features.createroom.impl.components_UserListView_Day_5_en","features.createroom.impl.components_UserListView_Night_5_en",0,], ["features.createroom.impl.components_UserListView_Day_6_en","features.createroom.impl.components_UserListView_Night_6_en",0,], -["features.createroom.impl.components_UserListView_Day_7_en","features.createroom.impl.components_UserListView_Night_7_en",19972,], +["features.createroom.impl.components_UserListView_Day_7_en","features.createroom.impl.components_UserListView_Night_7_en",19983,], ["features.createroom.impl.components_UserListView_Day_8_en","features.createroom.impl.components_UserListView_Night_8_en",0,], -["features.createroom.impl.components_UserListView_Day_9_en","features.createroom.impl.components_UserListView_Night_9_en",19972,], +["features.createroom.impl.components_UserListView_Day_9_en","features.createroom.impl.components_UserListView_Night_9_en",19983,], ["features.preferences.impl.user_UserPreferences_Day_0_en","features.preferences.impl.user_UserPreferences_Night_0_en",0,], ["features.preferences.impl.user_UserPreferences_Day_1_en","features.preferences.impl.user_UserPreferences_Night_1_en",0,], ["features.preferences.impl.user_UserPreferences_Day_2_en","features.preferences.impl.user_UserPreferences_Night_2_en",0,], ["features.userprofile.shared_UserProfileHeaderSection_Day_0_en","features.userprofile.shared_UserProfileHeaderSection_Night_0_en",0,], -["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",19972,], -["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",19972,], -["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",19972,], -["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",19972,], -["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",19972,], -["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",19972,], -["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",19972,], -["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",19972,], -["features.userprofile.shared_UserProfileView_Day_8_en","features.userprofile.shared_UserProfileView_Night_8_en",19972,], -["features.verifysession.impl_VerifySelfSessionView_Day_0_en","features.verifysession.impl_VerifySelfSessionView_Night_0_en",19972,], -["features.verifysession.impl_VerifySelfSessionView_Day_10_en","features.verifysession.impl_VerifySelfSessionView_Night_10_en",19975,], +["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",19983,], +["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",19983,], +["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",19983,], +["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",19983,], +["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",19983,], +["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",19983,], +["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",19983,], +["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",19983,], +["features.userprofile.shared_UserProfileView_Day_8_en","features.userprofile.shared_UserProfileView_Night_8_en",19983,], +["features.verifysession.impl_VerifySelfSessionView_Day_0_en","features.verifysession.impl_VerifySelfSessionView_Night_0_en",19983,], +["features.verifysession.impl_VerifySelfSessionView_Day_10_en","features.verifysession.impl_VerifySelfSessionView_Night_10_en",19983,], ["features.verifysession.impl_VerifySelfSessionView_Day_11_en","features.verifysession.impl_VerifySelfSessionView_Night_11_en",0,], ["features.verifysession.impl_VerifySelfSessionView_Day_12_en","features.verifysession.impl_VerifySelfSessionView_Night_12_en",0,], -["features.verifysession.impl_VerifySelfSessionView_Day_1_en","features.verifysession.impl_VerifySelfSessionView_Night_1_en",19972,], -["features.verifysession.impl_VerifySelfSessionView_Day_2_en","features.verifysession.impl_VerifySelfSessionView_Night_2_en",19972,], -["features.verifysession.impl_VerifySelfSessionView_Day_3_en","features.verifysession.impl_VerifySelfSessionView_Night_3_en",19972,], -["features.verifysession.impl_VerifySelfSessionView_Day_4_en","features.verifysession.impl_VerifySelfSessionView_Night_4_en",19972,], -["features.verifysession.impl_VerifySelfSessionView_Day_5_en","features.verifysession.impl_VerifySelfSessionView_Night_5_en",19972,], -["features.verifysession.impl_VerifySelfSessionView_Day_6_en","features.verifysession.impl_VerifySelfSessionView_Night_6_en",19972,], -["features.verifysession.impl_VerifySelfSessionView_Day_7_en","features.verifysession.impl_VerifySelfSessionView_Night_7_en",19972,], -["features.verifysession.impl_VerifySelfSessionView_Day_8_en","features.verifysession.impl_VerifySelfSessionView_Night_8_en",19972,], -["features.verifysession.impl_VerifySelfSessionView_Day_9_en","features.verifysession.impl_VerifySelfSessionView_Night_9_en",19972,], +["features.verifysession.impl_VerifySelfSessionView_Day_1_en","features.verifysession.impl_VerifySelfSessionView_Night_1_en",19983,], +["features.verifysession.impl_VerifySelfSessionView_Day_2_en","features.verifysession.impl_VerifySelfSessionView_Night_2_en",19983,], +["features.verifysession.impl_VerifySelfSessionView_Day_3_en","features.verifysession.impl_VerifySelfSessionView_Night_3_en",19983,], +["features.verifysession.impl_VerifySelfSessionView_Day_4_en","features.verifysession.impl_VerifySelfSessionView_Night_4_en",19983,], +["features.verifysession.impl_VerifySelfSessionView_Day_5_en","features.verifysession.impl_VerifySelfSessionView_Night_5_en",19983,], +["features.verifysession.impl_VerifySelfSessionView_Day_6_en","features.verifysession.impl_VerifySelfSessionView_Night_6_en",19983,], +["features.verifysession.impl_VerifySelfSessionView_Day_7_en","features.verifysession.impl_VerifySelfSessionView_Night_7_en",19983,], +["features.verifysession.impl_VerifySelfSessionView_Day_8_en","features.verifysession.impl_VerifySelfSessionView_Night_8_en",19983,], +["features.verifysession.impl_VerifySelfSessionView_Day_9_en","features.verifysession.impl_VerifySelfSessionView_Night_9_en",19983,], ["libraries.designsystem.ruler_VerticalRuler_Day_0_en","libraries.designsystem.ruler_VerticalRuler_Night_0_en",0,], ["features.viewfolder.impl.file_ViewFileView_Day_0_en","features.viewfolder.impl.file_ViewFileView_Night_0_en",0,], ["features.viewfolder.impl.file_ViewFileView_Day_1_en","features.viewfolder.impl.file_ViewFileView_Night_1_en",0,], @@ -1173,12 +1185,7 @@ export const screenshots = [ ["libraries.textcomposer.components_VoiceMessageRecorderButton_Day_0_en","libraries.textcomposer.components_VoiceMessageRecorderButton_Night_0_en",0,], ["libraries.textcomposer.components_VoiceMessageRecording_Day_0_en","libraries.textcomposer.components_VoiceMessageRecording_Night_0_en",0,], ["libraries.textcomposer.components_VoiceMessage_Day_0_en","libraries.textcomposer.components_VoiceMessage_Night_0_en",0,], -["features.login.impl.screens.waitlistscreen_WaitListView_Day_0_en","features.login.impl.screens.waitlistscreen_WaitListView_Night_0_en",19972,], -["features.login.impl.screens.waitlistscreen_WaitListView_Day_1_en","features.login.impl.screens.waitlistscreen_WaitListView_Night_1_en",19972,], -["features.login.impl.screens.waitlistscreen_WaitListView_Day_2_en","features.login.impl.screens.waitlistscreen_WaitListView_Night_2_en",19972,], -["features.login.impl.screens.waitlistscreen_WaitListView_Day_3_en","features.login.impl.screens.waitlistscreen_WaitListView_Night_3_en",19972,], -["features.login.impl.screens.waitlistscreen_WaitListView_Day_4_en","features.login.impl.screens.waitlistscreen_WaitListView_Night_4_en",19972,], ["libraries.designsystem.components.media_WaveformPlaybackView_Day_0_en","libraries.designsystem.components.media_WaveformPlaybackView_Night_0_en",0,], -["features.ftue.impl.welcome_WelcomeView_Day_0_en","features.ftue.impl.welcome_WelcomeView_Night_0_en",19972,], +["features.ftue.impl.welcome_WelcomeView_Day_0_en","features.ftue.impl.welcome_WelcomeView_Night_0_en",19983,], ["libraries.designsystem.ruler_WithRulers_Day_0_en","libraries.designsystem.ruler_WithRulers_Night_0_en",0,], ]; diff --git a/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/AppErrorView.kt b/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/AppErrorView.kt index 80d3de2dfe..2bd46eeb00 100644 --- a/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/AppErrorView.kt +++ b/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/AppErrorView.kt @@ -36,7 +36,7 @@ private fun AppErrorViewContent( ErrorDialog( title = title, content = body, - onDismiss = onDismiss, + onSubmit = onDismiss, ) } diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt index 7ec2784c70..797f13dbe8 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt @@ -68,11 +68,12 @@ class KonsistClassNameTest { .withoutName( "FakeFileSystem", "FakeImageLoader", - "FakeRustRoom", ) .assertTrue { - val interfaceName = it.name.replace("Fake", "") - it.name.startsWith("Fake") && + val interfaceName = it.name + .replace("FakeRust", "") + .replace("Fake", "") + (it.name.startsWith("Fake") || it.name.startsWith("FakeRust")) && it.parents().any { parent -> parent.name.replace(".", "") == interfaceName } } } diff --git a/tools/localazy/config.json b/tools/localazy/config.json index f736825725..f7555ea393 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -1,5 +1,12 @@ { "modules" : [ + { + "name" : ":appnav", + "includeRegex" : [ + "banner\\.migrate_to_native_sliding_sync\\.force_logout.title", + "banner\\.migrate_to_native_sliding_sync\\.action" + ] + }, { "name" : ":features:rageshake:impl", "includeRegex" : [ @@ -20,6 +27,12 @@ "screen_signout_.*" ] }, + { + "name" : ":features:deactivation:impl", + "includeRegex" : [ + "screen_deactivate_account_.*" + ] + }, { "name" : ":features:roomaliasresolver:impl", "includeRegex" : [ @@ -120,6 +133,7 @@ "screen_server_confirmation_.*", "screen_change_server_.*", "screen_change_account_provider_.*", + "screen_create_account_.*", "screen_account_provider_.*", "screen_waitlist_.*", "screen_qr_code_login_.*" diff --git a/upstream_infra/CHANGES.md b/upstream_infra/CHANGES.md index 2097277556..a3b77c6ec6 100644 --- a/upstream_infra/CHANGES.md +++ b/upstream_infra/CHANGES.md @@ -1,3 +1,54 @@ +Changes in Element X v0.6.2 (2024-09-17) +======================================== + +### ✨ Features +* Account deactivation. by @bmarty in https://github.com/element-hq/element-x-android/pull/3479 + +Changes in Element X v0.6.1 (2024-09-17) +======================================== + +### ✨ Features +* Add forced logout flow when the proxy is no longer available by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3458 +* Temporary account creation using Element Web. by @bmarty in https://github.com/element-hq/element-x-android/pull/3467 + +### 🙌 Improvements +* Feature/valere/invisible crypto feature flag by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/3451 +* Require acknowledgement to send to a verified user if their identity changed or if a device is unverified. by @ganfra in https://github.com/element-hq/element-x-android/pull/3461 +* Update pinned message actions by @ganfra in https://github.com/element-hq/element-x-android/pull/3438 + +### 🐛 Bugfixes +* Fix events blinking at the beginning of DM by @bmarty in https://github.com/element-hq/element-x-android/pull/3449 +* Fix not being able to decline an invite from the room list by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3466 + +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3464 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3469 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3476 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3477 + +### Others +* Upgrade Rust sdk to 0.2.45 by @bmarty in https://github.com/element-hq/element-x-android/pull/3472 +* SDK 0.2.46 by @bmarty in https://github.com/element-hq/element-x-android/pull/3475 + +Changes in Element X v0.6.0 (2024-09-12) +======================================== + +### 🙌 Improvements +* Enables pinned messages feature by default. by @ganfra in https://github.com/element-hq/element-x-android/pull/3439 +* Pinned messages list : hide reactions by @ganfra in https://github.com/element-hq/element-x-android/pull/3430 + +### 🐛 Bugfixes +* Feature/fga/pinned messages fix timeline provider by @ganfra in https://github.com/element-hq/element-x-android/pull/3432 + +### Dependency upgrades +* Update activity to v1.9.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3397 +* Update peter-evans/create-pull-request action to v7 by @renovate in https://github.com/element-hq/element-x-android/pull/3383 +* Rust sdk upgrade to 0.2.43 by @bmarty in https://github.com/element-hq/element-x-android/pull/3446 + +### Others +* DeviceId and cleanup. by @bmarty in https://github.com/element-hq/element-x-android/pull/3442 +* Update application store assets by @bmarty in https://github.com/element-hq/element-x-android/pull/3441 + Changes in Element X v0.5.3 (2024-09-10) ======================================== diff --git a/upstream_infra/fastlane/metadata/android/en-US/changelogs/40006010.txt b/upstream_infra/fastlane/metadata/android/en-US/changelogs/40006010.txt new file mode 100644 index 0000000000..0574894881 --- /dev/null +++ b/upstream_infra/fastlane/metadata/android/en-US/changelogs/40006010.txt @@ -0,0 +1,2 @@ +Element X is the new generation of Element for professional and personal use on mobile. It’s the fastest Matrix client with a seamless & intuitive user interface. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/upstream_infra/fastlane/metadata/android/en-US/changelogs/40006020.txt b/upstream_infra/fastlane/metadata/android/en-US/changelogs/40006020.txt new file mode 100644 index 0000000000..0574894881 --- /dev/null +++ b/upstream_infra/fastlane/metadata/android/en-US/changelogs/40006020.txt @@ -0,0 +1,2 @@ +Element X is the new generation of Element for professional and personal use on mobile. It’s the fastest Matrix client with a seamless & intuitive user interface. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/upstream_infra/fastlane/metadata/android/en-US/changelogs/40006030.txt b/upstream_infra/fastlane/metadata/android/en-US/changelogs/40006030.txt new file mode 100644 index 0000000000..0574894881 --- /dev/null +++ b/upstream_infra/fastlane/metadata/android/en-US/changelogs/40006030.txt @@ -0,0 +1,2 @@ +Element X is the new generation of Element for professional and personal use on mobile. It’s the fastest Matrix client with a seamless & intuitive user interface. +Full changelog: https://github.com/element-hq/element-x-android/releases