Merge branch 'release/25.06.0' into main
This commit is contained in:
commit
6e5001a2f1
820 changed files with 10309 additions and 4974 deletions
2
.github/workflows/maestro-local.yml
vendored
2
.github/workflows/maestro-local.yml
vendored
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,22 @@ appId: ${MAESTRO_APP_ID}
|
||||||
visible: 'Use without an account'
|
visible: 'Use without an account'
|
||||||
commands:
|
commands:
|
||||||
- tapOn: "Use without an account"
|
- 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.
|
## Working when running Maestro locally, but not on the CI yet.
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible:
|
||||||
|
id: "form-1"
|
||||||
|
timeout: 10000
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "form-1"
|
id: "form-1"
|
||||||
- inputText: ${MAESTRO_USERNAME}
|
- inputText: ${MAESTRO_USERNAME}
|
||||||
|
|
|
||||||
47
CHANGES.md
47
CHANGES.md
|
|
@ -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
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -354,3 +354,12 @@ fun Project.configureLicensesTasks(reportingExtension: ReportingExtension) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configurations.all {
|
||||||
|
resolutionStrategy {
|
||||||
|
dependencySubstitution {
|
||||||
|
val tink = libs.google.tink.get()
|
||||||
|
substitute(module("com.google.crypto.tink:tink")).using(module("${tink.group}:${tink.name}:${tink.version}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,8 @@
|
||||||
-->
|
-->
|
||||||
<full-backup-content>
|
<full-backup-content>
|
||||||
<exclude domain="root" path="." />
|
<exclude domain="root" path="." />
|
||||||
|
<exclude domain="file" path="." />
|
||||||
|
<exclude domain="database" path="." />
|
||||||
|
<exclude domain="sharedpref" path="." />
|
||||||
|
<exclude domain="external" path="."/>
|
||||||
</full-backup-content>
|
</full-backup-content>
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,18 @@
|
||||||
-->
|
-->
|
||||||
<data-extraction-rules>
|
<data-extraction-rules>
|
||||||
<cloud-backup>
|
<cloud-backup>
|
||||||
<exclude domain="root" path="." />
|
<exclude domain="root" path="."/>
|
||||||
|
<exclude domain="file" path="."/>
|
||||||
|
<exclude domain="database" path="."/>
|
||||||
|
<exclude domain="sharedpref" path="."/>
|
||||||
|
<exclude domain="external" path="."/>
|
||||||
</cloud-backup>
|
</cloud-backup>
|
||||||
|
|
||||||
<device-transfer>
|
<device-transfer>
|
||||||
<exclude domain="root" path="." />
|
<exclude domain="root" path="."/>
|
||||||
|
<exclude domain="file" path="."/>
|
||||||
|
<exclude domain="database" path="."/>
|
||||||
|
<exclude domain="sharedpref" path="."/>
|
||||||
|
<exclude domain="external" path="."/>
|
||||||
</device-transfer>
|
</device-transfer>
|
||||||
</data-extraction-rules>
|
</data-extraction-rules>
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
<locale android:name="sv"/>
|
<locale android:name="sv"/>
|
||||||
<locale android:name="tr"/>
|
<locale android:name="tr"/>
|
||||||
<locale android:name="uk"/>
|
<locale android:name="uk"/>
|
||||||
|
<locale android:name="ur"/>
|
||||||
<locale android:name="uz"/>
|
<locale android:name="uz"/>
|
||||||
<locale android:name="zh-CN"/>
|
<locale android:name="zh-CN"/>
|
||||||
<locale android:name="zh-TW"/>
|
<locale android:name="zh-TW"/>
|
||||||
|
|
|
||||||
|
|
@ -53,16 +53,15 @@ 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.test)
|
||||||
testImplementation(projects.libraries.preferences.test)
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.libraries.pushproviders.test)
|
testImplementation(projects.libraries.pushproviders.test)
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
testImplementation(projects.features.login.impl)
|
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testImplementation(projects.features.rageshake.test)
|
testImplementation(projects.features.rageshake.test)
|
||||||
testImplementation(projects.features.rageshake.impl)
|
|
||||||
testImplementation(projects.features.share.test)
|
testImplementation(projects.features.share.test)
|
||||||
testImplementation(projects.services.appnavstate.test)
|
testImplementation(projects.services.appnavstate.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,11 @@ 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.LoginParams
|
||||||
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
|
||||||
|
|
@ -46,10 +49,16 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
||||||
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(
|
||||||
|
|
@ -74,6 +83,12 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
loginEntryPoint
|
loginEntryPoint
|
||||||
.nodeBuilder(this, buildContext)
|
.nodeBuilder(this, buildContext)
|
||||||
|
.params(
|
||||||
|
LoginEntryPoint.Params(
|
||||||
|
accountProvider = inputs.loginParams?.accountProvider,
|
||||||
|
loginHint = inputs.loginParams?.loginHint,
|
||||||
|
)
|
||||||
|
)
|
||||||
.callback(callback)
|
.callback(callback)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -51,6 +52,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
private val appNavigationStateService: AppNavigationStateService,
|
private val appNavigationStateService: AppNavigationStateService,
|
||||||
private val appCoroutineScope: CoroutineScope,
|
private val appCoroutineScope: CoroutineScope,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
|
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||||
roomComponentFactory: RoomComponentFactory,
|
roomComponentFactory: RoomComponentFactory,
|
||||||
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
|
|
@ -85,6 +87,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
onCreate = {
|
onCreate = {
|
||||||
Timber.v("OnCreate => ${inputs.room.roomId}")
|
Timber.v("OnCreate => ${inputs.room.roomId}")
|
||||||
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
||||||
|
activeRoomsHolder.addRoom(inputs.room)
|
||||||
fetchRoomMembers()
|
fetchRoomMembers()
|
||||||
trackVisitedRoom()
|
trackVisitedRoom()
|
||||||
},
|
},
|
||||||
|
|
@ -95,6 +98,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
},
|
},
|
||||||
onDestroy = {
|
onDestroy = {
|
||||||
Timber.v("OnDestroy")
|
Timber.v("OnDestroy")
|
||||||
|
activeRoomsHolder.removeRoom(inputs.room.sessionId, inputs.room.roomId)
|
||||||
inputs.room.destroy()
|
inputs.room.destroy()
|
||||||
appNavigationStateService.onLeavingRoom(id)
|
appNavigationStateService.onLeavingRoom(id)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +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">"Amaitu saioa eta bertsio-berritu"</string>
|
<string name="banner_migrate_to_native_sliding_sync_action">"Amaitu saioa eta bertsio-berritu"</string>
|
||||||
|
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s(e)k ez da bateragarria lehengo protokoloarekin. Amaitu saioa eta hasi berriro aplikazioa erabiltzen jarraitzeko."</string>
|
||||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Zure zerbitzaria ez da bateragarria protokolo zaharrarekin. Amaitu saioa eta hasi berriro aplikazioa erabiltzen jarraitzeko."</string>
|
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Zure zerbitzaria ez da bateragarria protokolo zaharrarekin. Amaitu saioa eta hasi berriro aplikazioa erabiltzen jarraitzeko."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
6
appnav/src/main/res/values-pt-rBR/translations.xml
Normal file
6
appnav/src/main/res/values-pt-rBR/translations.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="banner_migrate_to_native_sliding_sync_action">"Sair e atualizar"</string>
|
||||||
|
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s não suporta mais o protocolo antigo. Termine sessão e volte a iniciar sessão para continuar a utilizar a aplicação."</string>
|
||||||
|
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Seu servidor doméstico não é mais compatível com o protocolo antigo. Faça logout e login novamente para continuar usando o aplicativo."</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -24,16 +24,18 @@ import io.element.android.features.messages.api.MessagesEntryPoint
|
||||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||||
import io.element.android.libraries.architecture.childNode
|
import io.element.android.libraries.architecture.childNode
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class JoinBaseRoomLoadedFlowNodeTest {
|
class JoinedRoomLoadedFlowNodeTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
|
@ -96,20 +98,21 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createJoinedRoomLoadedFlowNode(
|
private fun TestScope.createJoinedRoomLoadedFlowNode(
|
||||||
plugins: List<Plugin>,
|
plugins: List<Plugin>,
|
||||||
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
|
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
|
||||||
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
||||||
coroutineScope: CoroutineScope,
|
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||||
) = JoinedRoomLoadedFlowNode(
|
) = JoinedRoomLoadedFlowNode(
|
||||||
buildContext = BuildContext.root(savedStateMap = null),
|
buildContext = BuildContext.root(savedStateMap = null),
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
messagesEntryPoint = messagesEntryPoint,
|
messagesEntryPoint = messagesEntryPoint,
|
||||||
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
||||||
appNavigationStateService = FakeAppNavigationStateService(),
|
appNavigationStateService = FakeAppNavigationStateService(),
|
||||||
appCoroutineScope = coroutineScope,
|
appCoroutineScope = this,
|
||||||
roomComponentFactory = FakeRoomComponentFactory(),
|
roomComponentFactory = FakeRoomComponentFactory(),
|
||||||
matrixClient = FakeMatrixClient(),
|
matrixClient = FakeMatrixClient(),
|
||||||
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -121,7 +124,6 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
||||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||||
plugins = listOf(inputs),
|
plugins = listOf(inputs),
|
||||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||||
coroutineScope = this
|
|
||||||
)
|
)
|
||||||
// WHEN
|
// WHEN
|
||||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||||
|
|
@ -144,7 +146,6 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
||||||
plugins = listOf(inputs),
|
plugins = listOf(inputs),
|
||||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||||
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
||||||
coroutineScope = this
|
|
||||||
)
|
)
|
||||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||||
// WHEN
|
// WHEN
|
||||||
|
|
@ -154,4 +155,53 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
||||||
val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!!
|
val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!!
|
||||||
assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId)
|
assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the ActiveRoomsHolder will be updated with the loaded room on create`() = runTest {
|
||||||
|
// GIVEN
|
||||||
|
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
|
||||||
|
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||||
|
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||||
|
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
||||||
|
val activeRoomsHolder = ActiveRoomsHolder()
|
||||||
|
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||||
|
plugins = listOf(inputs),
|
||||||
|
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||||
|
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
||||||
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull()
|
||||||
|
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||||
|
// WHEN
|
||||||
|
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.CREATED)
|
||||||
|
// THEN
|
||||||
|
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the ActiveRoomsHolder will be removed on destroy`() = runTest {
|
||||||
|
// GIVEN
|
||||||
|
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
|
||||||
|
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||||
|
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||||
|
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
||||||
|
val activeRoomsHolder = ActiveRoomsHolder().apply {
|
||||||
|
addRoom(room)
|
||||||
|
}
|
||||||
|
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||||
|
plugins = listOf(inputs),
|
||||||
|
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||||
|
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
||||||
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
|
)
|
||||||
|
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||||
|
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.CREATED)
|
||||||
|
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNotNull()
|
||||||
|
// WHEN
|
||||||
|
roomFlowNode.updateLifecycleState(Lifecycle.State.DESTROYED)
|
||||||
|
// THEN
|
||||||
|
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Messages(null), Lifecycle.State.DESTROYED)
|
||||||
|
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -20,13 +22,10 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider
|
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
import io.element.android.libraries.oidc.api.OidcAction
|
import io.element.android.libraries.oidc.api.OidcAction
|
||||||
import io.element.android.libraries.oidc.impl.DefaultOidcIntentResolver
|
import io.element.android.libraries.oidc.test.FakeOidcIntentResolver
|
||||||
import io.element.android.libraries.oidc.impl.DefaultOidcUrlParser
|
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import org.junit.Assert.assertThrows
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
|
@ -116,8 +115,10 @@ class IntentResolverTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test resolve oidc go back`() {
|
fun `test resolve oidc`() {
|
||||||
val sut = createIntentResolver()
|
val sut = createIntentResolver(
|
||||||
|
oidcIntentResolverResult = { OidcAction.GoBack },
|
||||||
|
)
|
||||||
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
|
||||||
data = "io.element.android:/?error=access_denied&state=IFF1UETGye2ZA8pO".toUri()
|
data = "io.element.android:/?error=access_denied&state=IFF1UETGye2ZA8pO".toUri()
|
||||||
|
|
@ -130,42 +131,15 @@ class IntentResolverTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test resolve oidc success`() {
|
|
||||||
val sut = createIntentResolver()
|
|
||||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
|
||||||
action = Intent.ACTION_VIEW
|
|
||||||
data = "io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB".toUri()
|
|
||||||
}
|
|
||||||
val result = sut.resolve(intent)
|
|
||||||
assertThat(result).isEqualTo(
|
|
||||||
ResolvedIntent.Oidc(
|
|
||||||
oidcAction = OidcAction.Success(
|
|
||||||
url = "io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test resolve oidc invalid`() {
|
|
||||||
val sut = createIntentResolver()
|
|
||||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
|
||||||
action = Intent.ACTION_VIEW
|
|
||||||
data = "io.element.android:/invalid".toUri()
|
|
||||||
}
|
|
||||||
assertThrows(IllegalStateException::class.java) {
|
|
||||||
sut.resolve(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test resolve external permalink`() {
|
fun `test resolve external permalink`() {
|
||||||
val permalinkData = PermalinkData.UserLink(
|
val permalinkData = PermalinkData.UserLink(
|
||||||
userId = UserId("@alice:matrix.org")
|
userId = UserId("@alice:matrix.org")
|
||||||
)
|
)
|
||||||
val sut = createIntentResolver(
|
val sut = createIntentResolver(
|
||||||
permalinkParserResult = { permalinkData }
|
loginIntentResolverResult = { null },
|
||||||
|
permalinkParserResult = { permalinkData },
|
||||||
|
oidcIntentResolverResult = { 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
|
||||||
|
|
@ -182,7 +156,9 @@ 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 },
|
||||||
|
oidcIntentResolverResult = { 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
|
||||||
|
|
@ -198,7 +174,8 @@ class IntentResolverTest {
|
||||||
userId = UserId("@alice:matrix.org")
|
userId = UserId("@alice:matrix.org")
|
||||||
)
|
)
|
||||||
val sut = createIntentResolver(
|
val sut = createIntentResolver(
|
||||||
permalinkParserResult = { permalinkData }
|
permalinkParserResult = { permalinkData },
|
||||||
|
oidcIntentResolverResult = { null },
|
||||||
)
|
)
|
||||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||||
action = Intent.ACTION_BATTERY_LOW
|
action = Intent.ACTION_BATTERY_LOW
|
||||||
|
|
@ -210,7 +187,9 @@ class IntentResolverTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test incoming share simple`() {
|
fun `test incoming share simple`() {
|
||||||
val sut = createIntentResolver()
|
val sut = createIntentResolver(
|
||||||
|
oidcIntentResolverResult = { null },
|
||||||
|
)
|
||||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||||
action = Intent.ACTION_SEND
|
action = Intent.ACTION_SEND
|
||||||
}
|
}
|
||||||
|
|
@ -220,7 +199,9 @@ class IntentResolverTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test incoming share multiple`() {
|
fun `test incoming share multiple`() {
|
||||||
val sut = createIntentResolver()
|
val sut = createIntentResolver(
|
||||||
|
oidcIntentResolverResult = { null },
|
||||||
|
)
|
||||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||||
action = Intent.ACTION_SEND_MULTIPLE
|
action = Intent.ACTION_SEND_MULTIPLE
|
||||||
}
|
}
|
||||||
|
|
@ -231,7 +212,9 @@ 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 },
|
||||||
|
oidcIntentResolverResult = { 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,15 +224,33 @@ class IntentResolverTest {
|
||||||
assertThat(result).isNull()
|
assertThat(result).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test resolve login param`() {
|
||||||
|
val aLoginParams = LoginParams("accountProvider", null)
|
||||||
|
val sut = createIntentResolver(
|
||||||
|
loginIntentResolverResult = { aLoginParams },
|
||||||
|
oidcIntentResolverResult = { null },
|
||||||
|
)
|
||||||
|
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() },
|
||||||
|
oidcIntentResolverResult: (Intent) -> OidcAction? = { lambdaError() },
|
||||||
): IntentResolver {
|
): IntentResolver {
|
||||||
return IntentResolver(
|
return IntentResolver(
|
||||||
deeplinkParser = DeeplinkParser(),
|
deeplinkParser = DeeplinkParser(),
|
||||||
oidcIntentResolver = DefaultOidcIntentResolver(
|
loginIntentResolver = FakeLoginIntentResolver(
|
||||||
oidcUrlParser = DefaultOidcUrlParser(
|
parseResult = loginIntentResolverResult,
|
||||||
oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(),
|
),
|
||||||
)
|
oidcIntentResolver = FakeOidcIntentResolver(
|
||||||
|
resolveResult = oidcIntentResolverResult,
|
||||||
),
|
),
|
||||||
permalinkParser = FakePermalinkParser(
|
permalinkParser = FakePermalinkParser(
|
||||||
result = permalinkParserResult
|
result = permalinkParserResult
|
||||||
|
|
|
||||||
|
|
@ -275,7 +275,7 @@ Follow these steps to install and configure the plugin and templates:
|
||||||
|
|
||||||
1. Install the AS plugin for generating modules :
|
1. Install the AS plugin for generating modules :
|
||||||
[Generate Module from Template](https://plugins.jetbrains.com/plugin/13586-generate-module-from-template)
|
[Generate Module from Template](https://plugins.jetbrains.com/plugin/13586-generate-module-from-template)
|
||||||
2. Run the script `tools/templates/generate_templates.sh` to generate the template zip file
|
2. From repository root, run `./tools/templates/generate_templates.sh` to generate the template zip file
|
||||||
3. Import file templates in AS :
|
3. Import file templates in AS :
|
||||||
- Navigate to File/Manage IDE Settings/Import Settings
|
- Navigate to File/Manage IDE Settings/Import Settings
|
||||||
- Pick the `tmp/file_templates.zip` files
|
- Pick the `tmp/file_templates.zip` files
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit d3dffc97bf8b39386ace2db7d857bbff05c73c18
|
Subproject commit 4a07c862a23a9fd1418eabf132cf9d6b25ea4927
|
||||||
2
fastlane/metadata/android/en-US/changelogs/202506000.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/202506000.txt
Normal 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
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?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="screen_analytics_settings_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
|
<string name="screen_analytics_settings_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
|
||||||
<string name="screen_analytics_settings_read_terms">"Weitere Informationen findest du %1$s."</string>
|
<string name="screen_analytics_settings_read_terms">"Sie können unsere Bedingungen %1$s lesen."</string>
|
||||||
<string name="screen_analytics_settings_read_terms_content_link">"hier"</string>
|
<string name="screen_analytics_settings_read_terms_content_link">"hier"</string>
|
||||||
<string name="screen_analytics_settings_share_data">"Analysedaten teilen"</string>
|
<string name="screen_analytics_settings_share_data">"Analysedaten teilen"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_settings_help_us_improve">"مسائل کی نشاندہی کرنے میں ہماری مدد کے لیے گمنام استعمال کے بیانات کا اشتراک کریں۔"</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms">"آپ ہماری تمام شرائط پڑھ سکتے ہیں %1$s۔"</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms_content_link">"یہاں"</string>
|
||||||
|
<string name="screen_analytics_settings_share_data">"تجزیاتی بیانات کا اشتراک کریں"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_analytics_prompt_data_usage">"Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile."</string>
|
<string name="screen_analytics_prompt_data_usage">"Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile."</string>
|
||||||
<string name="screen_analytics_prompt_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
|
<string name="screen_analytics_prompt_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms">"Weitere Informationen findest du %1$s."</string>
|
<string name="screen_analytics_prompt_read_terms">"Sie können unsere Bedingungen %1$s lesen."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
|
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
|
||||||
<string name="screen_analytics_prompt_settings">"Du kannst diese Funktion jederzeit deaktivieren"</string>
|
<string name="screen_analytics_prompt_settings">"Sie können dies jederzeit beenden"</string>
|
||||||
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben deine Daten nicht an Dritte weiter"</string>
|
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben deine Daten nicht an Dritte weiter"</string>
|
||||||
<string name="screen_analytics_prompt_title">"Hilf uns %1$s zu verbessern"</string>
|
<string name="screen_analytics_prompt_title">"Hilf uns %1$s zu verbessern"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_prompt_data_usage">"ہم کسی بھی ذاتی ڈیٹا کو ثبت یا پروفائل نہیں کریں گے"</string>
|
||||||
|
<string name="screen_analytics_prompt_help_us_improve">"مسائل کی نشاندہی کرنے میں ہماری مدد کے لیے گمنام استعمال کے بیانات کا اشتراک کریں۔"</string>
|
||||||
|
<string name="screen_analytics_prompt_read_terms">"آپ ہماری تمام شرائط پڑھ سکتے ہیں %1$s۔"</string>
|
||||||
|
<string name="screen_analytics_prompt_read_terms_content_link">"یہاں"</string>
|
||||||
|
<string name="screen_analytics_prompt_settings">"آپ اسے کسی بھی وقت بند کر سکتے ہیں"</string>
|
||||||
|
<string name="screen_analytics_prompt_third_party_sharing">"ہم آپکے بیانات کا فریق ثالث کے ساتھ اشتراک نہیں کریں گے"</string>
|
||||||
|
<string name="screen_analytics_prompt_title">"%1$s کو بہتر بنانے میں مدد کریں"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||||
import io.element.android.services.analytics.api.ScreenTracker
|
import io.element.android.services.analytics.api.ScreenTracker
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -62,6 +63,7 @@ class CallScreenPresenter @AssistedInject constructor(
|
||||||
private val activeCallManager: ActiveCallManager,
|
private val activeCallManager: ActiveCallManager,
|
||||||
private val languageTagProvider: LanguageTagProvider,
|
private val languageTagProvider: LanguageTagProvider,
|
||||||
private val appForegroundStateService: AppForegroundStateService,
|
private val appForegroundStateService: AppForegroundStateService,
|
||||||
|
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||||
private val appCoroutineScope: CoroutineScope,
|
private val appCoroutineScope: CoroutineScope,
|
||||||
) : Presenter<CallScreenState> {
|
) : Presenter<CallScreenState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -241,8 +243,10 @@ class CallScreenPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) {
|
private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) {
|
||||||
if (!notifiedCallStart) {
|
if (!notifiedCallStart) {
|
||||||
getJoinedRoom(roomId)?.use { it.sendCallNotificationIfNeeded() }
|
val activeRoomForSession = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
|
||||||
?.onSuccess { notifiedCallStart = true }
|
val sendCallNotificationResult = activeRoomForSession?.sendCallNotificationIfNeeded()
|
||||||
|
?: getJoinedRoom(roomId)?.use { it.sendCallNotificationIfNeeded() }
|
||||||
|
sendCallNotificationResult?.onSuccess { notifiedCallStart = true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@ class DefaultCallWidgetProvider @Inject constructor(
|
||||||
private val matrixClientsProvider: MatrixClientProvider,
|
private val matrixClientsProvider: MatrixClientProvider,
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
|
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
|
||||||
|
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||||
) : CallWidgetProvider {
|
) : CallWidgetProvider {
|
||||||
override suspend fun getWidget(
|
override suspend fun getWidget(
|
||||||
sessionId: SessionId,
|
sessionId: SessionId,
|
||||||
|
|
@ -33,7 +35,9 @@ class DefaultCallWidgetProvider @Inject constructor(
|
||||||
theme: String?,
|
theme: String?,
|
||||||
): Result<CallWidgetProvider.GetWidgetResult> = runCatching {
|
): Result<CallWidgetProvider.GetWidgetResult> = runCatching {
|
||||||
val matrixClient = matrixClientsProvider.getOrRestore(sessionId).getOrThrow()
|
val matrixClient = matrixClientsProvider.getOrRestore(sessionId).getOrThrow()
|
||||||
val room = matrixClient.getJoinedRoom(roomId) ?: error("Room not found")
|
val room = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
|
||||||
|
?: matrixClient.getJoinedRoom(roomId)
|
||||||
|
?: error("Room not found")
|
||||||
|
|
||||||
val customBaseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
|
val customBaseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
|
||||||
val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL
|
val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,5 @@
|
||||||
<string name="call_foreground_service_channel_title_android">"Chamada em andamento"</string>
|
<string name="call_foreground_service_channel_title_android">"Chamada em andamento"</string>
|
||||||
<string name="call_foreground_service_message_android">"Toque para retornar à chamada"</string>
|
<string name="call_foreground_service_message_android">"Toque para retornar à chamada"</string>
|
||||||
<string name="call_foreground_service_title_android">"☎️ Chamada em andamento"</string>
|
<string name="call_foreground_service_title_android">"☎️ Chamada em andamento"</string>
|
||||||
|
<string name="screen_incoming_call_subtitle_android">"Chamada do Element recebida"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="call_foreground_service_channel_title_android">"جاری مکالمہ"</string>
|
||||||
|
<string name="call_foreground_service_message_android">"مکالمہ پر واپس جانے کے لیے تھپتھپائیں"</string>
|
||||||
|
<string name="call_foreground_service_title_android">"☎️ مکالمہ جاری ہے"</string>
|
||||||
|
<string name="screen_incoming_call_subtitle_android">"ورودی ایلیمنٹ کال"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||||
import io.element.android.services.analytics.api.ScreenTracker
|
import io.element.android.services.analytics.api.ScreenTracker
|
||||||
import io.element.android.services.analytics.test.FakeScreenTracker
|
import io.element.android.services.analytics.test.FakeScreenTracker
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
|
@ -367,6 +368,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
|
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
|
||||||
screenTracker: ScreenTracker = FakeScreenTracker(),
|
screenTracker: ScreenTracker = FakeScreenTracker(),
|
||||||
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
|
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
|
||||||
|
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||||
): CallScreenPresenter {
|
): CallScreenPresenter {
|
||||||
val userAgentProvider = object : UserAgentProvider {
|
val userAgentProvider = object : UserAgentProvider {
|
||||||
override fun provide(): String {
|
override fun provide(): String {
|
||||||
|
|
@ -387,6 +389,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
languageTagProvider = FakeLanguageTagProvider("en-US"),
|
languageTagProvider = FakeLanguageTagProvider("en-US"),
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
appCoroutineScope = backgroundScope,
|
appCoroutineScope = backgroundScope,
|
||||||
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,7 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.advanceTimeBy
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
import kotlinx.coroutines.test.runCurrent
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
|
@ -63,30 +59,28 @@ class DefaultActiveCallManagerTest {
|
||||||
fun `registerIncomingCall - sets the incoming call as active`() = runTest {
|
fun `registerIncomingCall - sets the incoming call as active`() = runTest {
|
||||||
setupShadowPowerManager()
|
setupShadowPowerManager()
|
||||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
inCancellableScope {
|
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
|
||||||
|
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
||||||
assertThat(manager.activeCall.value).isNull()
|
assertThat(manager.activeCall.value).isNull()
|
||||||
|
|
||||||
val callNotificationData = aCallNotificationData()
|
val callNotificationData = aCallNotificationData()
|
||||||
manager.registerIncomingCall(callNotificationData)
|
manager.registerIncomingCall(callNotificationData)
|
||||||
|
|
||||||
assertThat(manager.activeCall.value).isEqualTo(
|
assertThat(manager.activeCall.value).isEqualTo(
|
||||||
ActiveCall(
|
ActiveCall(
|
||||||
callType = CallType.RoomCall(
|
callType = CallType.RoomCall(
|
||||||
sessionId = callNotificationData.sessionId,
|
sessionId = callNotificationData.sessionId,
|
||||||
roomId = callNotificationData.roomId,
|
roomId = callNotificationData.roomId,
|
||||||
),
|
),
|
||||||
callState = CallState.Ringing(callNotificationData)
|
callState = CallState.Ringing(callNotificationData)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
runCurrent()
|
runCurrent()
|
||||||
|
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||||
verify { notificationManagerCompat.notify(notificationId, any()) }
|
verify { notificationManagerCompat.notify(notificationId, any()) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
@ -94,42 +88,38 @@ class DefaultActiveCallManagerTest {
|
||||||
fun `registerIncomingCall - when there is an already active call adds missed call notification`() = runTest {
|
fun `registerIncomingCall - when there is an already active call adds missed call notification`() = runTest {
|
||||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||||
val onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
val onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
||||||
inCancellableScope {
|
val manager = createActiveCallManager(
|
||||||
val manager = createActiveCallManager(
|
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
||||||
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
// Register existing call
|
// Register existing call
|
||||||
val callNotificationData = aCallNotificationData()
|
val callNotificationData = aCallNotificationData()
|
||||||
manager.registerIncomingCall(callNotificationData)
|
manager.registerIncomingCall(callNotificationData)
|
||||||
val activeCall = manager.activeCall.value
|
val activeCall = manager.activeCall.value
|
||||||
|
|
||||||
// Now add a new call
|
// Now add a new call
|
||||||
manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2))
|
manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2))
|
||||||
|
|
||||||
assertThat(manager.activeCall.value).isEqualTo(activeCall)
|
assertThat(manager.activeCall.value).isEqualTo(activeCall)
|
||||||
assertThat((manager.activeCall.value?.callType as? CallType.RoomCall)?.roomId).isNotEqualTo(A_ROOM_ID_2)
|
assertThat((manager.activeCall.value?.callType as? CallType.RoomCall)?.roomId).isNotEqualTo(A_ROOM_ID_2)
|
||||||
|
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
|
|
||||||
addMissedCallNotificationLambda.assertions()
|
addMissedCallNotificationLambda.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(value(A_SESSION_ID), value(A_ROOM_ID_2), value(AN_EVENT_ID))
|
.with(value(A_SESSION_ID), value(A_ROOM_ID_2), value(AN_EVENT_ID))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `incomingCallTimedOut - when there isn't an active call does nothing`() = runTest {
|
fun `incomingCallTimedOut - when there isn't an active call does nothing`() = runTest {
|
||||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||||
inCancellableScope {
|
val manager = createActiveCallManager(
|
||||||
val manager = createActiveCallManager(
|
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
||||||
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
manager.incomingCallTimedOut(displayMissedCallNotification = true)
|
manager.incomingCallTimedOut(displayMissedCallNotification = true)
|
||||||
|
|
||||||
addMissedCallNotificationLambda.assertions().isNeverCalled()
|
addMissedCallNotificationLambda.assertions().isNeverCalled()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
@ -138,90 +128,80 @@ class DefaultActiveCallManagerTest {
|
||||||
setupShadowPowerManager()
|
setupShadowPowerManager()
|
||||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||||
inCancellableScope {
|
val manager = createActiveCallManager(
|
||||||
val manager = createActiveCallManager(
|
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda),
|
||||||
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda),
|
notificationManagerCompat = notificationManagerCompat,
|
||||||
notificationManagerCompat = notificationManagerCompat,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
manager.registerIncomingCall(aCallNotificationData())
|
manager.registerIncomingCall(aCallNotificationData())
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||||
|
|
||||||
manager.incomingCallTimedOut(displayMissedCallNotification = true)
|
manager.incomingCallTimedOut(displayMissedCallNotification = true)
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
|
|
||||||
assertThat(manager.activeCall.value).isNull()
|
assertThat(manager.activeCall.value).isNull()
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
||||||
addMissedCallNotificationLambda.assertions().isCalledOnce()
|
addMissedCallNotificationLambda.assertions().isCalledOnce()
|
||||||
verify { notificationManagerCompat.cancel(notificationId) }
|
verify { notificationManagerCompat.cancel(notificationId) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `hungUpCall - removes existing call if the CallType matches`() = runTest {
|
fun `hungUpCall - removes existing call if the CallType matches`() = runTest {
|
||||||
setupShadowPowerManager()
|
setupShadowPowerManager()
|
||||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
// Create a cancellable coroutine scope to cancel the test when needed
|
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||||
inCancellableScope {
|
|
||||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
|
||||||
|
|
||||||
val notificationData = aCallNotificationData()
|
val notificationData = aCallNotificationData()
|
||||||
manager.registerIncomingCall(notificationData)
|
manager.registerIncomingCall(notificationData)
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||||
|
|
||||||
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
||||||
assertThat(manager.activeCall.value).isNull()
|
assertThat(manager.activeCall.value).isNull()
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
||||||
|
|
||||||
verify { notificationManagerCompat.cancel(notificationId) }
|
verify { notificationManagerCompat.cancel(notificationId) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
|
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
|
||||||
setupShadowPowerManager()
|
setupShadowPowerManager()
|
||||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
// Create a cancellable coroutine scope to cancel the test when needed
|
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||||
inCancellableScope {
|
|
||||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
|
||||||
|
|
||||||
manager.registerIncomingCall(aCallNotificationData())
|
manager.registerIncomingCall(aCallNotificationData())
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||||
|
|
||||||
manager.hungUpCall(CallType.ExternalUrl("https://example.com"))
|
manager.hungUpCall(CallType.ExternalUrl("https://example.com"))
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||||
|
|
||||||
verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
|
verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun `joinedCall - register an ongoing call and tries sending the call notify event`() = runTest {
|
fun `joinedCall - register an ongoing call and tries sending the call notify event`() = runTest {
|
||||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
inCancellableScope {
|
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
assertThat(manager.activeCall.value).isNull()
|
||||||
assertThat(manager.activeCall.value).isNull()
|
|
||||||
|
|
||||||
manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID))
|
manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID))
|
||||||
assertThat(manager.activeCall.value).isEqualTo(
|
assertThat(manager.activeCall.value).isEqualTo(
|
||||||
ActiveCall(
|
ActiveCall(
|
||||||
callType = CallType.RoomCall(
|
callType = CallType.RoomCall(
|
||||||
sessionId = A_SESSION_ID,
|
sessionId = A_SESSION_ID,
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
),
|
),
|
||||||
callState = CallState.InCall,
|
callState = CallState.InCall,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
runCurrent()
|
runCurrent()
|
||||||
|
|
||||||
verify { notificationManagerCompat.cancel(notificationId) }
|
verify { notificationManagerCompat.cancel(notificationId) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
@ -233,22 +213,19 @@ class DefaultActiveCallManagerTest {
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(A_ROOM_ID, room)
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
}
|
}
|
||||||
// Create a cancellable coroutine scope to cancel the test when needed
|
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) })
|
||||||
inCancellableScope {
|
val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider)
|
||||||
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) })
|
|
||||||
val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider)
|
|
||||||
|
|
||||||
manager.registerIncomingCall(aCallNotificationData())
|
manager.registerIncomingCall(aCallNotificationData())
|
||||||
|
|
||||||
// Call is active (the other user join the call)
|
// Call is active (the other user join the call)
|
||||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
// Call is cancelled (the other user left the call)
|
// Call is cancelled (the other user left the call)
|
||||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
|
|
||||||
assertThat(manager.activeCall.value).isNull()
|
assertThat(manager.activeCall.value).isNull()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
@ -260,44 +237,34 @@ class DefaultActiveCallManagerTest {
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(A_ROOM_ID, room)
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
}
|
}
|
||||||
// Create a cancellable coroutine scope to cancel the test when needed
|
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.failure(IllegalStateException("Matrix client not found")) })
|
||||||
inCancellableScope {
|
val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider)
|
||||||
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.failure(IllegalStateException("Matrix client not found")) })
|
|
||||||
val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider)
|
|
||||||
|
|
||||||
// No matrix client
|
// No matrix client
|
||||||
|
|
||||||
manager.registerIncomingCall(aCallNotificationData())
|
manager.registerIncomingCall(aCallNotificationData())
|
||||||
|
|
||||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
|
|
||||||
// The call should still be active
|
// The call should still be active
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
|
|
||||||
// No room
|
// No room
|
||||||
client.givenGetRoomResult(A_ROOM_ID, null)
|
client.givenGetRoomResult(A_ROOM_ID, null)
|
||||||
matrixClientProvider.getClient = { Result.success(client) }
|
matrixClientProvider.getClient = { Result.success(client) }
|
||||||
|
|
||||||
manager.registerIncomingCall(aCallNotificationData())
|
manager.registerIncomingCall(aCallNotificationData())
|
||||||
|
|
||||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
|
|
||||||
// The call should still be active
|
// The call should still be active
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun TestScope.inCancellableScope(block: suspend CoroutineScope.() -> Unit) {
|
|
||||||
launch(SupervisorJob()) {
|
|
||||||
block()
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupShadowPowerManager() {
|
private fun setupShadowPowerManager() {
|
||||||
|
|
@ -306,14 +273,13 @@ class DefaultActiveCallManagerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.createActiveCallManager(
|
private fun TestScope.createActiveCallManager(
|
||||||
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
||||||
onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(),
|
onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(),
|
||||||
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true),
|
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true),
|
||||||
coroutineScope: CoroutineScope = this,
|
|
||||||
) = DefaultActiveCallManager(
|
) = DefaultActiveCallManager(
|
||||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = backgroundScope,
|
||||||
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
||||||
ringingCallNotificationCreator = RingingCallNotificationCreator(
|
ringingCallNotificationCreator = RingingCallNotificationCreator(
|
||||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,13 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
|
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
|
||||||
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||||
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -77,6 +79,29 @@ class DefaultCallWidgetProviderTest {
|
||||||
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").getOrNull()).isNotNull()
|
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").getOrNull()).isNotNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getWidget - reuses the active room if possible`() = runTest {
|
||||||
|
val client = FakeMatrixClient().apply {
|
||||||
|
// No room from the client
|
||||||
|
givenGetRoomResult(A_ROOM_ID, null)
|
||||||
|
}
|
||||||
|
val activeRoomsHolder = ActiveRoomsHolder().apply {
|
||||||
|
// A current active room with the same room id
|
||||||
|
addRoom(
|
||||||
|
FakeJoinedRoom(
|
||||||
|
baseRoom = FakeBaseRoom(roomId = A_ROOM_ID),
|
||||||
|
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
|
||||||
|
getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val provider = createProvider(
|
||||||
|
matrixClientProvider = FakeMatrixClientProvider { Result.success(client) },
|
||||||
|
activeRoomsHolder = activeRoomsHolder
|
||||||
|
)
|
||||||
|
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isSuccess).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getWidget - will use a custom base url if it exists`() = runTest {
|
fun `getWidget - will use a custom base url if it exists`() = runTest {
|
||||||
val room = FakeJoinedRoom(
|
val room = FakeJoinedRoom(
|
||||||
|
|
@ -104,9 +129,11 @@ class DefaultCallWidgetProviderTest {
|
||||||
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
||||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(),
|
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(),
|
||||||
|
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||||
) = DefaultCallWidgetProvider(
|
) = DefaultCallWidgetProvider(
|
||||||
matrixClientsProvider = matrixClientProvider,
|
matrixClientsProvider = matrixClientProvider,
|
||||||
appPreferencesStore = appPreferencesStore,
|
appPreferencesStore = appPreferencesStore,
|
||||||
callWidgetSettingsProvider = callWidgetSettingsProvider,
|
callWidgetSettingsProvider = callWidgetSettingsProvider,
|
||||||
|
activeRoomsHolder = activeRoomsHolder,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
<?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="screen_create_room_action_create_room">"Neuer Raum"</string>
|
<string name="screen_create_room_action_create_room">"Neuer Raum"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Personen einladen"</string>
|
<string name="screen_create_room_add_people_title">"Nutzer einladen"</string>
|
||||||
<string name="screen_create_room_error_creating_room">"Beim Erstellen des Chats ist ein Fehler aufgetreten"</string>
|
<string name="screen_create_room_error_creating_room">"Beim Erstellen des Chats ist ein Fehler aufgetreten"</string>
|
||||||
<string name="screen_create_room_private_option_description">"Nur eingeladene Personen haben Zutritt zu diesem Chatroom. Alle Nachrichten sind durchgehend verschlüsselt."</string>
|
<string name="screen_create_room_private_option_description">"Nur eingeladene Personen haben Zutritt zu diesem Chatroom. Alle Nachrichten sind Ende-zu-Ende verschlüsselt."</string>
|
||||||
<string name="screen_create_room_private_option_title">"Privater Chatroom"</string>
|
<string name="screen_create_room_private_option_title">"Privater Chatroom"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Jeder kann diesen Chatroom finden.
|
<string name="screen_create_room_public_option_description">"Alle können diesen Chatroom finden.
|
||||||
Sie können dies aber jederzeit in den Chatroomeinstellungen ändern."</string>
|
Sie können dies aber jederzeit in den Chatroomeinstellungen ändern."</string>
|
||||||
<string name="screen_create_room_public_option_title">"Öffentlicher Raum"</string>
|
<string name="screen_create_room_public_option_title">"Öffentlicher Raum"</string>
|
||||||
<string name="screen_create_room_room_access_section_anyone_option_description">"Jeder kann diesem Chatroom beitreten"</string>
|
<string name="screen_create_room_room_access_section_anyone_option_description">"Jeder darf diesen Raum betreten"</string>
|
||||||
<string name="screen_create_room_room_access_section_anyone_option_title">"Jemand"</string>
|
<string name="screen_create_room_room_access_section_anyone_option_title">"Jeder"</string>
|
||||||
<string name="screen_create_room_room_access_section_header">"Chatroom Zugang"</string>
|
<string name="screen_create_room_room_access_section_header">"Chatroomzugang"</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_description">"Jeder kann darum bitten, dem Chatroom beizutreten, aber ein Administrator oder ein Moderator muss die Anfrage akzeptieren."</string>
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Jeder kann den Zutritt zum Raum beantragen, aber ein Moderator muss die Anfrage akzeptieren."</string>
|
||||||
<string name="screen_create_room_room_access_section_knocking_option_title">"Beitritt beantragen"</string>
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Beitritt beantragen"</string>
|
||||||
<string name="screen_create_room_room_address_section_footer">"Damit dieser Chatroom im öffentlichen Chatroomverzeichnis sichtbar ist, benötigen Sie eine Chatroomadresse."</string>
|
<string name="screen_create_room_room_address_section_footer">"Damit dieser Chatroom im öffentlichen Chatroomverzeichnis sichtbar ist, benötigen Sie eine Chatroomadresse."</string>
|
||||||
<string name="screen_create_room_room_address_section_title">"Chatroomadresse"</string>
|
<string name="screen_create_room_room_address_section_title">"Chatroomadresse"</string>
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,24 @@
|
||||||
<string name="screen_create_room_private_option_title">"Sala privada"</string>
|
<string name="screen_create_room_private_option_title">"Sala privada"</string>
|
||||||
<string name="screen_create_room_public_option_description">"Qualquer um pode encontrar esta sala.
|
<string name="screen_create_room_public_option_description">"Qualquer um pode encontrar esta sala.
|
||||||
Você pode mudar isso a qualquer momento nas configurações da sala."</string>
|
Você pode mudar isso a qualquer momento nas configurações da sala."</string>
|
||||||
|
<string name="screen_create_room_public_option_title">"Sala pública"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_description">"Qualquer pessoa pode entrar nesta sala"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_anyone_option_title">"Qualquer pessoa"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_header">"Acesso à sala"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_description">"Qualquer pessoa pode pedir para entrar na sala, mas um administrador ou moderador terá de aceitar a solicitação"</string>
|
||||||
|
<string name="screen_create_room_room_access_section_knocking_option_title">"Pedir para entrar"</string>
|
||||||
|
<string name="screen_create_room_room_address_section_footer">"Para que esta sala fique visível no diretório público de salas, você precisará de um endereço de sala."</string>
|
||||||
|
<string name="screen_create_room_room_address_section_title">"Endereço da sala"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Nome da sala"</string>
|
<string name="screen_create_room_room_name_label">"Nome da sala"</string>
|
||||||
|
<string name="screen_create_room_room_visibility_section_title">"Visibilidade da sala"</string>
|
||||||
<string name="screen_create_room_title">"Criar uma sala"</string>
|
<string name="screen_create_room_title">"Criar uma sala"</string>
|
||||||
<string name="screen_create_room_topic_label">"Tópico (opcional)"</string>
|
<string name="screen_create_room_topic_label">"Tópico (opcional)"</string>
|
||||||
<string name="screen_room_directory_search_title">"Diretório de salas"</string>
|
<string name="screen_room_directory_search_title">"Diretório de salas"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"Ocorreu um erro ao tentar iniciar um chat"</string>
|
<string name="screen_start_chat_error_starting_chat">"Ocorreu um erro ao tentar iniciar um chat"</string>
|
||||||
|
<string name="screen_start_chat_join_room_by_address_action">"Entrar na sala pelo endereço"</string>
|
||||||
|
<string name="screen_start_chat_join_room_by_address_invalid_address">"Não é um endereço válido"</string>
|
||||||
|
<string name="screen_start_chat_join_room_by_address_placeholder">"Entrar…"</string>
|
||||||
|
<string name="screen_start_chat_join_room_by_address_room_found">"Foi encontrada uma sala correspondente"</string>
|
||||||
|
<string name="screen_start_chat_join_room_by_address_room_not_found">"Sala não encontrada"</string>
|
||||||
|
<string name="screen_start_chat_join_room_by_address_supporting_text">"Por exemplo, #nome-da-sala:matrix.org"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_create_room_action_create_room">"نیا کمرہ"</string>
|
||||||
|
<string name="screen_create_room_add_people_title">"لوگوں کو مدعو کریں"</string>
|
||||||
|
<string name="screen_create_room_error_creating_room">"کمرہ تخلیق کرتے ہوئے ایک نقص واقع ہوا"</string>
|
||||||
|
<string name="screen_create_room_private_option_description">"صرف مدعو لوگ ہی اس کمرے تک رسائی حاصل کر سکتے ہیں۔ تمام پیغامات آخر تا آخر مرموز کردہ ہیں۔"</string>
|
||||||
|
<string name="screen_create_room_private_option_title">"نجی کمرہ"</string>
|
||||||
|
<string name="screen_create_room_public_option_description">"کوئی بھی یہ کمرہ ڈھونڈ سکتا ہے۔
|
||||||
|
آپ اسے کمرے کی ترتیبات میں کسی بھی وقت تبدیل کرسکتے ہیں۔"</string>
|
||||||
|
<string name="screen_create_room_public_option_title">"عوامی کمرہ"</string>
|
||||||
|
<string name="screen_create_room_room_name_label">"کمرے کا نام"</string>
|
||||||
|
<string name="screen_create_room_title">"ایک کمرہ بنائیں"</string>
|
||||||
|
<string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string>
|
||||||
|
<string name="screen_room_directory_search_title">"کمرے کا راہنامچہ"</string>
|
||||||
|
<string name="screen_start_chat_error_starting_chat">"گفتگو شروع کرنے کی کوشش کرتے وقت ایک خرابی واقع ہوگئی"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -10,5 +10,5 @@
|
||||||
<string name="screen_deactivate_account_list_item_2">"Sie werden aus allen Chatrooms entfernt."</string>
|
<string name="screen_deactivate_account_list_item_2">"Sie werden aus allen Chatrooms entfernt."</string>
|
||||||
<string name="screen_deactivate_account_list_item_3">"Löschen Sie Ihre Kontoinformationen von unserem Identitätsserver."</string>
|
<string name="screen_deactivate_account_list_item_3">"Löschen Sie Ihre Kontoinformationen von unserem Identitätsserver."</string>
|
||||||
<string name="screen_deactivate_account_list_item_4">"Gelöschte Nachrichten werden für bereits registrierte Benutzer weiterhin sichtbar sein, wenn sie auch neuen oder nicht registrierten Benutzern nicht mehr zur Verfügung stehen."</string>
|
<string name="screen_deactivate_account_list_item_4">"Gelöschte Nachrichten werden für bereits registrierte Benutzer weiterhin sichtbar sein, wenn sie auch neuen oder nicht registrierten Benutzern nicht mehr zur Verfügung stehen."</string>
|
||||||
<string name="screen_deactivate_account_title">"Benutzerkonto deaktivieren"</string>
|
<string name="screen_deactivate_account_title">"Nutzerkonto deaktivieren"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
<?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="screen_deactivate_account_confirmation_dialog_content">"Confirme que você deseja desativar sua conta. Essa ação não pode ser desfeita."</string>
|
||||||
|
<string name="screen_deactivate_account_delete_all_messages">"Excluir todas as minhas mensagens"</string>
|
||||||
|
<string name="screen_deactivate_account_delete_all_messages_notice">"Aviso: Os futuros usuários poderão ver conversas incompletas."</string>
|
||||||
<string name="screen_deactivate_account_description">"Desativar sua conta é %1$s, isso irá:"</string>
|
<string name="screen_deactivate_account_description">"Desativar sua conta é %1$s, isso irá:"</string>
|
||||||
|
<string name="screen_deactivate_account_description_bold_part">"irreversível"</string>
|
||||||
|
<string name="screen_deactivate_account_list_item_1">"%1$s sua conta (você não poderá fazer login novamente, e seu ID não poderá ser reutilizado)."</string>
|
||||||
|
<string name="screen_deactivate_account_list_item_1_bold_part">"Desativar permanentemente"</string>
|
||||||
<string name="screen_deactivate_account_list_item_2">"Te remover de todas as salas de conversa."</string>
|
<string name="screen_deactivate_account_list_item_2">"Te remover de todas as salas de conversa."</string>
|
||||||
|
<string name="screen_deactivate_account_list_item_3">"Exclua as informações da sua conta do nosso servidor de identidade."</string>
|
||||||
|
<string name="screen_deactivate_account_list_item_4">"Suas mensagens ainda estarão visíveis para os usuários registrados, mas não estarão disponíveis para usuários novos ou não registrados se você optar por excluí-las."</string>
|
||||||
<string name="screen_deactivate_account_title">"Desativar conta"</string>
|
<string name="screen_deactivate_account_title">"Desativar conta"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,21 @@ interface EnterpriseService {
|
||||||
fun defaultHomeserverList(): List<String>
|
fun defaultHomeserverList(): List<String>
|
||||||
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
|
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
|
||||||
|
|
||||||
|
suspend fun isElementCallAvailable(): Boolean
|
||||||
|
|
||||||
fun semanticColorsLight(): SemanticColors
|
fun semanticColorsLight(): SemanticColors
|
||||||
fun semanticColorsDark(): SemanticColors
|
fun semanticColorsDark(): SemanticColors
|
||||||
|
|
||||||
fun firebasePushGateway(): String?
|
fun firebasePushGateway(): String?
|
||||||
fun unifiedPushDefaultPushGateway(): String?
|
fun unifiedPushDefaultPushGateway(): String?
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ANY_ACCOUNT_PROVIDER = "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun EnterpriseService.canConnectToAnyHomeserver(): Boolean {
|
||||||
|
return defaultHomeserverList().let {
|
||||||
|
it.isEmpty() || it.contains(EnterpriseService.ANY_ACCOUNT_PROVIDER)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ class DefaultEnterpriseService @Inject constructor() : EnterpriseService {
|
||||||
override fun defaultHomeserverList(): List<String> = emptyList()
|
override fun defaultHomeserverList(): List<String> = emptyList()
|
||||||
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
|
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
|
||||||
|
|
||||||
|
override suspend fun isElementCallAvailable(): Boolean = true
|
||||||
|
|
||||||
override fun semanticColorsLight(): SemanticColors = compoundColorsLight
|
override fun semanticColorsLight(): SemanticColors = compoundColorsLight
|
||||||
|
|
||||||
override fun semanticColorsDark(): SemanticColors = compoundColorsDark
|
override fun semanticColorsDark(): SemanticColors = compoundColorsDark
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ class FakeEnterpriseService(
|
||||||
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
|
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
|
||||||
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
|
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
|
||||||
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
|
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
|
||||||
|
private val isElementCallAvailableResult: () -> Boolean = { lambdaError() },
|
||||||
private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() },
|
private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() },
|
||||||
private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() },
|
private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() },
|
||||||
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
|
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
|
||||||
|
|
@ -35,6 +36,10 @@ class FakeEnterpriseService(
|
||||||
isAllowedToConnectToHomeserverResult(homeserverUrl)
|
isAllowedToConnectToHomeserverResult(homeserverUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun isElementCallAvailable(): Boolean = simulateLongTask {
|
||||||
|
isElementCallAvailableResult()
|
||||||
|
}
|
||||||
|
|
||||||
override fun semanticColorsLight(): SemanticColors {
|
override fun semanticColorsLight(): SemanticColors {
|
||||||
return semanticColorsLightResult()
|
return semanticColorsLightResult()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,6 @@ dependencies {
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.services.analytics.noop)
|
testImplementation(projects.services.analytics.noop)
|
||||||
testImplementation(projects.libraries.permissions.impl)
|
|
||||||
testImplementation(projects.libraries.permissions.test)
|
testImplementation(projects.libraries.permissions.test)
|
||||||
testImplementation(projects.libraries.preferences.test)
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.features.lockscreen.test)
|
testImplementation(projects.features.lockscreen.test)
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
<?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="screen_identity_confirmation_cannot_confirm">"Sie können es nicht bestätigen?"</string>
|
<string name="screen_identity_confirmation_cannot_confirm">"Bestätigung unmöglich?"</string>
|
||||||
<string name="screen_identity_confirmation_create_new_recovery_key">"Erstelle einen neuen Wiederherstellungsschlüssel"</string>
|
<string name="screen_identity_confirmation_create_new_recovery_key">"Erstelle einen neuen Wiederherstellungsschlüssel"</string>
|
||||||
<string name="screen_identity_confirmation_subtitle">"Verifiziere dieses Gerät, um sicheres Messaging einzurichten."</string>
|
<string name="screen_identity_confirmation_subtitle">"Verifiziere dieses Gerät, um sicheres Messaging einzurichten."</string>
|
||||||
<string name="screen_identity_confirmation_title">"Bestätige, dass du es bist"</string>
|
<string name="screen_identity_confirmation_title">"Bestätigen Sie Ihre Identität"</string>
|
||||||
<string name="screen_identity_confirmation_use_another_device">"Ein anderes Gerät verwenden"</string>
|
<string name="screen_identity_confirmation_use_another_device">"Ein anderes Gerät verwenden"</string>
|
||||||
<string name="screen_identity_confirmation_use_recovery_key">"Wiederherstellungsschlüssel verwenden"</string>
|
<string name="screen_identity_confirmation_use_recovery_key">"Wiederherstellungsschlüssel verwenden"</string>
|
||||||
<string name="screen_identity_confirmed_subtitle">"Du kannst nun verschlüsselte Nachrichten lesen oder versenden."</string>
|
<string name="screen_identity_confirmed_subtitle">"Sie können jetzt verschlüsselte Nachrichten lesen und versenden. Ihre Chatpartner vertrauen nun diesem Gerät auch."</string>
|
||||||
<string name="screen_identity_confirmed_title">"Gerät verifiziert"</string>
|
<string name="screen_identity_confirmed_title">"Gerät verifiziert"</string>
|
||||||
<string name="screen_identity_use_another_device">"Ein anderes Gerät verwenden"</string>
|
<string name="screen_identity_use_another_device">"Ein anderes Gerät verwenden"</string>
|
||||||
<string name="screen_identity_waiting_on_other_device">"Bitte warten bis das andere Gerät bereit ist."</string>
|
<string name="screen_identity_waiting_on_other_device">"Bitte warten bis das andere Gerät bereit ist."</string>
|
||||||
<string name="screen_notification_optin_subtitle">"Du kannst deine Einstellungen später ändern."</string>
|
<string name="screen_notification_optin_subtitle">"Sie können Ihre Einstellungen später ändern."</string>
|
||||||
<string name="screen_notification_optin_title">"Erlaube Benachrichtigungen und verpasse keine Nachricht"</string>
|
<string name="screen_notification_optin_title">"Erlaube Benachrichtigungen und verpasse keine Nachricht"</string>
|
||||||
<string name="screen_session_verification_enter_recovery_key">"Wiederherstellungsschlüssel eingeben"</string>
|
<string name="screen_session_verification_enter_recovery_key">"Wiederherstellungsschlüssel eingeben"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt."</string>
|
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
|
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, von dir zu hören. Teile uns deine Meinung über die Einstellungsseite mit."</string>
|
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, von dir zu hören. Teile uns deine Meinung über die Einstellungsseite mit."</string>
|
||||||
<string name="screen_welcome_button">"Los geht\'s!"</string>
|
<string name="screen_welcome_button">"Los geht\'s!"</string>
|
||||||
<string name="screen_welcome_subtitle">"Folgendes musst du wissen:"</string>
|
<string name="screen_welcome_subtitle">"Folgendes sollten Sie wissen:"</string>
|
||||||
<string name="screen_welcome_title">"Willkommen bei %1$s!"</string>
|
<string name="screen_welcome_title">"Willkommen bei %1$s!"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
<?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="screen_identity_confirmation_cannot_confirm">"Não consegue confirmar?"</string>
|
||||||
|
<string name="screen_identity_confirmation_create_new_recovery_key">"Criar uma nova chave de recuperação"</string>
|
||||||
|
<string name="screen_identity_confirmation_subtitle">"Verifique este dispositivo para configurar mensagens seguras."</string>
|
||||||
|
<string name="screen_identity_confirmation_title">"Confirme sua identidade"</string>
|
||||||
<string name="screen_identity_confirmation_use_another_device">"Usar outro dispositivo"</string>
|
<string name="screen_identity_confirmation_use_another_device">"Usar outro dispositivo"</string>
|
||||||
|
<string name="screen_identity_confirmation_use_recovery_key">"Use a chave de recuperação"</string>
|
||||||
|
<string name="screen_identity_confirmed_subtitle">"Agora você pode ler ou enviar mensagens com segurança, e qualquer pessoa com quem você conversa também pode confiar neste dispositivo."</string>
|
||||||
<string name="screen_identity_confirmed_title">"Dispositivo verificado"</string>
|
<string name="screen_identity_confirmed_title">"Dispositivo verificado"</string>
|
||||||
<string name="screen_identity_use_another_device">"Usar outro dispositivo"</string>
|
<string name="screen_identity_use_another_device">"Usar outro dispositivo"</string>
|
||||||
|
<string name="screen_identity_waiting_on_other_device">"Aguardando outro dispositivo…"</string>
|
||||||
<string name="screen_notification_optin_subtitle">"Você pode alterar suas configurações mais tarde."</string>
|
<string name="screen_notification_optin_subtitle">"Você pode alterar suas configurações mais tarde."</string>
|
||||||
<string name="screen_notification_optin_title">"Permita notificações e nunca perca uma mensagem"</string>
|
<string name="screen_notification_optin_title">"Permita notificações e nunca perca uma mensagem"</string>
|
||||||
<string name="screen_session_verification_enter_recovery_key">"Insira a chave de recuperação"</string>
|
<string name="screen_session_verification_enter_recovery_key">"Insira a chave de recuperação"</string>
|
||||||
|
|
|
||||||
22
features/ftue/impl/src/main/res/values-ur/translations.xml
Normal file
22
features/ftue/impl/src/main/res/values-ur/translations.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_identity_confirmation_cannot_confirm">"تصدیق نہیں کر سکتے؟"</string>
|
||||||
|
<string name="screen_identity_confirmation_create_new_recovery_key">"ایک نئی بازیابی کلید تخلیق کریں"</string>
|
||||||
|
<string name="screen_identity_confirmation_subtitle">"محفوظ پیغام رسانی ترتیب دینے کیلئے اس آلے کی توثیق کریں۔"</string>
|
||||||
|
<string name="screen_identity_confirmation_title">"اپنی شناخت کی تصدیق کریں"</string>
|
||||||
|
<string name="screen_identity_confirmation_use_another_device">"دوسرا آلہ استعمال کریں"</string>
|
||||||
|
<string name="screen_identity_confirmation_use_recovery_key">"بازیابی کلید استعمال کریں"</string>
|
||||||
|
<string name="screen_identity_confirmed_subtitle">"اب آپ محفوظ طریقے سے پیغامات پڑھ یا بھیج سکتے ہیں، اور جسکے ساتھ آپ گفتگو کرتے ہیں وہ بھی اس آلہ پر بھروسہ کر سکتا ہے۔"</string>
|
||||||
|
<string name="screen_identity_confirmed_title">"آلہ توثیق شدہ"</string>
|
||||||
|
<string name="screen_identity_use_another_device">"دوسرا آلہ استعمال کریں"</string>
|
||||||
|
<string name="screen_identity_waiting_on_other_device">"دوسرے آلہ پر منتظر…"</string>
|
||||||
|
<string name="screen_notification_optin_subtitle">"آپ بعد میں اپنی ترتیبات تبدیل کر سکتے ہیں۔"</string>
|
||||||
|
<string name="screen_notification_optin_title">"اطلاعات کی اجازت دیں اور کبھی بھی کسی پیغام سے محروم نہ ہوں۔"</string>
|
||||||
|
<string name="screen_session_verification_enter_recovery_key">"بازیابی کلید درج کریں"</string>
|
||||||
|
<string name="screen_welcome_bullet_1">"مکالمات، رائے شماری، تلاش اور مزید بعد میں اس سال شامل کیا جائے گا۔"</string>
|
||||||
|
<string name="screen_welcome_bullet_2">"مرموز کردہ کمروں کیلئے سرگزشتِ پیغام ابھی دستیاب نہیں ہے"</string>
|
||||||
|
<string name="screen_welcome_bullet_3">"ہم آپ سے سننا پسند کریں گے، ترتیبات کے صفحہ کے ذریعے ہمیں بتائیں کہ آپ کیا سوچتے ہیں۔"</string>
|
||||||
|
<string name="screen_welcome_button">"چلیں!"</string>
|
||||||
|
<string name="screen_welcome_subtitle">"یہ ہے جو آپ کو جاننے کی ضرورت ہے:"</string>
|
||||||
|
<string name="screen_welcome_title">"%1$s میں خوش آمدید!"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -19,7 +19,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationS
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
||||||
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
import io.element.android.libraries.permissions.test.FakePermissionStateProvider
|
||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
||||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
import io.element.android.libraries.permissions.test.FakePermissionStateProvider
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
<?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="screen_decline_and_block_block_user_option_description">"Sie werden keine Nachrichten oder Chatroomeinladungen von diesem Benutzer sehen."</string>
|
<string name="screen_decline_and_block_block_user_option_description">"Sie werden keine Nachrichten oder Chateinladungen von diesem Nutzer sehen."</string>
|
||||||
<string name="screen_decline_and_block_block_user_option_title">"Benutzer blockieren"</string>
|
<string name="screen_decline_and_block_block_user_option_title">"Benutzer blockieren"</string>
|
||||||
<string name="screen_decline_and_block_report_user_option_description">"Melden Sie diesen Raum Ihrem Kontoanbieter."</string>
|
<string name="screen_decline_and_block_report_user_option_description">"Melden Sie diesen Raum Ihrem Kontoanbieter."</string>
|
||||||
<string name="screen_decline_and_block_report_user_reason_placeholder">"Beschreiben Sie den Grund für die Meldung…"</string>
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Nennen Sie den Grund für die Meldung…"</string>
|
||||||
<string name="screen_decline_and_block_title">"Ablehnen und blockieren"</string>
|
<string name="screen_decline_and_block_title">"Ablehnen und blockieren"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?"</string>
|
<string name="screen_invites_decline_chat_message">"Möchten Sie die Einladung zum Betreten von %1$s wirklich ablehnen?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Einladung ablehnen"</string>
|
<string name="screen_invites_decline_chat_title">"Einladung ablehnen"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Bist du sicher, dass du diese Direktnachricht von %1$s ablehnen möchtest?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Möchten Sie diesen privaten Chat mit %1$s wirklich ablehnen?"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_title">"Einladung ablehnen"</string>
|
<string name="screen_invites_decline_direct_chat_title">"Einladung ablehnen"</string>
|
||||||
<string name="screen_invites_empty_list">"Keine Einladungen"</string>
|
<string name="screen_invites_empty_list">"Keine Einladungen"</string>
|
||||||
<string name="screen_invites_invited_you">"%1$s (%2$s) hat dich eingeladen"</string>
|
<string name="screen_invites_invited_you">"%1$s (%2$s) hat dich eingeladen"</string>
|
||||||
<string name="screen_join_room_decline_and_block_alert_confirmation">"Ja, ablehnen und blockieren"</string>
|
<string name="screen_join_room_decline_and_block_alert_confirmation">"Ja, ablehnen & blockieren"</string>
|
||||||
<string name="screen_join_room_decline_and_block_alert_message">"Sind Sie sicher, dass Sie die Einladung zu diesem Raum ablehnen möchten? Dadurch wird auch verhindert, dass %1$s Sie kontaktiert oder in Räume einlädt."</string>
|
<string name="screen_join_room_decline_and_block_alert_message">"Sind Sie sicher, dass Sie die Einladung zu diesem Raum ablehnen möchten? Dadurch wird auch verhindert, dass %1$s Sie kontaktiert oder in Räume einlädt."</string>
|
||||||
<string name="screen_join_room_decline_and_block_alert_title">"Einladung ablehnen und blockieren"</string>
|
<string name="screen_join_room_decline_and_block_alert_title">"Einladung ablehnen & Nutzer blockieren"</string>
|
||||||
<string name="screen_join_room_decline_and_block_button_title">"Ablehnen und blockieren"</string>
|
<string name="screen_join_room_decline_and_block_button_title">"Ablehnen und blockieren"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,18 @@
|
||||||
<?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="screen_decline_and_block_block_user_option_description">"Você não verá nenhuma mensagem ou convite de sala deste usuário"</string>
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"Bloquear usuário"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_option_description">"Denuncie esta sala ao fornecedor da sua conta."</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Descreva o motivo da denúncia…"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"Recusar e bloquear"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Tem certeza de que deseja recusar o convite para ingressar em %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Tem certeza de que deseja recusar o convite para ingressar em %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Recusar convite"</string>
|
<string name="screen_invites_decline_chat_title">"Recusar convite"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Tem certeza de que deseja recusar esse chat privado com %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Tem certeza de que deseja recusar esse chat privado com %1$s?"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_title">"Recusar chat"</string>
|
<string name="screen_invites_decline_direct_chat_title">"Recusar chat"</string>
|
||||||
<string name="screen_invites_empty_list">"Sem convites"</string>
|
<string name="screen_invites_empty_list">"Sem convites"</string>
|
||||||
<string name="screen_invites_invited_you">"%1$s(%2$s) convidou você"</string>
|
<string name="screen_invites_invited_you">"%1$s(%2$s) convidou você"</string>
|
||||||
|
<string name="screen_join_room_decline_and_block_alert_confirmation">"Sim, recusar e bloquear"</string>
|
||||||
|
<string name="screen_join_room_decline_and_block_alert_message">"Você tem certeza de que deseja recusar o convite para participar desta sala? Isso também impedirá que %1$s entre em contato com você ou o convide para as salas."</string>
|
||||||
|
<string name="screen_join_room_decline_and_block_alert_title">"Recusar convite e bloquear"</string>
|
||||||
|
<string name="screen_join_room_decline_and_block_button_title">"Recusar e bloquear"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<?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="screen_decline_and_block_block_user_option_description">"Não verás quaisquer mensagens ou convites deste utilizador"</string>
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"Bloquear utilizador"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_option_description">"Denunciar esta sala ao teu operador de conta."</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Descreve a razão de denúncia…"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"Rejeitar e bloquear"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Tens a certeza que queres rejeitar o convite para entra em %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Tens a certeza que queres rejeitar o convite para entra em %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Rejeitar convite"</string>
|
<string name="screen_invites_decline_chat_title">"Rejeitar convite"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Tem a certeza que queres rejeitar esta conversa privada com %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Tem a certeza que queres rejeitar esta conversa privada com %1$s?"</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_invites_decline_chat_message">"کیا آپکو یقین ہے کہ آپ %1$s میں شامل ہونے کی درخواست مسترد کرنا چاہتے ہیں؟"</string>
|
||||||
|
<string name="screen_invites_decline_chat_title">"دعوت مسترد کریں"</string>
|
||||||
|
<string name="screen_invites_decline_direct_chat_message">"کیا آپکو یقین ہے کہ آپ %1$s کیساتھ نجی گفتگو مسترد کرنا چاہتے ہیں؟"</string>
|
||||||
|
<string name="screen_invites_decline_direct_chat_title">"گفتگو مسترد کریں"</string>
|
||||||
|
<string name="screen_invites_empty_list">"کوئی دعوت نامے نہیں"</string>
|
||||||
|
<string name="screen_invites_invited_you">"%1$s (%2$s) نے آپ کو مدعو کیا"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<string name="screen_decline_and_block_block_user_option_description">"您將不會看到來自此使用者的任何訊息或聊天室邀請"</string>
|
<string name="screen_decline_and_block_block_user_option_description">"您將不會看到來自此使用者的任何訊息或聊天室邀請"</string>
|
||||||
<string name="screen_decline_and_block_block_user_option_title">"封鎖使用者"</string>
|
<string name="screen_decline_and_block_block_user_option_title">"封鎖使用者"</string>
|
||||||
<string name="screen_decline_and_block_report_user_option_description">"向您的帳號提供者回報此聊天室。"</string>
|
<string name="screen_decline_and_block_report_user_option_description">"向您的帳號提供者回報此聊天室。"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"說明回報的原因……"</string>
|
||||||
<string name="screen_decline_and_block_title">"拒絕並封鎖"</string>
|
<string name="screen_decline_and_block_title">"拒絕並封鎖"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"您確定您想要拒絕加入 %1$s 的邀請嗎?"</string>
|
<string name="screen_invites_decline_chat_message">"您確定您想要拒絕加入 %1$s 的邀請嗎?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"拒絕邀請"</string>
|
<string name="screen_invites_decline_chat_title">"拒絕邀請"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,32 @@
|
||||||
<?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="screen_join_room_ban_by_message">"Sie wurden aus diesem Chatroom ausgeschlossen %1$s."</string>
|
<string name="screen_join_room_ban_by_message">"Sie wurden von %1$s für diesen Chatroom gesperrt."</string>
|
||||||
<string name="screen_join_room_ban_message">"Sie wurden aus diesem Chatroom verbannt"</string>
|
<string name="screen_join_room_ban_message">"Sie wurden für diesen Chatroom gesperrt"</string>
|
||||||
<string name="screen_join_room_ban_reason">"Grund:%1$s."</string>
|
<string name="screen_join_room_ban_reason">"Grund:%1$s."</string>
|
||||||
<string name="screen_join_room_cancel_knock_action">"Anfrage abbrechen"</string>
|
<string name="screen_join_room_cancel_knock_action">"Anfrage abbrechen"</string>
|
||||||
<string name="screen_join_room_cancel_knock_alert_confirmation">"Ja, abbrechen"</string>
|
<string name="screen_join_room_cancel_knock_alert_confirmation">"Ja, abbrechen"</string>
|
||||||
<string name="screen_join_room_cancel_knock_alert_description">"Möchten Sie Ihre Beitrittsanfrage für diesen Chatroom wirklich stornieren?"</string>
|
<string name="screen_join_room_cancel_knock_alert_description">"Möchten Sie Ihre Beitrittsanfrage für diesen Chatroom wirklich stornieren?"</string>
|
||||||
<string name="screen_join_room_cancel_knock_alert_title">"Beitrittsanfrage stornieren"</string>
|
<string name="screen_join_room_cancel_knock_alert_title">"Beitrittsanfrage stornieren"</string>
|
||||||
<string name="screen_join_room_decline_and_block_alert_confirmation">"Ja, ablehnen und blockieren"</string>
|
<string name="screen_join_room_decline_and_block_alert_confirmation">"Ja, ablehnen & blockieren"</string>
|
||||||
<string name="screen_join_room_decline_and_block_alert_message">"Sind Sie sicher, dass Sie die Einladung zu diesem Raum ablehnen möchten? Dadurch wird auch verhindert, dass %1$s Sie kontaktiert oder in Räume einlädt."</string>
|
<string name="screen_join_room_decline_and_block_alert_message">"Sind Sie sicher, dass Sie die Einladung zu diesem Raum ablehnen möchten? Dadurch wird auch verhindert, dass %1$s Sie kontaktiert oder in Räume einlädt."</string>
|
||||||
<string name="screen_join_room_decline_and_block_alert_title">"Einladung ablehnen und blockieren"</string>
|
<string name="screen_join_room_decline_and_block_alert_title">"Einladung ablehnen & Nutzer blockieren"</string>
|
||||||
<string name="screen_join_room_decline_and_block_button_title">"Ablehnen und blockieren"</string>
|
<string name="screen_join_room_decline_and_block_button_title">"Ablehnen und blockieren"</string>
|
||||||
<string name="screen_join_room_fail_message">"Der Beitritt zum Chatroom schlug fehl."</string>
|
<string name="screen_join_room_fail_message">"Der Beitritt zum Chatroom schlug fehl."</string>
|
||||||
<string name="screen_join_room_fail_reason">"Dieser Chatroom ist entweder nur auf Einladung zugänglich oder es kann zu Zugangsbeschränkungen auf Spaceebene kommen."</string>
|
<string name="screen_join_room_fail_reason">"Dieser Chatroom ist entweder nur auf Einladung zugänglich oder es gibt andere Zugangsbeschränkungen durch Spaces."</string>
|
||||||
<string name="screen_join_room_forget_action">"Vergessen Sie diesen Raum"</string>
|
<string name="screen_join_room_forget_action">"Vergessen Sie diesen Raum"</string>
|
||||||
<string name="screen_join_room_invite_required_message">"Sie benötigen eine Einladung, um diesem Raum beizutreten"</string>
|
<string name="screen_join_room_invite_required_message">"Sie benötigen eine Einladung, um diesem Chatroom zu betreten"</string>
|
||||||
<string name="screen_join_room_join_action">"Raum beitreten"</string>
|
<string name="screen_join_room_join_action">"Raum beitreten"</string>
|
||||||
<string name="screen_join_room_join_restricted_message">"Möglicherweise müssen Sie eingeladen sein oder Mitglied eines Spaces sein, um beitreten zu können."</string>
|
<string name="screen_join_room_join_restricted_message">"Möglicherweise müssen Sie eingeladen sein oder Mitglied eines Spaces sein, um beitreten zu können."</string>
|
||||||
<string name="screen_join_room_knock_action">"Anklopfen"</string>
|
<string name="screen_join_room_knock_action">"Anklopfen"</string>
|
||||||
<string name="screen_join_room_knock_message_description">"Nachricht (optional)"</string>
|
<string name="screen_join_room_knock_message_description">"Nachricht (optional)"</string>
|
||||||
<string name="screen_join_room_knock_sent_description">"Falls Ihre Anfrage, dem Raum beizutreten, akzeptiert wird, werden Sie eine Einladung erhalten."</string>
|
<string name="screen_join_room_knock_sent_description">"Falls Ihre Anfrage, den Raum zu betreten, akzeptiert wird, erhalten Sie eine Einladung."</string>
|
||||||
<string name="screen_join_room_knock_sent_title">"Beitrittsanfrage geschickt"</string>
|
<string name="screen_join_room_knock_sent_title">"Beitrittsanfrage geschickt"</string>
|
||||||
<string name="screen_join_room_loading_alert_message">"Wir konnten die Chatroomvorschau nicht anzeigen. Dies kann an Netzwerk- oder Serverproblemen liegen."</string>
|
<string name="screen_join_room_loading_alert_message">"Wir konnten die Chatroomvorschau nicht anzeigen. Dies kann an Netzwerk- oder Serverproblemen liegen."</string>
|
||||||
<string name="screen_join_room_loading_alert_title">"Wir konnten diese Chatroomvorschau nicht anzeigen"</string>
|
<string name="screen_join_room_loading_alert_title">"Wir konnten diese Chatroomvorschau nicht anzeigen"</string>
|
||||||
<string name="screen_join_room_space_not_supported_description">"%1$s unterstützt noch keine Spaces. Du kannst auf Spaces im Web zugreifen."</string>
|
<string name="screen_join_room_space_not_supported_description">"%1$s unterstützt noch keine Spaces. Sie können auf Spaces im Web zugreifen."</string>
|
||||||
<string name="screen_join_room_space_not_supported_title">"Spaces werden noch nicht unterstützt"</string>
|
<string name="screen_join_room_space_not_supported_title">"Spaces werden noch nicht unterstützt"</string>
|
||||||
<string name="screen_join_room_subtitle_knock">"Klopfe an um einen Administrator zu benachrichtigen. Nach der Freigabe kannst du dich an der Unterhaltung beteiligen."</string>
|
<string name="screen_join_room_subtitle_knock">"Klicken Sie auf die Schaltfläche unten und ein Chatroomadministrator wird benachrichtigt. Nach der Freigabe durch einen Chatroomadministrator können Sie sich an der Unterhaltung beteiligen."</string>
|
||||||
<string name="screen_join_room_subtitle_no_preview">"Du musst Mitglied in diesem Raum sein, um den Nachrichtenverlauf zu sehen."</string>
|
<string name="screen_join_room_subtitle_no_preview">"Sie müssen Mitglied in diesem Chatroom sein, um den Nachrichtenverlauf einsehen zu können."</string>
|
||||||
<string name="screen_join_room_title_knock">"Willst du diesem Raum beitreten?"</string>
|
<string name="screen_join_room_title_knock">"Möchten Sie diesem Chatroom betreten?"</string>
|
||||||
<string name="screen_join_room_title_no_preview">"Vorschau nicht verfügbar"</string>
|
<string name="screen_join_room_title_no_preview">"Vorschau nicht verfügbar"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<string name="screen_join_room_ban_reason">"Arrazoia: %1$s."</string>
|
<string name="screen_join_room_ban_reason">"Arrazoia: %1$s."</string>
|
||||||
<string name="screen_join_room_cancel_knock_action">"Utzi eskaera bertan behera"</string>
|
<string name="screen_join_room_cancel_knock_action">"Utzi eskaera bertan behera"</string>
|
||||||
<string name="screen_join_room_cancel_knock_alert_confirmation">"Bai, utzi bertan behera"</string>
|
<string name="screen_join_room_cancel_knock_alert_confirmation">"Bai, utzi bertan behera"</string>
|
||||||
|
<string name="screen_join_room_fail_message">"Gelara sartzeak huts egin du."</string>
|
||||||
<string name="screen_join_room_forget_action">"Ahaztu gela hau"</string>
|
<string name="screen_join_room_forget_action">"Ahaztu gela hau"</string>
|
||||||
<string name="screen_join_room_join_action">"Sartu gelan"</string>
|
<string name="screen_join_room_join_action">"Sartu gelan"</string>
|
||||||
<string name="screen_join_room_knock_action">"Bidali batzeko eskaera"</string>
|
<string name="screen_join_room_knock_action">"Bidali batzeko eskaera"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,32 @@
|
||||||
<?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="screen_join_room_ban_by_message">"Você foi banido desta sala por %1$s."</string>
|
||||||
|
<string name="screen_join_room_ban_message">"Você foi banido desta sala"</string>
|
||||||
|
<string name="screen_join_room_ban_reason">"Motivo: %1$s."</string>
|
||||||
|
<string name="screen_join_room_cancel_knock_action">"Cancelar pedido"</string>
|
||||||
|
<string name="screen_join_room_cancel_knock_alert_confirmation">"Sim, cancele"</string>
|
||||||
|
<string name="screen_join_room_cancel_knock_alert_description">"Tem a certeza de que pretende cancelar o seu pedido de adesão a esta sala?"</string>
|
||||||
|
<string name="screen_join_room_cancel_knock_alert_title">"Cancelar pedido de adesão"</string>
|
||||||
|
<string name="screen_join_room_decline_and_block_alert_confirmation">"Sim, recusar e bloquear"</string>
|
||||||
|
<string name="screen_join_room_decline_and_block_alert_message">"Você tem certeza de que deseja recusar o convite para participar desta sala? Isso também impedirá que %1$s entre em contato com você ou o convide para as salas."</string>
|
||||||
|
<string name="screen_join_room_decline_and_block_alert_title">"Recusar convite e bloquear"</string>
|
||||||
|
<string name="screen_join_room_decline_and_block_button_title">"Recusar e bloquear"</string>
|
||||||
|
<string name="screen_join_room_fail_message">"A entrada na sala falhou."</string>
|
||||||
|
<string name="screen_join_room_fail_reason">"Esta sala é apenas para convidados ou pode haver restrições de acesso no nível do espaço."</string>
|
||||||
|
<string name="screen_join_room_forget_action">"Esquecer esta sala"</string>
|
||||||
|
<string name="screen_join_room_invite_required_message">"Você precisa de um convite para entrar nesta sala"</string>
|
||||||
|
<string name="screen_join_room_join_action">"Entrar na sala"</string>
|
||||||
|
<string name="screen_join_room_join_restricted_message">"Talvez você precise ser convidado ou ser membro de um espaço para participar."</string>
|
||||||
|
<string name="screen_join_room_knock_action">"Enviar solicitação para participar"</string>
|
||||||
|
<string name="screen_join_room_knock_message_description">"Mensagem (opcional)"</string>
|
||||||
|
<string name="screen_join_room_knock_sent_description">"Você receberá um convite para participar da sala se seu pedido for aceito."</string>
|
||||||
|
<string name="screen_join_room_knock_sent_title">"Pedido de adesão enviado"</string>
|
||||||
|
<string name="screen_join_room_loading_alert_message">"Não foi possível exibir a visualização da sala. Isso pode ser devido a problemas de rede ou de servidor."</string>
|
||||||
|
<string name="screen_join_room_loading_alert_title">"Não foi possível exibir a visualização desta sala"</string>
|
||||||
<string name="screen_join_room_space_not_supported_description">"%1$s não suporta espaços ainda. Você pode acessar os espaços na web."</string>
|
<string name="screen_join_room_space_not_supported_description">"%1$s não suporta espaços ainda. Você pode acessar os espaços na web."</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_title">"Os espaços ainda não são compatíveis"</string>
|
||||||
|
<string name="screen_join_room_subtitle_knock">"Clique no botão abaixo e um administrador da sala será notificado. Você poderá participar da conversa assim que for aprovado."</string>
|
||||||
|
<string name="screen_join_room_subtitle_no_preview">"Você deve ser um membro desta sala para visualizar o histórico de mensagens."</string>
|
||||||
|
<string name="screen_join_room_title_knock">"Quer entrar nesta sala?"</string>
|
||||||
|
<string name="screen_join_room_title_no_preview">"A pré-visualização não está disponível"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_join_room_join_action">"کمرے میں شامل ہوں"</string>
|
||||||
|
<string name="screen_join_room_knock_action">"شامل ہونے کی درخواست بھیجیں"</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_description">"%1$s ابھی تک خالی جگہوں کی حمایت نہیں کرتا۔ آپ جال پر خالی جگہوں تک رسائی حاصل کرسکتے ہیں۔"</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_title">"ابھی تک جگہیں تعاون یافتہ نہیں"</string>
|
||||||
|
<string name="screen_join_room_subtitle_knock">"نیچے دیئے گئے کلید پر دبائیں اور کمرے کے منتظم کو مطلع کیا جائے گا۔ منظور ہونے کے بعد آپ گفتگو میں شامل ہو سکیں گے۔"</string>
|
||||||
|
<string name="screen_join_room_subtitle_no_preview">"پیغام کی سرگزشت دیکھنے کے لیے آپ کا اس کمرے کا رکن ہونا ضروری ہے۔"</string>
|
||||||
|
<string name="screen_join_room_title_knock">"اس کمرے میں شامل ہونا چاہتے ہیں؟"</string>
|
||||||
|
<string name="screen_join_room_title_no_preview">"پیش منظر دستیاب نہیں ہے"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -25,6 +25,14 @@
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
<string name="screen_knock_requests_list_accept_failed_alert_title">"Die Anfrage konnte nicht akzeptiert werden"</string>
|
<string name="screen_knock_requests_list_accept_failed_alert_title">"Die Anfrage konnte nicht akzeptiert werden"</string>
|
||||||
<string name="screen_knock_requests_list_accept_loading_title">"Beitrittsanfrage annehmen"</string>
|
<string name="screen_knock_requests_list_accept_loading_title">"Beitrittsanfrage annehmen"</string>
|
||||||
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Ja, ablehnen und sperren"</string>
|
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Ja, ablehnen und sperren"</string>
|
||||||
<string name="screen_knock_requests_list_ban_alert_description">"Sind Sie sicher, dass Sie %1$s ablehnen und sperren möchten ? Dieser Benutzer kann keinen erneuten Zugriff auf diesen Raum anfordern."</string>
|
<string name="screen_knock_requests_list_ban_alert_description">"Sind Sie sicher, dass Sie %1$s ablehnen und sperren möchten?Dieser Benutzer kann keine erneute Zulassung auf diesen Chatroom anfordern."</string>
|
||||||
<string name="screen_knock_requests_list_ban_alert_title">"Ablehnen und Zugriff verbieten"</string>
|
<string name="screen_knock_requests_list_ban_alert_title">"Ablehnen und Zugriff verbieten"</string>
|
||||||
<string name="screen_knock_requests_list_ban_loading_title">"Ablehnung und Sperrung des Zugriffs"</string>
|
<string name="screen_knock_requests_list_ban_loading_title">"Ablehnung und Sperrung des Zugriffs"</string>
|
||||||
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Ja, ablehnen"</string>
|
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Ja, ablehnen"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,36 @@
|
||||||
<?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="screen_knock_requests_list_accept_all_alert_confirm_button_title">"Sim, aceitar todos"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_alert_description">"Tem certeza de que deseja aceitar todos os pedidos de adesão?"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_alert_title">"Aceitar todos os pedidos"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_button_title">"Aceitar todos"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_failed_alert_description">"Não pudemos aceitar todas as solicitações. Você gostaria de tentar novamente?"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"Falha ao aceitar todas as solicitações"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_all_loading_title">"Aceitando todas as solicitações de adesão"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_failed_alert_description">"Não pudemos aceitar essa solicitação. Você gostaria de tentar novamente?"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_failed_alert_title">"Falha ao aceitar a solicitação"</string>
|
||||||
|
<string name="screen_knock_requests_list_accept_loading_title">"Aceitando solicitação de adesão"</string>
|
||||||
|
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Sim, recusar e banir"</string>
|
||||||
|
<string name="screen_knock_requests_list_ban_alert_description">"Você tem certeza de que deseja recusar e banir %1$s? Este usuário não poderá solicitar acesso para entrar nesta sala novamente."</string>
|
||||||
|
<string name="screen_knock_requests_list_ban_alert_title">"Recusar e proibir o acesso"</string>
|
||||||
|
<string name="screen_knock_requests_list_ban_loading_title">"Recusando e proibindo o acesso"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Sim, recusar"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_alert_description">"Você tem certeza de que deseja recusar a solicitação de %1$s para participar desta sala?"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_alert_title">"Recusar acesso"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Recusar e banir"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_failed_alert_description">"Não foi possível recusar esta solicitação. Você gostaria de tentar novamente?"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_failed_alert_title">"Falha ao recusar a solicitação"</string>
|
||||||
|
<string name="screen_knock_requests_list_decline_loading_title">"Recusando a solicitação de adesão"</string>
|
||||||
|
<string name="screen_knock_requests_list_empty_state_description">"Quando alguém pedir para entrar na sala, você poderá ver o pedido aqui."</string>
|
||||||
|
<string name="screen_knock_requests_list_empty_state_title">"Nenhum pedido pendente de adesão"</string>
|
||||||
|
<string name="screen_knock_requests_list_initial_loading_title">"Carregando solicitações para participar…"</string>
|
||||||
|
<string name="screen_knock_requests_list_title">"Solicitações para entrar"</string>
|
||||||
|
<plurals name="screen_room_multiple_knock_requests_title">
|
||||||
|
<item quantity="one">"%1$s +%2$d outro desejam entrar desta sala"</item>
|
||||||
|
<item quantity="other">"%1$s +%2$d outros desejam entrar desta sala"</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="screen_room_multiple_knock_requests_view_all_button_title">"Ver tudo"</string>
|
||||||
<string name="screen_room_single_knock_request_accept_button_title">"Aceitar"</string>
|
<string name="screen_room_single_knock_request_accept_button_title">"Aceitar"</string>
|
||||||
|
<string name="screen_room_single_knock_request_title">"%1$s quer entrar nesta sala"</string>
|
||||||
|
<string name="screen_room_single_knock_request_view_button_title">"Ver"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_room_single_knock_request_accept_button_title">"قبول کریں"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?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="leave_conversation_alert_subtitle">"Bist du sicher, dass du diese Unterhaltung verlassen willst? Diese Unterhaltung ist nicht öffentlich und du kannst ihr ohne Einladung nicht wieder beitreten."</string>
|
<string name="leave_conversation_alert_subtitle">"Sind Sie sicher, dass Sie diesen Chat verlassen wollen? Dieser Chat ist nicht öffentlich und Sie können ihn ohne Einladung nicht wieder betreten."</string>
|
||||||
<string name="leave_room_alert_empty_subtitle">"Bist du sicher, dass du diesen Raum verlassen möchtest? Du bist die einzige Person hier. Wenn du gehst, kann in Zukunft niemand mehr eintreten, auch du nicht."</string>
|
<string name="leave_room_alert_empty_subtitle">"Sind Sie sicher dass Sie diesen Chatroom verlassen möchten? Sie sind die einzige Person hier. Wenn Sie gehen, kann in Zukunft niemand mehr - auch Sie nicht - diesen Chatrooom betreten.."</string>
|
||||||
<string name="leave_room_alert_private_subtitle">"Bist du sicher, dass du diesen Raum verlassen möchtest? Dieser Raum ist nicht öffentlich und du kannst ihm ohne Einladung nicht erneut beitreten."</string>
|
<string name="leave_room_alert_private_subtitle">"Sind Sie sicher dass Sie diesen Chatroom verlassen möchten? Dieser Chatroom ist nicht öffentlich und Sie können ihn ohne Einladung nicht wieder betreten."</string>
|
||||||
<string name="leave_room_alert_subtitle">"Bist du sicher, dass du den Raum verlassen willst?"</string>
|
<string name="leave_room_alert_subtitle">"Sind Sie sicher, dass Sie den Raum verlassen möchten?"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="leave_conversation_alert_subtitle">"کیا آپ واقعی اس گفتگو کو چھوڑنا چاہتے ہیں؟ یہ گفتگو عوامی نہیں ہے اور آپ دعوت نامے کے بغیر دوبارہ شامل نہیں ہو سکیں گے۔"</string>
|
||||||
|
<string name="leave_room_alert_empty_subtitle">"کیا آپ واقعی یہ کمرہ چھوڑنا چاہتے ہیں؟ آپ یہاں واحد شخص ہیں۔ اگر آپ چھوڑتے ہیں، تو مستقبل میں کوئی بھی شامل نہیں ہو سکے گا، آپ سمیت۔"</string>
|
||||||
|
<string name="leave_room_alert_private_subtitle">"کیا آپ واقعی یہ کمرہ چھوڑنا چاہتے ہیں؟ یہ کمرہ عوامی نہیں اور آپ دعوت نامے کے بغیر پھر شامل نہیں ہو پائیں گے۔"</string>
|
||||||
|
<string name="leave_room_alert_subtitle">"کیا آپ واقعی کمرہ چھوڑنا چاہتے ہیں؟"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -24,6 +24,22 @@ 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>
|
||||||
|
|
|
||||||
|
|
@ -8,29 +8,29 @@
|
||||||
<string name="screen_app_lock_settings_change_pin">"PIN-Code ändern"</string>
|
<string name="screen_app_lock_settings_change_pin">"PIN-Code ändern"</string>
|
||||||
<string name="screen_app_lock_settings_enable_biometric_unlock">"Biometrisches Entsperren zulassen"</string>
|
<string name="screen_app_lock_settings_enable_biometric_unlock">"Biometrisches Entsperren zulassen"</string>
|
||||||
<string name="screen_app_lock_settings_remove_pin">"Pin entfernen"</string>
|
<string name="screen_app_lock_settings_remove_pin">"Pin entfernen"</string>
|
||||||
<string name="screen_app_lock_settings_remove_pin_alert_message">"Bist du sicher, dass du die PIN entfernen willst?"</string>
|
<string name="screen_app_lock_settings_remove_pin_alert_message">"Sind Sie sicher, dass Sie die PIN entfernen wollen?"</string>
|
||||||
<string name="screen_app_lock_settings_remove_pin_alert_title">"PIN entfernen?"</string>
|
<string name="screen_app_lock_settings_remove_pin_alert_title">"PIN entfernen?"</string>
|
||||||
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"%1$s zulassen"</string>
|
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"%1$s zulassen"</string>
|
||||||
<string name="screen_app_lock_setup_biometric_unlock_skip">"Ich möchte diese PIN verwenden."</string>
|
<string name="screen_app_lock_setup_biometric_unlock_skip">"Ich möchte diese PIN verwenden."</string>
|
||||||
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Spare dir etwas Zeit und benutze %1$s, um die App zu entsperren"</string>
|
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Spare dir etwas Zeit und benutze %1$s, um die App zu entsperren"</string>
|
||||||
<string name="screen_app_lock_setup_choose_pin">"PIN wählen"</string>
|
<string name="screen_app_lock_setup_choose_pin">"PIN wählen"</string>
|
||||||
<string name="screen_app_lock_setup_confirm_pin">"PIN bestätigen"</string>
|
<string name="screen_app_lock_setup_confirm_pin">"PIN bestätigen"</string>
|
||||||
<string name="screen_app_lock_setup_pin_context">"Sperre %1$s mit einem PIN Code, um den Zugriff auf deine Chats zu beschränken.
|
<string name="screen_app_lock_setup_pin_context">"Erhöhen Sie die Sicherheit von %1$s mit einem PIN Code.
|
||||||
|
|
||||||
Wähle etwas Einprägsames. Bei falscher Eingabe wirst du aus der App ausgeloggt."</string>
|
Wählen Sie etwas Einprägsames. Wenn Sie die PIN vergessen, werden Sie aus der App ausgeloggt."</string>
|
||||||
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Aus Sicherheitsgründen kann dieser PIN-Code nicht verwendet werden."</string>
|
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Aus Sicherheitsgründen kann dieser PIN-Code nicht verwendet werden."</string>
|
||||||
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Bitte eine andere PIN verwenden."</string>
|
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Bitte eine andere PIN verwenden."</string>
|
||||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Bitte gib die gleiche PIN wie zuvor ein."</string>
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Bitte gib die gleiche PIN wie zuvor ein."</string>
|
||||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Die PINs stimmen nicht überein"</string>
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Die PINs stimmen nicht überein"</string>
|
||||||
<string name="screen_app_lock_signout_alert_message">"Um fortzufahren, musst du dich erneut anmelden und eine neue PIN erstellen"</string>
|
<string name="screen_app_lock_signout_alert_message">"Um fortzufahren, müssen Sie sich erneut anmelden und eine neue PIN erstellen"</string>
|
||||||
<string name="screen_app_lock_signout_alert_title">"Du wirst abgemeldet"</string>
|
<string name="screen_app_lock_signout_alert_title">"Sie werden abgemeldet"</string>
|
||||||
<plurals name="screen_app_lock_subtitle">
|
<plurals name="screen_app_lock_subtitle">
|
||||||
<item quantity="one">"Du hast %1$d Versuch zu entsperren"</item>
|
<item quantity="one">"Sie haben %1$d Entsperrversuch"</item>
|
||||||
<item quantity="other">"Du hast %1$d Versuche zum Entsperren"</item>
|
<item quantity="other">"Sie haben %1$d Entsperrversuche"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="screen_app_lock_subtitle_wrong_pin">
|
<plurals name="screen_app_lock_subtitle_wrong_pin">
|
||||||
<item quantity="one">"Falsche PIN. Du hast %1$d weiteren Versuch"</item>
|
<item quantity="one">"Falsche PIN. Sie haben %1$d weiteren Versuch"</item>
|
||||||
<item quantity="other">"Falsche PIN. Du hast %1$d weitere Versuche"</item>
|
<item quantity="other">"Falsche PIN. Sie haben %1$d weitere Versuche"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="screen_app_lock_use_biometric_android">"Biometrie verwenden"</string>
|
<string name="screen_app_lock_use_biometric_android">"Biometrie verwenden"</string>
|
||||||
<string name="screen_app_lock_use_pin_android">"PIN verwenden"</string>
|
<string name="screen_app_lock_use_pin_android">"PIN verwenden"</string>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<string name="screen_app_lock_biometric_authentication">"autenticação por biometria"</string>
|
<string name="screen_app_lock_biometric_authentication">"autenticação por biometria"</string>
|
||||||
<string name="screen_app_lock_biometric_unlock">"desbloqueio por biometria"</string>
|
<string name="screen_app_lock_biometric_unlock">"desbloqueio por biometria"</string>
|
||||||
<string name="screen_app_lock_biometric_unlock_title_android">"Desbloquear com biometria"</string>
|
<string name="screen_app_lock_biometric_unlock_title_android">"Desbloquear com biometria"</string>
|
||||||
|
<string name="screen_app_lock_confirm_biometric_authentication_android">"Confirmar biometria"</string>
|
||||||
<string name="screen_app_lock_forgot_pin">"Esqueceu o PIN?"</string>
|
<string name="screen_app_lock_forgot_pin">"Esqueceu o PIN?"</string>
|
||||||
<string name="screen_app_lock_settings_change_pin">"Alterar código de PIN"</string>
|
<string name="screen_app_lock_settings_change_pin">"Alterar código de PIN"</string>
|
||||||
<string name="screen_app_lock_settings_enable_biometric_unlock">"Permitir desbloqueio biométrico"</string>
|
<string name="screen_app_lock_settings_enable_biometric_unlock">"Permitir desbloqueio biométrico"</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_app_lock_biometric_authentication">"زیست سنجی تصدیق"</string>
|
||||||
|
<string name="screen_app_lock_biometric_unlock">"زیست سنجی فتحِ قفل"</string>
|
||||||
|
<string name="screen_app_lock_biometric_unlock_title_android">"زیست سنجی کے ساتھ فتح قفل کریں"</string>
|
||||||
|
<string name="screen_app_lock_forgot_pin">"PIN بھول گئے؟"</string>
|
||||||
|
<string name="screen_app_lock_settings_change_pin">"PIN رمز بدلیں"</string>
|
||||||
|
<string name="screen_app_lock_settings_enable_biometric_unlock">"زیست سنجی فتحِ قفل کی اجازت دیں"</string>
|
||||||
|
<string name="screen_app_lock_settings_remove_pin">"PIN ہٹائیں"</string>
|
||||||
|
<string name="screen_app_lock_settings_remove_pin_alert_message">"کیا آپ کو یقین ہے کہ آپ PIN ہٹانا چاہتے ہیں؟"</string>
|
||||||
|
<string name="screen_app_lock_settings_remove_pin_alert_title">"PIN ہٹائیں؟"</string>
|
||||||
|
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"%1$s کی اجازت دیں"</string>
|
||||||
|
<string name="screen_app_lock_setup_biometric_unlock_skip">"میں اس کے بجائے PIN استعمال کروں گا"</string>
|
||||||
|
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"اپنے آپ کو کچھ وقت بچائیں اور ہر بار اطلاقیے کو غیر مقفل کرنے کے لئے %1$s کا استعمال کریں۔"</string>
|
||||||
|
<string name="screen_app_lock_setup_choose_pin">"PIN چنیں"</string>
|
||||||
|
<string name="screen_app_lock_setup_confirm_pin">"PIN کی تصدیق کریں"</string>
|
||||||
|
<string name="screen_app_lock_setup_pin_context">"اپنی گفتگوہا میں اضافی سلامتی شامل کرنے کیلئے %1$s مقفل کریں۔
|
||||||
|
|
||||||
|
کوئی یادگار چیز چنیں۔ اگر آپ اس PIN کو بھول گئے، آپ طلاقیے سے خارج ہوجائیں گے۔"</string>
|
||||||
|
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"حفاظتی وجوہات کی بنا پر آپ اسے اپنے PIN رمز کے طور پر منتخب نہیں کر سکتے"</string>
|
||||||
|
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"ایک مختلف PIN چنیں"</string>
|
||||||
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"برائے مہربانی ایک ہی PIN دو بار درج کریں۔"</string>
|
||||||
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINs مماثل نہیں ہیں"</string>
|
||||||
|
<string name="screen_app_lock_signout_alert_message">"آگے بڑھنے کیلئے آپکو دوبارہ داخل ہونے اور ایک نیا PIN بنانے کی ضرورت ہوگی۔"</string>
|
||||||
|
<string name="screen_app_lock_signout_alert_title">"آپکو خارج کیا جا رہا ہے"</string>
|
||||||
|
<plurals name="screen_app_lock_subtitle">
|
||||||
|
<item quantity="one">"آپکے پاس %1$d غیر مقفل کرنے کی کوشش ہے"</item>
|
||||||
|
<item quantity="other">"آپکے پاس %1$d غیر مقفل کرنے کی کوششیں ہیں"</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="screen_app_lock_subtitle_wrong_pin">
|
||||||
|
<item quantity="one">"غلط PIN۔ آپ کے پاس %1$d مزید موقع ہے"</item>
|
||||||
|
<item quantity="other">"غلط PIN۔ آپ کے پاس %1$d مزید موقعے ہیں"</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="screen_app_lock_use_biometric_android">"زیست سنجی استعمال کریں"</string>
|
||||||
|
<string name="screen_app_lock_use_pin_android">"PIN استعمال کریں"</string>
|
||||||
|
<string name="screen_signout_in_progress_dialog_content">"خارج ہورہاہے…"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -20,14 +20,14 @@ import io.element.android.features.lockscreen.impl.storage.LockScreenStore
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class LockScreenSettingsPresenterTest {
|
class LockScreenSettingsPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - remove pin option is hidden when mandatory`() = runTest {
|
fun `present - remove pin option is hidden when mandatory`() = runTest {
|
||||||
val presenter = createLockScreenSettingsPresenter(this, lockScreenConfig = aLockScreenConfig(isPinMandatory = true))
|
val presenter = createLockScreenSettingsPresenter(lockScreenConfig = aLockScreenConfig(isPinMandatory = true))
|
||||||
presenter.test {
|
presenter.test {
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.showRemovePinOption).isFalse()
|
assertThat(state.showRemovePinOption).isFalse()
|
||||||
|
|
@ -37,7 +37,7 @@ class LockScreenSettingsPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - remove pin flow`() = runTest {
|
fun `present - remove pin flow`() = runTest {
|
||||||
val presenter = createLockScreenSettingsPresenter(this)
|
val presenter = createLockScreenSettingsPresenter()
|
||||||
presenter.test {
|
presenter.test {
|
||||||
consumeItemsUntilPredicate { state ->
|
consumeItemsUntilPredicate { state ->
|
||||||
state.showRemovePinOption
|
state.showRemovePinOption
|
||||||
|
|
@ -71,7 +71,6 @@ class LockScreenSettingsPresenterTest {
|
||||||
isDeviceSecured = true,
|
isDeviceSecured = true,
|
||||||
)
|
)
|
||||||
val presenter = createLockScreenSettingsPresenter(
|
val presenter = createLockScreenSettingsPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
|
@ -88,7 +87,6 @@ class LockScreenSettingsPresenterTest {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
val presenter = createLockScreenSettingsPresenter(
|
val presenter = createLockScreenSettingsPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
|
@ -110,7 +108,6 @@ class LockScreenSettingsPresenterTest {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
val presenter = createLockScreenSettingsPresenter(
|
val presenter = createLockScreenSettingsPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
|
@ -130,7 +127,6 @@ class LockScreenSettingsPresenterTest {
|
||||||
)
|
)
|
||||||
val lockScreenStore = InMemoryLockScreenStore()
|
val lockScreenStore = InMemoryLockScreenStore()
|
||||||
val presenter = createLockScreenSettingsPresenter(
|
val presenter = createLockScreenSettingsPresenter(
|
||||||
coroutineScope = this,
|
|
||||||
lockScreenStore = lockScreenStore,
|
lockScreenStore = lockScreenStore,
|
||||||
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
||||||
)
|
)
|
||||||
|
|
@ -148,8 +144,7 @@ class LockScreenSettingsPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createLockScreenSettingsPresenter(
|
private suspend fun TestScope.createLockScreenSettingsPresenter(
|
||||||
coroutineScope: CoroutineScope,
|
|
||||||
lockScreenConfig: LockScreenConfig = aLockScreenConfig(),
|
lockScreenConfig: LockScreenConfig = aLockScreenConfig(),
|
||||||
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
|
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
|
||||||
lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
|
lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
|
||||||
|
|
@ -160,7 +155,7 @@ class LockScreenSettingsPresenterTest {
|
||||||
return LockScreenSettingsPresenter(
|
return LockScreenSettingsPresenter(
|
||||||
lockScreenStore = lockScreenStore,
|
lockScreenStore = lockScreenStore,
|
||||||
pinCodeManager = pinCodeManager,
|
pinCodeManager = pinCodeManager,
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = this,
|
||||||
lockScreenConfig = lockScreenConfig,
|
lockScreenConfig = lockScreenConfig,
|
||||||
biometricAuthenticatorManager = biometricAuthenticatorManager,
|
biometricAuthenticatorManager = biometricAuthenticatorManager,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.tests.testutils.lambda.assert
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ class PinUnlockPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - success verify flow`() = runTest {
|
fun `present - success verify flow`() = runTest {
|
||||||
val presenter = createPinUnlockPresenter(this)
|
val presenter = createPinUnlockPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -71,7 +71,7 @@ class PinUnlockPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - failure verify flow`() = runTest {
|
fun `present - failure verify flow`() = runTest {
|
||||||
val presenter = createPinUnlockPresenter(this)
|
val presenter = createPinUnlockPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -100,7 +100,7 @@ class PinUnlockPresenterTest {
|
||||||
fun `present - forgot pin flow`() = runTest {
|
fun `present - forgot pin flow`() = runTest {
|
||||||
val signOutLambda = lambdaRecorder<Boolean, Unit> {}
|
val signOutLambda = lambdaRecorder<Boolean, Unit> {}
|
||||||
val signOut = FakeLogoutUseCase(signOutLambda)
|
val signOut = FakeLogoutUseCase(signOutLambda)
|
||||||
val presenter = createPinUnlockPresenter(this, logoutUseCase = signOut)
|
val presenter = createPinUnlockPresenter(logoutUseCase = signOut)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -135,8 +135,7 @@ class PinUnlockPresenterTest {
|
||||||
dataOrNull()?.assertText(text)
|
dataOrNull()?.assertText(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createPinUnlockPresenter(
|
private suspend fun TestScope.createPinUnlockPresenter(
|
||||||
scope: CoroutineScope,
|
|
||||||
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
|
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
|
||||||
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
|
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
|
||||||
logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }),
|
logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }),
|
||||||
|
|
@ -149,7 +148,7 @@ class PinUnlockPresenterTest {
|
||||||
pinCodeManager = pinCodeManager,
|
pinCodeManager = pinCodeManager,
|
||||||
biometricAuthenticatorManager = biometricAuthenticatorManager,
|
biometricAuthenticatorManager = biometricAuthenticatorManager,
|
||||||
logoutUseCase = logoutUseCase,
|
logoutUseCase = logoutUseCase,
|
||||||
coroutineScope = scope,
|
coroutineScope = this,
|
||||||
pinUnlockHelper = PinUnlockHelper(biometricAuthenticatorManager, pinCodeManager),
|
pinUnlockHelper = PinUnlockHelper(biometricAuthenticatorManager, pinCodeManager),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,11 @@ import com.bumble.appyx.core.plugin.Plugin
|
||||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
|
|
||||||
interface LoginEntryPoint : FeatureEntryPoint {
|
interface LoginEntryPoint : FeatureEntryPoint {
|
||||||
|
data class Params(
|
||||||
|
val accountProvider: String?,
|
||||||
|
val loginHint: String?,
|
||||||
|
)
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onReportProblem()
|
fun onReportProblem()
|
||||||
}
|
}
|
||||||
|
|
@ -20,6 +25,7 @@ interface LoginEntryPoint : FeatureEntryPoint {
|
||||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||||
|
|
||||||
interface NodeBuilder {
|
interface NodeBuilder {
|
||||||
|
fun params(params: Params): NodeBuilder
|
||||||
fun callback(callback: Callback): NodeBuilder
|
fun callback(callback: Callback): NodeBuilder
|
||||||
fun build(): Node
|
fun build(): Node
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -58,10 +58,11 @@ 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.featureflag.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.oidc.impl)
|
testImplementation(projects.libraries.oidc.test)
|
||||||
testImplementation(projects.libraries.permissions.test)
|
testImplementation(projects.libraries.permissions.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,14 @@ class DefaultLoginEntryPoint @Inject constructor() : LoginEntryPoint {
|
||||||
val plugins = ArrayList<Plugin>()
|
val plugins = ArrayList<Plugin>()
|
||||||
|
|
||||||
return object : LoginEntryPoint.NodeBuilder {
|
return object : LoginEntryPoint.NodeBuilder {
|
||||||
|
override fun params(params: LoginEntryPoint.Params): LoginEntryPoint.NodeBuilder {
|
||||||
|
plugins += LoginFlowNode.Params(
|
||||||
|
accountProvider = params.accountProvider,
|
||||||
|
loginHint = params.loginHint,
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
override fun callback(callback: LoginEntryPoint.Callback): LoginEntryPoint.NodeBuilder {
|
override fun callback(callback: LoginEntryPoint.Callback): LoginEntryPoint.NodeBuilder {
|
||||||
plugins += callback
|
plugins += callback
|
||||||
return this
|
return this
|
||||||
|
|
|
||||||
|
|
@ -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.orEmpty().startsWith("/element").not()) return null
|
||||||
|
val accountProvider = uri.getQueryParameter("account_provider") ?: return null
|
||||||
|
val loginHint = uri.getQueryParameter("login_hint")
|
||||||
|
return LoginParams(
|
||||||
|
accountProvider = accountProvider,
|
||||||
|
loginHint = loginHint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,16 +28,19 @@ 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.LoginEntryPoint
|
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.onboarding.OnBoardingNode
|
|
||||||
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.chooseaccountprovider.ChooseAccountProviderNode
|
||||||
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
|
||||||
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||||
import io.element.android.libraries.oidc.api.OidcAction
|
import io.element.android.libraries.oidc.api.OidcAction
|
||||||
|
|
@ -63,6 +66,11 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
) {
|
) {
|
||||||
|
data class Params(
|
||||||
|
val accountProvider: String?,
|
||||||
|
val loginHint: String?,
|
||||||
|
) : NodeInputs
|
||||||
|
|
||||||
private var activity: Activity? = null
|
private var activity: Activity? = null
|
||||||
private var darkTheme: Boolean = false
|
private var darkTheme: Boolean = false
|
||||||
|
|
||||||
|
|
@ -100,6 +108,9 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
val isAccountCreation: Boolean,
|
val isAccountCreation: Boolean,
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object ChooseAccountProvider : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object ChangeAccountProvider : NavTarget
|
data object ChangeAccountProvider : NavTarget
|
||||||
|
|
||||||
|
|
@ -126,9 +137,13 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSignIn() {
|
override fun onSignIn(mustChooseAccountProvider: Boolean) {
|
||||||
backstack.push(
|
backstack.push(
|
||||||
NavTarget.ConfirmAccountProvider(isAccountCreation = false)
|
if (mustChooseAccountProvider) {
|
||||||
|
NavTarget.ChooseAccountProvider
|
||||||
|
} else {
|
||||||
|
NavTarget.ConfirmAccountProvider(isAccountCreation = false)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,8 +154,41 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
override fun onReportProblem() {
|
override fun onReportProblem() {
|
||||||
plugins<LoginEntryPoint.Callback>().forEach { it.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
createNode<OnBoardingNode>(buildContext, listOf(callback))
|
val params = inputs<Params>()
|
||||||
|
val inputs = OnBoardingNode.Params(
|
||||||
|
accountProvider = params.accountProvider,
|
||||||
|
loginHint = params.loginHint,
|
||||||
|
)
|
||||||
|
createNode<OnBoardingNode>(buildContext, listOf(callback, inputs))
|
||||||
|
}
|
||||||
|
NavTarget.ChooseAccountProvider -> {
|
||||||
|
val callback = object : ChooseAccountProviderNode.Callback {
|
||||||
|
override fun onOidcDetails(oidcDetails: OidcDetails) {
|
||||||
|
navigateToMas(oidcDetails)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateAccountContinue(url: String) {
|
||||||
|
backstack.push(NavTarget.CreateAccount(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoginPasswordNeeded() {
|
||||||
|
backstack.push(NavTarget.LoginPassword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<ChooseAccountProviderNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
NavTarget.QrCode -> {
|
NavTarget.QrCode -> {
|
||||||
createNode<QrCodeLoginFlowNode>(buildContext)
|
createNode<QrCodeLoginFlowNode>(buildContext)
|
||||||
|
|
@ -151,16 +199,7 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
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) {
|
||||||
|
|
@ -222,6 +261,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)
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,16 @@ import javax.inject.Inject
|
||||||
class AccountProviderDataSource @Inject constructor(
|
class AccountProviderDataSource @Inject constructor(
|
||||||
enterpriseService: EnterpriseService,
|
enterpriseService: EnterpriseService,
|
||||||
) {
|
) {
|
||||||
private val defaultAccountProvider = (enterpriseService.defaultHomeserverList().firstOrNull() ?: AuthenticationConfig.MATRIX_ORG_URL)
|
private val defaultAccountProvider =
|
||||||
.let { url ->
|
(enterpriseService.defaultHomeserverList().firstOrNull { it != EnterpriseService.ANY_ACCOUNT_PROVIDER } ?: 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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* 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.accountprovider
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.features.login.impl.R
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AccountProviderOtherView(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onClick() }
|
||||||
|
) {
|
||||||
|
HorizontalDivider()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 44.dp)
|
||||||
|
.padding(vertical = 4.dp, horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
RoundedIconAtom(
|
||||||
|
size = RoundedIconAtomSize.Medium,
|
||||||
|
imageVector = CompoundIcons.Search(),
|
||||||
|
tint = ElementTheme.colors.iconPrimary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 16.dp)
|
||||||
|
.weight(1f),
|
||||||
|
text = stringResource(R.string.screen_change_account_provider_other),
|
||||||
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun AccountProviderOtherViewPreview() = ElementPreview {
|
||||||
|
AccountProviderOtherView(
|
||||||
|
onClick = { },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -23,10 +23,14 @@ open class AccountProviderProvider : PreviewParameterProvider<AccountProvider> {
|
||||||
|
|
||||||
fun anAccountProvider(
|
fun anAccountProvider(
|
||||||
url: String = AuthenticationConfig.MATRIX_ORG_URL,
|
url: String = AuthenticationConfig.MATRIX_ORG_URL,
|
||||||
|
subtitle: String? = "Matrix.org is an open network for secure, decentralized communication.",
|
||||||
|
isPublic: Boolean = true,
|
||||||
|
isMatrixOrg: Boolean = true,
|
||||||
|
isValid: Boolean = true,
|
||||||
) = AccountProvider(
|
) = AccountProvider(
|
||||||
url = url,
|
url = url,
|
||||||
subtitle = "Matrix.org is an open network for secure, decentralized communication.",
|
subtitle = subtitle,
|
||||||
isPublic = true,
|
isPublic = isPublic,
|
||||||
isMatrixOrg = true,
|
isMatrixOrg = isMatrixOrg,
|
||||||
isValid = true,
|
isValid = isValid,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ fun AccountProviderView(
|
||||||
item: AccountProvider,
|
item: AccountProvider,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
selected: Boolean = false,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|
@ -66,7 +67,7 @@ fun AccountProviderView(
|
||||||
} else {
|
} else {
|
||||||
RoundedIconAtom(
|
RoundedIconAtom(
|
||||||
size = RoundedIconAtomSize.Medium,
|
size = RoundedIconAtomSize.Medium,
|
||||||
imageVector = CompoundIcons.Search(),
|
imageVector = CompoundIcons.Host(),
|
||||||
tint = ElementTheme.colors.iconPrimary,
|
tint = ElementTheme.colors.iconPrimary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -88,6 +89,15 @@ fun AccountProviderView(
|
||||||
tint = ElementTheme.colors.iconSecondary,
|
tint = ElementTheme.colors.iconSecondary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (selected) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 10.dp),
|
||||||
|
imageVector = CompoundIcons.Check(),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = ElementTheme.colors.iconAccentPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (item.subtitle != null) {
|
if (item.subtitle != null) {
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* 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.chooseaccountprovider.ChooseAccountProviderPresenter
|
||||||
|
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], [ConfirmAccountProviderPresenter]
|
||||||
|
* and [ChooseAccountProviderPresenter].
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.login.impl.onboarding
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.produceState
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import io.element.android.appconfig.OnBoardingConfig
|
|
||||||
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
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Note: this Presenter is ignored regarding code coverage because it cannot reach the coverage threshold.
|
|
||||||
* When this presenter get more code in it, please remove the ignore rule in the kover configuration.
|
|
||||||
*/
|
|
||||||
class OnBoardingPresenter @Inject constructor(
|
|
||||||
private val buildMeta: BuildMeta,
|
|
||||||
private val featureFlagService: FeatureFlagService,
|
|
||||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
|
||||||
) : Presenter<OnBoardingState> {
|
|
||||||
@Composable
|
|
||||||
override fun present(): OnBoardingState {
|
|
||||||
val canLoginWithQrCode by produceState(initialValue = false) {
|
|
||||||
value = featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin)
|
|
||||||
}
|
|
||||||
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
|
|
||||||
return OnBoardingState(
|
|
||||||
productionApplicationName = buildMeta.productionApplicationName,
|
|
||||||
canLoginWithQrCode = canLoginWithQrCode,
|
|
||||||
canCreateAccount = OnBoardingConfig.CAN_CREATE_ACCOUNT,
|
|
||||||
canReportBug = canReportBug,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.login.impl.onboarding
|
|
||||||
|
|
||||||
data class OnBoardingState(
|
|
||||||
val productionApplicationName: String,
|
|
||||||
val canLoginWithQrCode: Boolean,
|
|
||||||
val canCreateAccount: Boolean,
|
|
||||||
val canReportBug: Boolean,
|
|
||||||
)
|
|
||||||
|
|
@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
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.enterprise.api.EnterpriseService
|
||||||
|
import io.element.android.features.enterprise.api.canConnectToAnyHomeserver
|
||||||
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
|
||||||
|
|
@ -25,6 +26,7 @@ class ChangeAccountProviderPresenter @Inject constructor(
|
||||||
override fun present(): ChangeAccountProviderState {
|
override fun present(): ChangeAccountProviderState {
|
||||||
val staticAccountProviderList = remember {
|
val staticAccountProviderList = remember {
|
||||||
enterpriseService.defaultHomeserverList()
|
enterpriseService.defaultHomeserverList()
|
||||||
|
.filter { it != EnterpriseService.ANY_ACCOUNT_PROVIDER }
|
||||||
.map { it.ensureProtocol() }
|
.map { it.ensureProtocol() }
|
||||||
.ifEmpty { listOf(AuthenticationConfig.MATRIX_ORG_URL) }
|
.ifEmpty { listOf(AuthenticationConfig.MATRIX_ORG_URL) }
|
||||||
.map { url ->
|
.map { url ->
|
||||||
|
|
@ -38,9 +40,14 @@ class ChangeAccountProviderPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val canSearchForAccountProviders = remember {
|
||||||
|
enterpriseService.canConnectToAnyHomeserver()
|
||||||
|
}
|
||||||
|
|
||||||
val changeServerState = changeServerPresenter.present()
|
val changeServerState = changeServerPresenter.present()
|
||||||
return ChangeAccountProviderState(
|
return ChangeAccountProviderState(
|
||||||
accountProviders = staticAccountProviderList,
|
accountProviders = staticAccountProviderList,
|
||||||
|
canSearchForAccountProviders = canSearchForAccountProviders,
|
||||||
changeServerState = changeServerState,
|
changeServerState = changeServerState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,6 @@ 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(
|
data class ChangeAccountProviderState(
|
||||||
val accountProviders: List<AccountProvider>,
|
val accountProviders: List<AccountProvider>,
|
||||||
|
val canSearchForAccountProviders: Boolean,
|
||||||
val changeServerState: ChangeServerState,
|
val changeServerState: ChangeServerState,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,28 @@
|
||||||
package io.element.android.features.login.impl.screens.changeaccountprovider
|
package io.element.android.features.login.impl.screens.changeaccountprovider
|
||||||
|
|
||||||
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.anAccountProvider
|
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
||||||
|
import io.element.android.features.login.impl.changeserver.ChangeServerState
|
||||||
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
||||||
|
|
||||||
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
|
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
|
||||||
override val values: Sequence<ChangeAccountProviderState>
|
override val values: Sequence<ChangeAccountProviderState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aChangeAccountProviderState(),
|
aChangeAccountProviderState(),
|
||||||
|
aChangeAccountProviderState(canSearchForAccountProviders = false),
|
||||||
// Add other state here
|
// Add other state here
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aChangeAccountProviderState() = ChangeAccountProviderState(
|
fun aChangeAccountProviderState(
|
||||||
accountProviders = listOf(
|
accountProviders: List<AccountProvider> = listOf(
|
||||||
anAccountProvider()
|
anAccountProvider()
|
||||||
),
|
),
|
||||||
changeServerState = aChangeServerState(),
|
canSearchForAccountProviders: Boolean = true,
|
||||||
|
changeServerState: ChangeServerState = aChangeServerState(),
|
||||||
|
) = ChangeAccountProviderState(
|
||||||
|
accountProviders = accountProviders,
|
||||||
|
canSearchForAccountProviders = canSearchForAccountProviders,
|
||||||
|
changeServerState = changeServerState,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ 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.accountprovider.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.AccountProviderOtherView
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderView
|
import io.element.android.features.login.impl.accountprovider.AccountProviderView
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
|
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerView
|
import io.element.android.features.login.impl.changeserver.ChangeServerView
|
||||||
|
|
@ -95,13 +95,11 @@ fun ChangeAccountProviderView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Other
|
// Other
|
||||||
AccountProviderView(
|
if (state.canSearchForAccountProviders) {
|
||||||
item = AccountProvider(
|
AccountProviderOtherView(
|
||||||
url = "",
|
onClick = onOtherProviderClick
|
||||||
title = stringResource(id = R.string.screen_change_account_provider_other),
|
)
|
||||||
),
|
}
|
||||||
onClick = onOtherProviderClick
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(32.dp))
|
Spacer(Modifier.height(32.dp))
|
||||||
}
|
}
|
||||||
ChangeServerView(
|
ChangeServerView(
|
||||||
|
|
|
||||||
|
|
@ -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.chooseaccountprovider
|
||||||
|
|
||||||
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
|
|
||||||
|
sealed interface ChooseAccountProviderEvents {
|
||||||
|
data class SelectAccountProvider(val accountProvider: AccountProvider) : ChooseAccountProviderEvents
|
||||||
|
data object Continue : ChooseAccountProviderEvents
|
||||||
|
data object ClearError : ChooseAccountProviderEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* 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.chooseaccountprovider
|
||||||
|
|
||||||
|
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.di.AppScope
|
||||||
|
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||||
|
|
||||||
|
@ContributesNode(AppScope::class)
|
||||||
|
class ChooseAccountProviderNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val presenter: ChooseAccountProviderPresenter,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onLoginPasswordNeeded()
|
||||||
|
fun onOidcDetails(oidcDetails: OidcDetails)
|
||||||
|
fun onCreateAccountContinue(url: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onOidcDetails(oidcDetails: OidcDetails) {
|
||||||
|
plugins<Callback>().forEach { it.onOidcDetails(oidcDetails) }
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
ChooseAccountProviderView(
|
||||||
|
state = state,
|
||||||
|
modifier = modifier,
|
||||||
|
onBackClick = ::navigateUp,
|
||||||
|
onOidcDetails = ::onOidcDetails,
|
||||||
|
onNeedLoginPassword = ::onLoginPasswordNeeded,
|
||||||
|
onLearnMoreClick = { openLearnMorePage(context) },
|
||||||
|
onCreateAccountContinue = ::onCreateAccountContinue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* 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.chooseaccountprovider
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import 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.login.LoginHelper
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.core.uri.ensureProtocol
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ChooseAccountProviderPresenter @Inject constructor(
|
||||||
|
private val enterpriseService: EnterpriseService,
|
||||||
|
private val loginHelper: LoginHelper,
|
||||||
|
) : Presenter<ChooseAccountProviderState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): ChooseAccountProviderState {
|
||||||
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
val loginMode by loginHelper.collectLoginMode()
|
||||||
|
|
||||||
|
var selectedAccountProvider: AccountProvider? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
|
fun handleEvent(event: ChooseAccountProviderEvents) {
|
||||||
|
when (event) {
|
||||||
|
ChooseAccountProviderEvents.Continue -> {
|
||||||
|
selectedAccountProvider?.let {
|
||||||
|
loginHelper.submit(
|
||||||
|
coroutineScope = localCoroutineScope,
|
||||||
|
isAccountCreation = false,
|
||||||
|
homeserverUrl = it.url,
|
||||||
|
loginHint = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ChooseAccountProviderEvents.SelectAccountProvider -> {
|
||||||
|
// Ensure that the user do not change the server during processing
|
||||||
|
if (loginMode is AsyncData.Uninitialized) {
|
||||||
|
selectedAccountProvider = event.accountProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChooseAccountProviderEvents.ClearError -> loginHelper.clearError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val staticAccountProviderList = remember {
|
||||||
|
// The list cannot contains ANY_ACCOUNT_PROVIDER ("*") and cannot be empty at this point
|
||||||
|
enterpriseService.defaultHomeserverList()
|
||||||
|
.map { it.ensureProtocol() }
|
||||||
|
.map { url ->
|
||||||
|
AccountProvider(
|
||||||
|
url = url,
|
||||||
|
subtitle = null,
|
||||||
|
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||||
|
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||||
|
isValid = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChooseAccountProviderState(
|
||||||
|
accountProviders = staticAccountProviderList,
|
||||||
|
selectedAccountProvider = selectedAccountProvider,
|
||||||
|
loginMode = loginMode,
|
||||||
|
eventSink = ::handleEvent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* 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.chooseaccountprovider
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// Do not use default value, so no member get forgotten in the presenters.
|
||||||
|
data class ChooseAccountProviderState(
|
||||||
|
val accountProviders: List<AccountProvider>,
|
||||||
|
val selectedAccountProvider: AccountProvider?,
|
||||||
|
val loginMode: AsyncData<LoginMode>,
|
||||||
|
val eventSink: (ChooseAccountProviderEvents) -> Unit,
|
||||||
|
) {
|
||||||
|
val submitEnabled: Boolean
|
||||||
|
get() = selectedAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* 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.chooseaccountprovider
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
|
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
||||||
|
import io.element.android.features.login.impl.login.LoginMode
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
|
open class ChooseAccountProviderStateProvider : PreviewParameterProvider<ChooseAccountProviderState> {
|
||||||
|
private val server1 = anAccountProvider(
|
||||||
|
url = "https://server1.io",
|
||||||
|
subtitle = null,
|
||||||
|
isPublic = false,
|
||||||
|
isMatrixOrg = false,
|
||||||
|
)
|
||||||
|
private val server2 = anAccountProvider(
|
||||||
|
url = "https://server2.io",
|
||||||
|
subtitle = null,
|
||||||
|
isPublic = false,
|
||||||
|
isMatrixOrg = false,
|
||||||
|
)
|
||||||
|
private val server3 = anAccountProvider(
|
||||||
|
url = "https://server3.io",
|
||||||
|
subtitle = null,
|
||||||
|
isPublic = false,
|
||||||
|
isMatrixOrg = false,
|
||||||
|
)
|
||||||
|
override val values: Sequence<ChooseAccountProviderState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aChooseAccountProviderState(
|
||||||
|
accountProviders = listOf(
|
||||||
|
server1,
|
||||||
|
server2,
|
||||||
|
server3,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
aChooseAccountProviderState(
|
||||||
|
accountProviders = listOf(
|
||||||
|
server1,
|
||||||
|
server2,
|
||||||
|
server3,
|
||||||
|
),
|
||||||
|
selectedAccountProvider = server2,
|
||||||
|
),
|
||||||
|
aChooseAccountProviderState(
|
||||||
|
accountProviders = listOf(
|
||||||
|
server1,
|
||||||
|
server2,
|
||||||
|
server3,
|
||||||
|
),
|
||||||
|
selectedAccountProvider = server2,
|
||||||
|
loginMode = AsyncData.Loading(),
|
||||||
|
),
|
||||||
|
// Add other state here
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aChooseAccountProviderState(
|
||||||
|
accountProviders: List<AccountProvider> = listOf(
|
||||||
|
anAccountProvider()
|
||||||
|
),
|
||||||
|
selectedAccountProvider: AccountProvider? = null,
|
||||||
|
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
||||||
|
eventSink: (ChooseAccountProviderEvents) -> Unit = {},
|
||||||
|
) = ChooseAccountProviderState(
|
||||||
|
accountProviders = accountProviders,
|
||||||
|
selectedAccountProvider = selectedAccountProvider,
|
||||||
|
loginMode = loginMode,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.screens.chooseaccountprovider
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.features.login.impl.R
|
||||||
|
import io.element.android.features.login.impl.accountprovider.AccountProviderView
|
||||||
|
import io.element.android.features.login.impl.login.LoginModeView
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChooseAccountProviderView(
|
||||||
|
state: ChooseAccountProviderState,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onOidcDetails: (OidcDetails) -> Unit,
|
||||||
|
onNeedLoginPassword: () -> Unit,
|
||||||
|
onLearnMoreClick: () -> Unit,
|
||||||
|
onCreateAccountContinue: (url: String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val isLoading by remember(state.loginMode) {
|
||||||
|
derivedStateOf {
|
||||||
|
state.loginMode is AsyncData.Loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {},
|
||||||
|
navigationIcon = { BackButton(onClick = onBackClick) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.imePadding()
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(state = rememberScrollState())
|
||||||
|
) {
|
||||||
|
IconTitleSubtitleMolecule(
|
||||||
|
modifier = Modifier.padding(top = 16.dp, bottom = 32.dp, start = 16.dp, end = 16.dp),
|
||||||
|
iconStyle = BigIcon.Style.Default(CompoundIcons.HomeSolid()),
|
||||||
|
title = stringResource(id = R.string.screen_server_confirmation_title_picker_mode),
|
||||||
|
subTitle = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
state.accountProviders.forEach { item ->
|
||||||
|
val alteredItem = if (item.isMatrixOrg) {
|
||||||
|
// Set the subtitle from the resource
|
||||||
|
item.copy(
|
||||||
|
subtitle = stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
item
|
||||||
|
}
|
||||||
|
AccountProviderView(
|
||||||
|
item = alteredItem,
|
||||||
|
selected = item == state.selectedAccountProvider,
|
||||||
|
onClick = {
|
||||||
|
state.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(item))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
// Flexible spacing to keep the submit button at the bottom
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Button(
|
||||||
|
text = stringResource(id = CommonStrings.action_continue),
|
||||||
|
showProgress = isLoading,
|
||||||
|
onClick = {
|
||||||
|
state.eventSink(ChooseAccountProviderEvents.Continue)
|
||||||
|
},
|
||||||
|
enabled = state.submitEnabled || isLoading,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
}
|
||||||
|
LoginModeView(
|
||||||
|
loginMode = state.loginMode,
|
||||||
|
onClearError = {
|
||||||
|
state.eventSink(ChooseAccountProviderEvents.ClearError)
|
||||||
|
},
|
||||||
|
onLearnMoreClick = onLearnMoreClick,
|
||||||
|
onOidcDetails = onOidcDetails,
|
||||||
|
onNeedLoginPassword = onNeedLoginPassword,
|
||||||
|
onCreateAccountContinue = onCreateAccountContinue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun ChooseAccountProviderViewPreview(@PreviewParameter(ChooseAccountProviderStateProvider::class) state: ChooseAccountProviderState) = ElementPreview {
|
||||||
|
ChooseAccountProviderView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = { },
|
||||||
|
onLearnMoreClick = { },
|
||||||
|
onOidcDetails = { },
|
||||||
|
onNeedLoginPassword = { },
|
||||||
|
onCreateAccountContinue = { },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -5,10 +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.login.impl.onboarding
|
package io.element.android.features.login.impl.screens.onboarding
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
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
|
||||||
|
|
@ -16,26 +17,44 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
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.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.di.AppScope
|
||||||
|
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
@ContributesNode(AppScope::class)
|
||||||
class OnBoardingNode @AssistedInject constructor(
|
class OnBoardingNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val presenter: OnBoardingPresenter,
|
presenterFactory: OnBoardingPresenter.Factory,
|
||||||
) : Node(
|
) : Node(
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins
|
plugins = plugins
|
||||||
) {
|
) {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onSignUp()
|
fun onSignUp()
|
||||||
fun onSignIn()
|
fun onSignIn(mustChooseAccountProvider: Boolean)
|
||||||
fun onSignInWithQrCode()
|
fun onSignInWithQrCode()
|
||||||
fun onReportProblem()
|
fun onReportProblem()
|
||||||
|
fun onLoginPasswordNeeded()
|
||||||
|
fun onOidcDetails(oidcDetails: OidcDetails)
|
||||||
|
fun onCreateAccountContinue(url: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSignIn() {
|
data class Params(
|
||||||
plugins<Callback>().forEach { it.onSignIn() }
|
val accountProvider: String?,
|
||||||
|
val loginHint: String?,
|
||||||
|
) : NodeInputs
|
||||||
|
|
||||||
|
private val params = inputs<Params>()
|
||||||
|
|
||||||
|
private val presenter = presenterFactory.create(
|
||||||
|
params = params,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun onSignIn(mustChooseAccountProvider: Boolean) {
|
||||||
|
plugins<Callback>().forEach { it.onSignIn(mustChooseAccountProvider) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSignUp() {
|
private fun onSignUp() {
|
||||||
|
|
@ -50,9 +69,22 @@ class OnBoardingNode @AssistedInject constructor(
|
||||||
plugins<Callback>().forEach { it.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
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
val context = LocalContext.current
|
||||||
OnBoardingView(
|
OnBoardingView(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -60,6 +92,10 @@ class OnBoardingNode @AssistedInject constructor(
|
||||||
onCreateAccount = ::onSignUp,
|
onCreateAccount = ::onSignUp,
|
||||||
onSignInWithQrCode = ::onSignInWithQrCode,
|
onSignInWithQrCode = ::onSignInWithQrCode,
|
||||||
onReportProblem = ::onReportProblem,
|
onReportProblem = ::onReportProblem,
|
||||||
|
onOidcDetails = ::onOidcDetails,
|
||||||
|
onNeedLoginPassword = ::onLoginPasswordNeeded,
|
||||||
|
onLearnMoreClick = { openLearnMorePage(context) },
|
||||||
|
onCreateAccountContinue = ::onCreateAccountContinue,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* 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.enterprise.api.EnterpriseService
|
||||||
|
import io.element.android.features.enterprise.api.canConnectToAnyHomeserver
|
||||||
|
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 enterpriseService: EnterpriseService,
|
||||||
|
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||||
|
private val loginHelper: LoginHelper,
|
||||||
|
) : Presenter<OnBoardingState> {
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(
|
||||||
|
params: OnBoardingNode.Params,
|
||||||
|
): OnBoardingPresenter
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): OnBoardingState {
|
||||||
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
val forcedAccountProvider = remember {
|
||||||
|
// If defaultHomeserverList() returns a singleton list, this is the default account provider.
|
||||||
|
// In this case, the user can sign in using this homeserver, or use QrCode login
|
||||||
|
enterpriseService.defaultHomeserverList().singleOrNull()
|
||||||
|
}
|
||||||
|
val canConnectToAnyHomeserver = remember {
|
||||||
|
enterpriseService.canConnectToAnyHomeserver()
|
||||||
|
}
|
||||||
|
val mustChooseAccountProvider = remember {
|
||||||
|
!canConnectToAnyHomeserver && enterpriseService.defaultHomeserverList().size > 1
|
||||||
|
}
|
||||||
|
val linkAccountProvider by produceState<String?>(initialValue = null) {
|
||||||
|
// Account provider from the link, if allowed by the enterprise service
|
||||||
|
value = params.accountProvider?.takeIf {
|
||||||
|
enterpriseService.isAllowedToConnectToHomeserver(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val defaultAccountProvider = remember(linkAccountProvider) {
|
||||||
|
// If there is a forced account provider, this is the default account provider
|
||||||
|
// Else use the account provider passed in the params if any and if allowed
|
||||||
|
forcedAccountProvider ?: linkAccountProvider
|
||||||
|
}
|
||||||
|
val canLoginWithQrCode by produceState(initialValue = false, linkAccountProvider) {
|
||||||
|
value = linkAccountProvider == 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 = params.loginHint?.takeIf { forcedAccountProvider == null },
|
||||||
|
)
|
||||||
|
OnBoardingEvents.ClearError -> loginHelper.clearError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return OnBoardingState(
|
||||||
|
productionApplicationName = buildMeta.productionApplicationName,
|
||||||
|
defaultAccountProvider = defaultAccountProvider,
|
||||||
|
mustChooseAccountProvider = mustChooseAccountProvider,
|
||||||
|
canLoginWithQrCode = canLoginWithQrCode,
|
||||||
|
canCreateAccount = defaultAccountProvider == null && canConnectToAnyHomeserver && OnBoardingConfig.CAN_CREATE_ACCOUNT,
|
||||||
|
canReportBug = canReportBug,
|
||||||
|
loginMode = loginMode,
|
||||||
|
eventSink = ::handleEvent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue