Merge branch 'develop' into feature/fga/user_moderation_bottomsheet

This commit is contained in:
ganfra 2025-05-23 17:28:48 +02:00
commit 03e1926e14
250 changed files with 3339 additions and 2742 deletions

View file

@ -11,7 +11,7 @@ env:
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
ARCH: x86_64 ARCH: x86_64
DEVICE: pixel_7_pro DEVICE: pixel_7_pro
API_LEVEL: 35 API_LEVEL: 33
TARGET: google_apis TARGET: google_apis
jobs: jobs:
@ -56,7 +56,7 @@ jobs:
maestro-cloud: maestro-cloud:
name: Maestro test suite name: Maestro test suite
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build-apk] needs: [ build-apk ]
# Allow one per PR. # Allow one per PR.
concurrency: concurrency:
group: ${{ format('maestro-{0}', github.ref) }} group: ${{ format('maestro-{0}', github.ref) }}
@ -80,6 +80,7 @@ jobs:
- name: Install maestro - name: Install maestro
run: curl -fsSL "https://get.maestro.mobile.dev" | bash run: curl -fsSL "https://get.maestro.mobile.dev" | bash
- name: Run Maestro tests in emulator - name: Run Maestro tests in emulator
id: maestro_test
uses: reactivecircus/android-emulator-runner@v2 uses: reactivecircus/android-emulator-runner@v2
continue-on-error: true continue-on-error: true
env: env:
@ -109,3 +110,8 @@ jobs:
retention-days: 5 retention-days: 5
overwrite: true overwrite: true
if-no-files-found: error if-no-files-found: error
- name: Fail the workflow in case of error in test
if: steps.maestro_test.outcome != 'success'
run: |
echo "Maestro tests failed. Please check the logs."
exit 1

28
.github/workflows/post-release.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: Post-release
on:
push:
tags:
- 'v*'
jobs:
post-release:
runs-on: ubuntu-latest
# Skip in forks
if: github.repository == 'element-hq/element-x-android'
steps:
- name: Trigger pipeline
uses: actions/github-script@v7
with:
github-token: ${{ secrets.ENTERPRISE_ACTIONS_TOKEN }}
script: |
const tag = context.ref.replace('refs/tags/', '');
const inputs = { git_tag: tag };
await github.rest.actions.createWorkflowDispatch({
owner: 'element-hq',
repo: 'element-enterprise',
workflow_id: 'pipeline-android.yml',
ref: 'main',
inputs: inputs
});

View file

@ -82,7 +82,7 @@ jobs:
# https://github.com/codecov/codecov-action # https://github.com/codecov/codecov-action
- name: ☂️ Upload coverage reports to codecov - name: ☂️ Upload coverage reports to codecov
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
with: with:
fail_ci_if_error: true fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}

2
.idea/kotlinc.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="2.1.20" /> <option name="version" value="2.1.21" />
</component> </component>
</project> </project>

View file

@ -1,4 +1,5 @@
appId: ${MAESTRO_APP_ID} appId: ${MAESTRO_APP_ID}
androidWebViewHierarchy: devtools
--- ---
## Check that all env variables required in the whole test suite are declared (to fail faster) ## Check that all env variables required in the whole test suite are declared (to fail faster)
- runScript: ./scripts/checkEnv.js - runScript: ./scripts/checkEnv.js

View file

@ -7,22 +7,39 @@ appId: ${MAESTRO_APP_ID}
- runFlow: ../assertions/assertLoginDisplayed.yaml - runFlow: ../assertions/assertLoginDisplayed.yaml
- tapOn: - tapOn:
id: "login-continue" id: "login-continue"
## MAS page
## Conditional workflow to pass the Chrome first launch welcome page.
- runFlow:
when:
visible: 'Use without an account'
commands:
- tapOn: "Use without an account"
## For older chrome versions
- runFlow:
when:
visible: 'Accept & continue'
commands:
- tapOn: "Accept & continue"
- runFlow:
when:
visible: 'No thanks'
commands:
- tapOn: "No thanks"
## Working when running Maestro locally, but not on the CI yet.
- extendedWaitUntil:
visible:
id: "form-1"
timeout: 10000
- tapOn: - tapOn:
id: "login-email_username" id: "form-1"
- inputText: ${MAESTRO_USERNAME} - inputText: ${MAESTRO_USERNAME}
- pressKey: Enter - pressKey: Enter
- tapOn: - tapOn:
id: "login-password" id: "form-3"
- inputText: "wrong-password"
- pressKey: Enter
- tapOn: "Continue"
- tapOn: "OK"
- tapOn:
id: "login-password"
- eraseText: 20
- inputText: ${MAESTRO_PASSWORD} - inputText: ${MAESTRO_PASSWORD}
- pressKey: Enter - pressKey: Enter
- tapOn: "Continue" - tapOn: "Continue"
## Back to native world
- runFlow: ../assertions/assertSessionVerificationDisplayed.yaml - runFlow: ../assertions/assertSessionVerificationDisplayed.yaml
- runFlow: ./verifySession.yaml - runFlow: ./verifySession.yaml
- runFlow: ../assertions/assertAnalyticsDisplayed.yaml - runFlow: ../assertions/assertAnalyticsDisplayed.yaml

View file

@ -8,6 +8,6 @@ appId: ${MAESTRO_APP_ID}
- hideKeyboard - hideKeyboard
- tapOn: "Continue" - tapOn: "Continue"
- extendedWaitUntil: - extendedWaitUntil:
visible: "Device verified" visible: "Verification complete"
timeout: 30000 timeout: 30000
- tapOn: "Continue" - tapOn: "Continue"

View file

@ -7,8 +7,9 @@ appId: ${MAESTRO_APP_ID}
- tapOn: - tapOn:
text: ${MAESTRO_INVITEE1_MXID} text: ${MAESTRO_INVITEE1_MXID}
index: 1 index: 1
- tapOn: "Send invite"
- takeScreenshot: build/maestro/330-createAndDeleteDM - takeScreenshot: build/maestro/330-createAndDeleteDM
- tapOn: "maestroelement2" - tapOn: "maestroelement2"
- scroll - scroll
- tapOn: "Leave conversation" - tapOn: "Leave room"
- tapOn: "Leave" - tapOn: "Leave"

View file

@ -0,0 +1,13 @@
appId: ${MAESTRO_APP_ID}
---
- tapOn: "Start a call"
- takeScreenshot: build/maestro/700-Call
- extendedWaitUntil:
visible: "maestroelement"
timeout: 10000
- takeScreenshot: build/maestro/710-Call
# Hangup
- tapOn: "End call"
- extendedWaitUntil:
visible: "MyRoom"
timeout: 10000

View file

@ -6,5 +6,6 @@ appId: ${MAESTRO_APP_ID}
- runFlow: messages/text.yaml - runFlow: messages/text.yaml
- runFlow: messages/location.yaml - runFlow: messages/location.yaml
- runFlow: messages/poll.yaml - runFlow: messages/poll.yaml
- runFlow: call/call.yaml
- back - back
- runFlow: ../../assertions/assertHomeDisplayed.yaml - runFlow: ../../assertions/assertHomeDisplayed.yaml

View file

@ -1,3 +1,50 @@
<!-- Release notes generated using configuration in .github/release.yml at v25.05.4 -->
Changes in Element X v25.05.4
=============================
Rust SDK: https://github.com/matrix-org/matrix-rust-sdk/releases/tag/matrix-sdk-ffi%2F20250521
## What's Changed
### 🙌 Improvements
* Change (report room) : check if server supports the report room api by @ganfra in https://github.com/element-hq/element-x-android/pull/4718
### 🐛 Bugfixes
* Improve audio focus management by @bmarty in https://github.com/element-hq/element-x-android/pull/4707
* When transcoding a video fails, send it as a file by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4257
* Disable mutliple click (parallel or serial) on a room by @bmarty in https://github.com/element-hq/element-x-android/pull/4683
* Fix generic mime type used when externally sharing several files by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4715
* Fix issues on JoinedRoom / BaseRoom by @bmarty in https://github.com/element-hq/element-x-android/pull/4724
* Use the right live timeline instance in `RustRoomFactory` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4745
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4739
### 🧱 Build
* Ensure the CI is marked as failed when Maestro test is failing by @bmarty in https://github.com/element-hq/element-x-android/pull/4700
* Trigger pipeline build when a release tag is pushed by @bmarty in https://github.com/element-hq/element-x-android/pull/4741
* Fix compilation issues. by @bmarty in https://github.com/element-hq/element-x-android/pull/4750
### 📄 Documentation
* README.md: fix broken link by @richvdh in https://github.com/element-hq/element-x-android/pull/4728
### Dependency upgrades
* chore(config): migrate renovate config by @renovate in https://github.com/element-hq/element-x-android/pull/4688
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.5.13 by @renovate in https://github.com/element-hq/element-x-android/pull/4716
* fix(deps): update dependency io.sentry:sentry-android to v8.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4717
* chore(deps): update plugin sonarqube to v6.2.0.5505 by @renovate in https://github.com/element-hq/element-x-android/pull/4725
* fix(deps): update dependency com.posthog:posthog-android to v3.15.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4723
* fix(deps): update dependency com.squareup.retrofit2:retrofit-bom to v2.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4727
* chore(deps): update codecov/codecov-action action to v5.4.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4730
* fix(deps): update kotlin by @renovate in https://github.com/element-hq/element-x-android/pull/4713
* fix(deps): update dependency com.squareup.retrofit2:retrofit-bom to v3 by @renovate in https://github.com/element-hq/element-x-android/pull/4729
* fix(deps): update kotlinpoet to v2.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4732
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.5.21 by @renovate in https://github.com/element-hq/element-x-android/pull/4759
### Others
* Remove event cache feature flag by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4719
* Check homeserver when login using qr code by @bmarty in https://github.com/element-hq/element-x-android/pull/4708
* Merge on boarding module to login module by @bmarty in https://github.com/element-hq/element-x-android/pull/4746
* Allow configuration to provide multiple account providers. by @bmarty in https://github.com/element-hq/element-x-android/pull/4742
* Reduce API of JoinedRoom, caller must use the Timeline API from liveTimeline instead by @bmarty in https://github.com/element-hq/element-x-android/pull/4731
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.05.3...v25.05.4
Changes in Element X v25.05.3 Changes in Element X v25.05.3
============================= =============================

View file

