Merge branch 'develop' into renovate/accompanist
This commit is contained in:
commit
025f48a72c
437 changed files with 8375 additions and 1603 deletions
2
.github/workflows/sync-localazy.yml
vendored
2
.github/workflows/sync-localazy.yml
vendored
|
|
@ -36,7 +36,7 @@ jobs:
|
|||
./tools/localazy/importSupportedLocalesFromLocalazy.py
|
||||
./tools/test/generateAllScreenshots.py
|
||||
- name: Create Pull Request for Strings
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
|
||||
commit-message: Sync Strings from Localazy
|
||||
|
|
|
|||
2
.github/workflows/sync-sas-strings.yml
vendored
2
.github/workflows/sync-sas-strings.yml
vendored
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
- name: Run SAS String script
|
||||
run: ./tools/sas/import_sas_strings.py
|
||||
- name: Create Pull Request for SAS Strings
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
commit-message: Sync SAS Strings
|
||||
title: Sync SAS Strings
|
||||
|
|
|
|||
2
.idea/dictionaries/shared.xml
generated
2
.idea/dictionaries/shared.xml
generated
|
|
@ -1,6 +1,7 @@
|
|||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="shared">
|
||||
<words>
|
||||
<w>agpl</w>
|
||||
<w>backstack</w>
|
||||
<w>blurhash</w>
|
||||
<w>fdroid</w>
|
||||
|
|
@ -17,6 +18,7 @@
|
|||
<w>securebackup</w>
|
||||
<w>showkase</w>
|
||||
<w>snackbar</w>
|
||||
<w>spdx</w>
|
||||
<w>swipeable</w>
|
||||
<w>textfields</w>
|
||||
<w>tombstoned</w>
|
||||
|
|
|
|||
67
CHANGES.md
67
CHANGES.md
|
|
@ -1,3 +1,70 @@
|
|||
Changes in Element X v0.6.3 (2024-09-19)
|
||||
========================================
|
||||
|
||||
## What's Changed
|
||||
### 🙌 Improvements
|
||||
* Iterate send failure verification by @ganfra in https://github.com/element-hq/element-x-android/pull/3485
|
||||
### 🐛 Bugfixes
|
||||
* Make sure the logout action doesn't cause a crash by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3480
|
||||
* Distinguish between roomId and roomAlias. by @bmarty in https://github.com/element-hq/element-x-android/pull/3486
|
||||
* Fix sliding sync proxy login not working after native SS failure by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3489
|
||||
### Dependency upgrades
|
||||
* SDK 0.2.47 by @ganfra in https://github.com/element-hq/element-x-android/pull/3490
|
||||
### Others
|
||||
* Add tests on AccountDeactivationView by @bmarty in https://github.com/element-hq/element-x-android/pull/3481
|
||||
* Cleanup and fixtures for SDK classes. by @bmarty in https://github.com/element-hq/element-x-android/pull/3488
|
||||
* Timeline related improvements by @ganfra in https://github.com/element-hq/element-x-android/pull/3487
|
||||
* Room list : debounce subscribe to visible rooms. by @ganfra in https://github.com/element-hq/element-x-android/pull/3491
|
||||
* Improve code coverage metrics by @bmarty in https://github.com/element-hq/element-x-android/pull/3450
|
||||
|
||||
### ✨ 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)
|
||||
========================================
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<LoggedInState> {
|
||||
@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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,6 @@ data class LoggedInState(
|
|||
val showSyncSpinner: Boolean,
|
||||
val pusherRegistrationState: AsyncData<Unit>,
|
||||
val ignoreRegistrationError: Boolean,
|
||||
val forceNativeSlidingSyncMigration: Boolean,
|
||||
val eventSink: (LoggedInEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,15 +16,18 @@ open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
|
|||
aLoggedInState(),
|
||||
aLoggedInState(showSyncSpinner = true),
|
||||
aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())),
|
||||
aLoggedInState(forceNativeSlidingSyncMigration = true),
|
||||
)
|
||||
}
|
||||
|
||||
fun aLoggedInState(
|
||||
showSyncSpinner: Boolean = false,
|
||||
pusherRegistrationState: AsyncData<Unit> = AsyncData.Uninitialized,
|
||||
forceNativeSlidingSyncMigration: Boolean = false,
|
||||
) = LoggedInState(
|
||||
showSyncSpinner = showSyncSpinner,
|
||||
pusherRegistrationState = pusherRegistrationState,
|
||||
ignoreRegistrationError = false,
|
||||
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
|
||||
eventSink = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
5
appnav/src/main/res/values-cs/translations.xml
Normal file
5
appnav/src/main/res/values-cs/translations.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_migrate_to_native_sliding_sync_action">"Odhlásit se a upgradovat"</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Váš domovský server již nepodporuje starý protokol. Chcete-li pokračovat v používání aplikace, odhlaste se a znovu se přihlaste."</string>
|
||||
</resources>
|
||||
5
appnav/src/main/res/values-de/translations.xml
Normal file
5
appnav/src/main/res/values-de/translations.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_migrate_to_native_sliding_sync_action">"Abmelden und aktualisieren"</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Dein Homeserver unterstützt das alte Protokoll nicht mehr. Bitte logge dich aus und melde dich wieder an, um die App weiter zu nutzen."</string>
|
||||
</resources>
|
||||
5
appnav/src/main/res/values-el/translations.xml
Normal file
5
appnav/src/main/res/values-el/translations.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_migrate_to_native_sliding_sync_action">"Αποσύνδεση &amp; Αναβάθμιση"</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ο οικιακός διακομιστής σου δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για να συνεχίσεις να χρησιμοποιείς την εφαρμογή."</string>
|
||||
</resources>
|
||||
5
appnav/src/main/res/values-et/translations.xml
Normal file
5
appnav/src/main/res/values-et/translations.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_migrate_to_native_sliding_sync_action">"Logi välja ja uuenda"</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Sinu koduserver enam ei toeta vana protokolli. Jätkamaks rakenduse kasutamist palun logi välja ning seejärel tagasi."</string>
|
||||
</resources>
|
||||
5
appnav/src/main/res/values-fr/translations.xml
Normal file
5
appnav/src/main/res/values-fr/translations.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_migrate_to_native_sliding_sync_action">"Déconnecter et mettre à niveau"</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Votre serveur d’accueil ne prend plus en charge l’ancien protocole. Veuillez vous déconnecter puis vous reconnecter pour continuer à utiliser l’application."</string>
|
||||
</resources>
|
||||
5
appnav/src/main/res/values-hu/translations.xml
Normal file
5
appnav/src/main/res/values-hu/translations.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_migrate_to_native_sliding_sync_action">"Kijelentkezés és frissítés"</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"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."</string>
|
||||
</resources>
|
||||
5
appnav/src/main/res/values-ru/translations.xml
Normal file
5
appnav/src/main/res/values-ru/translations.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_migrate_to_native_sliding_sync_action">"Выйти и обновить"</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ваш homeserver больше не поддерживает старый протокол. Пожалуйста, выйдите из системы и войдите снова, чтобы продолжить использование приложения."</string>
|
||||
</resources>
|
||||
5
appnav/src/main/res/values-sk/translations.xml
Normal file
5
appnav/src/main/res/values-sk/translations.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_migrate_to_native_sliding_sync_action">"Odhlásiť sa a aktualizovať"</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Váš domovský server už nepodporuje starý protokol. Ak chcete pokračovať v používaní aplikácie, odhláste sa a znova sa prihláste."</string>
|
||||
</resources>
|
||||
5
appnav/src/main/res/values/localazy.xml
Normal file
5
appnav/src/main/res/values/localazy.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_migrate_to_native_sliding_sync_action">"Log Out & Upgrade"</string>
|
||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."</string>
|
||||
</resources>
|
||||
|
|
@ -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<Boolean, Boolean, String?> { 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 <T> ReceiveTurbine<T>.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
fastlane/metadata/android/en-US/changelogs/40006000.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40006000.txt
Normal file
|
|
@ -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
|
||||
2
fastlane/metadata/android/en-US/changelogs/40006010.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40006010.txt
Normal file
|
|
@ -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
|
||||
2
fastlane/metadata/android/en-US/changelogs/40006020.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40006020.txt
Normal file
|
|
@ -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
|
||||
2
fastlane/metadata/android/en-US/changelogs/40006030.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40006030.txt
Normal file
|
|
@ -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
|
||||
|
|
@ -1,23 +1,35 @@
|
|||
Element X is the future Element.
|
||||
Element X brings you both sovereign & seamless collaboration built on Matrix.
|
||||
|
||||
It is the brand new, and fastest ever, Matrix client. It is for personal and community use, and will support enterprise functionality later this year.
|
||||
The collaboration capabilities include chat & video calls with the modern set of features such as:
|
||||
• public & private channels
|
||||
• room moderation & access conUpdatetrol
|
||||
• replies, reactions, polls, read receipts, pinned messages, etc.
|
||||
• simultaneous chat & calls (picture in picture)
|
||||
• decentralized & federated communication across organizations
|
||||
|
||||
A complete new build, Element X transforms performance. It’s not just the fastest Matrix client, it’s also fresher and more reliable.
|
||||
All this comes in a secure & sovereign fashion without compromising responsiveness or overall usability of the app:
|
||||
• enterprise-grade single sign-on
|
||||
• easy & secure login & device verification via QR-code
|
||||
• end to end encryption & zero trust
|
||||
• protection against MITM & other cyber attacks
|
||||
|
||||
It’s so fast for a number of reasons, but in particular we’ve introduced a completely new syncing service (‘sliding sync’). So even in big end-to-end encrypted chat rooms it operates incredibly quickly.
|
||||
If you’re a new user, use the new Element X app from the start. Compared to the current Element app you will get:
|
||||
• greatly enhanced performance, sleek user interface and overall better user experience
|
||||
• enterprise-grade support for single sign-on (OIDC)
|
||||
• QR-code based login & device verification
|
||||
• natively integrated Element Call for video calls
|
||||
• continuous improvements, bug fixes and new features
|
||||
|
||||
It’s fresher because we’ve rebuilt the entire user experience. All the power of Matrix - and the complexity of decentralized end-to-end encryption - is now hidden under a beautiful and intuitive user interface using the very latest frameworks and accessibility features.
|
||||
|
||||
Element X delivers speed, usability and reliability on the decentralized Matrix open standard.
|
||||
If you’re an existing user, using the current Element app - check out the new Element X and start planning your transition. The current Element app will be phased out and will only get critical security updates.
|
||||
|
||||
<b>Own your data</b>
|
||||
Matrix-based, Element X lets you self-host your data or choose from any free public server (the default is matrix.org, but there are plenty of others to choose from). However you host, you have ownership; it’s your data. You’re not the product. You’re in control.
|
||||
|
||||
<b>Interoperate natively</b>
|
||||
Enjoy the freedom of the Matrix open standard! You have native interoperability with any other Matrix-based app. So just like email, it doesn't matter if your friends are on a different Matrix-based app you can still connect and chat.
|
||||
Enjoy the freedom of the Matrix open standard! You have native interoperability with any other Matrix-based app. So just like email, it doesn't matter if your friends, partners or customers are on a different Matrix-based app - you can still connect.
|
||||
|
||||
<b>Encrypt your data</b>
|
||||
Enjoy your right to private conversations - free from data mining, ads and all the rest of it - and stay secure. Only the people in your conversation can read your messages. And Element X E2EE applies to voice and video calls too.
|
||||
Enjoy your right to private conversations - free from data mining, ads and all the rest of it - and stay secure. Only the people in your conversation can read your messages.
|
||||
|
||||
<b>Chat across multiple devices</b>
|
||||
Stay in touch wherever you are with fully synchronized message history across all your devices, even those running ‘traditional’ Element, and on the web at https://app.element.io
|
||||
Stay in touch wherever you are with fully synchronized message history across all your devices, even those running Element legacy app, and on the web at https://app.element.io
|
||||
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/5.png
Normal file
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
|
|
@ -1 +1 @@
|
|||
Fastest ever Matrix client
|
||||
Sovereign. Seamless. On Matrix
|
||||
|
|
@ -1 +1 @@
|
|||
Element X - Secure messenger
|
||||
Element X - Secure Chat & Call
|
||||
|
|
@ -3,4 +3,5 @@
|
|||
<string name="screen_analytics_settings_help_us_improve">"Deel anonieme gebruiksgegevens om ons te helpen problemen te identificeren."</string>
|
||||
<string name="screen_analytics_settings_read_terms">"Je kunt al onze voorwaarden %1$s lezen."</string>
|
||||
<string name="screen_analytics_settings_read_terms_content_link">"hier"</string>
|
||||
<string name="screen_analytics_settings_share_data">"Gebruiksgegevens delen"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
17
features/deactivation/api/build.gradle.kts
Normal file
17
features/deactivation/api/build.gradle.kts
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
50
features/deactivation/impl/build.gradle.kts
Normal file
50
features/deactivation/impl/build.gradle.kts
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<Plugin>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AccountDeactivationState> {
|
||||
@Composable
|
||||
override fun present(): AccountDeactivationState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val action: MutableState<AsyncAction<Unit>> = 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<DeactivateFormState>, updateLambda: DeactivateFormState.() -> DeactivateFormState) {
|
||||
formState.value = updateLambda(formState.value)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.deactivateAccount(
|
||||
formState: DeactivateFormState,
|
||||
action: MutableState<AsyncAction<Unit>>,
|
||||
) = launch {
|
||||
suspend {
|
||||
matrixClient.deactivateAccount(
|
||||
password = formState.password,
|
||||
eraseData = formState.eraseData,
|
||||
).getOrThrow()
|
||||
}.runCatchingUpdatingState(action)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Unit>,
|
||||
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, "")
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AccountDeactivationState> {
|
||||
private val filledForm = aDeactivateFormState(eraseData = true, password = "password")
|
||||
override val values: Sequence<AccountDeactivationState>
|
||||
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<Unit> = AsyncAction.Uninitialized,
|
||||
eventSink: (AccountDeactivationEvents) -> Unit = {},
|
||||
) = AccountDeactivationState(
|
||||
deactivateFormState = deactivateFormState,
|
||||
accountDeactivationAction = accountDeactivationAction,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -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<AccountDeactivationNode>(buildContext)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Unit>,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
14
features/deactivation/impl/src/main/res/values/localazy.xml
Normal file
14
features/deactivation/impl/src/main/res/values/localazy.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Please confirm that you want to deactivate your account. This action cannot be undone."</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages">"Delete all my messages"</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages_notice">"Warning: Future users may see incomplete conversations."</string>
|
||||
<string name="screen_deactivate_account_description">"Deactivating your account is %1$s, it will:"</string>
|
||||
<string name="screen_deactivate_account_description_bold_part">"irreversible"</string>
|
||||
<string name="screen_deactivate_account_list_item_1">"%1$s your account (you can\'t log back in, and your ID can\'t be reused)."</string>
|
||||
<string name="screen_deactivate_account_list_item_1_bold_part">"Permanently disable"</string>
|
||||
<string name="screen_deactivate_account_list_item_2">"Remove you from all chat rooms."</string>
|
||||
<string name="screen_deactivate_account_list_item_3">"Delete your account information from our identity server."</string>
|
||||
<string name="screen_deactivate_account_list_item_4">"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."</string>
|
||||
<string name="screen_deactivate_account_title">"Deactivate account"</string>
|
||||
</resources>
|
||||
|
|
@ -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<String, Boolean, Result<Unit>> { _, _ ->
|
||||
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<String, Boolean, Result<Unit>> { _, _ ->
|
||||
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<String, Boolean, Result<Unit>> { _, _ ->
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on back invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>(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<AccountDeactivationEvents>()
|
||||
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<AccountDeactivationEvents>()
|
||||
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<AccountDeactivationEvents>()
|
||||
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<AccountDeactivationEvents>()
|
||||
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<AccountDeactivationEvents>()
|
||||
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<AccountDeactivationEvents>()
|
||||
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAccountDeactivationView(
|
||||
state: AccountDeactivationState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
AccountDeactivationView(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -112,8 +112,8 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
|||
|
||||
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<AsyncAction<RoomId>>) = launch {
|
||||
suspend {
|
||||
client.getRoom(roomId)?.use {
|
||||
it.leave().getOrThrow()
|
||||
client.getInvitedRoom(roomId)?.use {
|
||||
it.declineInvite().getOrThrow()
|
||||
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
|
||||
}
|
||||
roomId
|
||||
|
|
|
|||
|
|
@ -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<Unit>(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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_join_room_join_action">"Toetreden tot de kamer"</string>
|
||||
<string name="screen_join_room_knock_action">"Klop om deel te nemen"</string>
|
||||
<string name="screen_join_room_space_not_supported_description">"%1$s ondersteunt nog geen spaces. Je kunt spaces benaderen via de webbrowser."</string>
|
||||
<string name="screen_join_room_space_not_supported_title">"Spaces worden nog niet ondersteund"</string>
|
||||
<string name="screen_join_room_subtitle_knock">"Klik op de knop hieronder en een kamerbeheerder wordt op de hoogte gebracht. Na goedkeuring kun je deelnemen aan het gesprek."</string>
|
||||
<string name="screen_join_room_subtitle_no_preview">"Je moet lid zijn van deze kamer om de berichtgeschiedenis te bekijken."</string>
|
||||
<string name="screen_join_room_title_knock">"Wil je tot deze kamer toetreden?"</string>
|
||||
<string name="screen_join_room_title_no_preview">"Voorbeeld is niet beschikbaar"</string>
|
||||
</resources>
|
||||
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ private fun SetupPinContent(
|
|||
ErrorDialog(
|
||||
title = state.setupPinFailure.title(),
|
||||
content = state.setupPinFailure.content(),
|
||||
onDismiss = {
|
||||
onSubmit = {
|
||||
state.eventSink(SetupPinEvents.ClearFailure)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ Wähle etwas Einprägsames. Bei falscher Eingabe wirst du aus der App ausgeloggt
|
|||
<item quantity="other">"Du hast %1$d Versuche zum Entsperren"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_app_lock_subtitle_wrong_pin">
|
||||
<item quantity="one">"Falsche PIN. Du hast %1$d weitere Chance"</item>
|
||||
<item quantity="other">"Falsche PIN. Du hast %1$d weitere Chancen"</item>
|
||||
<item quantity="one">"Falsche PIN. Du hast %1$d weiteren Versuch"</item>
|
||||
<item quantity="other">"Falsche PIN. Du hast %1$d weitere Versuche"</item>
|
||||
</plurals>
|
||||
<string name="screen_app_lock_use_biometric_android">"Biometrie verwenden"</string>
|
||||
<string name="screen_app_lock_use_pin_android">"PIN verwenden"</string>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<CreateAccountNode>(buildContext, listOf(inputs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ fun ChangeServerView(
|
|||
ErrorDialog(
|
||||
modifier = modifier,
|
||||
content = error.message(),
|
||||
onDismiss = {
|
||||
onSubmit = {
|
||||
eventSink.invoke(ChangeServerEvents.ClearError)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
* <pre>
|
||||
* {
|
||||
* "registration_helper_url": "https://element.io"
|
||||
* }
|
||||
* </pre>
|
||||
* .
|
||||
*/
|
||||
@Serializable
|
||||
data class ElementWellKnown(
|
||||
@SerialName("registration_helper_url")
|
||||
val registrationHelperUrl: String? = null,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Callback>().forEach { it.onLoginPasswordNeeded() }
|
||||
}
|
||||
|
||||
private fun onCreateAccountContinue(url: String) {
|
||||
plugins<Callback>().forEach { it.onCreateAccountContinue(url) }
|
||||
}
|
||||
|
||||
private fun onChangeAccountProvider() {
|
||||
plugins<Callback>().forEach { it.onChangeAccountProvider() }
|
||||
}
|
||||
|
|
@ -67,6 +72,7 @@ class ConfirmAccountProviderNode @AssistedInject constructor(
|
|||
modifier = modifier,
|
||||
onOidcDetails = ::onOidcDetails,
|
||||
onNeedLoginPassword = ::onLoginPasswordNeeded,
|
||||
onCreateAccountContinue = ::onCreateAccountContinue,
|
||||
onChange = ::onChangeAccountProvider,
|
||||
onLearnMoreClick = { openLearnMorePage(context) },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<ConfirmAccountProviderState> {
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ConfirmAccountProviderState> {
|
||||
override val values: Sequence<ConfirmAccountProviderState>
|
||||
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<LoginFlow> = AsyncData.Uninitialized,
|
||||
eventSink: (ConfirmAccountProviderEvents) -> Unit = {},
|
||||
) = ConfirmAccountProviderState(
|
||||
accountProvider = accountProvider,
|
||||
isAccountCreation = isAccountCreation,
|
||||
loginFlow = loginFlow,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<Plugin>,
|
||||
presenterFactory: CreateAccountPresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
data class Inputs(
|
||||
val url: String,
|
||||
) : NodeInputs
|
||||
|
||||
private val presenter = presenterFactory.create(inputs<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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CreateAccountState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(url: String): CreateAccountPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): CreateAccountState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val pageProgress: MutableState<Int> = remember { mutableIntStateOf(0) }
|
||||
val createAction: MutableState<AsyncAction<SessionId>> = 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<AsyncAction<SessionId>>) = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SessionId>,
|
||||
val isDebugBuild: Boolean,
|
||||
val eventSink: (CreateAccountEvents) -> Unit
|
||||
)
|
||||
|
|
@ -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<CreateAccountState> {
|
||||
override val values: Sequence<CreateAccountState>
|
||||
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<SessionId> = AsyncAction.Uninitialized,
|
||||
) = CreateAccountState(
|
||||
url = "https://example.com",
|
||||
isDebugBuild = true,
|
||||
pageProgress = pageProgress,
|
||||
createAction = createAction,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -78,10 +78,4 @@
|
|||
<string name="screen_server_confirmation_message_register">"Тут будуць захоўвацца вашыя размовы - сапраўды гэтак жа, як вы выкарыстоўваеце паштовага правайдара для захоўвання сваіх лістоў."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Вы збіраецеся ўвайсці ў %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Вы збіраецеся стварыць уліковы запіс на %1$s"</string>
|
||||
<string name="screen_waitlist_message">"Зараз існуе высокі попыт на %1$s на %2$s. Калі ласка, вярніцеся ў праграму праз некалькі дзён і паспрабуйце зноў.
|
||||
|
||||
Дзякуй за цярпенне!"</string>
|
||||
<string name="screen_waitlist_message_success">"Вітаем у %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Амаль гатова."</string>
|
||||
<string name="screen_waitlist_title_success">"Вы зарэгістраваны."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -24,5 +24,4 @@
|
|||
<string name="screen_server_confirmation_message_register">"Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли."</string>
|
||||
<string name="screen_server_confirmation_title_login">"На път сте да влезете в %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"На път сте да създадете акаунт в %1$s"</string>
|
||||
<string name="screen_waitlist_message_success">"Добре дошли в %1$s!"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -78,10 +78,4 @@ Zkuste se přihlásit ručně nebo naskenujte QR kód pomocí jiného zařízen
|
|||
<string name="screen_server_confirmation_message_register">"Zde budou uloženy vaše konverzace - podobně jako u poskytovatele e-mailových služeb uchováváte své e-maily."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Chystáte se přihlásit do služby %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Chystáte se vytvořit účet na %1$s"</string>
|
||||
<string name="screen_waitlist_message">"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!"</string>
|
||||
<string name="screen_waitlist_message_success">"Vítá vás %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Jste v pořadníku!"</string>
|
||||
<string name="screen_waitlist_title_success">"Jdete do toho!"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<string name="screen_change_server_form_notice">"Du kannst nur eine Verbindung zu einem vorhandenen Server herstellen, der Sliding Sync unterstützt. Dein Homeserver-Administrator muss das konfigurieren. %1$s"</string>
|
||||
<string name="screen_change_server_subtitle">"Wie lautet die Adresse deines Servers?"</string>
|
||||
<string name="screen_change_server_title">"Wähle deinen Server aus"</string>
|
||||
<string name="screen_create_account_title">"Konto erstellen"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Dieses Konto wurde deaktiviert."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Falscher Benutzername und/oder Passwort"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Dies ist keine gültige Benutzerkennung. Erwartetes Format: \'@user:homeserver.org\'"</string>
|
||||
|
|
@ -78,10 +79,4 @@ Versuche, dich manuell anzumelden, oder scanne den QR-Code mit einem anderen Ger
|
|||
<string name="screen_server_confirmation_message_register">"Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würden."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Du bist dabei, dich bei %1$s anzumelden"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Du bist dabei, ein Konto auf %1$s zu erstellen"</string>
|
||||
<string name="screen_waitlist_message">"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!"</string>
|
||||
<string name="screen_waitlist_message_success">"Willkommen bei %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Du bist fast am Ziel."</string>
|
||||
<string name="screen_waitlist_title_success">"Du bist dabei."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<string name="screen_change_server_form_notice">"Μπορείτε να συνδεθείς μόνο σε υπάρχοντα διακομιστή που υποστηρίζει Sliding sync. Ο διαχειριστής του οικιακού διακομιστή σου θα πρέπει να το ρυθμίσει. %1$s"</string>
|
||||
<string name="screen_change_server_subtitle">"Ποια είναι η διεύθυνση του διακομιστή σου;"</string>
|
||||
<string name="screen_change_server_title">"Επέλεξε το διακομιστή σου"</string>
|
||||
<string name="screen_create_account_title">"Δημιουργία λογαριασμού"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Αυτός ο λογαριασμός έχει απενεργοποιηθεί."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Λανθασμένο όνομα χρήστη ή κωδικός πρόσβασης"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Αυτό δεν είναι έγκυρο αναγνωριστικό χρήστη. Αναμενόμενη μορφή: \'@χρήστης:homeserver.org\'"</string>
|
||||
|
|
@ -78,10 +79,4 @@
|
|||
<string name="screen_server_confirmation_message_register">"Εδώ θα ζουν οι συνομιλίες σου - όπως θα χρησιμοποιούσες έναν πάροχο email για να διατηρήσεις τα email σου."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Πρόκειται να συνδεθείς στο %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Πρόκειται να δημιουργήσεις έναν λογαριασμό στο %1$s"</string>
|
||||
<string name="screen_waitlist_message">"Υπάρχει μεγάλη ζήτηση για το %1$s στον %2$s αυτή τη στιγμή. Επέστρεψε στην εφαρμογή σε λίγες μέρες και δοκίμασε ξανά.
|
||||
|
||||
Ευχαριστώ για την υπομονή σου!"</string>
|
||||
<string name="screen_waitlist_message_success">"Καλώς ήρθες στο %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Σχεδόν τα κατάφερες."</string>
|
||||
<string name="screen_waitlist_title_success">"Είσαι μέσα."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -37,10 +37,4 @@
|
|||
<string name="screen_server_confirmation_message_register">"Aquí es donde se alojarán tus conversaciones — justo como utilizarías un proveedor de correo electrónico para guardar tus correos electrónicos."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Estás a punto de iniciar sesión en %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Estás a punto de crear una cuenta en %1$s"</string>
|
||||
<string name="screen_waitlist_message">"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!"</string>
|
||||
<string name="screen_waitlist_message_success">"¡Bienvenido a %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Ya casi has terminado."</string>
|
||||
<string name="screen_waitlist_title_success">"Estás dentro."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<string name="screen_change_server_form_notice">"Sa saad luua ühendust vaid olemasoleva serveriga, mis toetab Sliding sync režiimi. Sinu koduserveri haldur peaks selle seadistama. %1$s"</string>
|
||||
<string name="screen_change_server_subtitle">"Mis on sinu koduserveri aadress?"</string>
|
||||
<string name="screen_change_server_title">"Vali oma server"</string>
|
||||
<string name="screen_create_account_title">"Loo kasutajakonto"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Konto on kasutusest eemaldatud."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Vigane kasutajanimi ja/või salasõna"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"See ei ole korrektne kasutajanimi. Õige vorming on: „@kasutaja:koduserver.ee“"</string>
|
||||
|
|
@ -78,10 +79,4 @@ Proovi käsitsi sisselogimist või skaneeri QR-koodi mõne muu seadmega."</strin
|
|||
<string name="screen_server_confirmation_message_register">"See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postitenuse pakkujat."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Sa oled sisselogimas koduserverisse %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Sa oled loomas kasutajakontot koduserveris %1$s"</string>
|
||||
<string name="screen_waitlist_message">"%1$s kasutamiseks %2$s koduserveris on hetkel palju huvilisi. Proovi seda samast rakendusest mõne päeva pärast.
|
||||
|
||||
Täname kannatlikkuse eest!"</string>
|
||||
<string name="screen_waitlist_message_success">"Tere tulemast rakendusse %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Peaaegu olemas."</string>
|
||||
<string name="screen_waitlist_title_success">"Oled nüüd jututoas."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<string name="screen_change_server_form_notice">"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"</string>
|
||||
<string name="screen_change_server_subtitle">"Quelle est l’adresse de votre serveur ?"</string>
|
||||
<string name="screen_change_server_title">"Choisissez votre serveur"</string>
|
||||
<string name="screen_create_account_title">"Créer un compte"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Ce compte a été désactivé."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Nom d’utilisateur et/ou mot de passe incorrects"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Il ne s’agit pas d’un identifiant utilisateur valide. Format attendu : « @user:homeserver.org »"</string>
|
||||
|
|
@ -76,10 +77,4 @@
|
|||
<string name="screen_server_confirmation_message_register">"C’est ici que vos conversations seront enregistrées, comme vous le feriez avec un fournisseur de messagerie pour conserver vos e-mails."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Vous êtes sur le point de vous connecter à %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Vous êtes sur le point de créer un compte sur %1$s"</string>
|
||||
<string name="screen_waitlist_message">"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 !"</string>
|
||||
<string name="screen_waitlist_message_success">"Bienvenue dans %1$s !"</string>
|
||||
<string name="screen_waitlist_title">"Vous y êtes presque."</string>
|
||||
<string name="screen_waitlist_title_success">"Vous y êtes."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<string name="screen_change_server_form_notice">"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"</string>
|
||||
<string name="screen_change_server_subtitle">"Mi a kiszolgálója címe?"</string>
|
||||
<string name="screen_change_server_title">"Válassza ki a kiszolgálóját"</string>
|
||||
<string name="screen_create_account_title">"Fiók létrehozása"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Ez a fiók deaktiválva lett."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Helytelen felhasználónév vagy jelszó"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Ez nem érvényes felhasználóazonosító. A várt formátum: „@user:homeserver.org”"</string>
|
||||
|
|
@ -78,10 +79,4 @@ Próbáljon meg kézileg bejelentkezni, vagy olvassa be a QR-kódot egy másik e
|
|||
<string name="screen_server_confirmation_message_register">"Itt lesznek a beszélgetései – ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Hamarosan bejelentkezik ebbe: %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Hamarosan létrehoz egy fiókot ezen: %1$s"</string>
|
||||
<string name="screen_waitlist_message">"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!"</string>
|
||||
<string name="screen_waitlist_message_success">"Üdvözli az %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Már majdnem kész van."</string>
|
||||
<string name="screen_waitlist_title_success">"Bent van."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -78,10 +78,4 @@ Coba masuk secara manual, atau pindai kode QR dengan perangkat lain."</string>
|
|||
<string name="screen_server_confirmation_message_register">"Di sinilah percakapan Anda akan berlangsung — sama seperti Anda menggunakan penyedia surel untuk menyimpan surel Anda."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Anda akan masuk ke %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Anda akan membuat akun di %1$s"</string>
|
||||
<string name="screen_waitlist_message">"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!"</string>
|
||||
<string name="screen_waitlist_message_success">"Selamat datang di %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Anda hampir selesai."</string>
|
||||
<string name="screen_waitlist_title_success">"Anda sudah masuk."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -78,10 +78,4 @@ Prova ad accedere manualmente o scansiona il codice QR con un altro dispositivo.
|
|||
<string name="screen_server_confirmation_message_register">"Qui è dove vivranno le tue conversazioni — proprio come useresti un fornitore di posta elettronica per conservare le tue email."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Stai per accedere a %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Stai per creare un account su %1$s"</string>
|
||||
<string name="screen_waitlist_message">"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!"</string>
|
||||
<string name="screen_waitlist_message_success">"Benvenuti in %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Ci sei quasi."</string>
|
||||
<string name="screen_waitlist_title_success">"Sei dentro."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -34,10 +34,4 @@
|
|||
<string name="screen_server_confirmation_message_register">"აქ იქნება თქვენი საუბრები - ისევე, როგორც თქვენ ელ. ფოსტაში ინახება თქვენი ელ.წერილები."</string>
|
||||
<string name="screen_server_confirmation_title_login">"თქვენ აპირებთ შესვლას %1$s-ში"</string>
|
||||
<string name="screen_server_confirmation_title_register">"თქვენ აპირებთ ანგარიშის შექმნას %1$s-ში"</string>
|
||||
<string name="screen_waitlist_message">"ახლა დიდი მოთხოვნაა %1$s-ზე %2$s-ში. დაბრუნდით რამდენიმე დღეში და სცადეთ ერთხელაც.
|
||||
|
||||
მადლობა მოთმენისათვის!"</string>
|
||||
<string name="screen_waitlist_message_success">"კეთილი იყოს თქვენი მობრძანება %1$s-ში!"</string>
|
||||
<string name="screen_waitlist_title">"თითქმის მზადაა."</string>
|
||||
<string name="screen_waitlist_title_success">"თქვენ შეხვედით."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
<string name="screen_change_account_provider_subtitle">"Gebruik een andere accountprovider, zoals je eigen privéserver of een zakelijke account."</string>
|
||||
<string name="screen_change_account_provider_title">"Wijzig accountprovider"</string>
|
||||
<string name="screen_change_server_error_invalid_homeserver">"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."</string>
|
||||
<string name="screen_change_server_error_invalid_well_known">"Sliding sync is niet beschikbaar vanwege een probleem in het well-known bestand:
|
||||
%1$s"</string>
|
||||
<string name="screen_change_server_error_no_sliding_sync_message">"Deze server ondersteunt op dit moment geen sliding sync."</string>
|
||||
<string name="screen_change_server_form_header">"Homeserver-URL"</string>
|
||||
<string name="screen_change_server_form_notice">"Je kunt alleen verbinding maken met een bestaande server die sliding sync ondersteunt. De beheerder van de homeserver moet dit configureren. %1$s"</string>
|
||||
|
|
@ -28,17 +30,45 @@
|
|||
<string name="screen_login_subtitle">"Matrix is een open netwerk voor veilige, gedecentraliseerde communicatie."</string>
|
||||
<string name="screen_login_title">"Welkom terug!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Inloggen bij %1$s"</string>
|
||||
<string name="screen_qr_code_login_connecting_subtitle">"Een beveiligde verbinding tot stand brengen"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"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."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Wat nu?"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Probeer opnieuw in te loggen met een QR-code voor het geval dit een netwerkprobleem was"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Als je hetzelfde probleem ondervindt, probeer dan een ander wifi-netwerk of gebruik je mobiele data in plaats van wifi."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Als dat niet werkt, log dan handmatig in"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Verbinding niet veilig"</string>
|
||||
<string name="screen_qr_code_login_device_code_subtitle">"Daar word je gevraagd om de twee cijfers in te voeren die op dit apparaat worden weergegeven."</string>
|
||||
<string name="screen_qr_code_login_device_code_title">"Voer het onderstaande nummer in op je andere apparaat"</string>
|
||||
<string name="screen_qr_code_login_error_cancelled_subtitle">"De aanmelding is geannuleerd op het andere apparaat."</string>
|
||||
<string name="screen_qr_code_login_error_cancelled_title">"Login verzoek geannuleerd"</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"De aanmelding is geweigerd op het andere apparaat."</string>
|
||||
<string name="screen_qr_code_login_error_declined_title">"Aanmelden geweigerd"</string>
|
||||
<string name="screen_qr_code_login_error_expired_subtitle">"Aanmelden is verlopen. Probeer het opnieuw."</string>
|
||||
<string name="screen_qr_code_login_error_expired_title">"De aanmelding was niet op tijd voltooid"</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR-code wordt niet ondersteund"</string>
|
||||
<string name="screen_qr_code_login_initial_state_button_title">"Klaar om te scannen"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_1">"Open %1$s op een desktopapparaat"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_2">"Klik op je afbeelding"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3">"Selecteer %1$s"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3_action">"“Nieuw apparaat koppelen”"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_4">"Scan de QR-code met dit apparaat"</string>
|
||||
<string name="screen_qr_code_login_initial_state_title">"Open %1$s op een ander apparaat om de QR-code te krijgen"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Gebruik de QR-code die op het andere apparaat wordt weergegeven."</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Probeer het opnieuw"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Verkeerde QR-code"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_button">"Ga naar camera-instellingen"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Je moet %1$s toestemming geven om de camera van je apparaat te gebruiken om verder te gaan."</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Cameratoegang toestaan om de QR-code te scannen"</string>
|
||||
<string name="screen_qr_code_login_scanning_state_title">"Scan de QR-code"</string>
|
||||
<string name="screen_qr_code_login_start_over_button">"Opnieuw beginnen"</string>
|
||||
<string name="screen_qr_code_login_unknown_error_description">"Er is een onverwachte fout opgetreden. Probeer het opnieuw."</string>
|
||||
<string name="screen_qr_code_login_verify_code_loading">"Aan het wachten op je andere apparaat"</string>
|
||||
<string name="screen_qr_code_login_verify_code_subtitle">"Je accountprovider kan om de volgende code vragen om de aanmelding te verifiëren."</string>
|
||||
<string name="screen_qr_code_login_verify_code_title">"Je verificatiecode"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Accountprovider wijzigen"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Een privéserver voor medewerkers van Element."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix is een open netwerk voor veilige, gedecentraliseerde communicatie."</string>
|
||||
<string name="screen_server_confirmation_message_register">"Dit is waar je gesprekken zullen worden bewaard — net zoals je een e-mailprovider zou gebruiken om je e-mails te bewaren."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Je staat op het punt je aan te melden bij %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Je staat op het punt een account aan te maken op %1$s"</string>
|
||||
<string name="screen_waitlist_message">"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!"</string>
|
||||
<string name="screen_waitlist_message_success">"Welkom bij %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Je bent er bijna."</string>
|
||||
<string name="screen_waitlist_title_success">"Je bent binnen."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -78,10 +78,4 @@ Spróbuj zalogować się ręcznie lub zeskanuj kod QR na innym urządzeniu."</st
|
|||
<string name="screen_server_confirmation_message_register">"Tutaj będą przechowywane Twoje konwersacje - w podobnej formie jak wiadomości widnieją na skrzynce e-mail."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Zamierzasz się zalogować do %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Zamierzasz utworzyć konto na %1$s"</string>
|
||||
<string name="screen_waitlist_message">"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ść!"</string>
|
||||
<string name="screen_waitlist_message_success">"Witamy w %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Już prawie gotowe!"</string>
|
||||
<string name="screen_waitlist_title_success">"Witamy!"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -34,10 +34,4 @@
|
|||
<string name="screen_server_confirmation_message_register">"Aqui é onde suas conversas vão ficar — assim como você usa um provedor de e-mails para manter seus e-mails."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Você está prestes a fazer login em %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Você está prestes a criar uma conta em %1$s"</string>
|
||||
<string name="screen_waitlist_message">"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!"</string>
|
||||
<string name="screen_waitlist_message_success">"Bem-vindo ao %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Você está quase lá."</string>
|
||||
<string name="screen_waitlist_title_success">"Você está dentro."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -78,10 +78,4 @@ Tenta iniciar a sessão manualmente ou digitaliza o código QR com outro disposi
|
|||
<string name="screen_server_confirmation_message_register">"É aqui que as tuas conversas vão ficar — tal como num serviço de e-mail."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Irás iniciar sessão em %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Irás criar uma conta em %1$s"</string>
|
||||
<string name="screen_waitlist_message">"Há uma grande procura pela %1$s no %2$s, de momento. Volta à aplicação daqui a uns dias e tenta novamente.
|
||||
|
||||
Obrigado!"</string>
|
||||
<string name="screen_waitlist_message_success">"Bem-vindo à %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Estás quase lá."</string>
|
||||
<string name="screen_waitlist_title_success">"Estás dentro."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -78,10 +78,4 @@
|
|||
<string name="screen_server_confirmation_message_register">"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."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Sunteți pe cale să vă conectați la %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Sunteți pe cale să creați un cont pe %1$s"</string>
|
||||
<string name="screen_waitlist_message">"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!"</string>
|
||||
<string name="screen_waitlist_message_success">"Bun venit la%1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Sunteți pe lista de așteptare"</string>
|
||||
<string name="screen_waitlist_title_success">"Sunteți conectat!"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<string name="screen_change_server_form_notice">"Вы можете подключиться только к существующему серверу, поддерживающему sliding sync. Администратору домашнего сервера потребуется настроить его. %1$s"</string>
|
||||
<string name="screen_change_server_subtitle">"Какой адрес у вашего сервера?"</string>
|
||||
<string name="screen_change_server_title">"Выберите свой сервер"</string>
|
||||
<string name="screen_create_account_title">"Создать учетную запись"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Данная учетная запись была деактивирована."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Неверное имя пользователя и/или пароль"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Это не корректный идентификатор пользователя. Ожидаемый формат: \'@user:homeserver.org\'"</string>
|
||||
|
|
@ -78,10 +79,4 @@
|
|||
<string name="screen_server_confirmation_message_register">"Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Вы собираетесь войти в %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Вы собираетесь создать учетную запись на %1$s"</string>
|
||||
<string name="screen_waitlist_message">"В настоящее время существует высокий спрос на %1$s на %2$s. Вернитесь в приложение через несколько дней и попробуйте снова.
|
||||
|
||||
Спасибо за терпение!"</string>
|
||||
<string name="screen_waitlist_message_success">"Добро пожаловать в %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Почти готово."</string>
|
||||
<string name="screen_waitlist_title_success">"Вы зарегистрированы."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<string name="screen_change_server_form_notice">"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"</string>
|
||||
<string name="screen_change_server_subtitle">"Aká je adresa vášho servera?"</string>
|
||||
<string name="screen_change_server_title">"Vyberte svoj server"</string>
|
||||
<string name="screen_create_account_title">"Vytvoriť účet"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Tento účet bol deaktivovaný."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Nesprávne používateľské meno a/alebo heslo"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Toto nie je platný identifikátor používateľa. Očakávaný formát: \'@pouzivatel:homeserver.sk\'"</string>
|
||||
|
|
@ -78,10 +79,4 @@ Skúste sa prihlásiť manuálne alebo naskenujte QR kód pomocou iného zariade
|
|||
<string name="screen_server_confirmation_message_register">"Tu budú žiť vaše konverzácie - podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Chystáte sa prihlásiť do %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Chystáte sa vytvoriť účet na %1$s"</string>
|
||||
<string name="screen_waitlist_message">"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ť!"</string>
|
||||
<string name="screen_waitlist_message_success">"Vitajte v %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Ste na čakanej listine!"</string>
|
||||
<string name="screen_waitlist_title_success">"Ste dnu!"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -78,10 +78,4 @@ Prova att logga in manuellt eller skanna QR-koden med en annan enhet."</string>
|
|||
<string name="screen_server_confirmation_message_register">"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."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Du är på väg att logga in på %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Du är på väg att skapa ett konto på %1$s"</string>
|
||||
<string name="screen_waitlist_message">"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!"</string>
|
||||
<string name="screen_waitlist_message_success">"Välkommen till %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Du är nästan framme."</string>
|
||||
<string name="screen_waitlist_title_success">"Du är inne."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -78,10 +78,4 @@
|
|||
<string name="screen_server_confirmation_message_register">"Тут будуть зберігатися Ваші розмови - так само, як Ви використовуєте поштову скриньку для зберігання своїх електронних листів."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Ви збираєтесь увійти в %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Ви збираєтеся створити обліковий запис на %1$s"</string>
|
||||
<string name="screen_waitlist_message">"На цей момент існує високий попит на %1$s в %2$s. Поверніться до застосунку через кілька днів і спробуйте ще раз.
|
||||
|
||||
Дякуємо за терпіння!"</string>
|
||||
<string name="screen_waitlist_message_success">"Ласкаво просимо до %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"Майже готово."</string>
|
||||
<string name="screen_waitlist_title_success">"Готово."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -33,10 +33,4 @@
|
|||
<string name="screen_server_confirmation_message_register">"Bu sizning suhbatlaringiz yashaydigan joy - xuddi siz elektron pochta xabarlaringizni saqlash uchun elektron pochta provayderidan foydalanganingiz kabi."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Siz tizimga kirmoqchisiz%1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Hisob yaratmoqchisiz%1$s"</string>
|
||||
<string name="screen_waitlist_message">"Hozirgi paytda %2$sga %1$sda talab yuqori. Bir necha kundan keyin ilovaga qayting va qaytadan urining.
|
||||
|
||||
Sabr-toqatingiz uchun rahmat!"</string>
|
||||
<string name="screen_waitlist_message_success">"%1$sga Xush kelibsiz!"</string>
|
||||
<string name="screen_waitlist_title">"Siz deyarli keldingiz."</string>
|
||||
<string name="screen_waitlist_title_success">"Siz kirdingiz."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -29,5 +29,4 @@
|
|||
<string name="screen_server_confirmation_message_register">"您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。"</string>
|
||||
<string name="screen_server_confirmation_title_login">"您即將登入 %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"您即將在 %1$s 建立帳號"</string>
|
||||
<string name="screen_waitlist_message_success">"歡迎使用 %1$s!"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -78,10 +78,4 @@
|
|||
<string name="screen_server_confirmation_message_register">"这是您的对话将进行的地方,就像您使用电子邮件提供商来保存电子邮件一样。"</string>
|
||||
<string name="screen_server_confirmation_title_login">"即将登录 %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"即将在 %1$s 上创建一个账户"</string>
|
||||
<string name="screen_waitlist_message">"目前 %1$s 上 %2$s 的负载很大。过几天再回来试试吧。
|
||||
|
||||
感谢您的耐心!"</string>
|
||||
<string name="screen_waitlist_message_success">"欢迎使用 %1$s"</string>
|
||||
<string name="screen_waitlist_title">"马上就好。"</string>
|
||||
<string name="screen_waitlist_title_success">"您已加入。"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<string name="screen_change_server_form_notice">"You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$s"</string>
|
||||
<string name="screen_change_server_subtitle">"What is the address of your server?"</string>
|
||||
<string name="screen_change_server_title">"Select your server"</string>
|
||||
<string name="screen_create_account_title">"Create account"</string>
|
||||
<string name="screen_login_error_deactivated_account">"This account has been deactivated."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Incorrect username and/or password"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"This is not a valid user identifier. Expected format: ‘@user:homeserver.org’"</string>
|
||||
|
|
@ -78,10 +79,4 @@ Try signing in manually, or scan the QR code with another device."</string>
|
|||
<string name="screen_server_confirmation_message_register">"This is where your conversations will live — just like you would use an email provider to keep your emails."</string>
|
||||
<string name="screen_server_confirmation_title_login">"You’re about to sign in to %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"You’re about to create an account on %1$s"</string>
|
||||
<string name="screen_waitlist_message">"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!"</string>
|
||||
<string name="screen_waitlist_message_success">"Welcome to %1$s!"</string>
|
||||
<string name="screen_waitlist_title">"You’re almost there."</string>
|
||||
<string name="screen_waitlist_title_success">"You\'re in."</string>
|
||||
</resources>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue