Merge branch 'develop' into renovate/accompanist

This commit is contained in:
ganfra 2024-09-19 13:01:23 +02:00 committed by GitHub
commit 025f48a72c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
437 changed files with 8375 additions and 1603 deletions

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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)
========================================

View file

@ -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
}

View file

@ -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)

View file

@ -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
}

View file

@ -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
)
}

View file

@ -13,5 +13,6 @@ data class LoggedInState(
val showSyncSpinner: Boolean,
val pusherRegistrationState: AsyncData<Unit>,
val ignoreRegistrationError: Boolean,
val forceNativeSlidingSyncMigration: Boolean,
val eventSink: (LoggedInEvents) -> Unit,
)

View file

@ -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 = {},
)

View file

@ -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 {

View 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>

View 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>

View 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;amp; Αναβάθμιση"</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ο οικιακός διακομιστής σου δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για να συνεχίσεις να χρησιμοποιείς την εφαρμογή."</string>
</resources>

View 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>

View 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 daccueil ne prend plus en charge lancien protocole. Veuillez vous déconnecter puis vous reconnecter pour continuer à utiliser lapplication."</string>
</resources>

View 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>

View 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>

View 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>

View 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 &amp; 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>

View file

@ -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,
)
}
}

View file

@ -0,0 +1,2 @@
Element X is the new generation of Element for professional and personal use on mobile. Its the fastest Matrix client with a seamless & intuitive user interface.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -0,0 +1,2 @@
Element X is the new generation of Element for professional and personal use on mobile. Its the fastest Matrix client with a seamless & intuitive user interface.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -0,0 +1,2 @@
Element X is the new generation of Element for professional and personal use on mobile. Its the fastest Matrix client with a seamless & intuitive user interface.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -0,0 +1,2 @@
Element X is the new generation of Element for professional and personal use on mobile. Its the fastest Matrix client with a seamless & intuitive user interface.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -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. Its not just the fastest Matrix client, its 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
Its so fast for a number of reasons, but in particular weve introduced a completely new syncing service (sliding sync). So even in big end-to-end encrypted chat rooms it operates incredibly quickly.
If youre 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
Its fresher because weve 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 youre 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; its your data. Youre not the product. Youre 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View file

@ -1 +1 @@
Fastest ever Matrix client
Sovereign. Seamless. On Matrix

View file

@ -1 +1 @@
Element X - Secure messenger
Element X - Secure Chat & Call

View file

@ -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>

View file

@ -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
}

View 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)
}

View file

@ -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

View 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)
}

View file

@ -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
}

View file

@ -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,
)
}
}

View file

@ -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)
}
}

View file

@ -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, "")
}
}

View file

@ -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,
)

View file

@ -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 = {},
)
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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,
)
}

View 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 wont be available to new or unregistered users if you choose to delete them."</string>
<string name="screen_deactivate_account_title">"Deactivate account"</string>
</resources>

View file

@ -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,
)
}

View file

@ -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,
)
}
}

View file

@ -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

View file

@ -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,

View file

@ -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>

View file

@ -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) }
)
}
}

View file

@ -116,7 +116,7 @@ private fun SetupPinContent(
ErrorDialog(
title = state.setupPinFailure.title(),
content = state.setupPinFailure.content(),
onDismiss = {
onSubmit = {
state.eventSink(SetupPinEvents.ClearFailure)
}
)

View file

@ -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,
)
}
}

View file

@ -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>

View file

@ -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)

View file

@ -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))
}
}
}

View file

@ -36,7 +36,7 @@ fun ChangeServerView(
ErrorDialog(
modifier = modifier,
content = error.message(),
onDismiss = {
onSubmit = {
eventSink.invoke(ChangeServerEvents.ClearError)
}
)

View file

@ -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,
)

View file

@ -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
}

View file

@ -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) },
)

View file

@ -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(

View file

@ -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
}

View file

@ -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
)

View file

@ -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 = {},
)

View file

@ -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()

View file

@ -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
}

View file

@ -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)
},
)
}
}

View file

@ -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)
}
}
}

View file

@ -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
)

View file

@ -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 = {}
)

View file

@ -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 = {},
)
}

View file

@ -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
)
}
}

View file

@ -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,
)

View file

@ -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) }
}
}

View file

@ -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
)
}

View file

@ -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()
}
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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. Ladministrateur de votre serveur daccueil devra le configurer. %1$s"</string>
<string name="screen_change_server_subtitle">"Quelle est ladresse 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 dutilisateur et/ou mot de passe incorrects"</string>
<string name="screen_login_error_invalid_user_id">"Il ne sagit pas dun identifiant utilisateur valide. Format attendu : « @user:homeserver.org »"</string>
@ -76,10 +77,4 @@
<string name="screen_server_confirmation_message_register">"Cest 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 à lheure actuelle. Revenez sur lapplication 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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">"Youre about to sign in to %1$s"</string>
<string name="screen_server_confirmation_title_register">"Youre 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">"Youre 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