@ -87,7 +87,7 @@ Just clone the project and open it in Android Studio. Make sure to select the
`app` configuration when building (as we also have sample apps in the project). `app` configuration when building (as we also have sample apps in the project).
To build against a local copy of the Rust SDK, see the [Developer To build against a local copy of the Rust SDK, see the [Developer
onboarding](docs/_developer_onboarding.md#build-the-sdk-locally) instructions. onboarding](docs/_developer_onboarding.md#building-the-sdk-locally) instructions.
## Support ## Support

View file

@ -34,11 +34,17 @@
android:value='androidx.startup' /> android:value='androidx.startup' />
</provider> </provider>
<!--
Using launchMode singleTask to avoid multiple instances of the Activity
when the app is already open. This is important for incoming share (see
https://github.com/element-hq/element-x-android/issues/4074) and for opening
the application from a mobile.element.io link.
-->
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTask"
android:theme="@style/Theme.ElementX.Splash" android:theme="@style/Theme.ElementX.Splash"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
@ -54,6 +60,9 @@
android:host="open" android:host="open"
android:scheme="elementx" /> android:scheme="elementx" />
</intent-filter> </intent-filter>
<!--
Oidc redirection
-->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -80,6 +89,21 @@
<!-- Matching asset file: https://staging.element.io/.well-known/assetlinks.json --> <!-- Matching asset file: https://staging.element.io/.well-known/assetlinks.json -->
<data android:host="staging.element.io" /> <data android:host="staging.element.io" />
</intent-filter> </intent-filter>
<!--
Element mobile links
Example: https://mobile.element.io/element?account_provider=example.org&login_hint=mxid:@alice:example.org
-->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<!-- Matching asset file: https://mobile.element.io/.well-known/assetlinks.json -->
<data android:host="mobile.element.io" />
<data android:path="/element" />
</intent-filter>
<!-- <!--
matrix.to links matrix.to links
Note: On Android 12 and higher clicking a web link (that is not an Android App Link) always shows content in a web browser Note: On Android 12 and higher clicking a web link (that is not an Android App Link) always shows content in a web browser

View file

@ -10,7 +10,4 @@ package io.element.android.appconfig
object MatrixConfiguration { object MatrixConfiguration {
const val MATRIX_TO_PERMALINK_BASE_URL: String = "https://matrix.to/#/" const val MATRIX_TO_PERMALINK_BASE_URL: String = "https://matrix.to/#/"
val clientPermalinkBaseUrl: String? = null val clientPermalinkBaseUrl: String? = null
// TODO remove this when report is fixed
const val CAN_REPORT_ROOM = false
} }

View file

@ -53,6 +53,7 @@ dependencies {
testImplementation(libs.molecule.runtime) testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.features.login.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.impl) testImplementation(projects.libraries.oidc.impl)
testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.preferences.test)

View file

@ -20,15 +20,15 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.api.LoginEntryPoint import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.features.login.api.LoginFlowType import io.element.android.features.login.api.LoginParams
import io.element.android.features.onboarding.api.OnBoardingEntryPoint
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices
import io.element.android.libraries.designsystem.utils.ScreenOrientation import io.element.android.libraries.designsystem.utils.ScreenOrientation
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
@ -39,21 +39,26 @@ import kotlinx.parcelize.Parcelize
class NotLoggedInFlowNode @AssistedInject constructor( class NotLoggedInFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val onBoardingEntryPoint: OnBoardingEntryPoint,
private val loginEntryPoint: LoginEntryPoint, private val loginEntryPoint: LoginEntryPoint,
private val notLoggedInImageLoaderFactory: NotLoggedInImageLoaderFactory, private val notLoggedInImageLoaderFactory: NotLoggedInImageLoaderFactory,
) : BaseFlowNode<NotLoggedInFlowNode.NavTarget>( ) : BaseFlowNode<NotLoggedInFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.OnBoarding, initialElement = NavTarget.Root,
savedStateMap = buildContext.savedStateMap savedStateMap = buildContext.savedStateMap
), ),
buildContext = buildContext, buildContext = buildContext,
plugins = plugins, plugins = plugins,
) { ) {
data class Params(
val loginParams: LoginParams?,
) : NodeInputs
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenBugReport() fun onOpenBugReport()
} }
private val inputs = inputs<Params>()
override fun onBuilt() { override fun onBuilt() {
super.onBuilt() super.onBuilt()
lifecycle.subscribe( lifecycle.subscribe(
@ -65,42 +70,28 @@ class NotLoggedInFlowNode @AssistedInject constructor(
sealed interface NavTarget : Parcelable { sealed interface NavTarget : Parcelable {
@Parcelize @Parcelize
data object OnBoarding : NavTarget data object Root : NavTarget
@Parcelize
data class LoginFlow(val type: LoginFlowType) : NavTarget
} }
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
NavTarget.OnBoarding -> { NavTarget.Root -> {
val callback = object : OnBoardingEntryPoint.Callback { val callback = object : LoginEntryPoint.Callback {
override fun onSignUp() {
backstack.push(NavTarget.LoginFlow(type = LoginFlowType.SIGN_UP))
}
override fun onSignIn() {
backstack.push(NavTarget.LoginFlow(type = LoginFlowType.SIGN_IN_MANUAL))
}
override fun onSignInWithQrCode() {
backstack.push(NavTarget.LoginFlow(type = LoginFlowType.SIGN_IN_QR_CODE))
}
override fun onReportProblem() { override fun onReportProblem() {
plugins<Callback>().forEach { it.onOpenBugReport() } plugins<Callback>().forEach { it.onOpenBugReport() }
} }
} }
onBoardingEntryPoint loginEntryPoint
.nodeBuilder(this, buildContext) .nodeBuilder(this, buildContext)
.params(
LoginEntryPoint.Params(
accountProvider = inputs.loginParams?.accountProvider,
loginHint = inputs.loginParams?.loginHint,
)
)
.callback(callback) .callback(callback)
.build() .build()
} }
is NavTarget.LoginFlow -> {
loginEntryPoint.nodeBuilder(this, buildContext)
.params(LoginEntryPoint.Params(flowType = navTarget.type))
.build()
}
} }
} }

View file

@ -33,6 +33,8 @@ import io.element.android.appnav.intent.ResolvedIntent
import io.element.android.appnav.root.RootNavStateFlowFactory import io.element.android.appnav.root.RootNavStateFlowFactory
import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootPresenter
import io.element.android.appnav.root.RootView import io.element.android.appnav.root.RootView
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.login.api.LoginParams
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
import io.element.android.features.signedout.api.SignedOutEntryPoint import io.element.android.features.signedout.api.SignedOutEntryPoint
import io.element.android.features.viewfolder.api.ViewFolderEntryPoint import io.element.android.features.viewfolder.api.ViewFolderEntryPoint
@ -40,6 +42,7 @@ import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.waitForChildAttached import io.element.android.libraries.architecture.waitForChildAttached
import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.deeplink.DeeplinkData import io.element.android.libraries.deeplink.DeeplinkData
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
@ -61,6 +64,7 @@ class RootFlowNode @AssistedInject constructor(
@Assisted val buildContext: BuildContext, @Assisted val buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val authenticationService: MatrixAuthenticationService, private val authenticationService: MatrixAuthenticationService,
private val enterpriseService: EnterpriseService,
private val navStateFlowFactory: RootNavStateFlowFactory, private val navStateFlowFactory: RootNavStateFlowFactory,
private val matrixSessionCache: MatrixSessionCache, private val matrixSessionCache: MatrixSessionCache,
private val presenter: RootPresenter, private val presenter: RootPresenter,
@ -99,14 +103,14 @@ class RootFlowNode @AssistedInject constructor(
if (navState.loggedInState.isTokenValid) { if (navState.loggedInState.isTokenValid) {
tryToRestoreLatestSession( tryToRestoreLatestSession(
onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) }, onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
onFailure = { switchToNotLoggedInFlow() } onFailure = { switchToNotLoggedInFlow(null) }
) )
} else { } else {
switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId)) switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId))
} }
} }
LoggedInState.NotLoggedIn -> { LoggedInState.NotLoggedIn -> {
switchToNotLoggedInFlow() switchToNotLoggedInFlow(null)
} }
} }
} }
@ -117,9 +121,9 @@ class RootFlowNode @AssistedInject constructor(
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId)) backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId))
} }
private fun switchToNotLoggedInFlow() { private fun switchToNotLoggedInFlow(params: LoginParams?) {
matrixSessionCache.removeAll() matrixSessionCache.removeAll()
backstack.safeRoot(NavTarget.NotLoggedInFlow) backstack.safeRoot(NavTarget.NotLoggedInFlow(params))
} }
private fun switchToSignedOutFlow(sessionId: SessionId) { private fun switchToSignedOutFlow(sessionId: SessionId) {
@ -175,7 +179,9 @@ class RootFlowNode @AssistedInject constructor(
data object SplashScreen : NavTarget data object SplashScreen : NavTarget
@Parcelize @Parcelize
data object NotLoggedInFlow : NavTarget data class NotLoggedInFlow(
val params: LoginParams?
) : NavTarget
@Parcelize @Parcelize
data class LoggedInFlow( data class LoggedInFlow(
@ -211,13 +217,16 @@ class RootFlowNode @AssistedInject constructor(
} }
createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback)) createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback))
} }
NavTarget.NotLoggedInFlow -> { is NavTarget.NotLoggedInFlow -> {
val callback = object : NotLoggedInFlowNode.Callback { val callback = object : NotLoggedInFlowNode.Callback {
override fun onOpenBugReport() { override fun onOpenBugReport() {
backstack.push(NavTarget.BugReport) backstack.push(NavTarget.BugReport)
} }
} }
createNode<NotLoggedInFlowNode>(buildContext, plugins = listOf(callback)) val params = NotLoggedInFlowNode.Params(
loginParams = navTarget.params,
)
createNode<NotLoggedInFlowNode>(buildContext, plugins = listOf(params, callback))
} }
is NavTarget.SignedOutFlow -> { is NavTarget.SignedOutFlow -> {
signedOutEntryPoint.nodeBuilder(this, buildContext) signedOutEntryPoint.nodeBuilder(this, buildContext)
@ -272,18 +281,36 @@ class RootFlowNode @AssistedInject constructor(
val resolvedIntent = intentResolver.resolve(intent) ?: return val resolvedIntent = intentResolver.resolve(intent) ?: return
when (resolvedIntent) { when (resolvedIntent) {
is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData) is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData)
is ResolvedIntent.Login -> onLoginLink(resolvedIntent.params)
is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction) is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData) is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData)
is ResolvedIntent.IncomingShare -> onIncomingShare(resolvedIntent.intent) is ResolvedIntent.IncomingShare -> onIncomingShare(resolvedIntent.intent)
} }
} }
private suspend fun onLoginLink(params: LoginParams) {
// Is there a session already?
val latestSessionId = authenticationService.getLatestSessionId()
if (latestSessionId == null) {
// No session, open login
if (enterpriseService.isAllowedToConnectToHomeserver(params.accountProvider.ensureProtocol())) {
switchToNotLoggedInFlow(params)
} else {
Timber.w("Login link ignored, we are not allowed to connect to the homeserver")
switchToNotLoggedInFlow(null)
}
} else {
// Just ignore the login link if we already have a session
Timber.w("Login link ignored, we already have a session")
}
}
private suspend fun onIncomingShare(intent: Intent) { private suspend fun onIncomingShare(intent: Intent) {
// Is there a session already? // Is there a session already?
val latestSessionId = authenticationService.getLatestSessionId() val latestSessionId = authenticationService.getLatestSessionId()
if (latestSessionId == null) { if (latestSessionId == null) {
// No session, open login // No session, open login
switchToNotLoggedInFlow() switchToNotLoggedInFlow(null)
} else { } else {
attachSession(latestSessionId) attachSession(latestSessionId)
.attachIncomingShare(intent) .attachIncomingShare(intent)

View file

@ -8,6 +8,8 @@
package io.element.android.appnav.intent package io.element.android.appnav.intent
import android.content.Intent import android.content.Intent
import io.element.android.features.login.api.LoginIntentResolver
import io.element.android.features.login.api.LoginParams
import io.element.android.libraries.deeplink.DeeplinkData import io.element.android.libraries.deeplink.DeeplinkData
import io.element.android.libraries.deeplink.DeeplinkParser import io.element.android.libraries.deeplink.DeeplinkParser
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
@ -21,11 +23,13 @@ sealed interface ResolvedIntent {
data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent
data class Oidc(val oidcAction: OidcAction) : ResolvedIntent data class Oidc(val oidcAction: OidcAction) : ResolvedIntent
data class Permalink(val permalinkData: PermalinkData) : ResolvedIntent data class Permalink(val permalinkData: PermalinkData) : ResolvedIntent
data class Login(val params: LoginParams) : ResolvedIntent
data class IncomingShare(val intent: Intent) : ResolvedIntent data class IncomingShare(val intent: Intent) : ResolvedIntent
} }
class IntentResolver @Inject constructor( class IntentResolver @Inject constructor(
private val deeplinkParser: DeeplinkParser, private val deeplinkParser: DeeplinkParser,
private val loginIntentResolver: LoginIntentResolver,
private val oidcIntentResolver: OidcIntentResolver, private val oidcIntentResolver: OidcIntentResolver,
private val permalinkParser: PermalinkParser, private val permalinkParser: PermalinkParser,
) { ) {
@ -40,10 +44,17 @@ class IntentResolver @Inject constructor(
val oidcAction = oidcIntentResolver.resolve(intent) val oidcAction = oidcIntentResolver.resolve(intent)
if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction) if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction)
// External link clicked? (matrix.to, element.io, etc.) val actionViewData = intent
val permalinkData = intent
.takeIf { it.action == Intent.ACTION_VIEW } .takeIf { it.action == Intent.ACTION_VIEW }
?.dataString ?.dataString
// Mobile configuration link clicked? (mobile.element.io)
val mobileLoginData = actionViewData
?.let { loginIntentResolver.parse(it) }
if (mobileLoginData != null) return ResolvedIntent.Login(mobileLoginData)
// External link clicked? (matrix.to, element.io, etc.)
val permalinkData = actionViewData
?.let { permalinkParser.parse(it) } ?.let { permalinkParser.parse(it) }
?.takeIf { it !is PermalinkData.FallbackLink } ?.takeIf { it !is PermalinkData.FallbackLink }
if (permalinkData != null) return ResolvedIntent.Permalink(permalinkData) if (permalinkData != null) return ResolvedIntent.Permalink(permalinkData)

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Abmelden und aktualisieren"</string> <string name="banner_migrate_to_native_sliding_sync_action">"Abmelden und aktualisieren"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$sunterstützt das alte Protokoll nicht mehr. Bitte melden Sie sich ab und wieder an, um die App weiter nutzen zu können."</string> <string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s unterstützt das alte Protokoll nicht mehr. Bitte melden Sie sich ab und wieder an, um die App weiter nutzen zu können."</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> <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> </resources>

View file

@ -12,6 +12,8 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.api.LoginParams
import io.element.android.features.login.test.FakeLoginIntentResolver
import io.element.android.libraries.deeplink.DeepLinkCreator import io.element.android.libraries.deeplink.DeepLinkCreator
import io.element.android.libraries.deeplink.DeeplinkData import io.element.android.libraries.deeplink.DeeplinkData
import io.element.android.libraries.deeplink.DeeplinkParser import io.element.android.libraries.deeplink.DeeplinkParser
@ -165,6 +167,7 @@ class IntentResolverTest {
userId = UserId("@alice:matrix.org") userId = UserId("@alice:matrix.org")
) )
val sut = createIntentResolver( val sut = createIntentResolver(
loginIntentResolverResult = { null },
permalinkParserResult = { permalinkData } permalinkParserResult = { permalinkData }
) )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
@ -182,7 +185,8 @@ class IntentResolverTest {
@Test @Test
fun `test resolve external permalink, FallbackLink should be ignored`() { fun `test resolve external permalink, FallbackLink should be ignored`() {
val sut = createIntentResolver( val sut = createIntentResolver(
permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) } permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) },
loginIntentResolverResult = { null },
) )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
@ -231,7 +235,8 @@ class IntentResolverTest {
@Test @Test
fun `test resolve invalid`() { fun `test resolve invalid`() {
val sut = createIntentResolver( val sut = createIntentResolver(
permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) } permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) },
loginIntentResolverResult = { null },
) )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
@ -241,11 +246,29 @@ class IntentResolverTest {
assertThat(result).isNull() assertThat(result).isNull()
} }
@Test
fun `test resolve login param`() {
val aLoginParams = LoginParams("accountProvider", null)
val sut = createIntentResolver(
loginIntentResolverResult = { aLoginParams },
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "".toUri()
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(ResolvedIntent.Login(aLoginParams))
}
private fun createIntentResolver( private fun createIntentResolver(
permalinkParserResult: (String) -> PermalinkData = { lambdaError() } permalinkParserResult: (String) -> PermalinkData = { lambdaError() },
loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() },
): IntentResolver { ): IntentResolver {
return IntentResolver( return IntentResolver(
deeplinkParser = DeeplinkParser(), deeplinkParser = DeeplinkParser(),
loginIntentResolver = FakeLoginIntentResolver(
parseResult = loginIntentResolverResult,
),
oidcIntentResolver = DefaultOidcIntentResolver( oidcIntentResolver = DefaultOidcIntentResolver(
oidcUrlParser = DefaultOidcUrlParser( oidcUrlParser = DefaultOidcUrlParser(
oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(), oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(),

@ -1 +1 @@
Subproject commit c754703e720bcb20f5dfa1ea0dcd32976825f71c Subproject commit d3dffc97bf8b39386ace2db7d857bbff05c73c18

View file

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

View file

@ -13,7 +13,7 @@ import io.element.android.libraries.matrix.api.core.SessionId
interface EnterpriseService { interface EnterpriseService {
val isEnterpriseBuild: Boolean val isEnterpriseBuild: Boolean
suspend fun isEnterpriseUser(sessionId: SessionId): Boolean suspend fun isEnterpriseUser(sessionId: SessionId): Boolean
fun defaultHomeserver(): String? fun defaultHomeserverList(): List<String>
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
fun semanticColorsLight(): SemanticColors fun semanticColorsLight(): SemanticColors

View file

@ -22,7 +22,7 @@ class DefaultEnterpriseService @Inject constructor() : EnterpriseService {
override suspend fun isEnterpriseUser(sessionId: SessionId) = false override suspend fun isEnterpriseUser(sessionId: SessionId) = false
override fun defaultHomeserver() = null override fun defaultHomeserverList(): List<String> = emptyList()
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
override fun semanticColorsLight(): SemanticColors = compoundColorsLight override fun semanticColorsLight(): SemanticColors = compoundColorsLight

View file

@ -21,9 +21,9 @@ class DefaultEnterpriseServiceTest {
} }
@Test @Test
fun `defaultHomeserver should return null`() { fun `defaultHomeserverList should return empty list`() {
val defaultEnterpriseService = DefaultEnterpriseService() val defaultEnterpriseService = DefaultEnterpriseService()
assertThat<String?>(defaultEnterpriseService.defaultHomeserver()).isNull() assertThat(defaultEnterpriseService.defaultHomeserverList()).isEmpty()
} }
@Test @Test

View file

@ -16,7 +16,7 @@ import io.element.android.tests.testutils.simulateLongTask
class FakeEnterpriseService( class FakeEnterpriseService(
override val isEnterpriseBuild: Boolean = false, override val isEnterpriseBuild: Boolean = false,
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() }, private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
private val defaultHomeserverResult: () -> String? = { A_FAKE_HOMESERVER }, private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() }, private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() }, private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() },
private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() }, private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() },
@ -27,8 +27,8 @@ class FakeEnterpriseService(
isEnterpriseUserResult(sessionId) isEnterpriseUserResult(sessionId)
} }
override fun defaultHomeserver(): String? { override fun defaultHomeserverList(): List<String> {
return defaultHomeserverResult() return defaultHomeserverListResult()
} }
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean = simulateLongTask { override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean = simulateLongTask {

View file

@ -23,7 +23,6 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.appconfig.MatrixConfiguration
import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
@ -100,6 +99,8 @@ class JoinRoomPresenter @AssistedInject constructor(
val hideInviteAvatars by remember { val hideInviteAvatars by remember {
appPreferencesStore.getHideInviteAvatarsFlow() appPreferencesStore.getHideInviteAvatarsFlow()
}.collectAsState(initial = false) }.collectAsState(initial = false)
val canReportRoom by produceState(false) { value = matrixClient.canReportRoom() }
val contentState by produceState<ContentState>( val contentState by produceState<ContentState>(
initialValue = ContentState.Loading, initialValue = ContentState.Loading,
key1 = roomInfo, key1 = roomInfo,
@ -212,7 +213,7 @@ class JoinRoomPresenter @AssistedInject constructor(
applicationName = buildMeta.applicationName, applicationName = buildMeta.applicationName,
knockMessage = knockMessage, knockMessage = knockMessage,
hideInviteAvatars = hideInviteAvatars, hideInviteAvatars = hideInviteAvatars,
canReportRoom = MatrixConfiguration.CAN_REPORT_ROOM, canReportRoom = canReportRoom,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }

View file

@ -25,14 +25,6 @@
<string name="screen_knock_requests_list_empty_state_title">"Dim cais i ymuno yn disgwyl"</string> <string name="screen_knock_requests_list_empty_state_title">"Dim cais i ymuno yn disgwyl"</string>
<string name="screen_knock_requests_list_initial_loading_title">"Yn llwytho ceisiadau i ymuno…"</string> <string name="screen_knock_requests_list_initial_loading_title">"Yn llwytho ceisiadau i ymuno…"</string>
<string name="screen_knock_requests_list_title">"Ceisiadau i ymuno"</string> <string name="screen_knock_requests_list_title">"Ceisiadau i ymuno"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="zero">"Dyw %1$s na +%2$d arall eisiau ymuno â\'r ystafell hon"</item>
<item quantity="one">"Mae %1$s +%2$d arall eisiau ymuno â\'r ystafell hon"</item>
<item quantity="two">"Mae %1$s +%2$d arall eisiau ymuno â\'r ystafell hon"</item>
<item quantity="few">"Mae %1$s +%2$d arall eisiau ymuno â\'r ystafell hon"</item>
<item quantity="many">"Mae %1$s +%2$d arall eisiau ymuno â\'r ystafell hon"</item>
<item quantity="other">"Mae %1$s +%2$d arall eisiau ymuno â\'r ystafell hon"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Gweld y cyfan"</string> <string name="screen_room_multiple_knock_requests_view_all_button_title">"Gweld y cyfan"</string>
<string name="screen_room_single_knock_request_accept_button_title">"Derbyn"</string> <string name="screen_room_single_knock_request_accept_button_title">"Derbyn"</string>
<string name="screen_room_single_knock_request_title">"Mae %1$s eisiau ymuno â\'r ystafell hon"</string> <string name="screen_room_single_knock_request_title">"Mae %1$s eisiau ymuno â\'r ystafell hon"</string>

View file

@ -101,7 +101,7 @@ class SendLocationPresenter @Inject constructor(
when (mode) { when (mode) {
SendLocationState.Mode.PinLocation -> { SendLocationState.Mode.PinLocation -> {
val geoUri = event.cameraPosition.toGeoUri() val geoUri = event.cameraPosition.toGeoUri()
room.sendLocation( room.liveTimeline.sendLocation(
body = generateBody(geoUri), body = generateBody(geoUri),
geoUri = geoUri, geoUri = geoUri,
description = null, description = null,
@ -119,7 +119,7 @@ class SendLocationPresenter @Inject constructor(
} }
SendLocationState.Mode.SenderLocation -> { SendLocationState.Mode.SenderLocation -> {
val geoUri = event.toGeoUri() val geoUri = event.toGeoUri()
room.sendLocation( room.liveTimeline.sendLocation(
body = generateBody(geoUri), body = generateBody(geoUri),
geoUri = geoUri, geoUri = geoUri,
description = null, description = null,

View file

@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTran
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
@ -266,7 +267,9 @@ class SendLocationPresenterTest {
Result.success(Unit) Result.success(Unit)
} }
val joinedRoom = FakeJoinedRoom( val joinedRoom = FakeJoinedRoom(
sendLocationResult = sendLocationResult, liveTimeline = FakeTimeline().apply {
sendLocationLambda = sendLocationResult
},
) )
val sendLocationPresenter = createSendLocationPresenter(joinedRoom) val sendLocationPresenter = createSendLocationPresenter(joinedRoom)
fakePermissionsPresenter.givenState( fakePermissionsPresenter.givenState(
@ -327,7 +330,9 @@ class SendLocationPresenterTest {
Result.success(Unit) Result.success(Unit)
} }
val joinedRoom = FakeJoinedRoom( val joinedRoom = FakeJoinedRoom(
sendLocationResult = sendLocationResult, liveTimeline = FakeTimeline().apply {
sendLocationLambda = sendLocationResult
},
) )
val sendLocationPresenter = createSendLocationPresenter(joinedRoom) val sendLocationPresenter = createSendLocationPresenter(joinedRoom)
fakePermissionsPresenter.givenState( fakePermissionsPresenter.givenState(
@ -388,7 +393,9 @@ class SendLocationPresenterTest {
Result.success(Unit) Result.success(Unit)
} }
val joinedRoom = FakeJoinedRoom( val joinedRoom = FakeJoinedRoom(
sendLocationResult = sendLocationResult, liveTimeline = FakeTimeline().apply {
sendLocationLambda = sendLocationResult
},
) )
val sendLocationPresenter = createSendLocationPresenter(joinedRoom) val sendLocationPresenter = createSendLocationPresenter(joinedRoom)
fakePermissionsPresenter.givenState( fakePermissionsPresenter.givenState(

View file

@ -24,22 +24,6 @@ Dewiswch rywbeth cofiadwy. Os byddwch chi\'n anghofio\'r PIN hwn, byddwch chi\'n
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Nid yw\'r PINau\'n cyfateb"</string> <string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Nid yw\'r PINau\'n cyfateb"</string>
<string name="screen_app_lock_signout_alert_message">"Bydd angen i chi ail-fewngofnodi a chreu PIN newydd i barhau"</string> <string name="screen_app_lock_signout_alert_message">"Bydd angen i chi ail-fewngofnodi a chreu PIN newydd i barhau"</string>
<string name="screen_app_lock_signout_alert_title">"Rydych chi\'n cael eich allgofnodi"</string> <string name="screen_app_lock_signout_alert_title">"Rydych chi\'n cael eich allgofnodi"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="zero">"Does gennych %1$d ceisiadau i ddatgloi"</item>
<item quantity="one">"Mae gennych %1$d cais i ddatgloi"</item>
<item quantity="two">"Mae gennych %1$d gais i ddatgloi"</item>
<item quantity="few">"Mae gennych %1$d chais i ddatgloi"</item>
<item quantity="many">"Mae gennych %1$d chais i ddatgloi"</item>
<item quantity="other">"Mae gennych %1$d cais i ddatgloi"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="zero">"PIN anghywir. Does gennych %1$d cais arall"</item>
<item quantity="one">"PIN anghywir. Mae gennych %1$d cais arall"</item>
<item quantity="two">"PIN anghywir. Mae gennych %1$d gais arall"</item>
<item quantity="few">"PIN anghywir. Mae gennych %1$d chais arall"</item>
<item quantity="many">"PIN anghywir. Mae gennych %1$d chais arall"</item>
<item quantity="other">"PIN anghywir. Mae gennych %1$d cais arall"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Defnyddio biometreg"</string> <string name="screen_app_lock_use_biometric_android">"Defnyddio biometreg"</string>
<string name="screen_app_lock_use_pin_android">"Defnyddio PIN"</string> <string name="screen_app_lock_use_pin_android">"Defnyddio PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Yn allgofnodi…"</string> <string name="screen_signout_in_progress_dialog_content">"Yn allgofnodi…"</string>

View file

@ -7,28 +7,26 @@
package io.element.android.features.login.api package io.element.android.features.login.api
import android.os.Parcelable
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.FeatureEntryPoint
import kotlinx.parcelize.Parcelize
interface LoginEntryPoint : FeatureEntryPoint { interface LoginEntryPoint : FeatureEntryPoint {
data class Params( data class Params(
val flowType: LoginFlowType val accountProvider: String?,
val loginHint: String?,
) )
interface Callback : Plugin {
fun onReportProblem()
}
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
interface NodeBuilder { interface NodeBuilder {
fun params(params: Params): NodeBuilder fun params(params: Params): NodeBuilder
fun callback(callback: Callback): NodeBuilder
fun build(): Node fun build(): Node
} }
} }
@Parcelize
enum class LoginFlowType : Parcelable {
SIGN_IN_MANUAL,
SIGN_IN_QR_CODE,
SIGN_UP
}

View file

@ -0,0 +1,12 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.api
interface LoginIntentResolver {
fun parse(uriString: String): LoginParams?
}

View file

@ -0,0 +1,21 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.api
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* Parameters to start the login flow, when the application is opened
* from a mobile.element.io link.
*/
@Parcelize
data class LoginParams(
val accountProvider: String,
val loginHint: String?
) : Parcelable

View file

@ -29,9 +29,11 @@ setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP)
dependencies { dependencies {
implementation(projects.appconfig) implementation(projects.appconfig)
implementation(projects.features.enterprise.api) implementation(projects.features.enterprise.api)
implementation(projects.features.rageshake.api)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.network) implementation(projects.libraries.network)
@ -56,7 +58,9 @@ dependencies {
testImplementation(libs.test.robolectric) testImplementation(libs.test.robolectric)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.features.login.test)
testImplementation(projects.features.enterprise.test) testImplementation(projects.features.enterprise.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.impl) testImplementation(projects.libraries.oidc.impl)
testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.permissions.test)

View file

@ -23,7 +23,15 @@ class DefaultLoginEntryPoint @Inject constructor() : LoginEntryPoint {
return object : LoginEntryPoint.NodeBuilder { return object : LoginEntryPoint.NodeBuilder {
override fun params(params: LoginEntryPoint.Params): LoginEntryPoint.NodeBuilder { override fun params(params: LoginEntryPoint.Params): LoginEntryPoint.NodeBuilder {
plugins += LoginFlowNode.Inputs(flowType = params.flowType) plugins += LoginFlowNode.Params(
accountProvider = params.accountProvider,
loginHint = params.loginHint,
)
return this
}
override fun callback(callback: LoginEntryPoint.Callback): LoginEntryPoint.NodeBuilder {
plugins += callback
return this return this
} }

View file

@ -0,0 +1,30 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl
import androidx.core.net.toUri
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.login.api.LoginIntentResolver
import io.element.android.features.login.api.LoginParams
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultLoginIntentResolver @Inject constructor() : LoginIntentResolver {
override fun parse(uriString: String): LoginParams? {
val uri = uriString.toUri()
if (uri.host != "mobile.element.io") return null
if (uri.path?.startsWith("/element")?.not() == true) return null
val accountProvider = uri.getQueryParameter("account_provider") ?: return null
val loginHint = uri.getQueryParameter("login_hint")
return LoginParams(
accountProvider = accountProvider,
loginHint = loginHint,
)
}
}

View file

@ -18,6 +18,7 @@ import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.singleTop import com.bumble.appyx.navmodel.backstack.operation.singleTop
@ -25,13 +26,14 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.login.api.LoginFlowType import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode 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.changeaccountprovider.ChangeAccountProviderNode
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode 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.createaccount.CreateAccountNode
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode
import io.element.android.features.login.impl.screens.onboarding.OnBoardingNode
import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
@ -57,20 +59,19 @@ class LoginFlowNode @AssistedInject constructor(
private val oidcEntryPoint: OidcEntryPoint, private val oidcEntryPoint: OidcEntryPoint,
) : BaseFlowNode<LoginFlowNode.NavTarget>( ) : BaseFlowNode<LoginFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.Root, initialElement = NavTarget.OnBoarding,
savedStateMap = buildContext.savedStateMap, savedStateMap = buildContext.savedStateMap,
), ),
buildContext = buildContext, buildContext = buildContext,
plugins = plugins, plugins = plugins,
) { ) {
private var activity: Activity? = null data class Params(
private var darkTheme: Boolean = false val accountProvider: String?,
val loginHint: String?,
data class Inputs(
val flowType: LoginFlowType,
) : NodeInputs ) : NodeInputs
private val inputs: Inputs = inputs() private var activity: Activity? = null
private var darkTheme: Boolean = false
private var customChromeTabStarted = false private var customChromeTabStarted = false
@ -96,10 +97,15 @@ class LoginFlowNode @AssistedInject constructor(
sealed interface NavTarget : Parcelable { sealed interface NavTarget : Parcelable {
@Parcelize @Parcelize
data object Root : NavTarget data object OnBoarding : NavTarget
@Parcelize @Parcelize
data object ConfirmAccountProvider : NavTarget data object QrCode : NavTarget
@Parcelize
data class ConfirmAccountProvider(
val isAccountCreation: Boolean,
) : NavTarget
@Parcelize @Parcelize
data object ChangeAccountProvider : NavTarget data object ChangeAccountProvider : NavTarget
@ -119,29 +125,57 @@ class LoginFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
NavTarget.Root -> { NavTarget.OnBoarding -> {
if (inputs.flowType == LoginFlowType.SIGN_IN_QR_CODE) { val callback = object : OnBoardingNode.Callback {
createNode<QrCodeLoginFlowNode>(buildContext) override fun onSignUp() {
} else { backstack.push(
resolve(NavTarget.ConfirmAccountProvider, buildContext) NavTarget.ConfirmAccountProvider(isAccountCreation = true)
)
}
override fun onSignIn() {
backstack.push(
NavTarget.ConfirmAccountProvider(isAccountCreation = false)
)
}
override fun onSignInWithQrCode() {
backstack.push(NavTarget.QrCode)
}
override fun onReportProblem() {
plugins<LoginEntryPoint.Callback>().forEach { it.onReportProblem() }
}
override fun onOidcDetails(oidcDetails: OidcDetails) {
navigateToMas(oidcDetails)
}
override fun onCreateAccountContinue(url: String) {
backstack.push(NavTarget.CreateAccount(url))
}
override fun onLoginPasswordNeeded() {
backstack.push(NavTarget.LoginPassword)
}
} }
val params = inputs<Params>()
val inputs = OnBoardingNode.Params(
accountProvider = params.accountProvider,
loginHint = params.loginHint,
)
createNode<OnBoardingNode>(buildContext, listOf(callback, inputs))
} }
NavTarget.ConfirmAccountProvider -> { NavTarget.QrCode -> {
createNode<QrCodeLoginFlowNode>(buildContext)
}
is NavTarget.ConfirmAccountProvider -> {
val inputs = ConfirmAccountProviderNode.Inputs( val inputs = ConfirmAccountProviderNode.Inputs(
isAccountCreation = inputs.flowType == LoginFlowType.SIGN_UP, isAccountCreation = navTarget.isAccountCreation,
) )
val callback = object : ConfirmAccountProviderNode.Callback { val callback = object : ConfirmAccountProviderNode.Callback {
override fun onOidcDetails(oidcDetails: OidcDetails) { override fun onOidcDetails(oidcDetails: OidcDetails) {
if (oidcEntryPoint.canUseCustomTab()) { navigateToMas(oidcDetails)
// In this case open a Chrome Custom tab
activity?.let {
customChromeTabStarted = true
oidcEntryPoint.openUrlInCustomTab(it, darkTheme, oidcDetails.url)
}
} else {
// Fallback to WebView mode
backstack.push(NavTarget.OidcView(oidcDetails))
}
} }
override fun onCreateAccountContinue(url: String) { override fun onCreateAccountContinue(url: String) {
@ -162,7 +196,10 @@ class LoginFlowNode @AssistedInject constructor(
val callback = object : ChangeAccountProviderNode.Callback { val callback = object : ChangeAccountProviderNode.Callback {
override fun onDone() { override fun onDone() {
// Go back to the Account Provider screen // Go back to the Account Provider screen
backstack.singleTop(NavTarget.ConfirmAccountProvider) val confirmAccountProvider = backstack.elements.value.firstOrNull {
it.key.navTarget is NavTarget.ConfirmAccountProvider
}?.key?.navTarget ?: NavTarget.ConfirmAccountProvider(isAccountCreation = false)
backstack.singleTop(confirmAccountProvider)
} }
override fun onOtherClick() { override fun onOtherClick() {
@ -176,7 +213,10 @@ class LoginFlowNode @AssistedInject constructor(
val callback = object : SearchAccountProviderNode.Callback { val callback = object : SearchAccountProviderNode.Callback {
override fun onDone() { override fun onDone() {
// Go back to the Account Provider screen // Go back to the Account Provider screen
backstack.singleTop(NavTarget.ConfirmAccountProvider) val confirmAccountProvider = backstack.elements.value.firstOrNull {
it.key.navTarget is NavTarget.ConfirmAccountProvider
}?.key?.navTarget ?: NavTarget.ConfirmAccountProvider(isAccountCreation = false)
backstack.singleTop(confirmAccountProvider)
} }
} }
@ -197,6 +237,19 @@ class LoginFlowNode @AssistedInject constructor(
} }
} }
private fun navigateToMas(oidcDetails: OidcDetails) {
if (oidcEntryPoint.canUseCustomTab()) {
// In this case open a Chrome Custom tab
activity?.let {
customChromeTabStarted = true
oidcEntryPoint.openUrlInCustomTab(it, darkTheme, oidcDetails.url)
}
} else {
// Fallback to WebView mode
backstack.push(NavTarget.OidcView(oidcDetails))
}
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
activity = requireNotNull(LocalActivity.current) activity = requireNotNull(LocalActivity.current)

View file

@ -20,14 +20,15 @@ import javax.inject.Inject
class AccountProviderDataSource @Inject constructor( class AccountProviderDataSource @Inject constructor(
enterpriseService: EnterpriseService, enterpriseService: EnterpriseService,
) { ) {
private val defaultAccountProvider = (enterpriseService.defaultHomeserver() ?: AuthenticationConfig.MATRIX_ORG_URL).let { url -> private val defaultAccountProvider = (enterpriseService.defaultHomeserverList().firstOrNull() ?: AuthenticationConfig.MATRIX_ORG_URL)
AccountProvider( .let { url ->
url = url, AccountProvider(
subtitle = null, url = url,
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL, subtitle = null,
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL, isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
) isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
} )
}
private val accountProvider: MutableStateFlow<AccountProvider> = MutableStateFlow( private val accountProvider: MutableStateFlow<AccountProvider> = MutableStateFlow(
defaultAccountProvider defaultAccountProvider

View file

@ -21,8 +21,10 @@ open class AccountProviderProvider : PreviewParameterProvider<AccountProvider> {
) )
} }
fun anAccountProvider() = AccountProvider( fun anAccountProvider(
url = AuthenticationConfig.MATRIX_ORG_URL, url: String = AuthenticationConfig.MATRIX_ORG_URL,
) = AccountProvider(
url = url,
subtitle = "Matrix.org is an open network for secure, decentralized communication.", subtitle = "Matrix.org is an open network for secure, decentralized communication.",
isPublic = true, isPublic = true,
isMatrixOrg = true, isMatrixOrg = true,

View file

@ -56,7 +56,10 @@ class ChangeServerPresenter @Inject constructor(
) = launch { ) = launch {
suspend { suspend {
if (enterpriseService.isAllowedToConnectToHomeserver(data.url).not()) { if (enterpriseService.isAllowedToConnectToHomeserver(data.url).not()) {
throw UnauthorizedAccountProviderException(data) throw UnauthorizedAccountProviderException(
unauthorisedAccountProviderTitle = data.title,
authorisedAccountProviderTitles = enterpriseService.defaultHomeserverList(),
)
} }
authenticationService.setHomeserver(data.url).map { authenticationService.setHomeserver(data.url).map {
authenticationService.getHomeserverDetails().value!! authenticationService.getHomeserverDetails().value!!

View file

@ -8,7 +8,6 @@
package io.element.android.features.login.impl.changeserver package io.element.android.features.login.impl.changeserver
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -19,7 +18,14 @@ open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerStat
aChangeServerState(), aChangeServerState(),
aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.Error(CommonStrings.error_unknown))), aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.Error(CommonStrings.error_unknown))),
aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.SlidingSyncAlert)), aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.SlidingSyncAlert)),
aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.UnauthorizedAccountProvider(anAccountProvider()))), aChangeServerState(
changeServerAction = AsyncData.Failure(
ChangeServerError.UnauthorizedAccountProvider(
unauthorisedAccountProviderTitle = "example.com",
authorisedAccountProviderTitles = listOf("element.io", "element.org"),
)
)
),
) )
} }

View file

@ -62,7 +62,7 @@ fun ChangeServerView(
content = stringResource( content = stringResource(
id = R.string.screen_change_server_error_unauthorized_homeserver, id = R.string.screen_change_server_error_unauthorized_homeserver,
LocalBuildMeta.current.applicationName, LocalBuildMeta.current.applicationName,
error.accountProvider.title, error.unauthorisedAccountProviderTitle,
), ),
onSubmit = { onSubmit = {
eventSink.invoke(ChangeServerEvents.ClearError) eventSink.invoke(ChangeServerEvents.ClearError)

View file

@ -7,8 +7,7 @@
package io.element.android.features.login.impl.changeserver package io.element.android.features.login.impl.changeserver
import io.element.android.features.login.impl.accountprovider.AccountProvider
class UnauthorizedAccountProviderException( class UnauthorizedAccountProviderException(
val accountProvider: AccountProvider, val unauthorisedAccountProviderTitle: String,
val authorisedAccountProviderTitles: List<String>,
) : Exception() ) : Exception()

View file

@ -11,7 +11,6 @@ import androidx.annotation.StringRes
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException
import io.element.android.libraries.matrix.api.auth.AuthenticationException import io.element.android.libraries.matrix.api.auth.AuthenticationException
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -26,7 +25,8 @@ sealed class ChangeServerError : Throwable() {
} }
data class UnauthorizedAccountProvider( data class UnauthorizedAccountProvider(
val accountProvider: AccountProvider, val unauthorisedAccountProviderTitle: String,
val authorisedAccountProviderTitles: List<String>,
) : ChangeServerError() ) : ChangeServerError()
data object SlidingSyncAlert : ChangeServerError() data object SlidingSyncAlert : ChangeServerError()
@ -35,7 +35,10 @@ sealed class ChangeServerError : Throwable() {
fun from(error: Throwable): ChangeServerError = when (error) { fun from(error: Throwable): ChangeServerError = when (error) {
is AuthenticationException.SlidingSyncVersion -> SlidingSyncAlert is AuthenticationException.SlidingSyncVersion -> SlidingSyncAlert
is AuthenticationException.Oidc -> Error(messageStr = error.message) is AuthenticationException.Oidc -> Error(messageStr = error.message)
is UnauthorizedAccountProviderException -> UnauthorizedAccountProvider(error.accountProvider) is UnauthorizedAccountProviderException -> UnauthorizedAccountProvider(
unauthorisedAccountProviderTitle = error.unauthorisedAccountProviderTitle,
authorisedAccountProviderTitles = error.authorisedAccountProviderTitles,
)
else -> Error(messageId = R.string.screen_change_server_error_invalid_homeserver) else -> Error(messageId = R.string.screen_change_server_error_invalid_homeserver)
} }
} }

View file

@ -0,0 +1,119 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.login
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
import io.element.android.features.login.impl.screens.onboarding.OnBoardingPresenter
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.OidcPrompt
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* This class is responsible for managing the login flow, including handling OIDC actions and
* submitting login requests.
* It's an helper to avoid code duplication. It is used by [OnBoardingPresenter] and [ConfirmAccountProviderPresenter].
*/
class LoginHelper @Inject constructor(
private val oidcActionFlow: OidcActionFlow,
private val authenticationService: MatrixAuthenticationService,
private val defaultLoginUserStory: DefaultLoginUserStory,
private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever,
) {
private val loginModeState: MutableState<AsyncData<LoginMode>> = mutableStateOf(AsyncData.Uninitialized)
@Composable
fun collectLoginMode(): State<AsyncData<LoginMode>> {
LaunchedEffect(Unit) {
oidcActionFlow.collect { oidcAction ->
if (oidcAction != null) {
onOidcAction(oidcAction)
}
}
}
return loginModeState
}
fun clearError() {
loginModeState.value = AsyncData.Uninitialized
}
fun submit(
coroutineScope: CoroutineScope,
isAccountCreation: Boolean,
homeserverUrl: String,
loginHint: String?,
) = coroutineScope.launch {
suspend {
authenticationService.setHomeserver(homeserverUrl).map {
val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!!
if (matrixHomeServerDetails.supportsOidcLogin) {
// Retrieve the details right now
val oidcPrompt = if (isAccountCreation) OidcPrompt.Create else OidcPrompt.Login
LoginMode.Oidc(
authenticationService.getOidcUrl(prompt = oidcPrompt, loginHint = loginHint).getOrThrow()
)
} else if (isAccountCreation) {
val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl)
LoginMode.AccountCreation(url)
} else if (matrixHomeServerDetails.supportsPasswordLogin) {
LoginMode.PasswordLogin
} else {
error("Unsupported login flow")
}
}.getOrThrow()
}.runCatchingUpdatingState(
state = loginModeState,
errorTransform = {
when (it) {
is AccountCreationNotSupported -> it
else -> ChangeServerError.from(it)
}
}
)
}
private suspend fun onOidcAction(oidcAction: OidcAction) {
loginModeState.value = AsyncData.Loading()
when (oidcAction) {
OidcAction.GoBack -> {
authenticationService.cancelOidcLogin()
.onSuccess {
loginModeState.value = AsyncData.Uninitialized
}
.onFailure { failure ->
loginModeState.value = AsyncData.Failure(failure)
}
}
is OidcAction.Success -> {
authenticationService.loginWithOidc(oidcAction.url)
.onSuccess { _ ->
defaultLoginUserStory.setLoginFlowIsDone(true)
}
.onFailure { failure ->
loginModeState.value = AsyncData.Failure(failure)
}
}
}
oidcActionFlow.reset()
}
}

View file

@ -0,0 +1,16 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.login
import io.element.android.libraries.matrix.api.auth.OidcDetails
sealed interface LoginMode {
data object PasswordLogin : LoginMode
data class Oidc(val oidcDetails: OidcDetails) : LoginMode
data class AccountCreation(val url: String) : LoginMode
}

View file

@ -0,0 +1,87 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.login
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
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.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun LoginModeView(
loginMode: AsyncData<LoginMode>,
onClearError: () -> Unit,
onLearnMoreClick: () -> Unit,
onOidcDetails: (OidcDetails) -> Unit,
onNeedLoginPassword: () -> Unit,
onCreateAccountContinue: (url: String) -> Unit
) {
when (loginMode) {
is AsyncData.Failure -> {
when (val error = loginMode.error) {
is ChangeServerError -> {
when (error) {
is ChangeServerError.Error -> {
ErrorDialog(
content = error.message(),
onSubmit = onClearError,
)
}
is ChangeServerError.SlidingSyncAlert -> {
SlidingSyncNotSupportedDialog(
onLearnMoreClick = {
onLearnMoreClick()
onClearError()
},
onDismiss = onClearError,
)
}
is ChangeServerError.UnauthorizedAccountProvider -> {
ErrorDialog(
content = stringResource(
id = R.string.screen_change_server_error_unauthorized_homeserver,
LocalBuildMeta.current.applicationName,
error.unauthorisedAccountProviderTitle,
),
onSubmit = onClearError,
)
}
}
}
is AccountCreationNotSupported -> {
ErrorDialog(
content = stringResource(CommonStrings.error_account_creation_not_possible),
onSubmit = onClearError,
)
}
else -> {
ErrorDialog(
content = stringResource(CommonStrings.error_unknown),
onSubmit = onClearError,
)
}
}
}
is AsyncData.Loading -> Unit // The Continue button shows the loading state
is AsyncData.Success -> {
when (val loginModeData = loginMode.data) {
is LoginMode.Oidc -> onOidcDetails(loginModeData.oidcDetails)
LoginMode.PasswordLogin -> onNeedLoginPassword()
is LoginMode.AccountCreation -> onCreateAccountContinue(loginModeData.url)
}
}
AsyncData.Uninitialized -> Unit
}
}

View file

@ -8,29 +8,39 @@
package io.element.android.features.login.impl.screens.changeaccountprovider package io.element.android.features.login.impl.screens.changeaccountprovider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import io.element.android.appconfig.AuthenticationConfig import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.features.login.impl.changeserver.ChangeServerState
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.uri.ensureProtocol
import javax.inject.Inject import javax.inject.Inject
class ChangeAccountProviderPresenter @Inject constructor( class ChangeAccountProviderPresenter @Inject constructor(
private val changeServerPresenter: Presenter<ChangeServerState>, private val changeServerPresenter: Presenter<ChangeServerState>,
private val enterpriseService: EnterpriseService,
) : Presenter<ChangeAccountProviderState> { ) : Presenter<ChangeAccountProviderState> {
@Composable @Composable
override fun present(): ChangeAccountProviderState { override fun present(): ChangeAccountProviderState {
val staticAccountProviderList = remember {
enterpriseService.defaultHomeserverList()
.map { it.ensureProtocol() }
.ifEmpty { listOf(AuthenticationConfig.MATRIX_ORG_URL) }
.map { url ->
AccountProvider(
url = url,
subtitle = null,
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
isValid = true,
)
}
}
val changeServerState = changeServerPresenter.present() val changeServerState = changeServerPresenter.present()
return ChangeAccountProviderState( return ChangeAccountProviderState(
// Just matrix.org by default for now accountProviders = staticAccountProviderList,
accountProviders = listOf(
AccountProvider(
url = AuthenticationConfig.MATRIX_ORG_URL,
subtitle = null,
isPublic = true,
isMatrixOrg = true,
isValid = true,
)
),
changeServerState = changeServerState, changeServerState = changeServerState,
) )
} }

View file

@ -11,7 +11,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.features.login.impl.changeserver.ChangeServerState
// Do not use default value, so no member get forgotten in the presenters. // Do not use default value, so no member get forgotten in the presenters.
data class ChangeAccountProviderState constructor( data class ChangeAccountProviderState(
val accountProviders: List<AccountProvider>, val accountProviders: List<AccountProvider>,
val changeServerState: ChangeServerState, val changeServerState: ChangeServerState,
) )

View file

@ -8,38 +8,20 @@
package io.element.android.features.login.impl.screens.confirmaccountprovider package io.element.android.features.login.impl.screens.confirmaccountprovider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject 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.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.features.login.impl.login.LoginHelper
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.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.OidcPrompt
import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class ConfirmAccountProviderPresenter @AssistedInject constructor( class ConfirmAccountProviderPresenter @AssistedInject constructor(
@Assisted private val params: Params, @Assisted private val params: Params,
private val accountProviderDataSource: AccountProviderDataSource, private val accountProviderDataSource: AccountProviderDataSource,
private val authenticationService: MatrixAuthenticationService, private val loginHelper: LoginHelper,
private val oidcActionFlow: OidcActionFlow,
private val defaultLoginUserStory: DefaultLoginUserStory,
private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever,
) : Presenter<ConfirmAccountProviderState> { ) : Presenter<ConfirmAccountProviderState> {
data class Params( data class Params(
val isAccountCreation: Boolean, val isAccountCreation: Boolean,
@ -55,91 +37,27 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
val accountProvider by accountProviderDataSource.flow.collectAsState() val accountProvider by accountProviderDataSource.flow.collectAsState()
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val loginFlowAction: MutableState<AsyncData<LoginFlow>> = remember { val loginMode by loginHelper.collectLoginMode()
mutableStateOf(AsyncData.Uninitialized)
}
LaunchedEffect(Unit) {
oidcActionFlow.collect { oidcAction ->
if (oidcAction != null) {
onOidcAction(oidcAction, loginFlowAction)
}
}
}
fun handleEvents(event: ConfirmAccountProviderEvents) { fun handleEvents(event: ConfirmAccountProviderEvents) {
when (event) { when (event) {
ConfirmAccountProviderEvents.Continue -> { ConfirmAccountProviderEvents.Continue -> {
localCoroutineScope.submit(accountProvider.url, loginFlowAction) loginHelper.submit(
coroutineScope = localCoroutineScope,
isAccountCreation = params.isAccountCreation,
homeserverUrl = accountProvider.url,
loginHint = null,
)
} }
ConfirmAccountProviderEvents.ClearError -> loginFlowAction.value = AsyncData.Uninitialized ConfirmAccountProviderEvents.ClearError -> loginHelper.clearError()
} }
} }
return ConfirmAccountProviderState( return ConfirmAccountProviderState(
accountProvider = accountProvider, accountProvider = accountProvider,
isAccountCreation = params.isAccountCreation, isAccountCreation = params.isAccountCreation,
loginFlow = loginFlowAction.value, loginMode = loginMode,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
private fun CoroutineScope.submit(
homeserverUrl: String,
loginFlowAction: MutableState<AsyncData<LoginFlow>>,
) = launch {
suspend {
authenticationService.setHomeserver(homeserverUrl).map {
val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!!
if (matrixHomeServerDetails.supportsOidcLogin) {
// Retrieve the details right now
val oidcPrompt = if (params.isAccountCreation) OidcPrompt.Create else OidcPrompt.Login
LoginFlow.OidcFlow(authenticationService.getOidcUrl(oidcPrompt).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(
state = loginFlowAction,
errorTransform = {
when (it) {
is AccountCreationNotSupported -> it
else -> ChangeServerError.from(it)
}
}
)
}
private suspend fun onOidcAction(
oidcAction: OidcAction,
loginFlowAction: MutableState<AsyncData<LoginFlow>>,
) {
loginFlowAction.value = AsyncData.Loading()
when (oidcAction) {
OidcAction.GoBack -> {
authenticationService.cancelOidcLogin()
.onSuccess {
loginFlowAction.value = AsyncData.Uninitialized
}
.onFailure { failure ->
loginFlowAction.value = AsyncData.Failure(failure)
}
}
is OidcAction.Success -> {
authenticationService.loginWithOidc(oidcAction.url)
.onSuccess { _ ->
defaultLoginUserStory.setLoginFlowIsDone(true)
}
.onFailure { failure ->
loginFlowAction.value = AsyncData.Failure(failure)
}
}
}
oidcActionFlow.reset()
}
} }

View file

@ -8,21 +8,15 @@
package io.element.android.features.login.impl.screens.confirmaccountprovider package io.element.android.features.login.impl.screens.confirmaccountprovider
import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.auth.OidcDetails
// Do not use default value, so no member get forgotten in the presenters. // Do not use default value, so no member get forgotten in the presenters.
data class ConfirmAccountProviderState( data class ConfirmAccountProviderState(
val accountProvider: AccountProvider, val accountProvider: AccountProvider,
val isAccountCreation: Boolean, val isAccountCreation: Boolean,
val loginFlow: AsyncData<LoginFlow>, val loginMode: AsyncData<LoginMode>,
val eventSink: (ConfirmAccountProviderEvents) -> Unit val eventSink: (ConfirmAccountProviderEvents) -> Unit
) { ) {
val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginFlow is AsyncData.Uninitialized || loginFlow is AsyncData.Loading) val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading)
}
sealed interface LoginFlow {
data object PasswordLogin : LoginFlow
data class OidcFlow(val oidcDetails: OidcDetails) : LoginFlow
data class AccountCreationFlow(val url: String) : LoginFlow
} }

View file

@ -10,6 +10,7 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.features.login.impl.login.LoginMode
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -22,7 +23,7 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<Confir
), ),
aConfirmAccountProviderState( aConfirmAccountProviderState(
isAccountCreation = true, isAccountCreation = true,
loginFlow = AsyncData.Failure(AccountCreationNotSupported()) loginMode = AsyncData.Failure(AccountCreationNotSupported())
), ),
) )
} }
@ -30,11 +31,11 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<Confir
private fun aConfirmAccountProviderState( private fun aConfirmAccountProviderState(
accountProvider: AccountProvider = anAccountProvider(), accountProvider: AccountProvider = anAccountProvider(),
isAccountCreation: Boolean = false, isAccountCreation: Boolean = false,
loginFlow: AsyncData<LoginFlow> = AsyncData.Uninitialized, loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
eventSink: (ConfirmAccountProviderEvents) -> Unit = {}, eventSink: (ConfirmAccountProviderEvents) -> Unit = {},
) = ConfirmAccountProviderState( ) = ConfirmAccountProviderState(
accountProvider = accountProvider, accountProvider = accountProvider,
isAccountCreation = isAccountCreation, isAccountCreation = isAccountCreation,
loginFlow = loginFlow, loginMode = loginMode,
eventSink = eventSink eventSink = eventSink
) )

View file

@ -19,15 +19,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog import io.element.android.features.login.impl.login.LoginModeView
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.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
@ -47,9 +44,9 @@ fun ConfirmAccountProviderView(
onChange: () -> Unit, onChange: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val isLoading by remember(state.loginFlow) { val isLoading by remember(state.loginMode) {
derivedStateOf { derivedStateOf {
state.loginFlow is AsyncData.Loading state.loginMode is AsyncData.Loading
} }
} }
val eventSink = state.eventSink val eventSink = state.eventSink
@ -99,48 +96,16 @@ fun ConfirmAccountProviderView(
} }
} }
) { ) {
when (state.loginFlow) { LoginModeView(
is AsyncData.Failure -> { loginMode = state.loginMode,
when (val error = state.loginFlow.error) { onClearError = {
is ChangeServerError.Error -> { eventSink(ConfirmAccountProviderEvents.ClearError)
ErrorDialog( },
content = error.message(), onLearnMoreClick = onLearnMoreClick,
onSubmit = { onOidcDetails = onOidcDetails,
eventSink.invoke(ConfirmAccountProviderEvents.ClearError) onNeedLoginPassword = onNeedLoginPassword,
} onCreateAccountContinue = onCreateAccountContinue,
) )
}
is ChangeServerError.SlidingSyncAlert -> {
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)
}
)
}
}
}
is AsyncData.Loading -> Unit // The Continue button shows the loading state
is AsyncData.Success -> {
when (val loginFlowState = state.loginFlow.data) {
is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails)
LoginFlow.PasswordLogin -> onNeedLoginPassword()
is LoginFlow.AccountCreationFlow -> onCreateAccountContinue(loginFlowState.url)
}
}
AsyncData.Uninitialized -> Unit
}
} }
} }

View file

@ -0,0 +1,16 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.screens.onboarding
sealed interface OnBoardingEvents {
data class OnSignIn(
val defaultAccountProvider: String
) : OnBoardingEvents
data object ClearError : OnBoardingEvents
}

View file

@ -0,0 +1,101 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.screens.onboarding
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 com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.OidcDetails
@ContributesNode(AppScope::class)
class OnBoardingNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: OnBoardingPresenter.Factory,
) : Node(
buildContext = buildContext,
plugins = plugins
) {
interface Callback : Plugin {
fun onSignUp()
fun onSignIn()
fun onSignInWithQrCode()
fun onReportProblem()
fun onLoginPasswordNeeded()
fun onOidcDetails(oidcDetails: OidcDetails)
fun onCreateAccountContinue(url: String)
}
data class Params(
val accountProvider: String?,
val loginHint: String?,
) : NodeInputs
private val params = inputs<Params>()
private val presenter = presenterFactory.create(
params = params,
)
private fun onSignIn() {
plugins<Callback>().forEach { it.onSignIn() }
}
private fun onSignUp() {
plugins<Callback>().forEach { it.onSignUp() }
}
private fun onSignInWithQrCode() {
plugins<Callback>().forEach { it.onSignInWithQrCode() }
}
private fun onReportProblem() {
plugins<Callback>().forEach { it.onReportProblem() }
}
private fun onOidcDetails(data: OidcDetails) {
plugins<Callback>().forEach { it.onOidcDetails(data) }
}
private fun onLoginPasswordNeeded() {
plugins<Callback>().forEach { it.onLoginPasswordNeeded() }
}
private fun onCreateAccountContinue(url: String) {
plugins<Callback>().forEach { it.onCreateAccountContinue(url) }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val context = LocalContext.current
OnBoardingView(
state = state,
modifier = modifier,
onSignIn = ::onSignIn,
onCreateAccount = ::onSignUp,
onSignInWithQrCode = ::onSignInWithQrCode,
onReportProblem = ::onReportProblem,
onOidcDetails = ::onOidcDetails,
onNeedLoginPassword = ::onLoginPasswordNeeded,
onLearnMoreClick = { openLearnMorePage(context) },
onCreateAccountContinue = ::onCreateAccountContinue,
)
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.screens.onboarding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
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.appconfig.OnBoardingConfig
import io.element.android.features.login.impl.login.LoginHelper
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
class OnBoardingPresenter @AssistedInject constructor(
@Assisted private val params: OnBoardingNode.Params,
private val buildMeta: BuildMeta,
private val featureFlagService: FeatureFlagService,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val loginHelper: LoginHelper,
) : Presenter<OnBoardingState> {
@AssistedFactory
interface Factory {
fun create(
params: OnBoardingNode.Params,
): OnBoardingPresenter
}
private val defaultAccountProvider = params.accountProvider
private val loginHint = params.loginHint
@Composable
override fun present(): OnBoardingState {
val localCoroutineScope = rememberCoroutineScope()
val canLoginWithQrCode by produceState(initialValue = false) {
value = defaultAccountProvider == null &&
featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin)
}
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
val loginMode by loginHelper.collectLoginMode()
fun handleEvent(event: OnBoardingEvents) {
when (event) {
is OnBoardingEvents.OnSignIn -> loginHelper.submit(
coroutineScope = localCoroutineScope,
isAccountCreation = false,
homeserverUrl = event.defaultAccountProvider,
loginHint = loginHint,
)
OnBoardingEvents.ClearError -> loginHelper.clearError()
}
}
return OnBoardingState(
productionApplicationName = buildMeta.productionApplicationName,
defaultAccountProvider = defaultAccountProvider,
canLoginWithQrCode = canLoginWithQrCode,
canCreateAccount = defaultAccountProvider == null && OnBoardingConfig.CAN_CREATE_ACCOUNT,
canReportBug = canReportBug,
loginMode = loginMode,
eventSink = ::handleEvent,
)
}
}

View file

@ -0,0 +1,24 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.screens.onboarding
import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData
data class OnBoardingState(
val productionApplicationName: String,
val defaultAccountProvider: String?,
val canLoginWithQrCode: Boolean,
val canCreateAccount: Boolean,
val canReportBug: Boolean,
val loginMode: AsyncData<LoginMode>,
val eventSink: (OnBoardingEvents) -> Unit,
) {
val submitEnabled: Boolean
get() = defaultAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading)
}

View file

@ -5,9 +5,11 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
package io.element.android.features.onboarding.impl package io.element.android.features.login.impl.screens.onboarding
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData
open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> { open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> {
override val values: Sequence<OnBoardingState> override val values: Sequence<OnBoardingState>
@ -17,17 +19,24 @@ open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> {
anOnBoardingState(canCreateAccount = true), anOnBoardingState(canCreateAccount = true),
anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true), anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true),
anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true, canReportBug = true), anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true, canReportBug = true),
anOnBoardingState(defaultAccountProvider = "element.io", canCreateAccount = false, canReportBug = true),
) )
} }
fun anOnBoardingState( fun anOnBoardingState(
productionApplicationName: String = "Element", productionApplicationName: String = "Element",
defaultAccountProvider: String? = null,
canLoginWithQrCode: Boolean = false, canLoginWithQrCode: Boolean = false,
canCreateAccount: Boolean = false, canCreateAccount: Boolean = false,
canReportBug: Boolean = false, canReportBug: Boolean = false,
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
eventSink: (OnBoardingEvents) -> Unit = {},
) = OnBoardingState( ) = OnBoardingState(
productionApplicationName = productionApplicationName, productionApplicationName = productionApplicationName,
defaultAccountProvider = defaultAccountProvider,
canLoginWithQrCode = canLoginWithQrCode, canLoginWithQrCode = canLoginWithQrCode,
canCreateAccount = canCreateAccount, canCreateAccount = canCreateAccount,
canReportBug = canReportBug, canReportBug = canReportBug,
loginMode = loginMode,
eventSink = eventSink,
) )

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
package io.element.android.features.onboarding.impl package io.element.android.features.login.impl.screens.onboarding
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -16,6 +16,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.BiasAlignment import androidx.compose.ui.BiasAlignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -26,6 +29,9 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.login.LoginModeView
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
@ -36,6 +42,7 @@ import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -51,6 +58,10 @@ fun OnBoardingView(
onSignInWithQrCode: () -> Unit, onSignInWithQrCode: () -> Unit,
onSignIn: () -> Unit, onSignIn: () -> Unit,
onCreateAccount: () -> Unit, onCreateAccount: () -> Unit,
onOidcDetails: (OidcDetails) -> Unit,
onNeedLoginPassword: () -> Unit,
onLearnMoreClick: () -> Unit,
onCreateAccountContinue: (url: String) -> Unit,
onReportProblem: () -> Unit, onReportProblem: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -58,6 +69,16 @@ fun OnBoardingView(
modifier = modifier, modifier = modifier,
content = { content = {
OnBoardingContent(state = state) OnBoardingContent(state = state)
LoginModeView(
loginMode = state.loginMode,
onClearError = {
state.eventSink(OnBoardingEvents.ClearError)
},
onLearnMoreClick = onLearnMoreClick,
onOidcDetails = onOidcDetails,
onNeedLoginPassword = onNeedLoginPassword,
onCreateAccountContinue = onCreateAccountContinue,
)
}, },
footer = { footer = {
OnBoardingButtons( OnBoardingButtons(
@ -126,6 +147,12 @@ private fun OnBoardingButtons(
onCreateAccount: () -> Unit, onCreateAccount: () -> Unit,
onReportProblem: () -> Unit, onReportProblem: () -> Unit,
) { ) {
val isLoading by remember(state.loginMode) {
derivedStateOf {
state.loginMode is AsyncData.Loading
}
}
ButtonColumnMolecule { ButtonColumnMolecule {
val signInButtonStringRes = if (state.canLoginWithQrCode || state.canCreateAccount) { val signInButtonStringRes = if (state.canLoginWithQrCode || state.canCreateAccount) {
R.string.screen_onboarding_sign_in_manually R.string.screen_onboarding_sign_in_manually
@ -140,13 +167,27 @@ private fun OnBoardingButtons(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
} }
Button( val defaultAccountProvider = state.defaultAccountProvider
text = stringResource(id = signInButtonStringRes), if (defaultAccountProvider == null) {
onClick = onSignIn, Button(
modifier = Modifier text = stringResource(id = signInButtonStringRes),
.fillMaxWidth() onClick = onSignIn,
.testTag(TestTags.onBoardingSignIn) modifier = Modifier
) .fillMaxWidth()
.testTag(TestTags.onBoardingSignIn)
)
} else {
Button(
text = stringResource(id = R.string.screen_onboarding_sign_in_to, defaultAccountProvider),
showProgress = isLoading,
onClick = {
state.eventSink(OnBoardingEvents.OnSignIn(defaultAccountProvider))
},
enabled = state.submitEnabled || isLoading,
modifier = Modifier
.fillMaxWidth()
)
}
if (state.canCreateAccount) { if (state.canCreateAccount) {
TextButton( TextButton(
text = stringResource(id = R.string.screen_onboarding_sign_up), text = stringResource(id = R.string.screen_onboarding_sign_up),
@ -180,5 +221,9 @@ internal fun OnBoardingViewPreview(
onSignIn = {}, onSignIn = {},
onCreateAccount = {}, onCreateAccount = {},
onReportProblem = {}, onReportProblem = {},
onOidcDetails = {},
onNeedLoginPassword = {},
onLearnMoreClick = {},
onCreateAccountContinue = {},
) )
} }

View file

@ -15,6 +15,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException
import io.element.android.features.login.impl.qrcode.QrCodeLoginManager import io.element.android.features.login.impl.qrcode.QrCodeLoginManager
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
@ -36,6 +38,7 @@ class QrCodeScanPresenter @Inject constructor(
private val qrCodeLoginDataFactory: MatrixQrCodeLoginDataFactory, private val qrCodeLoginDataFactory: MatrixQrCodeLoginDataFactory,
private val qrCodeLoginManager: QrCodeLoginManager, private val qrCodeLoginManager: QrCodeLoginManager,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
private val enterpriseService: EnterpriseService,
) : Presenter<QrCodeScanState> { ) : Presenter<QrCodeScanState> {
private var isScanning by mutableStateOf(true) private var isScanning by mutableStateOf(true)
@ -90,9 +93,17 @@ class QrCodeScanPresenter @Inject constructor(
launch(coroutineDispatchers.computation) { launch(coroutineDispatchers.computation) {
suspend { suspend {
qrCodeLoginDataFactory.parseQrCodeData(code).onFailure { val data = qrCodeLoginDataFactory.parseQrCodeData(code).onFailure {
Timber.e(it, "Error parsing QR code data") Timber.e(it, "Error parsing QR code data")
}.getOrThrow() }.getOrThrow()
val serverName = data.serverName()
if (serverName != null && enterpriseService.isAllowedToConnectToHomeserver(serverName).not()) {
throw UnauthorizedAccountProviderException(
unauthorisedAccountProviderTitle = serverName,
authorisedAccountProviderTitles = enterpriseService.defaultHomeserverList(),
)
}
data
}.runCatchingUpdatingState(codeScannedAction) }.runCatchingUpdatingState(codeScannedAction)
}.invokeOnCompletion { }.invokeOnCompletion {
isProcessingCode.set(false) isProcessingCode.set(false)

View file

@ -8,6 +8,7 @@
package io.element.android.features.login.impl.screens.qrcode.scan package io.element.android.features.login.impl.screens.qrcode.scan
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
@ -19,6 +20,15 @@ open class QrCodeScanStateProvider : PreviewParameterProvider<QrCodeScanState> {
aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Loading), aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Loading),
aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Failure(Exception("Error"))), aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Failure(Exception("Error"))),
aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Failure(QrLoginException.OtherDeviceNotSignedIn)), aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Failure(QrLoginException.OtherDeviceNotSignedIn)),
aQrCodeScanState(
isScanning = false,
authenticationAction = AsyncAction.Failure(
UnauthorizedAccountProviderException(
unauthorisedAccountProviderTitle = "example.com",
authorisedAccountProviderTitles = listOf("element.io", "element.org"),
)
)
),
// Add other state here // Add other state here
) )
} }

View file

@ -35,6 +35,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.BigIcon
@ -144,6 +145,12 @@ private fun ColumnScope.Buttons(
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text( Text(
text = when (error) { text = when (error) {
is UnauthorizedAccountProviderException -> {
stringResource(
id = R.string.screen_change_server_error_unauthorized_homeserver_title,
error.unauthorisedAccountProviderTitle,
)
}
is QrLoginException.OtherDeviceNotSignedIn -> { is QrLoginException.OtherDeviceNotSignedIn -> {
stringResource(R.string.screen_qr_code_login_device_not_signed_in_scan_state_subtitle) stringResource(R.string.screen_qr_code_login_device_not_signed_in_scan_state_subtitle)
} }
@ -156,6 +163,12 @@ private fun ColumnScope.Buttons(
} }
Text( Text(
text = when (error) { text = when (error) {
is UnauthorizedAccountProviderException -> {
stringResource(
id = R.string.screen_change_server_error_unauthorized_homeserver_content,
error.authorisedAccountProviderTitles.joinToString(),
)
}
is QrLoginException.OtherDeviceNotSignedIn -> { is QrLoginException.OtherDeviceNotSignedIn -> {
stringResource(R.string.screen_qr_code_login_device_not_signed_in_scan_state_description) stringResource(R.string.screen_qr_code_login_device_not_signed_in_scan_state_description)
} }

View file

@ -29,6 +29,12 @@
<string name="screen_login_subtitle">"Matrix - гэта адкрытая сетка для бяспечнай, дэцэнтралізаванай сувязі."</string> <string name="screen_login_subtitle">"Matrix - гэта адкрытая сетка для бяспечнай, дэцэнтралізаванай сувязі."</string>
<string name="screen_login_title">"Сардэчна запрашаем!"</string> <string name="screen_login_title">"Сардэчна запрашаем!"</string>
<string name="screen_login_title_with_homeserver">"Увайсці ў %1$s"</string> <string name="screen_login_title_with_homeserver">"Увайсці ў %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Увайсці ўручную"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Увайсці з QR-кодам"</string>
<string name="screen_onboarding_sign_up">"Стварыць уліковы запіс"</string>
<string name="screen_onboarding_welcome_message">"Сардэчна запрашаем у самы хуткі %1$s. Перавага ў хуткасці і прастаце."</string>
<string name="screen_onboarding_welcome_subtitle">"Сардэчна запрашаем у %1$s. Зараджаны, для хуткасці і прастаты."</string>
<string name="screen_onboarding_welcome_title">"Будзьце ў сваім element"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Ўсталяванне бяспечнага злучэння"</string> <string name="screen_qr_code_login_connecting_subtitle">"Ўсталяванне бяспечнага злучэння"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Не атрымалася ўсталяваць бяспечнае злучэнне з новай прыладай. Існуючыя прылады па-ранейшаму ў бяспецы, і вам не трэба турбавацца пра іх."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Не атрымалася ўсталяваць бяспечнае злучэнне з новай прыладай. Існуючыя прылады па-ранейшаму ў бяспецы, і вам не трэба турбавацца пра іх."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Што зараз?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Што зараз?"</string>

View file

@ -19,6 +19,12 @@
<string name="screen_login_subtitle">"Matrix е отворена мрежа за сигурна, децентрализирана комуникация."</string> <string name="screen_login_subtitle">"Matrix е отворена мрежа за сигурна, децентрализирана комуникация."</string>
<string name="screen_login_title">"Добре дошли отново!"</string> <string name="screen_login_title">"Добре дошли отново!"</string>
<string name="screen_login_title_with_homeserver">"Влизане в %1$s"</string> <string name="screen_login_title_with_homeserver">"Влизане в %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Влизане ръчно"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Влизане с QR код"</string>
<string name="screen_onboarding_sign_up">"Създаване на акаунт"</string>
<string name="screen_onboarding_welcome_message">"Добре дошли в най-бързия %1$s досега. Супер зареден за скорост и простота."</string>
<string name="screen_onboarding_welcome_subtitle">"Добре дошли в %1$s. Супер зареден за скорост и простота."</string>
<string name="screen_onboarding_welcome_title">"Бъдете в стихията си"</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторен опит"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторен опит"</string>
<string name="screen_server_confirmation_change_server">"Промяна на доставчика на акаунт"</string> <string name="screen_server_confirmation_change_server">"Промяна на доставчика на акаунт"</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix е отворена мрежа за сигурна, децентрализирана комуникация."</string> <string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix е отворена мрежа за сигурна, децентрализирана комуникация."</string>

View file

@ -17,6 +17,9 @@
<string name="screen_change_server_error_invalid_well_known">"Server není k dispozici kvůli problému se souborem well-known: <string name="screen_change_server_error_invalid_well_known">"Server není k dispozici kvůli problému se souborem well-known:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Vybraný poskytovatel účtu nepodporuje klouzavou synchronizaci. Pro použití %1$s je nutná aktualizace serveru."</string> <string name="screen_change_server_error_no_sliding_sync_message">"Vybraný poskytovatel účtu nepodporuje klouzavou synchronizaci. Pro použití %1$s je nutná aktualizace serveru."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"Uživateli %1$s není dovoleno se připojit do %2$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_content">"Tato aplikace byla nakonfigurována tak, aby umožňovala: %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_title">"Poskytovatel účtu %1$s není povolen."</string>
<string name="screen_change_server_form_header">"Adresa URL domovského serveru"</string> <string name="screen_change_server_form_header">"Adresa URL domovského serveru"</string>
<string name="screen_change_server_form_notice">"Zadejte adresu domény."</string> <string name="screen_change_server_form_notice">"Zadejte adresu domény."</string>
<string name="screen_change_server_subtitle">"Jaká je adresa vašeho serveru?"</string> <string name="screen_change_server_subtitle">"Jaká je adresa vašeho serveru?"</string>
@ -31,6 +34,13 @@
<string name="screen_login_subtitle">"Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci."</string> <string name="screen_login_subtitle">"Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci."</string>
<string name="screen_login_title">"Vítejte zpět!"</string> <string name="screen_login_title">"Vítejte zpět!"</string>
<string name="screen_login_title_with_homeserver">"Přihlaste se k %1$s"</string> <string name="screen_login_title_with_homeserver">"Přihlaste se k %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Ruční přihlášení"</string>
<string name="screen_onboarding_sign_in_to">"Přihlásit se do %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Přihlásit se pomocí QR kódu"</string>
<string name="screen_onboarding_sign_up">"Vytvořit účet"</string>
<string name="screen_onboarding_welcome_message">"Vítejte v dosud nejrychlejším %1$su. Vylepšený pro rychlost a jednoduchost."</string>
<string name="screen_onboarding_welcome_subtitle">"Vítejte v %1$su. Vylepšený, pro rychlost a jednoduchost."</string>
<string name="screen_onboarding_welcome_title">"Buďte ve svém živlu"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Navazování zabezpečeného spojení"</string> <string name="screen_qr_code_login_connecting_subtitle">"Navazování zabezpečeného spojení"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"K novému zařízení se nepodařilo navázat bezpečné připojení. Vaše stávající zařízení jsou stále v bezpečí a nemusíte se o ně obávat."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"K novému zařízení se nepodařilo navázat bezpečné připojení. Vaše stávající zařízení jsou stále v bezpečí a nemusíte se o ně obávat."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Co teď?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Co teď?"</string>

View file

@ -14,9 +14,12 @@
<string name="screen_change_account_provider_subtitle">"Defnyddiwch ddarparwr cyfrif gwahanol, fel eich gweinydd preifat eich hun neu gyfrif gwaith."</string> <string name="screen_change_account_provider_subtitle">"Defnyddiwch ddarparwr cyfrif gwahanol, fel eich gweinydd preifat eich hun neu gyfrif gwaith."</string>
<string name="screen_change_account_provider_title">"Newid darparwr cyfrif"</string> <string name="screen_change_account_provider_title">"Newid darparwr cyfrif"</string>
<string name="screen_change_server_error_invalid_homeserver">"Doedd dim modd i ni gyrraedd y gweinydd cartref hwn. Gwiriwch eich bod wedi rhoi URL y gweinydd cartref yn gywir. Os yw\'r URL yn gywir, cysylltwch â gweinyddwr eich gweinydd cartref am ragor o help."</string> <string name="screen_change_server_error_invalid_homeserver">"Doedd dim modd i ni gyrraedd y gweinydd cartref hwn. Gwiriwch eich bod wedi rhoi URL y gweinydd cartref yn gywir. Os yw\'r URL yn gywir, cysylltwch â gweinyddwr eich gweinydd cartref am ragor o help."</string>
<string name="screen_change_server_error_invalid_well_known">"Nid yw cydweddu llithrig ar gael oherwydd problem yn y ffeil adnabyddus: <string name="screen_change_server_error_invalid_well_known">"Dyw cydweddu llithrig ddim ar gael oherwydd problem yn y ffeil .well-known:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Dyw\'r darparwr cyfrif hwn ddim yn cefnogi cydweddu llithro. Mae angen uwchraddio\'r gweinydd i ddefnyddio %1$s."</string> <string name="screen_change_server_error_no_sliding_sync_message">"Dyw\'r darparwr cyfrif hwn ddim yn cefnogi cydweddu llithro. Mae angen uwchraddio\'r gweinydd i ddefnyddio %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"Does dim caniatâd i %1$s gysylltu â %2$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_content">"Mae\'r ap hwn wedi\'i ffurfweddu i ganiatáu: %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_title">"Dyw darparwr cyfrif %1$s dddim yn cael ei ganiatáu."</string>
<string name="screen_change_server_form_header">"URL y Gweinydd Cartref"</string> <string name="screen_change_server_form_header">"URL y Gweinydd Cartref"</string>
<string name="screen_change_server_form_notice">"Rhowch gyfeiriad parth."</string> <string name="screen_change_server_form_notice">"Rhowch gyfeiriad parth."</string>
<string name="screen_change_server_subtitle">"Beth yw cyfeiriad eich gweinydd?"</string> <string name="screen_change_server_subtitle">"Beth yw cyfeiriad eich gweinydd?"</string>
@ -31,6 +34,13 @@
<string name="screen_login_subtitle">"Mae Matrix yn rhwydwaith agored ar gyfer cyfathrebu diogel, datganoledig."</string> <string name="screen_login_subtitle">"Mae Matrix yn rhwydwaith agored ar gyfer cyfathrebu diogel, datganoledig."</string>
<string name="screen_login_title">"Croeso nôl!"</string> <string name="screen_login_title">"Croeso nôl!"</string>
<string name="screen_login_title_with_homeserver">"Mewngofnodi i %1$s"</string> <string name="screen_login_title_with_homeserver">"Mewngofnodi i %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Mewngofnodwch â llaw"</string>
<string name="screen_onboarding_sign_in_to">"Mewngofnodi i %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Mewngofnodwch gyda chod QR"</string>
<string name="screen_onboarding_sign_up">"Creu cyfrif"</string>
<string name="screen_onboarding_welcome_message">"Croeso i\'r %1$s cyflymaf erioed. Yn nodedig am gyflymder a symlrwydd."</string>
<string name="screen_onboarding_welcome_subtitle">"Croeso i %1$s. Yn nodedig ar gyfer cyflymder a symlrwydd."</string>
<string name="screen_onboarding_welcome_title">"Byddwch yn eich elfen"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Yn creu cysylltiad diogel"</string> <string name="screen_qr_code_login_connecting_subtitle">"Yn creu cysylltiad diogel"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Nid oedd modd gwneud cysylltiad diogel â\'r ddyfais newydd. Mae eich dyfeisiau presennol yn dal yn ddiogel a does dim angen i chi boeni amdanyn nhw."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Nid oedd modd gwneud cysylltiad diogel â\'r ddyfais newydd. Mae eich dyfeisiau presennol yn dal yn ddiogel a does dim angen i chi boeni amdanyn nhw."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Beth nawr?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Beth nawr?"</string>

View file

@ -17,7 +17,7 @@
<string name="screen_change_server_error_invalid_well_known">"Der Server ist aufgrund eines Problems im \"well-known file\" nicht verfügbar: <string name="screen_change_server_error_invalid_well_known">"Der Server ist aufgrund eines Problems im \"well-known file\" nicht verfügbar:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Der gewählte Kontoanbieter unterstützt Sliding Sync nicht. Für die Verwendung von %1$s ist ein Upgrade des Servers erforderlich."</string> <string name="screen_change_server_error_no_sliding_sync_message">"Der gewählte Kontoanbieter unterstützt Sliding Sync nicht. Für die Verwendung von %1$s ist ein Upgrade des Servers erforderlich."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$sdarf keine Verbindung herstellen zu%2$s."</string> <string name="screen_change_server_error_unauthorized_homeserver">"%1$s darf keine Verbindung herstellen zu %2$s."</string>
<string name="screen_change_server_form_header">"Homeserver-URL"</string> <string name="screen_change_server_form_header">"Homeserver-URL"</string>
<string name="screen_change_server_form_notice">"Geben Sie eine Domainadresse ein."</string> <string name="screen_change_server_form_notice">"Geben Sie eine Domainadresse ein."</string>
<string name="screen_change_server_subtitle">"Wie lautet die Adresse deines Servers?"</string> <string name="screen_change_server_subtitle">"Wie lautet die Adresse deines Servers?"</string>
@ -32,6 +32,12 @@
<string name="screen_login_subtitle">"Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation."</string> <string name="screen_login_subtitle">"Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation."</string>
<string name="screen_login_title">"Willkommen zurück!"</string> <string name="screen_login_title">"Willkommen zurück!"</string>
<string name="screen_login_title_with_homeserver">"Anmelden bei %1$s"</string> <string name="screen_login_title_with_homeserver">"Anmelden bei %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Manuell anmelden"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Mit QR-Code anmelden"</string>
<string name="screen_onboarding_sign_up">"Konto erstellen"</string>
<string name="screen_onboarding_welcome_message">"Willkommen beim schnellsten %1$s aller Zeiten. Optimiert für Geschwindigkeit und Einfachheit."</string>
<string name="screen_onboarding_welcome_subtitle">"Willkommen zu %1$s. Aufgeladen, für Geschwindigkeit und Einfachheit."</string>
<string name="screen_onboarding_welcome_title">"Sei in Deinem Element"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Sichere Verbindung aufbauen"</string> <string name="screen_qr_code_login_connecting_subtitle">"Sichere Verbindung aufbauen"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Es konnte keine sichere Verbindung zu dem neuen Gerät hergestellt werden."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Es konnte keine sichere Verbindung zu dem neuen Gerät hergestellt werden."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Und jetzt?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Und jetzt?"</string>
@ -54,7 +60,7 @@
Versuche, dich manuell anzumelden, oder scanne den QR-Code mit einem anderen Gerät."</string> Versuche, dich manuell anzumelden, oder scanne den QR-Code mit einem anderen Gerät."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR-Code wird nicht unterstützt"</string> <string name="screen_qr_code_login_error_linking_not_suported_title">"QR-Code wird nicht unterstützt"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Ihr Kontoanbieter unterstützt %1$s nicht."</string> <string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Ihr Kontoanbieter unterstützt %1$s nicht."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$swird nicht unterstützt"</string> <string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s wird nicht unterstützt"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Bereit zum Scannen"</string> <string name="screen_qr_code_login_initial_state_button_title">"Bereit zum Scannen"</string>
<string name="screen_qr_code_login_initial_state_item_1">"%1$s auf einem Desktop-Gerät öffnen"</string> <string name="screen_qr_code_login_initial_state_item_1">"%1$s auf einem Desktop-Gerät öffnen"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Klick auf deinen Avatar"</string> <string name="screen_qr_code_login_initial_state_item_2">"Klick auf deinen Avatar"</string>

View file

@ -29,6 +29,12 @@
<string name="screen_login_subtitle">"Το Matrix είναι ένα ανοιχτό δίκτυο για ασφαλή, αποκεντρωμένη επικοινωνία."</string> <string name="screen_login_subtitle">"Το Matrix είναι ένα ανοιχτό δίκτυο για ασφαλή, αποκεντρωμένη επικοινωνία."</string>
<string name="screen_login_title">"Καλωσόρισες ξανά!"</string> <string name="screen_login_title">"Καλωσόρισες ξανά!"</string>
<string name="screen_login_title_with_homeserver">"Συνδέσου στο %1$s"</string> <string name="screen_login_title_with_homeserver">"Συνδέσου στο %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Σύνδεση χειροκίνητα"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Συνδέσου με κωδικό QR"</string>
<string name="screen_onboarding_sign_up">"Δημιουργία λογαριασμού"</string>
<string name="screen_onboarding_welcome_message">"Καλώς ήλθατε στο γρηγορότερο %1$s όλων των εποχών. Υπερτροφοδοτούμενο με ταχύτητα και απλότητα."</string>
<string name="screen_onboarding_welcome_subtitle">"Καλώς ήρθες στο %1$s. Υπερφορτισμένο, για ταχύτητα και απλότητα."</string>
<string name="screen_onboarding_welcome_title">"Μείνε στο element σου"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Εγκαθίδρυση ασφαλούς σύνδεσης"</string> <string name="screen_qr_code_login_connecting_subtitle">"Εγκαθίδρυση ασφαλούς σύνδεσης"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Δεν ήταν δυνατή η πραγματοποίηση ασφαλούς σύνδεσης στη νέα συσκευή. Οι υπάρχουσες συσκευές σας εξακολουθούν να είναι ασφαλείς και δεν χρειάζεται να ανησυχείς για αυτές."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Δεν ήταν δυνατή η πραγματοποίηση ασφαλούς σύνδεσης στη νέα συσκευή. Οι υπάρχουσες συσκευές σας εξακολουθούν να είναι ασφαλείς και δεν χρειάζεται να ανησυχείς για αυτές."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Τί είναι πάλι;"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Τί είναι πάλι;"</string>

View file

@ -29,6 +29,12 @@
<string name="screen_login_subtitle">"Matrix es una red abierta para una comunicación segura y descentralizada."</string> <string name="screen_login_subtitle">"Matrix es una red abierta para una comunicación segura y descentralizada."</string>
<string name="screen_login_title">"¡Hola de nuevo!"</string> <string name="screen_login_title">"¡Hola de nuevo!"</string>
<string name="screen_login_title_with_homeserver">"Iniciar sesión en %1$s"</string> <string name="screen_login_title_with_homeserver">"Iniciar sesión en %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Iniciar sesión manualmente"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Iniciar sesión con un código QR"</string>
<string name="screen_onboarding_sign_up">"Crear cuenta"</string>
<string name="screen_onboarding_welcome_message">"Bienvenido al %1$s más rápido de todos los tiempos. Diseñado para la velocidad y la simplicidad."</string>
<string name="screen_onboarding_welcome_subtitle">"Bienvenido a %1$s. Vitaminado, para mayor rapidez y sencillez."</string>
<string name="screen_onboarding_welcome_title">"Siéntete en tu Elemento"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Estableciendo una conexión segura"</string> <string name="screen_qr_code_login_connecting_subtitle">"Estableciendo una conexión segura"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"No se pudo establecer una conexión segura con el nuevo dispositivo. Tus dispositivos actuales siguen siendo seguros y no tienes que preocuparte por ellos."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"No se pudo establecer una conexión segura con el nuevo dispositivo. Tus dispositivos actuales siguen siendo seguros y no tienes que preocuparte por ellos."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"¿Y ahora qué?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"¿Y ahora qué?"</string>

View file

@ -18,6 +18,8 @@
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Valitud teenusepakkuja ei toeta „sliding sync“ režiimi. Rakenduse %1$s kasutamiseks on vaja serverit uuendada."</string> <string name="screen_change_server_error_no_sliding_sync_message">"Valitud teenusepakkuja ei toeta „sliding sync“ režiimi. Rakenduse %1$s kasutamiseks on vaja serverit uuendada."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s ei saa kasutada %2$s koduserverit."</string> <string name="screen_change_server_error_unauthorized_homeserver">"%1$s ei saa kasutada %2$s koduserverit."</string>
<string name="screen_change_server_error_unauthorized_homeserver_content">"See rakendus on seadistatud järgneva koduserveri kasutamiseks: %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_title">"%1$s teenusepakkuja pole lubatud."</string>
<string name="screen_change_server_form_header">"Koduserveri url"</string> <string name="screen_change_server_form_header">"Koduserveri url"</string>
<string name="screen_change_server_form_notice">"Sisesta domeeni aadress."</string> <string name="screen_change_server_form_notice">"Sisesta domeeni aadress."</string>
<string name="screen_change_server_subtitle">"Mis on sinu koduserveri aadress?"</string> <string name="screen_change_server_subtitle">"Mis on sinu koduserveri aadress?"</string>
@ -32,6 +34,13 @@
<string name="screen_login_subtitle">"Matrix on avatud võrk turvalise ja hajutatud suhtluse jaoks."</string> <string name="screen_login_subtitle">"Matrix on avatud võrk turvalise ja hajutatud suhtluse jaoks."</string>
<string name="screen_login_title">"Tere tulemast tagasi!"</string> <string name="screen_login_title">"Tere tulemast tagasi!"</string>
<string name="screen_login_title_with_homeserver">"Logi sisse serverisse %1$s"</string> <string name="screen_login_title_with_homeserver">"Logi sisse serverisse %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Logi sisse käsitsi"</string>
<string name="screen_onboarding_sign_in_to">"Logi sisse teenusesse %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Logi sisse QR-koodi alusel"</string>
<string name="screen_onboarding_sign_up">"Loo kasutajakonto"</string>
<string name="screen_onboarding_welcome_message">"Läbi aegade kiireim ja mugavaim %1$s."</string>
<string name="screen_onboarding_welcome_subtitle">"Tere tulemast kasutama kiiret ja lihtsat suhtlusrakendust %1$s."</string>
<string name="screen_onboarding_welcome_title">"Ole oma elemendis"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Loome turvalist ühendust"</string> <string name="screen_qr_code_login_connecting_subtitle">"Loome turvalist ühendust"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Turvalise ühenduse loomine uue seadmega ei õnnestunud. Sinu olemasolevad seadmed on jätkuvalt turvatud ja sa ei pea nende pärast muretsema."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Turvalise ühenduse loomine uue seadmega ei õnnestunud. Sinu olemasolevad seadmed on jätkuvalt turvatud ja sa ei pea nende pärast muretsema."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Mida järgmiseks teeme?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Mida järgmiseks teeme?"</string>

View file

@ -20,6 +20,12 @@
<string name="screen_login_subtitle">"Matrix komunikazio seguru eta deszentralizaturako sare irekia da."</string> <string name="screen_login_subtitle">"Matrix komunikazio seguru eta deszentralizaturako sare irekia da."</string>
<string name="screen_login_title">"Ongi etorri!"</string> <string name="screen_login_title">"Ongi etorri!"</string>
<string name="screen_login_title_with_homeserver">"Hasi saioa %1$s(e)n"</string> <string name="screen_login_title_with_homeserver">"Hasi saioa %1$s(e)n"</string>
<string name="screen_onboarding_sign_in_manually">"Hasi saioa eskuz"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Hasi saioa QR kodearekin"</string>
<string name="screen_onboarding_sign_up">"Sortu kontua"</string>
<string name="screen_onboarding_welcome_message">"Ongi etorri inoizko %1$s azkarrenera. Abiaduraz eta sinpletasunaz gainkargatua."</string>
<string name="screen_onboarding_welcome_subtitle">"Ongi etorri %1$s-ra. Abiaduraz eta sinpletasunez gainezka."</string>
<string name="screen_onboarding_welcome_title">"Egon zure saltsan"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Konexio segurua ezartzen"</string> <string name="screen_qr_code_login_connecting_subtitle">"Konexio segurua ezartzen"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Ezin izan da konexio segururik ezarri gailu berriarekin. Lehendik dauden gailuak seguru daude oraindik ere eta ez duzu haietaz kezkatu beharrik."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Ezin izan da konexio segururik ezarri gailu berriarekin. Lehendik dauden gailuak seguru daude oraindik ere eta ez duzu haietaz kezkatu beharrik."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Orain zer?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Orain zer?"</string>

View file

@ -25,6 +25,12 @@
<string name="screen_login_subtitle">"ماتریکس شبکه‌ای بار برای ارتباطات نامتمرکز و امن است."</string> <string name="screen_login_subtitle">"ماتریکس شبکه‌ای بار برای ارتباطات نامتمرکز و امن است."</string>
<string name="screen_login_title">"خوش برگشتید!"</string> <string name="screen_login_title">"خوش برگشتید!"</string>
<string name="screen_login_title_with_homeserver">"ورود به %1$s"</string> <string name="screen_login_title_with_homeserver">"ورود به %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"ورود دستی"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"ورود با کد QR"</string>
<string name="screen_onboarding_sign_up">"ایجاد حساب"</string>
<string name="screen_onboarding_welcome_message">"به سریع‌ترین %1$s خوش آمدید. بازطرّاحی شده برای سرعت و سادگی."</string>
<string name="screen_onboarding_welcome_subtitle">"به %1$s خوش آمدید. بازطرّاحی شده برای سرعت و سادگی."</string>
<string name="screen_onboarding_welcome_title">"در المنتتان باشید"</string>
<string name="screen_qr_code_login_connecting_subtitle">"برقرار کدن اتّصالی امن"</string> <string name="screen_qr_code_login_connecting_subtitle">"برقرار کدن اتّصالی امن"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"نتوانست اتّصالی امن به افزارهٔ جدید بسازد. افزاره‌های موجودتان هنوز امنند و نیازی نیست نگرانشان باشید."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"نتوانست اتّصالی امن به افزارهٔ جدید بسازد. افزاره‌های موجودتان هنوز امنند و نیازی نیست نگرانشان باشید."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"اکنون چه؟"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"اکنون چه؟"</string>

View file

@ -17,6 +17,9 @@
<string name="screen_change_server_error_invalid_well_known">"Sliding sync ei ole saatavilla well-known tiedostossa olevan ongelman vuoksi: <string name="screen_change_server_error_invalid_well_known">"Sliding sync ei ole saatavilla well-known tiedostossa olevan ongelman vuoksi:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Valitsemasi palveluntarjoaja ei tue sliding syncia. Palvelimen päivitys tarvitaan %1$s -sovelluksen käyttämiseen."</string> <string name="screen_change_server_error_no_sliding_sync_message">"Valitsemasi palveluntarjoaja ei tue sliding syncia. Palvelimen päivitys tarvitaan %1$s -sovelluksen käyttämiseen."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s ei saa yhdistää %2$s -palvelimeen."</string>
<string name="screen_change_server_error_unauthorized_homeserver_content">"Tämä sovellus on määritetty sallimaan: %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_title">"Palveluntarjoaja %1$s ei ole sallittu."</string>
<string name="screen_change_server_form_header">"Kotipalvelimen osoite"</string> <string name="screen_change_server_form_header">"Kotipalvelimen osoite"</string>
<string name="screen_change_server_form_notice">"Anna verkkotunnuksen osoite."</string> <string name="screen_change_server_form_notice">"Anna verkkotunnuksen osoite."</string>
<string name="screen_change_server_subtitle">"Mikä on palvelimesi osoite?"</string> <string name="screen_change_server_subtitle">"Mikä on palvelimesi osoite?"</string>
@ -31,6 +34,13 @@
<string name="screen_login_subtitle">"Matrix on avoin verkko turvallista, hajautettua viestintää varten."</string> <string name="screen_login_subtitle">"Matrix on avoin verkko turvallista, hajautettua viestintää varten."</string>
<string name="screen_login_title">"Tervetuloa takaisin!"</string> <string name="screen_login_title">"Tervetuloa takaisin!"</string>
<string name="screen_login_title_with_homeserver">"Kirjaudu sisään %1$s -palvelimelle"</string> <string name="screen_login_title_with_homeserver">"Kirjaudu sisään %1$s -palvelimelle"</string>
<string name="screen_onboarding_sign_in_manually">"Kirjaudu sisään manuaalisesti"</string>
<string name="screen_onboarding_sign_in_to">"Kirjaudu sisään %1$s -palvelimelle"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Kirjaudu sisään QR-koodilla"</string>
<string name="screen_onboarding_sign_up">"Luo tili"</string>
<string name="screen_onboarding_welcome_message">"Tervetuloa kaikkien aikojen nopeimpaan %1$s -sovellukseen. Ahdettu nopeudella ja yksinkertaisuudella."</string>
<string name="screen_onboarding_welcome_subtitle">"Tervetuloa %1$s -sovellukseen. Ahdettu nopeudella ja yksinkertaisuudella."</string>
<string name="screen_onboarding_welcome_title">"Ole elementissäsi"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Muodostetaan turvallista yhteyttä"</string> <string name="screen_qr_code_login_connecting_subtitle">"Muodostetaan turvallista yhteyttä"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Turvallista yhteyttä uuteen laitteeseen ei voitu muodostaa. Olemassa olevat laitteesi ovat edelleen turvassa, eikä sinun tarvitse huolehtia niistä."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Turvallista yhteyttä uuteen laitteeseen ei voitu muodostaa. Olemassa olevat laitteesi ovat edelleen turvassa, eikä sinun tarvitse huolehtia niistä."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Mitä nyt?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Mitä nyt?"</string>

View file

@ -17,6 +17,9 @@
<string name="screen_change_server_error_invalid_well_known">"Ce fournisseur de compte nest pas disponible en raison dun problème dans le fichier .well-known: <string name="screen_change_server_error_invalid_well_known">"Ce fournisseur de compte nest pas disponible en raison dun problème dans le fichier .well-known:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Le fournisseur de compte sélectionné ne prend pas en charge le sliding sync. Une mise à jour du serveur est nécessaire pour pouvoir utiliser %1$s."</string> <string name="screen_change_server_error_no_sliding_sync_message">"Le fournisseur de compte sélectionné ne prend pas en charge le sliding sync. Une mise à jour du serveur est nécessaire pour pouvoir utiliser %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s nest pas autorisé à se connecter à %2$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_content">"Cette application a été configurée pour autoriser: %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_title">"Le fournisseur de compte %1$s nest pas autorisé."</string>
<string name="screen_change_server_form_header">"URL du serveur daccueil"</string> <string name="screen_change_server_form_header">"URL du serveur daccueil"</string>
<string name="screen_change_server_form_notice">"Saisissez une adresse de domaine."</string> <string name="screen_change_server_form_notice">"Saisissez une adresse de domaine."</string>
<string name="screen_change_server_subtitle">"Quelle est ladresse de votre serveur ?"</string> <string name="screen_change_server_subtitle">"Quelle est ladresse de votre serveur ?"</string>
@ -31,6 +34,13 @@
<string name="screen_login_subtitle">"Matrix est un réseau ouvert pour une communication sécurisée et décentralisée."</string> <string name="screen_login_subtitle">"Matrix est un réseau ouvert pour une communication sécurisée et décentralisée."</string>
<string name="screen_login_title">"Content de vous revoir !"</string> <string name="screen_login_title">"Content de vous revoir !"</string>
<string name="screen_login_title_with_homeserver">"Connectez-vous à %1$s"</string> <string name="screen_login_title_with_homeserver">"Connectez-vous à %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Se connecter manuellement"</string>
<string name="screen_onboarding_sign_in_to">"Se connecter à %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Se connecter avec un QR code"</string>
<string name="screen_onboarding_sign_up">"Créer un compte"</string>
<string name="screen_onboarding_welcome_message">"Bienvenue dans lapplication %1$s la plus rapide de tous les temps. Boosté pour plus de rapidité et de simplicité."</string>
<string name="screen_onboarding_welcome_subtitle">"Bienvenue sur %1$s. Boosté, pour plus de rapidité et de simplicité."</string>
<string name="screen_onboarding_welcome_title">"Soyez dans votre Element"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Établissement dune connexion sécurisée"</string> <string name="screen_qr_code_login_connecting_subtitle">"Établissement dune connexion sécurisée"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Aucune connexion sécurisée na pu être établie avec la nouvelle session. Vos sessions existantes sont toujours en sécurité et vous navez pas à vous en soucier."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Aucune connexion sécurisée na pu être établie avec la nouvelle session. Vos sessions existantes sont toujours en sécurité et vous navez pas à vous en soucier."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Et maintenant ?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Et maintenant ?"</string>

View file

@ -17,6 +17,9 @@
<string name="screen_change_server_error_invalid_well_known">"A kiszolgáló a well-known fájl problémája miatt nem érhető el: <string name="screen_change_server_error_invalid_well_known">"A kiszolgáló a well-known fájl problémája miatt nem érhető el:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"A kiválasztott fiókszolgáltató nem támogatja a csúszóablakos szinkronizálást. Az %1$s használatához kiszolgálófrissítés szükséges."</string> <string name="screen_change_server_error_no_sliding_sync_message">"A kiválasztott fiókszolgáltató nem támogatja a csúszóablakos szinkronizálást. Az %1$s használatához kiszolgálófrissítés szükséges."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s nem csatlakozhat ide: %2$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_content">"Ezt az alkalmazást úgy konfigurálták, hogy engedélyezi ezt: %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_title">"A(z) %1$s fiókszolgáltató nem engedélyezett."</string>
<string name="screen_change_server_form_header">"Matrix-kiszolgáló webcíme"</string> <string name="screen_change_server_form_header">"Matrix-kiszolgáló webcíme"</string>
<string name="screen_change_server_form_notice">"Adjon meg egy domaincímet."</string> <string name="screen_change_server_form_notice">"Adjon meg egy domaincímet."</string>
<string name="screen_change_server_subtitle">"Mi a kiszolgálója címe?"</string> <string name="screen_change_server_subtitle">"Mi a kiszolgálója címe?"</string>
@ -31,6 +34,13 @@
<string name="screen_login_subtitle">"A Matrix egy nyitott hálózat a biztonságos, decentralizált kommunikációhoz."</string> <string name="screen_login_subtitle">"A Matrix egy nyitott hálózat a biztonságos, decentralizált kommunikációhoz."</string>
<string name="screen_login_title">"Örülünk, hogy visszatért!"</string> <string name="screen_login_title">"Örülünk, hogy visszatért!"</string>
<string name="screen_login_title_with_homeserver">"Bejelentkezés ide: %1$s"</string> <string name="screen_login_title_with_homeserver">"Bejelentkezés ide: %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Kézi bejelentkezés"</string>
<string name="screen_onboarding_sign_in_to">"Bejelentkezés ide: %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Bejelentkezés QR-kóddal"</string>
<string name="screen_onboarding_sign_up">"Fiók létrehozása"</string>
<string name="screen_onboarding_welcome_message">"Üdvözöljük a valaha volt leggyorsabb %1$sben. Felturbózva, a sebesség és az egyszerűség érdekében."</string>
<string name="screen_onboarding_welcome_subtitle">"Üdvözli az %1$s. Felturbózva, a sebesség és az egyszerűség jegyében."</string>
<string name="screen_onboarding_welcome_title">"Legyen elemében"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Biztonságos kapcsolat létesítése"</string> <string name="screen_qr_code_login_connecting_subtitle">"Biztonságos kapcsolat létesítése"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Nem sikerült biztonságos kapcsolatot létesíteni az új eszközzel. A meglévő eszközei továbbra is biztonságban vannak, és nem kell aggódnia miattuk."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Nem sikerült biztonságos kapcsolatot létesíteni az új eszközzel. A meglévő eszközei továbbra is biztonságban vannak, és nem kell aggódnia miattuk."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Most mi lesz?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Most mi lesz?"</string>

View file

@ -29,6 +29,12 @@
<string name="screen_login_subtitle">"Matrix adalah jaringan terbuka untuk komunikasi yang aman dan terdesentralisasi."</string> <string name="screen_login_subtitle">"Matrix adalah jaringan terbuka untuk komunikasi yang aman dan terdesentralisasi."</string>
<string name="screen_login_title">"Selamat datang kembali!"</string> <string name="screen_login_title">"Selamat datang kembali!"</string>
<string name="screen_login_title_with_homeserver">"Masuk ke %1$s"</string> <string name="screen_login_title_with_homeserver">"Masuk ke %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Masuk secara manual"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Masuk dengan kode QR"</string>
<string name="screen_onboarding_sign_up">"Buat akun"</string>
<string name="screen_onboarding_welcome_message">"Selamat datang di %1$s tercepat yang pernah ada. Berdaya besar untuk kecepatan dan kesederhanaan."</string>
<string name="screen_onboarding_welcome_subtitle">"Selamat datang di %1$s. Berdaya penuh, untuk kecepatan dan kesederhanaan."</string>
<string name="screen_onboarding_welcome_title">"Berada di elemen Anda"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Membuat koneksi aman"</string> <string name="screen_qr_code_login_connecting_subtitle">"Membuat koneksi aman"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Koneksi aman tidak dapat dibuat ke perangkat baru. Perangkat Anda yang ada masih aman dan Anda tidak perlu khawatir tentang mereka."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Koneksi aman tidak dapat dibuat ke perangkat baru. Perangkat Anda yang ada masih aman dan Anda tidak perlu khawatir tentang mereka."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Apa sekarang?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Apa sekarang?"</string>

View file

@ -29,6 +29,12 @@
<string name="screen_login_subtitle">"Matrix è una rete aperta per comunicazioni sicure e decentralizzate."</string> <string name="screen_login_subtitle">"Matrix è una rete aperta per comunicazioni sicure e decentralizzate."</string>
<string name="screen_login_title">"Bentornato!"</string> <string name="screen_login_title">"Bentornato!"</string>
<string name="screen_login_title_with_homeserver">"Accedi a %1$s"</string> <string name="screen_login_title_with_homeserver">"Accedi a %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Accedi manualmente"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Accedi con codice QR"</string>
<string name="screen_onboarding_sign_up">"Crea account"</string>
<string name="screen_onboarding_welcome_message">"Benvenuti nell\'%1$s più veloce di sempre. Potenziato per velocità e semplicità."</string>
<string name="screen_onboarding_welcome_subtitle">"Benvenuto su %1$s. Potenziato in velocità e semplicità."</string>
<string name="screen_onboarding_welcome_title">"Sii nel tuo elemento"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Stabilendo la connessione"</string> <string name="screen_qr_code_login_connecting_subtitle">"Stabilendo la connessione"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Non è stato possibile stabilire una connessione sicura con il nuovo dispositivo. I tuoi dispositivi esistenti sono ancora al sicuro e non devi preoccuparti di loro."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Non è stato possibile stabilire una connessione sicura con il nuovo dispositivo. I tuoi dispositivi esistenti sono ancora al sicuro e non devi preoccuparti di loro."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"E adesso?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"E adesso?"</string>

View file

@ -28,6 +28,12 @@
<string name="screen_login_subtitle">"Matrix არის ღია ქსელი უსაფრთხო, დეცენტრალიზებული კომუნიკაციისთვის."</string> <string name="screen_login_subtitle">"Matrix არის ღია ქსელი უსაფრთხო, დეცენტრალიზებული კომუნიკაციისთვის."</string>
<string name="screen_login_title">"კეთილი იყოს თქვენი მობრძანება!"</string> <string name="screen_login_title">"კეთილი იყოს თქვენი მობრძანება!"</string>
<string name="screen_login_title_with_homeserver">"შესვლა %1$s-ში"</string> <string name="screen_login_title_with_homeserver">"შესვლა %1$s-ში"</string>
<string name="screen_onboarding_sign_in_manually">"ხელით შესვლა"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"შესვლა QR კოდით"</string>
<string name="screen_onboarding_sign_up">"ანგარიშის შექმნა"</string>
<string name="screen_onboarding_welcome_message">"კეთილი იყოს თქვენი მობრძანება უსწრაფეს %1$s-ში. დამუხტულია სიჩქარისა და სიმარტივისათვის."</string>
<string name="screen_onboarding_welcome_subtitle">"კეთილი იყოს თქვენი მობრძანება %1$s-ში! დამუხტული სიჩქარისა და სიმარტივისთვის."</string>
<string name="screen_onboarding_welcome_title">"იყავი შენს element-ში"</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"ხელახლა ცდა"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"ხელახლა ცდა"</string>
<string name="screen_server_confirmation_change_server">"შეცვალეთ ანგარიშის მომწოდებელი"</string> <string name="screen_server_confirmation_change_server">"შეცვალეთ ანგარიშის მომწოდებელი"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"კერძო სერვერი Element-ის თანამშრომლებისთვის."</string> <string name="screen_server_confirmation_message_login_element_dot_io">"კერძო სერვერი Element-ის თანამშრომლებისთვის."</string>

View file

@ -22,6 +22,11 @@
<string name="screen_login_subtitle">"Matrix yra atviras tinklas, skirtas saugiam, decentralizuotam bendravimui."</string> <string name="screen_login_subtitle">"Matrix yra atviras tinklas, skirtas saugiam, decentralizuotam bendravimui."</string>
<string name="screen_login_title">"Sveiki sugrįžę!"</string> <string name="screen_login_title">"Sveiki sugrįžę!"</string>
<string name="screen_login_title_with_homeserver">"Prisijungti prie %1$s"</string> <string name="screen_login_title_with_homeserver">"Prisijungti prie %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Prisijunkite rankiniu būdu"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Prisijunkite naudodami QR kodą"</string>
<string name="screen_onboarding_sign_up">"Sukurti paskyrą"</string>
<string name="screen_onboarding_welcome_subtitle">"Sveiki atvykę į %1$s. Įkrautas greitumui ir paprastumui."</string>
<string name="screen_onboarding_welcome_title">"Būkite savo elemente"</string>
<string name="screen_server_confirmation_change_server">"Keisti paskyros teikėją"</string> <string name="screen_server_confirmation_change_server">"Keisti paskyros teikėją"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Privatus serveris “Element” darbuotojams."</string> <string name="screen_server_confirmation_message_login_element_dot_io">"Privatus serveris “Element” darbuotojams."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix yra atviras tinklas, skirtas saugiam, decentralizuotam bendravimui."</string> <string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix yra atviras tinklas, skirtas saugiam, decentralizuotam bendravimui."</string>

View file

@ -17,6 +17,9 @@
<string name="screen_change_server_error_invalid_well_known">"Serveren er ikke tilgjengelig på grunn av et problem i den velkjente filen: <string name="screen_change_server_error_invalid_well_known">"Serveren er ikke tilgjengelig på grunn av et problem i den velkjente filen:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Den valgte kontoleverandøren støtter ikke sliding sync. En oppgradering av serveren er nødvendig for å bruke %1$s."</string> <string name="screen_change_server_error_no_sliding_sync_message">"Den valgte kontoleverandøren støtter ikke sliding sync. En oppgradering av serveren er nødvendig for å bruke %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s har ikke lov til å koble seg til %2$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_content">"Denne appen er konfigurert til å tillate: %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_title">"Kontoleverandør %1$s er ikke tillatt."</string>
<string name="screen_change_server_form_header">"URL til hjemmeserver"</string> <string name="screen_change_server_form_header">"URL til hjemmeserver"</string>
<string name="screen_change_server_form_notice">"Skriv inn en domeneadresse."</string> <string name="screen_change_server_form_notice">"Skriv inn en domeneadresse."</string>
<string name="screen_change_server_subtitle">"Hva er adressen til serveren din?"</string> <string name="screen_change_server_subtitle">"Hva er adressen til serveren din?"</string>
@ -31,6 +34,13 @@
<string name="screen_login_subtitle">"Matrix er et åpent nettverk for sikker, desentralisert kommunikasjon."</string> <string name="screen_login_subtitle">"Matrix er et åpent nettverk for sikker, desentralisert kommunikasjon."</string>
<string name="screen_login_title">"Velkommen tilbake!"</string> <string name="screen_login_title">"Velkommen tilbake!"</string>
<string name="screen_login_title_with_homeserver">"Logg inn på %1$s"</string> <string name="screen_login_title_with_homeserver">"Logg inn på %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Logg på manuelt"</string>
<string name="screen_onboarding_sign_in_to">"Logg inn på %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Logg inn med QR-kode"</string>
<string name="screen_onboarding_sign_up">"Opprett konto"</string>
<string name="screen_onboarding_welcome_message">"Velkommen til den raskeste %1$s noensinne. Superladet for hastighet og enkelhet."</string>
<string name="screen_onboarding_welcome_subtitle">"Velkommen til %1$s. Supercharged, for hastighet og enkelhet."</string>
<string name="screen_onboarding_welcome_title">"Vær i ditt rette element"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Etablere en sikker forbindelse"</string> <string name="screen_qr_code_login_connecting_subtitle">"Etablere en sikker forbindelse"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"En sikker tilkobling kunne ikke opprettes til den nye enheten. Dine eksisterende enheter er fortsatt trygge, og du trenger ikke å bekymre deg for dem."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"En sikker tilkobling kunne ikke opprettes til den nye enheten. Dine eksisterende enheter er fortsatt trygge, og du trenger ikke å bekymre deg for dem."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Hva nå?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Hva nå?"</string>

View file

@ -29,6 +29,12 @@
<string name="screen_login_subtitle">"Matrix is een open netwerk voor veilige, gedecentraliseerde communicatie."</string> <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">"Welkom terug!"</string>
<string name="screen_login_title_with_homeserver">"Inloggen bij %1$s"</string> <string name="screen_login_title_with_homeserver">"Inloggen bij %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Handmatig inloggen"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Inloggen met QR-code"</string>
<string name="screen_onboarding_sign_up">"Account aanmaken"</string>
<string name="screen_onboarding_welcome_message">"Welkom bij de snelste %1$s ooit. Supercharged, voor snelheid en eenvoud."</string>
<string name="screen_onboarding_welcome_subtitle">"Welkom bij %1$s. Supercharged, voor snelheid en eenvoud."</string>
<string name="screen_onboarding_welcome_title">"Wees in je element"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Een beveiligde verbinding tot stand brengen"</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_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_header">"Wat nu?"</string>

View file

@ -32,6 +32,13 @@
<string name="screen_login_subtitle">"Matrix to otwarta sieć do bezpiecznej i zdecentralizowanej komunikacji."</string> <string name="screen_login_subtitle">"Matrix to otwarta sieć do bezpiecznej i zdecentralizowanej komunikacji."</string>
<string name="screen_login_title">"Witaj ponownie!"</string> <string name="screen_login_title">"Witaj ponownie!"</string>
<string name="screen_login_title_with_homeserver">"Zaloguj się do %1$s"</string> <string name="screen_login_title_with_homeserver">"Zaloguj się do %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Zaloguj się ręcznie"</string>
<string name="screen_onboarding_sign_in_to">"Zaloguj się do %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Zaloguj się za pomocą kodu QR"</string>
<string name="screen_onboarding_sign_up">"Utwórz konto"</string>
<string name="screen_onboarding_welcome_message">"Witamy w %1$s. Szybszy i prostszy niż kiedykolwiek."</string>
<string name="screen_onboarding_welcome_subtitle">"Witamy w %1$s. Doładowany, dla szybkości i prostoty."</string>
<string name="screen_onboarding_welcome_title">"Be in your element"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Nawiązanie bezpiecznego połączenia"</string> <string name="screen_qr_code_login_connecting_subtitle">"Nawiązanie bezpiecznego połączenia"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Nie udało się nawiązać bezpiecznego połączenia z nowym urządzeniem. Twoje istniejące urządzenia są nadal bezpieczne i nie musisz się o nie martwić."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Nie udało się nawiązać bezpiecznego połączenia z nowym urządzeniem. Twoje istniejące urządzenia są nadal bezpieczne i nie musisz się o nie martwić."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Co teraz?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Co teraz?"</string>

View file

@ -27,6 +27,12 @@
<string name="screen_login_subtitle">"A Matrix é uma rede aberta para comunicação segura e descentralizada."</string> <string name="screen_login_subtitle">"A Matrix é uma rede aberta para comunicação segura e descentralizada."</string>
<string name="screen_login_title">"Bem-vindo de volta!"</string> <string name="screen_login_title">"Bem-vindo de volta!"</string>
<string name="screen_login_title_with_homeserver">"Iniciar sessão em %1$s"</string> <string name="screen_login_title_with_homeserver">"Iniciar sessão em %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Iniciar sessão manualmente"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Iniciar sessão com código QR"</string>
<string name="screen_onboarding_sign_up">"Criar conta"</string>
<string name="screen_onboarding_welcome_message">"Bem-vindo ao mais rápido %1$s de todos os tempos. Turbinado para velocidade e simplicidade."</string>
<string name="screen_onboarding_welcome_subtitle">"Bem-vindo ao %1$s. Turbinado, para velocidade e simplicidade"</string>
<string name="screen_onboarding_welcome_title">"Esteja no seu elemento"</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Tente novamente"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Tente novamente"</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"Você deve permitir ao %1$s usar a câmera do seu dispositivo para continuar."</string> <string name="screen_qr_code_login_no_camera_permission_state_description">"Você deve permitir ao %1$s usar a câmera do seu dispositivo para continuar."</string>
<string name="screen_qr_code_login_verify_code_title">"Seu código de verificação"</string> <string name="screen_qr_code_login_verify_code_title">"Seu código de verificação"</string>

View file

@ -29,6 +29,12 @@
<string name="screen_login_subtitle">"A Matrix é uma rede aberta de comunicação descentralizada e segura."</string> <string name="screen_login_subtitle">"A Matrix é uma rede aberta de comunicação descentralizada e segura."</string>
<string name="screen_login_title">"Bem-vindo(a) de volta!"</string> <string name="screen_login_title">"Bem-vindo(a) de volta!"</string>
<string name="screen_login_title_with_homeserver">"Iniciar sessão em %1$s"</string> <string name="screen_login_title_with_homeserver">"Iniciar sessão em %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Iniciar sessão manualmente"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Iniciar sessão com código QR"</string>
<string name="screen_onboarding_sign_up">"Criar conta"</string>
<string name="screen_onboarding_welcome_message">"Bem-vindo(a) à %1$s mais rápida de sempre. Super rápida e simples."</string>
<string name="screen_onboarding_welcome_subtitle">"Bem-vindo(a) à %1$s. Revitalizado, rápido e simples."</string>
<string name="screen_onboarding_welcome_title">"A liberdade do teu elemento"</string>
<string name="screen_qr_code_login_connecting_subtitle">"A estabelecer uma ligação segura"</string> <string name="screen_qr_code_login_connecting_subtitle">"A estabelecer uma ligação segura"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Não foi possível estabelecer uma ligação segura com o novo dispositivo. Os teus outros dispositivos continuam seguros, não precisas de te preocupar com eles."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Não foi possível estabelecer uma ligação segura com o novo dispositivo. Os teus outros dispositivos continuam seguros, não precisas de te preocupar com eles."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"E agora?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"E agora?"</string>

View file

@ -29,6 +29,12 @@
<string name="screen_login_subtitle">"Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată."</string> <string name="screen_login_subtitle">"Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată."</string>
<string name="screen_login_title">"Bine ați revenit!"</string> <string name="screen_login_title">"Bine ați revenit!"</string>
<string name="screen_login_title_with_homeserver">"Conectați-vă la %1$s"</string> <string name="screen_login_title_with_homeserver">"Conectați-vă la %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Conectați-vă manual"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Conectați-vă cu un cod QR"</string>
<string name="screen_onboarding_sign_up">"Creați un cont"</string>
<string name="screen_onboarding_welcome_message">"Bine ați venit la cel mai rapid %1$s din toate timpurile. Supraalimentat pentru viteză și simplitate."</string>
<string name="screen_onboarding_welcome_subtitle">"Bun venit în %1$s. Supraalimentat, pentru viteză și simplitate."</string>
<string name="screen_onboarding_welcome_title">"Fii în Elementul tău"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Se stabilește o conexiune securizată"</string> <string name="screen_qr_code_login_connecting_subtitle">"Se stabilește o conexiune securizată"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Nu a putut fi făcută o conexiune sigură la noul dispozitiv. Dispozitivele existente sunt încă în siguranță și nu trebuie să vă faceți griji cu privire la ele."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Nu a putut fi făcută o conexiune sigură la noul dispozitiv. Dispozitivele existente sunt încă în siguranță și nu trebuie să vă faceți griji cu privire la ele."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Și acum?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Și acum?"</string>

View file

@ -30,6 +30,12 @@
<string name="screen_login_subtitle">"Matrix — это открытая сеть для безопасной децентрализованной связи."</string> <string name="screen_login_subtitle">"Matrix — это открытая сеть для безопасной децентрализованной связи."</string>
<string name="screen_login_title">"Рады видеть вас снова!"</string> <string name="screen_login_title">"Рады видеть вас снова!"</string>
<string name="screen_login_title_with_homeserver">"Войти в %1$s"</string> <string name="screen_login_title_with_homeserver">"Войти в %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Войти вручную"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Войти QR-кодом"</string>
<string name="screen_onboarding_sign_up">"Создать учетную запись"</string>
<string name="screen_onboarding_welcome_message">"Добро пожаловать в самый быстрый клиент %1$s. Ориентирован на скорость и простоту."</string>
<string name="screen_onboarding_welcome_subtitle">"Добро пожаловать в %1$s. Ориентирован на скорость и простоту."</string>
<string name="screen_onboarding_welcome_title">"Чувствуйте себя как дома с Element"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Установление безопасного соединения"</string> <string name="screen_qr_code_login_connecting_subtitle">"Установление безопасного соединения"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Не удалось установить безопасное соединение с новым устройством. Существующие устройства по-прежнему в безопасности, и вам не нужно беспокоиться о них."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Не удалось установить безопасное соединение с новым устройством. Существующие устройства по-прежнему в безопасности, и вам не нужно беспокоиться о них."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Что теперь?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Что теперь?"</string>

View file

@ -18,6 +18,8 @@
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Vybraný poskytovateľ účtu nepodporuje kĺzavú synchronizáciu. Na používanie aplikácie %1$s je potrebná aktualizácia servera,"</string> <string name="screen_change_server_error_no_sliding_sync_message">"Vybraný poskytovateľ účtu nepodporuje kĺzavú synchronizáciu. Na používanie aplikácie %1$s je potrebná aktualizácia servera,"</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s nemá dovolené pripojiť sa k %2$s."</string> <string name="screen_change_server_error_unauthorized_homeserver">"%1$s nemá dovolené pripojiť sa k %2$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_content">"Táto aplikácia bola nastavená tak, aby povoľovala: %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_title">"Poskytovateľ účtu %1$s nie je povolený."</string>
<string name="screen_change_server_form_header">"Adresa URL domovského servera"</string> <string name="screen_change_server_form_header">"Adresa URL domovského servera"</string>
<string name="screen_change_server_form_notice">"Zadajte adresu domény."</string> <string name="screen_change_server_form_notice">"Zadajte adresu domény."</string>
<string name="screen_change_server_subtitle">"Aká je adresa vášho servera?"</string> <string name="screen_change_server_subtitle">"Aká je adresa vášho servera?"</string>
@ -32,6 +34,13 @@
<string name="screen_login_subtitle">"Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu."</string> <string name="screen_login_subtitle">"Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu."</string>
<string name="screen_login_title">"Vitajte späť!"</string> <string name="screen_login_title">"Vitajte späť!"</string>
<string name="screen_login_title_with_homeserver">"Prihlásiť sa do %1$s"</string> <string name="screen_login_title_with_homeserver">"Prihlásiť sa do %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Prihlásiť sa manuálne"</string>
<string name="screen_onboarding_sign_in_to">"Prihlásiť sa do %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Prihlásiť sa pomocou QR kódu"</string>
<string name="screen_onboarding_sign_up">"Vytvoriť účet"</string>
<string name="screen_onboarding_welcome_message">"Vitajte v najrýchlejšom %1$s vôbec. Nadupaný pre rýchlosť a jednoduchosť."</string>
<string name="screen_onboarding_welcome_subtitle">"Vitajte v %1$s. Nadupaný, pre rýchlosť a jednoduchosť."</string>
<string name="screen_onboarding_welcome_title">"Buďte vo svojom elemente"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Nadväzovanie bezpečného spojenia"</string> <string name="screen_qr_code_login_connecting_subtitle">"Nadväzovanie bezpečného spojenia"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"K novému zariadeniu sa nepodarilo vytvoriť bezpečné pripojenie. Vaše existujúce zariadenia sú stále v bezpečí a nemusíte sa o ne obávať."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"K novému zariadeniu sa nepodarilo vytvoriť bezpečné pripojenie. Vaše existujúce zariadenia sú stále v bezpečí a nemusíte sa o ne obávať."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Čo teraz?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Čo teraz?"</string>

View file

@ -29,6 +29,12 @@
<string name="screen_login_subtitle">"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."</string> <string name="screen_login_subtitle">"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."</string>
<string name="screen_login_title">"Välkommen tillbaka!"</string> <string name="screen_login_title">"Välkommen tillbaka!"</string>
<string name="screen_login_title_with_homeserver">"Logga in på %1$s"</string> <string name="screen_login_title_with_homeserver">"Logga in på %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Logga in manuellt"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Logga in med QR-kod"</string>
<string name="screen_onboarding_sign_up">"Skapa konto"</string>
<string name="screen_onboarding_welcome_message">"Välkommen till den snabbaste %1$s någonsin. Superladdad för snabbhet och enkelhet."</string>
<string name="screen_onboarding_welcome_subtitle">"Välkommen till %1$s. Superladdad, för snabbhet och enkelhet."</string>
<string name="screen_onboarding_welcome_title">"Var i ditt rätta element"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Upprättar en säker anslutning"</string> <string name="screen_qr_code_login_connecting_subtitle">"Upprättar en säker anslutning"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"En säker anslutning kunde inte göras till den nya enheten. Dina befintliga enheter är fortfarande säkra och du behöver inte oroa dig för dem."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"En säker anslutning kunde inte göras till den nya enheten. Dina befintliga enheter är fortfarande säkra och du behöver inte oroa dig för dem."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Nu då?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Nu då?"</string>

View file

@ -29,6 +29,12 @@
<string name="screen_login_subtitle">"Matrix, güvenli, merkezi olmayan iletişim için açık bir ağdır."</string> <string name="screen_login_subtitle">"Matrix, güvenli, merkezi olmayan iletişim için açık bir ağdır."</string>
<string name="screen_login_title">"Tekrar hoş geldiniz!"</string> <string name="screen_login_title">"Tekrar hoş geldiniz!"</string>
<string name="screen_login_title_with_homeserver">"%1$s adresinde oturum aç"</string> <string name="screen_login_title_with_homeserver">"%1$s adresinde oturum aç"</string>
<string name="screen_onboarding_sign_in_manually">"Manuel olarak oturum aç"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"QR kodu ile giriş yap"</string>
<string name="screen_onboarding_sign_up">"Hesap oluştur"</string>
<string name="screen_onboarding_welcome_message">"Şimdiye kadarki en hızlı %1$s hoş geldiniz. Hız ve basitlik için güçlendirildi."</string>
<string name="screen_onboarding_welcome_subtitle">"%1$s\'e hoş geldiniz. Hız ve basitlik için süper şarjlı."</string>
<string name="screen_onboarding_welcome_title">"Kendi elementinizde olun"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Güvenli bir bağlantı kuruluyor"</string> <string name="screen_qr_code_login_connecting_subtitle">"Güvenli bir bağlantı kuruluyor"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Yeni cihaza güvenli bir bağlantı kurulamadı. Mevcut cihazlarınız hala güvende ve onlar için endişelenmenize gerek yok."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Yeni cihaza güvenli bir bağlantı kurulamadı. Mevcut cihazlarınız hala güvende ve onlar için endişelenmenize gerek yok."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Şimdi ne olacak?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Şimdi ne olacak?"</string>

View file

@ -14,9 +14,10 @@
<string name="screen_change_account_provider_subtitle">"Використати іншого провайдера облікових записів, наприклад, власний приватний сервер або робочий обліковий запис."</string> <string name="screen_change_account_provider_subtitle">"Використати іншого провайдера облікових записів, наприклад, власний приватний сервер або робочий обліковий запис."</string>
<string name="screen_change_account_provider_title">"Змінити провайдера облікового запису"</string> <string name="screen_change_account_provider_title">"Змінити провайдера облікового запису"</string>
<string name="screen_change_server_error_invalid_homeserver">"Не вдалося під\'єднатися до цього домашнього сервера. Перевірте правильність введеної URL-адреси домашнього сервера. Якщо URL-адреса правильна, зверніться по додаткову допомогу до адміністратора домашнього сервера."</string> <string name="screen_change_server_error_invalid_homeserver">"Не вдалося під\'єднатися до цього домашнього сервера. Перевірте правильність введеної URL-адреси домашнього сервера. Якщо URL-адреса правильна, зверніться по додаткову допомогу до адміністратора домашнього сервера."</string>
<string name="screen_change_server_error_invalid_well_known">"Sliding sync недоступний через проблему у файлі well-known: <string name="screen_change_server_error_invalid_well_known">"Сервер недоступний через помилку у файлі well-known:
%1$s"</string> %1$s"</string>
<string name="screen_change_server_form_header">"URL-адреса домашнього сервера"</string> <string name="screen_change_server_form_header">"URL-адреса домашнього сервера"</string>
<string name="screen_change_server_form_notice">"Введіть адресу домену."</string>
<string name="screen_change_server_subtitle">"Яка адреса вашого сервера?"</string> <string name="screen_change_server_subtitle">"Яка адреса вашого сервера?"</string>
<string name="screen_change_server_title">"Виберіть свій сервер"</string> <string name="screen_change_server_title">"Виберіть свій сервер"</string>
<string name="screen_create_account_title">"Створити обліковий запис"</string> <string name="screen_create_account_title">"Створити обліковий запис"</string>
@ -29,6 +30,13 @@
<string name="screen_login_subtitle">"Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."</string> <string name="screen_login_subtitle">"Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."</string>
<string name="screen_login_title">"З поверненням!"</string> <string name="screen_login_title">"З поверненням!"</string>
<string name="screen_login_title_with_homeserver">"Увійти в %1$s"</string> <string name="screen_login_title_with_homeserver">"Увійти в %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Увійти вручну"</string>
<string name="screen_onboarding_sign_in_to">"Увійти в %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Увійти за допомогою QR-коду"</string>
<string name="screen_onboarding_sign_up">"Створити обліковий запис"</string>
<string name="screen_onboarding_welcome_message">"Ласкаво просимо до найшвидшого %1$s. Заряджений для швидкості та простоти."</string>
<string name="screen_onboarding_welcome_subtitle">"Ласкаво просимо до %1$s. Заряджений, для швидкості та простоти."</string>
<string name="screen_onboarding_welcome_title">"Будьте у своєму element"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Встановлення безпечного з\'єднання"</string> <string name="screen_qr_code_login_connecting_subtitle">"Встановлення безпечного з\'єднання"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Не вдалося встановити безпечне з\'єднання з новим пристроєм. Ваші наявні пристрої досі в безпеці, і вам не потрібно про них турбуватися."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"Не вдалося встановити безпечне з\'єднання з новим пристроєм. Ваші наявні пристрої досі в безпеці, і вам не потрібно про них турбуватися."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Що тепер?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"Що тепер?"</string>

View file

@ -26,6 +26,12 @@
<string name="screen_login_subtitle">"Matrix xavfsiz, markazlashmagan aloqa uchun ochiq tarmoqdir."</string> <string name="screen_login_subtitle">"Matrix xavfsiz, markazlashmagan aloqa uchun ochiq tarmoqdir."</string>
<string name="screen_login_title">"Qaytib kelganingizdan xursandmiz!"</string> <string name="screen_login_title">"Qaytib kelganingizdan xursandmiz!"</string>
<string name="screen_login_title_with_homeserver">"Kirish%1$s"</string> <string name="screen_login_title_with_homeserver">"Kirish%1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Qo\'lda tizimga kiring"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"QR kod bilan tizimga kiring"</string>
<string name="screen_onboarding_sign_up">"Hisob yaratish"</string>
<string name="screen_onboarding_welcome_message">"Eng tezkor %1$sga xush kelibsiz. Tezlik va oddylik uchun super zaryadlangan."</string>
<string name="screen_onboarding_welcome_subtitle">"%1$sga Xush kelibsiz. Tezlik va oddylik uchun o\'ta zaryadlangan."</string>
<string name="screen_onboarding_welcome_title">"Elementingizda bo\'ling"</string>
<string name="screen_server_confirmation_change_server">"Hisob provayderini o\'zgartiring"</string> <string name="screen_server_confirmation_change_server">"Hisob provayderini o\'zgartiring"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Element xodimlari uchun shaxsiy server."</string> <string name="screen_server_confirmation_message_login_element_dot_io">"Element xodimlari uchun shaxsiy server."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix xavfsiz, markazlashmagan aloqa uchun ochiq tarmoqdir."</string> <string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix xavfsiz, markazlashmagan aloqa uchun ochiq tarmoqdir."</string>

View file

@ -29,6 +29,12 @@
<string name="screen_login_subtitle">"Matrix 是一個開放網路,為了安全且去中心化的通訊而生。"</string> <string name="screen_login_subtitle">"Matrix 是一個開放網路,為了安全且去中心化的通訊而生。"</string>
<string name="screen_login_title">"歡迎回來!"</string> <string name="screen_login_title">"歡迎回來!"</string>
<string name="screen_login_title_with_homeserver">"登入 %1$s"</string> <string name="screen_login_title_with_homeserver">"登入 %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"手動登入"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"使用 QR code 登入"</string>
<string name="screen_onboarding_sign_up">"建立帳號"</string>
<string name="screen_onboarding_welcome_message">"歡迎使用有史以來最快的 %1$s。速度超快操作簡便。"</string>
<string name="screen_onboarding_welcome_subtitle">"歡迎使用 %1$s。速度超快且簡單。"</string>
<string name="screen_onboarding_welcome_title">"Be in your element"</string>
<string name="screen_qr_code_login_connecting_subtitle">"建立安全連線"</string> <string name="screen_qr_code_login_connecting_subtitle">"建立安全連線"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"無法與新裝置建立安全連線。您現有的裝置仍然安全,您不必擔心它們。"</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"無法與新裝置建立安全連線。您現有的裝置仍然安全,您不必擔心它們。"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"現在怎麼辦?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"現在怎麼辦?"</string>

View file

@ -29,6 +29,12 @@
<string name="screen_login_subtitle">"Matrix 是一个用于安全、去中心化通信的开放网络。"</string> <string name="screen_login_subtitle">"Matrix 是一个用于安全、去中心化通信的开放网络。"</string>
<string name="screen_login_title">"欢迎回来!"</string> <string name="screen_login_title">"欢迎回来!"</string>
<string name="screen_login_title_with_homeserver">"登录到 %1$s"</string> <string name="screen_login_title_with_homeserver">"登录到 %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"手动登录"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"使用二维码登录"</string>
<string name="screen_onboarding_sign_up">"创建账户"</string>
<string name="screen_onboarding_welcome_message">"欢迎使用 %1$s快而简约的消息应用。"</string>
<string name="screen_onboarding_welcome_subtitle">"欢迎使用 %1$s速度与简洁的极致。"</string>
<string name="screen_onboarding_welcome_title">"融入您的 Element"</string>
<string name="screen_qr_code_login_connecting_subtitle">"建立安全连接"</string> <string name="screen_qr_code_login_connecting_subtitle">"建立安全连接"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"无法与新设备建立安全连接。您现有的设备仍然安全,无需担心。"</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"无法与新设备建立安全连接。您现有的设备仍然安全,无需担心。"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"现在怎么办?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"现在怎么办?"</string>

View file

@ -18,6 +18,8 @@
%1$s"</string> %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"The selected account provider does not support sliding sync. An upgrade to the server is needed to use %1$s."</string> <string name="screen_change_server_error_no_sliding_sync_message">"The selected account provider does not support sliding sync. An upgrade to the server is needed to use %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s is not allowed to connect to %2$s."</string> <string name="screen_change_server_error_unauthorized_homeserver">"%1$s is not allowed to connect to %2$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_content">"This app has been configured to allow: %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_title">"Account provider %1$s not allowed."</string>
<string name="screen_change_server_form_header">"Homeserver URL"</string> <string name="screen_change_server_form_header">"Homeserver URL"</string>
<string name="screen_change_server_form_notice">"Enter a domain address."</string> <string name="screen_change_server_form_notice">"Enter a domain address."</string>
<string name="screen_change_server_subtitle">"What is the address of your server?"</string> <string name="screen_change_server_subtitle">"What is the address of your server?"</string>
@ -32,6 +34,13 @@
<string name="screen_login_subtitle">"Matrix is an open network for secure, decentralised communication."</string> <string name="screen_login_subtitle">"Matrix is an open network for secure, decentralised communication."</string>
<string name="screen_login_title">"Welcome back!"</string> <string name="screen_login_title">"Welcome back!"</string>
<string name="screen_login_title_with_homeserver">"Sign in to %1$s"</string> <string name="screen_login_title_with_homeserver">"Sign in to %1$s"</string>
<string name="screen_onboarding_sign_in_manually">"Sign in manually"</string>
<string name="screen_onboarding_sign_in_to">"Sign in to %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Sign in with QR code"</string>
<string name="screen_onboarding_sign_up">"Create account"</string>
<string name="screen_onboarding_welcome_message">"Welcome to the fastest %1$s ever. Supercharged for speed and simplicity."</string>
<string name="screen_onboarding_welcome_subtitle">"Welcome to %1$s. Supercharged, for speed and simplicity."</string>
<string name="screen_onboarding_welcome_title">"Be in your element"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Establishing a secure connection"</string> <string name="screen_qr_code_login_connecting_subtitle">"Establishing a secure connection"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"A secure connection could not be made to the new device. Your existing devices are still safe and you don\'t need to worry about them."</string> <string name="screen_qr_code_login_connection_note_secure_state_description">"A secure connection could not be made to the new device. Your existing devices are still safe and you don\'t need to worry about them."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"What now?"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_header">"What now?"</string>

View file

@ -0,0 +1,81 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.api.LoginParams
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DefaultLoginIntentResolverTest {
@Test
fun `nominal case`() {
val sut = DefaultLoginIntentResolver()
val uriString = "https://mobile.element.io/element?account_provider=example.org&login_hint=mxid:@alice:example.org"
assertThat(sut.parse(uriString)).isEqualTo(
LoginParams(
accountProvider = "example.org",
loginHint = "mxid:@alice:example.org",
)
)
}
@Test
fun `extra unknown param`() {
val sut = DefaultLoginIntentResolver()
val uriString = "https://mobile.element.io/element?account_provider=example.org&login_hint=mxid:@alice:example.org&extra=uknown"
assertThat(sut.parse(uriString)).isEqualTo(
LoginParams(
accountProvider = "example.org",
loginHint = "mxid:@alice:example.org",
)
)
}
@Test
fun `no account provider`() {
val sut = DefaultLoginIntentResolver()
val uriString = "https://mobile.element.io/element?login_hint=mxid:@alice:example.org"
assertThat(sut.parse(uriString)).isNull()
}
@Test
fun `no path`() {
val sut = DefaultLoginIntentResolver()
val uriString = "https://mobile.element.io?account_provider=example.org&login_hint=mxid:@alice:example.org"
assertThat(sut.parse(uriString)).isNull()
}
@Test
fun `wrong path`() {
val sut = DefaultLoginIntentResolver()
val uriString = "https://mobile.element.io/wrong?account_provider=example.org&login_hint=mxid:@alice:example.org"
assertThat(sut.parse(uriString)).isNull()
}
@Test
fun `wrong host`() {
val sut = DefaultLoginIntentResolver()
val uriString = "https://wrong.element.io/element?account_provider=example.org&login_hint=mxid:@alice:example.org"
assertThat(sut.parse(uriString)).isNull()
}
@Test
fun `no login_hint param`() {
val sut = DefaultLoginIntentResolver()
val uriString = "https://mobile.element.io/element?account_provider=example.org"
assertThat(sut.parse(uriString)).isEqualTo(
LoginParams(
accountProvider = "example.org",
loginHint = null,
)
)
}
}

View file

@ -27,11 +27,11 @@ class AccountProviderDataSourceTest {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState).isEqualTo( assertThat(initialState).isEqualTo(
AccountProvider( AccountProvider(
url = FakeEnterpriseService.A_FAKE_HOMESERVER, url = AuthenticationConfig.MATRIX_ORG_URL,
title = FakeEnterpriseService.A_FAKE_HOMESERVER, title = "matrix.org",
subtitle = null, subtitle = null,
isPublic = false, isPublic = true,
isMatrixOrg = false, isMatrixOrg = true,
isValid = false, isValid = false,
) )
) )
@ -40,9 +40,11 @@ class AccountProviderDataSourceTest {
@Test @Test
fun `present - initial state - matrix org`() = runTest { fun `present - initial state - matrix org`() = runTest {
val sut = AccountProviderDataSource(FakeEnterpriseService( val sut = AccountProviderDataSource(
defaultHomeserverResult = { AuthenticationConfig.MATRIX_ORG_URL } FakeEnterpriseService(
)) defaultHomeserverListResult = { listOf(AuthenticationConfig.MATRIX_ORG_URL) }
)
)
sut.flow.test { sut.flow.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState).isEqualTo( assertThat(initialState).isEqualTo(
@ -63,7 +65,7 @@ class AccountProviderDataSourceTest {
val sut = AccountProviderDataSource(FakeEnterpriseService()) val sut = AccountProviderDataSource(FakeEnterpriseService())
sut.flow.test { sut.flow.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.url).isEqualTo(FakeEnterpriseService.A_FAKE_HOMESERVER) assertThat(initialState.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL)
sut.userSelection(AccountProvider(url = "https://example.com")) sut.userSelection(AccountProvider(url = "https://example.com"))
val changedState = awaitItem() val changedState = awaitItem()
assertThat(changedState).isEqualTo( assertThat(changedState).isEqualTo(
@ -78,7 +80,7 @@ class AccountProviderDataSourceTest {
) )
sut.reset() sut.reset()
val resetState = awaitItem() val resetState = awaitItem()
assertThat(resetState.url).isEqualTo(FakeEnterpriseService.A_FAKE_HOMESERVER) assertThat(resetState.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL)
} }
} }
} }

View file

@ -84,6 +84,7 @@ class ChangeServerPresenterTest {
createPresenter( createPresenter(
enterpriseService = FakeEnterpriseService( enterpriseService = FakeEnterpriseService(
isAllowedToConnectToHomeserverResult = isAllowedToConnectToHomeserverResult, isAllowedToConnectToHomeserverResult = isAllowedToConnectToHomeserverResult,
defaultHomeserverListResult = { listOf("element.io") },
), ),
).test { ).test {
val initialState = awaitItem() val initialState = awaitItem()
@ -94,8 +95,11 @@ class ChangeServerPresenterTest {
assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java) assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java)
val failureState = awaitItem() val failureState = awaitItem()
assertThat( assertThat(
(failureState.changeServerAction.errorOrNull() as ChangeServerError.UnauthorizedAccountProvider).accountProvider (failureState.changeServerAction.errorOrNull() as ChangeServerError.UnauthorizedAccountProvider).unauthorisedAccountProviderTitle
).isEqualTo(anAccountProvider) ).isEqualTo(anAccountProvider.title)
assertThat(
(failureState.changeServerAction.errorOrNull() as ChangeServerError.UnauthorizedAccountProvider).authorisedAccountProviderTitles
).containsExactly("element.io")
isAllowedToConnectToHomeserverResult.assertions() isAllowedToConnectToHomeserverResult.assertions()
.isCalledOnce() .isCalledOnce()
.with(value(A_HOMESERVER_URL)) .with(value(A_HOMESERVER_URL))

View file

@ -11,6 +11,7 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.changeserver.aChangeServerState import io.element.android.features.login.impl.changeserver.aChangeServerState
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
@ -25,7 +26,8 @@ class ChangeAccountProviderPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = ChangeAccountProviderPresenter( val presenter = ChangeAccountProviderPresenter(
changeServerPresenter = { aChangeServerState() } changeServerPresenter = { aChangeServerState() },
enterpriseService = FakeEnterpriseService(),
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()

View file

@ -11,10 +11,13 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.login.LoginMode
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
import io.element.android.features.login.impl.screens.onboarding.createLoginHelper
import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -24,6 +27,7 @@ import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow
import io.element.android.libraries.oidc.impl.customtab.DefaultOidcActionFlow import io.element.android.libraries.oidc.impl.customtab.DefaultOidcActionFlow
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.waitForPredicate import io.element.android.tests.testutils.waitForPredicate
@ -44,8 +48,8 @@ class ConfirmAccountProviderPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.isAccountCreation).isFalse() assertThat(initialState.isAccountCreation).isFalse()
assertThat(initialState.submitEnabled).isTrue() assertThat(initialState.submitEnabled).isTrue()
assertThat(initialState.accountProvider.url).isEqualTo(FakeEnterpriseService.A_FAKE_HOMESERVER) assertThat(initialState.accountProvider.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL)
assertThat(initialState.loginFlow).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.loginMode).isEqualTo(AsyncData.Uninitialized)
} }
} }
@ -63,11 +67,11 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java) assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginFlow.dataOrNull()).isEqualTo(LoginFlow.PasswordLogin) assertThat(successState.loginMode.dataOrNull()).isEqualTo(LoginMode.PasswordLogin)
} }
} }
@ -85,11 +89,11 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java) assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
} }
} }
@ -109,15 +113,15 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java) assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
authenticationService.givenOidcCancelError(A_THROWABLE) authenticationService.givenOidcCancelError(A_THROWABLE)
defaultOidcActionFlow.post(OidcAction.GoBack) defaultOidcActionFlow.post(OidcAction.GoBack)
val cancelFailureState = awaitItem() val cancelFailureState = awaitItem()
assertThat(cancelFailureState.loginFlow).isInstanceOf(AsyncData.Failure::class.java) assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
} }
} }
@ -137,14 +141,14 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java) assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
defaultOidcActionFlow.post(OidcAction.GoBack) defaultOidcActionFlow.post(OidcAction.GoBack)
val cancelFinalState = awaitItem() val cancelFinalState = awaitItem()
assertThat(cancelFinalState.loginFlow).isInstanceOf(AsyncData.Uninitialized::class.java) assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java)
} }
} }
@ -164,17 +168,17 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java) assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
authenticationService.givenLoginError(A_THROWABLE) authenticationService.givenLoginError(A_THROWABLE)
defaultOidcActionFlow.post(OidcAction.Success("aUrl")) defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
val cancelLoadingState = awaitItem() val cancelLoadingState = awaitItem()
assertThat(cancelLoadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java) assertThat(cancelLoadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
val cancelFailureState = awaitItem() val cancelFailureState = awaitItem()
assertThat(cancelFailureState.loginFlow).isInstanceOf(AsyncData.Failure::class.java) assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
} }
} }
@ -198,15 +202,15 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java) assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse() assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse()
defaultOidcActionFlow.post(OidcAction.Success("aUrl")) defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
val successSuccessState = awaitItem() val successSuccessState = awaitItem()
assertThat(successSuccessState.loginFlow).isInstanceOf(AsyncData.Loading::class.java) assertThat(successSuccessState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
waitForPredicate { defaultLoginUserStory.loginFlowIsDone.value } waitForPredicate { defaultLoginUserStory.loginFlowIsDone.value }
} }
} }
@ -226,7 +230,7 @@ class ConfirmAccountProviderPresenterTest {
skipItems(1) // Loading skipItems(1) // Loading
val failureState = awaitItem() val failureState = awaitItem()
assertThat(failureState.submitEnabled).isFalse() assertThat(failureState.submitEnabled).isFalse()
assertThat(failureState.loginFlow).isInstanceOf(AsyncData.Failure::class.java) assertThat(failureState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
} }
} }
@ -249,12 +253,12 @@ class ConfirmAccountProviderPresenterTest {
// Check an error was returned // Check an error was returned
val submittedState = awaitItem() val submittedState = awaitItem()
assertThat(submittedState.loginFlow).isInstanceOf(AsyncData.Failure::class.java) assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
// Assert the error is then cleared // Assert the error is then cleared
submittedState.eventSink(ConfirmAccountProviderEvents.ClearError) submittedState.eventSink(ConfirmAccountProviderEvents.ClearError)
val clearedState = awaitItem() val clearedState = awaitItem()
assertThat(clearedState.loginFlow).isEqualTo(AsyncData.Uninitialized) assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized)
} }
} }
@ -277,11 +281,11 @@ class ConfirmAccountProviderPresenterTest {
skipItems(1) // Loading skipItems(1) // Loading
// Check an error was returned // Check an error was returned
val submittedState = awaitItem() val submittedState = awaitItem()
assertThat(submittedState.loginFlow.errorOrNull()).isInstanceOf(AccountCreationNotSupported::class.java) assertThat(submittedState.loginMode.errorOrNull()).isInstanceOf(AccountCreationNotSupported::class.java)
// Assert the error is then cleared // Assert the error is then cleared
submittedState.eventSink(ConfirmAccountProviderEvents.ClearError) submittedState.eventSink(ConfirmAccountProviderEvents.ClearError)
val clearedState = awaitItem() val clearedState = awaitItem()
assertThat(clearedState.loginFlow).isEqualTo(AsyncData.Uninitialized) assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized)
} }
} }
@ -300,8 +304,8 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink(ConfirmAccountProviderEvents.Continue) initialState.eventSink(ConfirmAccountProviderEvents.Continue)
skipItems(1) // Loading skipItems(1) // Loading
val submittedState = awaitItem() val submittedState = awaitItem()
assertThat(submittedState.loginFlow).isInstanceOf(AsyncData.Success::class.java) assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(submittedState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
} }
} }
@ -322,8 +326,8 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink(ConfirmAccountProviderEvents.Continue) initialState.eventSink(ConfirmAccountProviderEvents.Continue)
skipItems(1) // Loading skipItems(1) // Loading
val submittedState = awaitItem() val submittedState = awaitItem()
assertThat(submittedState.loginFlow).isInstanceOf(AsyncData.Success::class.java) assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(submittedState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
} }
} }
@ -344,7 +348,7 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink(ConfirmAccountProviderEvents.Continue) initialState.eventSink(ConfirmAccountProviderEvents.Continue)
skipItems(1) // Loading skipItems(1) // Loading
val submittedState = awaitItem() val submittedState = awaitItem()
assertThat(submittedState.loginFlow.dataOrNull()).isEqualTo(LoginFlow.AccountCreationFlow(aUrl)) assertThat(submittedState.loginMode.dataOrNull()).isEqualTo(LoginMode.AccountCreation(aUrl))
} }
} }
@ -352,15 +356,17 @@ class ConfirmAccountProviderPresenterTest {
params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false), params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
defaultOidcActionFlow: DefaultOidcActionFlow = DefaultOidcActionFlow(), defaultOidcActionFlow: OidcActionFlow = DefaultOidcActionFlow(),
defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(), defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(),
webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(),
) = ConfirmAccountProviderPresenter( ) = ConfirmAccountProviderPresenter(
params = params, params = params,
accountProviderDataSource = accountProviderDataSource, accountProviderDataSource = accountProviderDataSource,
authenticationService = matrixAuthenticationService, loginHelper = createLoginHelper(
oidcActionFlow = defaultOidcActionFlow, authenticationService = matrixAuthenticationService,
defaultLoginUserStory = defaultLoginUserStory, oidcActionFlow = defaultOidcActionFlow,
webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever defaultLoginUserStory = defaultLoginUserStory,
webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever,
),
) )
} }

Some files were not shown because too many files have changed in this diff Show more