Merge branch 'release/25.06.0' into main
This commit is contained in:
commit
4dbfa905f9
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
|
||||
ARCH: x86_64
|
||||
DEVICE: pixel_7_pro
|
||||
API_LEVEL: 35
|
||||
API_LEVEL: 33
|
||||
TARGET: google_apis
|
||||
|
||||
jobs:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
appId: ${MAESTRO_APP_ID}
|
||||
androidWebViewHierarchy: devtools
|
||||
---
|
||||
## Check that all env variables required in the whole test suite are declared (to fail faster)
|
||||
- runScript: ./scripts/checkEnv.js
|
||||
|
|
|
|||
|
|
@ -14,7 +14,22 @@ appId: ${MAESTRO_APP_ID}
|
|||
visible: 'Use without an account'
|
||||
commands:
|
||||
- tapOn: "Use without an account"
|
||||
## For older chrome versions
|
||||
- runFlow:
|
||||
when:
|
||||
visible: 'Accept & continue'
|
||||
commands:
|
||||
- tapOn: "Accept & continue"
|
||||
- runFlow:
|
||||
when:
|
||||
visible: 'No thanks'
|
||||
commands:
|
||||
- tapOn: "No thanks"
|
||||
## Working when running Maestro locally, but not on the CI yet.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "form-1"
|
||||
timeout: 10000
|
||||
- tapOn:
|
||||
id: "form-1"
|
||||
- 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
|
||||
=============================
|
||||
|
||||
|
|
|
|||
|
|
@ -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' />
|
||||
</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
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.ElementX.Splash"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
|
|
@ -54,6 +60,9 @@
|
|||
android:host="open"
|
||||
android:scheme="elementx" />
|
||||
</intent-filter>
|
||||
<!--
|
||||
Oidc redirection
|
||||
-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
|
|
@ -80,6 +89,21 @@
|
|||
<!-- Matching asset file: https://staging.element.io/.well-known/assetlinks.json -->
|
||||
<data android:host="staging.element.io" />
|
||||
</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
|
||||
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>
|
||||
<exclude domain="root" path="." />
|
||||
<exclude domain="file" path="." />
|
||||
<exclude domain="database" path="." />
|
||||
<exclude domain="sharedpref" path="." />
|
||||
<exclude domain="external" path="."/>
|
||||
</full-backup-content>
|
||||
|
|
|
|||
|
|
@ -9,9 +9,18 @@
|
|||
-->
|
||||
<data-extraction-rules>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</data-extraction-rules>
|
||||
</data-extraction-rules>
|
||||
|
|
@ -30,6 +30,7 @@
|
|||
<locale android:name="sv"/>
|
||||
<locale android:name="tr"/>
|
||||
<locale android:name="uk"/>
|
||||
<locale android:name="ur"/>
|
||||
<locale android:name="uz"/>
|
||||
<locale android:name="zh-CN"/>
|
||||
<locale android:name="zh-TW"/>
|
||||
|
|
|
|||
|
|
@ -53,16 +53,15 @@ dependencies {
|
|||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.features.login.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.oidc.impl)
|
||||
testImplementation(projects.libraries.oidc.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.libraries.pushproviders.test)
|
||||
testImplementation(projects.features.networkmonitor.test)
|
||||
testImplementation(projects.features.login.impl)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.features.rageshake.test)
|
||||
testImplementation(projects.features.rageshake.impl)
|
||||
testImplementation(projects.features.share.test)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
|
|
|
|||
|
|
@ -24,8 +24,11 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
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.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.ScreenOrientation
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
|
@ -46,10 +49,16 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
|||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) {
|
||||
data class Params(
|
||||
val loginParams: LoginParams?,
|
||||
) : NodeInputs
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onOpenBugReport()
|
||||
}
|
||||
|
||||
private val inputs = inputs<Params>()
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
lifecycle.subscribe(
|
||||
|
|
@ -74,6 +83,12 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
|||
}
|
||||
loginEntryPoint
|
||||
.nodeBuilder(this, buildContext)
|
||||
.params(
|
||||
LoginEntryPoint.Params(
|
||||
accountProvider = inputs.loginParams?.accountProvider,
|
||||
loginHint = inputs.loginParams?.loginHint,
|
||||
)
|
||||
)
|
||||
.callback(callback)
|
||||
.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.RootPresenter
|
||||
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.signedout.api.SignedOutEntryPoint
|
||||
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.createNode
|
||||
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.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
|
@ -61,6 +64,7 @@ class RootFlowNode @AssistedInject constructor(
|
|||
@Assisted val buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val enterpriseService: EnterpriseService,
|
||||
private val navStateFlowFactory: RootNavStateFlowFactory,
|
||||
private val matrixSessionCache: MatrixSessionCache,
|
||||
private val presenter: RootPresenter,
|
||||
|
|
@ -99,14 +103,14 @@ class RootFlowNode @AssistedInject constructor(
|
|||
if (navState.loggedInState.isTokenValid) {
|
||||
tryToRestoreLatestSession(
|
||||
onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
|
||||
onFailure = { switchToNotLoggedInFlow() }
|
||||
onFailure = { switchToNotLoggedInFlow(null) }
|
||||
)
|
||||
} else {
|
||||
switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId))
|
||||
}
|
||||
}
|
||||
LoggedInState.NotLoggedIn -> {
|
||||
switchToNotLoggedInFlow()
|
||||
switchToNotLoggedInFlow(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -117,9 +121,9 @@ class RootFlowNode @AssistedInject constructor(
|
|||
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId))
|
||||
}
|
||||
|
||||
private fun switchToNotLoggedInFlow() {
|
||||
private fun switchToNotLoggedInFlow(params: LoginParams?) {
|
||||
matrixSessionCache.removeAll()
|
||||
backstack.safeRoot(NavTarget.NotLoggedInFlow)
|
||||
backstack.safeRoot(NavTarget.NotLoggedInFlow(params))
|
||||
}
|
||||
|
||||
private fun switchToSignedOutFlow(sessionId: SessionId) {
|
||||
|
|
@ -175,7 +179,9 @@ class RootFlowNode @AssistedInject constructor(
|
|||
data object SplashScreen : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object NotLoggedInFlow : NavTarget
|
||||
data class NotLoggedInFlow(
|
||||
val params: LoginParams?
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class LoggedInFlow(
|
||||
|
|
@ -211,13 +217,16 @@ class RootFlowNode @AssistedInject constructor(
|
|||
}
|
||||
createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback))
|
||||
}
|
||||
NavTarget.NotLoggedInFlow -> {
|
||||
is NavTarget.NotLoggedInFlow -> {
|
||||
val callback = object : NotLoggedInFlowNode.Callback {
|
||||
override fun onOpenBugReport() {
|
||||
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 -> {
|
||||
signedOutEntryPoint.nodeBuilder(this, buildContext)
|
||||
|
|
@ -272,18 +281,36 @@ class RootFlowNode @AssistedInject constructor(
|
|||
val resolvedIntent = intentResolver.resolve(intent) ?: return
|
||||
when (resolvedIntent) {
|
||||
is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData)
|
||||
is ResolvedIntent.Login -> onLoginLink(resolvedIntent.params)
|
||||
is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
|
||||
is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData)
|
||||
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) {
|
||||
// Is there a session already?
|
||||
val latestSessionId = authenticationService.getLatestSessionId()
|
||||
if (latestSessionId == null) {
|
||||
// No session, open login
|
||||
switchToNotLoggedInFlow()
|
||||
switchToNotLoggedInFlow(null)
|
||||
} else {
|
||||
attachSession(latestSessionId)
|
||||
.attachIncomingShare(intent)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
package io.element.android.appnav.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.DeeplinkParser
|
||||
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 Oidc(val oidcAction: OidcAction) : ResolvedIntent
|
||||
data class Permalink(val permalinkData: PermalinkData) : ResolvedIntent
|
||||
data class Login(val params: LoginParams) : ResolvedIntent
|
||||
data class IncomingShare(val intent: Intent) : ResolvedIntent
|
||||
}
|
||||
|
||||
class IntentResolver @Inject constructor(
|
||||
private val deeplinkParser: DeeplinkParser,
|
||||
private val loginIntentResolver: LoginIntentResolver,
|
||||
private val oidcIntentResolver: OidcIntentResolver,
|
||||
private val permalinkParser: PermalinkParser,
|
||||
) {
|
||||
|
|
@ -40,10 +44,17 @@ class IntentResolver @Inject constructor(
|
|||
val oidcAction = oidcIntentResolver.resolve(intent)
|
||||
if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction)
|
||||
|
||||
// External link clicked? (matrix.to, element.io, etc.)
|
||||
val permalinkData = intent
|
||||
val actionViewData = intent
|
||||
.takeIf { it.action == Intent.ACTION_VIEW }
|
||||
?.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) }
|
||||
?.takeIf { it !is PermalinkData.FallbackLink }
|
||||
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.permalink.PermalinkData
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -51,6 +52,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
|||
private val appNavigationStateService: AppNavigationStateService,
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||
roomComponentFactory: RoomComponentFactory,
|
||||
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
|
|
@ -85,6 +87,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
|||
onCreate = {
|
||||
Timber.v("OnCreate => ${inputs.room.roomId}")
|
||||
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
||||
activeRoomsHolder.addRoom(inputs.room)
|
||||
fetchRoomMembers()
|
||||
trackVisitedRoom()
|
||||
},
|
||||
|
|
@ -95,6 +98,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
|||
},
|
||||
onDestroy = {
|
||||
Timber.v("OnDestroy")
|
||||
activeRoomsHolder.removeRoom(inputs.room.sessionId, inputs.room.roomId)
|
||||
inputs.room.destroy()
|
||||
appNavigationStateService.onLeavingRoom(id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +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">"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>
|
||||
</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.libraries.architecture.childNode
|
||||
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.room.FakeBaseRoom
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class JoinBaseRoomLoadedFlowNodeTest {
|
||||
class JoinedRoomLoadedFlowNodeTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
|
|
@ -96,20 +98,21 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun createJoinedRoomLoadedFlowNode(
|
||||
private fun TestScope.createJoinedRoomLoadedFlowNode(
|
||||
plugins: List<Plugin>,
|
||||
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
|
||||
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
||||
coroutineScope: CoroutineScope,
|
||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||
) = JoinedRoomLoadedFlowNode(
|
||||
buildContext = BuildContext.root(savedStateMap = null),
|
||||
plugins = plugins,
|
||||
messagesEntryPoint = messagesEntryPoint,
|
||||
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
||||
appNavigationStateService = FakeAppNavigationStateService(),
|
||||
appCoroutineScope = coroutineScope,
|
||||
appCoroutineScope = this,
|
||||
roomComponentFactory = FakeRoomComponentFactory(),
|
||||
matrixClient = FakeMatrixClient(),
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
)
|
||||
|
||||
@Test
|
||||
|
|
@ -121,7 +124,6 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
|||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||
plugins = listOf(inputs),
|
||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||
coroutineScope = this
|
||||
)
|
||||
// WHEN
|
||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||
|
|
@ -144,7 +146,6 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
|||
plugins = listOf(inputs),
|
||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
||||
coroutineScope = this
|
||||
)
|
||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||
// WHEN
|
||||
|
|
@ -154,4 +155,53 @@ class JoinBaseRoomLoadedFlowNodeTest {
|
|||
val roomDetailsNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.RoomDetails)!!
|
||||
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 androidx.core.net.toUri
|
||||
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.DeeplinkData
|
||||
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_SESSION_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.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.impl.DefaultOidcIntentResolver
|
||||
import io.element.android.libraries.oidc.impl.DefaultOidcUrlParser
|
||||
import io.element.android.libraries.oidc.test.FakeOidcIntentResolver
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
|
@ -116,8 +115,10 @@ class IntentResolverTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `test resolve oidc go back`() {
|
||||
val sut = createIntentResolver()
|
||||
fun `test resolve oidc`() {
|
||||
val sut = createIntentResolver(
|
||||
oidcIntentResolverResult = { OidcAction.GoBack },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
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
|
||||
fun `test resolve external permalink`() {
|
||||
val permalinkData = PermalinkData.UserLink(
|
||||
userId = UserId("@alice:matrix.org")
|
||||
)
|
||||
val sut = createIntentResolver(
|
||||
permalinkParserResult = { permalinkData }
|
||||
loginIntentResolverResult = { null },
|
||||
permalinkParserResult = { permalinkData },
|
||||
oidcIntentResolverResult = { null },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
|
|
@ -182,7 +156,9 @@ class IntentResolverTest {
|
|||
@Test
|
||||
fun `test resolve external permalink, FallbackLink should be ignored`() {
|
||||
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 {
|
||||
action = Intent.ACTION_VIEW
|
||||
|
|
@ -198,7 +174,8 @@ class IntentResolverTest {
|
|||
userId = UserId("@alice:matrix.org")
|
||||
)
|
||||
val sut = createIntentResolver(
|
||||
permalinkParserResult = { permalinkData }
|
||||
permalinkParserResult = { permalinkData },
|
||||
oidcIntentResolverResult = { null },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_BATTERY_LOW
|
||||
|
|
@ -210,7 +187,9 @@ class IntentResolverTest {
|
|||
|
||||
@Test
|
||||
fun `test incoming share simple`() {
|
||||
val sut = createIntentResolver()
|
||||
val sut = createIntentResolver(
|
||||
oidcIntentResolverResult = { null },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_SEND
|
||||
}
|
||||
|
|
@ -220,7 +199,9 @@ class IntentResolverTest {
|
|||
|
||||
@Test
|
||||
fun `test incoming share multiple`() {
|
||||
val sut = createIntentResolver()
|
||||
val sut = createIntentResolver(
|
||||
oidcIntentResolverResult = { null },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_SEND_MULTIPLE
|
||||
}
|
||||
|
|
@ -231,7 +212,9 @@ class IntentResolverTest {
|
|||
@Test
|
||||
fun `test resolve invalid`() {
|
||||
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 {
|
||||
action = Intent.ACTION_VIEW
|
||||
|
|
@ -241,15 +224,33 @@ class IntentResolverTest {
|
|||
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(
|
||||
permalinkParserResult: (String) -> PermalinkData = { lambdaError() }
|
||||
permalinkParserResult: (String) -> PermalinkData = { lambdaError() },
|
||||
loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() },
|
||||
oidcIntentResolverResult: (Intent) -> OidcAction? = { lambdaError() },
|
||||
): IntentResolver {
|
||||
return IntentResolver(
|
||||
deeplinkParser = DeeplinkParser(),
|
||||
oidcIntentResolver = DefaultOidcIntentResolver(
|
||||
oidcUrlParser = DefaultOidcUrlParser(
|
||||
oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(),
|
||||
)
|
||||
loginIntentResolver = FakeLoginIntentResolver(
|
||||
parseResult = loginIntentResolverResult,
|
||||
),
|
||||
oidcIntentResolver = FakeOidcIntentResolver(
|
||||
resolveResult = oidcIntentResolverResult,
|
||||
),
|
||||
permalinkParser = FakePermalinkParser(
|
||||
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 :
|
||||
[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 :
|
||||
- Navigate to File/Manage IDE Settings/Import Settings
|
||||
- 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"?>
|
||||
<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_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_share_data">"Analysedaten teilen"</string>
|
||||
</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">
|
||||
<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_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_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_title">"Hilf uns %1$s zu verbessern"</string>
|
||||
</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.network.useragent.UserAgentProvider
|
||||
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.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -62,6 +63,7 @@ class CallScreenPresenter @AssistedInject constructor(
|
|||
private val activeCallManager: ActiveCallManager,
|
||||
private val languageTagProvider: LanguageTagProvider,
|
||||
private val appForegroundStateService: AppForegroundStateService,
|
||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
) : Presenter<CallScreenState> {
|
||||
@AssistedFactory
|
||||
|
|
@ -241,8 +243,10 @@ class CallScreenPresenter @AssistedInject constructor(
|
|||
|
||||
private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) {
|
||||
if (!notifiedCallStart) {
|
||||
getJoinedRoom(roomId)?.use { it.sendCallNotificationIfNeeded() }
|
||||
?.onSuccess { notifiedCallStart = true }
|
||||
val activeRoomForSession = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
|
||||
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.widget.CallWidgetSettingsProvider
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ class DefaultCallWidgetProvider @Inject constructor(
|
|||
private val matrixClientsProvider: MatrixClientProvider,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
|
||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||
) : CallWidgetProvider {
|
||||
override suspend fun getWidget(
|
||||
sessionId: SessionId,
|
||||
|
|
@ -33,7 +35,9 @@ class DefaultCallWidgetProvider @Inject constructor(
|
|||
theme: String?,
|
||||
): Result<CallWidgetProvider.GetWidgetResult> = runCatching {
|
||||
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 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_message_android">"Toque para retornar à chamada"</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>
|
||||
|
|
|
|||
|
|
@ -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.services.analytics.api.ScreenTracker
|
||||
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.toolbox.api.systemclock.SystemClock
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
|
|
@ -367,6 +368,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
|
||||
screenTracker: ScreenTracker = FakeScreenTracker(),
|
||||
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||
): CallScreenPresenter {
|
||||
val userAgentProvider = object : UserAgentProvider {
|
||||
override fun provide(): String {
|
||||
|
|
@ -387,6 +389,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||
languageTagProvider = FakeLanguageTagProvider("en-US"),
|
||||
appForegroundStateService = appForegroundStateService,
|
||||
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.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
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.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
|
|
@ -63,30 +59,28 @@ class DefaultActiveCallManagerTest {
|
|||
fun `registerIncomingCall - sets the incoming call as active`() = runTest {
|
||||
setupShadowPowerManager()
|
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||
inCancellableScope {
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||
|
||||
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
|
||||
val callNotificationData = aCallNotificationData()
|
||||
manager.registerIncomingCall(callNotificationData)
|
||||
val callNotificationData = aCallNotificationData()
|
||||
manager.registerIncomingCall(callNotificationData)
|
||||
|
||||
assertThat(manager.activeCall.value).isEqualTo(
|
||||
ActiveCall(
|
||||
callType = CallType.RoomCall(
|
||||
sessionId = callNotificationData.sessionId,
|
||||
roomId = callNotificationData.roomId,
|
||||
),
|
||||
callState = CallState.Ringing(callNotificationData)
|
||||
)
|
||||
assertThat(manager.activeCall.value).isEqualTo(
|
||||
ActiveCall(
|
||||
callType = CallType.RoomCall(
|
||||
sessionId = callNotificationData.sessionId,
|
||||
roomId = callNotificationData.roomId,
|
||||
),
|
||||
callState = CallState.Ringing(callNotificationData)
|
||||
)
|
||||
)
|
||||
|
||||
runCurrent()
|
||||
runCurrent()
|
||||
|
||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||
verify { notificationManagerCompat.notify(notificationId, any()) }
|
||||
}
|
||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||
verify { notificationManagerCompat.notify(notificationId, any()) }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
|
@ -94,42 +88,38 @@ class DefaultActiveCallManagerTest {
|
|||
fun `registerIncomingCall - when there is an already active call adds missed call notification`() = runTest {
|
||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||
val onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
||||
inCancellableScope {
|
||||
val manager = createActiveCallManager(
|
||||
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
||||
)
|
||||
val manager = createActiveCallManager(
|
||||
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
||||
)
|
||||
|
||||
// Register existing call
|
||||
val callNotificationData = aCallNotificationData()
|
||||
manager.registerIncomingCall(callNotificationData)
|
||||
val activeCall = manager.activeCall.value
|
||||
// Register existing call
|
||||
val callNotificationData = aCallNotificationData()
|
||||
manager.registerIncomingCall(callNotificationData)
|
||||
val activeCall = manager.activeCall.value
|
||||
|
||||
// Now add a new call
|
||||
manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2))
|
||||
// Now add a new call
|
||||
manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2))
|
||||
|
||||
assertThat(manager.activeCall.value).isEqualTo(activeCall)
|
||||
assertThat((manager.activeCall.value?.callType as? CallType.RoomCall)?.roomId).isNotEqualTo(A_ROOM_ID_2)
|
||||
assertThat(manager.activeCall.value).isEqualTo(activeCall)
|
||||
assertThat((manager.activeCall.value?.callType as? CallType.RoomCall)?.roomId).isNotEqualTo(A_ROOM_ID_2)
|
||||
|
||||
advanceTimeBy(1)
|
||||
advanceTimeBy(1)
|
||||
|
||||
addMissedCallNotificationLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SESSION_ID), value(A_ROOM_ID_2), value(AN_EVENT_ID))
|
||||
}
|
||||
addMissedCallNotificationLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SESSION_ID), value(A_ROOM_ID_2), value(AN_EVENT_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `incomingCallTimedOut - when there isn't an active call does nothing`() = runTest {
|
||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||
inCancellableScope {
|
||||
val manager = createActiveCallManager(
|
||||
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
||||
)
|
||||
val manager = createActiveCallManager(
|
||||
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
||||
)
|
||||
|
||||
manager.incomingCallTimedOut(displayMissedCallNotification = true)
|
||||
manager.incomingCallTimedOut(displayMissedCallNotification = true)
|
||||
|
||||
addMissedCallNotificationLambda.assertions().isNeverCalled()
|
||||
}
|
||||
addMissedCallNotificationLambda.assertions().isNeverCalled()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
|
@ -138,90 +128,80 @@ class DefaultActiveCallManagerTest {
|
|||
setupShadowPowerManager()
|
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||
inCancellableScope {
|
||||
val manager = createActiveCallManager(
|
||||
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda),
|
||||
notificationManagerCompat = notificationManagerCompat,
|
||||
)
|
||||
val manager = createActiveCallManager(
|
||||
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda),
|
||||
notificationManagerCompat = notificationManagerCompat,
|
||||
)
|
||||
|
||||
manager.registerIncomingCall(aCallNotificationData())
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||
manager.registerIncomingCall(aCallNotificationData())
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||
|
||||
manager.incomingCallTimedOut(displayMissedCallNotification = true)
|
||||
advanceTimeBy(1)
|
||||
manager.incomingCallTimedOut(displayMissedCallNotification = true)
|
||||
advanceTimeBy(1)
|
||||
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
||||
addMissedCallNotificationLambda.assertions().isCalledOnce()
|
||||
verify { notificationManagerCompat.cancel(notificationId) }
|
||||
}
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
||||
addMissedCallNotificationLambda.assertions().isCalledOnce()
|
||||
verify { notificationManagerCompat.cancel(notificationId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hungUpCall - removes existing call if the CallType matches`() = runTest {
|
||||
setupShadowPowerManager()
|
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||
// Create a cancellable coroutine scope to cancel the test when needed
|
||||
inCancellableScope {
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||
|
||||
val notificationData = aCallNotificationData()
|
||||
manager.registerIncomingCall(notificationData)
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||
val notificationData = aCallNotificationData()
|
||||
manager.registerIncomingCall(notificationData)
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||
|
||||
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
||||
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
assertThat(manager.activeWakeLock?.isHeld).isFalse()
|
||||
|
||||
verify { notificationManagerCompat.cancel(notificationId) }
|
||||
}
|
||||
verify { notificationManagerCompat.cancel(notificationId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
|
||||
setupShadowPowerManager()
|
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||
// Create a cancellable coroutine scope to cancel the test when needed
|
||||
inCancellableScope {
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||
|
||||
manager.registerIncomingCall(aCallNotificationData())
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||
manager.registerIncomingCall(aCallNotificationData())
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||
|
||||
manager.hungUpCall(CallType.ExternalUrl("https://example.com"))
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||
manager.hungUpCall(CallType.ExternalUrl("https://example.com"))
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
assertThat(manager.activeWakeLock?.isHeld).isTrue()
|
||||
|
||||
verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
|
||||
}
|
||||
verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `joinedCall - register an ongoing call and tries sending the call notify event`() = runTest {
|
||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||
inCancellableScope {
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
|
||||
manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID))
|
||||
assertThat(manager.activeCall.value).isEqualTo(
|
||||
ActiveCall(
|
||||
callType = CallType.RoomCall(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
),
|
||||
callState = CallState.InCall,
|
||||
)
|
||||
manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID))
|
||||
assertThat(manager.activeCall.value).isEqualTo(
|
||||
ActiveCall(
|
||||
callType = CallType.RoomCall(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
),
|
||||
callState = CallState.InCall,
|
||||
)
|
||||
)
|
||||
|
||||
runCurrent()
|
||||
runCurrent()
|
||||
|
||||
verify { notificationManagerCompat.cancel(notificationId) }
|
||||
}
|
||||
verify { notificationManagerCompat.cancel(notificationId) }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
|
@ -233,22 +213,19 @@ class DefaultActiveCallManagerTest {
|
|||
val client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
// Create a cancellable coroutine scope to cancel the test when needed
|
||||
inCancellableScope {
|
||||
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) })
|
||||
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)
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||
advanceTimeBy(1)
|
||||
// Call is cancelled (the other user left the call)
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||
advanceTimeBy(1)
|
||||
// Call is active (the other user join the call)
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||
advanceTimeBy(1)
|
||||
// Call is cancelled (the other user left the call)
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||
advanceTimeBy(1)
|
||||
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
}
|
||||
assertThat(manager.activeCall.value).isNull()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
|
@ -260,44 +237,34 @@ class DefaultActiveCallManagerTest {
|
|||
val client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
// Create a cancellable coroutine scope to cancel the test when needed
|
||||
inCancellableScope {
|
||||
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.failure(IllegalStateException("Matrix client not found")) })
|
||||
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))
|
||||
advanceTimeBy(1)
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||
advanceTimeBy(1)
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||
advanceTimeBy(1)
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||
advanceTimeBy(1)
|
||||
|
||||
// The call should still be active
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
// The call should still be active
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
|
||||
// No room
|
||||
client.givenGetRoomResult(A_ROOM_ID, null)
|
||||
matrixClientProvider.getClient = { Result.success(client) }
|
||||
// No room
|
||||
client.givenGetRoomResult(A_ROOM_ID, null)
|
||||
matrixClientProvider.getClient = { Result.success(client) }
|
||||
|
||||
manager.registerIncomingCall(aCallNotificationData())
|
||||
manager.registerIncomingCall(aCallNotificationData())
|
||||
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||
advanceTimeBy(1)
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||
advanceTimeBy(1)
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||
advanceTimeBy(1)
|
||||
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||
advanceTimeBy(1)
|
||||
|
||||
// The call should still be active
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.inCancellableScope(block: suspend CoroutineScope.() -> Unit) {
|
||||
launch(SupervisorJob()) {
|
||||
block()
|
||||
cancel()
|
||||
}
|
||||
// The call should still be active
|
||||
assertThat(manager.activeCall.value).isNotNull()
|
||||
}
|
||||
|
||||
private fun setupShadowPowerManager() {
|
||||
|
|
@ -306,14 +273,13 @@ class DefaultActiveCallManagerTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.createActiveCallManager(
|
||||
private fun TestScope.createActiveCallManager(
|
||||
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
||||
onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(),
|
||||
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true),
|
||||
coroutineScope: CoroutineScope = this,
|
||||
) = DefaultActiveCallManager(
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
coroutineScope = coroutineScope,
|
||||
coroutineScope = backgroundScope,
|
||||
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
||||
ringingCallNotificationCreator = RingingCallNotificationCreator(
|
||||
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.FakeMatrixClient
|
||||
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.widget.FakeCallWidgetSettingsProvider
|
||||
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -77,6 +79,29 @@ class DefaultCallWidgetProviderTest {
|
|||
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
|
||||
fun `getWidget - will use a custom base url if it exists`() = runTest {
|
||||
val room = FakeJoinedRoom(
|
||||
|
|
@ -104,9 +129,11 @@ class DefaultCallWidgetProviderTest {
|
|||
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||
) = DefaultCallWidgetProvider(
|
||||
matrixClientsProvider = matrixClientProvider,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
callWidgetSettingsProvider = callWidgetSettingsProvider,
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<?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">"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_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_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>
|
||||
<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_title">"Jemand"</string>
|
||||
<string name="screen_create_room_room_access_section_header">"Chatroom Zugang"</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_anyone_option_description">"Jeder darf diesen Raum betreten"</string>
|
||||
<string name="screen_create_room_room_access_section_anyone_option_title">"Jeder"</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 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_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>
|
||||
|
|
|
|||
|
|
@ -7,9 +7,24 @@
|
|||
<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.
|
||||
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_visibility_section_title">"Visibilidade da 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_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_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>
|
||||
|
|
|
|||
|
|
@ -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_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_title">"Benutzerkonto deaktivieren"</string>
|
||||
<string name="screen_deactivate_account_title">"Nutzerkonto deaktivieren"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"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_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_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>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -16,9 +16,21 @@ interface EnterpriseService {
|
|||
fun defaultHomeserverList(): List<String>
|
||||
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
|
||||
|
||||
suspend fun isElementCallAvailable(): Boolean
|
||||
|
||||
fun semanticColorsLight(): SemanticColors
|
||||
fun semanticColorsDark(): SemanticColors
|
||||
|
||||
fun firebasePushGateway(): 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 suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
|
||||
|
||||
override suspend fun isElementCallAvailable(): Boolean = true
|
||||
|
||||
override fun semanticColorsLight(): SemanticColors = compoundColorsLight
|
||||
|
||||
override fun semanticColorsDark(): SemanticColors = compoundColorsDark
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class FakeEnterpriseService(
|
|||
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
|
||||
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
|
||||
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
|
||||
private val isElementCallAvailableResult: () -> Boolean = { lambdaError() },
|
||||
private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() },
|
||||
private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() },
|
||||
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
|
||||
|
|
@ -35,6 +36,10 @@ class FakeEnterpriseService(
|
|||
isAllowedToConnectToHomeserverResult(homeserverUrl)
|
||||
}
|
||||
|
||||
override suspend fun isElementCallAvailable(): Boolean = simulateLongTask {
|
||||
isElementCallAvailableResult()
|
||||
}
|
||||
|
||||
override fun semanticColorsLight(): SemanticColors {
|
||||
return semanticColorsLightResult()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ dependencies {
|
|||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.services.analytics.noop)
|
||||
testImplementation(projects.libraries.permissions.impl)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.features.lockscreen.test)
|
||||
|
|
|
|||
|
|
@ -1,22 +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">"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_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_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_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_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_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_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_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>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
<?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">"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_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_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_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>
|
||||
|
|
|
|||
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.test.verification.FakeSessionVerificationService
|
||||
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.test.InMemorySessionPreferencesStore
|
||||
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 io.element.android.libraries.permissions.api.PermissionStateProvider
|
||||
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.FakePermissionsPresenterFactory
|
||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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_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_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_empty_list">"Keine Einladungen"</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_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>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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_title">"Recusar chat"</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_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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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>
|
||||
|
|
|
|||
|
|
@ -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_title">"封鎖使用者"</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_invites_decline_chat_message">"您確定您想要拒絕加入 %1$s 的邀請嗎?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"拒絕邀請"</string>
|
||||
|
|
|
|||
|
|
@ -1,32 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_message">"Sie wurden aus diesem Chatroom verbannt"</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 für diesen Chatroom gesperrt"</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_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_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_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_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_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_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_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_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_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_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_no_preview">"Du musst Mitglied in diesem Raum sein, um den Nachrichtenverlauf zu sehen."</string>
|
||||
<string name="screen_join_room_title_knock">"Willst du diesem Raum beitreten?"</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">"Sie müssen Mitglied in diesem Chatroom sein, um den Nachrichtenverlauf einsehen zu können."</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>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<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_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_join_action">"Sartu gelan"</string>
|
||||
<string name="screen_join_room_knock_action">"Bidali batzeko eskaera"</string>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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>
|
||||
|
|
|
|||
|
|
@ -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_initial_loading_title">"Yn llwytho 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_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>
|
||||
|
|
|
|||
|
|
@ -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_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_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_loading_title">"Ablehnung und Sperrung des Zugriffs"</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"?>
|
||||
<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_title">"%1$s quer entrar nesta sala"</string>
|
||||
<string name="screen_room_single_knock_request_view_button_title">"Ver"</string>
|
||||
</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"?>
|
||||
<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_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_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_subtitle">"Bist du sicher, dass du den Raum verlassen willst?"</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">"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">"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">"Sind Sie sicher, dass Sie den Raum verlassen möchten?"</string>
|
||||
</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_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>
|
||||
<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_pin_android">"Defnyddio PIN"</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_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_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_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_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_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_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_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_title">"Du wirst abgemeldet"</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">"Sie werden abgemeldet"</string>
|
||||
<plurals name="screen_app_lock_subtitle">
|
||||
<item quantity="one">"Du hast %1$d Versuch zu entsperren"</item>
|
||||
<item quantity="other">"Du hast %1$d Versuche zum Entsperren"</item>
|
||||
<item quantity="one">"Sie haben %1$d Entsperrversuch"</item>
|
||||
<item quantity="other">"Sie haben %1$d Entsperrversuche"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_app_lock_subtitle_wrong_pin">
|
||||
<item quantity="one">"Falsche PIN. Du hast %1$d weiteren Versuch"</item>
|
||||
<item quantity="other">"Falsche PIN. Du hast %1$d weitere Versuche"</item>
|
||||
<item quantity="one">"Falsche PIN. Sie haben %1$d weiteren Versuch"</item>
|
||||
<item quantity="other">"Falsche PIN. Sie haben %1$d weitere Versuche"</item>
|
||||
</plurals>
|
||||
<string name="screen_app_lock_use_biometric_android">"Biometrie verwenden"</string>
|
||||
<string name="screen_app_lock_use_pin_android">"PIN verwenden"</string>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<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_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_settings_change_pin">"Alterar código de PIN"</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.consumeItemsUntilPredicate
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class LockScreenSettingsPresenterTest {
|
||||
@Test
|
||||
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 {
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.showRemovePinOption).isFalse()
|
||||
|
|
@ -37,7 +37,7 @@ class LockScreenSettingsPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - remove pin flow`() = runTest {
|
||||
val presenter = createLockScreenSettingsPresenter(this)
|
||||
val presenter = createLockScreenSettingsPresenter()
|
||||
presenter.test {
|
||||
consumeItemsUntilPredicate { state ->
|
||||
state.showRemovePinOption
|
||||
|
|
@ -71,7 +71,6 @@ class LockScreenSettingsPresenterTest {
|
|||
isDeviceSecured = true,
|
||||
)
|
||||
val presenter = createLockScreenSettingsPresenter(
|
||||
coroutineScope = this,
|
||||
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
||||
)
|
||||
presenter.test {
|
||||
|
|
@ -88,7 +87,6 @@ class LockScreenSettingsPresenterTest {
|
|||
}
|
||||
)
|
||||
val presenter = createLockScreenSettingsPresenter(
|
||||
coroutineScope = this,
|
||||
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
||||
)
|
||||
presenter.test {
|
||||
|
|
@ -110,7 +108,6 @@ class LockScreenSettingsPresenterTest {
|
|||
}
|
||||
)
|
||||
val presenter = createLockScreenSettingsPresenter(
|
||||
coroutineScope = this,
|
||||
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
||||
)
|
||||
presenter.test {
|
||||
|
|
@ -130,7 +127,6 @@ class LockScreenSettingsPresenterTest {
|
|||
)
|
||||
val lockScreenStore = InMemoryLockScreenStore()
|
||||
val presenter = createLockScreenSettingsPresenter(
|
||||
coroutineScope = this,
|
||||
lockScreenStore = lockScreenStore,
|
||||
biometricAuthenticatorManager = fakeBiometricAuthenticatorManager
|
||||
)
|
||||
|
|
@ -148,8 +144,7 @@ class LockScreenSettingsPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun createLockScreenSettingsPresenter(
|
||||
coroutineScope: CoroutineScope,
|
||||
private suspend fun TestScope.createLockScreenSettingsPresenter(
|
||||
lockScreenConfig: LockScreenConfig = aLockScreenConfig(),
|
||||
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
|
||||
lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
|
||||
|
|
@ -160,7 +155,7 @@ class LockScreenSettingsPresenterTest {
|
|||
return LockScreenSettingsPresenter(
|
||||
lockScreenStore = lockScreenStore,
|
||||
pinCodeManager = pinCodeManager,
|
||||
coroutineScope = coroutineScope,
|
||||
coroutineScope = this,
|
||||
lockScreenConfig = lockScreenConfig,
|
||||
biometricAuthenticatorManager = biometricAuthenticatorManager,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import io.element.android.libraries.architecture.AsyncAction
|
|||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ class PinUnlockPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - success verify flow`() = runTest {
|
||||
val presenter = createPinUnlockPresenter(this)
|
||||
val presenter = createPinUnlockPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -71,7 +71,7 @@ class PinUnlockPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - failure verify flow`() = runTest {
|
||||
val presenter = createPinUnlockPresenter(this)
|
||||
val presenter = createPinUnlockPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -100,7 +100,7 @@ class PinUnlockPresenterTest {
|
|||
fun `present - forgot pin flow`() = runTest {
|
||||
val signOutLambda = lambdaRecorder<Boolean, Unit> {}
|
||||
val signOut = FakeLogoutUseCase(signOutLambda)
|
||||
val presenter = createPinUnlockPresenter(this, logoutUseCase = signOut)
|
||||
val presenter = createPinUnlockPresenter(logoutUseCase = signOut)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -135,8 +135,7 @@ class PinUnlockPresenterTest {
|
|||
dataOrNull()?.assertText(text)
|
||||
}
|
||||
|
||||
private suspend fun createPinUnlockPresenter(
|
||||
scope: CoroutineScope,
|
||||
private suspend fun TestScope.createPinUnlockPresenter(
|
||||
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
|
||||
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
|
||||
logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }),
|
||||
|
|
@ -149,7 +148,7 @@ class PinUnlockPresenterTest {
|
|||
pinCodeManager = pinCodeManager,
|
||||
biometricAuthenticatorManager = biometricAuthenticatorManager,
|
||||
logoutUseCase = logoutUseCase,
|
||||
coroutineScope = scope,
|
||||
coroutineScope = this,
|
||||
pinUnlockHelper = PinUnlockHelper(biometricAuthenticatorManager, pinCodeManager),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
|
||||
interface LoginEntryPoint : FeatureEntryPoint {
|
||||
data class Params(
|
||||
val accountProvider: String?,
|
||||
val loginHint: String?,
|
||||
)
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onReportProblem()
|
||||
}
|
||||
|
|
@ -20,6 +25,7 @@ interface LoginEntryPoint : FeatureEntryPoint {
|
|||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
||||
interface NodeBuilder {
|
||||
fun params(params: Params): NodeBuilder
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
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.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.features.login.test)
|
||||
testImplementation(projects.features.enterprise.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.oidc.impl)
|
||||
testImplementation(projects.libraries.oidc.test)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,14 @@ class DefaultLoginEntryPoint @Inject constructor() : LoginEntryPoint {
|
|||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
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 {
|
||||
plugins += callback
|
||||
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.features.login.api.LoginEntryPoint
|
||||
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.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.createaccount.CreateAccountNode
|
||||
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.libraries.architecture.BackstackView
|
||||
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.inputs
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
|
|
@ -63,6 +66,11 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) {
|
||||
data class Params(
|
||||
val accountProvider: String?,
|
||||
val loginHint: String?,
|
||||
) : NodeInputs
|
||||
|
||||
private var activity: Activity? = null
|
||||
private var darkTheme: Boolean = false
|
||||
|
||||
|
|
@ -100,6 +108,9 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
val isAccountCreation: Boolean,
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object ChooseAccountProvider : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object ChangeAccountProvider : NavTarget
|
||||
|
||||
|
|
@ -126,9 +137,13 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override fun onSignIn() {
|
||||
override fun onSignIn(mustChooseAccountProvider: Boolean) {
|
||||
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() {
|
||||
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 -> {
|
||||
createNode<QrCodeLoginFlowNode>(buildContext)
|
||||
|
|
@ -151,16 +199,7 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
)
|
||||
val callback = object : ConfirmAccountProviderNode.Callback {
|
||||
override fun onOidcDetails(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))
|
||||
}
|
||||
navigateToMas(oidcDetails)
|
||||
}
|
||||
|
||||
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
|
||||
override fun View(modifier: Modifier) {
|
||||
activity = requireNotNull(LocalActivity.current)
|
||||
|
|
|
|||
|
|
@ -20,15 +20,16 @@ import javax.inject.Inject
|
|||
class AccountProviderDataSource @Inject constructor(
|
||||
enterpriseService: EnterpriseService,
|
||||
) {
|
||||
private val defaultAccountProvider = (enterpriseService.defaultHomeserverList().firstOrNull() ?: AuthenticationConfig.MATRIX_ORG_URL)
|
||||
.let { url ->
|
||||
AccountProvider(
|
||||
url = url,
|
||||
subtitle = null,
|
||||
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||
)
|
||||
}
|
||||
private val defaultAccountProvider =
|
||||
(enterpriseService.defaultHomeserverList().firstOrNull { it != EnterpriseService.ANY_ACCOUNT_PROVIDER } ?: AuthenticationConfig.MATRIX_ORG_URL)
|
||||
.let { url ->
|
||||
AccountProvider(
|
||||
url = url,
|
||||
subtitle = null,
|
||||
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||
)
|
||||
}
|
||||
|
||||
private val accountProvider: MutableStateFlow<AccountProvider> = MutableStateFlow(
|
||||
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(
|
||||
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(
|
||||
url = url,
|
||||
subtitle = "Matrix.org is an open network for secure, decentralized communication.",
|
||||
isPublic = true,
|
||||
isMatrixOrg = true,
|
||||
isValid = true,
|
||||
subtitle = subtitle,
|
||||
isPublic = isPublic,
|
||||
isMatrixOrg = isMatrixOrg,
|
||||
isValid = isValid,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ fun AccountProviderView(
|
|||
item: AccountProvider,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
selected: Boolean = false,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
|
|
@ -66,7 +67,7 @@ fun AccountProviderView(
|
|||
} else {
|
||||
RoundedIconAtom(
|
||||
size = RoundedIconAtomSize.Medium,
|
||||
imageVector = CompoundIcons.Search(),
|
||||
imageVector = CompoundIcons.Host(),
|
||||
tint = ElementTheme.colors.iconPrimary,
|
||||
)
|
||||
}
|
||||
|
|
@ -88,6 +89,15 @@ fun AccountProviderView(
|
|||
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) {
|
||||
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 io.element.android.appconfig.AuthenticationConfig
|
||||
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.changeserver.ChangeServerState
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
|
@ -25,6 +26,7 @@ class ChangeAccountProviderPresenter @Inject constructor(
|
|||
override fun present(): ChangeAccountProviderState {
|
||||
val staticAccountProviderList = remember {
|
||||
enterpriseService.defaultHomeserverList()
|
||||
.filter { it != EnterpriseService.ANY_ACCOUNT_PROVIDER }
|
||||
.map { it.ensureProtocol() }
|
||||
.ifEmpty { listOf(AuthenticationConfig.MATRIX_ORG_URL) }
|
||||
.map { url ->
|
||||
|
|
@ -38,9 +40,14 @@ class ChangeAccountProviderPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val canSearchForAccountProviders = remember {
|
||||
enterpriseService.canConnectToAnyHomeserver()
|
||||
}
|
||||
|
||||
val changeServerState = changeServerPresenter.present()
|
||||
return ChangeAccountProviderState(
|
||||
accountProviders = staticAccountProviderList,
|
||||
canSearchForAccountProviders = canSearchForAccountProviders,
|
||||
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.
|
||||
data class ChangeAccountProviderState(
|
||||
val accountProviders: List<AccountProvider>,
|
||||
val canSearchForAccountProviders: Boolean,
|
||||
val changeServerState: ChangeServerState,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,20 +8,28 @@
|
|||
package io.element.android.features.login.impl.screens.changeaccountprovider
|
||||
|
||||
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.changeserver.ChangeServerState
|
||||
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
||||
|
||||
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
|
||||
override val values: Sequence<ChangeAccountProviderState>
|
||||
get() = sequenceOf(
|
||||
aChangeAccountProviderState(),
|
||||
aChangeAccountProviderState(canSearchForAccountProviders = false),
|
||||
// Add other state here
|
||||
)
|
||||
}
|
||||
|
||||
fun aChangeAccountProviderState() = ChangeAccountProviderState(
|
||||
accountProviders = listOf(
|
||||
fun aChangeAccountProviderState(
|
||||
accountProviders: List<AccountProvider> = listOf(
|
||||
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 io.element.android.compound.tokens.generated.CompoundIcons
|
||||
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.changeserver.ChangeServerEvents
|
||||
import io.element.android.features.login.impl.changeserver.ChangeServerView
|
||||
|
|
@ -95,13 +95,11 @@ fun ChangeAccountProviderView(
|
|||
)
|
||||
}
|
||||
// Other
|
||||
AccountProviderView(
|
||||
item = AccountProvider(
|
||||
url = "",
|
||||
title = stringResource(id = R.string.screen_change_account_provider_other),
|
||||
),
|
||||
onClick = onOtherProviderClick
|
||||
)
|
||||
if (state.canSearchForAccountProviders) {
|
||||
AccountProviderOtherView(
|
||||
onClick = onOtherProviderClick
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(32.dp))
|
||||
}
|
||||
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
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.login.impl.DefaultLoginUserStory
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.error.ChangeServerError
|
||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.features.login.impl.login.LoginHelper
|
||||
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(
|
||||
@Assisted private val params: Params,
|
||||
private val accountProviderDataSource: AccountProviderDataSource,
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
private val defaultLoginUserStory: DefaultLoginUserStory,
|
||||
private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever,
|
||||
private val loginHelper: LoginHelper,
|
||||
) : Presenter<ConfirmAccountProviderState> {
|
||||
data class Params(
|
||||
val isAccountCreation: Boolean,
|
||||
|
|
@ -55,91 +37,27 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
|||
val accountProvider by accountProviderDataSource.flow.collectAsState()
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
val loginFlowAction: MutableState<AsyncData<LoginFlow>> = remember {
|
||||
mutableStateOf(AsyncData.Uninitialized)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
oidcActionFlow.collect { oidcAction ->
|
||||
if (oidcAction != null) {
|
||||
onOidcAction(oidcAction, loginFlowAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
val loginMode by loginHelper.collectLoginMode()
|
||||
|
||||
fun handleEvents(event: ConfirmAccountProviderEvents) {
|
||||
when (event) {
|
||||
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(
|
||||
accountProvider = accountProvider,
|
||||
isAccountCreation = params.isAccountCreation,
|
||||
loginFlow = loginFlowAction.value,
|
||||
loginMode = loginMode,
|
||||
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
|
||||
|
||||
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.matrix.api.auth.OidcDetails
|
||||
|
||||
// Do not use default value, so no member get forgotten in the presenters.
|
||||
data class ConfirmAccountProviderState(
|
||||
val accountProvider: AccountProvider,
|
||||
val isAccountCreation: Boolean,
|
||||
val loginFlow: AsyncData<LoginFlow>,
|
||||
val loginMode: AsyncData<LoginMode>,
|
||||
val eventSink: (ConfirmAccountProviderEvents) -> Unit
|
||||
) {
|
||||
val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginFlow is AsyncData.Uninitialized || loginFlow is AsyncData.Loading)
|
||||
}
|
||||
|
||||
sealed interface LoginFlow {
|
||||
data object PasswordLogin : LoginFlow
|
||||
data class OidcFlow(val oidcDetails: OidcDetails) : LoginFlow
|
||||
data class AccountCreationFlow(val url: String) : LoginFlow
|
||||
val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
||||
import io.element.android.features.login.impl.login.LoginMode
|
||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
|
|
@ -22,7 +23,7 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<Confir
|
|||
),
|
||||
aConfirmAccountProviderState(
|
||||
isAccountCreation = true,
|
||||
loginFlow = AsyncData.Failure(AccountCreationNotSupported())
|
||||
loginMode = AsyncData.Failure(AccountCreationNotSupported())
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -30,11 +31,11 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<Confir
|
|||
private fun aConfirmAccountProviderState(
|
||||
accountProvider: AccountProvider = anAccountProvider(),
|
||||
isAccountCreation: Boolean = false,
|
||||
loginFlow: AsyncData<LoginFlow> = AsyncData.Uninitialized,
|
||||
loginMode: AsyncData<LoginMode> = AsyncData.Uninitialized,
|
||||
eventSink: (ConfirmAccountProviderEvents) -> Unit = {},
|
||||
) = ConfirmAccountProviderState(
|
||||
accountProvider = accountProvider,
|
||||
isAccountCreation = isAccountCreation,
|
||||
loginFlow = loginFlow,
|
||||
loginMode = loginMode,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,15 +19,12 @@ 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.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.features.login.impl.login.LoginModeView
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
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.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
|
|
@ -47,9 +44,9 @@ fun ConfirmAccountProviderView(
|
|||
onChange: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isLoading by remember(state.loginFlow) {
|
||||
val isLoading by remember(state.loginMode) {
|
||||
derivedStateOf {
|
||||
state.loginFlow is AsyncData.Loading
|
||||
state.loginMode is AsyncData.Loading
|
||||
}
|
||||
}
|
||||
val eventSink = state.eventSink
|
||||
|
|
@ -99,48 +96,16 @@ fun ConfirmAccountProviderView(
|
|||
}
|
||||
}
|
||||
) {
|
||||
when (state.loginFlow) {
|
||||
is AsyncData.Failure -> {
|
||||
when (val error = state.loginFlow.error) {
|
||||
is ChangeServerError.Error -> {
|
||||
ErrorDialog(
|
||||
content = error.message(),
|
||||
onSubmit = {
|
||||
eventSink.invoke(ConfirmAccountProviderEvents.ClearError)
|
||||
}
|
||||
)
|
||||
}
|
||||
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
|
||||
}
|
||||
LoginModeView(
|
||||
loginMode = state.loginMode,
|
||||
onClearError = {
|
||||
eventSink(ConfirmAccountProviderEvents.ClearError)
|
||||
},
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onOidcDetails = onOidcDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
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.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
|
||||
|
|
@ -16,26 +17,44 @@ import com.bumble.appyx.core.plugin.plugins
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.login.impl.util.openLearnMorePage
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
class OnBoardingNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: OnBoardingPresenter,
|
||||
presenterFactory: OnBoardingPresenter.Factory,
|
||||
) : Node(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
interface Callback : Plugin {
|
||||
fun onSignUp()
|
||||
fun onSignIn()
|
||||
fun onSignIn(mustChooseAccountProvider: Boolean)
|
||||
fun onSignInWithQrCode()
|
||||
fun onReportProblem()
|
||||
fun onLoginPasswordNeeded()
|
||||
fun onOidcDetails(oidcDetails: OidcDetails)
|
||||
fun onCreateAccountContinue(url: String)
|
||||
}
|
||||
|
||||
private fun onSignIn() {
|
||||
plugins<Callback>().forEach { it.onSignIn() }
|
||||
data class Params(
|
||||
val accountProvider: String?,
|
||||
val loginHint: String?,
|
||||
) : NodeInputs
|
||||
|
||||
private val params = inputs<Params>()
|
||||
|
||||
private val presenter = presenterFactory.create(
|
||||
params = params,
|
||||
)
|
||||
|
||||
private fun onSignIn(mustChooseAccountProvider: Boolean) {
|
||||
plugins<Callback>().forEach { it.onSignIn(mustChooseAccountProvider) }
|
||||
}
|
||||
|
||||
private fun onSignUp() {
|
||||
|
|
@ -50,9 +69,22 @@ class OnBoardingNode @AssistedInject constructor(
|
|||
plugins<Callback>().forEach { it.onReportProblem() }
|
||||
}
|
||||
|
||||
private fun onOidcDetails(data: OidcDetails) {
|
||||
plugins<Callback>().forEach { it.onOidcDetails(data) }
|
||||
}
|
||||
|
||||
private fun onLoginPasswordNeeded() {
|
||||
plugins<Callback>().forEach { it.onLoginPasswordNeeded() }
|
||||
}
|
||||
|
||||
private fun onCreateAccountContinue(url: String) {
|
||||
plugins<Callback>().forEach { it.onCreateAccountContinue(url) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
val context = LocalContext.current
|
||||
OnBoardingView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
|
|
@ -60,6 +92,10 @@ class OnBoardingNode @AssistedInject constructor(
|
|||
onCreateAccount = ::onSignUp,
|
||||
onSignInWithQrCode = ::onSignInWithQrCode,
|
||||
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